diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 8b41c53a635..1439e11da6c 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -326,7 +326,7 @@ impl NodeSigner for KeyProvider { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { #[rustfmt::skip] let random_bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, self.node_secret[31]]; ExpandedKey::new(random_bytes) diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 5281a933526..435bb24dae1 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -405,7 +405,7 @@ impl NodeSigner for KeyProvider { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { self.inbound_payment_key } diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index a5782dacd42..fd3294fab28 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -228,7 +228,7 @@ impl NodeSigner for KeyProvider { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 239ff3f0c98..3a03f96679d 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -1174,7 +1174,7 @@ mod tests { use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path, RouteHop}; use lightning::routing::scoring::{ChannelUsage, LockableScore, ScoreLookUp, ScoreUpdate}; - use lightning::sign::{ChangeDestinationSourceSync, InMemorySigner, KeysManager}; + use lightning::sign::{ChangeDestinationSourceSync, InMemorySigner, KeysManager, NodeSigner}; use lightning::types::features::{ChannelFeatures, NodeFeatures}; use lightning::types::payment::PaymentHash; use lightning::util::config::UserConfig; @@ -1650,6 +1650,7 @@ mod tests { let msg_router = Arc::new(DefaultMessageRouter::new( network_graph.clone(), Arc::clone(&keys_manager), + keys_manager.get_expanded_key(), )); let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Bitcoin)); let kv_store = diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index 2259d1eae06..f2639685e64 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -24,7 +24,7 @@ use lightning::onion_message::messenger::DefaultMessageRouter; use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path}; use lightning::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate}; -use lightning::sign::{InMemorySigner, KeysManager}; +use lightning::sign::{InMemorySigner, KeysManager, NodeSigner}; use lightning::util::config::UserConfig; use lightning::util::persist::{ KVStore, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -418,8 +418,11 @@ pub(crate) fn create_liquidity_node( scorer.clone(), Default::default(), )); - let msg_router = - Arc::new(DefaultMessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); + let msg_router = Arc::new(DefaultMessageRouter::new( + Arc::clone(&network_graph), + Arc::clone(&keys_manager), + keys_manager.get_expanded_key(), + )); let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Bitcoin)); let kv_store = Arc::new(FilesystemStore::new(format!("{}_persister_{}", &persist_dir, i).into())); diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 0e3977ab68e..8e2afa13e73 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -11,6 +11,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; +use crate::offers::signer; #[allow(unused_imports)] use crate::prelude::*; @@ -19,9 +20,9 @@ use crate::blinded_path::{BlindedHop, BlindedPath, Direction, IntroductionNode, use crate::crypto::streams::ChaChaPolyReadAdapter; use crate::io; use crate::io::Cursor; -use crate::ln::channelmanager::PaymentId; +use crate::ln::channelmanager::{PaymentId, Verification}; use crate::ln::msgs::DecodeError; -use crate::ln::onion_utils; +use crate::ln::{inbound_payment, onion_utils}; use crate::offers::nonce::Nonce; use crate::onion_message::packet::ControlTlvs; use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; @@ -68,7 +69,6 @@ impl BlindedMessagePath { /// pubkey in `node_pks` will be the destination node. /// /// Errors if no hops are provided or if `node_pk`(s) are invalid. - // TODO: make all payloads the same size with padding + add dummy hops pub fn new( intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1, @@ -88,9 +88,53 @@ impl BlindedMessagePath { blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret), blinded_hops: blinded_hops( secp_ctx, + entropy_source, + None, intermediate_nodes, recipient_node_id, context, + 0, + &blinding_secret, + ) + .map_err(|_| ())?, + })) + } + + /// Create a path for an onion message, to be forwarded along `node_pks`. + /// + /// Additionally allows appending a number of dummy hops before the final hop, + /// increasing the total path length and enhancing privacy by obscuring the true + /// distance between sender and recipient. + /// + /// The last node pubkey in `node_pks` will be the destination node. + /// + /// Errors if no hops are provided or if `node_pk`(s) are invalid. + pub fn new_with_dummy_hops( + intermediate_nodes: &[MessageForwardNode], dummy_hops_count: u8, + recipient_node_id: PublicKey, context: MessageContext, entropy_source: ES, + expanded_key: &inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, + ) -> Result + where + ES::Target: EntropySource, + { + let introduction_node = IntroductionNode::NodeId( + intermediate_nodes.first().map_or(recipient_node_id, |n| n.node_id), + ); + let blinding_secret_bytes = entropy_source.get_secure_random_bytes(); + let blinding_secret = + SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted"); + + Ok(Self(BlindedPath { + introduction_node, + blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret), + blinded_hops: blinded_hops( + secp_ctx, + entropy_source, + Some(expanded_key), + intermediate_nodes, + recipient_node_id, + context, + dummy_hops_count, &blinding_secret, ) .map_err(|_| ())?, @@ -258,6 +302,94 @@ pub(crate) struct ForwardTlvs { pub(crate) next_blinding_override: Option, } +/// Represents a dummy TLV that can be used in a blinded path to extend the path length. +/// The first dummy TLV is authenticated and contains an HMAC, while subsequent dummy TLVs are +/// empty and do not contain any data. This allows for the path length to be extended without +/// adding any additional data, while still ensuring that the path is legitimate and terminates +/// in valid [`ReceiveTlvs`] data. +pub(crate) enum DummyTlv { + /// The first dummy TLV, which contains an HMAC and is authenticated. + /// This TLV is used to ensure that the path is legitimate and terminates in valid + /// [`ReceiveTlvs`] data. + Primary(PrimaryDummyTlv), + /// Subsequent dummy TLVs, which are empty and do not contain any data. + /// These TLVs are used to extend the path length without adding any additional data. + Subsequent, +} + +impl Writeable for DummyTlv { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + match self { + DummyTlv::Primary(primary) => primary.write(writer)?, + DummyTlv::Subsequent => { + // Subsequent dummy TLVs are empty, so we don't write anything. + // This is to ensure that the path length can be extended without + // adding any additional data. + encode_tlv_stream!(writer, { + (65541, (), required), // Represents that this is a Dummy Tlv variant + }) + }, + } + Ok(()) + } +} + +/// Represents the first dummy TLV in a blinded path, which is authenticated and contains an HMAC. +/// These TLV are intended for the final node. +/// +/// ## Authentication +/// Authentication provides an additional layer of security, ensuring that the path is legitimate +/// and terminates in valid [`ReceiveTlvs`] data. Verification begins with the first dummy hop and +/// continues recursively until the final [`ReceiveTlvs`] is reached. +/// +/// This prevents an attacker from crafting a bogus blinded path consisting solely of dummy tlv +/// without any valid payload, which could otherwise waste resources through recursive +/// processing — a potential vector for DoS-like attacks. +pub(crate) struct PrimaryDummyTlv { + /// The dummy TLV that holds the empty data for the Dummy Hop. + pub(crate) dummy_tlv: UnauthenticatedDummyTlv, + /// An HMAC of `tlvs` along with a nonce used to construct it. + pub(crate) authentication: (Hmac, Nonce), +} + +impl Writeable for PrimaryDummyTlv { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + encode_tlv_stream!(writer, { + (65539, self.authentication, required), + // The Some(()) represents that this is a Dummy Tlv variant + (65541, (), required), + }); + + Ok(()) + } +} + +/// A blank struct, representing a dummy TLV prior to authentication. +/// +/// For more details, see [`PrimaryDummyTlv`]. +pub(crate) struct UnauthenticatedDummyTlv {} + +impl Writeable for UnauthenticatedDummyTlv { + fn write(&self, _writer: &mut W) -> Result<(), io::Error> { + Ok(()) + } +} + +impl Verification for UnauthenticatedDummyTlv { + /// Constructs an HMAC to include in [`OffersContext`] for the data along with the given + /// [`Nonce`]. + fn hmac_data(&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey) -> Hmac { + signer::hmac_for_dummy_tlv(self, nonce, expanded_key) + } + + /// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`]. + fn verify_data( + &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Result<(), ()> { + signer::verify_dummy_tlv(self, hmac, nonce, expanded_key) + } +} + /// Similar to [`ForwardTlvs`], but these TLVs are for the final node. pub(crate) struct ReceiveTlvs { /// If `context` is `Some`, it is used to identify the blinded path that this onion message is @@ -505,13 +637,23 @@ impl_writeable_tlv_based!(DNSResolverContext, { pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100; /// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`. -pub(super) fn blinded_hops( - secp_ctx: &Secp256k1, intermediate_nodes: &[MessageForwardNode], - recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey, -) -> Result, secp256k1::Error> { +pub(super) fn blinded_hops( + secp_ctx: &Secp256k1, entropy_source: ES, + expanded_key: Option<&inbound_payment::ExpandedKey>, intermediate_nodes: &[MessageForwardNode], + recipient_node_id: PublicKey, context: MessageContext, dummy_hops_count: u8, + session_priv: &SecretKey, +) -> Result, secp256k1::Error> +where + ES::Target: EntropySource, +{ + if expanded_key.is_none() && dummy_hops_count > 0 { + debug_assert!(false, "Dummy hops are not supported without expanded keys"); + } + let pks = intermediate_nodes .iter() .map(|node| node.node_id) + .chain((0..dummy_hops_count).map(|_| recipient_node_id)) .chain(core::iter::once(recipient_node_id)); let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some()); @@ -526,6 +668,14 @@ pub(super) fn blinded_hops( .map(|next_hop| { ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None }) }) + .chain((0..1).filter(|_| dummy_hops_count > 0).map(|_| { + let dummy_tlv = UnauthenticatedDummyTlv {}; + let nonce = Nonce::from_entropy_source(&*entropy_source); + let hmac = dummy_tlv.hmac_data(nonce, expanded_key.unwrap()); + let tlv = PrimaryDummyTlv { dummy_tlv, authentication: (hmac, nonce) }; + ControlTlvs::Dummy(DummyTlv::Primary(tlv)) + })) + .chain((1..dummy_hops_count).map(|_| ControlTlvs::Dummy(DummyTlv::Subsequent))) .chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); if is_compact { diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index f015886933a..ae26d51b1f2 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -351,10 +351,7 @@ impl UnauthenticatedReceiveTlvs { /// Creates an authenticated [`ReceiveTlvs`], which includes an HMAC and the provide [`Nonce`] /// that can be use later to verify it authenticity. pub fn authenticate(self, nonce: Nonce, expanded_key: &ExpandedKey) -> ReceiveTlvs { - ReceiveTlvs { - authentication: (self.hmac_for_offer_payment(nonce, expanded_key), nonce), - tlvs: self, - } + ReceiveTlvs { authentication: (self.hmac_data(nonce, expanded_key), nonce), tlvs: self } } } diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index a956f2ebae2..9277775b38d 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -668,7 +668,7 @@ fn amount_doesnt_match_invreq() { valid_invreq = Some(invoice_request.clone()); *invoice_request = offer .request_invoice( - &nodes[0].keys_manager.get_inbound_payment_key(), + &nodes[0].keys_manager.get_expanded_key(), Nonce::from_entropy_source(nodes[0].keys_manager), &secp_ctx, payment_id, diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 18a1089d647..43de09a936c 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -83,7 +83,7 @@ pub fn blinded_payment_path( }; let nonce = Nonce([42u8; 16]); - let expanded_key = keys_manager.get_inbound_payment_key(); + let expanded_key = keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); @@ -168,7 +168,7 @@ fn do_one_hop_blinded_path(success: bool) { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[1].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); @@ -222,7 +222,7 @@ fn mpp_to_one_hop_blinded_path() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[3].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[3].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let blinded_path = BlindedPaymentPath::new( &[], nodes[3].node.get_our_node_id(), payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16, @@ -1342,7 +1342,7 @@ fn custom_tlvs_to_blinded_path() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[1].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); let blinded_path = BlindedPaymentPath::new( @@ -1396,7 +1396,7 @@ fn fails_receive_tlvs_authentication() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[1].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); @@ -1622,7 +1622,7 @@ fn route_blinding_spec_test_vector() { } Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { unreachable!() } + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } fn get_node_id(&self, _recipient: Recipient) -> Result { unreachable!() } fn sign_invoice( &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, @@ -1931,7 +1931,7 @@ fn test_trampoline_inbound_payment_decoding() { } Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { unreachable!() } + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } fn get_node_id(&self, _recipient: Recipient) -> Result { unreachable!() } fn sign_invoice( &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, @@ -2016,7 +2016,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { }; let nonce = Nonce([42u8; 16]); - let expanded_key = nodes[2].keys_manager.get_inbound_payment_key(); + let expanded_key = nodes[2].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let carol_unblinded_tlvs = payee_tlvs.encode(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 170d8261d5a..71285938c6b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -256,7 +256,7 @@ pub enum PendingHTLCRouting { requires_blinded_error: bool, /// Set if we are receiving a keysend to a blinded path, meaning we created the /// [`PaymentSecret`] and should verify it using our - /// [`NodeSigner::get_inbound_payment_key`]. + /// [`NodeSigner::get_expanded_key`]. has_recipient_created_payment_secret: bool, /// The [`InvoiceRequest`] associated with the [`Offer`] corresponding to this payment. invoice_request: Option, @@ -475,12 +475,12 @@ impl Ord for ClaimableHTLC { pub trait Verification { /// Constructs an HMAC to include in [`OffersContext`] for the data along with the given /// [`Nonce`]. - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac; /// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`]. - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()>; } @@ -488,7 +488,7 @@ pub trait Verification { impl Verification for PaymentHash { /// Constructs an HMAC to include in [`OffersContext::InboundPayment`] for the payment hash /// along with the given [`Nonce`]. - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { signer::hmac_for_payment_hash(*self, nonce, expanded_key) @@ -496,7 +496,7 @@ impl Verification for PaymentHash { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::InboundPayment`]. - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_payment_hash(*self, hmac, nonce, expanded_key) @@ -504,13 +504,13 @@ impl Verification for PaymentHash { } impl Verification for UnauthenticatedReceiveTlvs { - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { signer::hmac_for_payment_tlvs(self, nonce, expanded_key) } - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_payment_tlvs(self, hmac, nonce, expanded_key) @@ -550,7 +550,7 @@ impl PaymentId { impl Verification for PaymentId { /// Constructs an HMAC to include in [`OffersContext::OutboundPayment`] for the payment id /// along with the given [`Nonce`]. - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { signer::hmac_for_offer_payment_id(*self, nonce, expanded_key) @@ -558,7 +558,7 @@ impl Verification for PaymentId { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::OutboundPayment`]. - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_offer_payment_id(*self, hmac, nonce, expanded_key) @@ -3576,7 +3576,7 @@ where ) -> Self { let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); - let expanded_inbound_key = node_signer.get_inbound_payment_key(); + let expanded_inbound_key = node_signer.get_expanded_key(); ChannelManager { default_configuration: config.clone(), chain_hash: ChainHash::using_genesis_block(params.network), @@ -10560,7 +10560,7 @@ where }; let invoice_request = builder.build_and_sign()?; - let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); + let hmac = payment_id.hmac_data(nonce, expanded_key); let context = MessageContext::Offers( OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) } ); @@ -10664,7 +10664,7 @@ where let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; let nonce = Nonce::from_entropy_source(entropy); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let hmac = payment_hash.hmac_data(nonce, expanded_key); let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash: invoice.payment_hash(), nonce, hmac }); @@ -12444,7 +12444,7 @@ where .release_invoice_requests_awaiting_invoice() { let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; - let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); + let hmac = payment_id.hmac_data(nonce, &self.inbound_payment_key); let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce, @@ -12627,7 +12627,7 @@ where match response { Ok(invoice) => { let nonce = Nonce::from_entropy_source(&*self.entropy_source); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let hmac = payment_hash.hmac_data(nonce, expanded_key); let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) }, @@ -12664,7 +12664,7 @@ where OffersMessage::StaticInvoice(invoice) => { let payment_id = match context { Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { + if payment_id.verify_data(hmac, nonce, expanded_key).is_err() { return None } payment_id @@ -12677,7 +12677,7 @@ where OffersMessage::InvoiceError(invoice_error) => { let payment_hash = match context { Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => { - match payment_hash.verify_for_offer_payment(hmac, nonce, expanded_key) { + match payment_hash.verify_data(hmac, nonce, expanded_key) { Ok(_) => Some(payment_hash), Err(_) => None, } @@ -12690,7 +12690,7 @@ where match context { Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if let Ok(()) = payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) { + if let Ok(()) = payment_id.verify_data(hmac, nonce, expanded_key) { self.abandon_payment_with_reason( payment_id, PaymentFailureReason::InvoiceRequestRejected, ); @@ -14569,7 +14569,7 @@ where }, None)); } - let expanded_inbound_key = args.node_signer.get_inbound_payment_key(); + let expanded_inbound_key = args.node_signer.get_expanded_key(); let mut claimable_payments = hash_map_with_capacity(claimable_htlcs_list.len()); if let Some(purposes) = claimable_htlc_purposes { diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 8a9a5cb1762..6ac40d1897b 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -29,7 +29,7 @@ use crate::onion_message::messenger::OnionMessenger; use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::routing::gossip::{P2PGossipSync, NetworkGraph, NetworkUpdate}; use crate::routing::router::{self, PaymentParameters, Route, RouteParameters}; -use crate::sign::{EntropySource, RandomBytes}; +use crate::sign::{EntropySource, NodeSigner, RandomBytes}; use crate::util::config::{MaxDustHTLCExposure, UserConfig}; use crate::util::logger::Logger; use crate::util::scid_utils; @@ -725,7 +725,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { signer_provider: self.keys_manager, fee_estimator: &test_utils::TestFeeEstimator::new(253), router: &test_utils::TestRouter::new(Arc::clone(&network_graph), &self.logger, &scorer), - message_router: &test_utils::TestMessageRouter::new(network_graph, self.keys_manager), + message_router: &test_utils::TestMessageRouter::new(network_graph, self.keys_manager, self.keys_manager.get_expanded_key()), chain_monitor: self.chain_monitor, tx_broadcaster: &broadcaster, logger: &self.logger, @@ -3347,13 +3347,14 @@ pub fn create_node_cfgs_with_persisters<'a>(node_count: usize, chanmon_cfgs: &'a let chain_monitor = test_utils::TestChainMonitor::new(Some(&chanmon_cfgs[i].chain_source), &chanmon_cfgs[i].tx_broadcaster, &chanmon_cfgs[i].logger, &chanmon_cfgs[i].fee_estimator, persisters[i], &chanmon_cfgs[i].keys_manager); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &chanmon_cfgs[i].logger)); let seed = [i as u8; 32]; + let expanded_key = chanmon_cfgs[i].keys_manager.get_expanded_key(); nodes.push(NodeCfg { chain_source: &chanmon_cfgs[i].chain_source, logger: &chanmon_cfgs[i].logger, tx_broadcaster: &chanmon_cfgs[i].tx_broadcaster, fee_estimator: &chanmon_cfgs[i].fee_estimator, router: test_utils::TestRouter::new(network_graph.clone(), &chanmon_cfgs[i].logger, &chanmon_cfgs[i].scorer), - message_router: test_utils::TestMessageRouter::new(network_graph.clone(), &chanmon_cfgs[i].keys_manager), + message_router: test_utils::TestMessageRouter::new(network_graph.clone(), &chanmon_cfgs[i].keys_manager, expanded_key), chain_monitor, keys_manager: &chanmon_cfgs[i].keys_manager, node_seed: seed, diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 0c0bb0713cb..2130819a5a9 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -50,7 +50,7 @@ use crate::routing::gossip::{NetworkGraph, NetworkUpdate}; use crate::routing::router::{ get_route, Path, PaymentParameters, Route, RouteHop, RouteParameters, }; -use crate::sign::{EntropySource, OutputSpender, SignerProvider}; +use crate::sign::{EntropySource, NodeSigner, OutputSpender, SignerProvider}; use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::config::{ @@ -5112,7 +5112,11 @@ pub fn test_key_derivation_params() { let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph.clone(), &chanmon_cfgs[0].logger, &scorer); - let message_router = test_utils::TestMessageRouter::new(network_graph.clone(), &keys_manager); + let message_router = test_utils::TestMessageRouter::new( + network_graph.clone(), + &keys_manager, + keys_manager.get_expanded_key(), + ); let node = NodeCfg { chain_source: &chanmon_cfgs[0].chain_source, logger: &chanmon_cfgs[0].logger, diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 53b212428ca..6a0b52d40b5 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -37,9 +37,9 @@ const AMT_MSAT_LEN: usize = 8; // retrieve said payment type bits. const METHOD_TYPE_OFFSET: usize = 5; -/// A set of keys that were HKDF-expanded. Returned by [`NodeSigner::get_inbound_payment_key`]. +/// A set of keys that were HKDF-expanded. Returned by [`NodeSigner::get_expanded_key`]. /// -/// [`NodeSigner::get_inbound_payment_key`]: crate::sign::NodeSigner::get_inbound_payment_key +/// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key #[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)] pub struct ExpandedKey { /// The key used to encrypt the bytes containing the payment metadata (i.e. the amount and @@ -133,7 +133,7 @@ fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 { /// `ChannelManager` is required. Useful for generating invoices for [phantom node payments] without /// a `ChannelManager`. /// -/// `keys` is generated by calling [`NodeSigner::get_inbound_payment_key`]. It is recommended to +/// `keys` is generated by calling [`NodeSigner::get_expanded_key`]. It is recommended to /// cache this value and not regenerate it for each new inbound payment. /// /// `current_time` is a Unix timestamp representing the current time. @@ -142,7 +142,7 @@ fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 { /// on versions of LDK prior to 0.0.114. /// /// [phantom node payments]: crate::sign::PhantomKeysManager -/// [`NodeSigner::get_inbound_payment_key`]: crate::sign::NodeSigner::get_inbound_payment_key +/// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key pub fn create( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option, @@ -322,7 +322,7 @@ fn construct_payment_secret( /// For payments including a custom `min_final_cltv_expiry_delta`, the metadata is constructed as: /// payment method (3 bits) || payment amount (8 bytes - 3 bits) || min_final_cltv_expiry_delta (2 bytes) || expiry (6 bytes) /// -/// In both cases the result is then encrypted using a key derived from [`NodeSigner::get_inbound_payment_key`]. +/// In both cases the result is then encrypted using a key derived from [`NodeSigner::get_expanded_key`]. /// /// Then on payment receipt, we verify in this method that the payment preimage and payment secret /// match what was constructed. @@ -343,7 +343,7 @@ fn construct_payment_secret( /// /// See [`ExpandedKey`] docs for more info on the individual keys used. /// -/// [`NodeSigner::get_inbound_payment_key`]: crate::sign::NodeSigner::get_inbound_payment_key +/// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key /// [`create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment /// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash pub(super) fn verify( diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index f2e8284a617..876f5eccb27 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -195,7 +195,7 @@ where }, }; - let keys = node_signer.get_inbound_payment_key(); + let keys = node_signer.get_expanded_key(); let (payment_hash, payment_secret) = if let Some(payment_hash) = payment_hash { let payment_secret = create_from_hash( &keys, diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index 95a1fbaaa10..8fce1f20e3b 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -173,7 +173,7 @@ fn one_hop_blinded_path_with_custom_tlv() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[2].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[2].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); let blinded_path = BlindedPaymentPath::new( diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 2e9c3b90957..1c19c3a15d5 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3443,8 +3443,8 @@ where }, ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs) } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; - let expanded_key = node_signer.get_inbound_payment_key(); - if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() { + let expanded_key = node_signer.get_expanded_key(); + if tlvs.verify_data(hmac, nonce, &expanded_key).is_err() { return Err(DecodeError::InvalidValue); } @@ -3595,8 +3595,8 @@ where readable: BlindedTrampolineTlvs::Receive(receive_tlvs), } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; - let expanded_key = node_signer.get_inbound_payment_key(); - if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() { + let expanded_key = node_signer.get_expanded_key(); + if tlvs.verify_data(hmac, nonce, &expanded_key).is_err() { return Err(DecodeError::InvalidValue); } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index f8649111a0c..bc9abd59914 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2223,7 +2223,7 @@ fn fails_paying_invoice_with_unknown_required_features() { let payment_paths = invoice.payment_paths().to_vec(); let payment_hash = invoice.payment_hash(); - let expanded_key = alice.keys_manager.get_inbound_payment_key(); + let expanded_key = alice.keys_manager.get_expanded_key(); let secp_ctx = Secp256k1::new(); let created_at = alice.node.duration_since_epoch(); diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 329b90d2076..82b5b6a3f78 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -9,6 +9,7 @@ //! Utilities for signing offer messages and verifying metadata. +use crate::blinded_path::message::UnauthenticatedDummyTlv; use crate::blinded_path::payment::UnauthenticatedReceiveTlvs; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; @@ -570,3 +571,26 @@ pub(crate) fn verify_held_htlc_available_context( Err(()) } } + +pub(crate) fn hmac_for_dummy_tlv( + tlvs: &UnauthenticatedDummyTlv, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Msgs Dummies"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(PAYMENT_TLVS_HMAC_INPUT); + tlvs.write(&mut hmac).unwrap(); + + Hmac::from_engine(hmac) +} + +pub(crate) fn verify_dummy_tlv( + tlvs: &UnauthenticatedDummyTlv, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_dummy_tlv(tlvs, nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index b28819ee692..a7dd0d6f6f5 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -26,6 +26,7 @@ use crate::blinded_path::message::{ use crate::blinded_path::utils::is_padded; use crate::blinded_path::EmptyNodeIdLookUp; use crate::events::{Event, EventsProvider}; +use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{self, BaseMessageHandler, DecodeError, OnionMessageHandler}; use crate::routing::gossip::{NetworkGraph, P2PGossipSync}; use crate::routing::test_utils::{add_channel, add_or_update_node}; @@ -279,8 +280,11 @@ fn create_nodes_using_cfgs(cfgs: Vec) -> Vec { let node_signer = Arc::new(TestNodeSigner::new(secret_key)); let node_id_lookup = Arc::new(EmptyNodeIdLookUp {}); - let message_router = - Arc::new(DefaultMessageRouter::new(network_graph.clone(), entropy_source.clone())); + let message_router = Arc::new(DefaultMessageRouter::new( + network_graph.clone(), + entropy_source.clone(), + node_signer.get_expanded_key(), + )); let offers_message_handler = Arc::new(TestOffersMessageHandler {}); let async_payments_message_handler = Arc::new(TestAsyncPaymentsMessageHandler {}); let dns_resolver_message_handler = Arc::new(TestDNSResolverMessageHandler {}); @@ -418,6 +422,34 @@ fn one_blinded_hop() { pass_along_path(&nodes); } +#[test] +fn blinded_path_with_dummy() { + let nodes = create_nodes(2); + let test_msg = TestCustomMessage::Pong; + + let secp_ctx = Secp256k1::new(); + let context = MessageContext::Custom(Vec::new()); + let entropy = &*nodes[1].entropy_source; + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new_with_dummy_hops( + &[], + 5, + nodes[1].node_id, + context, + entropy, + &expanded_key, + &secp_ctx, + ) + .unwrap(); + // Ensure that dummy hops are added to the blinded path. + assert_eq!(blinded_path.blinded_hops().len(), 6); + let destination = Destination::BlindedPath(blinded_path); + let instructions = MessageSendInstructions::WithoutReplyPath { destination }; + nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); + nodes[1].custom_message_handler.expect_message(TestCustomMessage::Pong); + pass_along_path(&nodes); +} + #[test] fn two_unblinded_two_blinded() { let nodes = create_nodes(5); @@ -611,11 +643,14 @@ fn test_blinded_path_padding_for_full_length_path() { // Update the context to create a larger final receive TLVs, ensuring that // the hop sizes vary before padding. let context = MessageContext::Custom(vec![0u8; 42]); - let blinded_path = BlindedMessagePath::new( + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, + 5, nodes[3].node_id, context, &*nodes[3].entropy_source, + &expanded_key, &secp_ctx, ) .unwrap(); @@ -644,15 +679,19 @@ fn test_blinded_path_no_padding_for_compact_path() { // Update the context to create a larger final receive TLVs, ensuring that // the hop sizes vary before padding. let context = MessageContext::Custom(vec![0u8; 42]); - let blinded_path = BlindedMessagePath::new( + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, + 5, nodes[3].node_id, context, &*nodes[3].entropy_source, + &expanded_key, &secp_ctx, ) .unwrap(); + // Ensure that the hops are not padded. assert!(!is_padded(&blinded_path.blinded_hops(), MESSAGE_PADDING_ROUND_OFF)); } diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 6009a276976..c99e54e941c 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -13,6 +13,7 @@ use bitcoin::hashes::hmac::{Hmac, HmacEngine}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; +use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1, SecretKey}; #[cfg(async_payments)] @@ -20,8 +21,8 @@ use super::async_payments::AsyncPaymentsMessage; use super::async_payments::AsyncPaymentsMessageHandler; use super::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler}; use super::offers::{OffersMessage, OffersMessageHandler}; -use super::packet::OnionMessageContents; use super::packet::ParsedOnionMessageContents; +use super::packet::{DummyControlTlvs, OnionMessageContents}; use super::packet::{ ForwardControlTlvs, Packet, Payload, ReceiveControlTlvs, BIG_PACKET_HOP_DATA_LEN, SMALL_PACKET_HOP_DATA_LEN, @@ -29,16 +30,17 @@ use super::packet::{ #[cfg(async_payments)] use crate::blinded_path::message::AsyncPaymentsContext; use crate::blinded_path::message::{ - BlindedMessagePath, DNSResolverContext, ForwardTlvs, MessageContext, MessageForwardNode, - NextMessageHop, OffersContext, ReceiveTlvs, + BlindedMessagePath, DNSResolverContext, DummyTlv, ForwardTlvs, MessageContext, + MessageForwardNode, NextMessageHop, OffersContext, PrimaryDummyTlv, ReceiveTlvs, }; use crate::blinded_path::utils; use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; use crate::events::{Event, EventHandler, EventsProvider, ReplayEvent}; +use crate::ln::channelmanager::Verification; use crate::ln::msgs::{ self, BaseMessageHandler, MessageSendEvent, OnionMessage, OnionMessageHandler, SocketAddress, }; -use crate::ln::onion_utils; +use crate::ln::{inbound_payment, onion_utils}; use crate::routing::gossip::{NetworkGraph, NodeId, ReadOnlyNetworkGraph}; use crate::sign::{EntropySource, NodeSigner, Recipient}; use crate::types::features::{InitFeatures, NodeFeatures}; @@ -539,6 +541,7 @@ where { network_graph: G, entropy_source: ES, + expanded_key: inbound_payment::ExpandedKey, } impl>, L: Deref, ES: Deref> DefaultMessageRouter @@ -547,8 +550,10 @@ where ES::Target: EntropySource, { /// Creates a [`DefaultMessageRouter`] using the given [`NetworkGraph`]. - pub fn new(network_graph: G, entropy_source: ES) -> Self { - Self { network_graph, entropy_source } + pub fn new( + network_graph: G, entropy_source: ES, expanded_key: inbound_payment::ExpandedKey, + ) -> Self { + Self { network_graph, entropy_source, expanded_key } } fn create_blinded_paths_from_iter< @@ -556,7 +561,8 @@ where T: secp256k1::Signing + secp256k1::Verification, >( network_graph: &G, recipient: PublicKey, context: MessageContext, peers: I, - entropy_source: &ES, secp_ctx: &Secp256k1, compact_paths: bool, + entropy_source: &ES, expanded_key: &inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, + compact_paths: bool, ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; @@ -565,6 +571,19 @@ where // recipient's node_id. const MIN_PEER_CHANNELS: usize = 3; + // Add a random number (0 to 5) of dummy hops to each non-compact blinded path + // to make it harder to infer the recipient's position. + // + // # Note on compact paths: + // + // Compact paths are optimized for minimal size. Adding dummy hops to them + // would increase their size and negate their primary advantage. + // Therefore, we avoid adding dummy hops to compact paths. + let dummy_hops_count = compact_paths.then_some(0).unwrap_or_else(|| { + let random_byte = entropy_source.get_secure_random_bytes()[0]; + random_byte % 6 + }); + let network_graph = network_graph.deref().read_only(); let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); @@ -595,7 +614,15 @@ where let paths = peer_info .into_iter() .map(|(peer, _, _)| { - BlindedMessagePath::new(&[peer], recipient, context.clone(), entropy, secp_ctx) + BlindedMessagePath::new_with_dummy_hops( + &[peer], + dummy_hops_count, + recipient, + context.clone(), + entropy, + expanded_key, + secp_ctx, + ) }) .take(MAX_PATHS) .collect::, _>>(); @@ -664,7 +691,7 @@ where pub(crate) fn create_blinded_paths( network_graph: &G, recipient: PublicKey, context: MessageContext, peers: Vec, - entropy_source: &ES, secp_ctx: &Secp256k1, + entropy_source: &ES, expanded_key: &inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, ) -> Result, ()> { let peers = peers.into_iter().map(|node_id| MessageForwardNode { node_id, short_channel_id: None }); @@ -674,6 +701,7 @@ where context, peers.into_iter(), entropy_source, + expanded_key, secp_ctx, false, ) @@ -681,7 +709,8 @@ where pub(crate) fn create_compact_blinded_paths( network_graph: &G, recipient: PublicKey, context: MessageContext, - peers: Vec, entropy_source: &ES, secp_ctx: &Secp256k1, + peers: Vec, entropy_source: &ES, + expanded_key: &inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, ) -> Result, ()> { Self::create_blinded_paths_from_iter( network_graph, @@ -689,6 +718,7 @@ where context, peers.into_iter(), entropy_source, + expanded_key, secp_ctx, true, ) @@ -717,6 +747,7 @@ where context, peers, &self.entropy_source, + &self.expanded_key, secp_ctx, ) } @@ -731,6 +762,7 @@ where context, peers, &self.entropy_source, + &self.expanded_key, secp_ctx, ) } @@ -1045,44 +1077,46 @@ where L::Target: Logger, CMH::Target: CustomOnionMessageHandler, { - let control_tlvs_ss = match node_signer.ecdh(Recipient::Node, &msg.blinding_point, None) { - Ok(ss) => ss, - Err(e) => { - log_error!(logger, "Failed to retrieve node secret: {:?}", e); - return Err(()); - }, - }; - let onion_decode_ss = { - let blinding_factor = { - let mut hmac = HmacEngine::::new(b"blinded_node_id"); - hmac.input(control_tlvs_ss.as_ref()); - let hmac = Hmac::from_engine(hmac).to_byte_array(); - Scalar::from_be_bytes(hmac).unwrap() - }; - let packet_pubkey = &msg.onion_routing_packet.public_key; - match node_signer.ecdh(Recipient::Node, packet_pubkey, Some(&blinding_factor)) { - Ok(ss) => ss.secret_bytes(), - Err(()) => { - log_trace!(logger, "Failed to compute onion packet shared secret"); + // Helper function to compute shared secrets for onion decoding + let compute_shared_secrets = |blinding_point: &PublicKey, + packet_pubkey: &PublicKey| + -> Result<(SharedSecret, [u8; 32]), ()> { + let control_tlvs_ss = match node_signer.ecdh(Recipient::Node, blinding_point, None) { + Ok(ss) => ss, + Err(e) => { + log_error!(logger, "Failed to retrieve node secret: {:?}", e); return Err(()); }, - } + }; + + let onion_decode_ss = { + let blinding_factor = { + let mut hmac = HmacEngine::::new(b"blinded_node_id"); + hmac.input(control_tlvs_ss.as_ref()); + let hmac = Hmac::from_engine(hmac).to_byte_array(); + Scalar::from_be_bytes(hmac).unwrap() + }; + match node_signer.ecdh(Recipient::Node, packet_pubkey, Some(&blinding_factor)) { + Ok(ss) => ss.secret_bytes(), + Err(()) => { + log_trace!(logger, "Failed to compute onion packet shared secret"); + return Err(()); + }, + } + }; + + Ok((control_tlvs_ss, onion_decode_ss)) }; - let next_hop = onion_utils::decode_next_untagged_hop( - onion_decode_ss, - &msg.onion_routing_packet.hop_data[..], - msg.onion_routing_packet.hmac, - (control_tlvs_ss, custom_handler.deref(), logger.deref()), - ); - match next_hop { - Ok(( - Payload::Receive { - message, - control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }), - reply_path, - }, - None, - )) => match (message, context) { + + // Helper function to process receive payloads + let process_receive_payload = |message, + context, + reply_path| + -> Result< + PeeledOnion<<::Target as CustomOnionMessageHandler>::CustomMessage>, + (), + > { + match (message, context) { (ParsedOnionMessageContents::Offers(msg), Some(MessageContext::Offers(ctx))) => { Ok(PeeledOnion::Offers(msg, Some(ctx), reply_path)) }, @@ -1114,55 +1148,201 @@ where ); Err(()) }, - }, - Ok(( - Payload::Forward(ForwardControlTlvs::Unblinded(ForwardTlvs { - next_hop, - next_blinding_override, - })), - Some((next_hop_hmac, new_packet_bytes)), - )) => { - // TODO: we need to check whether `next_hop` is our node, in which case this is a dummy - // blinded hop and this onion message is destined for us. In this situation, we should keep - // unwrapping the onion layers to get to the final payload. Since we don't have the option - // of creating blinded paths with dummy hops currently, we should be ok to not handle this - // for now. - let packet_pubkey = msg.onion_routing_packet.public_key; - let new_pubkey_opt = - onion_utils::next_hop_pubkey(&secp_ctx, packet_pubkey, &onion_decode_ss); - let new_pubkey = match new_pubkey_opt { - Ok(pk) => pk, + } + }; + + // Constructs the next onion message using packet data and blinding logic. + let compute_onion_message = |packet_pubkey: PublicKey, + next_hop_hmac: [u8; 32], + new_packet_bytes: Vec, + current_blinding_point: PublicKey, + current_control_tlvs_ss: SharedSecret, + current_onion_decode_ss: [u8; 32], + blinding_point_opt: Option| + -> Result { + let new_pubkey = match onion_utils::next_hop_pubkey( + &secp_ctx, + packet_pubkey, + ¤t_onion_decode_ss, + ) { + Ok(pk) => pk, + Err(e) => { + log_trace!(logger, "Failed to compute next hop packet pubkey: {}", e); + return Err(()); + }, + }; + let outgoing_packet = Packet { + version: 0, + public_key: new_pubkey, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + let blinding_point = match blinding_point_opt { + Some(bp) => bp, + None => match onion_utils::next_hop_pubkey( + &secp_ctx, + current_blinding_point, + current_control_tlvs_ss.as_ref(), + ) { + Ok(bp) => bp, Err(e) => { - log_trace!(logger, "Failed to compute next hop packet pubkey: {}", e); + log_trace!(logger, "Failed to compute next blinding point: {}", e); return Err(()); }, - }; - let outgoing_packet = Packet { - version: 0, - public_key: new_pubkey, - hop_data: new_packet_bytes, - hmac: next_hop_hmac, - }; - let onion_message = OnionMessage { - blinding_point: match next_blinding_override { - Some(blinding_point) => blinding_point, - None => { - match onion_utils::next_hop_pubkey( + }, + }; + Ok(OnionMessage { blinding_point, onion_routing_packet: outgoing_packet }) + }; + + let (control_tlvs_ss, onion_decode_ss) = + compute_shared_secrets(&msg.blinding_point, &msg.onion_routing_packet.public_key)?; + + let next_hop = onion_utils::decode_next_untagged_hop( + onion_decode_ss, + &msg.onion_routing_packet.hop_data[..], + msg.onion_routing_packet.hmac, + (control_tlvs_ss, custom_handler.deref(), logger.deref()), + ); + + match next_hop { + Ok(( + Payload::Receive { + message, + control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }), + reply_path, + }, + None, + )) => process_receive_payload(message, context, reply_path), + Ok(( + Payload::Dummy(DummyControlTlvs::Unblinded(DummyTlv::Primary(PrimaryDummyTlv { + dummy_tlv, + authentication, + }))), + Some((next_hop_hmac, new_packet_bytes)), + )) => { + let expanded_key = node_signer.get_expanded_key(); + dummy_tlv.verify_data(authentication.0, authentication.1, &expanded_key)?; + + // Start iterative processing of the dummy chain + let mut current_message = compute_onion_message( + msg.onion_routing_packet.public_key, + next_hop_hmac, + new_packet_bytes, + msg.blinding_point, + control_tlvs_ss, + onion_decode_ss, + None, + )?; + + // Process subsequent dummy hops iteratively + loop { + let (current_control_tlvs_ss, current_onion_decode_ss) = compute_shared_secrets( + ¤t_message.blinding_point, + ¤t_message.onion_routing_packet.public_key, + )?; + + let current_next_hop = onion_utils::decode_next_untagged_hop( + current_onion_decode_ss, + ¤t_message.onion_routing_packet.hop_data[..], + current_message.onion_routing_packet.hmac, + (current_control_tlvs_ss, custom_handler.deref(), logger.deref()), + ); + + match current_next_hop { + Ok(( + Payload::Dummy(DummyControlTlvs::Unblinded(DummyTlv::Subsequent)), + Some((next_hop_hmac, new_packet_bytes)), + )) => { + // Valid: Subsequent dummy after Primary dummy, continue processing + let new_pubkey = match onion_utils::next_hop_pubkey( + &secp_ctx, + current_message.onion_routing_packet.public_key, + ¤t_onion_decode_ss, + ) { + Ok(pk) => pk, + Err(e) => { + log_trace!( + logger, + "Failed to compute next hop packet pubkey: {}", + e + ); + return Err(()); + }, + }; + let outgoing_packet = Packet { + version: 0, + public_key: new_pubkey, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + let blinding_point = match onion_utils::next_hop_pubkey( &secp_ctx, - msg.blinding_point, - control_tlvs_ss.as_ref(), + current_message.blinding_point, + current_control_tlvs_ss.as_ref(), ) { Ok(bp) => bp, Err(e) => { log_trace!(logger, "Failed to compute next blinding point: {}", e); return Err(()); }, - } + }; + current_message = + OnionMessage { blinding_point, onion_routing_packet: outgoing_packet }; }, - }, - onion_routing_packet: outgoing_packet, - }; - + Ok(( + Payload::Receive { + message, + control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }), + reply_path, + }, + None, + )) => { + // End of dummy chain, process the final receive payload + return process_receive_payload(message, context, reply_path); + }, + Err(e) => { + log_trace!( + logger, + "Errored decoding onion message packet in dummy chain: {:?}", + e + ); + return Err(()); + }, + _ => { + // Invalid: anything else breaks the expected structure + log_trace!( + logger, + "Invalid payload type in dummy chain. Expected Subsequent dummy or Receive payload." + ); + return Err(()); + }, + } + } + }, + Ok((Payload::Dummy(DummyControlTlvs::Unblinded(DummyTlv::Subsequent)), _)) => { + // ERROR: Subsequent dummy without preceding Primary dummy + log_trace!( + logger, + "Received Subsequent dummy without preceding Primary dummy - invalid structure" + ); + Err(()) + }, + Ok(( + Payload::Forward(ForwardControlTlvs::Unblinded(ForwardTlvs { + next_hop, + next_blinding_override, + })), + Some((next_hop_hmac, new_packet_bytes)), + )) => { + let onion_message = compute_onion_message( + msg.onion_routing_packet.public_key, + next_hop_hmac, + new_packet_bytes, + msg.blinding_point, + control_tlvs_ss, + onion_decode_ss, + next_blinding_override, + )?; Ok(PeeledOnion::Forward(next_hop, onion_message)) }, Err(e) => { diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 632cbc9c8a3..d3e6e204113 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -17,7 +17,10 @@ use super::async_payments::AsyncPaymentsMessage; use super::dns_resolution::DNSResolverMessage; use super::messenger::CustomOnionMessageHandler; use super::offers::OffersMessage; -use crate::blinded_path::message::{BlindedMessagePath, ForwardTlvs, NextMessageHop, ReceiveTlvs}; +use crate::blinded_path::message::{ + BlindedMessagePath, DummyTlv, ForwardTlvs, NextMessageHop, PrimaryDummyTlv, ReceiveTlvs, + UnauthenticatedDummyTlv, +}; use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter}; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; @@ -111,6 +114,8 @@ impl LengthReadable for Packet { pub(super) enum Payload { /// This payload is for an intermediate hop. Forward(ForwardControlTlvs), + /// This payload is dummy, and is inteded to be peeled. + Dummy(DummyControlTlvs), /// This payload is for the final hop. Receive { control_tlvs: ReceiveControlTlvs, reply_path: Option, message: T }, } @@ -204,6 +209,11 @@ pub(super) enum ForwardControlTlvs { Unblinded(ForwardTlvs), } +pub(super) enum DummyControlTlvs { + /// See [`ForwardControlTlvs::Unblinded`] + Unblinded(DummyTlv), +} + /// Receive control TLVs in their blinded and unblinded form. pub(super) enum ReceiveControlTlvs { /// See [`ForwardControlTlvs::Blinded`]. @@ -234,6 +244,10 @@ impl Writeable for (Payload, [u8; 32]) { let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); _encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) }) }, + Payload::Dummy(DummyControlTlvs::Unblinded(control_tlvs)) => { + let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); + _encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) }) + }, Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs), reply_path, @@ -310,6 +324,9 @@ impl ReadableArgs<(Sh } Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs))) }, + Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Dummy(tlvs) }) => { + Ok(Payload::Dummy(DummyControlTlvs::Unblinded(tlvs))) + }, Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => { Ok(Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(tlvs), @@ -328,6 +345,8 @@ impl ReadableArgs<(Sh pub(crate) enum ControlTlvs { /// This onion message is intended to be forwarded. Forward(ForwardTlvs), + /// This onion message is a dummy, and is intended to be peeled. + Dummy(DummyTlv), /// This onion message is intended to be received. Receive(ReceiveTlvs), } @@ -343,6 +362,8 @@ impl Readable for ControlTlvs { (4, next_node_id, option), (8, next_blinding_override, option), (65537, context, option), + (65539, authentication, option), + (65541, dummy_tlv, option), }); let next_hop = match (short_channel_id, next_node_id) { @@ -352,18 +373,18 @@ impl Readable for ControlTlvs { (None, None) => None, }; - let valid_fwd_fmt = next_hop.is_some(); - let valid_recv_fmt = next_hop.is_none() && next_blinding_override.is_none(); - - let payload_fmt = if valid_fwd_fmt { - ControlTlvs::Forward(ForwardTlvs { - next_hop: next_hop.unwrap(), - next_blinding_override, - }) - } else if valid_recv_fmt { - ControlTlvs::Receive(ReceiveTlvs { context }) - } else { - return Err(DecodeError::InvalidValue); + let payload_fmt = match (dummy_tlv, next_hop, next_blinding_override, authentication) { + (None, Some(hop), _, None) => { + ControlTlvs::Forward(ForwardTlvs { next_hop: hop, next_blinding_override }) + }, + (None, None, None, None) => ControlTlvs::Receive(ReceiveTlvs { context }), + (Some(()), None, None, Some(auth)) => { + let tlv = + PrimaryDummyTlv { dummy_tlv: UnauthenticatedDummyTlv {}, authentication: auth }; + ControlTlvs::Dummy(DummyTlv::Primary(tlv)) + }, + (Some(()), None, None, None) => ControlTlvs::Dummy(DummyTlv::Subsequent), + _ => return Err(DecodeError::InvalidValue), }; Ok(payload_fmt) @@ -374,6 +395,7 @@ impl Writeable for ControlTlvs { fn write(&self, w: &mut W) -> Result<(), io::Error> { match self { Self::Forward(tlvs) => tlvs.write(w), + Self::Dummy(tlvs) => tlvs.write(w), Self::Receive(tlvs) => tlvs.write(w), } } diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index f35a407634a..57af75d1ef1 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -820,19 +820,24 @@ pub trait EntropySource { /// A trait that can handle cryptographic operations at the scope level of a node. pub trait NodeSigner { - /// Get the [`ExpandedKey`] for use in encrypting and decrypting inbound payment data. + /// Get the [`ExpandedKey`] which provides cryptographic material for various Lightning Network operations. + /// + /// This key set is used for: + /// - Encrypting and decrypting inbound payment metadata + /// - Authenticating payment hashes (both LDK-provided and user-provided) + /// - Supporting BOLT 12 Offers functionality (signing and encryption) + /// - Authenticating spontaneous payments' metadata /// /// If the implementor of this trait supports [phantom node payments], then every node that is /// intended to be included in the phantom invoice route hints must return the same value from /// this method. - // This is because LDK avoids storing inbound payment data by encrypting payment data in the - // payment hash and/or payment secret, therefore for a payment to be receivable by multiple - // nodes, they must share the key that encrypts this payment data. /// - /// This method must return the same value each time it is called. + /// This method must return the same value each time it is called, as LDK avoids storing inbound + /// payment data by encrypting it in the payment hash and/or payment secret. Consistency is also + /// required for signature and encryption verification in Offers and spontaneous payments. /// /// [phantom node payments]: PhantomKeysManager - fn get_inbound_payment_key(&self) -> ExpandedKey; + fn get_expanded_key(&self) -> ExpandedKey; /// Get node id based on the provided [`Recipient`]. /// @@ -2113,7 +2118,7 @@ impl NodeSigner for KeysManager { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { self.inbound_payment_key.clone() } @@ -2274,7 +2279,7 @@ impl NodeSigner for PhantomKeysManager { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { self.inbound_payment_key.clone() } diff --git a/lightning/src/util/dyn_signer.rs b/lightning/src/util/dyn_signer.rs index 939e40cf7c4..2efedbb2355 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -214,7 +214,7 @@ inner, fn sign_bolt12_invoice(, invoice: &crate::offers::invoice::UnsignedBolt12Invoice ) -> Result, - fn get_inbound_payment_key(,) -> ExpandedKey + fn get_expanded_key(,) -> ExpandedKey ); delegate!(DynKeysInterface, SignerProvider, @@ -278,7 +278,7 @@ delegate!(DynPhantomKeysInterface, NodeSigner, fn sign_invoice(, invoice: &RawBolt11Invoice, recipient: Recipient) -> Result, fn sign_bolt12_invoice(, invoice: &crate::offers::invoice::UnsignedBolt12Invoice ) -> Result, - fn get_inbound_payment_key(,) -> ExpandedKey + fn get_expanded_key(,) -> ExpandedKey ); impl SignerProvider for DynPhantomKeysInterface { diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index f90bfb97ef7..c658a4ab87a 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -25,11 +25,11 @@ use crate::events::bump_transaction::{Utxo, WalletSource}; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::chan_utils::CommitmentTransaction; use crate::ln::channel_state::ChannelDetails; -use crate::ln::channelmanager; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{BaseMessageHandler, MessageSendEvent}; use crate::ln::script::ShutdownScript; use crate::ln::types::ChannelId; +use crate::ln::{channelmanager, inbound_payment}; use crate::ln::{msgs, wire}; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::onion_message::messenger::{ @@ -324,8 +324,9 @@ pub struct TestMessageRouter<'a> { impl<'a> TestMessageRouter<'a> { pub fn new( network_graph: Arc>, entropy_source: &'a TestKeysInterface, + expanded_key: inbound_payment::ExpandedKey, ) -> Self { - Self { inner: DefaultMessageRouter::new(network_graph, entropy_source) } + Self { inner: DefaultMessageRouter::new(network_graph, entropy_source, expanded_key) } } } @@ -1486,8 +1487,8 @@ impl TestNodeSigner { } impl NodeSigner for TestNodeSigner { - fn get_inbound_payment_key(&self) -> ExpandedKey { - unreachable!() + fn get_expanded_key(&self) -> ExpandedKey { + ExpandedKey::new([42; 32]) } fn get_node_id(&self, recipient: Recipient) -> Result { @@ -1559,8 +1560,8 @@ impl NodeSigner for TestKeysInterface { self.backing.ecdh(recipient, other_key, tweak) } - fn get_inbound_payment_key(&self) -> ExpandedKey { - self.backing.get_inbound_payment_key() + fn get_expanded_key(&self) -> ExpandedKey { + self.backing.get_expanded_key() } fn sign_invoice(