Skip to content

Commit 1725a3b

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 1725a3b

File tree

6 files changed

+85
-40
lines changed

6 files changed

+85
-40
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: 36 additions & 13 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,26 +137,35 @@ where
137137
Ok(())
138138
}
139139

140-
#[inline]
141-
pub(super) fn construct_keys_for_blinded_path<'a, T, I, F, H>(
142-
secp_ctx: &Secp256k1<T>, unblinded_path: I, session_priv: &SecretKey, mut callback: F,
140+
fn construct_keys_for_blinded_path<'a, T, I, F, H>(
141+
secp_ctx: &Secp256k1<T>, unblinded_path: I, session_priv: &SecretKey,
142+
mut callback: F,
143143
) -> Result<(), secp256k1::Error>
144144
where
145145
T: secp256k1::Signing + secp256k1::Verification,
146146
H: Borrow<PublicKey>,
147147
I: Iterator<Item = H>,
148-
F: FnMut(PublicKey, SharedSecret, PublicKey, [u8; 32], Option<H>, Option<Vec<u8>>),
148+
F: FnMut(
149+
PublicKey,
150+
SharedSecret,
151+
PublicKey,
152+
[u8; 32],
153+
Option<H>,
154+
Option<Vec<u8>>,
155+
),
149156
{
150157
build_keys_helper!(session_priv, secp_ctx, callback);
151158

152-
for pk in unblinded_path {
159+
let mut iter = unblinded_path.peekable();
160+
while let Some(pk) = iter.next() {
153161
build_keys_in_loop!(pk, false, None);
154162
}
155163
Ok(())
156164
}
157165

158166
struct PublicKeyWithTlvs<W: Writeable> {
159167
pubkey: PublicKey,
168+
hop_recv_key: Option<[u8; 32]>,
160169
tlvs: W,
161170
}
162171

@@ -171,20 +180,24 @@ pub(crate) fn construct_blinded_hops<'a, T, I, W>(
171180
) -> Result<Vec<BlindedHop>, secp256k1::Error>
172181
where
173182
T: secp256k1::Signing + secp256k1::Verification,
174-
I: Iterator<Item = (PublicKey, W)>,
183+
I: Iterator<Item = ((PublicKey, Option<[u8; 32]>), W)>,
175184
W: Writeable,
176185
{
177186
let mut blinded_hops = Vec::with_capacity(unblinded_path.size_hint().0);
178187
construct_keys_for_blinded_path(
179188
secp_ctx,
180-
unblinded_path.map(|(pubkey, tlvs)| PublicKeyWithTlvs { pubkey, tlvs }),
189+
unblinded_path.map(|((pubkey, hop_recv_key), tlvs)| {
190+
PublicKeyWithTlvs { pubkey, hop_recv_key, tlvs }
191+
}),
181192
session_priv,
182193
|blinded_node_id, _, _, encrypted_payload_rho, unblinded_hop_data, _| {
194+
let hop_data = unblinded_hop_data.unwrap();
183195
blinded_hops.push(BlindedHop {
184196
blinded_node_id,
185197
encrypted_payload: encrypt_payload(
186-
unblinded_hop_data.unwrap().tlvs,
198+
hop_data.tlvs,
187199
encrypted_payload_rho,
200+
hop_data.hop_recv_key,
188201
),
189202
});
190203
},
@@ -193,9 +206,19 @@ where
193206
}
194207

195208
/// 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()
209+
fn encrypt_payload<P: Writeable>(
210+
payload: P, encrypted_tlvs_rho: [u8; 32], hop_recv_key: Option<[u8; 32]>,
211+
) -> Vec<u8> {
212+
let mut payload_data = payload.encode();
213+
if let Some(hop_recv_key) = hop_recv_key {
214+
chachapoly_encrypt_with_swapped_aad(payload_data, encrypted_tlvs_rho, hop_recv_key)
215+
} else {
216+
let mut chacha = ChaCha20Poly1305RFC::new(&encrypted_tlvs_rho, &[0; 12], &[]);
217+
let mut tag = [0; 16];
218+
chacha.encrypt_full_message_in_place(&mut payload_data, &mut tag);
219+
payload_data.extend_from_slice(&tag);
220+
payload_data
221+
}
199222
}
200223

201224
/// 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: 7 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,7 @@ 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 { control_tlvs, reply_path: reply_path.take(), message, control_tlvs_authenticated: false, },
22982302
prev_control_tlvs_ss.unwrap(),
22992303
));
23002304
} else {
@@ -2303,6 +2307,7 @@ fn packet_payloads_and_keys<
23032307
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context: None }),
23042308
reply_path: reply_path.take(),
23052309
message,
2310+
control_tlvs_authenticated: false,
23062311
},
23072312
prev_control_tlvs_ss.unwrap(),
23082313
));

lightning/src/onion_message/packet.rs

Lines changed: 17 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,11 @@ 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, reply_path: Option<BlindedMessagePath>, message: T },
116120
}
117121

118122
/// The contents of an [`OnionMessage`] as read from the wire.
@@ -223,6 +227,7 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
223227
control_tlvs: ReceiveControlTlvs::Blinded(encrypted_bytes),
224228
reply_path,
225229
message,
230+
control_tlvs_authenticated: _,
226231
} => {
227232
_encode_varint_length_prefixed_tlv!(w, {
228233
(2, reply_path, option),
@@ -238,6 +243,7 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
238243
control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs),
239244
reply_path,
240245
message,
246+
control_tlvs_authenticated: _,
241247
} => {
242248
let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs);
243249
_encode_varint_length_prefixed_tlv!(w, {
@@ -252,22 +258,22 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
252258
}
253259

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

261267
let v: BigSize = Readable::read(r)?;
262268
let mut rd = FixedLengthReader::new(r, v.0);
263269
let mut reply_path: Option<BlindedMessagePath> = None;
264-
let mut read_adapter: Option<ChaChaPolyReadAdapter<ControlTlvs>> = None;
270+
let mut read_adapter: Option<ChaChaDualPolyReadAdapter<ControlTlvs>> = None;
265271
let rho = onion_utils::gen_rho_from_shared_secret(&encrypted_tlvs_ss.secret_bytes());
266272
let mut message_type: Option<u64> = None;
267273
let mut message = None;
268274
decode_tlv_stream_with_custom_tlv_decode!(&mut rd, {
269275
(2, reply_path, option),
270-
(4, read_adapter, (option: LengthReadableArgs, rho)),
276+
(4, read_adapter, (option: LengthReadableArgs, (rho, receive_tlvs_key))),
271277
}, |msg_type, msg_reader| {
272278
if msg_type < 64 { return Ok(false) }
273279
// Don't allow reading more than one data TLV from an onion message.
@@ -304,17 +310,18 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> ReadableArgs<(Sh
304310

305311
match read_adapter {
306312
None => return Err(DecodeError::InvalidValue),
307-
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Forward(tlvs) }) => {
308-
if message_type.is_some() {
313+
Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => {
314+
if used_aad || message_type.is_some() {
309315
return Err(DecodeError::InvalidValue);
310316
}
311317
Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs)))
312318
},
313-
Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => {
319+
Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => {
314320
Ok(Payload::Receive {
315321
control_tlvs: ReceiveControlTlvs::Unblinded(tlvs),
316322
reply_path,
317323
message: message.ok_or(DecodeError::InvalidValue)?,
324+
control_tlvs_authenticated: used_aad,
318325
})
319326
},
320327
}

0 commit comments

Comments
 (0)