Skip to content

Commit 59993a8

Browse files
committed
Trampoline error decryption and vector tests
Create unit tests covering the hybrid outer and inner onion shared secrets, as well as the Trampoline error test vectors. Additionally, we allow the `outer_session_priv` to be overridden to accommodate the test vector requirements. We do so by separating out a façade method without the override.
1 parent 3da1c88 commit 59993a8

File tree

1 file changed

+257
-20
lines changed

1 file changed

+257
-20
lines changed

lightning/src/ln/onion_utils.rs

Lines changed: 257 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -937,23 +937,58 @@ pub(crate) struct DecodedOnionFailure {
937937
pub(crate) onion_error_data: Option<Vec<u8>>,
938938
}
939939

940+
pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
941+
secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource,
942+
encrypted_packet: OnionErrorPacket,
943+
) -> DecodedOnionFailure
944+
where
945+
L::Target: Logger,
946+
{
947+
let (path, primary_session_priv) = match htlc_source {
948+
HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => (path, session_priv),
949+
_ => unreachable!(),
950+
};
951+
952+
if path.has_trampoline_hops() {
953+
// If we have Trampoline hops, the outer onion session_priv is a hash of the inner one.
954+
let session_priv_hash = Sha256::hash(&primary_session_priv.secret_bytes()).to_byte_array();
955+
let outer_session_priv =
956+
SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!");
957+
process_onion_failure_inner(
958+
secp_ctx,
959+
logger,
960+
htlc_source,
961+
&outer_session_priv,
962+
Some(primary_session_priv),
963+
encrypted_packet,
964+
)
965+
} else {
966+
process_onion_failure_inner(
967+
secp_ctx,
968+
logger,
969+
htlc_source,
970+
primary_session_priv,
971+
None,
972+
encrypted_packet,
973+
)
974+
}
975+
}
976+
940977
/// Process failure we got back from upstream on a payment we sent (implying htlc_source is an
941978
/// OutboundRoute).
942979
#[inline]
943-
pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
944-
secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource,
945-
mut encrypted_packet: OnionErrorPacket,
980+
pub(super) fn process_onion_failure_inner<T: secp256k1::Signing, L: Deref>(
981+
secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource, outer_session_priv: &SecretKey,
982+
inner_session_priv: Option<&SecretKey>, mut encrypted_packet: OnionErrorPacket,
946983
) -> DecodedOnionFailure
947984
where
948985
L::Target: Logger,
949986
{
950-
let (path, session_priv, first_hop_htlc_msat) = match htlc_source {
951-
HTLCSource::OutboundRoute {
952-
ref path, ref session_priv, ref first_hop_htlc_msat, ..
953-
} => (path, session_priv, first_hop_htlc_msat),
954-
_ => {
955-
unreachable!()
987+
let (path, first_hop_htlc_msat) = match htlc_source {
988+
HTLCSource::OutboundRoute { ref path, ref first_hop_htlc_msat, .. } => {
989+
(path, first_hop_htlc_msat)
956990
},
991+
_ => unreachable!(),
957992
};
958993

959994
// Learnings from the HTLC failure to inform future payment retries and scoring.
@@ -1002,12 +1037,6 @@ where
10021037
}
10031038
}
10041039

1005-
let outer_session_priv = path.has_trampoline_hops().then(|| {
1006-
// if we have Trampoline hops, the outer onion session_priv is a hash of the inner one
1007-
let session_priv_hash = Sha256::hash(&session_priv.secret_bytes()).to_byte_array();
1008-
SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!")
1009-
});
1010-
10111040
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
10121041

10131042
// We are first collecting all the unblinded `RouteHop`s inside `onion_keys`. Then, if applicable,
@@ -1018,7 +1047,7 @@ where
10181047
&path.hops,
10191048
// if we have Trampoline hops, the blinded hops are part of the inner Trampoline onion
10201049
if path.has_trampoline_hops() { None } else { path.blinded_tail.as_ref() },
1021-
outer_session_priv.as_ref().unwrap_or(session_priv),
1050+
outer_session_priv,
10221051
|shared_secret, _, _, route_hop_option: Option<&RouteHop>, _| {
10231052
onion_keys.push((route_hop_option.map(|rh| ErrorHop::RouteHop(rh)), shared_secret))
10241053
},
@@ -1031,7 +1060,7 @@ where
10311060
// Trampoline hops are part of the blinded tail, so this can never panic
10321061
&path.blinded_tail.as_ref().unwrap().trampoline_hops,
10331062
path.blinded_tail.as_ref(),
1034-
session_priv,
1063+
inner_session_priv.expect("Trampoline hops always have an inner session priv"),
10351064
|shared_secret, _, _, trampoline_hop_option: Option<&TrampolineHop>, _| {
10361065
onion_keys.push((
10371066
trampoline_hop_option.map(|th| ErrorHop::TrampolineHop(th)),
@@ -2039,11 +2068,11 @@ mod tests {
20392068
use crate::prelude::*;
20402069
use crate::util::test_utils::TestLogger;
20412070

2071+
use super::*;
20422072
use bitcoin::hex::FromHex;
20432073
use bitcoin::secp256k1::Secp256k1;
20442074
use bitcoin::secp256k1::{PublicKey, SecretKey};
2045-
2046-
use super::*;
2075+
use types::features::Features;
20472076

20482077
fn get_test_session_key() -> SecretKey {
20492078
let hex = "4141414141414141414141414141414141414141414141414141414141414141";
@@ -2400,10 +2429,218 @@ mod tests {
24002429
// Assert that the original failure can be retrieved and that all hmacs check out.
24012430
let decrypted_failure =
24022431
process_onion_failure(&ctx_full, &logger, &htlc_source, onion_error);
2403-
24042432
assert_eq!(decrypted_failure.onion_error_code, Some(0x2002));
24052433
}
24062434

2435+
fn build_trampoline_test_path() -> Path {
2436+
Path {
2437+
hops: vec![
2438+
// Bob
2439+
RouteHop {
2440+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()).unwrap(),
2441+
node_features: NodeFeatures::empty(),
2442+
short_channel_id: 0,
2443+
channel_features: ChannelFeatures::empty(),
2444+
fee_msat: 3_000,
2445+
cltv_expiry_delta: 24,
2446+
maybe_announced_channel: false,
2447+
},
2448+
2449+
// Carol
2450+
RouteHop {
2451+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(),
2452+
node_features: NodeFeatures::empty(),
2453+
short_channel_id: (572330 << 40) + (42 << 16) + 2821,
2454+
channel_features: ChannelFeatures::empty(),
2455+
fee_msat: 153_000,
2456+
cltv_expiry_delta: 0,
2457+
maybe_announced_channel: false,
2458+
},
2459+
],
2460+
blinded_tail: Some(BlindedTail {
2461+
trampoline_hops: vec![
2462+
// Carol's pubkey
2463+
TrampolineHop {
2464+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(),
2465+
node_features: Features::empty(),
2466+
fee_msat: 2_500,
2467+
cltv_expiry_delta: 24,
2468+
},
2469+
2470+
// Dave's pubkey
2471+
TrampolineHop {
2472+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145").unwrap()).unwrap(),
2473+
node_features: Features::empty(),
2474+
fee_msat: 2_500,
2475+
cltv_expiry_delta: 24,
2476+
},
2477+
2478+
// Emily's pubkey
2479+
TrampolineHop {
2480+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()).unwrap(),
2481+
node_features: Features::empty(),
2482+
fee_msat: 150_500,
2483+
cltv_expiry_delta: 36,
2484+
},
2485+
],
2486+
2487+
// Dummy blinded hop (because LDK doesn't allow unblinded Trampoline receives)
2488+
hops: vec![
2489+
// Emily's dummy blinded node id
2490+
BlindedHop {
2491+
blinded_node_id: PublicKey::from_slice(&<Vec<u8>>::from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be").unwrap()).unwrap(),
2492+
encrypted_payload: vec![],
2493+
}
2494+
],
2495+
blinding_point: PublicKey::from_slice(&<Vec<u8>>::from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e").unwrap()).unwrap(),
2496+
excess_final_cltv_expiry_delta: 0,
2497+
final_value_msat: 150_000_000,
2498+
}),
2499+
}
2500+
}
2501+
2502+
#[test]
2503+
fn test_trampoline_onion_error_cryptography() {
2504+
// TODO(arik): check intermediate hops' perspectives once we have implemented forwarding
2505+
2506+
let secp_ctx = Secp256k1::new();
2507+
let logger: Arc<TestLogger> = Arc::new(TestLogger::new());
2508+
let dummy_amt_msat = 150_000_000;
2509+
2510+
{
2511+
// test vector per https://github.com/lightning/bolts/blob/079f761bf68caa48544bd6bf0a29591d43425b0b/bolt04/trampoline-onion-error-test.json
2512+
// all dummy values
2513+
let trampoline_session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
2514+
let outer_session_priv = SecretKey::from_slice(&[4; 32]).unwrap();
2515+
2516+
let htlc_source = HTLCSource::OutboundRoute {
2517+
path: build_trampoline_test_path(),
2518+
session_priv: trampoline_session_priv,
2519+
first_hop_htlc_msat: dummy_amt_msat,
2520+
payment_id: PaymentId([1; 32]),
2521+
};
2522+
2523+
let error_packet_hex = "f8941a320b8fde4ad7b9b920c69cbf334114737497d93059d77e591eaa78d6334d3e2aeefcb0cc83402eaaf91d07d695cd895d9cad1018abdaf7d2a49d7657b1612729db7f393f0bb62b25afaaaa326d72a9214666025385033f2ec4605dcf1507467b5726d806da180ea224a7d8631cd31b0bdd08eead8bfe14fc8c7475e17768b1321b54dd4294aecc96da391efe0ca5bd267a45ee085c85a60cf9a9ac152fa4795fff8700a3ea4f848817f5e6943e855ab2e86f6929c9e885d8b20c49b14d2512c59ed21f10bd38691110b0d82c00d9fa48a20f10c7550358724c6e8e2b966e56a0aadf458695b273768062fa7c6e60eb72d4cdc67bf525c194e4a17fdcaa0e9d80480b586bf113f14eea530b6728a1c53fe5cee092e24a90f21f4b764015e7ed5e23";
2524+
let error_packet =
2525+
OnionErrorPacket { data: <Vec<u8>>::from_hex(error_packet_hex).unwrap() };
2526+
let decrypted_failure = process_onion_failure_inner(
2527+
&secp_ctx,
2528+
&logger,
2529+
&htlc_source,
2530+
&outer_session_priv,
2531+
Some(&trampoline_session_priv),
2532+
error_packet,
2533+
);
2534+
assert_eq!(decrypted_failure.onion_error_code, Some(0x400f));
2535+
}
2536+
2537+
{
2538+
// shared secret cryptography sanity tests
2539+
let session_priv = get_test_session_key();
2540+
let path = build_trampoline_test_path();
2541+
2542+
let trampoline_onion_keys = construct_trampoline_onion_keys(
2543+
&secp_ctx,
2544+
&path.blinded_tail.as_ref().unwrap(),
2545+
&session_priv,
2546+
)
2547+
.unwrap();
2548+
2549+
let outer_onion_keys = {
2550+
let session_priv_hash = Sha256::hash(&session_priv.secret_bytes()).to_byte_array();
2551+
let outer_session_priv = SecretKey::from_slice(&session_priv_hash[..]).unwrap();
2552+
construct_onion_keys(&Secp256k1::new(), &path, &outer_session_priv).unwrap()
2553+
};
2554+
2555+
let htlc_source = HTLCSource::OutboundRoute {
2556+
path,
2557+
session_priv,
2558+
first_hop_htlc_msat: dummy_amt_msat,
2559+
payment_id: PaymentId([1; 32]),
2560+
};
2561+
2562+
{
2563+
// Ensure error decryption works without the Trampoline hops having been hit.
2564+
let error_code = 0x2002;
2565+
let mut first_hop_error_packet = build_unencrypted_failure_packet(
2566+
outer_onion_keys[0].shared_secret.as_ref(),
2567+
error_code,
2568+
&[0; 0],
2569+
);
2570+
2571+
crypt_failure_packet(
2572+
outer_onion_keys[0].shared_secret.as_ref(),
2573+
&mut first_hop_error_packet,
2574+
);
2575+
2576+
let decrypted_failure =
2577+
process_onion_failure(&secp_ctx, &logger, &htlc_source, first_hop_error_packet);
2578+
assert_eq!(decrypted_failure.onion_error_code, Some(error_code));
2579+
};
2580+
2581+
{
2582+
// Ensure error decryption works from the first Trampoline hop, but at the outer onion.
2583+
let error_code = 0x2003;
2584+
let mut trampoline_outer_hop_error_packet = build_unencrypted_failure_packet(
2585+
outer_onion_keys[1].shared_secret.as_ref(),
2586+
error_code,
2587+
&[0; 0],
2588+
);
2589+
2590+
crypt_failure_packet(
2591+
outer_onion_keys[1].shared_secret.as_ref(),
2592+
&mut trampoline_outer_hop_error_packet,
2593+
);
2594+
2595+
crypt_failure_packet(
2596+
outer_onion_keys[0].shared_secret.as_ref(),
2597+
&mut trampoline_outer_hop_error_packet,
2598+
);
2599+
2600+
let decrypted_failure = process_onion_failure(
2601+
&secp_ctx,
2602+
&logger,
2603+
&htlc_source,
2604+
trampoline_outer_hop_error_packet,
2605+
);
2606+
assert_eq!(decrypted_failure.onion_error_code, Some(error_code));
2607+
};
2608+
2609+
{
2610+
// Ensure error decryption works from the Trampoline inner onion.
2611+
let error_code = 0x2004;
2612+
let mut trampoline_inner_hop_error_packet = build_unencrypted_failure_packet(
2613+
trampoline_onion_keys[0].shared_secret.as_ref(),
2614+
error_code,
2615+
&[0; 0],
2616+
);
2617+
2618+
crypt_failure_packet(
2619+
trampoline_onion_keys[0].shared_secret.as_ref(),
2620+
&mut trampoline_inner_hop_error_packet,
2621+
);
2622+
2623+
crypt_failure_packet(
2624+
outer_onion_keys[1].shared_secret.as_ref(),
2625+
&mut trampoline_inner_hop_error_packet,
2626+
);
2627+
2628+
crypt_failure_packet(
2629+
outer_onion_keys[0].shared_secret.as_ref(),
2630+
&mut trampoline_inner_hop_error_packet,
2631+
);
2632+
2633+
let decrypted_failure = process_onion_failure(
2634+
&secp_ctx,
2635+
&logger,
2636+
&htlc_source,
2637+
trampoline_inner_hop_error_packet,
2638+
);
2639+
assert_eq!(decrypted_failure.onion_error_code, Some(error_code));
2640+
}
2641+
}
2642+
}
2643+
24072644
#[test]
24082645
fn test_non_attributable_failure_packet_onion() {
24092646
// Create a failure packet with bogus data.

0 commit comments

Comments
 (0)