diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 0e3977ab68e..510a2e02384 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -68,11 +68,37 @@ 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, ) -> Result + where + ES::Target: EntropySource, + { + BlindedMessagePath::new_with_dummy_hops( + intermediate_nodes, + 0, + recipient_node_id, + context, + entropy_source, + secp_ctx, + ) + } + + /// 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, + secp_ctx: &Secp256k1, + ) -> Result where ES::Target: EntropySource, { @@ -91,6 +117,7 @@ impl BlindedMessagePath { intermediate_nodes, recipient_node_id, context, + dummy_hops_count, &blinding_secret, ) .map_err(|_| ())?, @@ -507,11 +534,13 @@ 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, + recipient_node_id: PublicKey, context: MessageContext, dummy_hops_count: u8, + session_priv: &SecretKey, ) -> Result, secp256k1::Error> { 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 +555,12 @@ pub(super) fn blinded_hops( .map(|next_hop| { ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None }) }) + .chain((0..dummy_hops_count).map(|_| { + ControlTlvs::Forward(ForwardTlvs { + next_hop: NextMessageHop::NodeId(recipient_node_id), + next_blinding_override: None, + }) + })) .chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); if is_compact { diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index b28819ee692..ebf3fad484e 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -418,6 +418,32 @@ 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 blinded_path = BlindedMessagePath::new_with_dummy_hops( + &[], + 5, + nodes[1].node_id, + context, + entropy, + &secp_ctx, + ) + .unwrap(); + // Make sure that dummy hops are do 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,8 +637,9 @@ 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 blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, + 5, nodes[3].node_id, context, &*nodes[3].entropy_source, @@ -644,8 +671,9 @@ 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 blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, + 5, nodes[3].node_id, context, &*nodes[3].entropy_source, diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index df317d45df2..0e833bf743c 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -563,6 +563,13 @@ where // recipient's node_id. const MIN_PEER_CHANNELS: usize = 3; + // Add a random number (1 to 5) of dummy hops to each non-compact blinded path + // to make it harder to infer the recipient's position. + let dummy_hops_count = compact_paths.then_some(0).unwrap_or_else(|| { + let random_byte = entropy_source.get_secure_random_bytes()[0]; + (random_byte % 5) + 1 + }); + let network_graph = network_graph.deref().read_only(); let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); @@ -602,8 +609,15 @@ where Ok(paths) if !paths.is_empty() => Ok(paths), _ => { if is_recipient_announced { - BlindedMessagePath::new(&[], recipient, context, &**entropy_source, secp_ctx) - .map(|path| vec![path]) + BlindedMessagePath::new_with_dummy_hops( + &[], + dummy_hops_count, + recipient, + context, + &**entropy_source, + secp_ctx, + ) + .map(|path| vec![path]) } else { Err(()) } @@ -1104,11 +1118,6 @@ where })), 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); @@ -1145,7 +1154,18 @@ where onion_routing_packet: outgoing_packet, }; - Ok(PeeledOnion::Forward(next_hop, onion_message)) + let our_node_id = node_signer.get_node_id(Recipient::Node).unwrap(); + + match next_hop { + NextMessageHop::NodeId(ref id) if id == &our_node_id => peel_onion_message( + &onion_message, + secp_ctx, + node_signer, + logger, + custom_handler, + ), + _ => Ok(PeeledOnion::Forward(next_hop, onion_message)), + } }, Err(e) => { log_trace!(logger, "Errored decoding onion message packet: {:?}", e);