Skip to content

Commit

Permalink
Payment request: ignore fields with invalid length (#1308)
Browse files Browse the repository at this point in the history
* Ignore fields with invalid length

As per the spec:
> A reader:
>   * MUST skip over unknown fields, OR an f field with unknown version, OR p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively.

* Add more Bolt 11 tests

See lightning/bolts#699
and lightning/bolts#736

Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com>
  • Loading branch information
pm47 and t-bast authored Feb 10, 2020
1 parent 13f5b03 commit 8ff7f4b
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteV
import fr.acinq.eclair.Features.{PaymentSecret => PaymentSecretF, _}
import fr.acinq.eclair.payment.PaymentRequest._
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32}
import scodec.Codec
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
import scodec.codecs.{list, ubyte}
import scodec.{Codec, Err}

import scala.compat.Platform
import scala.concurrent.duration._
Expand Down Expand Up @@ -160,9 +160,11 @@ object PaymentRequest {

sealed trait UnknownTaggedField extends TaggedField

sealed trait InvalidTaggedField extends TaggedField

// @formatter:off
case class UnknownTag0(data: BitVector) extends UnknownTaggedField
case class UnknownTag1(data: BitVector) extends UnknownTaggedField
case class InvalidTag1(data: BitVector) extends InvalidTaggedField
case class UnknownTag2(data: BitVector) extends UnknownTaggedField
case class UnknownTag4(data: BitVector) extends UnknownTaggedField
case class UnknownTag7(data: BitVector) extends UnknownTaggedField
Expand All @@ -172,12 +174,14 @@ object PaymentRequest {
case class UnknownTag12(data: BitVector) extends UnknownTaggedField
case class UnknownTag14(data: BitVector) extends UnknownTaggedField
case class UnknownTag15(data: BitVector) extends UnknownTaggedField
case class InvalidTag16(data: BitVector) extends InvalidTaggedField
case class UnknownTag17(data: BitVector) extends UnknownTaggedField
case class UnknownTag18(data: BitVector) extends UnknownTaggedField
case class UnknownTag19(data: BitVector) extends UnknownTaggedField
case class UnknownTag20(data: BitVector) extends UnknownTaggedField
case class UnknownTag21(data: BitVector) extends UnknownTaggedField
case class UnknownTag22(data: BitVector) extends UnknownTaggedField
case class InvalidTag23(data: BitVector) extends InvalidTaggedField
case class UnknownTag25(data: BitVector) extends UnknownTaggedField
case class UnknownTag26(data: BitVector) extends UnknownTaggedField
case class UnknownTag27(data: BitVector) extends UnknownTaggedField
Expand Down Expand Up @@ -377,11 +381,17 @@ object PaymentRequest {

val dataLengthCodec: Codec[Long] = uint(10).xmap(_ * 5, s => (s / 5 + (if (s % 5 == 0) 0 else 1)).toInt)

def dataCodec[A](valueCodec: Codec[A]): Codec[A] = paddedVarAlignedBits(dataLengthCodec, valueCodec, multipleForPadding = 5)
def dataCodec[A](valueCodec: Codec[A], expectedLength: Option[Long] = None): Codec[A] = paddedVarAlignedBits(
dataLengthCodec.narrow(l => if (expectedLength.getOrElse(l) == l) Attempt.successful(l) else Attempt.failure(Err(s"invalid length $l")), l => l),
valueCodec,
multipleForPadding = 5)

val taggedFieldCodec: Codec[TaggedField] = discriminated[TaggedField].by(ubyte(5))
.typecase(0, dataCodec(bits).as[UnknownTag0])
.typecase(1, dataCodec(bytes32).as[PaymentHash])
.\(1) {
case a: PaymentHash => a: TaggedField
case a: InvalidTag1 => a: TaggedField
}(choice(dataCodec(bytes32, expectedLength = Some(52 * 5)).as[PaymentHash].upcast[TaggedField], dataCodec(bits).as[InvalidTag1].upcast[TaggedField]))
.typecase(2, dataCodec(bits).as[UnknownTag2])
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo])
.typecase(4, dataCodec(bits).as[UnknownTag4])
Expand All @@ -396,14 +406,20 @@ object PaymentRequest {
.typecase(13, dataCodec(alignedBytesCodec(utf8)).as[Description])
.typecase(14, dataCodec(bits).as[UnknownTag14])
.typecase(15, dataCodec(bits).as[UnknownTag15])
.typecase(16, dataCodec(bytes32).as[PaymentSecret])
.\(16) {
case a: PaymentSecret => a: TaggedField
case a: InvalidTag16 => a: TaggedField
}(choice(dataCodec(bytes32, expectedLength = Some(52 * 5)).as[PaymentSecret].upcast[TaggedField], dataCodec(bits).as[InvalidTag16].upcast[TaggedField]))
.typecase(17, dataCodec(bits).as[UnknownTag17])
.typecase(18, dataCodec(bits).as[UnknownTag18])
.typecase(19, dataCodec(bits).as[UnknownTag19])
.typecase(20, dataCodec(bits).as[UnknownTag20])
.typecase(21, dataCodec(bits).as[UnknownTag21])
.typecase(22, dataCodec(bits).as[UnknownTag22])
.typecase(23, dataCodec(bytes32).as[DescriptionHash])
.\(23) {
case a: DescriptionHash => a: TaggedField
case a: InvalidTag23 => a: TaggedField
}(choice(dataCodec(bytes32, expectedLength = Some(52 * 5)).as[DescriptionHash].upcast[TaggedField], dataCodec(bits).as[InvalidTag23].upcast[TaggedField]))
.typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
.typecase(25, dataCodec(bits).as[UnknownTag25])
.typecase(26, dataCodec(bits).as[UnknownTag26])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,34 @@ class PaymentRequestSpec extends FunSuite {
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu"
val refs = Seq(
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu",
// All upper-case
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu".toUpperCase,
// With ignored fields
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2jxxfsnucm4jf4zwtznpaxphce606fvhvje5x7d4gw7n73994hgs7nteqvenq8a4ml8aqtchv5d9pf7l558889hp4yyrqv6a7zpq9fgpskqhza"
)

for (ref <- refs) {
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(2500000000L msat))
assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111")))
assert(pr.timestamp === 1496314658L)
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(!pr.features.allowMultiPart)
assert(!pr.features.requirePaymentSecret)
assert(!pr.features.allowTrampoline)
assert(pr.features.supported)
assert(PaymentRequest.write(pr.sign(priv)) === ref.toLowerCase)
}
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk"
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(2500000000L msat))
Expand All @@ -228,33 +255,51 @@ class PaymentRequestSpec extends FunSuite {
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.fallbackAddress().isEmpty)
assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(!pr.features.allowMultiPart)
assert(!pr.features.requirePaymentSecret)
assert(!pr.features.allowTrampoline)
assert(pr.features.supported)
assert(!pr.features.supported)
assert(PaymentRequest.write(pr.sign(priv)) === ref)
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk"
test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") {
val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9qn07ytgrxxzad9hc4xt3mawjjt8znfv8xzscs7007v9gh9j569lencxa8xeujzkxs0uamak9aln6ez02uunw6rd2ht2sqe4hz8thcdagpleym0j"
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(2500000000L msat))
assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111")))
assert(pr.timestamp === 1496314658L)
assert(pr.amount === Some(967878534 msat))
assert(pr.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f")
assert(pr.timestamp === 1572468703L)
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items"))
assert(pr.fallbackAddress().isEmpty)
assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(!pr.features.allowMultiPart)
assert(!pr.features.requirePaymentSecret)
assert(!pr.features.allowTrampoline)
assert(!pr.features.supported)
assert(pr.expiry === Some(604800L))
assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(10)))
assert(pr.routingInfo === Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40)))))
assert(pr.features.supported)
assert(PaymentRequest.write(pr.sign(priv)) === ref)
}

