bitcoin-kmp icon indicating copy to clipboard operation
bitcoin-kmp copied to clipboard

Eclair/Lightning-kmp incorrectly parses BOLT12 offer with invalid Bech32 encoding

Open erickcestari opened this issue 5 months ago • 3 comments

Eclair and lightning-kmp is successfully parsing and extracting data from a BOLT12 offer string that contains invalid Bech32 encoding, while rust-lightning rejects it with a parsing error. bitcoin-kmp's Bech32 validation is insufficient during offer deserialization.

Offer deserialization failed for lno1zcss88lll8vlpqqqqqqclllllllvwvcqpq8qllllgqrqqgqq8s(q8888
Module: rust-lightning
Result: Bech32(Parse(Char(InvalidChar('('))))
Module: Eclair
Result: CHAINS=6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000;METADATA=;DESCRIPTION=;FEATURES=;ABSOLUTE_EXPIRY=;ISSUER=;QUANTITY=;ISSUER_ID=039ffff9d9f0800000018ffffffffec73300080e0fffff40060020003c3e039ce7
Offer deserialization failed for lno1zcss88lll8vlpqqqqqqclllllllvwvcqpq8qllllgqrqqgqq8s(q8888
Module: rust-lightning
Result: Bech32(Parse(Char(InvalidChar('('))))
Module: LightningKmp
Result: CHAINS=6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000;METADATA=;DESCRIPTION=;FEATURES=;ABSOLUTE_EXPIRY=null;ISSUER=null;QUANTITY=null;ISSUER_ID=039ffff9d9f0800000018ffffffffec73300080e0fffff40060020003c3e039ce7
public fun decode(bech32: String, noChecksum: Boolean = false): Triple<String, Array<Int5>, Encoding> {
        require(bech32.lowercase() == bech32 || bech32.uppercase() == bech32) { "mixed case strings are not valid bech32" }
        bech32.forEach { require(it.code in 33..126) { "invalid character " } } <-- `)` is ASCII (40)
        val input = bech32.lowercase()
        val pos = input.lastIndexOf('1')
        val hrp = input.take(pos)
        require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" }
        val data = Array<Int5>(input.length - pos - 1) { 0 }
        for (i in 0..data.lastIndex) data[i] = map[input[pos + 1 + i].code] <-- `)` will return -1
        return if (noChecksum) {
            Triple(hrp, data, Encoding.Beck32WithoutChecksum)
        } else {
            val encoding = when (polymod(expand(hrp), data)) {
                Encoding.Bech32.constant -> Encoding.Bech32
                Encoding.Bech32m.constant -> Encoding.Bech32m
                else -> throw IllegalArgumentException("invalid checksum for $bech32")
            }
            Triple(hrp, data.dropLast(6).toTypedArray(), encoding)
        }
    }

erickcestari avatar Aug 01 '25 12:08 erickcestari

Thanks! There is indeed a bug as we do not check for invalid characters properly, but decode() still fails later when it tries to verify checksums, with an ugly Index 5 out of bounds for length 5 error. Are you sure that you're properly checking the result ? (bitcoin-kmp would throw an exception, which lighning-kmp wraps inside a Failure object).

sstone avatar Aug 18 '25 16:08 sstone

Thanks! There is indeed a bug as we do not check for invalid characters properly, but decode() still fails later when it tries to verify checksums, with an ugly Index 5 out of bounds for length 5 error. Are you sure that you're properly checking the result ? (bitcoin-kmp would throw an exception, which lighning-kmp wraps inside a Failure object).

I didn't get the error because I'm using it to decode offers which don't have checksums.

erickcestari avatar Aug 18 '25 17:08 erickcestari

You're right, Offer decoding bypasses checksum verification. I've checked that this change will fix offer decoding in lightning-kmp (using Offer.decode()). Thanks!

sstone avatar Aug 19 '25 07:08 sstone