Skip to content

Commit 45f395a

Browse files
TheBlueMattshaavan
authored andcommitted
Prepare to auth blinded path contexts with a secret AAD in the MAC
When we receive an onion message, we often want to make sure it was sent through a blinded path we constructed. This protects us from various deanonymization attacks where someone can send a message to every node on the network until they find us, effectively unwrapping the blinded path and identifying its recipient. We generally do so by adding authentication tags to our `MessageContext` variants. Because the contexts themselves are encrypted (and MAC'd) to us, we only have to ensure that they cannot be forged, which is trivially accomplished with a simple nonce and a MAC covering it. This logic has ended up being repeated in nearly all of our onion message handlers, and has gotten quite repetitive. Instead, here, we simply authenticate the blinded path contexts using the MAC that's already there, but tweaking it with an additional secret as the AAD in Poly1305. This prevents forgery as the secret is now required to make the MAC check pass. Ultimately this means that no one can ever build a blinded path which terminates at an LDK node that we'll accept, but over time we've come to recognize this as a useful property, rather than something to fight. Here we finally break from the spec fully in our context encryption (not just the contents thereof). This will save a bit of space in some of our `MessageContext`s, though sadly not in the blinded path we include in `Bolt12Offer`s, so they're generally not in space-sensitive blinded paths. We can apply the same logic in our blinded payment paths as well, but we do not do so here. This commit only adds the required changes to the cryptography, for now it uses a constant key of `[41; 32]`.
1 parent beee9fb commit 45f395a

File tree

6 files changed

+92
-40
lines changed

6 files changed

+92
-40
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::offers::nonce::Nonce;
2626
use crate::offers::offer::OfferId;
2727
use crate::onion_message::packet::ControlTlvs;
2828
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
29-
use crate::sign::{EntropySource, NodeSigner, Recipient};
29+
use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient};
3030
use crate::types::payment::PaymentHash;
3131
use crate::util::scid_utils;
3232
use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer};
@@ -93,6 +93,7 @@ impl BlindedMessagePath {
9393
recipient_node_id,
9494
context,
9595
&blinding_secret,
96+
ReceiveAuthKey([41; 32]), // TODO: Pass this in
9697
)
9798
.map_err(|_| ())?,
9899
}))
@@ -556,18 +557,19 @@ pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100;
556557
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
557558
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
558559
recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey,
560+
local_node_receive_key: ReceiveAuthKey,
559561
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
560562
let pks = intermediate_nodes
561563
.iter()
562-
.map(|node| node.node_id)
563-
.chain(core::iter::once(recipient_node_id));
564+
.map(|node| (node.node_id, None))
565+
.chain(core::iter::once((recipient_node_id, Some(local_node_receive_key))));
564566
let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some());
565567

566568
let tlvs = pks
567569
.clone()
568570
.skip(1) // The first node's TLVs contains the next node's pubkey
569571
.zip(intermediate_nodes.iter().map(|node| node.short_channel_id))
570-
.map(|(pubkey, scid)| match scid {
572+
.map(|((pubkey, _), scid)| match scid {
571573
Some(scid) => NextMessageHop::ShortChannelId(scid),
572574
None => NextMessageHop::NodeId(pubkey),
573575
})

lightning/src/blinded_path/payment.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,10 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
664664
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
665665
payee_tlvs: ReceiveTlvs, session_priv: &SecretKey,
666666
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
667-
let pks =
668-
intermediate_nodes.iter().map(|node| node.node_id).chain(core::iter::once(payee_node_id));
667+
let pks = intermediate_nodes
668+
.iter()
669+
.map(|node| (node.node_id, None))
670+
.chain(core::iter::once((payee_node_id, None)));
669671
let tlvs = intermediate_nodes
670672
.iter()
671673
.map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs))

lightning/src/blinded_path/utils.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1, SecretKey};
1717

