Skip to content

Commit 4178dd7

Browse files
authored
Merge pull request #3163 from shaavan/invoice_reply_path
Introduce Reply Paths for BOLT12 Invoice in Offers Flow.
2 parents 82b3f62 + 7b49993 commit 4178dd7

File tree

4 files changed

+113
-15
lines changed

4 files changed

+113
-15
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,19 @@ pub enum OffersContext {
347347
///
348348
/// [`Bolt12Invoice::payment_hash`]: crate::offers::invoice::Bolt12Invoice::payment_hash
349349
payment_hash: PaymentHash,
350+
351+
/// A nonce used for authenticating that a received [`InvoiceError`] is for a valid
352+
/// sent [`Bolt12Invoice`].
353+
///
354+
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
355+
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
356+
nonce: Nonce,
357+
358+
/// Authentication code for the [`PaymentHash`], which should be checked when the context is
359+
/// used to log the received [`InvoiceError`].
360+
///
361+
/// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError
362+
hmac: Hmac<Sha256>,
350363
},
351364
}
352365

@@ -366,6 +379,8 @@ impl_writeable_tlv_based_enum!(OffersContext,
366379
},
367380
(2, InboundPayment) => {
368381
(0, payment_hash, required),
382+
(1, nonce, required),
383+
(2, hmac, required)
369384
},
370385
);
371386

lightning/src/ln/channelmanager.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,38 @@ impl From<&ClaimableHTLC> for events::ClaimedHTLC {
409409
}
410410
}
411411

412+
/// A trait defining behavior for creating and verifing the HMAC for authenticating a given data.
413+
pub trait Verification {
414+
/// Constructs an HMAC to include in [`OffersContext`] for the data along with the given
415+
/// [`Nonce`].
416+
fn hmac_for_offer_payment(
417+
&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
418+
) -> Hmac<Sha256>;
419+
420+
/// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`].
421+
fn verify(
422+
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
423+
) -> Result<(), ()>;
424+
}
425+
426+
impl Verification for PaymentHash {
427+
/// Constructs an HMAC to include in [`OffersContext::InboundPayment`] for the payment hash
428+
/// along with the given [`Nonce`].
429+
fn hmac_for_offer_payment(
430+
&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
431+
) -> Hmac<Sha256> {
432+
signer::hmac_for_payment_hash(*self, nonce, expanded_key)
433+
}
434+
435+
/// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an
436+
/// [`OffersContext::InboundPayment`].
437+
fn verify(
438+
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
439+
) -> Result<(), ()> {
440+
signer::verify_payment_hash(*self, hmac, nonce, expanded_key)
441+
}
442+
}
443+
412444
/// A user-provided identifier in [`ChannelManager::send_payment`] used to uniquely identify
413445
/// a payment and ensure idempotency in LDK.
414446
///
@@ -419,18 +451,20 @@ pub struct PaymentId(pub [u8; Self::LENGTH]);
419451
impl PaymentId {
420452
/// Number of bytes in the id.
421453
pub const LENGTH: usize = 32;
454+
}
422455

456+
impl Verification for PaymentId {
423457
/// Constructs an HMAC to include in [`OffersContext::OutboundPayment`] for the payment id
424458
/// along with the given [`Nonce`].
425-
pub fn hmac_for_offer_payment(
459+
fn hmac_for_offer_payment(
426460
&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
427461
) -> Hmac<Sha256> {
428462
signer::hmac_for_payment_id(*self, nonce, expanded_key)
429463
}
430464

431465
/// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an
432466
/// [`OffersContext::OutboundPayment`].
433-
pub fn verify(
467+
fn verify(
434468
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
435469
) -> Result<(), ()> {
436470
signer::verify_payment_id(*self, hmac, nonce, expanded_key)
@@ -9195,8 +9229,10 @@ where
91959229
let builder: InvoiceBuilder<DerivedSigningPubkey> = builder.into();
91969230
let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?;
91979231

9232+
let nonce = Nonce::from_entropy_source(entropy);
9233+
let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key);
91989234
let context = OffersContext::InboundPayment {
9199-
payment_hash: invoice.payment_hash(),
9235+
payment_hash: invoice.payment_hash(), nonce, hmac
92009236
};
92019237
let reply_paths = self.create_blinded_paths(context)
92029238
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
@@ -10894,7 +10930,12 @@ where
1089410930
};
1089510931

1089610932
match response {
10897-
Ok(invoice) => Some((OffersMessage::Invoice(invoice), responder.respond())),
10933+
Ok(invoice) => {
10934+
let nonce = Nonce::from_entropy_source(&*self.entropy_source);
10935+
let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key);
10936+
let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac });
10937+
Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context)))
10938+
},
1089810939
Err(error) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())),
1089910940
}
1090010941
},
@@ -10956,7 +10997,12 @@ where
1095610997
},
1095710998
OffersMessage::InvoiceError(invoice_error) => {
1095810999
let payment_hash = match context {
10959-
Some(OffersContext::InboundPayment { payment_hash }) => Some(payment_hash),
11000+
Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => {
11001+
match payment_hash.verify(hmac, nonce, expanded_key) {
11002+
Ok(_) => Some(payment_hash),
11003+
Err(_) => None,
11004+
}
11005+
},
1096011006
_ => None,
1096111007
};
1096211008

lightning/src/ln/offers_tests.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,12 @@ fn extract_invoice_request<'a, 'b, 'c>(
219219
}
220220
}
221221

