Skip to content

Commit 3ec4df5

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 3ec4df5

File tree

3 files changed

+150
-15
lines changed

3 files changed

+150
-15
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 91 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,94 @@ pub(crate) struct ForwardTlvs {
258259
pub(crate) next_blinding_override: Option<PublicKey>,
259260
}
260261

262+
/// Represents a dummy TLV that can be used in a blinded path to extend the path length.
263+
/// The first dummy TLV is authenticated and contains an HMAC, while subsequent dummy TLVs are
264+
/// empty and do not contain any data. This allows for the path length to be extended without
265+
/// adding any additional data, while still ensuring that the path is legitimate and terminates
266+
/// in valid [`ReceiveTlvs`] data.
267+
pub(crate) enum DummyTlv {
268+
/// The first dummy TLV, which contains an HMAC and is authenticated.
269+
/// This TLV is used to ensure that the path is legitimate and terminates in valid
270+
/// [`ReceiveTlvs`] data.
271+
Primary(PrimaryDummyTlv),
272+
/// Subsequent dummy TLVs, which are empty and do not contain any data.
273+
/// These TLVs are used to extend the path length without adding any additional data.
274+
Subsequent,
275+
}
276+
277+
impl Writeable for DummyTlv {
278+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
279+
match self {
280+
DummyTlv::Primary(primary) => primary.write(writer)?,
281+
DummyTlv::Subsequent => {
282+
// Subsequent dummy TLVs are empty, so we don't write anything.
283+
// This is to ensure that the path length can be extended without
284+
// adding any additional data.
285+
encode_tlv_stream!(writer, {
286+
(65541, (), required), // Represents that this is a Dummy Tlv variant
287+
})
288+
},
289+
}
290+
Ok(())
291+
}
292+
}
293+
294+
/// Represents the first dummy TLV in a blinded path, which is authenticated and contains an HMAC.
295+
/// These TLV are intended for the final node.
296+
///
297+
/// ## Authentication
298+
/// Authentication provides an additional layer of security, ensuring that the path is legitimate
299+
/// and terminates in valid [`ReceiveTlvs`] data. Verification begins with the first dummy hop and
300+
/// continues recursively until the final [`ReceiveTlvs`] is reached.
301+
///
302+
/// This prevents an attacker from crafting a bogus blinded path consisting solely of dummy tlv
303+
/// without any valid payload, which could otherwise waste resources through recursive
304+
/// processing — a potential vector for DoS-like attacks.
305+
pub(crate) struct PrimaryDummyTlv {
306+
/// The dummy TLV that holds the empty data for the Dummy Hop.
307+
pub(crate) dummy_tlv: UnauthenticatedDummyTlv,
308+
/// An HMAC of `tlvs` along with a nonce used to construct it.
309+
pub(crate) authentication: (Hmac<Sha256>, Nonce),
310+
}
311+
312+
impl Writeable for PrimaryDummyTlv {
313+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
314+
encode_tlv_stream!(writer, {
315+
(65539, self.authentication, required),
316+
// The Some(()) represents that this is a Dummy Tlv variant
317+
(65541, (), required),
318+
});
319+
320+
Ok(())
321+
}
322+
}
323+
324+
/// A blank struct, representing a dummy TLV prior to authentication.
325+
///
326+
/// For more details, see [`PrimaryDummyTlv`].
327+
pub(crate) struct UnauthenticatedDummyTlv {}
328+
329+
impl Writeable for UnauthenticatedDummyTlv {
330+
fn write<W: Writer>(&self, _writer: &mut W) -> Result<(), io::Error> {
331+
Ok(())
332+
}
333+
}
334+
335+
impl Verification for UnauthenticatedDummyTlv {
336+
/// Constructs an HMAC to include in [`OffersContext`] for the data along with the given
337+
/// [`Nonce`].
338+
fn hmac_data(&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey) -> Hmac<Sha256> {
339+
signer::hmac_for_dummy_tlv(self, nonce, expanded_key)
340+
}
341+
342+
/// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`].
343+
fn verify_data(
344+
&self, hmac: Hmac<Sha256>, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey,
345+
) -> Result<(), ()> {
346+
signer::verify_dummy_tlv(self, hmac, nonce, expanded_key)
347+
}
348+
}
349+
261350
/// Similar to [`ForwardTlvs`], but these TLVs are for the final node.
262351
pub(crate) struct ReceiveTlvs {
263352
/// 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: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ 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, PrimaryDummyTlv, ReceiveTlvs,
22+
UnauthenticatedDummyTlv,
23+
};
2124
use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter};
2225
use crate::ln::msgs::DecodeError;
2326
use crate::ln::onion_utils;
@@ -111,6 +114,8 @@ impl LengthReadable for Packet {
111114
pub(super) enum Payload<T: OnionMessageContents> {
112115
/// This payload is for an intermediate hop.
113116
Forward(ForwardControlTlvs),
117+
/// This payload is dummy, and is inteded to be peeled.
118+
Dummy(DummyControlTlvs),
114119
/// This payload is for the final hop.
115120
Receive { control_tlvs: ReceiveControlTlvs, reply_path: Option<BlindedMessagePath>, message: T },
116121
}
@@ -204,6 +209,11 @@ pub(super) enum ForwardControlTlvs {
204209
Unblinded(ForwardTlvs),
205210
}
206211

212+
pub(super) enum DummyControlTlvs {
213+
/// See [`ForwardControlTlvs::Unblinded`]
214+
Unblinded(DummyTlv),
215+
}
216+
207217
/// Receive control TLVs in their blinded and unblinded form.
208218
pub(super) enum ReceiveControlTlvs {
209219
/// See [`ForwardControlTlvs::Blinded`].
@@ -234,6 +244,10 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
234244
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
235245
_encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) })
236246
},
247+
Payload::Dummy(DummyControlTlvs::Unblinded(control_tlvs)) => {
248+
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
249+
_encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) })
250+
},
237251
Payload::Receive {
238252
control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs),
239253
reply_path,
@@ -310,6 +324,9 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(Sh
310324
}
311325
Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs)))
312326
},
327+
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Dummy(tlvs) }) => {
328+
Ok(Payload::Dummy(DummyControlTlvs::Unblinded(tlvs)))
329+
},
313330
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => {
314331
Ok(Payload::Receive {
315332
control_tlvs: ReceiveControlTlvs::Unblinded(tlvs),
@@ -328,6 +345,8 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(Sh
328345
pub(crate) enum ControlTlvs {
329346
/// This onion message is intended to be forwarded.
330347
Forward(ForwardTlvs),
348+
/// This onion message is a dummy, and is intended to be peeled.
349+
Dummy(DummyTlv),
331350
/// This onion message is intended to be received.
332351
Receive(ReceiveTlvs),
333352
}
@@ -343,6 +362,8 @@ impl Readable for ControlTlvs {
343362
(4, next_node_id, option),
344363
(8, next_blinding_override, option),
345364
(65537, context, option),
365+
(65539, authentication, option),
366+
(65541, dummy_tlv, option),
346367
});
347368

348369
let next_hop = match (short_channel_id, next_node_id) {
@@ -352,18 +373,18 @@ impl Readable for ControlTlvs {
352373
(None, None) => None,
353374
};
354375

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);
376+
let payload_fmt = match (dummy_tlv, next_hop, next_blinding_override, authentication) {
377+
(None, Some(hop), _, None) => {
378+
ControlTlvs::Forward(ForwardTlvs { next_hop: hop, next_blinding_override })
379+
},
380+
(None, None, None, None) => ControlTlvs::Receive(ReceiveTlvs { context }),
381+
(Some(()), None, None, Some(auth)) => {
382+
let tlv =
383+
PrimaryDummyTlv { dummy_tlv: UnauthenticatedDummyTlv {}, authentication: auth };
384+
ControlTlvs::Dummy(DummyTlv::Primary(tlv))
385+
},
386+
(Some(()), None, None, None) => ControlTlvs::Dummy(DummyTlv::Subsequent),
387+
_ => return Err(DecodeError::InvalidValue),
367388
};
368389

369390
Ok(payload_fmt)
@@ -374,6 +395,7 @@ impl Writeable for ControlTlvs {
374395
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
375396
match self {
376397
Self::Forward(tlvs) => tlvs.write(w),
398+
Self::Dummy(tlvs) => tlvs.write(w),
377399
Self::Receive(tlvs) => tlvs.write(w),
378400
}
379401
}

0 commit comments

Comments
 (0)