test("reject invalid invoices") {
val refs = Seq(
// Bech32 checksum is invalid.
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt",
// Malformed bech32 string (no 1).
"pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
// Malformed bech32 string (mixed case).
"LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
// Signature is not recoverable.
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspk28uwq",
// String is too short.
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh",
// Invalid multiplier.
"lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg"
)
for (ref <- refs) {
assertThrows[Exception](PaymentRequest.read(ref))
}
}

test("correctly serialize/deserialize variable-length tagged fields") {
val number = 123456

Expand Down Expand Up @@ -293,6 +338,30 @@ class PaymentRequestSpec extends FunSuite {
val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u }
}

test("ignore hash tags with invalid length") {
// Bolt11: A reader: MUST skip over p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively.
def bits(i: Int) = BitVector.fill(i * 5)(high = false)

val inputs = Map(
"ppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag1(bits(51)),
"pp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag1(bits(53)),
"hpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag23(bits(51)),
"hp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag23(bits(53)),
"spnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag16(bits(51)),
"sp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag16(bits(53)),
"np5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> UnknownTag19(bits(52)),
"npkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> UnknownTag19(bits(54))
)

for ((input, value) <- inputs) {
val data = string2Bits(input)
val decoded = Codecs.taggedFieldCodec.decode(data).require.value
assert(decoded === value)
val encoded = Codecs.taggedFieldCodec.encode(value).require
assert(encoded === data)
}
}

test("accept uppercase payment request") {
val input = "lntb1500n1pwxx94fpp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23sjzv0d8794te26xhexuc26eswf9sjpv4t8sma2d9y8dmpgf0qseg8259my8tcs6zte7ex0tz4exm5pjezuxrq9u0vjewa02qhedk9x4gppweupu"

Expand Down

0 comments on commit 8ff7f4b

Please sign in to comment.