222-
fn extract_invoice<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> (Bolt12Invoice, Option<BlindedMessagePath>) {
222+
fn extract_invoice<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> (Bolt12Invoice, BlindedMessagePath) {
223223
match node.onion_messenger.peel_onion_message(message) {
224224
Ok(PeeledOnion::Receive(message, _, reply_path)) => match message {
225225
ParsedOnionMessageContents::Offers(offers_message) => match offers_message {
226226
OffersMessage::InvoiceRequest(invoice_request) => panic!("Unexpected invoice_request: {:?}", invoice_request),
227-
OffersMessage::Invoice(invoice) => (invoice, reply_path),
227+
OffersMessage::Invoice(invoice) => (invoice, reply_path.unwrap()),
228228
#[cfg(async_payments)]
229229
OffersMessage::StaticInvoice(invoice) => panic!("Unexpected static invoice: {:?}", invoice),
230230
OffersMessage::InvoiceError(error) => panic!("Unexpected invoice_error: {:?}", error),
@@ -580,13 +580,22 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
580580
let onion_message = charlie.onion_messenger.next_onion_message_for_peer(david_id).unwrap();
581581
david.onion_messenger.handle_onion_message(&charlie_id, &onion_message);
582582

583-
let (invoice, _) = extract_invoice(david, &onion_message);
583+
let (invoice, reply_path) = extract_invoice(david, &onion_message);
584584
assert_eq!(invoice.amount_msats(), 10_000_000);
585585
assert_ne!(invoice.signing_pubkey(), alice_id);
586586
assert!(!invoice.payment_paths().is_empty());
587587
for path in invoice.payment_paths() {
588588
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id));
589589
}
590+
// Both Bob and Charlie have an equal number of channels and need to be connected
591+
// to Alice when she's handling the message. Therefore, either Bob or Charlie could
592+
// serve as the introduction node for the reply path back to Alice.
593+
assert!(
594+
matches!(
595+
reply_path.introduction_node(),
596+
&IntroductionNode::NodeId(node_id) if node_id == bob_id || node_id == charlie_id,
597+
)
598+
);
590599

591600
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
592601
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);
@@ -659,7 +668,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
659668
let onion_message = charlie.onion_messenger.next_onion_message_for_peer(david_id).unwrap();
660669
david.onion_messenger.handle_onion_message(&charlie_id, &onion_message);
661670

662-
let (invoice, _) = extract_invoice(david, &onion_message);
671+
let (invoice, reply_path) = extract_invoice(david, &onion_message);
663672
assert_eq!(invoice, expected_invoice);
664673

665674
assert_eq!(invoice.amount_msats(), 10_000_000);
@@ -668,6 +677,8 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
668677
for path in invoice.payment_paths() {
669678
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id));
670679
}
680+
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id));
681+
671682

672683
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
673684
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);
@@ -726,13 +737,14 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
726737
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
727738
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);
728739

729-
let (invoice, _) = extract_invoice(bob, &onion_message);
740+
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
730741
assert_eq!(invoice.amount_msats(), 10_000_000);
731742
assert_ne!(invoice.signing_pubkey(), alice_id);
732743
assert!(!invoice.payment_paths().is_empty());
733744
for path in invoice.payment_paths() {
734745
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
735746
}
747+
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id));
736748

