Skip to content

Commit b906000

Browse files
committed
Return optional recovered BOLT11 payee keys
Recovering a BOLT11 payee key can fail even when an invoice includes a valid n field. Return the recovery result as an option and document get_payee_pub_key as the canonical accessor. Co-Authored-By: HAL 9000 This finding was discovered by Project Loupe
1 parent 78be7a7 commit b906000

1 file changed

Lines changed: 23 additions & 13 deletions

File tree

lightning-invoice/src/lib.rs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,7 @@ impl Bolt11Invoice {
14781478
unreachable!("ensured by constructor");
14791479
}
14801480

1481-
/// Get the payee's public key if one was included in the invoice
1481+
/// Get the payee's public key if one was explicitly included in the invoice's `n` field.
14821482
pub fn payee_pub_key(&self) -> Option<&PublicKey> {
14831483
self.signed_invoice.payee_pub_key().map(|x| &x.0)
14841484
}
@@ -1498,22 +1498,21 @@ impl Bolt11Invoice {
14981498
self.signed_invoice.features()
14991499
}
15001500

1501-
/// Get the invoice's payee public key.
1501+
/// Recover the payee's public key from the invoice signature.
15021502
///
1503-
/// This uses the explicitly included payee public key, if present, otherwise it recovers the
1504-
/// payee public key from the signature. Prefer [`Self::get_payee_pub_key`] for clarity.
1505-
pub fn recover_payee_pub_key(&self) -> PublicKey {
1506-
self.get_payee_pub_key()
1503+
/// This attempts signature recovery regardless of whether a payee public key was explicitly
1504+
/// included in the invoice's `n` field. Recovery can fail for a valid invoice with an included
1505+
/// `n` field, so [`Self::get_payee_pub_key`] should be used to obtain the invoice's payee key.
1506+
pub fn recover_payee_pub_key(&self) -> Option<PublicKey> {
1507+
self.signed_invoice.recover_payee_pub_key().ok().map(|p| p.0)
15071508
}
15081509

15091510
/// Get the invoice's payee public key, preferring an explicitly included payee public key and
15101511
/// falling back to recovering the key from the signature.
15111512
pub fn get_payee_pub_key(&self) -> PublicKey {
15121513
match self.payee_pub_key() {
15131514
Some(pk) => *pk,
1514-
None => {
1515-
self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0
1516-
},
1515+
None => self.recover_payee_pub_key().expect("was checked by constructor"),
15171516
}
15181517
}
15191518

@@ -2063,7 +2062,7 @@ mod test {
20632062
}
20642063

20652064
#[test]
2066-
fn recover_payee_pub_key_uses_included_payee_pub_key() {
2065+
fn recover_payee_pub_key_returns_signature_recovery_result() {
20672066
use crate::{
20682067
Bolt11Invoice, Bolt11InvoiceSignature, Currency, InvoiceBuilder, PaymentHash,
20692068
PaymentSecret, SignedRawBolt11Invoice,
@@ -2076,17 +2075,28 @@ mod test {
20762075
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
20772076
let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key);
20782077

2079-
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
2078+
let invoice_without_payee_pub_key = InvoiceBuilder::new(Currency::Bitcoin)
20802079
.description("Test".to_string())
20812080
.payment_hash(PaymentHash([0; 32]))
20822081
.payment_secret(PaymentSecret([21; 32]))
2082+
.min_final_cltv_expiry_delta(144)
2083+
.duration_since_epoch(Duration::from_secs(1234567))
2084+
.build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key))
2085+
.unwrap();
2086+
assert_eq!(invoice_without_payee_pub_key.recover_payee_pub_key(), Some(public_key));
2087+
assert_eq!(invoice_without_payee_pub_key.get_payee_pub_key(), public_key);
2088+
2089+
let invoice_with_payee_pub_key = InvoiceBuilder::new(Currency::Bitcoin)
2090+
.description("Test".to_string())
2091+
.payment_hash(PaymentHash([1; 32]))
2092+
.payment_secret(PaymentSecret([21; 32]))
20832093
.payee_pub_key(public_key)
20842094
.min_final_cltv_expiry_delta(144)
20852095
.duration_since_epoch(Duration::from_secs(1234567))
20862096
.build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key))
20872097
.unwrap();
20882098

2089-
let signed_raw = invoice.into_signed_raw();
2099+
let signed_raw = invoice_with_payee_pub_key.into_signed_raw();
20902100
let (raw_invoice, hash, signature) = signed_raw.into_parts();
20912101
let (_orig_rid, sig_bytes) = signature.0.serialize_compact();
20922102
let bad_rid = RecoveryId::from_i32(2).unwrap();
@@ -2099,7 +2109,7 @@ mod test {
20992109
let bad_invoice = Bolt11Invoice::from_signed(bad_signed_raw).unwrap();
21002110

21012111
assert_eq!(bad_invoice.payee_pub_key(), Some(&public_key));
2102-
assert_eq!(bad_invoice.recover_payee_pub_key(), public_key);
2112+
assert_eq!(bad_invoice.recover_payee_pub_key(), None);
21032113
assert_eq!(bad_invoice.get_payee_pub_key(), public_key);
21042114
}
21052115

0 commit comments

Comments
 (0)