-
Notifications
You must be signed in to change notification settings - Fork 406
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
base: main
Are you sure you want to change the base?
Changes from all commits
c7f1080
a3e89a7
3548d56
124b762
9e654bf
9ae8e25
d2aeac9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}; | ||
|
@@ -55,23 +56,51 @@ impl Readable for BlindedMessagePath { | |
impl BlindedMessagePath { | ||
/// Create a one-hop blinded path for a message. | ||
pub fn one_hop<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>( | ||
recipient_node_id: PublicKey, context: MessageContext, entropy_source: ES, | ||
secp_ctx: &Secp256k1<T>, | ||
recipient_node_id: PublicKey, context: MessageContext, | ||
expanded_key: inbound_payment::ExpandedKey, entropy_source: ES, secp_ctx: &Secp256k1<T>, | ||
) -> Result<Self, ()> | ||
where | ||
ES::Target: EntropySource, | ||
{ | ||
Self::new(&[], recipient_node_id, context, entropy_source, secp_ctx) | ||
Self::new(&[], recipient_node_id, context, entropy_source, expanded_key, secp_ctx) | ||
} | ||
|
||
/// Create a path for an onion message, to be forwarded along `node_pks`. 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. | ||
// TODO: make all payloads the same size with padding + add dummy hops | ||
pub fn new<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>( | ||
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, | ||
context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1<T>, | ||
context: MessageContext, entropy_source: ES, expanded_key: inbound_payment::ExpandedKey, | ||
secp_ctx: &Secp256k1<T>, | ||
) -> Result<Self, ()> | ||
where | ||
ES::Target: EntropySource, | ||
{ | ||
BlindedMessagePath::new_with_dummy_hops( | ||
intermediate_nodes, | ||
0, | ||
recipient_node_id, | ||
context, | ||
entropy_source, | ||
expanded_key, | ||
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<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>( | ||
intermediate_nodes: &[MessageForwardNode], dummy_hops_count: u8, | ||
recipient_node_id: PublicKey, context: MessageContext, entropy_source: ES, | ||
expanded_key: inbound_payment::ExpandedKey, secp_ctx: &Secp256k1<T>, | ||
) -> Result<Self, ()> | ||
where | ||
ES::Target: EntropySource, | ||
|
@@ -88,9 +117,12 @@ impl BlindedMessagePath { | |
blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret), | ||
blinded_hops: blinded_hops( | ||
secp_ctx, | ||
entropy_source, | ||
expanded_key, | ||
intermediate_nodes, | ||
recipient_node_id, | ||
context, | ||
dummy_hops_count, | ||
&blinding_secret, | ||
) | ||
.map_err(|_| ())?, | ||
|
@@ -258,6 +290,63 @@ pub(crate) struct ForwardTlvs { | |
pub(crate) next_blinding_override: Option<PublicKey>, | ||
} | ||
|
||
/// A blank struct, representing dummy tlv prior to authentication. | ||
/// | ||
/// For more details, see [`DummyTlv`]. | ||
pub(crate) struct UnauthenticatedDummyTlv {} | ||
|
||
impl Writeable for UnauthenticatedDummyTlv { | ||
fn write<W: Writer>(&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<Sha256> { | ||
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<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, | ||
) -> Result<(), ()> { | ||
signer::verify_dummy_tlv(self, hmac, nonce, expanded_key) | ||
} | ||
} | ||
|
||
/// 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 and verified 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. | ||
/// | ||
/// ## 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 DummyTlv { | ||
pub(crate) dummy_tlv: UnauthenticatedDummyTlv, | ||
/// An HMAC of `tlvs` along with a nonce used to construct it. | ||
pub(crate) authentication: (Hmac<Sha256>, Nonce), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks so much for pointing that out, Jeff — that makes a lot of sense! |
||
} | ||
|
||
impl Writeable for DummyTlv { | ||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { | ||
encode_tlv_stream!(writer, { | ||
(65539, self.authentication, 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 | ||
|
@@ -505,13 +594,18 @@ 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<T: secp256k1::Signing + secp256k1::Verification>( | ||
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode], | ||
recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey, | ||
) -> Result<Vec<BlindedHop>, secp256k1::Error> { | ||
pub(super) fn blinded_hops<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>( | ||
secp_ctx: &Secp256k1<T>, entropy_source: ES, expanded_key: inbound_payment::ExpandedKey, | ||
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, | ||
context: MessageContext, dummy_hops_count: u8, session_priv: &SecretKey, | ||
) -> Result<Vec<BlindedHop>, secp256k1::Error> | ||
where | ||
ES::Target: EntropySource, | ||
{ | ||
let pks = intermediate_nodes | ||
.iter() | ||
.map(|node| node.node_id) | ||
.chain((0..dummy_hops_count).map(|_| recipient_node_id)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked up the bolt spec and read "MAY add additional "dummy" hops at the end of the path (which it will ignore on receipt) to obscure the path length." What does ignore mean exactly? It seems in the next commit that it means to keep peeling? Using the recipient node id for all the dummy hops isn't really described in the bolt I think. Maybe mistaking. Also a mention of padding is made: "The padding field can be used to ensure that all encrypted_recipient_data have the same length. It's particularly useful when adding dummy hops at the end of a blinded route, to prevent the sender from figuring out which node is the final recipient" Not sure if that is done now too? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The thinking behind this approach was that if dummy hops were added after the To avoid that, I added the dummy hops just before the final node. This way, even after receiving a dummy hop (with
Yes! In PR #3177, we added support for padding in both Since the I've also updated the padding tests to use Thanks so much again for the super helpful feedback! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If this is strictly better, it could be worth a PR to the bolt spec? At the minimum it might get you some feedback on this line of thinking. I am not sure if the timing attack is avoided though, and worth the extra complexity. Peeling seems to be so much faster than an actual hop with network latency etc. Some random delay might be more effective? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code also still works for blinded paths where dummy hops are added after the ReceiveTlvs right? Just making sure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There shouldn't be a need to support that because we only support receiving to blinded paths that we create. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The timing attack does seem like a potential issue though. Not sure how to address that without adding some kind of |
||
.chain(core::iter::once(recipient_node_id)); | ||
let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some()); | ||
|
||
|
@@ -526,6 +620,12 @@ 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_hops_count).map(|_| { | ||
let dummy_tlv = UnauthenticatedDummyTlv {}; | ||
let nonce = Nonce::from_entropy_source(&*entropy_source); | ||
let hmac = dummy_tlv.hmac_data(nonce, &expanded_key); | ||
ControlTlvs::Dummy(DummyTlv { dummy_tlv, authentication: (hmac, nonce) }) | ||
})) | ||
.chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); | ||
|
||
if is_compact { | ||
|
Uh oh!
There was an error while loading. Please reload this page.