Skip to content
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
57 changes: 50 additions & 7 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,50 @@ impl BlindedMessagePath {
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, context: MessageContext,
compact_padding: bool, entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Self {
Self::new_inner(
intermediate_nodes,
recipient_node_id,
dummy_hop_count,
Some(local_node_receive_key),
Some(context),
compact_padding,
entropy_source,
secp_ctx,
)
}

/// Create a path for a message to a recipient that is not an LDK node (e.g. CLN).
///
/// The other constructors authenticate the recipient hop with a [`ReceiveAuthKey`] and require a
/// [`MessageContext`], which a non-LDK node cannot decrypt. This drops deanonymization protections
/// and adds no dummy hops.
///
/// See [`BlindedMessagePath::new`] regarding `compact_padding`.
pub fn new_for_external_recipient<
ES: EntropySource,
T: secp256k1::Signing + secp256k1::Verification,
>(
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
compact_padding: bool, entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Self {
Self::new_inner(
intermediate_nodes,
recipient_node_id,
0,
None,
None,
compact_padding,
entropy_source,
secp_ctx,
)
}

fn new_inner<ES: EntropySource, T: secp256k1::Signing + secp256k1::Verification>(
intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
dummy_hop_count: usize, local_node_receive_key: Option<ReceiveAuthKey>,
context: Option<MessageContext>, compact_padding: bool, entropy_source: ES,
secp_ctx: &Secp256k1<T>,
) -> Self {
let introduction_node = IntroductionNode::NodeId(
intermediate_nodes.first().map_or(recipient_node_id, |n| n.node_id),
Expand Down Expand Up @@ -760,17 +804,16 @@ pub 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, dummy_hop_count: usize, context: MessageContext,
session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, compact_padding: bool,
recipient_node_id: PublicKey, dummy_hop_count: usize, context: Option<MessageContext>,
session_priv: &SecretKey, local_node_receive_key: Option<ReceiveAuthKey>,
compact_padding: bool,
) -> Vec<BlindedHop> {
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))));
.chain(core::iter::repeat((recipient_node_id, local_node_receive_key)).take(dummy_count))
.chain(core::iter::once((recipient_node_id, local_node_receive_key)));

let intermediate_tlvs = pks
.clone()
Expand Down Expand Up @@ -816,7 +859,7 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
res
})
.chain(core::iter::once(BlindedPathWithPadding {
tlvs: ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }),
tlvs: ControlTlvs::Receive(ReceiveTlvs { context }),
round_off: if compact_padding { 0 } else { MESSAGE_PADDING_ROUND_OFF },
}));

Expand Down
102 changes: 90 additions & 12 deletions lightning/src/blinded_path/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,35 @@ impl BlindedPaymentPath {
BlindedPaymentPath::new_inner(
intermediate_nodes,
payee_node_id,
local_node_receive_key,
Some(local_node_receive_key),
&[],
payee_tlvs,
htlc_maximum_msat,
min_final_cltv_expiry_delta,
entropy_source,
secp_ctx,
)
}

