Skip to content

Improve privacy for Blinded Message Paths using Dummy Hops #3726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fuzz/src/chanmon_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,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)
Expand Down
2 changes: 1 addition & 1 deletion fuzz/src/full_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,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
}

Expand Down
2 changes: 1 addition & 1 deletion fuzz/src/onion_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,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!()
}

Expand Down
54 changes: 51 additions & 3 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ use crate::types::payment::PaymentHash;
use crate::util::scid_utils;
use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer};

use core::mem;
use core::ops::Deref;
use core::time::Duration;
use core::{cmp, mem};

/// A blinded path to be used for sending or receiving a message, hiding the identity of the
/// recipient.
Expand Down Expand Up @@ -74,6 +74,26 @@ impl BlindedMessagePath {
local_node_receive_key: ReceiveAuthKey, context: MessageContext, entropy_source: ES,
secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()>
where
ES::Target: EntropySource,
{
BlindedMessagePath::new_with_dummy_hops(
intermediate_nodes,
recipient_node_id,
0,
local_node_receive_key,
context,
entropy_source,
secp_ctx,
)
}

/// Same as [`BlindedMessagePath::new`] but allow specifying a number of dummy hops
pub fn new_with_dummy_hops<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, context: MessageContext,
entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()>
where
ES::Target: EntropySource,
{
Expand All @@ -91,6 +111,7 @@ impl BlindedMessagePath {
secp_ctx,
intermediate_nodes,
recipient_node_id,
dummy_hop_count,
context,
&blinding_secret,
local_node_receive_key,
Expand Down Expand Up @@ -266,6 +287,23 @@ pub(crate) struct ForwardTlvs {
pub(crate) next_blinding_override: Option<PublicKey>,
}

/// Represents the dummy TLV encoded immediately before the actual [`ReceiveTlvs`] in a blinded path.
/// These TLVs are intended for the final node and are recursively authenticated until the real
/// [`ReceiveTlvs`] is reached.
///
/// Their purpose is to arbitrarily extend the path length, obscuring the receiver's position in the
/// route and thereby enhancing privacy.
pub(crate) struct DummyTlv {}

impl Writeable for DummyTlv {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
encode_tlv_stream!(writer, {
(65539, (), required),
});
Ok(())
}
}

/// 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
Expand Down Expand Up @@ -621,15 +659,24 @@ impl_writeable_tlv_based!(DNSResolverContext, {
/// to pad message blinded path's [`BlindedHop`]
pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100;

/// The maximum number of dummy hops that can be added to a blinded path.
/// This is to prevent paths from becoming too long and potentially causing
/// issues with message processing or routing.
pub(crate) const MAX_DUMMY_HOPS_COUNT: usize = 10;

/// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`.
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey,
local_node_receive_key: ReceiveAuthKey,
recipient_node_id: PublicKey, dummy_hop_count: usize, context: MessageContext,
session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey,
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
let dummy_count = cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT);
let pks = intermediate_nodes
.iter()
.map(|node| (node.node_id, None))
.chain(
core::iter::repeat((recipient_node_id, Some(local_node_receive_key))).take(dummy_count),
)
.chain(core::iter::once((recipient_node_id, Some(local_node_receive_key))));
let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some());

Expand All @@ -644,6 +691,7 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
.map(|next_hop| {
ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None })
})
.chain((0..dummy_count).map(|_| ControlTlvs::Dummy(DummyTlv {})))
.chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) })));

if is_compact {
Expand Down
35 changes: 28 additions & 7 deletions lightning/src/blinded_path/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,37 @@ impl<T: Writeable> Writeable for BlindedPathWithPadding<T> {
}

#[cfg(test)]
/// Checks if all the packets in the blinded path are properly padded.
/// Verifies whether all hops in the blinded path follow the expected padding scheme.
///
/// In the padded encoding scheme, each hop's encrypted payload is expected to be of the form:
/// `n * padding_round_off + extra`, where:
/// - `padding_round_off` is the fixed block size to which unencrypted payloads are padded.
/// - `n` is a positive integer (n ≥ 1).
/// - `extra` is the fixed overhead added during encryption (assumed uniform across hops).
///
/// This function infers the `extra` from the first hop, and checks that all other hops conform
/// to the same pattern.
///
/// # Returns
/// - `true` if all hop payloads are padded correctly.
/// - `false` if padding is incorrectly applied or intentionally absent (e.g., in compact paths).
pub fn is_padded(hops: &[BlindedHop], padding_round_off: usize) -> bool {
let first_hop = hops.first().expect("BlindedPath must have at least one hop");
let first_payload_size = first_hop.encrypted_payload.len();
let first_len = first_hop.encrypted_payload.len();

// Early rejection: if the first hop is too small, it can't be correctly padded.
if first_len <= padding_round_off {
return false;
}

// Compute the extra encrypted overhead by taking the remainder.
let extra = first_len % padding_round_off;

// All hops must follow the same padding structure:
// their length minus `extra` should be a clean multiple of `padding_round_off`.

// The unencrypted payload data is padded before getting encrypted.
// Assuming the first payload is padded properly, get the extra data length.
let extra_length = first_payload_size % padding_round_off;
hops.iter().all(|hop| {
// Check that every packet is padded to the round off length subtracting the extra length.
(hop.encrypted_payload.len() - extra_length) % padding_round_off == 0
let len = hop.encrypted_payload.len();
len > extra && (len - extra) % padding_round_off == 0
})
}
2 changes: 1 addition & 1 deletion lightning/src/ln/async_payments_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,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,
Expand Down
16 changes: 8 additions & 8 deletions lightning/src/ln/blinded_payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,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();
Expand Down Expand Up @@ -172,7 +172,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();
Expand Down Expand Up @@ -226,7 +226,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,
Expand Down Expand Up @@ -1336,7 +1336,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(
Expand Down Expand Up @@ -1390,7 +1390,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();
Expand Down Expand Up @@ -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<PublicKey, ()> { unreachable!() }
fn sign_invoice(
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
Expand Down Expand Up @@ -1935,7 +1935,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<PublicKey, ()> { unreachable!() }
fn sign_invoice(
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
Expand Down Expand Up @@ -2023,7 +2023,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();

Expand Down
6 changes: 3 additions & 3 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,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<InvoiceRequest>,
Expand Down Expand Up @@ -3715,7 +3715,7 @@ where
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();
let our_network_pubkey = node_signer.get_node_id(Recipient::Node).unwrap();

let flow = OffersMessageFlow::new(
Expand Down Expand Up @@ -16513,7 +16513,7 @@ where
}
}

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 {
Expand Down
12 changes: 6 additions & 6 deletions lightning/src/ln/inbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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<ES: Deref>(
keys: &ExpandedKey, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32,
entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option<u16>,
Expand Down Expand Up @@ -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.
Expand All @@ -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<L: Deref>(
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/invoice_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/max_payment_path_len_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,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(
Expand Down
4 changes: 2 additions & 2 deletions lightning/src/ln/msgs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3548,7 +3548,7 @@ where
},
ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs) } => {
let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs;
let expanded_key = node_signer.get_inbound_payment_key();
let expanded_key = node_signer.get_expanded_key();
if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() {
return Err(DecodeError::InvalidValue);
}
Expand Down Expand Up @@ -3700,7 +3700,7 @@ where
readable: BlindedTrampolineTlvs::Receive(receive_tlvs),
} => {
let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs;
let expanded_key = node_signer.get_inbound_payment_key();
let expanded_key = node_signer.get_expanded_key();
if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() {
return Err(DecodeError::InvalidValue);
}
Expand Down
Loading
Loading