BOLT 12: Validate bech32 padding per BIP-173
What changed, and why it matters
This change fixes how LDK parses BOLT 12 offers encoded in bech32. Previously, LDK accepted offers whose bech32 padding was invalid according to the BIP-173 standard. The fix now rejects such malformed offers, matching the protocol specification and preventing potential parsing disagreements between Lightning implementations.
Review whether other bech32-decoding paths in the codebase (e.g., invoice parsing, node announcements, other BOLT 12 message types) similarly lack padding validation and should be hardened. Ensure the new InvalidPadding error is handled appropriately by downstream consumers and bindings.
Security signals we found
Input validation hardening: enforces BIP-173 bech32 padding rules during BOLT 12 offer parsing
New error variant surfaces previously swallowed malformed input
Adds specification test vectors for invalid padding rejection
Cross-implementation differential fuzzing identified the discrepancy
Evidence from the diff
The commit adds BIP-173-compliant bech32 padding validation to BOLT 12 offer parsing by calling validate_segwit_padding() from the bech32 crate. It introduces a new Bolt12ParseError::InvalidPadding variant and adds BOLT 12 test vectors demonstrating rejection of padding that exceeds the 4-bit limit or is non-zero. The issue was found through differential fuzzing across Lightning implementations.
Changed components
lightning/src/offers/parse.rslightning/src/offers/offer.rsBolt12ParseError enumBOLT 12 offer parsing pathInspect captured patch +35 / −2
diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs
index 7ad3c28..5592c50 100644
--- a/lightning/src/offers/offer.rs
+++ b/lightning/src/offers/offer.rs
@@ -2528,5 +2528,12 @@ mod bolt12_tests {
"lno1pgx9getnwss8vetrw3hhyucsespjgef743p5fzqq9nqxh0ah7y87rzv3ud0eleps9kl2d5348hq2k8qzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgqpqqqqqqqqqqqqqqqqqqqqqqqqqqqzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqqzq3zyg3zyg3zygszqqqqyqqqqsqqvpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsq".parse::<Offer>(),
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::<Offer>(),
+ Err(Bolt12ParseError::InvalidPadding(_))
+ ));
}
}
diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs
index 99dd1bb..df71e86 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::<Vec<u8>>();
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<CheckedHrpstringError> for Bolt12ParseError {
}
}
+impl From<PaddingError> for Bolt12ParseError {
+ fn from(error: PaddingError) -> Self {
+ Self::InvalidPadding(error)
+ }
+}
+
impl From<DecodeError> 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::<Offer>() {
+ Ok(_) => panic!("Valid offer: {}", encoded_offer),
+ Err(e) => assert_eq!(e, Bolt12ParseError::InvalidPadding(PaddingError::TooMuch)),
+ }
+ }
}
Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.