Skip to content

Commit cfa4877

Browse files
committed
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 b7fda0e commit cfa4877

File tree

6 files changed

+88
-38
lines changed

6 files changed

+88
-38
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ impl BlindedMessagePath {
9292
recipient_node_id,
9393
context,
9494
&blinding_secret,
95+
[41; 32], // TODO: Pass this in
9596
)
9697
.map_err(|_| ())?,
9798
}))
@@ -514,18 +515,19 @@ pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100;
514515
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
515516
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
516517
recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey,
518+
local_node_receive_key: [u8; 32],
517519
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
518520
let pks = intermediate_nodes
519521
.iter()
520-
.map(|node| node.node_id)
521-
.chain(core::iter::once(recipient_node_id));
522+
.map(|node| (node.node_id, None))
523+
.chain(core::iter::once((recipient_node_id, Some(local_node_receive_key))));
522524
let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some());
523525

524526
let tlvs = pks
525527
.clone()
526528
.skip(1) // The first node's TLVs contains the next node's pubkey
527529
.zip(intermediate_nodes.iter().map(|node| node.short_channel_id))
528-
.map(|(pubkey, scid)| match scid {
530+
.map(|((pubkey, _), scid)| match scid {
529531
Some(scid) => NextMessageHop::ShortChannelId(scid),
530532
None => NextMessageHop::NodeId(pubkey),
531533
})

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: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ 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;
@@ -105,7 +106,6 @@ macro_rules! build_keys_helper {
105106
};
106107
}
107108

108-
#[inline]
109109
pub(crate) fn construct_keys_for_onion_message<'a, T, I, F>(
110110
secp_ctx: &Secp256k1<T>, unblinded_path: I, destination: Destination, session_priv: &SecretKey,
111111
mut callback: F,
@@ -137,8 +137,7 @@ where
137137
Ok(())
138138
}
139139

140-
#[inline]
141-
pub(super) fn construct_keys_for_blinded_path<'a, T, I, F, H>(
140+
fn construct_keys_for_blinded_path<'a, T, I, F, H>(
142141
secp_ctx: &Secp256k1<T>, unblinded_path: I, session_priv: &SecretKey, mut callback: F,
143142
) -> Result<(), secp256k1::Error>
144143
where
@@ -149,14 +148,16 @@ where
149148
{
150149
build_keys_helper!(session_priv, secp_ctx, callback);
151150

152-
for pk in unblinded_path {
151+
let mut iter = unblinded_path.peekable();
152+
while let Some(pk) = iter.next() {
153153
build_keys_in_loop!(pk, false, None);
154154
}
155155
Ok(())
156156
}
157157

158158
struct PublicKeyWithTlvs<W: Writeable> {
159159
pubkey: PublicKey,
160+
hop_recv_key: Option<[u8; 32]>,
160161
tlvs: W,
161162
}
162163

@@ -171,20 +172,26 @@ pub(crate) fn construct_blinded_hops<'a, T, I, W>(
171172
) -> Result<Vec<BlindedHop>, secp256k1::Error>
172173
where
173174
T: secp256k1::Signing + secp256k1::Verification,
174-
I: Iterator<Item = (PublicKey, W)>,
175+
I: Iterator<Item = ((PublicKey, Option<[u8; 32]>), W)>,
175176
W: Writeable,
176177
{
177178
let mut blinded_hops = Vec::with_capacity(unblinded_path.size_hint().0);
178179
construct_keys_for_blinded_path(
179180
secp_ctx,
180-
unblinded_path.map(|(pubkey, tlvs)| PublicKeyWithTlvs { pubkey, tlvs }),
181+
unblinded_path.map(|((pubkey, hop_recv_key), tlvs)| PublicKeyWithTlvs {
182+
pubkey,
183+
hop_recv_key,
184+
tlvs,
185+
}),
181186
session_priv,
182187
|blinded_node_id, _, _, encrypted_payload_rho, unblinded_hop_data, _| {
188+
let hop_data = unblinded_hop_data.unwrap();
183189
blinded_hops.push(BlindedHop {
184190
blinded_node_id,
185191
encrypted_payload: encrypt_payload(
186-
unblinded_hop_data.unwrap().tlvs,
192+
hop_data.tlvs,
187193
encrypted_payload_rho,
194+
hop_data.hop_recv_key,
188195
),
189196
});
190197
},
@@ -193,9 +200,19 @@ where
193200
}
194201

195202
/// 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()
203+
fn encrypt_payload<P: Writeable>(
204+
payload: P, encrypted_tlvs_rho: [u8; 32], hop_recv_key: Option<[u8; 32]>,
205+
) -> Vec<u8> {
206+
let mut payload_data = payload.encode();
207+
if let Some(hop_recv_key) = hop_recv_key {
208+
chachapoly_encrypt_with_swapped_aad(payload_data, encrypted_tlvs_rho, hop_recv_key)
209+
} else {
210+
let mut chacha = ChaCha20Poly1305RFC::new(&encrypted_tlvs_rho, &[0; 12], &[]);
211+
let mut tag = [0; 16];
212+
chacha.encrypt_full_message_in_place(&mut payload_data, &mut tag);
213+
payload_data.extend_from_slice(&tag);
214+
payload_data
215+
}
199216
}
200217

201218
/// 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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,18 +1068,20 @@ where
10681068
},
10691069
}
10701070
};
1071+
let receiving_context_auth_key = [41; 32]; // TODO: pass this in
10711072
let next_hop = onion_utils::decode_next_untagged_hop(
10721073
onion_decode_ss,
10731074
&msg.onion_routing_packet.hop_data[..],
10741075
msg.onion_routing_packet.hmac,
1075-
(control_tlvs_ss, custom_handler.deref(), logger.deref()),
1076+
(control_tlvs_ss, custom_handler.deref(), receiving_context_auth_key, logger.deref()),
10761077
);
10771078
match next_hop {
10781079
Ok((
10791080
Payload::Receive {
10801081
message,
10811082
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }),
10821083
reply_path,
1084+
control_tlvs_authenticated,
10831085
},
10841086
None,
10851087
)) => match (message, context) {
@@ -1108,6 +1110,8 @@ where
11081110
Ok(PeeledOnion::DNSResolver(msg, None, reply_path))
11091111
},
11101112
_ => {
1113+
// Hide the "`control_tlvs_authenticated` is unused warning". We'll use it here soon
1114+
let _ = control_tlvs_authenticated;
11111115
log_trace!(
11121116
logger,
11131117
"Received message was sent on a blinded path with wrong or missing context."
@@ -2294,7 +2298,12 @@ fn packet_payloads_and_keys<
22942298

22952299
if let Some(control_tlvs) = final_control_tlvs {
22962300
payloads.push((
2297-
Payload::Receive { control_tlvs, reply_path: reply_path.take(), message },
2301+
Payload::Receive {
2302+
control_tlvs,
2303+
reply_path: reply_path.take(),
2304+
message,
2305+
control_tlvs_authenticated: false,
2306+
},
22982307
prev_control_tlvs_ss.unwrap(),
22992308
));
23002309
} else {
@@ -2303,6 +2312,7 @@ fn packet_payloads_and_keys<
23032312
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context: None }),
23042313
reply_path: reply_path.take(),
23052314
message,
2315+
control_tlvs_authenticated: false,
23062316
},
23072317
prev_control_tlvs_ss.unwrap(),
23082318
));

