Skip to content

Commit 8692dfd

Browse files
committed
Introduce DummyTlvs
Adds new `Dummy` variant to `ControlTlvs`, allowing insertion of arbitrary dummy hops before the final `ReceiveTlvs`. This increases the length of the blinded path, making it harder for a malicious actor to infer the position of the true final hop.
1 parent a3e89a7 commit 8692dfd

File tree

3 files changed

+115
-15
lines changed

3 files changed

+115
-15
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
1212
use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
1313

14+
use crate::offers::signer;
1415
#[allow(unused_imports)]
1516
use crate::prelude::*;
1617

@@ -19,9 +20,9 @@ use crate::blinded_path::{BlindedHop, BlindedPath, Direction, IntroductionNode,
1920
use crate::crypto::streams::ChaChaPolyReadAdapter;
2021
use crate::io;
2122
use crate::io::Cursor;
22-
use crate::ln::channelmanager::PaymentId;
23+
use crate::ln::channelmanager::{PaymentId, Verification};
2324
use crate::ln::msgs::DecodeError;
24-
use crate::ln::onion_utils;
25+
use crate::ln::{inbound_payment, onion_utils};
2526
use crate::offers::nonce::Nonce;
2627
use crate::onion_message::packet::ControlTlvs;
2728
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
@@ -258,6 +259,63 @@ pub(crate) struct ForwardTlvs {
258259
pub(crate) next_blinding_override: Option<PublicKey>,
259260
}
260261

262+
/// A blank struct, representing dummy tlv prior to authentication.
263+
///
264+
/// For more details, see [`DummyTlv`].
265+
pub(crate) struct UnauthenticatedDummyTlv {}
266+
267+
impl Writeable for UnauthenticatedDummyTlv {
268+
fn write<W: Writer>(&self, _writer: &mut W) -> Result<(), io::Error> {
269+
Ok(())
270+
}
271+
}
272+
273+
impl Verification for UnauthenticatedDummyTlv {
274+
/// Constructs an HMAC to include in [`OffersContext`] for the data along with the given
275+
/// [`Nonce`].
276+
fn hmac_data(&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey) -> Hmac<Sha256> {
277+
signer::hmac_for_dummy_tlv(self, nonce, expanded_key)
278+
}
279+
280+
/// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`].
281+
fn verify_data(
282+
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
283+
) -> Result<(), ()> {
284+
signer::verify_dummy_tlv(self, hmac, nonce, expanded_key)
285+
}
286+
}
287+
288+
/// Represents the dummy TLV encoded immediately before the actual [`ReceiveTlvs`] in a blinded path.
289+
/// These TLVs are intended for the final node and are recursively authenticated and verified until
290+
/// the real [`ReceiveTlvs`] is reached.
291+
///
292+
/// Their purpose is to arbitrarily extend the path length, obscuring the receiver's position in the
293+
/// route and thereby enhancing privacy.
294+
///
295+
/// ## Authentication
296+
/// Authentication provides an additional layer of security, ensuring that the path is legitimate
297+
/// and terminates in valid [`ReceiveTlvs`] data. Verification begins with the first dummy hop and
298+
/// continues recursively until the final [`ReceiveTlvs`] is reached.
299+
///
300+
/// This prevents an attacker from crafting a bogus blinded path consisting solely of dummy tlv
301+
/// without any valid payload, which could otherwise waste resources through recursive
302+
/// processing — a potential vector for DoS-like attacks.
303+
pub(crate) struct DummyTlv {
304+
pub(crate) dummy_tlv: UnauthenticatedDummyTlv,
305+
/// An HMAC of `tlvs` along with a nonce used to construct it.
306+
pub(crate) authentication: (Hmac<Sha256>, Nonce),
307+
}
308+
309+
impl Writeable for DummyTlv {
310+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
311+
encode_tlv_stream!(writer, {
312+
(65539, self.authentication, required),
313+
});
314+
315+
Ok(())
316+
}
317+
}
318+
261319
/// Similar to [`ForwardTlvs`], but these TLVs are for the final node.
262320
pub(crate) struct ReceiveTlvs {
263321
/// If `context` is `Some`, it is used to identify the blinded path that this onion message is

lightning/src/offers/signer.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
//! Utilities for signing offer messages and verifying metadata.
1111
12+
use crate::blinded_path::message::UnauthenticatedDummyTlv;
1213
use crate::blinded_path::payment::UnauthenticatedReceiveTlvs;
1314
use crate::ln::channelmanager::PaymentId;
1415
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
@@ -570,3 +571,26 @@ pub(crate) fn verify_held_htlc_available_context(
570571
Err(())
571572
}
572573
}
574+
575+
pub(crate) fn hmac_for_dummy_tlv(
576+
tlvs: &UnauthenticatedDummyTlv, nonce: Nonce, expanded_key: &ExpandedKey,
577+
) -> Hmac<Sha256> {
578+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Msgs Dummies";
579+
let mut hmac = expanded_key.hmac_for_offer();
580+
hmac.input(IV_BYTES);
581+
hmac.input(&nonce.0);
582+
hmac.input(PAYMENT_TLVS_HMAC_INPUT);
583+
tlvs.write(&mut hmac).unwrap();
584+
585+
Hmac::from_engine(hmac)
586+
}
587+
588+
pub(crate) fn verify_dummy_tlv(
589+
tlvs: &UnauthenticatedDummyTlv, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &ExpandedKey,
590+
) -> Result<(), ()> {
591+
if hmac_for_dummy_tlv(tlvs, nonce, expanded_key) == hmac {
592+
Ok(())
593+
} else {
594+
Err(())
595+
}
596+
}