737749
route_bolt12_payment(bob, &[alice], &invoice);
738750
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
@@ -779,7 +791,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
779791
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
780792
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);
781793

782-
let (invoice, _) = extract_invoice(bob, &onion_message);
794+
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
783795
assert_eq!(invoice, expected_invoice);
784796

785797
assert_eq!(invoice.amount_msats(), 10_000_000);
@@ -788,6 +800,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
788800
for path in invoice.payment_paths() {
789801
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
790802
}
803+
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id));
791804

792805
route_bolt12_payment(bob, &[alice], &invoice);
793806
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
@@ -1044,7 +1057,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() {
10441057
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
10451058

10461059
let (_, reply_path) = extract_invoice(alice, &onion_message);
1047-
assert_eq!(reply_path.unwrap().introduction_node(), &IntroductionNode::NodeId(charlie_id));
1060+
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id));
10481061

10491062
// Send, extract and verify the second Invoice Request message
10501063
let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
@@ -1053,7 +1066,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() {
10531066
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
10541067

10551068
let (_, reply_path) = extract_invoice(alice, &onion_message);
1056-
assert_eq!(reply_path.unwrap().introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id()));
1069+
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id()));
10571070
}
10581071

10591072
/// Checks that a deferred invoice can be paid asynchronously from an Event::InvoiceReceived.
@@ -1190,12 +1203,13 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
11901203
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
11911204
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);
11921205

1193-
let (invoice, _) = extract_invoice(bob, &onion_message);
1206+
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
11941207
assert_ne!(invoice.signing_pubkey(), alice_id);
11951208
assert!(!invoice.payment_paths().is_empty());
11961209
for path in invoice.payment_paths() {
11971210
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id));
11981211
}
1212+
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id));
11991213

12001214
route_bolt12_payment(bob, &[alice], &invoice);
12011215
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
@@ -1239,7 +1253,7 @@ fn creates_refund_with_blinded_path_using_unannounced_introduction_node() {
12391253

12401254
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
12411255

1242-
let (invoice, _) = extract_invoice(bob, &onion_message);
1256+
let (invoice, _reply_path) = extract_invoice(bob, &onion_message);
12431257
assert_eq!(invoice, expected_invoice);
12441258
assert_ne!(invoice.signing_pubkey(), alice_id);
12451259
assert!(!invoice.payment_paths().is_empty());

lightning/src/offers/signer.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use bitcoin::hashes::cmp::fixed_time_eq;
1414
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
1515
use bitcoin::hashes::sha256::Hash as Sha256;
1616
use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey, self};
17+
use types::payment::PaymentHash;
1718
use core::fmt;
1819
use crate::ln::channelmanager::PaymentId;
1920
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
@@ -39,6 +40,9 @@ const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16];
3940
// HMAC input for a `PaymentId`. The HMAC is used in `OffersContext::OutboundPayment`.
4041
const PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16];
4142

43+
// HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`.
44+
const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[6; 16];
45+
4246
/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
4347
/// verified.
4448
#[derive(Clone)]
@@ -413,3 +417,22 @@ pub(crate) fn verify_payment_id(
413417
) -> Result<(), ()> {
414418
if hmac_for_payment_id(payment_id, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) }
415419
}
420+
421+
pub(crate) fn hmac_for_payment_hash(
422+
payment_hash: PaymentHash, nonce: Nonce, expanded_key: &ExpandedKey,
423+
) -> Hmac<Sha256> {
424+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment Hash";
425+
let mut hmac = expanded_key.hmac_for_offer();
426+
hmac.input(IV_BYTES);
427+
hmac.input(&nonce.0);
428+
hmac.input(PAYMENT_HASH_HMAC_INPUT);
429+
hmac.input(&payment_hash.0);
430+
431+
Hmac::from_engine(hmac)
432+
}
433+
434+
pub(crate) fn verify_payment_hash(
435+
payment_hash: PaymentHash, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &ExpandedKey,
436+
) -> Result<(), ()> {
437+
if hmac_for_payment_hash(payment_hash, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) }
438+
}

0 commit comments

Comments
 (0)