diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index fe1a8b76681..15bf1a94940 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -22,6 +22,7 @@ use crate::io; use crate::io::Cursor; use crate::ln::channelmanager::PaymentId; use crate::ln::onion_utils; +use crate::offers::nonce::Nonce; use crate::onion_message::packet::ControlTlvs; use crate::sign::{NodeSigner, Recipient}; use crate::crypto::streams::ChaChaPolyReadAdapter; @@ -85,37 +86,71 @@ impl Writeable for ReceiveTlvs { } } -/// Represents additional data included by the recipient in a [`BlindedPath`]. +/// Additional data included by the recipient in a [`BlindedPath`]. /// -/// This data is encrypted by the recipient and remains invisible to anyone else. -/// It is included in the [`BlindedPath`], making it accessible again to the recipient -/// whenever the [`BlindedPath`] is used. -/// The recipient can authenticate the message and utilize it for further processing -/// if needed. +/// This data is encrypted by the recipient and will be given to the corresponding message handler +/// when handling a message sent over the [`BlindedPath`]. The recipient can use this data to +/// authenticate the message or for further processing if needed. #[derive(Clone, Debug)] pub enum MessageContext { - /// Represents the data specific to [`OffersMessage`] + /// Context specific to an [`OffersMessage`]. /// /// [`OffersMessage`]: crate::onion_message::offers::OffersMessage Offers(OffersContext), - /// Represents custom data received in a Custom Onion Message. + /// Context specific to a [`CustomOnionMessageHandler::CustomMessage`]. + /// + /// [`CustomOnionMessageHandler::CustomMessage`]: crate::onion_message::messenger::CustomOnionMessageHandler::CustomMessage Custom(Vec), } -/// Contains the data specific to [`OffersMessage`] +/// Contains data specific to an [`OffersMessage`]. /// /// [`OffersMessage`]: crate::onion_message::offers::OffersMessage -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum OffersContext { - /// Represents an unknown BOLT12 payment context. - /// This variant is used when a message is sent without - /// using a [`BlindedPath`] or over one created prior to - /// LDK version 0.0.124. + /// Represents an unknown BOLT12 message context. + /// + /// This variant is used when a message is sent without using a [`BlindedPath`] or over one + /// created prior to LDK version 0.0.124. Unknown {}, - /// Represents an outbound BOLT12 payment context. + /// Context used by a [`BlindedPath`] within an [`Offer`]. + /// + /// This variant is intended to be received when handling an [`InvoiceRequest`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + InvoiceRequest { + /// A nonce used for authenticating that an [`InvoiceRequest`] is for a valid [`Offer`] and + /// for deriving the offer's signing keys. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Offer`]: crate::offers::offer::Offer + nonce: Nonce, + }, + /// Context used by a [`BlindedPath`] within a [`Refund`] or as a reply path for an + /// [`InvoiceRequest`]. + /// + /// This variant is intended to be received when handling a [`Bolt12Invoice`] or an + /// [`InvoiceError`]. + /// + /// [`Refund`]: crate::offers::refund::Refund + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError OutboundPayment { - /// Payment ID of the outbound BOLT12 payment. - payment_id: PaymentId + /// Payment ID used when creating a [`Refund`] or [`InvoiceRequest`]. + /// + /// [`Refund`]: crate::offers::refund::Refund + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + payment_id: PaymentId, + + /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid [`Refund`] or + /// [`InvoiceRequest`] and for deriving their signing keys. + /// + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`Refund`]: crate::offers::refund::Refund + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + nonce: Nonce, }, } @@ -126,8 +161,12 @@ impl_writeable_tlv_based_enum!(MessageContext, impl_writeable_tlv_based_enum!(OffersContext, (0, Unknown) => {}, - (1, OutboundPayment) => { + (1, InvoiceRequest) => { + (0, nonce, required), + }, + (2, OutboundPayment) => { (0, payment_id, required), + (1, nonce, required), }, ); diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 9b4c203c30d..9aa449efbaa 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -18,6 +18,7 @@ pub mod bump_transaction; pub use bump_transaction::BumpTransactionEvent; +use crate::blinded_path::message::OffersContext; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext, PaymentContextRef}; use crate::chain::transaction; use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; @@ -806,6 +807,10 @@ pub enum Event { payment_id: PaymentId, /// The invoice to pay. invoice: Bolt12Invoice, + /// The context of the [`BlindedPath`] used to send the invoice. + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath + context: OffersContext, /// A responder for replying with an [`InvoiceError`] if needed. /// /// `None` if the invoice wasn't sent with a reply path. @@ -1648,12 +1653,13 @@ impl Writeable for Event { (0, peer_node_id, required), }); }, - &Event::InvoiceReceived { ref payment_id, ref invoice, ref responder } => { + &Event::InvoiceReceived { ref payment_id, ref invoice, ref context, ref responder } => { 41u8.write(writer)?; write_tlv_fields!(writer, { (0, payment_id, required), (2, invoice, required), - (4, responder, option), + (4, context, required), + (6, responder, option), }); }, &Event::FundingTxBroadcastSafe { ref channel_id, ref user_channel_id, ref funding_txo, ref counterparty_node_id, ref former_temporary_channel_id} => { @@ -2107,11 +2113,13 @@ impl MaybeReadable for Event { _init_and_read_len_prefixed_tlv_fields!(reader, { (0, payment_id, required), (2, invoice, required), - (4, responder, option), + (4, context, required), + (6, responder, option), }); Ok(Some(Event::InvoiceReceived { payment_id: payment_id.0.unwrap(), invoice: invoice.0.unwrap(), + context: context.0.unwrap(), responder, })) }; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9bceb5816b6..3e4ac872e1b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -64,6 +64,7 @@ use crate::ln::wire::Encode; use crate::offers::invoice::{BlindedPayInfo, Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{DerivedPayerId, InvoiceRequestBuilder}; +use crate::offers::nonce::Nonce; use crate::offers::offer::{Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; @@ -2254,7 +2255,10 @@ where event_persist_notifier: Notifier, needs_persist_flag: AtomicBool, + #[cfg(not(any(test, feature = "_test_utils")))] pending_offers_messages: Mutex>>, + #[cfg(any(test, feature = "_test_utils"))] + pub(crate) pending_offers_messages: Mutex>>, /// Tracks the message events that are to be broadcasted when we are connected to some peer. pending_broadcast_messages: Mutex>, @@ -4199,15 +4203,35 @@ where /// whether or not the payment was successful. /// /// [timer tick]: Self::timer_tick_occurred - pub fn send_payment_for_bolt12_invoice(&self, invoice: &Bolt12Invoice) -> Result<(), Bolt12PaymentError> { - let secp_ctx = &self.secp_ctx; - let expanded_key = &self.inbound_payment_key; - match invoice.verify(expanded_key, secp_ctx) { + pub fn send_payment_for_bolt12_invoice( + &self, invoice: &Bolt12Invoice, context: &OffersContext, + ) -> Result<(), Bolt12PaymentError> { + match self.verify_bolt12_invoice(invoice, context) { Ok(payment_id) => self.send_payment_for_verified_bolt12_invoice(invoice, payment_id), Err(()) => Err(Bolt12PaymentError::UnexpectedInvoice), } } + fn verify_bolt12_invoice( + &self, invoice: &Bolt12Invoice, context: &OffersContext, + ) -> Result { + let secp_ctx = &self.secp_ctx; + let expanded_key = &self.inbound_payment_key; + + match context { + OffersContext::Unknown {} if invoice.is_for_refund_without_paths() => { + invoice.verify_using_metadata(expanded_key, secp_ctx) + }, + OffersContext::OutboundPayment { payment_id, nonce } => { + invoice + .verify_using_payer_data(*payment_id, *nonce, expanded_key, secp_ctx) + .then(|| *payment_id) + .ok_or(()) + }, + _ => Err(()), + } + } + fn send_payment_for_verified_bolt12_invoice(&self, invoice: &Bolt12Invoice, payment_id: PaymentId) -> Result<(), Bolt12PaymentError> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -8784,13 +8808,12 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let secp_ctx = &$self.secp_ctx; - let path = $self.create_blinded_paths_using_absolute_expiry(OffersContext::Unknown {}, absolute_expiry) + let nonce = Nonce::from_entropy_source(entropy); + let context = OffersContext::InvoiceRequest { nonce }; + let path = $self.create_blinded_paths_using_absolute_expiry(context, absolute_expiry) .and_then(|paths| paths.into_iter().next().ok_or(())) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - let builder = OfferBuilder::deriving_signing_pubkey( - node_id, expanded_key, entropy, secp_ctx - ) + let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) .chain_hash($self.chain_hash) .path(path); @@ -8858,13 +8881,14 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let secp_ctx = &$self.secp_ctx; - let context = OffersContext::OutboundPayment { payment_id }; + let nonce = Nonce::from_entropy_source(entropy); + let context = OffersContext::OutboundPayment { payment_id, nonce }; let path = $self.create_blinded_paths_using_absolute_expiry(context, Some(absolute_expiry)) .and_then(|paths| paths.into_iter().next().ok_or(())) .map_err(|_| Bolt12SemanticError::MissingPaths)?; let builder = RefundBuilder::deriving_payer_id( - node_id, expanded_key, entropy, secp_ctx, amount_msats, payment_id + node_id, expanded_key, nonce, secp_ctx, amount_msats, payment_id )? .chain_hash($self.chain_hash) .absolute_expiry(absolute_expiry) @@ -8973,8 +8997,9 @@ where let entropy = &*self.entropy_source; let secp_ctx = &self.secp_ctx; + let nonce = Nonce::from_entropy_source(entropy); let builder: InvoiceRequestBuilder = offer - .request_invoice_deriving_payer_id(expanded_key, entropy, secp_ctx, payment_id)? + .request_invoice_deriving_payer_id(expanded_key, nonce, secp_ctx, payment_id)? .into(); let builder = builder.chain_hash(self.chain_hash)?; @@ -8992,8 +9017,9 @@ where }; let invoice_request = builder.build_and_sign()?; - let context = OffersContext::OutboundPayment { payment_id }; - let reply_paths = self.create_blinded_paths(context).map_err(|_| Bolt12SemanticError::MissingPaths)?; + let context = OffersContext::OutboundPayment { payment_id, nonce }; + let reply_paths = self.create_blinded_paths(context) + .map_err(|_| Bolt12SemanticError::MissingPaths)?; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -10692,7 +10718,7 @@ where let abandon_if_payment = |context| { match context { - OffersContext::OutboundPayment { payment_id } => self.abandon_payment(payment_id), + OffersContext::OutboundPayment { payment_id, .. } => self.abandon_payment(payment_id), _ => {}, } }; @@ -10703,19 +10729,32 @@ where Some(responder) => responder, None => return ResponseInstruction::NoResponse, }; + + let nonce = match context { + OffersContext::Unknown {} if invoice_request.metadata().is_some() => None, + OffersContext::InvoiceRequest { nonce } => Some(nonce), + _ => return ResponseInstruction::NoResponse, + }; + + let invoice_request = match nonce { + Some(nonce) => match invoice_request.verify_using_recipient_data( + nonce, expanded_key, secp_ctx, + ) { + Ok(invoice_request) => invoice_request, + Err(()) => return ResponseInstruction::NoResponse, + }, + None => match invoice_request.verify_using_metadata(expanded_key, secp_ctx) { + Ok(invoice_request) => invoice_request, + Err(()) => return ResponseInstruction::NoResponse, + }, + }; + let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request + &invoice_request.inner ) { Ok(amount_msats) => amount_msats, Err(error) => return responder.respond(OffersMessage::InvoiceError(error.into())), }; - let invoice_request = match invoice_request.verify(expanded_key, secp_ctx) { - Ok(invoice_request) => invoice_request, - Err(()) => { - let error = Bolt12SemanticError::InvalidMetadata; - return responder.respond(OffersMessage::InvoiceError(error.into())); - }, - }; let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; let (payment_hash, payment_secret) = match self.create_inbound_payment( @@ -10788,24 +10827,28 @@ where } }, OffersMessage::Invoice(invoice) => { - let result = match invoice.verify(expanded_key, secp_ctx) { - Ok(payment_id) => { - let features = self.bolt12_invoice_features(); - if invoice.invoice_features().requires_unknown_bits_from(&features) { - Err(InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures)) - } else if self.default_configuration.manually_handle_bolt12_invoices { - let event = Event::InvoiceReceived { payment_id, invoice, responder }; - self.pending_events.lock().unwrap().push_back((event, None)); - return ResponseInstruction::NoResponse; - } else { - self.send_payment_for_verified_bolt12_invoice(&invoice, payment_id) - .map_err(|e| { - log_trace!(self.logger, "Failed paying invoice: {:?}", e); - InvoiceError::from_string(format!("{:?}", e)) - }) - } - }, - Err(()) => Err(InvoiceError::from_string("Unrecognized invoice".to_owned())), + let payment_id = match self.verify_bolt12_invoice(&invoice, &context) { + Ok(payment_id) => payment_id, + Err(()) => return ResponseInstruction::NoResponse, + }; + + let result = { + let features = self.bolt12_invoice_features(); + if invoice.invoice_features().requires_unknown_bits_from(&features) { + Err(InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures)) + } else if self.default_configuration.manually_handle_bolt12_invoices { + let event = Event::InvoiceReceived { + payment_id, invoice, context, responder, + }; + self.pending_events.lock().unwrap().push_back((event, None)); + return ResponseInstruction::NoResponse; + } else { + self.send_payment_for_verified_bolt12_invoice(&invoice, payment_id) + .map_err(|e| { + log_trace!(self.logger, "Failed paying invoice: {:?}", e); + InvoiceError::from_string(format!("{:?}", e)) + }) + } }; match result { diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index a85d93b7135..2d807d55070 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -13,12 +13,14 @@ use bitcoin::hashes::{Hash, HashEngine}; use bitcoin::hashes::cmp::fixed_time_eq; use bitcoin::hashes::hmac::{Hmac, HmacEngine}; use bitcoin::hashes::sha256::Hash as Sha256; -use crate::sign::{KeyMaterial, EntropySource}; -use crate::ln::types::{PaymentHash, PaymentPreimage, PaymentSecret}; -use crate::ln::msgs; -use crate::ln::msgs::MAX_VALUE_MSAT; + use crate::crypto::chacha20::ChaCha20; use crate::crypto::utils::hkdf_extract_expand_5x; +use crate::ln::msgs; +use crate::ln::msgs::MAX_VALUE_MSAT; +use crate::ln::types::{PaymentHash, PaymentPreimage, PaymentSecret}; +use crate::offers::nonce::Nonce; +use crate::sign::{KeyMaterial, EntropySource}; use crate::util::errors::APIError; use crate::util::logger::Logger; @@ -96,53 +98,6 @@ impl ExpandedKey { } } -/// A 128-bit number used only once. -/// -/// Needed when constructing [`Offer::metadata`] and deriving [`Offer::signing_pubkey`] from -/// [`ExpandedKey`]. Must not be reused for any other derivation without first hashing. -/// -/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata -/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey -#[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) struct Nonce(pub(crate) [u8; Self::LENGTH]); - -impl Nonce { - /// Number of bytes in the nonce. - pub const LENGTH: usize = 16; - - /// Creates a `Nonce` from the given [`EntropySource`]. - pub fn from_entropy_source(entropy_source: ES) -> Self - where - ES::Target: EntropySource, - { - let mut bytes = [0u8; Self::LENGTH]; - let rand_bytes = entropy_source.get_secure_random_bytes(); - bytes.copy_from_slice(&rand_bytes[..Self::LENGTH]); - - Nonce(bytes) - } - - /// Returns a slice of the underlying bytes of size [`Nonce::LENGTH`]. - pub fn as_slice(&self) -> &[u8] { - &self.0 - } -} - -impl TryFrom<&[u8]> for Nonce { - type Error = (); - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() != Self::LENGTH { - return Err(()); - } - - let mut copied_bytes = [0u8; Self::LENGTH]; - copied_bytes.copy_from_slice(bytes); - - Ok(Self(copied_bytes)) - } -} - enum Method { LdkPaymentHash = 0, UserPaymentHash = 1, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index cdd78d02ca8..627fc812646 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -54,7 +54,7 @@ use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::PeeledOnion; +use crate::onion_message::messenger::{Destination, PeeledOnion}; use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::routing::gossip::{NodeAlias, NodeId}; @@ -1085,10 +1085,10 @@ fn pays_bolt12_invoice_asynchronously() { 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 = match get_event!(bob, Event::InvoiceReceived) { - Event::InvoiceReceived { payment_id: actual_payment_id, invoice, .. } => { + let (invoice, context) = match get_event!(bob, Event::InvoiceReceived) { + Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => { assert_eq!(actual_payment_id, payment_id); - invoice + (invoice, context) }, _ => panic!("No Event::InvoiceReceived"), }; @@ -1099,9 +1099,9 @@ fn pays_bolt12_invoice_asynchronously() { assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id)); } - assert!(bob.node.send_payment_for_bolt12_invoice(&invoice).is_ok()); + assert!(bob.node.send_payment_for_bolt12_invoice(&invoice, &context).is_ok()); assert_eq!( - bob.node.send_payment_for_bolt12_invoice(&invoice), + bob.node.send_payment_for_bolt12_invoice(&invoice, &context), Err(Bolt12PaymentError::DuplicateInvoice), ); @@ -1112,7 +1112,7 @@ fn pays_bolt12_invoice_asynchronously() { expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); assert_eq!( - bob.node.send_payment_for_bolt12_invoice(&invoice), + bob.node.send_payment_for_bolt12_invoice(&invoice, &context), Err(Bolt12PaymentError::DuplicateInvoice), ); @@ -1121,7 +1121,7 @@ fn pays_bolt12_invoice_asynchronously() { } assert_eq!( - bob.node.send_payment_for_bolt12_invoice(&invoice), + bob.node.send_payment_for_bolt12_invoice(&invoice, &context), Err(Bolt12PaymentError::UnexpectedInvoice), ); } @@ -1234,6 +1234,346 @@ fn creates_refund_with_blinded_path_using_unannounced_introduction_node() { } } +/// Check that authentication fails when an invoice request is handled using the wrong context +/// (i.e., was sent directly or over an unexpected blinded path). +#[test] +fn fails_authentication_when_handling_invoice_request() { + let mut accept_forward_cfg = test_default_channel_config(); + accept_forward_cfg.accept_forwards_to_priv_channels = true; + + let mut features = channelmanager::provided_init_features(&accept_forward_cfg); + features.set_onion_messages_optional(); + features.set_route_blinding_optional(); + + let chanmon_cfgs = create_chanmon_cfgs(6); + let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); + + *node_cfgs[1].override_init_features.borrow_mut() = Some(features); + + let node_chanmgrs = create_node_chanmgrs( + 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] + ); + let nodes = create_network(6, &node_cfgs, &node_chanmgrs); + + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); + + let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); + let alice_id = alice.node.get_our_node_id(); + let bob_id = bob.node.get_our_node_id(); + let charlie_id = charlie.node.get_our_node_id(); + let david_id = david.node.get_our_node_id(); + + disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); + disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); + + let offer = alice.node + .create_offer_builder(None) + .unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + assert_eq!(offer.metadata(), None); + assert_ne!(offer.signing_pubkey(), Some(alice_id)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); + } + + let invalid_path = alice.node + .create_offer_builder(None) + .unwrap() + .build().unwrap() + .paths().first().unwrap() + .clone(); + assert_eq!(invalid_path.introduction_node, IntroductionNode::NodeId(bob_id)); + + // Send the invoice request directly to Alice instead of using a blinded path. + let payment_id = PaymentId([1; 32]); + david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + .unwrap(); + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); + + connect_peers(david, alice); + #[cfg(not(c_bindings))] { + david.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().destination = + Destination::Node(alice_id); + } + #[cfg(c_bindings)] { + david.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 = + Destination::Node(alice_id); + } + + let onion_message = david.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(&david_id, &onion_message); + + let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + assert_eq!(invoice_request.amount_msats(), None); + assert_ne!(invoice_request.payer_id(), david_id); + assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(charlie_id)); + + assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None); + + david.node.abandon_payment(payment_id); + get_event!(david, Event::InvoiceRequestFailed); + + // Send the invoice request to Alice using an invalid blinded path. + let payment_id = PaymentId([2; 32]); + david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + .unwrap(); + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); + + #[cfg(not(c_bindings))] { + david.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().destination = + Destination::BlindedPath(invalid_path); + } + #[cfg(c_bindings)] { + david.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 = + Destination::BlindedPath(invalid_path); + } + + connect_peers(david, bob); + + let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(&david_id, &onion_message); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(&bob_id, &onion_message); + + let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + assert_eq!(invoice_request.amount_msats(), None); + assert_ne!(invoice_request.payer_id(), david_id); + assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(charlie_id)); + + assert_eq!(alice.onion_messenger.next_onion_message_for_peer(charlie_id), None); +} + +/// Check that authentication fails when an invoice is handled using the wrong context (i.e., was +/// sent over an unexpected blinded path). +#[test] +fn fails_authentication_when_handling_invoice_for_offer() { + let mut accept_forward_cfg = test_default_channel_config(); + accept_forward_cfg.accept_forwards_to_priv_channels = true; + + let mut features = channelmanager::provided_init_features(&accept_forward_cfg); + features.set_onion_messages_optional(); + features.set_route_blinding_optional(); + + let chanmon_cfgs = create_chanmon_cfgs(6); + let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); + + *node_cfgs[1].override_init_features.borrow_mut() = Some(features); + + let node_chanmgrs = create_node_chanmgrs( + 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] + ); + let nodes = create_network(6, &node_cfgs, &node_chanmgrs); + + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); + + let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); + let alice_id = alice.node.get_our_node_id(); + let bob_id = bob.node.get_our_node_id(); + let charlie_id = charlie.node.get_our_node_id(); + let david_id = david.node.get_our_node_id(); + + disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); + disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); + + let offer = alice.node + .create_offer_builder(None) + .unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + assert_ne!(offer.signing_pubkey(), Some(alice_id)); + assert!(!offer.paths().is_empty()); + for path in offer.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id)); + } + + // Initiate an invoice request, but abandon tracking it. + let payment_id = PaymentId([1; 32]); + david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + .unwrap(); + david.node.abandon_payment(payment_id); + get_event!(david, Event::InvoiceRequestFailed); + + // Don't send the invoice request, but grab its reply path to use with a different request. + let invalid_reply_path = { + let mut pending_offers_messages = david.node.pending_offers_messages.lock().unwrap(); + let pending_invoice_request = pending_offers_messages.pop().unwrap(); + pending_offers_messages.clear(); + #[cfg(not(c_bindings))] { + pending_invoice_request.reply_path + } + #[cfg(c_bindings)] { + pending_invoice_request.2 + } + }; + + let payment_id = PaymentId([2; 32]); + david.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None) + .unwrap(); + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); + + // Swap out the reply path to force authentication to fail when handling the invoice since it + // will be sent over the wrong blinded path. + { + let mut pending_offers_messages = david.node.pending_offers_messages.lock().unwrap(); + let mut pending_invoice_request = pending_offers_messages.first_mut().unwrap(); + #[cfg(not(c_bindings))] { + pending_invoice_request.reply_path = invalid_reply_path; + } + #[cfg(c_bindings)] { + pending_invoice_request.2 = invalid_reply_path; + } + } + + connect_peers(david, bob); + + let onion_message = david.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(&david_id, &onion_message); + + connect_peers(alice, charlie); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(&bob_id, &onion_message); + + let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + assert_eq!(invoice_request.amount_msats(), None); + assert_ne!(invoice_request.payer_id(), david_id); + assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(charlie_id)); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); + charlie.onion_messenger.handle_onion_message(&alice_id, &onion_message); + + let onion_message = charlie.onion_messenger.next_onion_message_for_peer(david_id).unwrap(); + david.onion_messenger.handle_onion_message(&charlie_id, &onion_message); + + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); +} + +/// Check that authentication fails when an invoice is handled using the wrong context (i.e., was +/// sent directly or over an unexpected blinded path). +#[test] +fn fails_authentication_when_handling_invoice_for_refund() { + let mut accept_forward_cfg = test_default_channel_config(); + accept_forward_cfg.accept_forwards_to_priv_channels = true; + + let mut features = channelmanager::provided_init_features(&accept_forward_cfg); + features.set_onion_messages_optional(); + features.set_route_blinding_optional(); + + let chanmon_cfgs = create_chanmon_cfgs(6); + let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); + + *node_cfgs[1].override_init_features.borrow_mut() = Some(features); + + let node_chanmgrs = create_node_chanmgrs( + 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] + ); + let nodes = create_network(6, &node_cfgs, &node_chanmgrs); + + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); + + let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); + let alice_id = alice.node.get_our_node_id(); + let charlie_id = charlie.node.get_our_node_id(); + let david_id = david.node.get_our_node_id(); + + disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); + disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); + + let absolute_expiry = Duration::from_secs(u64::MAX); + let payment_id = PaymentId([1; 32]); + let refund = david.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + assert_ne!(refund.payer_id(), david_id); + assert!(!refund.paths().is_empty()); + for path in refund.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(charlie_id)); + } + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); + + // Send the invoice directly to David instead of using a blinded path. + let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + + connect_peers(david, alice); + #[cfg(not(c_bindings))] { + alice.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().destination = + Destination::Node(david_id); + } + #[cfg(c_bindings)] { + alice.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 = + Destination::Node(david_id); + } + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(david_id).unwrap(); + david.onion_messenger.handle_onion_message(&alice_id, &onion_message); + + let (invoice, _) = extract_invoice(david, &onion_message); + assert_eq!(invoice, expected_invoice); + + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); + david.node.abandon_payment(payment_id); + get_event!(david, Event::InvoiceRequestFailed); + + // Send the invoice to David using an invalid blinded path. + let invalid_path = refund.paths().first().unwrap().clone(); + let payment_id = PaymentId([2; 32]); + let refund = david.node + .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None) + .unwrap() + .build().unwrap(); + assert_ne!(refund.payer_id(), david_id); + assert!(!refund.paths().is_empty()); + for path in refund.paths() { + assert_eq!(path.introduction_node, IntroductionNode::NodeId(charlie_id)); + } + + let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); + + #[cfg(not(c_bindings))] { + alice.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().destination = + Destination::BlindedPath(invalid_path); + } + #[cfg(c_bindings)] { + alice.node.pending_offers_messages.lock().unwrap().first_mut().unwrap().1 = + Destination::BlindedPath(invalid_path); + } + + connect_peers(alice, charlie); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap(); + charlie.onion_messenger.handle_onion_message(&alice_id, &onion_message); + + 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); + assert_eq!(invoice, expected_invoice); + + expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); +} + /// Fails creating or paying an offer when a blinded path cannot be created because no peers are /// connected. #[test] diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 3f96e703b2a..e1f30138212 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -119,11 +119,12 @@ use crate::ln::msgs::DecodeError; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; +use crate::offers::nonce::Nonce; use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::refund::{IV_BYTES as REFUND_IV_BYTES, Refund, RefundContents}; -use crate::offers::signer; +use crate::offers::signer::{Metadata, self}; use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, Readable, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -770,12 +771,31 @@ impl Bolt12Invoice { self.tagged_hash.as_digest().as_ref().clone() } - /// Verifies that the invoice was for a request or refund created using the given key. Returns - /// the associated [`PaymentId`] to use when sending the payment. - pub fn verify( + /// Verifies that the invoice was for a request or refund created using the given key by + /// checking the payer metadata from the invoice request. + /// + /// Returns the associated [`PaymentId`] to use when sending the payment. + pub fn verify_using_metadata( &self, key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> Result { - self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx) + let metadata = match &self.contents { + InvoiceContents::ForOffer { invoice_request, .. } => &invoice_request.inner.payer.0, + InvoiceContents::ForRefund { refund, .. } => &refund.payer.0, + }; + self.contents.verify(TlvStream::new(&self.bytes), metadata, key, secp_ctx) + } + + /// Verifies that the invoice was for a request or refund created using the given key by + /// checking a payment id and nonce included with the [`BlindedPath`] for which the invoice was + /// sent through. + pub fn verify_using_payer_data( + &self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> bool { + let metadata = Metadata::payer_data(payment_id, nonce, key); + match self.contents.verify(TlvStream::new(&self.bytes), &metadata, key, secp_ctx) { + Ok(extracted_payment_id) => payment_id == extracted_payment_id, + Err(()) => false, + } } pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { @@ -787,6 +807,13 @@ impl Bolt12Invoice { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, signature_tlv_stream) } + + pub(crate) fn is_for_refund_without_paths(&self) -> bool { + match self.contents { + InvoiceContents::ForOffer { .. } => false, + InvoiceContents::ForRefund { .. } => self.message_paths().is_empty(), + } + } } impl PartialEq for Bolt12Invoice { @@ -999,35 +1026,28 @@ impl InvoiceContents { } fn verify( - &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 + &self, tlv_stream: TlvStream<'_>, metadata: &Metadata, key: &ExpandedKey, + secp_ctx: &Secp256k1 ) -> Result { let offer_records = tlv_stream.clone().range(OFFER_TYPES); let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| { match record.r#type { PAYER_METADATA_TYPE => false, // Should be outside range - INVOICE_REQUEST_PAYER_ID_TYPE => !self.derives_keys(), + INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(), _ => true, } }); let tlv_stream = offer_records.chain(invreq_records); - let (metadata, payer_id, iv_bytes) = match self { - InvoiceContents::ForOffer { invoice_request, .. } => { - (invoice_request.metadata(), invoice_request.payer_id(), INVOICE_REQUEST_IV_BYTES) - }, - InvoiceContents::ForRefund { refund, .. } => { - (refund.metadata(), refund.payer_id(), REFUND_IV_BYTES) - }, + let payer_id = self.payer_id(); + let iv_bytes = match self { + InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES, + InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES, }; - signer::verify_payer_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) - } - - fn derives_keys(&self) -> bool { - match self { - InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.derives_keys(), - InvoiceContents::ForRefund { refund, .. } => refund.derives_keys(), - } + signer::verify_payer_metadata( + metadata.as_ref(), key, iv_bytes, payer_id, tlv_stream, secp_ctx, + ) } fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { @@ -1416,6 +1436,7 @@ mod tests { use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; + use crate::offers::nonce::Nonce; use crate::offers::offer::{Amount, OfferTlvStreamRef, Quantity}; use crate::prelude::*; #[cfg(not(c_bindings))] @@ -1752,6 +1773,7 @@ mod tests { let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let blinded_path = BlindedPath { @@ -1765,8 +1787,7 @@ mod tests { #[cfg(c_bindings)] use crate::offers::offer::OfferWithDerivedMetadataBuilder as OfferBuilder; - let offer = OfferBuilder - ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) .path(blinded_path) .build().unwrap(); @@ -1775,7 +1796,7 @@ mod tests { .sign(payer_sign).unwrap(); if let Err(e) = invoice_request.clone() - .verify(&expanded_key, &secp_ctx).unwrap() + .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap() .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()).unwrap() .build_and_sign(&secp_ctx) { @@ -1783,10 +1804,11 @@ mod tests { } let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32])); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); + assert!( + invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).is_err() + ); - let offer = OfferBuilder - ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) // Omit the path so that node_id is used for the signing pubkey instead of deriving .build().unwrap(); @@ -1795,7 +1817,7 @@ mod tests { .sign(payer_sign).unwrap(); match invoice_request - .verify(&expanded_key, &secp_ctx).unwrap() + .verify_using_metadata(&expanded_key, &secp_ctx).unwrap() .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()) { Ok(_) => panic!("expected error"), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index cdf94a2e80d..4ad99645002 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -61,17 +61,16 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; -use core::ops::Deref; -use crate::sign::EntropySource; use crate::io; use crate::blinded_path::BlindedPath; use crate::ln::types::PaymentHash; use crate::ln::channelmanager::PaymentId; use crate::ln::features::InvoiceRequestFeatures; -use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::DecodeError; use crate::offers::invoice::BlindedPayInfo; use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self}; +use crate::offers::nonce::Nonce; use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -170,11 +169,10 @@ macro_rules! invoice_request_explicit_payer_id_builder_methods { ($self: ident, } #[cfg_attr(c_bindings, allow(dead_code))] - pub(super) fn deriving_metadata( - offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + pub(super) fn deriving_metadata( + offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, nonce: Nonce, payment_id: PaymentId, - ) -> Self where ES::Target: EntropySource { - let nonce = Nonce::from_entropy_source(entropy_source); + ) -> Self { let payment_id = Some(payment_id); let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id); let metadata = Metadata::Derived(derivation_material); @@ -200,11 +198,10 @@ macro_rules! invoice_request_derived_payer_id_builder_methods { ( $self: ident, $self_type: ty, $secp_context: ty ) => { #[cfg_attr(c_bindings, allow(dead_code))] - pub(super) fn deriving_payer_id( - offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, + pub(super) fn deriving_payer_id( + offer: &'a Offer, expanded_key: &ExpandedKey, nonce: Nonce, secp_ctx: &'b Secp256k1<$secp_context>, payment_id: PaymentId - ) -> Self where ES::Target: EntropySource { - let nonce = Nonce::from_entropy_source(entropy_source); + ) -> Self { let payment_id = Some(payment_id); let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id); let metadata = Metadata::DerivedSigningPubkey(derivation_material); @@ -604,15 +601,16 @@ pub struct InvoiceRequest { signature: Signature, } -/// An [`InvoiceRequest`] that has been verified by [`InvoiceRequest::verify`] and exposes different -/// ways to respond depending on whether the signing keys were derived. +/// An [`InvoiceRequest`] that has been verified by [`InvoiceRequest::verify_using_metadata`] or +/// [`InvoiceRequest::verify_using_recipient_data`] and exposes different ways to respond depending +/// on whether the signing keys were derived. #[derive(Clone, Debug)] pub struct VerifiedInvoiceRequest { /// The identifier of the [`Offer`] for which the [`InvoiceRequest`] was made. pub offer_id: OfferId, /// The verified request. - inner: InvoiceRequest, + pub(crate) inner: InvoiceRequest, /// Keys used for signing a [`Bolt12Invoice`] if they can be derived. /// @@ -638,7 +636,7 @@ pub(super) struct InvoiceRequestContents { #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] pub(super) struct InvoiceRequestContentsWithoutPayerId { - payer: PayerContents, + pub(super) payer: PayerContents, pub(super) offer: OfferContents, chain: Option, amount_msats: Option, @@ -736,7 +734,9 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( /// # Note /// /// If the originating [`Offer`] was created using [`OfferBuilder::deriving_signing_pubkey`], - /// then use [`InvoiceRequest::verify`] and [`VerifiedInvoiceRequest`] methods instead. + /// then first use [`InvoiceRequest::verify_using_metadata`] or + /// [`InvoiceRequest::verify_using_recipient_data`] and then [`VerifiedInvoiceRequest`] methods + /// instead. /// /// [`Bolt12Invoice::created_at`]: crate::offers::invoice::Bolt12Invoice::created_at /// [`OfferBuilder::deriving_signing_pubkey`]: crate::offers::offer::OfferBuilder::deriving_signing_pubkey @@ -773,12 +773,14 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { ( } } macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => { - /// Verifies that the request was for an offer created using the given key. Returns the verified - /// request which contains the derived keys needed to sign a [`Bolt12Invoice`] for the request - /// if they could be extracted from the metadata. + /// Verifies that the request was for an offer created using the given key by checking the + /// metadata from the offer. + /// + /// Returns the verified request which contains the derived keys needed to sign a + /// [`Bolt12Invoice`] for the request if they could be extracted from the metadata. /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - pub fn verify< + pub fn verify_using_metadata< #[cfg(not(c_bindings))] T: secp256k1::Signing >( @@ -788,7 +790,8 @@ macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => { #[cfg(c_bindings)] secp_ctx: &Secp256k1, ) -> Result { - let (offer_id, keys) = $self.contents.inner.offer.verify(&$self.bytes, key, secp_ctx)?; + let (offer_id, keys) = + $self.contents.inner.offer.verify_using_metadata(&$self.bytes, key, secp_ctx)?; Ok(VerifiedInvoiceRequest { offer_id, #[cfg(not(c_bindings))] @@ -799,6 +802,35 @@ macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => { }) } + /// Verifies that the request was for an offer created using the given key by checking a nonce + /// included with the [`BlindedPath`] for which the request was sent through. + /// + /// Returns the verified request which contains the derived keys needed to sign a + /// [`Bolt12Invoice`] for the request if they could be extracted from the metadata. + /// + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + pub fn verify_using_recipient_data< + #[cfg(not(c_bindings))] + T: secp256k1::Signing + >( + $self: $self_type, nonce: Nonce, key: &ExpandedKey, + #[cfg(not(c_bindings))] + secp_ctx: &Secp256k1, + #[cfg(c_bindings)] + secp_ctx: &Secp256k1, + ) -> Result { + let (offer_id, keys) = $self.contents.inner.offer.verify_using_recipient_data( + &$self.bytes, nonce, key, secp_ctx + )?; + Ok(VerifiedInvoiceRequest { + offer_id, + #[cfg(not(c_bindings))] + inner: $self, + #[cfg(c_bindings)] + inner: $self.clone(), + keys, + }) + } } } #[cfg(not(c_bindings))] @@ -921,10 +953,6 @@ impl InvoiceRequestContents { self.inner.metadata() } - pub(super) fn derives_keys(&self) -> bool { - self.inner.payer.0.derives_payer_keys() - } - pub(super) fn chain(&self) -> ChainHash { self.inner.chain() } @@ -1216,6 +1244,7 @@ mod tests { use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; + use crate::offers::nonce::Nonce; use crate::offers::offer::{Amount, OfferTlvStreamRef, Quantity}; #[cfg(not(c_bindings))] use { @@ -1366,6 +1395,7 @@ mod tests { let payer_id = payer_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); @@ -1373,7 +1403,7 @@ mod tests { .amount_msats(1000) .build().unwrap(); let invoice_request = offer - .request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy, payment_id) + .request_invoice_deriving_metadata(payer_id, &expanded_key, nonce, payment_id) .unwrap() .build().unwrap() .sign(payer_sign).unwrap(); @@ -1383,10 +1413,11 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - match invoice.verify(&expanded_key, &secp_ctx) { + match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered fields let ( @@ -1409,7 +1440,7 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered metadata let ( @@ -1432,13 +1463,14 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); } #[test] fn builds_invoice_request_with_derived_payer_id() { let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); @@ -1446,7 +1478,7 @@ mod tests { .amount_msats(1000) .build().unwrap(); let invoice_request = offer - .request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx, payment_id) + .request_invoice_deriving_payer_id(&expanded_key, nonce, &secp_ctx, payment_id) .unwrap() .build_and_sign() .unwrap(); @@ -1455,10 +1487,8 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - match invoice.verify(&expanded_key, &secp_ctx) { - Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), - Err(()) => panic!("verification failed"), - } + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); + assert!(invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered fields let ( @@ -1481,7 +1511,7 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered payer id let ( @@ -1504,7 +1534,7 @@ mod tests { signature_tlv_stream.write(&mut encoded_invoice).unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); } #[test] @@ -2273,12 +2303,12 @@ mod tests { let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); #[cfg(c_bindings)] use crate::offers::offer::OfferWithDerivedMetadataBuilder as OfferBuilder; - let offer = OfferBuilder - ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .chain(Network::Testnet) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) @@ -2291,7 +2321,7 @@ mod tests { .payer_note("0".repeat(PAYER_NOTE_LIMIT * 2)) .build().unwrap() .sign(payer_sign).unwrap(); - match invoice_request.verify(&expanded_key, &secp_ctx) { + match invoice_request.verify_using_metadata(&expanded_key, &secp_ctx) { Ok(invoice_request) => { let fields = invoice_request.fields(); assert_eq!(invoice_request.offer_id, offer.id()); diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index e5e894e2a12..e4fe7d789db 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -20,6 +20,7 @@ pub mod invoice_error; mod invoice_macros; pub mod invoice_request; pub mod merkle; +pub mod nonce; pub mod parse; mod payer; pub mod refund; diff --git a/lightning/src/offers/nonce.rs b/lightning/src/offers/nonce.rs new file mode 100644 index 00000000000..1dd21e6c83d --- /dev/null +++ b/lightning/src/offers/nonce.rs @@ -0,0 +1,79 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! A number used only once. + +use crate::io::{self, Read}; +use crate::ln::msgs::DecodeError; +use crate::sign::EntropySource; +use crate::util::ser::{Readable, Writeable, Writer}; +use core::ops::Deref; + +#[allow(unused_imports)] +use crate::prelude::*; + +/// A 128-bit number used only once. +/// +/// Needed when constructing [`Offer::metadata`] and deriving [`Offer::signing_pubkey`] from +/// [`ExpandedKey`]. Must not be reused for any other derivation without first hashing. +/// +/// [`Offer::metadata`]: crate::offers::offer::Offer::metadata +/// [`Offer::signing_pubkey`]: crate::offers::offer::Offer::signing_pubkey +/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Nonce(pub(crate) [u8; Self::LENGTH]); + +impl Nonce { + /// Number of bytes in the nonce. + pub const LENGTH: usize = 16; + + /// Creates a `Nonce` from the given [`EntropySource`]. + pub fn from_entropy_source(entropy_source: ES) -> Self + where + ES::Target: EntropySource, + { + let mut bytes = [0u8; Self::LENGTH]; + let rand_bytes = entropy_source.get_secure_random_bytes(); + bytes.copy_from_slice(&rand_bytes[..Self::LENGTH]); + + Nonce(bytes) + } + + /// Returns a slice of the underlying bytes of size [`Nonce::LENGTH`]. + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} + +impl TryFrom<&[u8]> for Nonce { + type Error = (); + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != Self::LENGTH { + return Err(()); + } + + let mut copied_bytes = [0u8; Self::LENGTH]; + copied_bytes.copy_from_slice(bytes); + + Ok(Self(copied_bytes)) + } +} + +impl Writeable for Nonce { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w) + } +} + +impl Readable for Nonce { + fn read(r: &mut R) -> Result { + Ok(Nonce(Readable::read(r)?)) + } +} diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 253de8652bb..29220125f66 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -82,17 +82,16 @@ use bitcoin::network::Network; use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, self}; use core::hash::{Hash, Hasher}; use core::num::NonZeroU64; -use core::ops::Deref; use core::str::FromStr; use core::time::Duration; -use crate::sign::EntropySource; use crate::io; use crate::blinded_path::BlindedPath; use crate::ln::channelmanager::PaymentId; use crate::ln::features::OfferFeatures; -use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::merkle::{TaggedHash, TlvStream}; +use crate::offers::nonce::Nonce; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::signer::{Metadata, MetadataMaterial, self}; use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; @@ -242,21 +241,23 @@ macro_rules! offer_explicit_metadata_builder_methods { ( macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { /// Similar to [`OfferBuilder::new`] except, if [`OfferBuilder::path`] is called, the signing - /// pubkey is derived from the given [`ExpandedKey`] and [`EntropySource`]. This provides - /// recipient privacy by using a different signing pubkey for each offer. Otherwise, the - /// provided `node_id` is used for the signing pubkey. + /// pubkey is derived from the given [`ExpandedKey`] and [`Nonce`]. This provides recipient + /// privacy by using a different signing pubkey for each offer. Otherwise, the provided + /// `node_id` is used for the signing pubkey. /// /// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used by - /// [`InvoiceRequest::verify`] to determine if the request was produced for the offer given an - /// [`ExpandedKey`]. + /// [`InvoiceRequest::verify_using_metadata`] to determine if the request was produced for the + /// offer given an [`ExpandedKey`]. However, if [`OfferBuilder::path`] is called, then the + /// metadata will not be set and must be included in each [`BlindedPath`] instead. In this case, + /// use [`InvoiceRequest::verify_using_recipient_data`]. /// - /// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify + /// [`InvoiceRequest::verify_using_metadata`]: crate::offers::invoice_request::InvoiceRequest::verify_using_metadata + /// [`InvoiceRequest::verify_using_recipient_data`]: crate::offers::invoice_request::InvoiceRequest::verify_using_recipient_data /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey - pub fn deriving_signing_pubkey( - node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + pub fn deriving_signing_pubkey( + node_id: PublicKey, expanded_key: &ExpandedKey, nonce: Nonce, secp_ctx: &'a Secp256k1<$secp_context> - ) -> Self where ES::Target: EntropySource { - let nonce = Nonce::from_entropy_source(entropy_source); + ) -> Self { let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, None); let metadata = Metadata::DerivedSigningPubkey(derivation_material); Self { @@ -384,9 +385,12 @@ macro_rules! offer_builder_methods { ( } fn build_without_checks($($self_mut)* $self: $self_type) -> Offer { - // Create the metadata for stateless verification of an InvoiceRequest. if let Some(mut metadata) = $self.offer.metadata.take() { + // Create the metadata for stateless verification of an InvoiceRequest. if metadata.has_derivation_material() { + + // Don't derive keys if no blinded paths were given since this means the signing + // pubkey must be the node id of an announced node. if $self.offer.paths.is_none() { metadata = metadata.without_keys(); } @@ -398,14 +402,17 @@ macro_rules! offer_builder_methods { ( tlv_stream.node_id = None; } + // Either replace the signing pubkey with the derived pubkey or include the metadata + // for verification. In the former case, the blinded paths must include + // `OffersContext::InvoiceRequest` instead. let (derived_metadata, keys) = metadata.derive_from(tlv_stream, $self.secp_ctx); - metadata = derived_metadata; - if let Some(keys) = keys { - $self.offer.signing_pubkey = Some(keys.public_key()); + match keys { + Some(keys) => $self.offer.signing_pubkey = Some(keys.public_key()), + None => $self.offer.metadata = Some(derived_metadata), } + } else { + $self.offer.metadata = Some(metadata); } - - $self.offer.metadata = Some(metadata); } let mut bytes = Vec::new(); @@ -667,9 +674,9 @@ impl Offer { #[cfg(async_payments)] pub(super) fn verify( - &self, key: &ExpandedKey, secp_ctx: &Secp256k1 + &self, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> Result<(OfferId, Option), ()> { - self.contents.verify(&self.bytes, key, secp_ctx) + self.contents.verify_using_recipient_data(&self.bytes, nonce, key, secp_ctx) } } @@ -678,8 +685,9 @@ macro_rules! request_invoice_derived_payer_id { ($self: ident, $builder: ty) => /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each /// request, /// - sets [`InvoiceRequest::payer_metadata`] when [`InvoiceRequestBuilder::build`] is called - /// such that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice was - /// requested using a base [`ExpandedKey`] from which the payer id was derived, and + /// such that it can be used by [`Bolt12Invoice::verify_using_metadata`] to determine if the + /// invoice was requested using a base [`ExpandedKey`] from which the payer id was derived, + /// and /// - includes the [`PaymentId`] encrypted in [`InvoiceRequest::payer_metadata`] so that it can /// be used when sending the payment for the requested invoice. /// @@ -687,28 +695,25 @@ macro_rules! request_invoice_derived_payer_id { ($self: ident, $builder: ty) => /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id /// [`InvoiceRequest::payer_metadata`]: crate::offers::invoice_request::InvoiceRequest::payer_metadata - /// [`Bolt12Invoice::verify`]: crate::offers::invoice::Bolt12Invoice::verify + /// [`Bolt12Invoice::verify_using_metadata`]: crate::offers::invoice::Bolt12Invoice::verify_using_metadata /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn request_invoice_deriving_payer_id< - 'a, 'b, ES: Deref, + 'a, 'b, #[cfg(not(c_bindings))] T: secp256k1::Signing >( - &'a $self, expanded_key: &ExpandedKey, entropy_source: ES, + &'a $self, expanded_key: &ExpandedKey, nonce: Nonce, #[cfg(not(c_bindings))] secp_ctx: &'b Secp256k1, #[cfg(c_bindings)] secp_ctx: &'b Secp256k1, payment_id: PaymentId - ) -> Result<$builder, Bolt12SemanticError> - where - ES::Target: EntropySource, - { + ) -> Result<$builder, Bolt12SemanticError> { if $self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - Ok(<$builder>::deriving_payer_id($self, expanded_key, entropy_source, secp_ctx, payment_id)) + Ok(<$builder>::deriving_payer_id($self, expanded_key, nonce, secp_ctx, payment_id)) } } } @@ -719,18 +724,15 @@ macro_rules! request_invoice_explicit_payer_id { ($self: ident, $builder: ty) => /// Useful for recurring payments using the same `payer_id` with different invoices. /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id - pub fn request_invoice_deriving_metadata( - &$self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + pub fn request_invoice_deriving_metadata( + &$self, payer_id: PublicKey, expanded_key: &ExpandedKey, nonce: Nonce, payment_id: PaymentId - ) -> Result<$builder, Bolt12SemanticError> - where - ES::Target: EntropySource, - { + ) -> Result<$builder, Bolt12SemanticError> { if $self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - Ok(<$builder>::deriving_metadata($self, payer_id, expanded_key, entropy_source, payment_id)) + Ok(<$builder>::deriving_metadata($self, payer_id, expanded_key, nonce, payment_id)) } /// Creates an [`InvoiceRequestBuilder`] for the offer with the given `metadata` and `payer_id`, @@ -913,18 +915,28 @@ impl OfferContents { self.signing_pubkey } - /// Verifies that the offer metadata was produced from the offer in the TLV stream. - pub(super) fn verify( + pub(super) fn verify_using_metadata( &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> Result<(OfferId, Option), ()> { - match self.metadata() { + self.verify(bytes, self.metadata.as_ref(), key, secp_ctx) + } + + pub(super) fn verify_using_recipient_data( + &self, bytes: &[u8], nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result<(OfferId, Option), ()> { + self.verify(bytes, Some(&Metadata::RecipientData(nonce)), key, secp_ctx) + } + + /// Verifies that the offer metadata was produced from the offer in the TLV stream. + fn verify( + &self, bytes: &[u8], metadata: Option<&Metadata>, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result<(OfferId, Option), ()> { + match metadata { Some(metadata) => { let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| { match record.r#type { OFFER_METADATA_TYPE => false, - OFFER_NODE_ID_TYPE => { - !self.metadata.as_ref().unwrap().derives_recipient_keys() - }, + OFFER_NODE_ID_TYPE => !metadata.derives_recipient_keys(), _ => true, } }); @@ -933,7 +945,7 @@ impl OfferContents { None => return Err(()), }; let keys = signer::verify_recipient_metadata( - metadata, key, IV_BYTES, signing_pubkey, tlv_stream, secp_ctx + metadata.as_ref(), key, IV_BYTES, signing_pubkey, tlv_stream, secp_ctx )?; let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes); @@ -1163,6 +1175,7 @@ mod tests { use crate::ln::features::OfferFeatures; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; + use crate::offers::nonce::Nonce; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::test_utils::*; use crate::util::ser::{BigSize, Writeable}; @@ -1277,24 +1290,33 @@ mod tests { let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); #[cfg(c_bindings)] use super::OfferWithDerivedMetadataBuilder as OfferBuilder; - let offer = OfferBuilder - ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) .build().unwrap(); + assert!(offer.metadata().is_some()); assert_eq!(offer.signing_pubkey(), Some(node_id)); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - match invoice_request.verify(&expanded_key, &secp_ctx) { + match invoice_request.verify_using_metadata(&expanded_key, &secp_ctx) { Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), Err(_) => panic!("unexpected error"), } + // Fails verification when using the wrong method + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!( + invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).is_err() + ); + // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); tlv_stream.amount = Some(100); @@ -1306,7 +1328,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); + assert!(invoice_request.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered metadata let mut tlv_stream = offer.as_tlv_stream(); @@ -1320,7 +1342,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); + assert!(invoice_request.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); } #[test] @@ -1328,6 +1350,7 @@ mod tests { let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let blinded_path = BlindedPath { @@ -1341,21 +1364,27 @@ mod tests { #[cfg(c_bindings)] use super::OfferWithDerivedMetadataBuilder as OfferBuilder; - let offer = OfferBuilder - ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .amount_msats(1000) .path(blinded_path) .build().unwrap(); + assert!(offer.metadata().is_none()); assert_ne!(offer.signing_pubkey(), Some(node_id)); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - match invoice_request.verify(&expanded_key, &secp_ctx) { + match invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx) { Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), Err(_) => panic!("unexpected error"), } + // Fails verification when using the wrong method + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + assert!(invoice_request.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); + // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); tlv_stream.amount = Some(100); @@ -1367,7 +1396,9 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); + assert!( + invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).is_err() + ); // Fails verification with altered signing pubkey let mut tlv_stream = offer.as_tlv_stream(); @@ -1381,7 +1412,9 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); + assert!( + invoice_request.verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).is_err() + ); } #[test] diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 624036d1957..9cfa3147c63 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -95,10 +95,11 @@ use crate::blinded_path::BlindedPath; use crate::ln::types::PaymentHash; use crate::ln::channelmanager::PaymentId; use crate::ln::features::InvoiceRequestFeatures; -use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::BlindedPayInfo; use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::nonce::Nonce; use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -188,23 +189,26 @@ macro_rules! refund_builder_methods { ( /// different payer id for each refund, assuming a different nonce is used. Otherwise, the /// provided `node_id` is used for the payer id. /// - /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used to - /// verify that an [`InvoiceRequest`] was produced for the refund given an [`ExpandedKey`]. + /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used by + /// [`Bolt12Invoice::verify_using_metadata`] to determine if the invoice was produced for the + /// refund given an [`ExpandedKey`]. However, if [`RefundBuilder::path`] is called, then the + /// metadata must be included in each [`BlindedPath`] instead. In this case, use + /// [`Bolt12Invoice::verify_using_payer_data`]. /// /// The `payment_id` is encrypted in the metadata and should be unique. This ensures that only /// one invoice will be paid for the refund and that payments can be uniquely identified. /// - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Bolt12Invoice::verify_using_metadata`]: crate::offers::invoice::Bolt12Invoice::verify_using_metadata + /// [`Bolt12Invoice::verify_using_payer_data`]: crate::offers::invoice::Bolt12Invoice::verify_using_payer_data /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey - pub fn deriving_payer_id( - node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + pub fn deriving_payer_id( + node_id: PublicKey, expanded_key: &ExpandedKey, nonce: Nonce, secp_ctx: &'a Secp256k1<$secp_context>, amount_msats: u64, payment_id: PaymentId - ) -> Result where ES::Target: EntropySource { + ) -> Result { if amount_msats > MAX_VALUE_MSAT { return Err(Bolt12SemanticError::InvalidAmount); } - let nonce = Nonce::from_entropy_source(entropy_source); let payment_id = Some(payment_id); let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id); let metadata = Metadata::DerivedSigningPubkey(derivation_material); @@ -415,7 +419,7 @@ pub struct Refund { #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] pub(super) struct RefundContents { - payer: PayerContents, + pub(super) payer: PayerContents, // offer fields description: String, absolute_expiry: Option, @@ -727,10 +731,6 @@ impl RefundContents { self.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) } - pub(super) fn derives_keys(&self) -> bool { - self.payer.0.derives_payer_keys() - } - pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef { let payer = PayerTlvStreamRef { metadata: self.payer.0.as_bytes(), @@ -939,6 +939,7 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; + use crate::offers::nonce::Nonce; use crate::offers::offer::OfferTlvStreamRef; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; @@ -1028,11 +1029,12 @@ mod tests { let node_id = payer_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); let refund = RefundBuilder - ::deriving_payer_id(node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) + ::deriving_payer_id(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id) .unwrap() .build().unwrap(); assert_eq!(refund.payer_id(), node_id); @@ -1043,10 +1045,11 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - match invoice.verify(&expanded_key, &secp_ctx) { + match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); let mut tlv_stream = refund.as_tlv_stream(); tlv_stream.2.amount = Some(2000); @@ -1059,7 +1062,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered metadata let mut tlv_stream = refund.as_tlv_stream(); @@ -1074,7 +1077,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); } #[test] @@ -1082,6 +1085,7 @@ mod tests { let node_id = payer_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); @@ -1095,7 +1099,7 @@ mod tests { }; let refund = RefundBuilder - ::deriving_payer_id(node_id, &expanded_key, &entropy, &secp_ctx, 1000, payment_id) + ::deriving_payer_id(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id) .unwrap() .path(blinded_path) .build().unwrap(); @@ -1106,10 +1110,8 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - match invoice.verify(&expanded_key, &secp_ctx) { - Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), - Err(()) => panic!("verification failed"), - } + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); + assert!(invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered fields let mut tlv_stream = refund.as_tlv_stream(); @@ -1123,7 +1125,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); // Fails verification with altered payer_id let mut tlv_stream = refund.as_tlv_stream(); @@ -1138,7 +1140,7 @@ mod tests { .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); - assert!(invoice.verify(&expanded_key, &secp_ctx).is_err()); + assert!(!invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx)); } #[test] diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index fff0514564c..2e12a17056e 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -16,8 +16,9 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey, self}; use core::fmt; use crate::ln::channelmanager::PaymentId; -use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; +use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; use crate::offers::merkle::TlvRecord; +use crate::offers::nonce::Nonce; use crate::util::ser::Writeable; use crate::prelude::*; @@ -40,27 +41,54 @@ const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16]; #[derive(Clone)] pub(super) enum Metadata { /// Metadata as parsed, supplied by the user, or derived from the message contents. + /// + /// This is the terminal variant; any `Metadata` in a created message will always use this. Bytes(Vec), + /// Metadata for deriving keys included as recipient data in a blinded path. + /// + /// This variant should only be used at verification time, never when building. + RecipientData(Nonce), + + /// Metadata for deriving keys included as payer data in a blinded path. + /// + /// This variant should only be used at verification time, never when building. + PayerData([u8; PaymentId::LENGTH + Nonce::LENGTH]), + /// Metadata to be derived from message contents and given material. + /// + /// This variant should only be used at building time. Derived(MetadataMaterial), /// Metadata and signing pubkey to be derived from message contents and given material. + /// + /// This variant should only be used at building time. DerivedSigningPubkey(MetadataMaterial), } impl Metadata { + pub fn payer_data(payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey) -> Self { + let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); + + let mut bytes = [0u8; PaymentId::LENGTH + Nonce::LENGTH]; + bytes[..PaymentId::LENGTH].copy_from_slice(encrypted_payment_id.as_slice()); + bytes[PaymentId::LENGTH..].copy_from_slice(nonce.as_slice()); + + Metadata::PayerData(bytes) + } + pub fn as_bytes(&self) -> Option<&Vec> { match self { Metadata::Bytes(bytes) => Some(bytes), - Metadata::Derived(_) => None, - Metadata::DerivedSigningPubkey(_) => None, + _ => { debug_assert!(false); None }, } } pub fn has_derivation_material(&self) -> bool { match self { Metadata::Bytes(_) => false, + Metadata::RecipientData(_) => { debug_assert!(false); false }, + Metadata::PayerData(_) => { debug_assert!(false); false }, Metadata::Derived(_) => true, Metadata::DerivedSigningPubkey(_) => true, } @@ -74,6 +102,8 @@ impl Metadata { // derived, as wouldn't be the case if a Metadata::Bytes with length PaymentId::LENGTH + // Nonce::LENGTH had been set explicitly. Metadata::Bytes(bytes) => bytes.len() == PaymentId::LENGTH + Nonce::LENGTH, + Metadata::RecipientData(_) => false, + Metadata::PayerData(_) => true, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -87,14 +117,23 @@ impl Metadata { // derived, as wouldn't be the case if a Metadata::Bytes with length Nonce::LENGTH had // been set explicitly. Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH, + Metadata::RecipientData(_) => true, + Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } } + /// Indicates that signing keys should not be derived when calling [`derive_from`]. Only + /// applicable to state [`Metadata::DerivedSigningPubkey`]; calling this in other states will + /// result in no change. + /// + /// [`derive_from`]: Self::derive_from pub fn without_keys(self) -> Self { match self { Metadata::Bytes(_) => self, + Metadata::RecipientData(_) => { debug_assert!(false); self }, + Metadata::PayerData(_) => { debug_assert!(false); self }, Metadata::Derived(_) => self, Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material), } @@ -105,6 +144,8 @@ impl Metadata { ) -> (Self, Option) { match self { Metadata::Bytes(_) => (self, None), + Metadata::RecipientData(_) => { debug_assert!(false); (self, None) }, + Metadata::PayerData(_) => { debug_assert!(false); (self, None) }, Metadata::Derived(mut metadata_material) => { tlv_stream.write(&mut metadata_material.hmac).unwrap(); (Metadata::Bytes(metadata_material.derive_metadata()), None) @@ -125,10 +166,24 @@ impl Default for Metadata { } } +impl AsRef<[u8]> for Metadata { + fn as_ref(&self) -> &[u8] { + match self { + Metadata::Bytes(bytes) => &bytes, + Metadata::RecipientData(nonce) => &nonce.0, + Metadata::PayerData(bytes) => bytes.as_slice(), + Metadata::Derived(_) => { debug_assert!(false); &[] }, + Metadata::DerivedSigningPubkey(_) => { debug_assert!(false); &[] }, + } + } +} + impl fmt::Debug for Metadata { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Metadata::Bytes(bytes) => bytes.fmt(f), + Metadata::RecipientData(Nonce(bytes)) => bytes.fmt(f), + Metadata::PayerData(bytes) => bytes.fmt(f), Metadata::Derived(_) => f.write_str("Derived"), Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"), } @@ -144,6 +199,8 @@ impl PartialEq for Metadata { } else { false }, + Metadata::RecipientData(_) => false, + Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => false, } @@ -192,8 +249,7 @@ impl MetadataMaterial { self.hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT); self.maybe_include_encrypted_payment_id(); - let mut bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or(vec![]); - bytes.extend_from_slice(self.nonce.as_slice()); + let bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or(vec![]); let hmac = Hmac::from_engine(self.hmac); let privkey = SecretKey::from_slice(hmac.as_byte_array()).unwrap(); diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 69f4073e678..555b878a670 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -22,6 +22,7 @@ use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_me use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, }; +use crate::offers::nonce::Nonce; use crate::offers::offer::{ Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, }; @@ -97,7 +98,7 @@ impl<'a> StaticInvoiceBuilder<'a> { pub fn for_offer_using_derived_keys( offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, message_paths: Vec, created_at: Duration, expanded_key: &ExpandedKey, - secp_ctx: &Secp256k1, + nonce: Nonce, secp_ctx: &Secp256k1, ) -> Result { if offer.chains().len() > 1 { return Err(Bolt12SemanticError::UnexpectedChain); @@ -111,7 +112,7 @@ impl<'a> StaticInvoiceBuilder<'a> { offer.signing_pubkey().ok_or(Bolt12SemanticError::MissingSigningPubkey)?; let keys = offer - .verify(&expanded_key, &secp_ctx) + .verify(nonce, &expanded_key, &secp_ctx) .map_err(|()| Bolt12SemanticError::InvalidMetadata)? .1 .ok_or(Bolt12SemanticError::MissingSigningPubkey)?; @@ -565,6 +566,7 @@ mod tests { use crate::offers::invoice::InvoiceTlvStreamRef; use crate::offers::merkle; use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash}; + use crate::offers::nonce::Nonce; use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::static_invoice::{ @@ -608,13 +610,13 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); - let offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) - .path(blinded_path()) - .build() - .unwrap(); + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -622,6 +624,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) .unwrap() @@ -647,13 +650,13 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); - let offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) - .path(blinded_path()) - .build() - .unwrap(); + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -661,6 +664,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) .unwrap() @@ -671,7 +675,7 @@ mod tests { invoice.write(&mut buffer).unwrap(); assert_eq!(invoice.bytes, buffer.as_slice()); - assert!(invoice.metadata().is_some()); + assert_eq!(invoice.metadata(), None); assert_eq!(invoice.amount(), None); assert_eq!(invoice.description(), None); assert_eq!(invoice.offer_features(), &OfferFeatures::empty()); @@ -697,13 +701,12 @@ mod tests { ); let paths = vec![blinded_path()]; - let metadata = vec![42; 16]; assert_eq!( invoice.as_tlv_stream(), ( OfferTlvStreamRef { chains: None, - metadata: Some(&metadata), + metadata: None, currency: None, amount: None, description: None, @@ -742,13 +745,14 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); let valid_offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .absolute_expiry(future_expiry) .build() @@ -760,6 +764,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) .unwrap() @@ -769,7 +774,7 @@ mod tests { assert_eq!(invoice.absolute_expiry(), Some(future_expiry)); let expired_offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .absolute_expiry(past_expiry) .build() @@ -780,6 +785,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) .unwrap() @@ -797,10 +803,11 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let valid_offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .build() .unwrap(); @@ -812,6 +819,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) { assert_eq!(e, Bolt12SemanticError::MissingPaths); @@ -826,6 +834,7 @@ mod tests { Vec::new(), now, &expanded_key, + nonce, &secp_ctx, ) { assert_eq!(e, Bolt12SemanticError::MissingPaths); @@ -846,6 +855,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) { assert_eq!(e, Bolt12SemanticError::MissingPaths); @@ -860,10 +870,11 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let valid_offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .build() .unwrap(); @@ -882,6 +893,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) { assert_eq!(e, Bolt12SemanticError::MissingSigningPubkey); @@ -902,6 +914,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) { assert_eq!(e, Bolt12SemanticError::InvalidMetadata); @@ -916,10 +929,11 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); let offer_with_extra_chain = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) + OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) .path(blinded_path()) .chain(Network::Bitcoin) .chain(Network::Testnet) @@ -932,6 +946,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) { assert_eq!(e, Bolt12SemanticError::UnexpectedChain); @@ -947,13 +962,13 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); - let offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) - .path(blinded_path()) - .build() - .unwrap(); + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); const TEST_RELATIVE_EXPIRY: u32 = 3600; let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( @@ -962,6 +977,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) .unwrap() @@ -988,13 +1004,13 @@ mod tests { let now = now(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); let secp_ctx = Secp256k1::new(); - let offer = - OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) - .path(blinded_path()) - .build() - .unwrap(); + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( &offer, @@ -1002,6 +1018,7 @@ mod tests { vec![blinded_path()], now, &expanded_key, + nonce, &secp_ctx, ) .unwrap() diff --git a/pending_changelog/3139-bolt12-compatability.txt b/pending_changelog/3139-bolt12-compatability.txt new file mode 100644 index 00000000000..7907ccf6b1f --- /dev/null +++ b/pending_changelog/3139-bolt12-compatability.txt @@ -0,0 +1,10 @@ +## Backwards Compatibility + * BOLT12 `Offers` created in prior versions are still valid but are at risk of + de-anonymization attacks. + * BOLT12 outbound payments in state `RecentPaymentDetails::AwaitingInvoice` are + considered invalid by `ChannelManager`. Any invoices received through the + corresponding `InvoiceRequest` reply path will be ignored. + * BOLT12 `Refund`s created in prior versions with non-empty `Refund::paths` are + considered invalid by `ChannelManager`. Any invoices sent through those + blinded paths will be ignored. `Refund`'s without blinded paths are + unaffected.