From cea1c72cea2bc314198743772e887b797d11c093 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Mon, 26 Jan 2026 19:39:54 +0100 Subject: [PATCH] BOLT 12: Validate bech32 padding per BIP-173 Add validation for bech32 padding in BOLT 12 offer parsing per BIP-173 which states: "Any incomplete group at the end MUST be 4 bits or less, MUST be all zeroes, and is discarded." This adds a test vector from the BOLT specification that ensures offers with invalid padding (exceeding the 4-bit limit) are properly rejected. Previously, LDK would accept offers with invalid bech32 padding. This was identified through differential fuzzing across Lightning implementations (see lightning/bolts#1312). The fix calls `validate_segwit_padding()` from the bech32 crate during offer parsing, and introduces a new `InvalidPadding` variant to `Bolt12ParseError` to surface these errors. Signed-off-by: Vincenzo Palazzo --- lightning/src/offers/offer.rs | 7 +++++++ lightning/src/offers/parse.rs | 30 ++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 7ad3c282c77..5592c50a264 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -2528,5 +2528,12 @@ mod bolt12_tests { "lno1pgx9getnwss8vetrw3hhyucsespjgef743p5fzqq9nqxh0ah7y87rzv3ud0eleps9kl2d5348hq2k8qzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgqpqqqqqqqqqqqqqqqqqqqqqqqqqqqzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqqzq3zyg3zyg3zygszqqqqyqqqqsqqvpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsq".parse::(), Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)), ); + + // Bech32 padding exceeds 4-bit limit (BOLT 12 test vector) + // See: https://github.com/lightning/bolts/pull/1312 + assert!(matches!( + "lno1zcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pkseseq".parse::(), + Err(Bolt12ParseError::InvalidPadding(_)) + )); } } diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 99dd1bb938d..df71e860d2d 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -12,7 +12,7 @@ use crate::io; use crate::ln::msgs::DecodeError; use crate::util::ser::CursorReadable; -use bech32::primitives::decode::CheckedHrpstringError; +use bech32::primitives::decode::{CheckedHrpstringError, PaddingError}; use bitcoin::secp256k1; #[allow(unused_imports)] @@ -76,6 +76,10 @@ mod sealed { return Err(Bolt12ParseError::InvalidBech32Hrp); } + // Validate that bech32 padding is valid per BIP-173: + // "Any incomplete group at the end MUST be 4 bits or less, MUST be all zeroes" + parsed.validate_segwit_padding()?; + let data = parsed.byte_iter().collect::>(); Self::try_from(data) } @@ -146,6 +150,11 @@ pub enum Bolt12ParseError { /// This is not exported to bindings users as the details don't matter much CheckedHrpstringError, ), + /// The bech32 data has invalid padding per BIP-173 (more than 4 bits or non-zero padding). + InvalidPadding( + /// This is not exported to bindings users as the details don't matter much + PaddingError, + ), /// The bech32 decoded string could not be decoded as the expected message type. Decode(DecodeError), /// The parsed message has invalid semantics. @@ -232,6 +241,12 @@ impl From for Bolt12ParseError { } } +impl From for Bolt12ParseError { + fn from(error: PaddingError) -> Self { + Self::InvalidPadding(error) + } +} + impl From for Bolt12ParseError { fn from(error: DecodeError) -> Self { Self::Decode(error) @@ -326,7 +341,7 @@ mod bolt12_tests { #[cfg(test)] mod tests { - use super::Bolt12ParseError; + use super::{Bolt12ParseError, PaddingError}; use crate::ln::msgs::DecodeError; use crate::offers::offer::Offer; use bech32::primitives::decode::{CharError, CheckedHrpstringError, UncheckedHrpstringError}; @@ -371,4 +386,15 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), } } + + #[test] + fn fails_parsing_bech32_encoded_offer_with_invalid_padding() { + // BOLT 12 test vector for invalid bech32 padding + // See: https://github.com/lightning/bolts/pull/1312 + let encoded_offer = "lno1zcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pkseseq"; + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidPadding(PaddingError::TooMuch)), + } + } }