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 b14369c432a..993a320d187 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) @@ -9192,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)?; @@ -10891,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())), } }, @@ -10953,7 +10994,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, }; 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()); 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(()) } +}