Skip to content

Commit d6dee2b

Browse files
committed
Add a BlindedMessagePath constructor for external recipients
Add `new_for_external_recipient`, which builds a recipient hop using standard BOLT-4 ChaCha20Poly1305 (empty AAD) and no `MessageContext` or deanonymisation protections. Tests by Claude
1 parent 1c1a4ad commit d6dee2b

2 files changed

Lines changed: 91 additions & 10 deletions

File tree

lightning/src/blinded_path/message.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,50 @@ impl BlindedMessagePath {
113113
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
114114
dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, context: MessageContext,
115115
compact_padding: bool, entropy_source: ES, secp_ctx: &Secp256k1<T>,
116+
) -> Self {
117+
Self::new_inner(
118+
intermediate_nodes,
119+
recipient_node_id,
120+
dummy_hop_count,
121+
Some(local_node_receive_key),
122+
Some(context),
123+
compact_padding,
124+
entropy_source,
125+
secp_ctx,
126+
)
127+
}
128+
129+
/// Create a path for a message to a recipient that is not an LDK node (e.g. CLN).
130+
///
131+
/// The other constructors authenticate the recipient hop with a [`ReceiveAuthKey`] and require a
132+
/// [`MessageContext`], which a non-LDK node cannot decrypt. This drops deanonymization protections
133+
/// and adds no dummy hops.
134+
///
135+
/// See [`BlindedMessagePath::new`] regarding `compact_padding`.
136+
pub fn new_for_external_recipient<
137+
ES: EntropySource,
138+
T: secp256k1::Signing + secp256k1::Verification,
139+
>(
140+
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
141+
compact_padding: bool, entropy_source: ES, secp_ctx: &Secp256k1<T>,
142+
) -> Self {
143+
Self::new_inner(
144+
intermediate_nodes,
145+
recipient_node_id,
146+
0,
147+
None,
148+
None,
149+
compact_padding,
150+
entropy_source,
151+
secp_ctx,
152+
)
153+
}
154+
155+
fn new_inner<ES: EntropySource, T: secp256k1::Signing + secp256k1::Verification>(
156+
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
157+
dummy_hop_count: usize, local_node_receive_key: Option<ReceiveAuthKey>,
158+
context: Option<MessageContext>, compact_padding: bool, entropy_source: ES,
159+
secp_ctx: &Secp256k1<T>,
116160
) -> Self {
117161
let introduction_node = IntroductionNode::NodeId(
118162
intermediate_nodes.first().map_or(recipient_node_id, |n| n.node_id),
@@ -760,17 +804,16 @@ pub const MAX_DUMMY_HOPS_COUNT: usize = 10;
760804
/// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`.
761805
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
762806
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
763-
recipient_node_id: PublicKey, dummy_hop_count: usize, context: MessageContext,
764-
session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, compact_padding: bool,
807+
recipient_node_id: PublicKey, dummy_hop_count: usize, context: Option<MessageContext>,
808+
session_priv: &SecretKey, local_node_receive_key: Option<ReceiveAuthKey>,
809+
compact_padding: bool,
765810
) -> Vec<BlindedHop> {
766811
let dummy_count = cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT);
767812
let pks = intermediate_nodes
768813
.iter()
769814
.map(|node| (node.node_id, None))
770-
.chain(
771-
core::iter::repeat((recipient_node_id, Some(local_node_receive_key))).take(dummy_count),
772-
)
773-
.chain(core::iter::once((recipient_node_id, Some(local_node_receive_key))));
815+
.chain(core::iter::repeat((recipient_node_id, local_node_receive_key)).take(dummy_count))
816+
.chain(core::iter::once((recipient_node_id, local_node_receive_key)));
774817

775818
let intermediate_tlvs = pks
776819
.clone()
@@ -816,7 +859,7 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
816859
res
817860
})
818861
.chain(core::iter::once(BlindedPathWithPadding {
819-
tlvs: ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }),
862+
tlvs: ControlTlvs::Receive(ReceiveTlvs { context }),
820863
round_off: if compact_padding { 0 } else { MESSAGE_PADDING_ROUND_OFF },
821864
}));
822865

