Skip to content

Commit 6570504

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]`. Co-authored-by: Shashwat Vangani <[email protected]>
1 parent d756768 commit 6570504

File tree

7 files changed

+119
-41
lines changed

7 files changed

+119
-41
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};
@@ -94,6 +94,7 @@ impl BlindedMessagePath {
9494
recipient_node_id,
9595
context,
9696
&blinding_secret,
97+
ReceiveAuthKey([41; 32]), // TODO: Pass this in
9798
)
9899
.map_err(|_| ())?,
99100
}))
@@ -661,18 +662,19 @@ pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100;
661662
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
662663
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
663664
recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey,
665+
local_node_receive_key: ReceiveAuthKey,
664666
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
665667
let pks = intermediate_nodes
666668
.iter()
667-
.map(|node| node.node_id)
668-
.chain(core::iter::once(recipient_node_id));
669+
.map(|node| (node.node_id, None))
670+
.chain(core::iter::once((recipient_node_id, Some(local_node_receive_key))));
669671
let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some());
670672

671673
let tlvs = pks
672674
.clone()
673675
.skip(1) // The first node's TLVs contains the next node's pubkey
674676
.zip(intermediate_nodes.iter().map(|node| node.short_channel_id))
675-
.map(|(pubkey, scid)| match scid {
677+
.map(|((pubkey, _), scid)| match scid {
676678
Some(scid) => NextMessageHop::ShortChannelId(scid),
677679
None => NextMessageHop::NodeId(pubkey),
678680
})

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: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ 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::streams::{chachapoly_encrypt_with_swapped_aad, ChaChaPolyWriteAdapter};
2121
use crate::io;
2222
use crate::ln::onion_utils;
2323
use crate::onion_message::messenger::Destination;
24+
use crate::sign::ReceiveAuthKey;
2425
use crate::util::ser::{Writeable, Writer};
2526

2627
use core::borrow::Borrow;
@@ -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
@@ -157,6 +156,7 @@ where
157156

158157
struct PublicKeyWithTlvs<W: Writeable> {
159158
pubkey: PublicKey,
159+
hop_recv_key: Option<ReceiveAuthKey>,
160160
tlvs: W,
161161
}
162162

@@ -171,20 +171,26 @@ pub(crate) fn construct_blinded_hops<'a, T, I, W>(
171171
) -> Result<Vec<BlindedHop>, secp256k1::Error>
172172
where
173173
T: secp256k1::Signing + secp256k1::Verification,
174-
I: Iterator<Item = (PublicKey, W)>,
174+
I: Iterator<Item = ((PublicKey, Option<ReceiveAuthKey>), W)>,
175175
W: Writeable,
176176
{
177177
let mut blinded_hops = Vec::with_capacity(unblinded_path.size_hint().0);
178178
construct_keys_for_blinded_path(
179179
secp_ctx,
180-
unblinded_path.map(|(pubkey, tlvs)| PublicKeyWithTlvs { pubkey, tlvs }),
180+
unblinded_path.map(|((pubkey, hop_recv_key), tlvs)| PublicKeyWithTlvs {
181+
pubkey,
182+
hop_recv_key,
183+
tlvs,
184+
}),
181185
session_priv,
182186
|blinded_node_id, _, _, encrypted_payload_rho, unblinded_hop_data, _| {
187+
let hop_data = unblinded_hop_data.unwrap();
183188
blinded_hops.push(BlindedHop {
184189
blinded_node_id,
185190
encrypted_payload: encrypt_payload(
186-
unblinded_hop_data.unwrap().tlvs,
191+
hop_data.tlvs,
187192
encrypted_payload_rho,
193+
hop_data.hop_recv_key,
188194
),
189195
});
190196
},
@@ -193,9 +199,15 @@ where
193199
}
194200

195201
/// 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()
202+
fn encrypt_payload<P: Writeable>(
203+
payload: P, encrypted_tlvs_rho: [u8; 32], hop_recv_key: Option<ReceiveAuthKey>,
204+
) -> Vec<u8> {
205+
if let Some(hop_recv_key) = hop_recv_key {
206+
chachapoly_encrypt_with_swapped_aad(payload.encode(), encrypted_tlvs_rho, hop_recv_key.0)
207+
} else {
208+
let write_adapter = ChaChaPolyWriteAdapter::new(encrypted_tlvs_rho, &payload);
209+
write_adapter.encode()
210+
}
199211
}
200212

201213
/// 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: 44 additions & 5 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,22 +1074,39 @@ 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) {
10921094
(ParsedOnionMessageContents::Offers(msg), Some(MessageContext::Offers(ctx))) => {
1095+
match ctx {
1096+
OffersContext::InvoiceRequest { .. } => {
1097+
// Note: We introduced the `control_tlvs_authenticated` check in LDK v0.2
1098+
// to simplify and standardize onion message authentication.
1099+
// To continue supporting offers created before v0.2, we allow
1100+
// unauthenticated control TLVs for these messages, as they can be
1101+
// verified using the legacy method.
1102+
},
1103+
_ => {
1104+
if !control_tlvs_authenticated {
1105+
log_trace!(logger, "Received an unauthenticated offers onion message");
1106+
return Err(());
1107+
}
1108+
},
1109+
}
10931110
Ok(PeeledOnion::Offers(msg, Some(ctx), reply_path))
10941111
},
10951112
(ParsedOnionMessageContents::Offers(msg), None) => {
@@ -1099,8 +1116,18 @@ where
10991116
(
11001117
ParsedOnionMessageContents::AsyncPayments(msg),
11011118
Some(MessageContext::AsyncPayments(ctx)),
1102-
) => Ok(PeeledOnion::AsyncPayments(msg, ctx, reply_path)),
1119+
) => {
1120+
if !control_tlvs_authenticated {
1121+
log_trace!(logger, "Received an unauthenticated async payments onion message");
1122+
return Err(());
1123+
}
1124+
Ok(PeeledOnion::AsyncPayments(msg, ctx, reply_path))
1125+
},
11031126
(ParsedOnionMessageContents::Custom(msg), Some(MessageContext::Custom(ctx))) => {
1127+
if !control_tlvs_authenticated {
1128+
log_trace!(logger, "Received an unauthenticated custom onion message");
1129+
return Err(());
1130+
}
11041131
Ok(PeeledOnion::Custom(msg, Some(ctx), reply_path))
11051132
},
11061133
(ParsedOnionMessageContents::Custom(msg), None) => {
@@ -1109,7 +1136,13 @@ where
11091136
(
11101137
ParsedOnionMessageContents::DNSResolver(msg),
11111138
Some(MessageContext::DNSResolver(ctx)),
1112-
) => Ok(PeeledOnion::DNSResolver(msg, Some(ctx), reply_path)),
1139+
) => {
1140+
if !control_tlvs_authenticated {
1141+
log_trace!(logger, "Received an unauthenticated DNS resolver onion message");
1142+
return Err(());
1143+
}
1144+
Ok(PeeledOnion::DNSResolver(msg, Some(ctx), reply_path))
1145+
},
11131146
(ParsedOnionMessageContents::DNSResolver(msg), None) => {
11141147
Ok(PeeledOnion::DNSResolver(msg, None, reply_path))
11151148
},
@@ -2334,7 +2367,12 @@ fn packet_payloads_and_keys<
23342367

23352368
if let Some(control_tlvs) = final_control_tlvs {
23362369
payloads.push((
2337-
Payload::Receive { control_tlvs, reply_path: reply_path.take(), message },
2370+
Payload::Receive {
2371+
control_tlvs,
2372+
reply_path: reply_path.take(),
2373+
message,
2374+
control_tlvs_authenticated: false,
2375+
},
23382376
prev_control_tlvs_ss.unwrap(),
23392377
));
23402378
} else {
@@ -2343,6 +2381,7 @@ fn packet_payloads_and_keys<
23432381
control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context: None }),
23442382
reply_path: reply_path.take(),
23452383
message,
2384+
control_tlvs_authenticated: false,
23462385
},
23472386
prev_control_tlvs_ss.unwrap(),
23482387
));

0 commit comments

Comments
 (0)