Skip to content

Commit 06393eb

Browse files
committed
Use BOLT11 invoice payee keys for payment params
Payment parameters should use the canonical payee key from BOLT11 invoices. When an invoice includes an n field, using that key avoids attempting signature recovery that may legitimately be unavailable. Co-Authored-By: HAL 9000 This finding was discovered by Project Loupe
1 parent c9260ee commit 06393eb

3 files changed

Lines changed: 105 additions & 9 deletions

File tree

lightning-invoice/src/lib.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,17 +1498,22 @@ impl Bolt11Invoice {
14981498
self.signed_invoice.features()
14991499
}
15001500

1501-
/// Recover the payee's public key (only to be used if none was included in the invoice)
1501+
/// Get the invoice's payee public key.
1502+
///
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.
15021505
pub fn recover_payee_pub_key(&self) -> PublicKey {
1503-
self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0
1506+
self.get_payee_pub_key()
15041507
}
15051508

1506-
/// Recover the payee's public key if one was included in the invoice, otherwise return the
1507-
/// recovered public key from the signature
1509+
/// Get the invoice's payee public key, preferring an explicitly included payee public key and
1510+
/// falling back to recovering the key from the signature.
15081511
pub fn get_payee_pub_key(&self) -> PublicKey {
15091512
match self.payee_pub_key() {
15101513
Some(pk) => *pk,
1511-
None => self.recover_payee_pub_key(),
1514+
None => {
1515+
self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0
1516+
},
15121517
}
15131518
}
15141519

@@ -2057,6 +2062,47 @@ mod test {
20572062
assert!(new_signed.check_signature());
20582063
}
20592064