lightning/src/onion_message/functional_tests.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,27 @@ use super::messenger::{
2121
OnionMessagePath, OnionMessenger, Responder, ResponseInstruction, SendError, SendSuccess,
2222
};
2323
use super::offers::{OffersMessage, OffersMessageHandler};
24-
use super::packet::{OnionMessageContents, Packet};
24+
use super::packet::{ControlTlvs, OnionMessageContents, Packet};
2525
use crate::blinded_path::message::{
2626
AsyncPaymentsContext, BlindedMessagePath, DNSResolverContext, MessageContext,
27-
MessageForwardNode, NextMessageHop, OffersContext, MESSAGE_PADDING_ROUND_OFF,
27+
MessageForwardNode, NextMessageHop, OffersContext, ReceiveTlvs, MESSAGE_PADDING_ROUND_OFF,
2828
};
2929
use crate::blinded_path::utils::is_padded;
3030
use crate::blinded_path::NodeIdLookUp;
31+
use crate::crypto::streams::ChaChaPolyReadAdapter;
3132
use crate::events::{Event, EventsProvider};
3233
use crate::ln::msgs::{self, BaseMessageHandler, DecodeError, OnionMessageHandler};
34+
use crate::ln::onion_utils::gen_rho_from_shared_secret;
3335
use crate::routing::gossip::{NetworkGraph, P2PGossipSync};
3436
use crate::routing::test_utils::{add_channel, add_or_update_node};
3537
use crate::sign::{NodeSigner, Recipient};
3638
use crate::types::features::{ChannelFeatures, InitFeatures};
37-
use crate::util::ser::{FixedLengthReader, LengthReadable, Writeable, Writer};
39+
use crate::util::ser::{FixedLengthReader, LengthReadable, LengthReadableArgs, Writeable, Writer};
3840
use crate::util::test_utils::{TestChainSource, TestKeysInterface, TestLogger, TestNodeSigner};
3941

4042
use bitcoin::hex::FromHex;
4143
use bitcoin::network::Network;
44+
use bitcoin::secp256k1::ecdh::SharedSecret;
4245
use bitcoin::secp256k1::{All, PublicKey, Secp256k1, SecretKey};
4346

4447
use crate::io;
@@ -480,6 +483,41 @@ fn one_blinded_hop() {
480483
pass_along_path(&nodes);
481484
}
482485

486+
#[test]
487+
fn blinded_path_for_external_recipient() {
488+
// Check a path for a non-LDK recipient is delivered, and that its recipient hop can be read by a
489+
// spec-standard ChaCha20Poly1305 decryptor with no context.
490+
let nodes = create_nodes(2);
491+
let test_msg = TestCustomMessage::Pong;
492+
493+
let secp_ctx = Secp256k1::new();
494+
let entropy = &*nodes[1].entropy_source;
495+
let node_id = nodes[1].node_id;
496+
let blinded_path =
497+
BlindedMessagePath::new_for_external_recipient(&[], node_id, false, entropy, &secp_ctx);
498+
499+
// The recipient hop must decrypt under standard ChaCha20Poly1305 (empty AAD) with no context.
500+
{
501+
let ss = SharedSecret::new(&blinded_path.blinding_point(), &nodes[1].privkey);
502+
let rho = gen_rho_from_shared_secret(&ss.secret_bytes());
503+
let encrypted_payload = &blinded_path.blinded_hops()[0].encrypted_payload;
504+
let mut s = io::Cursor::new(encrypted_payload);
505+
let mut reader = FixedLengthReader::new(&mut s, encrypted_payload.len() as u64);
506+
let ChaChaPolyReadAdapter { readable } =
507+
<ChaChaPolyReadAdapter<ControlTlvs>>::read(&mut reader, rho).unwrap();
508+
match readable {
509+
ControlTlvs::Receive(ReceiveTlvs { context }) => assert!(context.is_none()),
510+
_ => panic!("Expected a receive-hop control TLV"),
511+
}
512+
}
513+
514+
let destination = Destination::BlindedPath(blinded_path);
515+
let instructions = MessageSendInstructions::WithoutReplyPath { destination };
516+
nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap();
517+
nodes[1].custom_message_handler.expect_message(TestCustomMessage::Pong);
518+
pass_along_path(&nodes);
519+
}
520+
483521
#[test]
484522
fn blinded_path_with_dummy_hops() {
485523
let nodes = create_nodes(2);

0 commit comments

Comments
 (0)