Accept blinded paths built by a phantom node participant
What changed, and why it matters
This commit prepares the Lightning Dev Kit (LDK) to support a new 'phantom node' feature for BOLT 12 offers. It changes how encrypted control data in blinded payment paths and onion messages is authenticated: instead of checking one extra node-specific key, it now checks two keys (the existing node-specific key and a new shared phantom-node key). The commit itself does not enable the actual phantom-node building logic yet, but it adds the cryptographic plumbing so that future versions can authenticate blinded paths from any participant in a phantom-node setup while still keeping normal onion messages tied to a specific node.
Review the new ExpandedKey.phantom_node_blinded_path_key derivation for correctness and independence from the ReceiveAuthKey. Ensure that the TriPolyAADUsed logic cannot be confused by an attacker who crafts a tag that validates under an unintended AAD (e.g., collision between no-AAD and AAD MACs). Verify that downstream consumers of control_tlvs_from_phantom_participant enforce the intended restricted usage once the feature is fully enabled. Treat this as a feature commit with security-relevant plumbing rather than an active vulnerability fix.
Security signals we found
Cryptographic authentication logic changed from two MAC checks to three MAC checks
New key derivation added to ExpandedKey for phantom-node blinded paths
Onion message authentication now distinguishes local-node vs phantom-participant origin
Normal onion message contexts still require node-specific authentication
Blinded payment/trampoline TLV parsing now accepts a third authentication key
Commit message explicitly frames change as compatibility groundwork, not yet active feature
Evidence from the diff
The patch extends the ChaCha20Poly1305 read adapter from dual-AAD to triple-AAD (ChaChaDualPolyReadAdapter -> ChaChaTriPolyReadAdapter / TriPolyAADUsed). It now accepts tags validated with no AAD, the existing ReceiveAuthKey-derived AAD, or a new ExpandedKey.phantom_node_blinded_path_key-derived AAD. In blinded payment path and trampoline parsing, the new phantom key is passed in and the AAD usage is tracked. In onion message parsing, the Receive payload now distinguishes control_tlvs_from_local_node (first AAD) from control_tlvs_from_phantom_participant (second AAD), and downstream handlers require control_tlvs_from_local_node for most contexts, preserving node-specific authentication for onion messages while allowing phantom authentication only for invoice_request compatibility. The commit also updates test/fuzz signers to return a concrete ExpandedKey instead of unreachable!().
Changed components
lightning/src/crypto/streams.rslightning/src/blinded_path/payment.rslightning/src/ln/msgs.rslightning/src/onion_message/packet.rslightning/src/onion_message/messenger.rslightning/src/ln/blinded_payment_tests.rslightning/src/util/test_utils.rsfuzz/src/onion_message.rsInspect captured patch +124 / −77
diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs
index 09634a1..70dfb07 100644
--- a/fuzz/src/onion_message.rs
+++ b/fuzz/src/onion_message.rs
@@ -260,7 +260,7 @@ impl NodeSigner for KeyProvider {
}
fn get_expanded_key(&self) -> ExpandedKey {
- unreachable!()
+ ExpandedKey::new([42; 32])
}
fn sign_invoice(
diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs
index 27292ba..03b676a 100644
--- a/lightning/src/blinded_path/payment.rs
+++ b/lightning/src/blinded_path/payment.rs
@@ -14,7 +14,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
use crate::blinded_path::utils::{self, BlindedPathWithPadding};
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp};
-use crate::crypto::streams::ChaChaDualPolyReadAdapter;
+use crate::crypto::streams::{ChaChaTriPolyReadAdapter, TriPolyAADUsed};
use crate::io;
use crate::io::Cursor;
use crate::ln::channel_state::CounterpartyForwardingInfo;
@@ -268,18 +268,20 @@ impl BlindedPaymentPath {
node_signer.ecdh(Recipient::Node, &self.inner_path.blinding_point, None)?;
let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes());
let receive_auth_key = node_signer.get_receive_auth_key();
+ let phantom_auth_key = node_signer.get_expanded_key().phantom_node_blinded_path_key;
+ let read_arg = (rho, receive_auth_key.0, phantom_auth_key);
+
let encrypted_control_tlvs =
&self.inner_path.blinded_hops.get(0).ok_or(())?.encrypted_payload;
let mut s = Cursor::new(encrypted_control_tlvs);
let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64);
- let ChaChaDualPolyReadAdapter { readable, used_aad } =
- ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))
- .map_err(|_| ())?;
-
- match (&readable, used_aad) {
- (BlindedPaymentTlvs::Forward(_), false)
- | (BlindedPaymentTlvs::Dummy(_), true)
- | (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)),
+ let ChaChaTriPolyReadAdapter { readable, used_aad } =
+ ChaChaTriPolyReadAdapter::read(&mut reader, read_arg).map_err(|_| ())?;
+
+ match (&readable, used_aad == TriPolyAADUsed::None) {
+ (BlindedPaymentTlvs::Forward(_), true)
+ | (BlindedPaymentTlvs::Dummy(_), false)
+ | (BlindedPaymentTlvs::Receive(_), false) => Ok((readable, control_tlvs_ss)),
_ => Err(()),
}
}
diff --git a/lightning/src/crypto/streams.rs b/lightning/src/crypto/streams.rs
index c406e93..23a2315 100644
--- a/lightning/src/crypto/streams.rs
+++ b/lightning/src/crypto/streams.rs
@@ -58,7 +58,7 @@ impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> {
}
/// Encrypts the provided plaintext with the given key using ChaCha20Poly1305 in the modified
-/// with-AAD form used in [`ChaChaDualPolyReadAdapter`].
+/// with-AAD form used in [`ChaChaTriPolyReadAdapter`].
pub(crate) fn chachapoly_encrypt_with_swapped_aad(
mut plaintext: Vec<u8>, key: [u8; 32], aad: [u8; 32],
) -> Vec<u8> {
@@ -84,34 +84,48 @@ pub(crate) fn chachapoly_encrypt_with_swapped_aad(
plaintext
}
+#[derive(PartialEq, Eq)]
+pub(crate) enum TriPolyAADUsed {
+ /// No AAD was used.
+ ///
+ /// The HMAC validated with standard ChaCha20Poly1305.
+ None,
+ /// The HMAC vlidated using the first AAD provided.
+ First,
+ /// The HMAC vlidated using the second AAD provided.
+ Second,
+}
+
/// Enables the use of the serialization macros for objects that need to be simultaneously decrypted
/// and deserialized. This allows us to avoid an intermediate Vec allocation.
///
-/// This variant of [`ChaChaPolyReadAdapter`] calculates Poly1305 tags twice, once using the given
-/// key and once with the given 32-byte AAD appended after the encrypted stream, accepting either
-/// being correct as sufficient.
+/// This variant of [`ChaChaPolyReadAdapter`] calculates Poly1305 tags thrice, once using the given
+/// key and once each for the two given 32-byte AADs appended after the encrypted stream, accepting
+/// any being correct as sufficient.
///
-/// Note that we do *not* use the provided AAD as the standard ChaCha20Poly1305 AAD as that would
+/// Note that we do *not* use the provided AADs as the standard ChaCha20Poly1305 AAD as that would
/// require placing it first and prevent us from avoiding redundant Poly1305 rounds. Instead, the
/// ChaCha20Poly1305 MAC check is tweaked to move the AAD to *after* the the contents being
/// checked, effectively treating the contents as the AAD for the AAD-containing MAC but behaving
/// like classic ChaCha20Poly1305 for the non-AAD-containing MAC.
-pub(crate) struct ChaChaDualPolyReadAdapter<R: Readable> {
+pub(crate) struct ChaChaTriPolyReadAdapter<R: Readable> {
pub readable: R,
- pub used_aad: bool,
+ pub used_aad: TriPolyAADUsed,
}
-impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyReadAdapter<T> {
+impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32], [u8; 32])>
+ for ChaChaTriPolyReadAdapter<T>
+{
// Simultaneously read and decrypt an object from a LengthLimitedRead storing it in
// Self::readable. LengthLimitedRead must be used instead of std::io::Read because we need the
// total length to separate out the tag at the end.
fn read<R: LengthLimitedRead>(
- r: &mut R, params: ([u8; 32], [u8; 32]),
+ r: &mut R, params: ([u8; 32], [u8; 32], [u8; 32]),
) -> Result<Self, DecodeError> {
if r.remaining_bytes() < 16 {
return Err(DecodeError::InvalidValue);
}
- let (key, aad) = params;
+ let (key, aad_a, aad_b) = params;
let mut chacha = ChaCha20::new(&key[..], &[0; 12]);
let mut mac_key = [0u8; 64];
@@ -125,7 +139,7 @@ impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyRea
let decrypted_len = r.remaining_bytes() - 16;
let s = FixedLengthReader::new(r, decrypted_len);
let mut chacha_stream =
- ChaChaDualPolyReader { chacha: &mut chacha, poly: &mut mac, read_len: 0, read: s };
+ ChaChaTriPolyReader { chacha: &mut chacha, poly: &mut mac, read_len: 0, read: s };
let readable: T = Readable::read(&mut chacha_stream)?;
while chacha_stream.read.bytes_remain() {
@@ -142,14 +156,18 @@ impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyRea
mac.input(&[0; 16][0..16 - (read_len % 16)]);
}
- let mut mac_aad = mac;
+ let mut mac_aad_a = mac;
+ let mut mac_aad_b = mac;
- mac_aad.input(&aad[..]);
+ mac_aad_a.input(&aad_a[..]);
+ mac_aad_b.input(&aad_b[..]);
// Note that we don't need to pad the AAD since its a multiple of 16 bytes
// For the AAD-containing MAC, swap the AAD and the read data, effectively.
- mac_aad.input(&(read_len as u64).to_le_bytes());
- mac_aad.input(&32u64.to_le_bytes());
+ mac_aad_a.input(&(read_len as u64).to_le_bytes());
+ mac_aad_b.input(&(read_len as u64).to_le_bytes());
+ mac_aad_a.input(&32u64.to_le_bytes());
+ mac_aad_b.input(&32u64.to_le_bytes());
// For the non-AAD-containing MAC, leave the data and AAD where they belong.
mac.input(&0u64.to_le_bytes());
@@ -158,23 +176,25 @@ impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyRea
let mut tag = [0 as u8; 16];
r.read_exact(&mut tag)?;
if fixed_time_eq(&mac.result(), &tag) {
- Ok(Self { readable, used_aad: false })
- } else if fixed_time_eq(&mac_aad.result(), &tag) {
- Ok(Self { readable, used_aad: true })
+ Ok(Self { readable, used_aad: TriPolyAADUsed::None })
+ } else if fixed_time_eq(&mac_aad_a.result(), &tag) {
+ Ok(Self { readable, used_aad: TriPolyAADUsed::First })
+ } else if fixed_time_eq(&mac_aad_b.result(), &tag) {
+ Ok(Self { readable, used_aad: TriPolyAADUsed::Second })
} else {
return Err(DecodeError::InvalidValue);
}
}
}
-struct ChaChaDualPolyReader<'a, R: Read> {
+struct ChaChaTriPolyReader<'a, R: Read> {
chacha: &'a mut ChaCha20,
poly: &'a mut Poly1305,
read_len: usize,
pub read: R,
}
-impl<'a, R: Read> Read for ChaChaDualPolyReader<'a, R> {
+impl<'a, R: Read> Read for ChaChaTriPolyReader<'a, R> {
// Decrypts bytes from Self::read into `dest`.
// After all reads complete, the caller must compare the expected tag with
// the result of `Poly1305::result()`.
@@ -349,15 +369,15 @@ mod tests {
}
#[test]
- fn short_read_chacha_dual_read_adapter() {
- // Previously, if we attempted to read from a ChaChaDualPolyReadAdapter but the object
+ fn short_read_chacha_tri_read_adapter() {
+ // Previously, if we attempted to read from a ChaChaTriPolyReadAdapter but the object
// being read is shorter than the available buffer while the buffer passed to
- // ChaChaDualPolyReadAdapter itself always thinks it has room, we'd end up
+ // ChaChaTriPolyReadAdapter itself always thinks it has room, we'd end up
// infinite-looping as we didn't handle `Read::read`'s 0 return values at EOF.
let mut stream = &[0; 1024][..];
let mut too_long_stream = FixedLengthReader::new(&mut stream, 2048);
- let keys = ([42; 32], [99; 32]);
- let res = super::ChaChaDualPolyReadAdapter::<u8>::read(&mut too_long_stream, keys);
+ let keys = ([42; 32], [98; 32], [99; 32]);
+ let res = super::ChaChaTriPolyReadAdapter::<u8>::read(&mut too_long_stream, keys);
match res {
Ok(_) => panic!(),
Err(e) => assert_eq!(e, DecodeError::ShortRead),
diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs
index d78b9df..d9f3374 100644
--- a/lightning/src/ln/blinded_payment_tests.rs
+++ b/lightning/src/ln/blinded_payment_tests.rs
@@ -1696,7 +1696,7 @@ fn route_blinding_spec_test_vector() {
}
Ok(SharedSecret::new(other_key, &node_secret))
}
- fn get_expanded_key(&self) -> ExpandedKey { unreachable!() }
+ fn get_expanded_key(&self) -> ExpandedKey { ExpandedKey::new([42; 32]) }
fn get_node_id(&self, _recipient: Recipient) -> Result<PublicKey, ()> { unreachable!() }
fn sign_invoice(
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
@@ -2011,7 +2011,7 @@ fn test_trampoline_inbound_payment_decoding() {
}
Ok(SharedSecret::new(other_key, &node_secret))
}
- fn get_expanded_key(&self) -> ExpandedKey { unreachable!() }
+ fn get_expanded_key(&self) -> ExpandedKey { ExpandedKey::new([42; 32]) }
fn get_node_id(&self, _recipient: Recipient) -> Result<PublicKey, ()> { unreachable!() }
fn sign_invoice(
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index 67f7807..ac549dd 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -56,7 +56,7 @@ use core::str::FromStr;
#[cfg(feature = "std")]
use std::net::SocketAddr;
-use crate::crypto::streams::ChaChaDualPolyReadAdapter;
+use crate::crypto::streams::{ChaChaTriPolyReadAdapter, TriPolyAADUsed};
use crate::util::base32;
use crate::util::logger;
use crate::util::ser::{
@@ -3924,10 +3924,13 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo
.map_err(|_| DecodeError::InvalidValue)?;
let rho = onion_utils::gen_rho_from_shared_secret(&enc_tlvs_ss.secret_bytes());
let receive_auth_key = node_signer.get_receive_auth_key();
+ let phantom_auth_key = node_signer.get_expanded_key().phantom_node_blinded_path_key;
+ let read_args = (rho, receive_auth_key.0, phantom_auth_key);
+
let mut s = Cursor::new(&enc_tlvs);
let mut reader = FixedLengthReader::new(&mut s, enc_tlvs.len() as u64);
- match ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))? {
- ChaChaDualPolyReadAdapter {
+ match ChaChaTriPolyReadAdapter::read(&mut reader, read_args)? {
+ ChaChaTriPolyReadAdapter {
readable:
BlindedPaymentTlvs::Forward(ForwardTlvs {
short_channel_id,
@@ -3942,7 +3945,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo
|| cltv_value.is_some() || total_msat.is_some()
|| keysend_preimage.is_some()
|| invoice_request.is_some()
- || used_aad
+ || used_aad != TriPolyAADUsed::None
{
return Err(DecodeError::InvalidValue);
}
@@ -3955,7 +3958,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo
next_blinding_override,
}))
},
- ChaChaDualPolyReadAdapter {
+ ChaChaTriPolyReadAdapter {
readable:
BlindedPaymentTlvs::Dummy(DummyTlvs { payment_relay, payment_constraints }),
used_aad,
@@ -3964,7 +3967,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo
|| cltv_value.is_some() || total_msat.is_some()
|| keysend_preimage.is_some()
|| invoice_request.is_some()
- || !used_aad
+ || used_aad == TriPolyAADUsed::None
{
return Err(DecodeError::InvalidValue);
}
@@ -3974,11 +3977,11 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo
intro_node_blinding_point,
}))
},
- ChaChaDualPolyReadAdapter {
+ ChaChaTriPolyReadAdapter {
readable: BlindedPaymentTlvs::Receive(receive_tlvs),
used_aad,
} => {
- if !used_aad {
+ if used_aad == TriPolyAADUsed::None {
return Err(DecodeError::InvalidValue);
}
@@ -4041,6 +4044,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline
fn read<R: Read>(r: &mut R, args: (Option<PublicKey>, NS)) -> Result<Self, DecodeError> {
let (update_add_blinding_point, node_signer) = args;
let receive_auth_key = node_signer.get_receive_auth_key();
+ let phantom_auth_key = node_signer.get_expanded_key().phantom_node_blinded_path_key;
let mut amt = None;
let mut cltv_value = None;
@@ -4094,8 +4098,9 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline
let rho = onion_utils::gen_rho_from_shared_secret(&enc_tlvs_ss.secret_bytes());
let mut s = Cursor::new(&enc_tlvs);
let mut reader = FixedLengthReader::new(&mut s, enc_tlvs.len() as u64);
- match ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))? {
- ChaChaDualPolyReadAdapter {
+ let read_args = (rho, receive_auth_key.0, phantom_auth_key);
+ match ChaChaTriPolyReadAdapter::read(&mut reader, read_args)? {
+ ChaChaTriPolyReadAdapter {
readable:
BlindedTrampolineTlvs::Forward(TrampolineForwardTlvs {
next_trampoline,
@@ -4110,7 +4115,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline
|| cltv_value.is_some() || total_msat.is_some()
|| keysend_preimage.is_some()
|| invoice_request.is_some()
- || used_aad
+ || used_aad != TriPolyAADUsed::None
{
return Err(DecodeError::InvalidValue);
}
@@ -4123,11 +4128,11 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline
next_blinding_override,
}))
},
- ChaChaDualPolyReadAdapter {
+ ChaChaTriPolyReadAdapter {
readable: BlindedTrampolineTlvs::Receive(receive_tlvs),
used_aad,
} => {
- if !used_aad {
+ if used_aad == TriPolyAADUsed::None {
return Err(DecodeError::InvalidValue);
}
diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs
index e688c02..f94eb78 100644
--- a/lightning/src/onion_message/messenger.rs
+++ b/lightning/src/onion_message/messenger.rs
@@ -1168,12 +1168,13 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand
},
}
};
- let receiving_context_auth_key = node_signer.get_receive_auth_key();
+ let receive_auth_key = node_signer.get_receive_auth_key();
+ let expanded_key = &node_signer.get_expanded_key();
let next_hop = onion_utils::decode_next_untagged_hop(
onion_decode_ss,
&msg.onion_routing_packet.hop_data[..],
msg.onion_routing_packet.hmac,
- (control_tlvs_ss, &custom_handler, receiving_context_auth_key, &logger),
+ (control_tlvs_ss, &custom_handler, receive_auth_key, expanded_key, &logger),
);
// Constructs the next onion message using packet data and blinding logic.
@@ -1219,21 +1220,24 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand
message,
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }),
reply_path,
- control_tlvs_authenticated,
+ control_tlvs_from_local_node,
+ control_tlvs_from_phantom_participant: _,
},
None,
)) => match (message, context) {
(ParsedOnionMessageContents::Offers(msg), Some(MessageContext::Offers(ctx))) => {
match ctx {
OffersContext::InvoiceRequest { .. } => {
- // Note: We introduced the `control_tlvs_authenticated` check in LDK v0.2
+ // Note: We introduced the `control_tlvs_from_*` check in LDK v0.2
// to simplify and standardize onion message authentication.
// To continue supporting offers created before v0.2, we allow
// unauthenticated control TLVs for these messages, as they can be
// verified using the legacy method.
},
_ => {
- if !control_tlvs_authenticated {
+ // In any other offers context, we only allow message authenticated as
+ // coming from our local, node, not any other phantom participant.
+ if !control_tlvs_from_local_node {
log_trace!(logger, "Received an unauthenticated offers onion message");
return Err(());
}
@@ -1248,14 +1252,14 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand
ParsedOnionMessageContents::AsyncPayments(msg),
Some(MessageContext::AsyncPayments(ctx)),
) => {
- if !control_tlvs_authenticated {
+ if !control_tlvs_from_local_node {
log_trace!(logger, "Received an unauthenticated async payments onion message");
return Err(());
}
Ok(PeeledOnion::AsyncPayments(msg, ctx, reply_path))
},
(ParsedOnionMessageContents::Custom(msg), Some(MessageContext::Custom(ctx))) => {
- if !control_tlvs_authenticated {
+ if !control_tlvs_from_local_node {
log_trace!(logger, "Received an unauthenticated custom onion message");
return Err(());
}
@@ -1268,7 +1272,7 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand
ParsedOnionMessageContents::DNSResolver(msg),
Some(MessageContext::DNSResolver(ctx)),
) => {
- if !control_tlvs_authenticated {
+ if !control_tlvs_from_local_node {
log_trace!(logger, "Received an unauthenticated DNS resolver onion message");
return Err(());
}
@@ -2504,7 +2508,8 @@ fn packet_payloads_and_keys<
control_tlvs,
reply_path: reply_path.take(),
message,
- control_tlvs_authenticated: false,
+ control_tlvs_from_local_node: false,
+ control_tlvs_from_phantom_participant: false,
},
prev_control_tlvs_ss.unwrap(),
));
@@ -2514,7 +2519,8 @@ fn packet_payloads_and_keys<
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context: None }),
reply_path: reply_path.take(),
message,
- control_tlvs_authenticated: false,
+ control_tlvs_from_local_node: false,
+ control_tlvs_from_phantom_participant: false,
},
prev_control_tlvs_ss.unwrap(),
));
diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs
index 2e0ccaf..cd9a923 100644
--- a/lightning/src/onion_message/packet.rs
+++ b/lightning/src/onion_message/packet.rs
@@ -19,7 +19,8 @@ use super::offers::OffersMessage;
use crate::blinded_path::message::{
BlindedMessagePath, DummyTlv, ForwardTlvs, NextMessageHop, ReceiveTlvs,
};
-use crate::crypto::streams::{ChaChaDualPolyReadAdapter, ChaChaPolyWriteAdapter};
+use crate::crypto::streams::{ChaChaPolyWriteAdapter, ChaChaTriPolyReadAdapter, TriPolyAADUsed};
+use crate::ln::inbound_payment::ExpandedKey;
use crate::ln::msgs::DecodeError;
use crate::ln::onion_utils;
use crate::sign::ReceiveAuthKey;
@@ -121,9 +122,16 @@ pub(super) enum Payload<T: OnionMessageContents> {
},
/// This payload is for the final hop.
Receive {
- /// The [`ReceiveControlTlvs`] were authenticated with the additional key which was
+ /// The [`ReceiveControlTlvs`] were authenticated with the [`ReceiveAuthKey`] which was
/// provided to [`ReadableArgs::read`].
- control_tlvs_authenticated: bool,
+ control_tlvs_from_local_node: bool,
+ /// The [`ReceiveControlTlvs`] were authenticated with the
+ /// [`ExpandedKey::phantom_node_blinded_path_key`] which was provided to
+ /// [`ReadableArgs::read`].
+ /// Note that this is currently never actually read, but exists to signal the type of
+ /// authentication we can do.
+ #[allow(dead_code)]
+ control_tlvs_from_phantom_participant: bool,
control_tlvs: ReceiveControlTlvs,
reply_path: Option<BlindedMessagePath>,
message: T,
@@ -233,7 +241,8 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
control_tlvs: ReceiveControlTlvs::Blinded(encrypted_bytes),
reply_path,
message,
- control_tlvs_authenticated: _,
+ control_tlvs_from_local_node: _,
+ control_tlvs_from_phantom_participant: _,
} => {
_encode_varint_length_prefixed_tlv!(w, {
(2, reply_path, option),
@@ -253,7 +262,8 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs),
reply_path,
message,
- control_tlvs_authenticated: _,
+ control_tlvs_from_local_node: _,
+ control_tlvs_from_phantom_participant: _,
} => {
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
_encode_varint_length_prefixed_tlv!(w, {
@@ -269,24 +279,27 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
// Uses the provided secret to simultaneously decode and decrypt the control TLVs and data TLV.
impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized>
- ReadableArgs<(SharedSecret, &H, ReceiveAuthKey, &L)>
+ ReadableArgs<(SharedSecret, &H, ReceiveAuthKey, &ExpandedKey, &L)>
for Payload<ParsedOnionMessageContents<<H as CustomOnionMessageHandler>::CustomMessage>>
{
fn read<R: Read>(
- r: &mut R, args: (SharedSecret, &H, ReceiveAuthKey, &L),
+ r: &mut R, args: (SharedSecret, &H, ReceiveAuthKey, &ExpandedKey, &L),
) -> Result<Self, DecodeError> {
- let (encrypted_tlvs_ss, handler, receive_tlvs_key, logger) = args;
+ let (encrypted_tlvs_ss, handler, receive_tlvs_key, expanded_key, logger) = args;
let v: BigSize = Readable::read(r)?;
let mut rd = FixedLengthReader::new(r, v.0);
let mut reply_path: Option<BlindedMessagePath> = None;
- let mut read_adapter: Option<ChaChaDualPolyReadAdapter<ControlTlvs>> = None;
+ let mut read_adapter: Option<ChaChaTriPolyReadAdapter<ControlTlvs>> = None;
let rho = onion_utils::gen_rho_from_shared_secret(&encrypted_tlvs_ss.secret_bytes());
+ let read_adapter_args =
+ (rho, receive_tlvs_key.0, expanded_key.phantom_node_blinded_path_key);
let mut message_type: Option<u64> = None;
let mut message = None;
+
decode_tlv_stream_with_custom_tlv_decode!(&mut rd, {
(2, reply_path, option),
- (4, read_adapter, (option: LengthReadableArgs, (rho, receive_tlvs_key.0))),
+ (4, read_adapter, (option: LengthReadableArgs, read_adapter_args)),
}, |msg_type, msg_reader| {
if msg_type < 64 { return Ok(false) }
// Don't allow reading more than one data TLV from an onion message.
@@ -322,21 +335,22 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized>
match read_adapter {
None => return Err(DecodeError::InvalidValue),
- Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => {
- if used_aad || message_type.is_some() {
+ Some(ChaChaTriPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => {
+ if used_aad != TriPolyAADUsed::None || message_type.is_some() {
return Err(DecodeError::InvalidValue);
}
Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs)))
},
- Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Dummy, used_aad }) => {
- Ok(Payload::Dummy { control_tlvs_authenticated: used_aad })
+ Some(ChaChaTriPolyReadAdapter { readable: ControlTlvs::Dummy, used_aad }) => {
+ Ok(Payload::Dummy { control_tlvs_authenticated: used_aad != TriPolyAADUsed::None })
},
- Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => {
+ Some(ChaChaTriPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => {
Ok(Payload::Receive {
control_tlvs: ReceiveControlTlvs::Unblinded(tlvs),
reply_path,
message: message.ok_or(DecodeError::InvalidValue)?,
- control_tlvs_authenticated: used_aad,
+ control_tlvs_from_local_node: used_aad == TriPolyAADUsed::First,
+ control_tlvs_from_phantom_participant: used_aad == TriPolyAADUsed::Second,
})
},
}
diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs
index 34f5d5f..a12b113 100644
--- a/lightning/src/util/test_utils.rs
+++ b/lightning/src/util/test_utils.rs
@@ -1772,7 +1772,7 @@ impl TestNodeSigner {
impl NodeSigner for TestNodeSigner {
fn get_expanded_key(&self) -> ExpandedKey {
- unreachable!()
+ ExpandedKey::new([42; 32])
}
fn get_peer_storage_key(&self) -> PeerStorageKey {
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.