offer: fix path validation to only require non-empty paths when issuer_id is missing
What changed, and why it matters
This commit fixes a validation bug in how Lightning offers (BOLT12) are checked. Previously, an offer that included an issuer ID but had an empty list of payment paths was incorrectly rejected. The fix allows empty paths when an issuer ID is present, because the issuer ID itself can be used for signing. The bug was a logic error, not a clear-cut security vulnerability, but it could cause valid offers to be rejected or force users to add unnecessary paths, which may have privacy or usability implications.
Review whether the stricter prior behavior caused any operational issues (e.g., rejected valid offers or forced inclusion of unnecessary blinded paths). No immediate patch deployment is required solely for security, but users relying on BOLT12 offers with issuer IDs and no paths should upgrade to avoid spurious validation failures.
Security signals we found
Logic error in cryptographic/identity validation
Change relaxes a previously over-strict validation rule
Test modified to preserve coverage of the error case
Evidence from the diff
In rust-lightning’s BOLT12 offer parsing, the match arm (_, Some(paths)) if paths.is_empty() => Err(...) rejected any offer with an empty paths vector regardless of whether an issuer_id was present. The corrected code changes the wildcard _ to None, so the error is only raised when issuer_id is missing. A test was updated to explicitly clear issuer_signing_pubkey so that the empty-paths case still exercises the expected error path. This aligns validation with the intended semantics: paths are needed to extract a blinded node ID for signing only when no explicit issuer signing key is available.
Changed components
lightning/src/offers/offer.rsBOLT12 offer parsing and validationInspect captured patch +4 / −1
diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs
index 5b61309..7fd2c4e 100644
--- a/lightning/src/offers/offer.rs
+++ b/lightning/src/offers/offer.rs
@@ -1287,7 +1287,9 @@ impl TryFrom<FullOfferTlvStream> for OfferContents {
let (issuer_signing_pubkey, paths) = match (issuer_id, paths) {
(None, None) => return Err(Bolt12SemanticError::MissingIssuerSigningPubkey),
- (_, Some(paths)) if paths.is_empty() => return Err(Bolt12SemanticError::MissingPaths),
+ (None, Some(paths)) if paths.is_empty() => {
+ return Err(Bolt12SemanticError::MissingPaths)
+ },
(issuer_id, paths) => (issuer_id, paths),
};
@@ -2001,6 +2003,7 @@ mod tests {
}
let mut builder = OfferBuilder::new(pubkey(42));
+ builder.offer.issuer_signing_pubkey = None;
builder.offer.paths = Some(vec![]);
let offer = builder.build().unwrap();
Why this scored 51/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.