1818
use super::message::BlindedMessagePath;
1919
use super::{BlindedHop, BlindedPath};
20-
use crate::crypto::streams::ChaChaPolyWriteAdapter;
20+
use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC;
21+
use crate::crypto::streams::chachapoly_encrypt_with_swapped_aad;
2122
use crate::io;
2223
use crate::ln::onion_utils;
2324
use crate::onion_message::messenger::Destination;
25+
use crate::sign::ReceiveAuthKey;
2426
use crate::util::ser::{Writeable, Writer};
2527

2628
use core::borrow::Borrow;
@@ -105,7 +107,6 @@ macro_rules! build_keys_helper {
105107
};
106108
}
107109

108-
#[inline]
109110
pub(crate) fn construct_keys_for_onion_message<'a, T, I, F>(
110111
secp_ctx: &Secp256k1<T>, unblinded_path: I, destination: Destination, session_priv: &SecretKey,
111112
mut callback: F,
@@ -137,8 +138,7 @@ where
137138
Ok(())
138139
}
139140

140-
#[inline]
141-
pub(super) fn construct_keys_for_blinded_path<'a, T, I, F, H>(
141+
fn construct_keys_for_blinded_path<'a, T, I, F, H>(
142142
secp_ctx: &Secp256k1<T>, unblinded_path: I, session_priv: &SecretKey, mut callback: F,
143143
) -> Result<(), secp256k1::Error>
144144
where
@@ -149,14 +149,16 @@ where
149149
{
150150
build_keys_helper!(session_priv, secp_ctx, callback);
151151

152-
for pk in unblinded_path {
152+
let mut iter = unblinded_path.peekable();
153+
while let Some(pk) = iter.next() {
153154
build_keys_in_loop!(pk, false, None);
154155
}
155156
Ok(())
156157
}
157158

158159
struct PublicKeyWithTlvs<W: Writeable> {
159160
pubkey: PublicKey,
161+
hop_recv_key: Option<ReceiveAuthKey>,
160162
tlvs: W,
161163
}
162164

@@ -171,20 +173,26 @@ pub(crate) fn construct_blinded_hops<'a, T, I, W>(
171173
) -> Result<Vec<BlindedHop>, secp256k1::Error>
172174
where
173175
T: secp256k1::Signing + secp256k1::Verification,
174-
I: Iterator<Item = (PublicKey, W)>,
176+
I: Iterator<Item = ((PublicKey, Option<ReceiveAuthKey>), W)>,
175177
W: Writeable,
176178
{
177179
let mut blinded_hops = Vec::with_capacity(unblinded_path.size_hint().0);
178180
construct_keys_for_blinded_path(
179181
secp_ctx,
180-
unblinded_path.map(|(pubkey, tlvs)| PublicKeyWithTlvs { pubkey, tlvs }),
182+
unblinded_path.map(|((pubkey, hop_recv_key), tlvs)| PublicKeyWithTlvs {
183+
pubkey,
184+
hop_recv_key,
185+
tlvs,
186+
}),
181187
session_priv,
182188
|blinded_node_id, _, _, encrypted_payload_rho, unblinded_hop_data, _| {
189+
let hop_data = unblinded_hop_data.unwrap();
183190
blinded_hops.push(BlindedHop {
184191
blinded_node_id,
185192
encrypted_payload: encrypt_payload(
186-
unblinded_hop_data.unwrap().tlvs,
193+
hop_data.tlvs,
187194
encrypted_payload_rho,
195+
hop_data.hop_recv_key,
188196
),
189197
});
190198
},
@@ -193,9 +201,19 @@ where
193201
}
194202

195203
/// Encrypt TLV payload to be used as a [`crate::blinded_path::BlindedHop::encrypted_payload`].
196-
fn encrypt_payload<P: Writeable>(payload: P, encrypted_tlvs_rho: [u8; 32]) -> Vec<u8> {
197-
let write_adapter = ChaChaPolyWriteAdapter::new(encrypted_tlvs_rho, &payload);
198-
write_adapter.encode()
204+
fn encrypt_payload<P: Writeable>(
205+
payload: P, encrypted_tlvs_rho: [u8; 32], hop_recv_key: Option<ReceiveAuthKey>,
206+
) -> Vec<u8> {
207+
let mut payload_data = payload.encode();
208+
if let Some(hop_recv_key) = hop_recv_key {
209+
chachapoly_encrypt_with_swapped_aad(payload_data, encrypted_tlvs_rho, hop_recv_key.0)
210+
} else {
211+
let mut chacha = ChaCha20Poly1305RFC::new(&encrypted_tlvs_rho, &[0; 12], &[]);
212+
let mut tag = [0; 16];
213+
chacha.encrypt_full_message_in_place(&mut payload_data, &mut tag);
214+
payload_data.extend_from_slice(&tag);
215+
payload_data
216+
}
199217
}
200218

201219
/// A data structure used exclusively to pad blinded path payloads, ensuring they are of

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,17 +1556,23 @@ fn route_blinding_spec_test_vector() {
15561556
let blinding_override = PublicKey::from_secret_key(&secp_ctx, &dave_eve_session_priv);
15571557
assert_eq!(blinding_override, pubkey_from_hex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"));
15581558
// Can't use the public API here as the encrypted payloads contain unknown TLVs.
1559-
let path = [(dave_node_id, WithoutLength(&dave_unblinded_tlvs)), (eve_node_id, WithoutLength(&eve_unblinded_tlvs))];
1559+
let path = [
1560+
((dave_node_id, None), WithoutLength(&dave_unblinded_tlvs)),
1561+
((eve_node_id, None), WithoutLength(&eve_unblinded_tlvs)),
1562+
];
15601563
let mut dave_eve_blinded_hops = blinded_path::utils::construct_blinded_hops(
1561-
&secp_ctx, path.into_iter(), &dave_eve_session_priv
1564+
&secp_ctx, path.into_iter(), &dave_eve_session_priv,
15621565
).unwrap();
15631566

15641567
// Concatenate an additional Bob -> Carol blinded path to the Eve -> Dave blinded path.
15651568
let bob_carol_session_priv = secret_from_hex("0202020202020202020202020202020202020202020202020202020202020202");
15661569
let bob_blinding_point = PublicKey::from_secret_key(&secp_ctx, &bob_carol_session_priv);
1567-
let path = [(bob_node_id, WithoutLength(&bob_unblinded_tlvs)), (carol_node_id, WithoutLength(&carol_unblinded_tlvs))];
1570+
let path = [
1571+
((bob_node_id, None), WithoutLength(&bob_unblinded_tlvs)),
1572+
((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs)),
1573+
];
15681574
let bob_carol_blinded_hops = blinded_path::utils::construct_blinded_hops(
1569-
&secp_ctx, path.into_iter(), &bob_carol_session_priv
1575+
&secp_ctx, path.into_iter(), &bob_carol_session_priv,
15701576
).unwrap();
15711577

15721578
let mut blinded_hops = bob_carol_blinded_hops;
@@ -2026,9 +2032,9 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
20262032
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
20272033
let carol_unblinded_tlvs = payee_tlvs.encode();
20282034

2029-
let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))];
2035+
let path = [((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs))];
20302036
blinded_path::utils::construct_blinded_hops(
2031-
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv
2037+
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv,
20322038
).unwrap()
20332039
} else {
20342040
let payee_tlvs = blinded_path::payment::TrampolineForwardTlvs {
@@ -2047,9 +2053,9 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
20472053
};
20482054

20492055
let carol_unblinded_tlvs = payee_tlvs.encode();
2050-
let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))];
2056+
let path = [((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs))];
20512057
blinded_path::utils::construct_blinded_hops(
2052-
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv
2058+
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv,
20532059
).unwrap()
20542060
};
20552061

