Skip to content

Commit e8341e4

Browse files
committed
Bind invoice hashes to encoded bytes
Previously, deserialized invoices recomputed their signature hash by re-encoding the parsed invoice. Non-canonical amount digits could then be dropped, letting distinct encodings share a hash. Hash deserialized invoices from the HRP and unsigned data bytes accepted by the parser so the cached hash remains bound to the encoded invoice. Reported by Project Loupe. Co-Authored-By: HAL 9000
1 parent 9df5c9d commit e8341e4

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

lightning-invoice/src/de.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,15 @@ impl FromStr for SignedRawBolt11Invoice {
401401
return Err(Bolt11ParseError::TooShortDataPart);
402402
}
403403

404-
let raw_hrp: RawHrp = hrp.to_string().to_lowercase().parse()?;
405-
let data_part = RawDataPart::from_base32(&data[..data.len() - SIGNATURE_LEN_5])?;
404+
let raw_hrp_str = hrp.to_string().to_lowercase();
405+
let raw_hrp: RawHrp = raw_hrp_str.parse()?;
406+
let data_without_signature = &data[..data.len() - SIGNATURE_LEN_5];
407+
let data_part = RawDataPart::from_base32(data_without_signature)?;
406408
let raw_invoice = RawBolt11Invoice { hrp: raw_hrp, data: data_part };
407-
let hash = raw_invoice.signable_hash();
409+
let hash = RawBolt11Invoice::hash_from_parts(
410+
raw_hrp_str.as_bytes(),
411+
data_without_signature.iter().copied(),
412+
);
408413

409414
Ok(SignedRawBolt11Invoice {
410415
raw_invoice,
@@ -1459,4 +1464,34 @@ mod test {
14591464
let input = vec![Fe32::try_from(0).unwrap(); 53];
14601465
assert!(PaymentHash::from_base32(&input).is_err());
14611466
}
1467+
1468+
#[test]
1469+
fn test_deserialized_signable_hash_binds_to_original_bytes() {
1470+
use crate::{Bolt11Bech32, SignedRawBolt11Invoice};
1471+
use bech32::primitives::decode::CheckedHrpstring;
1472+
use bech32::{Fe32IterExt, Hrp};
1473+
1474+
let canonical_str = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu";
1475+
let parsed = CheckedHrpstring::new::<Bolt11Bech32>(canonical_str).unwrap();
1476+
let data_fes = parsed.fe32_iter::<&mut dyn Iterator<Item = u8>>().collect::<Vec<_>>();
1477+
1478+
let malleated_hrp = Hrp::parse_unchecked("lnbc025m");
1479+
let malleated_str = data_fes
1480+
.iter()
1481+
.copied()
1482+
.with_checksum::<Bolt11Bech32>(&malleated_hrp)
1483+
.chars()
1484+
.collect::<String>();
1485+
assert_ne!(canonical_str, malleated_str.as_str());
1486+
1487+
let canonical: SignedRawBolt11Invoice = canonical_str.parse().unwrap();
1488+
let malleated: SignedRawBolt11Invoice = malleated_str.parse().unwrap();
1489+
1490+
assert_eq!(canonical.signature(), malleated.signature());
1491+
assert_ne!(canonical.signable_hash(), malleated.signable_hash());
1492+
assert_ne!(
1493+
canonical.recover_payee_pub_key().unwrap(),
1494+
malleated.recover_payee_pub_key().unwrap()
1495+
);
1496+
}
14621497
}

lightning-invoice/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,7 @@ macro_rules! find_all_extract {
10891089
#[allow(missing_docs)]
10901090
impl RawBolt11Invoice {
10911091
/// Hash the HRP (as bytes) and signatureless data part (as Fe32 iterator)
1092-
fn hash_from_parts<'s, I: Iterator<Item = Fe32> + 's>(
1092+
pub(crate) fn hash_from_parts<'s, I: Iterator<Item = Fe32> + 's>(
10931093
hrp_bytes: &[u8], data_without_signature: I,
10941094
) -> [u8; 32] {
10951095
use crate::bech32::Fe32IterExt;

0 commit comments

Comments
 (0)