From e2ee3251018a17f1e87c57484ed0e48331dd7b7c Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 6 Sep 2024 17:31:08 +0530 Subject: [PATCH 1/5] HMAC Construction and Verification for PaymentHash When a InvoiceError is received for a sent BOLT12Invoice, the corresponding PaymentHash is to be logged. Introduce hmac construction and verification function for PaymentHash for this purpose. --- lightning/src/offers/signer.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 2ee54c58811..80e17f3b0e9 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -14,6 +14,7 @@ use bitcoin::hashes::cmp::fixed_time_eq; use bitcoin::hashes::hmac::{Hmac, HmacEngine}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey, self}; +use types::payment::PaymentHash; use core::fmt; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; @@ -39,6 +40,9 @@ const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16]; // HMAC input for a `PaymentId`. The HMAC is used in `OffersContext::OutboundPayment`. const PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16]; +// HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`. +const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[6; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -413,3 +417,22 @@ pub(crate) fn verify_payment_id( ) -> Result<(), ()> { if hmac_for_payment_id(payment_id, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } } + +pub(crate) fn hmac_for_payment_hash( + payment_hash: PaymentHash, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment Hash"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(PAYMENT_HASH_HMAC_INPUT); + hmac.input(&payment_hash.0); + + Hmac::from_engine(hmac) +} + +pub(crate) fn verify_payment_hash( + payment_hash: PaymentHash, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_payment_hash(payment_hash, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } +} From 6500277ba810471dc527f5078672a49020d9d6e9 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 10 Sep 2024 15:37:10 +0530 Subject: [PATCH 2/5] Introduce Verification trait. - The trait defines the public method one may define for creating and verifying the HMAC. - Using a pub trait to define these method allows the flexibility for other `OffersMessageHandler` construct to construct the HMAC and authenticate the message. --- lightning/src/ln/channelmanager.rs | 38 ++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b14369c432a..56457e79cba 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -409,6 +409,38 @@ impl From<&ClaimableHTLC> for events::ClaimedHTLC { } } +/// A trait defining behavior for creating and verifing the HMAC for authenticating a given data. +pub trait Verification { + /// Constructs an HMAC to include in [`OffersContext`] for the data along with the given + /// [`Nonce`]. + fn hmac_for_offer_payment( + &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Hmac; + + /// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`]. + fn verify( + &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Result<(), ()>; +} + +impl Verification for PaymentHash { + /// Constructs an HMAC to include in [`OffersContext::InboundPayment`] for the payment hash + /// along with the given [`Nonce`]. + fn hmac_for_offer_payment( + &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Hmac { + signer::hmac_for_payment_hash(*self, nonce, expanded_key) + } + + /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an + /// [`OffersContext::InboundPayment`]. + fn verify( + &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Result<(), ()> { + signer::verify_payment_hash(*self, hmac, nonce, expanded_key) + } +} + /// A user-provided identifier in [`ChannelManager::send_payment`] used to uniquely identify /// a payment and ensure idempotency in LDK. /// @@ -419,10 +451,12 @@ pub struct PaymentId(pub [u8; Self::LENGTH]); impl PaymentId { /// Number of bytes in the id. pub const LENGTH: usize = 32; +} +impl Verification for PaymentId { /// Constructs an HMAC to include in [`OffersContext::OutboundPayment`] for the payment id /// along with the given [`Nonce`]. - pub fn hmac_for_offer_payment( + fn hmac_for_offer_payment( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { signer::hmac_for_payment_id(*self, nonce, expanded_key) @@ -430,7 +464,7 @@ impl PaymentId { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::OutboundPayment`]. - pub fn verify( + fn verify( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_payment_id(*self, hmac, nonce, expanded_key) From 8b479ac5873b1d2e11514ac7d7141892d6099946 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 6 Sep 2024 17:31:13 +0530 Subject: [PATCH 3/5] Add HMAC, and nonce to OffersContext::InboundPayment Introduce HMAC and nonce calculation when sending Invoice with reply path, so that if we receive InvoiceError back for the corresponding Invoice we can verify the payment hash before logging it. --- lightning/src/blinded_path/message.rs | 15 +++++++++++++++ lightning/src/ln/channelmanager.rs | 11 +++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index e3899b50edb..256483fec01 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -347,6 +347,19 @@ pub enum OffersContext { /// /// [`Bolt12Invoice::payment_hash`]: crate::offers::invoice::Bolt12Invoice::payment_hash payment_hash: PaymentHash, + + /// A nonce used for authenticating that a received [`InvoiceError`] is for a valid + /// sent [`Bolt12Invoice`]. + /// + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + nonce: Nonce, + + /// Authentication code for the [`PaymentHash`], which should be checked when the context is + /// used to log the received [`InvoiceError`]. + /// + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError + hmac: Hmac, }, } @@ -366,6 +379,8 @@ impl_writeable_tlv_based_enum!(OffersContext, }, (2, InboundPayment) => { (0, payment_hash, required), + (1, nonce, required), + (2, hmac, required) }, ); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 56457e79cba..c8597f8f035 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9226,8 +9226,10 @@ where let builder: InvoiceBuilder = builder.into(); let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; + let nonce = Nonce::from_entropy_source(entropy); + let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); let context = OffersContext::InboundPayment { - payment_hash: invoice.payment_hash(), + payment_hash: invoice.payment_hash(), nonce, hmac }; let reply_paths = self.create_blinded_paths(context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -10987,7 +10989,12 @@ where }, OffersMessage::InvoiceError(invoice_error) => { let payment_hash = match context { - Some(OffersContext::InboundPayment { payment_hash }) => Some(payment_hash), + Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => { + match payment_hash.verify(hmac, nonce, expanded_key) { + Ok(_) => Some(payment_hash), + Err(_) => None, + } + }, _ => None, }; From a4bf93610109c907a4a368069b3d0ace2604b602 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 6 Sep 2024 17:51:37 +0530 Subject: [PATCH 4/5] Add reply_path to BOLT12Invoices in Offers Flow 1. Introduced reply_path in BOLT12Invoices to address a gap in error handling. Previously, if a BOLT12Invoice sent in the offers flow generated an Invoice Error, the payer had no way to send this error back to the payee. 2. By adding a reply_path to the Invoice Message, the payer can now communicate any errors back to the payee, ensuring better error handling and communication within the offers flow. --- lightning/src/ln/channelmanager.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c8597f8f035..993a320d187 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10927,7 +10927,12 @@ where }; match response { - Ok(invoice) => Some((OffersMessage::Invoice(invoice), responder.respond())), + Ok(invoice) => { + let nonce = Nonce::from_entropy_source(&*self.entropy_source); + let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); + Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) + }, Err(error) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())), } }, From 7b499931012f9a8710bdb24ad67ec7f92daade4f Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 29 Aug 2024 17:06:18 +0530 Subject: [PATCH 5/5] Update Offers Test to Verify BOLT12 Invoice Reply Paths 1. Updated the Offers Test to check the reply paths in BOLT12 Invoices. 2. Changed the `extract_invoice` return type from `Option` to `BlindedMessagePath` since all BOLT12Invoices now have a corresponding reply path by default. --- lightning/src/ln/offers_tests.rs | 34 ++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 8bbf9a0ad7d..8e4de107a28 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -219,12 +219,12 @@ fn extract_invoice_request<'a, 'b, 'c>( } } -fn extract_invoice<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> (Bolt12Invoice, Option) { +fn extract_invoice<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> (Bolt12Invoice, BlindedMessagePath) { match node.onion_messenger.peel_onion_message(message) { Ok(PeeledOnion::Receive(message, _, reply_path)) => match message { ParsedOnionMessageContents::Offers(offers_message) => match offers_message { OffersMessage::InvoiceRequest(invoice_request) => panic!("Unexpected invoice_request: {:?}", invoice_request), - OffersMessage::Invoice(invoice) => (invoice, reply_path), + OffersMessage::Invoice(invoice) => (invoice, reply_path.unwrap()), #[cfg(async_payments)] OffersMessage::StaticInvoice(invoice) => panic!("Unexpected static invoice: {:?}", invoice), OffersMessage::InvoiceError(error) => panic!("Unexpected invoice_error: {:?}", error), @@ -580,13 +580,22 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { let onion_message = charlie.onion_messenger.next_onion_message_for_peer(david_id).unwrap(); david.onion_messenger.handle_onion_message(&charlie_id, &onion_message); - let (invoice, _) = extract_invoice(david, &onion_message); + let (invoice, reply_path) = extract_invoice(david, &onion_message); assert_eq!(invoice.amount_msats(), 10_000_000); assert_ne!(invoice.signing_pubkey(), alice_id); assert!(!invoice.payment_paths().is_empty()); for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); } + // Both Bob and Charlie have an equal number of channels and need to be connected + // to Alice when she's handling the message. Therefore, either Bob or Charlie could + // serve as the introduction node for the reply path back to Alice. + assert!( + matches!( + reply_path.introduction_node(), + &IntroductionNode::NodeId(node_id) if node_id == bob_id || node_id == charlie_id, + ) + ); route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); @@ -659,7 +668,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { let onion_message = charlie.onion_messenger.next_onion_message_for_peer(david_id).unwrap(); david.onion_messenger.handle_onion_message(&charlie_id, &onion_message); - let (invoice, _) = extract_invoice(david, &onion_message); + let (invoice, reply_path) = extract_invoice(david, &onion_message); assert_eq!(invoice, expected_invoice); assert_eq!(invoice.amount_msats(), 10_000_000); @@ -668,6 +677,8 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); } + assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); @@ -726,13 +737,14 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); - let (invoice, _) = extract_invoice(bob, &onion_message); + let (invoice, reply_path) = extract_invoice(bob, &onion_message); assert_eq!(invoice.amount_msats(), 10_000_000); assert_ne!(invoice.signing_pubkey(), alice_id); assert!(!invoice.payment_paths().is_empty()); for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } + assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id)); route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); @@ -779,7 +791,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); - let (invoice, _) = extract_invoice(bob, &onion_message); + let (invoice, reply_path) = extract_invoice(bob, &onion_message); assert_eq!(invoice, expected_invoice); assert_eq!(invoice.amount_msats(), 10_000_000); @@ -788,6 +800,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); } + assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id)); route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); @@ -1044,7 +1057,7 @@ fn send_invoice_for_refund_with_distinct_reply_path() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (_, reply_path) = extract_invoice(alice, &onion_message); - assert_eq!(reply_path.unwrap().introduction_node(), &IntroductionNode::NodeId(charlie_id)); + assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id)); // Send, extract and verify the second Invoice Request message 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() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); let (_, reply_path) = extract_invoice(alice, &onion_message); - assert_eq!(reply_path.unwrap().introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id())); + assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(nodes[6].node.get_our_node_id())); } /// 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() { let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); - let (invoice, _) = extract_invoice(bob, &onion_message); + let (invoice, reply_path) = extract_invoice(bob, &onion_message); assert_ne!(invoice.signing_pubkey(), alice_id); assert!(!invoice.payment_paths().is_empty()); for path in invoice.payment_paths() { assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(bob_id)); } + assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); @@ -1239,7 +1253,7 @@ fn creates_refund_with_blinded_path_using_unannounced_introduction_node() { let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); - let (invoice, _) = extract_invoice(bob, &onion_message); + let (invoice, _reply_path) = extract_invoice(bob, &onion_message); assert_eq!(invoice, expected_invoice); assert_ne!(invoice.signing_pubkey(), alice_id); assert!(!invoice.payment_paths().is_empty());