@@ -2249,11 +2255,11 @@ fn test_trampoline_unblinded_receive() {
22492255
};
22502256

22512257
let carol_unblinded_tlvs = payee_tlvs.encode();
2252-
let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))];
2258+
let path = [((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs))];
22532259
let carol_alice_trampoline_session_priv = secret_from_hex("a0f4b8d7b6c2d0ffdfaf718f76e9decaef4d9fb38a8c4addb95c4007cc3eee03");
22542260
let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &carol_alice_trampoline_session_priv);
22552261
let carol_blinded_hops = blinded_path::utils::construct_blinded_hops(
2256-
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv
2262+
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv,
22572263
).unwrap();
22582264

22592265
let route = Route {

lightning/src/onion_message/messenger.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use crate::ln::msgs::{
4040
};
4141
use crate::ln::onion_utils;
4242
use crate::routing::gossip::{NetworkGraph, NodeId, ReadOnlyNetworkGraph};
43-
use crate::sign::{EntropySource, NodeSigner, Recipient};
43+
use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient};
4444
use crate::types::features::{InitFeatures, NodeFeatures};
4545
use crate::util::async_poll::{MultiResultFuturePoller, ResultFuture};
4646
use crate::util::logger::{Logger, WithContext};
@@ -1074,18 +1074,20 @@ where
10741074
},
10751075
}
10761076
};
1077+
let receiving_context_auth_key = ReceiveAuthKey([41; 32]); // TODO: pass this in
10771078
let next_hop = onion_utils::decode_next_untagged_hop(
10781079
onion_decode_ss,
10791080
&msg.onion_routing_packet.hop_data[..],
10801081
msg.onion_routing_packet.hmac,
1081-
(control_tlvs_ss, custom_handler.deref(), logger.deref()),
1082+
(control_tlvs_ss, custom_handler.deref(), receiving_context_auth_key, logger.deref()),
10821083
);
10831084
match next_hop {
10841085
Ok((
10851086
Payload::Receive {
10861087
message,
10871088
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }),
10881089
reply_path,
1090+
control_tlvs_authenticated,
10891091
},
10901092
None,
10911093
)) => match (message, context) {
@@ -1114,6 +1116,8 @@ where
11141116
Ok(PeeledOnion::DNSResolver(msg, None, reply_path))
11151117
},
11161118
_ => {
1119+
// Hide the "`control_tlvs_authenticated` is unused warning". We'll use it here soon
1120+
let _ = control_tlvs_authenticated;
11171121
log_trace!(
11181122
logger,
11191123
"Received message was sent on a blinded path with wrong or missing context."
@@ -2322,7 +2326,12 @@ fn packet_payloads_and_keys<
23222326

23232327
if let Some(control_tlvs) = final_control_tlvs {
23242328
payloads.push((
2325-
Payload::Receive { control_tlvs, reply_path: reply_path.take(), message },
2329+
Payload::Receive {
2330+
control_tlvs,
2331+
reply_path: reply_path.take(),
2332+
message,
2333+
control_tlvs_authenticated: false,
2334+
},
23262335
prev_control_tlvs_ss.unwrap(),
23272336
));
23282337
} else {
@@ -2331,6 +2340,7 @@ fn packet_payloads_and_keys<
23312340
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context: None }),
23322341
reply_path: reply_path.take(),
23332342
message,
2343+
control_tlvs_authenticated: false,
23342344
},
23352345
prev_control_tlvs_ss.unwrap(),
23362346
));