lightning/src/onion_message/packet.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ 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;
2424
use crate::util::logger::Logger;
@@ -112,7 +112,14 @@ pub(super) enum Payload<T: OnionMessageContents> {
112112
/// This payload is for an intermediate hop.
113113
Forward(ForwardControlTlvs),
114114
/// This payload is for the final hop.
115-
Receive { control_tlvs: ReceiveControlTlvs, reply_path: Option<BlindedMessagePath>, message: T },
115+
Receive {
116+
/// The [`ReceiveControlTlvs`] were authenticated with the additional key which was
117+
/// provided to [`ReadableArgs::read`].
118+
control_tlvs_authenticated: bool,
119+
control_tlvs: ReceiveControlTlvs,
120+
reply_path: Option<BlindedMessagePath>,
121+
message: T,
122+
},
116123
}
117124

118125
/// The contents of an [`OnionMessage`] as read from the wire.
@@ -223,6 +230,7 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
223230
control_tlvs: ReceiveControlTlvs::Blinded(encrypted_bytes),
224231
reply_path,
225232
message,
233+
control_tlvs_authenticated: _,
226234
} => {
227235
_encode_varint_length_prefixed_tlv!(w, {
228236
(2, reply_path, option),
@@ -238,6 +246,7 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
238246
control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs),
239247
reply_path,
240248
message,
249+
control_tlvs_authenticated: _,
241250
} => {
242251
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
243252
_encode_varint_length_prefixed_tlv!(w, {
@@ -252,22 +261,25 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
252261
}
253262

254263
// 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)>
264+
impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized>
265+
ReadableArgs<(SharedSecret, &H, [u8; 32], &L)>
256266
for Payload<ParsedOnionMessageContents<<H as CustomOnionMessageHandler>::CustomMessage>>
257267
{
258-
fn read<R: Read>(r: &mut R, args: (SharedSecret, &H, &L)) -> Result<Self, DecodeError> {
259-
let (encrypted_tlvs_ss, handler, logger) = args;
268+
fn read<R: Read>(
269+
r: &mut R, args: (SharedSecret, &H, [u8; 32], &L),
270+
) -> Result<Self, DecodeError> {
271+
let (encrypted_tlvs_ss, handler, receive_tlvs_key, logger) = args;
260272

261273
let v: BigSize = Readable::read(r)?;
262274
let mut rd = FixedLengthReader::new(r, v.0);
263275
let mut reply_path: Option<BlindedMessagePath> = None;
264-
let mut read_adapter: Option<ChaChaPolyReadAdapter<ControlTlvs>> = None;
276+
let mut read_adapter: Option<ChaChaDualPolyReadAdapter<ControlTlvs>> = None;
265277
let rho = onion_utils::gen_rho_from_shared_secret(&encrypted_tlvs_ss.secret_bytes());
266278
let mut message_type: Option<u64> = None;
267279
let mut message = None;
268280
decode_tlv_stream_with_custom_tlv_decode!(&mut rd, {
269281
(2, reply_path, option),
270-
(4, read_adapter, (option: LengthReadableArgs, rho)),
282+
(4, read_adapter, (option: LengthReadableArgs, (rho, receive_tlvs_key))),
271283
}, |msg_type, msg_reader| {
272284
if msg_type < 64 { return Ok(false) }
273285
// Don't allow reading more than one data TLV from an onion message.
@@ -304,17 +316,18 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(Sh
304316

305317
match read_adapter {
306318
None => return Err(DecodeError::InvalidValue),
307-
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Forward(tlvs) }) => {
308-
if message_type.is_some() {
319+
Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => {
320+
if used_aad || message_type.is_some() {
309321
return Err(DecodeError::InvalidValue);
310322
}
311323
Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs)))
312324
},
313-
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => {
325+
Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => {
314326
Ok(Payload::Receive {
315327
control_tlvs: ReceiveControlTlvs::Unblinded(tlvs),
316328
reply_path,
317329
message: message.ok_or(DecodeError::InvalidValue)?,
330+
control_tlvs_authenticated: used_aad,
318331
})
319332
},
320333
}

0 commit comments

Comments
 (0)