lightning/src/onion_message/packet.rs

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use super::async_payments::AsyncPaymentsMessage;
1717
use super::dns_resolution::DNSResolverMessage;
1818
use super::messenger::CustomOnionMessageHandler;
1919
use super::offers::OffersMessage;
20-
use crate::blinded_path::message::{BlindedMessagePath, ForwardTlvs, NextMessageHop, ReceiveTlvs};
20+
use crate::blinded_path::message::{
21+
BlindedMessagePath, DummyTlv, ForwardTlvs, NextMessageHop, ReceiveTlvs, UnauthenticatedDummyTlv,
22+
};
2123
use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter};
2224
use crate::ln::msgs::DecodeError;
2325
use crate::ln::onion_utils;
@@ -111,6 +113,8 @@ impl LengthReadable for Packet {
111113
pub(super) enum Payload<T: OnionMessageContents> {
112114
/// This payload is for an intermediate hop.
113115
Forward(ForwardControlTlvs),
116+
/// This payload is dummy, and is inteded to be peeled.
117+
Dummy(DummyControlTlvs),
114118
/// This payload is for the final hop.
115119
Receive { control_tlvs: ReceiveControlTlvs, reply_path: Option<BlindedMessagePath>, message: T },
116120
}
@@ -204,6 +208,11 @@ pub(super) enum ForwardControlTlvs {
204208
Unblinded(ForwardTlvs),
205209
}
206210

211+
pub(super) enum DummyControlTlvs {
212+
/// See [`ForwardControlTlvs::Unblinded`]
213+
Unblinded(DummyTlv),
214+
}
215+
207216
/// Receive control TLVs in their blinded and unblinded form.
208217
pub(super) enum ReceiveControlTlvs {
209218
/// See [`ForwardControlTlvs::Blinded`].
@@ -234,6 +243,10 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
234243
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
235244
_encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) })
236245
},
246+
Payload::Dummy(DummyControlTlvs::Unblinded(control_tlvs)) => {
247+
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
248+
_encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) })
249+
},
237250
Payload::Receive {
238251
control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs),
239252
reply_path,
@@ -310,6 +323,9 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(Sh
310323
}
311324
Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs)))
312325
},
326+
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Dummy(tlvs) }) => {
327+
Ok(Payload::Dummy(DummyControlTlvs::Unblinded(tlvs)))
328+
},
313329
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => {
314330
Ok(Payload::Receive {
315331
control_tlvs: ReceiveControlTlvs::Unblinded(tlvs),
@@ -328,6 +344,8 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(Sh
328344
pub(crate) enum ControlTlvs {
329345
/// This onion message is intended to be forwarded.
330346
Forward(ForwardTlvs),
347+
/// This onion message is a dummy, and is intended to be peeled.
348+
Dummy(DummyTlv),
331349
/// This onion message is intended to be received.
332350
Receive(ReceiveTlvs),
333351
}
@@ -343,6 +361,7 @@ impl Readable for ControlTlvs {
343361
(4, next_node_id, option),
344362
(8, next_blinding_override, option),
345363
(65537, context, option),
364+
(65539, authentication, option),
346365
});
347366

348367
let next_hop = match (short_channel_id, next_node_id) {
@@ -352,18 +371,16 @@ impl Readable for ControlTlvs {
352371
(None, None) => None,
353372
};
354373

355-
let valid_fwd_fmt = next_hop.is_some();
356-
let valid_recv_fmt = next_hop.is_none() && next_blinding_override.is_none();
357-
358-
let payload_fmt = if valid_fwd_fmt {
359-
ControlTlvs::Forward(ForwardTlvs {
360-
next_hop: next_hop.unwrap(),
361-
next_blinding_override,
362-
})
363-
} else if valid_recv_fmt {
364-
ControlTlvs::Receive(ReceiveTlvs { context })
365-
} else {
366-
return Err(DecodeError::InvalidValue);
374+
let payload_fmt = match (next_hop, next_blinding_override, authentication) {
375+
(Some(hop), _, None) => {
376+
ControlTlvs::Forward(ForwardTlvs { next_hop: hop, next_blinding_override })
377+
},
378+
(None, None, Some(auth)) => {
379+
let tlv = DummyTlv { dummy_tlv: UnauthenticatedDummyTlv {}, authentication: auth };
380+
ControlTlvs::Dummy(tlv)
381+
},
382+
(None, None, None) => ControlTlvs::Receive(ReceiveTlvs { context }),
383+
_ => return Err(DecodeError::InvalidValue),
367384
};
368385

369386
Ok(payload_fmt)
@@ -374,6 +391,7 @@ impl Writeable for ControlTlvs {
374391
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
375392
match self {
376393
Self::Forward(tlvs) => tlvs.write(w),
394+
Self::Dummy(tlvs) => tlvs.write(w),
377395
Self::Receive(tlvs) => tlvs.write(w),
378396
}
379397
}

0 commit comments

Comments
 (0)