lightning/src/onion_message/packet.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ use super::dns_resolution::DNSResolverMessage;
1818
use super::messenger::CustomOnionMessageHandler;
1919
use super::offers::OffersMessage;
2020
use crate::blinded_path::message::{BlindedMessagePath, ForwardTlvs, NextMessageHop, ReceiveTlvs};
21-
use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter};
21+
use crate::crypto::streams::{ChaChaDualPolyReadAdapter, ChaChaPolyWriteAdapter};
2222
use crate::ln::msgs::DecodeError;
2323
use crate::ln::onion_utils;
24+
use crate::sign::ReceiveAuthKey;
2425
use crate::util::logger::Logger;
2526
use crate::util::ser::{
2627
BigSize, FixedLengthReader, LengthLimitedRead, LengthReadable, LengthReadableArgs, Readable,
@@ -112,7 +113,14 @@ pub(super) enum Payload<T: OnionMessageContents> {
112113
/// This payload is for an intermediate hop.
113114
Forward(ForwardControlTlvs),
114115
/// This payload is for the final hop.
115-
Receive { control_tlvs: ReceiveControlTlvs, reply_path: Option<BlindedMessagePath>, message: T },
116+
Receive {
117+
/// The [`ReceiveControlTlvs`] were authenticated with the additional key which was
118+
/// provided to [`ReadableArgs::read`].
119+
control_tlvs_authenticated: bool,
120+
control_tlvs: ReceiveControlTlvs,
121+
reply_path: Option<BlindedMessagePath>,
122+
message: T,
123+
},
116124
}
117125

118126
/// The contents of an [`OnionMessage`] as read from the wire.
@@ -223,6 +231,7 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
223231
control_tlvs: ReceiveControlTlvs::Blinded(encrypted_bytes),
224232
reply_path,
225233
message,
234+
control_tlvs_authenticated: _,
226235
} => {
227236
_encode_varint_length_prefixed_tlv!(w, {
228237
(2, reply_path, option),
@@ -238,6 +247,7 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
238247
control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs),
239248
reply_path,
240249
message,
250+
control_tlvs_authenticated: _,
241251
} => {
242252
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
243253
_encode_varint_length_prefixed_tlv!(w, {
@@ -252,22 +262,25 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
252262
}
253263

254264
// Uses the provided secret to simultaneously decode and decrypt the control TLVs and data TLV.
255-
impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(SharedSecret, &H, &L)>
265+
impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized>
266+
ReadableArgs<(SharedSecret, &H, ReceiveAuthKey, &L)>
256267
for Payload<ParsedOnionMessageContents<<H as CustomOnionMessageHandler>::CustomMessage>>
257268
{
258-
fn read<R: Read>(r: &mut R, args: (SharedSecret, &H, &L)) -> Result<Self, DecodeError> {
259-
let (encrypted_tlvs_ss, handler, logger) = args;
269+
fn read<R: Read>(
270+
r: &mut R, args: (SharedSecret, &H, ReceiveAuthKey, &L),
271+
) -> Result<Self, DecodeError> {
272+
let (encrypted_tlvs_ss, handler, receive_tlvs_key, logger) = args;
260273

261274
let v: BigSize = Readable::read(r)?;
262275
let mut rd = FixedLengthReader::new(r, v.0);
263276
let mut reply_path: Option<BlindedMessagePath> = None;
264-
let mut read_adapter: Option<ChaChaPolyReadAdapter<ControlTlvs>> = None;
277+
let mut read_adapter: Option<ChaChaDualPolyReadAdapter<ControlTlvs>> = None;
265278
let rho = onion_utils::gen_rho_from_shared_secret(&encrypted_tlvs_ss.secret_bytes());
266279
let mut message_type: Option<u64> = None;
267280
let mut message = None;
268281
decode_tlv_stream_with_custom_tlv_decode!(&mut rd, {
269282
(2, reply_path, option),
270-
(4, read_adapter, (option: LengthReadableArgs, rho)),
283+
(4, read_adapter, (option: LengthReadableArgs, (rho, receive_tlvs_key.0))),
271284
}, |msg_type, msg_reader| {
272285
if msg_type < 64 { return Ok(false) }
273286
// Don't allow reading more than one data TLV from an onion message.
@@ -304,17 +317,18 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(Sh
304317

305318
match read_adapter {
306319
None => return Err(DecodeError::InvalidValue),
307-
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Forward(tlvs) }) => {
308-
if message_type.is_some() {
320+
Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => {
321+
if used_aad || message_type.is_some() {
309322
return Err(DecodeError::InvalidValue);
310323
}
311324
Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs)))
312325
},
313-
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => {
326+
Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => {
314327
Ok(Payload::Receive {
315328
control_tlvs: ReceiveControlTlvs::Unblinded(tlvs),
316329
reply_path,
317330
message: message.ok_or(DecodeError::InvalidValue)?,
331+
control_tlvs_authenticated: used_aad,
318332
})
319333
},
320334
}

0 commit comments

Comments
 (0)