/// Create a blinded path for a payment to a recipient that is not an LDK node (e.g. CLN).
///
/// The other constructors authenticate the payee hop with a [`ReceiveAuthKey`], which a non-LDK
/// node cannot decrypt. This instead builds a standard BOLT-4 payee hop (no [`ReceiveAuthKey`]),
/// dropping the [`ReceiveAuthKey`]-based deanonymization protections and adding no dummy hops.
///
/// See [`BlindedPaymentPath::new`] regarding errors.
pub fn new_for_external_recipient<
ES: EntropySource,
T: secp256k1::Signing + secp256k1::Verification,
>(
intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()> {
BlindedPaymentPath::new_inner(
intermediate_nodes,
payee_node_id,
None,
&[],
payee_tlvs,
htlc_maximum_msat,
Expand Down Expand Up @@ -153,7 +181,7 @@ impl BlindedPaymentPath {
BlindedPaymentPath::new_inner(
intermediate_nodes,
payee_node_id,
local_node_receive_key,
Some(local_node_receive_key),
dummy_tlvs,
payee_tlvs,
htlc_maximum_msat,
Expand All @@ -176,7 +204,7 @@ impl BlindedPaymentPath {
Self::new_inner(
intermediate_nodes,
payee_node_id,
local_node_receive_key,
Some(local_node_receive_key),
&[],
payee_tlvs,
htlc_maximum_msat,
Expand All @@ -192,9 +220,9 @@ impl BlindedPaymentPath {
T: secp256k1::Signing + secp256k1::Verification,
>(
intermediate_nodes: &[ForwardNode<F>], payee_node_id: PublicKey,
local_node_receive_key: ReceiveAuthKey, dummy_tlvs: &[DummyTlvs], payee_tlvs: ReceiveTlvs,
htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES,
secp_ctx: &Secp256k1<T>,
local_node_receive_key: Option<ReceiveAuthKey>, dummy_tlvs: &[DummyTlvs],
payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()> {
let introduction_node = IntroductionNode::NodeId(
intermediate_nodes.first().map_or(payee_node_id, |n| n.node_id),
Expand Down Expand Up @@ -884,13 +912,13 @@ pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30;
pub(super) fn blinded_hops<F: ForwardTlvsInfo, T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[ForwardNode<F>], payee_node_id: PublicKey,
dummy_tlvs: &[DummyTlvs], payee_tlvs: ReceiveTlvs, session_priv: &SecretKey,
local_node_receive_key: ReceiveAuthKey,
local_node_receive_key: Option<ReceiveAuthKey>,
) -> Vec<BlindedHop> {
let pks = intermediate_nodes
.iter()
.map(|node| (node.node_id, None))
.chain(dummy_tlvs.iter().map(|_| (payee_node_id, Some(local_node_receive_key))))
.chain(core::iter::once((payee_node_id, Some(local_node_receive_key))));
.chain(dummy_tlvs.iter().map(|_| (payee_node_id, local_node_receive_key)))
.chain(core::iter::once((payee_node_id, local_node_receive_key)));
let tlvs = intermediate_nodes
.iter()
.map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs))
Expand Down Expand Up @@ -1126,13 +1154,19 @@ impl_ser_tlv_based!(Bolt12RefundContext, {
#[cfg(test)]
mod tests {
use crate::blinded_path::payment::{
Bolt12RefundContext, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentForwardNode,
PaymentRelay, ReceiveTlvs,
BlindedPaymentPath, BlindedPaymentTlvs, Bolt12RefundContext, ForwardTlvs,
PaymentConstraints, PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs,
};
use crate::crypto::streams::ChaChaPolyReadAdapter;
use crate::io;
use crate::ln::functional_test_utils::TEST_FINAL_CLTV;
use crate::ln::onion_utils::gen_rho_from_shared_secret;
use crate::sign::RandomBytes;
use crate::types::features::BlindedHopFeatures;
use crate::types::payment::PaymentSecret;
use bitcoin::secp256k1::PublicKey;
use crate::util::ser::{FixedLengthReader, LengthReadableArgs};
use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};

#[test]
fn compute_payinfo() {
Expand Down Expand Up @@ -1430,4 +1464,48 @@ mod tests {
};
assert!(super::amt_to_forward_msat(2, &payment_relay).is_none());
}

#[test]
fn blinded_path_for_external_recipient() {
// Check the payee hop of a path for a non-LDK recipient can be read by a spec-standard
// ChaCha20Poly1305 decryptor keyed on `rho` (no `ReceiveAuthKey`, no swapped AAD).
let secp_ctx = Secp256k1::new();
let entropy_source = RandomBytes::new([42; 32]);
let payee_secret = SecretKey::from_slice(&[2; 32]).unwrap();
let payee_node_id = PublicKey::from_secret_key(&secp_ctx, &payee_secret);
let payment_secret = PaymentSecret([3; 32]);
let payee_tlvs = ReceiveTlvs {
payment_secret,
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {
payment_metadata: None,
}),
};

let path = BlindedPaymentPath::new_for_external_recipient(
&[],
payee_node_id,
payee_tlvs,
u64::max_value(),
TEST_FINAL_CLTV as u16,
&entropy_source,
&secp_ctx,
)
.unwrap();

// The payee hop must decrypt under standard BOLT-4 rho, i.e. without swapped-AAD framing.
let ss = SharedSecret::new(&path.blinding_point(), &payee_secret);
let rho = gen_rho_from_shared_secret(&ss.secret_bytes());
let encrypted_payload = &path.blinded_hops()[0].encrypted_payload;
let mut s = io::Cursor::new(encrypted_payload);
let mut reader = FixedLengthReader::new(&mut s, encrypted_payload.len() as u64);
let ChaChaPolyReadAdapter { readable } =
<ChaChaPolyReadAdapter<BlindedPaymentTlvs>>::read(&mut reader, rho).unwrap();
match readable {
BlindedPaymentTlvs::Receive(tlvs) => {
assert_eq!(tlvs.payment_secret, payment_secret);
},
_ => panic!("Expected a receive-hop payment TLV"),
}
}
}
44 changes: 41 additions & 3 deletions lightning/src/onion_message/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,27 @@ use super::messenger::{
OnionMessagePath, OnionMessenger, Responder, ResponseInstruction, SendError, SendSuccess,
};
use super::offers::{OffersMessage, OffersMessageHandler};
use super::packet::{OnionMessageContents, Packet};
use super::packet::{ControlTlvs, OnionMessageContents, Packet};
use crate::blinded_path::message::{
AsyncPaymentsContext, BlindedMessagePath, DNSResolverContext, MessageContext,
MessageForwardNode, NextMessageHop, OffersContext, MESSAGE_PADDING_ROUND_OFF,
MessageForwardNode, NextMessageHop, OffersContext, ReceiveTlvs, MESSAGE_PADDING_ROUND_OFF,
};
use crate::blinded_path::utils::is_padded;
use crate::blinded_path::NodeIdLookUp;
use crate::crypto::streams::ChaChaPolyReadAdapter;
use crate::events::{Event, EventsProvider};
use crate::ln::msgs::{self, BaseMessageHandler, DecodeError, OnionMessageHandler};
use crate::ln::onion_utils::gen_rho_from_shared_secret;
use crate::routing::gossip::{NetworkGraph, P2PGossipSync};
use crate::routing::test_utils::{add_channel, add_or_update_node};
use crate::sign::{NodeSigner, Recipient};
use crate::types::features::{ChannelFeatures, InitFeatures};
use crate::util::ser::{FixedLengthReader, LengthReadable, Writeable, Writer};
use crate::util::ser::{FixedLengthReader, LengthReadable, LengthReadableArgs, Writeable, Writer};
use crate::util::test_utils::{TestChainSource, TestKeysInterface, TestLogger, TestNodeSigner};

use bitcoin::hex::FromHex;
use bitcoin::network::Network;
use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::{All, PublicKey, Secp256k1, SecretKey};

use crate::io;
Expand Down Expand Up @@ -480,6 +483,41 @@ fn one_blinded_hop() {
pass_along_path(&nodes);
}

#[test]
fn blinded_path_for_external_recipient() {
// Check a path for a non-LDK recipient is delivered, and that its recipient hop can be read by a
// spec-standard ChaCha20Poly1305 decryptor with no context.
let nodes = create_nodes(2);
let test_msg = TestCustomMessage::Pong;

let secp_ctx = Secp256k1::new();
let entropy = &*nodes[1].entropy_source;
let node_id = nodes[1].node_id;
let blinded_path =
BlindedMessagePath::new_for_external_recipient(&[], node_id, false, entropy, &secp_ctx);

// The recipient hop must decrypt under standard ChaCha20Poly1305 (empty AAD) with no context.
{
let ss = SharedSecret::new(&blinded_path.blinding_point(), &nodes[1].privkey);
let rho = gen_rho_from_shared_secret(&ss.secret_bytes());
let encrypted_payload = &blinded_path.blinded_hops()[0].encrypted_payload;
let mut s = io::Cursor::new(encrypted_payload);
let mut reader = FixedLengthReader::new(&mut s, encrypted_payload.len() as u64);
let ChaChaPolyReadAdapter { readable } =
<ChaChaPolyReadAdapter<ControlTlvs>>::read(&mut reader, rho).unwrap();
match readable {
ControlTlvs::Receive(ReceiveTlvs { context }) => assert!(context.is_none()),
_ => panic!("Expected a receive-hop control TLV"),
}
}

let destination = Destination::BlindedPath(blinded_path);
let instructions = MessageSendInstructions::WithoutReplyPath { destination };
nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap();
nodes[1].custom_message_handler.expect_message(TestCustomMessage::Pong);
pass_along_path(&nodes);
}

#[test]
fn blinded_path_with_dummy_hops() {
let nodes = create_nodes(2);
Expand Down
11 changes: 11 additions & 0 deletions pending_changelog/blinded-path-external-recipient.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# API Updates

* `BlindedMessagePath::new_for_external_recipient` was added to build a blinded message path whose
recipient hop terminates at a non-LDK node (e.g. CLN). It builds a standard BOLT-4 recipient hop
with no `ReceiveAuthKey` and no `MessageContext`, so that such a recipient can decrypt its
`encrypted_recipient_data`, dropping the `ReceiveAuthKey`-based deanonymization protections and
adding no dummy hops.
* `BlindedPaymentPath::new_for_external_recipient` was added to build a blinded payment path whose
payee hop terminates at a non-LDK node (e.g. CLN). It builds a standard BOLT-4 payee hop with no
`ReceiveAuthKey`, so that such a recipient can decrypt its `encrypted_recipient_data`, dropping
the `ReceiveAuthKey`-based deanonymization protections and adding no dummy hops.
Loading