2065+
#[test]
2066+
fn recover_payee_pub_key_uses_included_payee_pub_key() {
2067+
use crate::{
2068+
Bolt11Invoice, Bolt11InvoiceSignature, Currency, InvoiceBuilder, PaymentHash,
2069+
PaymentSecret, SignedRawBolt11Invoice,
2070+
};
2071+
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
2072+
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
2073+
use core::time::Duration;
2074+
2075+
let secp_ctx = Secp256k1::new();
2076+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
2077+
let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key);
2078+
2079+
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
2080+
.description("Test".to_string())
2081+
.payment_hash(PaymentHash([0; 32]))
2082+
.payment_secret(PaymentSecret([21; 32]))
2083+
.payee_pub_key(public_key)
2084+
.min_final_cltv_expiry_delta(144)
2085+
.duration_since_epoch(Duration::from_secs(1234567))
2086+
.build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key))
2087+
.unwrap();
2088+
2089+
let signed_raw = invoice.into_signed_raw();
2090+
let (raw_invoice, hash, signature) = signed_raw.into_parts();
2091+
let (_orig_rid, sig_bytes) = signature.0.serialize_compact();
2092+
let bad_rid = RecoveryId::from_i32(2).unwrap();
2093+
let bad_sig = RecoverableSignature::from_compact(&sig_bytes, bad_rid).unwrap();
2094+
let bad_signed_raw = SignedRawBolt11Invoice {
2095+
raw_invoice,
2096+
hash,
2097+
signature: Bolt11InvoiceSignature(bad_sig),
2098+
};
2099+
let bad_invoice = Bolt11Invoice::from_signed(bad_signed_raw).unwrap();
2100+
2101+
assert_eq!(bad_invoice.payee_pub_key(), Some(&public_key));
2102+
assert_eq!(bad_invoice.recover_payee_pub_key(), public_key);
2103+
assert_eq!(bad_invoice.get_payee_pub_key(), public_key);
2104+
}
2105+
20602106
#[test]
20612107
fn test_check_feature_bits() {
20622108
use crate::TaggedField::*;

lightning/src/ln/invoice_utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,7 +1281,7 @@ mod test {
12811281
assert!(!invoice.features().unwrap().supports_basic_mpp());
12821282

12831283
let payment_params = PaymentParameters::from_node_id(
1284-
invoice.recover_payee_pub_key(),
1284+
invoice.get_payee_pub_key(),
12851285
invoice.min_final_cltv_expiry_delta() as u32,
12861286
)
12871287
.with_bolt11_features(invoice.features().unwrap().clone())
@@ -1347,7 +1347,7 @@ mod test {
13471347
payment_secret,
13481348
payment_amt,
13491349
payment_preimage_opt,
1350-
invoice.recover_payee_pub_key(),
1350+
invoice.get_payee_pub_key(),
13511351
);
13521352
do_claim_payment_along_route(ClaimAlongRouteArgs::new(
13531353
&nodes[0],

lightning/src/routing/router.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@ impl PaymentParameters {
11771177
/// [`PaymentParameters::expiry_time`].
11781178
pub fn from_bolt11_invoice(invoice: &Bolt11Invoice) -> Self {
11791179
let mut payment_params = Self::from_node_id(
1180-
invoice.recover_payee_pub_key(),
1180+
invoice.get_payee_pub_key(),
11811181
invoice.min_final_cltv_expiry_delta() as u32,
11821182
)
11831183
.with_route_hints(invoice.route_hints())
@@ -4094,7 +4094,7 @@ mod tests {
40944094
use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId, P2PGossipSync};
40954095
use crate::routing::router::{
40964096
add_random_cltv_offset, build_route_from_hops_internal, default_node_features, get_route,
4097-
BlindedPathCandidate, BlindedTail, CandidateRouteHop, InFlightHtlcs, Path,
4097+
BlindedPathCandidate, BlindedTail, CandidateRouteHop, InFlightHtlcs, Path, Payee,
40984098
PaymentParameters, PublicHopCandidate, Route, RouteHint, RouteHintHop, RouteHop,
40994099
RouteParameters, RoutingFees, ScorerAccountingForInFlightHtlcs,
41004100
DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE,
@@ -4113,6 +4113,8 @@ mod tests {
41134113
use crate::util::test_utils as ln_test_utils;
41144114

41154115
use bitcoin::amount::Amount;
4116+
use bitcoin::bech32::primitives::decode::CheckedHrpstring;
4117+
use bitcoin::bech32::{ByteIterExt, Fe32IterExt};
41164118
use bitcoin::constants::ChainHash;
41174119
use bitcoin::hashes::Hash;
41184120
use bitcoin::hex::FromHex;
@@ -4124,10 +4126,58 @@ mod tests {
41244126
use bitcoin::transaction::TxOut;
41254127
use chacha20_poly1305::chacha20::ChaCha20;
41264128
use chacha20_poly1305::{Key, Nonce};
4129+
use lightning_invoice::{Bolt11Bech32, Bolt11Invoice, Currency, InvoiceBuilder};
41274130

41284131
use crate::io::Cursor;
41294132
use crate::prelude::*;
41304133
use crate::sync::{Arc, Mutex};
4134+
use crate::types::payment::{PaymentHash, PaymentSecret};
4135+
4136+
fn invoice_with_included_payee_pub_key_and_bad_recovery_id() -> (Bolt11Invoice, PublicKey) {
4137+
let secp_ctx = Secp256k1::new();
4138+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
4139+
let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key);
4140+
4141+
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
4142+
.description("Test".to_string())
4143+
.amount_milli_satoshis(1000)
4144+
.payment_hash(PaymentHash([0; 32]))
4145+
.payment_secret(PaymentSecret([21; 32]))
4146+
.payee_pub_key(public_key)
4147+
.min_final_cltv_expiry_delta(144)
4148+
.duration_since_epoch(core::time::Duration::from_secs(1234567))
4149+
.build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key))
4150+
.unwrap();
4151+
4152+
let invoice_string = invoice.to_string();
4153+
let parsed = CheckedHrpstring::new::<Bolt11Bech32>(&invoice_string).unwrap();
4154+
let hrp = parsed.hrp();
4155+
let mut data: Vec<_> = parsed.fe32_iter::<&mut dyn Iterator<Item = u8>>().collect();
4156+
let signature_start = data.len() - 104;
4157+
let mut signature_bytes: Vec<u8> =
4158+
data[signature_start..].iter().copied().fes_to_bytes().collect();
4159+
signature_bytes[64] = 2;
4160+
let signature_data: Vec<_> = signature_bytes.into_iter().bytes_to_fes().collect();
4161+
data.splice(signature_start.., signature_data);
4162+
4163+
let bad_invoice_string = data
4164+
.into_iter()
4165+
.with_checksum::<bitcoin::bech32::Bech32>(&hrp)
4166+
.chars()
4167+
.collect::<String>();
4168+
(bad_invoice_string.parse().unwrap(), public_key)
4169+
}
4170+
4171+
#[test]
4172+
fn payment_params_from_bolt11_invoice_uses_included_payee_pub_key() {
4173+
let (invoice, public_key) = invoice_with_included_payee_pub_key_and_bad_recovery_id();
4174+
let payment_params = PaymentParameters::from_bolt11_invoice(&invoice);
4175+
4176+
match payment_params.payee {
4177+
Payee::Clear { node_id, .. } => assert_eq!(node_id, public_key),
4178+
Payee::Blinded { .. } => panic!("BOLT11 invoice should create a clear payee"),
4179+
}
4180+
}
41314181

41324182
#[rustfmt::skip]
41334183
fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,

0 commit comments

Comments
 (0)