Skip to content

Commit

Permalink
Ignore invalid hash length
Browse files Browse the repository at this point in the history
Correctly implement skipping over hashed tags with invalid length.

See lightning/bolts#736.
  • Loading branch information
t-bast committed Feb 4, 2020
1 parent 3b580de commit d982b52
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,12 @@ object PaymentRequest {

sealed trait TaggedField

sealed trait UnknownTaggedField extends TaggedField
sealed trait UnknownTaggedField extends TaggedField {
def data: BitVector
}

// @formatter:off
case class UnknownTag0(data: BitVector) extends UnknownTaggedField
case class UnknownTag1(data: BitVector) extends UnknownTaggedField
case class UnknownTag2(data: BitVector) extends UnknownTaggedField
case class UnknownTag4(data: BitVector) extends UnknownTaggedField
case class UnknownTag7(data: BitVector) extends UnknownTaggedField
Expand All @@ -187,19 +188,27 @@ object PaymentRequest {
case class UnknownTag31(data: BitVector) extends UnknownTaggedField
// @formatter:on

// @formatter:off
sealed trait PaymentHashTag extends TaggedField
/**
* Payment Hash
*
* @param hash payment hash
*/
case class PaymentHash(hash: ByteVector32) extends TaggedField
case class PaymentHash(hash: ByteVector32) extends PaymentHashTag
case class InvalidPaymentHash(data: BitVector) extends PaymentHashTag with UnknownTaggedField
// @formatter:on

// @formatter:off
sealed trait PaymentSecretTag extends TaggedField
/**
* Payment secret. This is currently random bytes used to protect against probing from the next-to-last node.
*
* @param secret payment secret
*/
case class PaymentSecret(secret: ByteVector32) extends TaggedField
case class PaymentSecret(secret: ByteVector32) extends PaymentSecretTag
case class InvalidPaymentSecret(data: BitVector) extends PaymentSecretTag with UnknownTaggedField
// @formatter:on

/**
* Description
Expand All @@ -208,13 +217,17 @@ object PaymentRequest {
*/
case class Description(description: String) extends TaggedField

// @formatter:off
sealed trait DescriptionHashTag extends TaggedField
/**
* Hash
*
* @param hash hash that will be included in the payment request, and can be checked against the hash of a
* long description, an invoice, ...
*/
case class DescriptionHash(hash: ByteVector32) extends TaggedField
case class DescriptionHash(hash: ByteVector32) extends DescriptionHashTag
case class InvalidDescriptionHash(data: BitVector) extends DescriptionHashTag with UnknownTaggedField
// @formatter:on

/**
* Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
Expand Down Expand Up @@ -377,41 +390,58 @@ 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 <: TaggedField](valueCodec: Codec[A]): Codec[A] = paddedVarAlignedBits(dataLengthCodec, valueCodec, multipleForPadding = 5)

def dataCodecLengthDiscriminated[A <: TaggedField](length: Long, valueCodec: Codec[A]): Codec[A] = paddedVarAlignedBits(provide(length), valueCodec, multipleForPadding = 5)

val paymentHashCodec: Codec[PaymentHashTag] = discriminatorWithDefault(
discriminated.by(dataLengthCodec)
.typecase(260, dataCodecLengthDiscriminated(260, bytes32.as[PaymentHash])),
bits.as[InvalidPaymentHash].upcast[PaymentHashTag])

val paymentSecretCodec: Codec[PaymentSecretTag] = discriminatorWithDefault(
discriminated.by(dataLengthCodec)
.typecase(260, dataCodecLengthDiscriminated(260, bytes32.as[PaymentSecret])),
bits.as[InvalidPaymentSecret].upcast[PaymentSecretTag])

val descriptionHashCodec: Codec[DescriptionHashTag] = discriminatorWithDefault(
discriminated.by(dataLengthCodec)
.typecase(260, dataCodecLengthDiscriminated(260, bytes32.as[DescriptionHash])),
bits.as[InvalidDescriptionHash].upcast[DescriptionHashTag])

val taggedFieldCodec: Codec[TaggedField] = discriminated[TaggedField].by(ubyte(5))
.typecase(0, dataCodec(bits).as[UnknownTag0])
.typecase(1, dataCodec(bytes32).as[PaymentHash])
.typecase(2, dataCodec(bits).as[UnknownTag2])
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo])
.typecase(4, dataCodec(bits).as[UnknownTag4])
.typecase(5, dataCodec(bits).as[Features])
.typecase(6, dataCodec(bits).as[Expiry])
.typecase(7, dataCodec(bits).as[UnknownTag7])
.typecase(8, dataCodec(bits).as[UnknownTag8])
.typecase(9, dataCodec(ubyte(5) :: alignedBytesCodec(bytes)).as[FallbackAddress])
.typecase(10, dataCodec(bits).as[UnknownTag10])
.typecase(11, dataCodec(bits).as[UnknownTag11])
.typecase(12, dataCodec(bits).as[UnknownTag12])
.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])
.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])
.typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
.typecase(25, dataCodec(bits).as[UnknownTag25])
.typecase(26, dataCodec(bits).as[UnknownTag26])
.typecase(27, dataCodec(bits).as[UnknownTag27])
.typecase(28, dataCodec(bits).as[UnknownTag28])
.typecase(29, dataCodec(bits).as[UnknownTag29])
.typecase(30, dataCodec(bits).as[UnknownTag30])
.typecase(31, dataCodec(bits).as[UnknownTag31])
.typecase(0, dataCodec(bits.as[UnknownTag0]))
.typecase(1, paymentHashCodec)
.typecase(2, dataCodec(bits.as[UnknownTag2]))
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec).as[RoutingInfo]))
.typecase(4, dataCodec(bits.as[UnknownTag4]))
.typecase(5, dataCodec(bits.as[Features]))
.typecase(6, dataCodec(bits.as[Expiry]))
.typecase(7, dataCodec(bits.as[UnknownTag7]))
.typecase(8, dataCodec(bits.as[UnknownTag8]))
.typecase(9, dataCodec((ubyte(5) :: alignedBytesCodec(bytes)).as[FallbackAddress]))
.typecase(10, dataCodec(bits.as[UnknownTag10]))
.typecase(11, dataCodec(bits.as[UnknownTag11]))
.typecase(12, dataCodec(bits.as[UnknownTag12]))
.typecase(13, dataCodec(alignedBytesCodec(utf8).as[Description]))
.typecase(14, dataCodec(bits.as[UnknownTag14]))
.typecase(15, dataCodec(bits.as[UnknownTag15]))
.typecase(16, paymentSecretCodec)
.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, descriptionHashCodec)
.typecase(24, dataCodec(bits.as[MinFinalCltvExpiry]))
.typecase(25, dataCodec(bits.as[UnknownTag25]))
.typecase(26, dataCodec(bits.as[UnknownTag26]))
.typecase(27, dataCodec(bits.as[UnknownTag27]))
.typecase(28, dataCodec(bits.as[UnknownTag28]))
.typecase(29, dataCodec(bits.as[UnknownTag29]))
.typecase(30, dataCodec(bits.as[UnknownTag30]))
.typecase(31, dataCodec(bits.as[UnknownTag31]))

def fixedSizeTrailingCodec[A](codec: Codec[A], size: Int): Codec[A] = Codec[A](
(data: A) => codec.encode(data),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,22 +218,30 @@ 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 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.fallbackAddress().isEmpty)
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)
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") {
Expand Down Expand Up @@ -272,10 +280,30 @@ class PaymentRequestSpec extends FunSuite {
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

val codec = PaymentRequest.Codecs.dataCodec(scodec.codecs.bits).as[PaymentRequest.Expiry]
val codec = PaymentRequest.Codecs.dataCodec(scodec.codecs.bits.as[PaymentRequest.Expiry])
val field = PaymentRequest.Expiry(number)

assert(field.toLong == number)
Expand Down Expand Up @@ -310,6 +338,26 @@ 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.
val inputs = Seq(
"ppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"pp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"hpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"hp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"spnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"sp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"np5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"npkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
)

for (input <- inputs) {
val data = string2Bits(input)
val decoded = Codecs.taggedFieldCodec.decode(data).require.value
assert(decoded.isInstanceOf[UnknownTaggedField], input)
}
}

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

Expand Down

0 comments on commit d982b52

Please sign in to comment.