Lp/stateless handshake (#6437)

* perform KKT/PSQ handshake outside of LPStateMachine

* initiator

* responder

* concurrent test

* remove KTT/PSQ from the LpStateMachine

* adjusted gateway's Handler to accomodate new changes

* filling in placehlders

* fixed imports in nym-kkt crate

* naming

* clippy and moved more placeholder tests

* split up the initiator side of the PSQ

* split up the responder side of the PSQ

* additional helpers

* addressing review comments

* additional tests and explicit Error message
This commit is contained in:
Jędrzej Stuczyński
2026-02-10 17:20:54 +00:00
committed by GitHub
parent 9cb2655e7d
commit bb694855d5
41 changed files with 2815 additions and 5126 deletions
Generated
+4 -1
View File
@@ -6925,6 +6925,7 @@ dependencies = [
name = "nym-lp"
version = "0.1.0"
dependencies = [
"anyhow",
"bs58",
"bytes",
"chacha20poly1305",
@@ -6933,20 +6934,22 @@ dependencies = [
"libcrux-kem",
"libcrux-psq",
"libcrux-traits",
"mock_instant",
"num_enum",
"nym-crypto",
"nym-kkt",
"nym-lp-common",
"nym-lp-transport",
"nym-test-utils",
"parking_lot",
"rand 0.8.5",
"rand 0.9.2",
"rand_chacha 0.3.1",
"serde",
"sha2 0.10.9",
"snow",
"thiserror 2.0.17",
"tls_codec",
"tokio",
"tracing",
"zeroize",
]
+4 -3
View File
@@ -320,6 +320,7 @@ publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand09 = { package = "rand", version = "=0.9.2" }
rand_chacha = "0.3"
rand_core = "0.6.3"
rand_distr = "0.4"
@@ -483,9 +484,9 @@ nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smar
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
+1 -1
View File
@@ -10,7 +10,7 @@ pub use opentelemetry;
pub use opentelemetry_jaeger;
#[cfg(feature = "tracing")]
pub use tracing_opentelemetry;
#[cfg(feature = "tracing")]
#[cfg(feature = "basic_tracing")]
pub use tracing_subscriber;
#[cfg(feature = "tracing")]
pub use tracing_tree;
+2 -2
View File
@@ -33,7 +33,7 @@ thiserror = { workspace = true }
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
# internal
nym-sphinx-types = { workspace = true }
nym-sphinx-types = { workspace = true, optional = true }
nym-pemstore = { workspace = true }
[dev-dependencies]
@@ -51,7 +51,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
sphinx = ["nym-sphinx-types/sphinx"]
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
[lints]
workspace = true
+2 -1
View File
@@ -21,7 +21,8 @@ libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
rand = "0.9.2"
# rand 0.9 for libcrux integration (libcrux uses rand 0.9)
rand09 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
+22 -22
View File
@@ -18,13 +18,13 @@ use nym_kkt::{
responder_ingest_message, responder_process,
},
};
use rand::prelude::*;
use rand09::prelude::*;
pub fn gen_ed25519_keypair(c: &mut Criterion) {
c.bench_function("Generate Ed25519 Keypair", |b| {
b.iter(|| {
let mut s: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut s);
rand09::rng().fill_bytes(&mut s);
ed25519::KeyPair::from_secret(s, 0)
});
});
@@ -33,13 +33,13 @@ pub fn gen_ed25519_keypair(c: &mut Criterion) {
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
c.bench_function("Generate MlKem768 Keypair", |b| {
b.iter(|| {
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
});
});
}
pub fn kkt_benchmark(c: &mut Criterion) {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
@@ -111,7 +111,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (mut i_context, i_frame) =
let (i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
c.bench_function(
@@ -143,7 +143,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (mut r_context, _) =
let (r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
c.bench_function(
@@ -153,7 +153,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -163,7 +163,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -184,7 +184,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -196,7 +196,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -208,7 +208,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
let (i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::OneWay,
ciphersuite,
@@ -262,7 +262,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (mut r_context, r_obtained_key) = responder_ingest_message(
let (r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
@@ -279,7 +279,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -290,7 +290,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -311,7 +311,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -323,7 +323,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -352,7 +352,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (mut i_context, i_frame) = initiator_process(
let (i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::Mutual,
ciphersuite,
@@ -394,7 +394,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (mut r_context, r_obtained_key) = responder_ingest_message(
let (r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
@@ -411,7 +411,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -422,7 +422,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -445,7 +445,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -457,7 +457,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
+3 -4
View File
@@ -5,7 +5,7 @@ use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::
use blake3::Hasher;
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
use nym_crypto::asymmetric::x25519;
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
use zeroize::Zeroize;
#[derive(Clone, Copy, Zeroize)]
@@ -182,8 +182,7 @@ mod test {
encryption::{KKTSessionSecret, decrypt, encrypt},
key_utils::generate_keypair_x25519,
};
use rand::{RngCore, SeedableRng, rng};
use rand_chacha::ChaCha20Rng;
use rand09::{RngCore, SeedableRng, rng};
#[test]
fn test_keygen() {
@@ -227,7 +226,7 @@ mod test {
#[test]
fn kkt_frame_encryption() -> anyhow::Result<()> {
let mut rng = ChaCha20Rng::seed_from_u64(42);
let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
let aad = b"my-amazing-aad";
+1 -2
View File
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use classic_mceliece_rust::keypair_boxed;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
pub fn generate_keypair_ed25519<R>(
rng: &mut R,
@@ -61,7 +61,6 @@ pub fn generate_keypair_mceliece<'a, R>(
classic_mceliece_rust::PublicKey<'a>,
)
where
// this is annoying because mceliece lib uses rand 0.8.5...
R: RngCore + CryptoRng,
{
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
+14 -15
View File
@@ -9,7 +9,7 @@
//! The underlying KKT protocol is implemented in the `session` module.
use nym_crypto::asymmetric::{ed25519, x25519};
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey},
@@ -33,7 +33,7 @@ use crate::frame::KKTFrame;
/// The request will be signed with the provided signing key.
///
/// # Arguments
/// * `rng` - Random number generator
/// * `rng` - random number generator
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
/// * `signing_key` - Client's Ed25519 signing key for authentication
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
@@ -90,7 +90,7 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
/// # Example
/// ```ignore
/// let gateway_kem_key = validate_kem_response(
/// &mut context,
/// &context,
/// &session_secret,
/// &gateway_verification_key,
/// &expected_hash_from_directory,
@@ -199,14 +199,14 @@ mod tests {
fn random_x25519_key() -> x25519::PrivateKey {
let mut bytes = [0u8; 32];
let mut rng = rand::rng();
let mut rng = rand09::rng();
rng.fill_bytes(&mut bytes);
x25519::PrivateKey::from_secret(bytes)
}
#[test]
fn test_kkt_wrappers_oneway_authenticated() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// Generate Ed25519 keypairs for both parties
let mut initiator_secret = [0u8; 32];
@@ -241,7 +241,7 @@ mod tests {
);
// Client: Request KEM key
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
let (session_key, context, request_frame_ciphertext) = request_kem_key(
&mut rng,
ciphersuite,
ed25519_init.private_key(),
@@ -262,7 +262,7 @@ mod tests {
// Client: Validate response
let obtained_key = validate_kem_response(
&mut context,
&context,
&session_key,
ed25519_resp.public_key(),
&key_hash,
@@ -276,7 +276,7 @@ mod tests {
#[test]
fn test_kkt_wrappers_anonymous() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// Only responder has keys
let mut responder_secret = [0u8; 32];
@@ -304,8 +304,7 @@ mod tests {
);
// Anonymous initiator
let (mut context, request_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// Generate the session's shared secret and encrypt the Initiator's request
let (session_secret, encrypted_request_bytes) =
@@ -324,7 +323,7 @@ mod tests {
// Initiator: Validate response
let obtained_key = validate_kem_response(
&mut context,
&context,
&session_secret,
responder_keypair.public_key(),
&key_hash,
@@ -337,7 +336,7 @@ mod tests {
#[test]
fn test_invalid_signature_rejected() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
@@ -390,7 +389,7 @@ mod tests {
#[test]
fn test_hash_mismatch_rejected() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
@@ -417,7 +416,7 @@ mod tests {
// Use WRONG hash
let wrong_hash = [0u8; 32];
let (session_key, mut context, request_frame) = request_kem_key(
let (session_key, context, request_frame) = request_kem_key(
&mut rng,
ciphersuite,
initiator_keypair.private_key(),
@@ -437,7 +436,7 @@ mod tests {
// Client validates with WRONG hash
let result = validate_kem_response(
&mut context,
&context,
&session_key,
responder_keypair.public_key(),
&wrong_hash, // Wrong!
+26 -26
View File
@@ -38,7 +38,7 @@ mod test {
#[test]
fn test_kkt_psq_e2e_clear() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// generate ed25519 keys
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
@@ -106,18 +106,18 @@ mod test {
// Anonymous Initiator, OneWay
{
let (mut i_context, i_frame) =
let (i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, _) =
let (r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -129,7 +129,7 @@ mod test {
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -141,7 +141,7 @@ mod test {
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
let (i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
@@ -154,7 +154,7 @@ mod test {
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
let (r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
@@ -165,7 +165,7 @@ mod test {
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -177,7 +177,7 @@ mod test {
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -190,7 +190,7 @@ mod test {
// Initiator, Mutual
{
let (mut i_context, i_frame) = initiator_process(
let (i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
@@ -203,7 +203,7 @@ mod test {
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
let (r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
@@ -214,7 +214,7 @@ mod test {
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -226,7 +226,7 @@ mod test {
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -241,7 +241,7 @@ mod test {
}
#[test]
fn test_kkt_psq_e2e_encrypted() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// generate ed25519 keys
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
@@ -312,7 +312,7 @@ mod test {
// Anonymous Initiator, OneWay
{
let (mut i_context, i_frame) =
let (i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// encryption - initiator frame
@@ -330,11 +330,11 @@ mod test {
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (mut r_context, _) =
let (r_context, _) =
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -352,7 +352,7 @@ mod test {
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -364,7 +364,7 @@ mod test {
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
let (i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
@@ -388,7 +388,7 @@ mod test {
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
let (r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
@@ -399,7 +399,7 @@ mod test {
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -417,7 +417,7 @@ mod test {
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -430,7 +430,7 @@ mod test {
// Initiator, Mutual
{
let (mut i_context, i_frame) = initiator_process(
let (i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
@@ -454,7 +454,7 @@ mod test {
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
let (r_context, r_obtained_key) = responder_ingest_message(
&i_context_r,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
@@ -465,7 +465,7 @@ mod test {
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&mut r_context,
&r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -483,7 +483,7 @@ mod test {
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
+3 -3
View File
@@ -1,5 +1,5 @@
use nym_crypto::asymmetric::ed25519::{self, Signature};
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
use crate::frame::KKTSessionId;
use crate::{
@@ -73,7 +73,7 @@ where
}
pub fn initiator_ingest_response<'a>(
own_context: &mut KKTContext,
own_context: &KKTContext,
remote_frame: &KKTFrame,
remote_context: &KKTContext,
remote_verification_key: &ed25519::PublicKey,
@@ -201,7 +201,7 @@ pub fn responder_ingest_message<'a>(
}
pub fn responder_process<'a>(
own_context: &mut KKTContext,
own_context: &KKTContext,
session_id: KKTSessionId,
signing_key: &ed25519::PrivateKey,
encapsulation_key: &EncapsulationKey<'a>,
+79 -51
View File
@@ -10,7 +10,7 @@ use tracing::debug;
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
pub trait LpTransport: Sized {
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
@@ -24,32 +24,7 @@ pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
///
/// # Errors
/// Returns an error on network transmission fails.
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>
where
Self: Unpin,
{
// Send 4-byte length prefix (u32 big-endian)
let len = packet_data.len() as u32;
self.write_all(&len.to_be_bytes())
.await
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
// Send the actual packet data
self.write_all(packet_data)
.await
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
// Flush to ensure data is sent immediately
self.flush()
.await
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
tracing::trace!(
"Sent LP packet ({} bytes + 4 byte header)",
packet_data.len()
);
Ok(())
}
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
/// Receives an LP packet from a TCP stream with length-prefixed framing.
///
@@ -57,35 +32,72 @@ pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
///
/// # Errors
/// Returns an error on network transmission fails.
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>
where
Self: Unpin,
{
// Read 4-byte length prefix (u32 big-endian)
let mut len_buf = [0u8; 4];
self.read_exact(&mut len_buf)
.await
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>;
}
let packet_len = u32::from_be_bytes(len_buf) as usize;
async fn send_serialised_packet_async_write<W>(
writer: &mut W,
packet_data: &[u8],
) -> std::io::Result<()>
where
W: AsyncWrite + Unpin,
{
// Send 4-byte length prefix (u32 big-endian)
let len = packet_data.len() as u32;
writer
.write_all(&len.to_be_bytes())
.await
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
// Sanity check to prevent huge allocations
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
if packet_len > MAX_PACKET_SIZE {
return Err(std::io::Error::other(format!(
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
)));
}
// Send the actual packet data
writer
.write_all(packet_data)
.await
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
// Read the actual packet data
let mut packet_buf = vec![0u8; packet_len];
self.read_exact(&mut packet_buf)
.await
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
// Flush to ensure data is sent immediately
writer
.flush()
.await
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
Ok(packet_buf)
tracing::trace!(
"Sent LP packet ({} bytes + 4 byte header)",
packet_data.len()
);
Ok(())
}
async fn receive_raw_packet_async_read<R>(reader: &mut R) -> std::io::Result<Vec<u8>>
where
R: AsyncRead + Unpin,
{
// Read 4-byte length prefix (u32 big-endian)
let mut len_buf = [0u8; 4];
reader
.read_exact(&mut len_buf)
.await
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
let packet_len = u32::from_be_bytes(len_buf) as usize;
// Sanity check to prevent huge allocations
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
if packet_len > MAX_PACKET_SIZE {
return Err(std::io::Error::other(format!(
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
)));
}
// Read the actual packet data
let mut packet_buf = vec![0u8; packet_len];
reader
.read_exact(&mut packet_buf)
.await
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
Ok(packet_buf)
}
impl LpTransport for TcpStream {
@@ -97,6 +109,14 @@ impl LpTransport for TcpStream {
// Set TCP_NODELAY for low latency
self.set_nodelay(nodelay)
}
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
send_serialised_packet_async_write(self, packet_data).await
}
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
receive_raw_packet_async_read(self).await
}
}
#[cfg(feature = "io-mocks")]
@@ -108,4 +128,12 @@ impl LpTransport for MockIOStream {
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
Ok(())
}
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
send_serialised_packet_async_write(self, packet_data).await
}
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
receive_raw_packet_async_read(self).await
}
}
+12 -2
View File
@@ -17,11 +17,12 @@ sha2 = { workspace = true }
tracing = { workspace = true }
rand = { workspace = true }
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
rand09 = { package = "rand", version = "0.9.2" }
rand09 = { workspace = true }
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
nym-kkt = { path = "../nym-kkt" }
nym-lp-common = { path = "../nym-lp-common" }
nym-lp-transport = { path = "../nym-lp-transport" }
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
@@ -34,12 +35,21 @@ num_enum = { workspace = true }
chacha20poly1305 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
# needed for the 'mock 'feature
nym-test-utils = { workspace = true, optional = true }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand_chacha = "0.3"
#rand_chacha = "0.3"
mock_instant = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
nym-test-utils = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] }
[features]
mock = ["nym-test-utils", "nym-crypto/rand"]
[[bench]]
name = "replay_protection"
+3 -3
View File
@@ -1,8 +1,8 @@
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
use nym_lp::replay::ReceivingKeyCounterValidator;
use nym_test_utils::helpers::u64_seeded_rng;
use parking_lot::Mutex;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use rand::Rng;
use std::sync::Arc;
fn bench_sequential_counters(c: &mut Criterion) {
@@ -47,7 +47,7 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
let validator = ReceivingKeyCounterValidator::default();
// Create random counters within a valid window
let mut rng = ChaCha8Rng::seed_from_u64(42);
let mut rng = u64_seeded_rng(42);
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
b.iter(|| {
+37 -4
View File
@@ -555,7 +555,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type
// Need payload and trailer to meet min_size requirement
let payload_size = 10; // Arbitrary
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
@@ -565,7 +565,7 @@ mod tests {
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::InvalidMessageType(255)) => {} // Expected error
Err(LpError::InvalidMessageType(231)) => {} // Expected error
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
Ok(_) => panic!("Expected error, but got Ok"),
}
@@ -628,7 +628,7 @@ mod tests {
receiver_idx: 42,
counter: 123,
},
message: LpMessage::ClientHello(hello_data.clone()),
message: LpMessage::ClientHello(hello_data),
trailer: [0; TRAILER_LEN],
};
@@ -681,7 +681,7 @@ mod tests {
receiver_idx: 100,
counter: 200,
},
message: LpMessage::ClientHello(hello_data.clone()),
message: LpMessage::ClientHello(hello_data),
trailer: [55; TRAILER_LEN],
};
@@ -1289,4 +1289,37 @@ mod tests {
_ => panic!("Expected SubsessionKK1 message"),
}
}
#[test]
fn test_serialize_parse_error() {
use crate::message::ErrorPacketData;
let mut dst = BytesMut::new();
let error_data = ErrorPacketData {
message: "this is an error".to_string(),
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 200,
},
message: LpMessage::Error(error_data.clone()),
trailer: [0; TRAILER_LEN],
};
serialize_lp_packet(&packet, &mut dst, None).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
assert_eq!(decoded.header.receiver_idx, 42);
match decoded.message {
LpMessage::Error(data) => {
assert_eq!(data.message, "this is an error");
}
_ => panic!("Expected Error message"),
}
}
}
+23
View File
@@ -1,9 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::message::MessageType;
use crate::{noise_protocol::NoiseError, replay::ReplayError};
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use nym_kkt::ciphersuite::{HashFunction, KEM};
use nym_kkt::error::KKTError;
use thiserror::Error;
#[derive(Error, Debug)]
@@ -102,4 +104,25 @@ pub enum LpError {
kem: KEM,
hash_function: HashFunction,
},
#[error("failed to complete KKT/PSQ handshake: {0}")]
KKTPSQHandshake(String),
#[error("failed to complete the KKT exchange: {source}")]
KKTFailure {
#[from]
source: KKTError,
},
}
impl LpError {
pub fn kkt_psq_handshake(msg: impl Into<String>) -> Self {
Self::KKTPSQHandshake(msg.into())
}
pub fn unexpected_handshake_response(got: MessageType, expected: MessageType) -> LpError {
Self::KKTPSQHandshake(format!(
"received unexpected response, got: {got:?}, expected: {expected:?}"
))
}
}
+136 -61
View File
@@ -10,6 +10,7 @@ pub mod noise_protocol;
pub mod packet;
pub mod peer;
pub mod psk;
mod psq;
pub mod replay;
pub mod session;
mod session_integration;
@@ -21,62 +22,156 @@ pub use error::LpError;
pub use message::{ClientHelloData, LpMessage};
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
pub use session::{LpSession, generate_fresh_salt};
pub use session::LpSession;
pub use session_manager::SessionManager;
pub use state_machine::LpStateMachine;
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
pub const NOISE_PSK_INDEX: u8 = 3;
#[cfg(test)]
#[cfg(any(feature = "mock", test))]
pub struct SessionsMock {
pub initiator: LpSession,
pub responder: LpSession,
}
#[cfg(any(feature = "mock", test))]
impl SessionsMock {
pub fn mock_post_handshake(session_id: u32) -> SessionsMock {
use crate::peer::mock_peers;
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
let (init, resp) = mock_peers();
let resp_remote = resp.as_remote();
let init_remote = init.as_remote();
let salt = [42u8; 32];
let session_id_bytes = session_id.to_le_bytes();
// skip KKT by just deriving the kem key locally
let kem_keys = resp.kem_psq.as_ref().unwrap();
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.private_key().as_bytes(),
)
.unwrap();
let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key);
let libcrux_public_key = libcrux_kem::PublicKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.public_key().as_bytes(),
)
.unwrap();
let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key);
// INIT -> RESP: PSQ MSG1
let psq_initiator = crate::psk::psq_initiator_create_message(
init.x25519.private_key(),
&resp_remote.x25519_public,
&encapsulation_key,
init.ed25519.private_key(),
init.ed25519.public_key(),
&salt,
&session_id_bytes,
)
.unwrap();
let psk = psq_initiator.psk;
let psq_payload = psq_initiator.payload;
let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk);
let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
.local_private_key(init.x25519().private_key().as_bytes())
.remote_public_key(resp_remote.x25519_public.as_bytes())
.psk(crate::NOISE_PSK_INDEX, &psk)
.build_initiator()
.unwrap();
let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init);
let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
let psq_responder = crate::psk::psq_responder_process_message(
resp.x25519.private_key(),
&init_remote.x25519_public,
(&decapsulation_key, &encapsulation_key),
&init_remote.ed25519_public,
&psq_payload,
&salt,
&session_id_bytes,
)
.unwrap();
let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
.local_private_key(resp.x25519().private_key().as_bytes())
.remote_public_key(init_remote.x25519_public.as_bytes())
.psk(crate::NOISE_PSK_INDEX, &psk)
.build_responder()
.unwrap();
let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp);
noise_protocol_resp.read_message(&noise_msg1).unwrap();
let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap();
noise_protocol_init.read_message(&noise_msg2).unwrap();
let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
assert!(noise_protocol_init.is_handshake_finished());
noise_protocol_resp.read_message(&noise_msg3).unwrap();
assert!(noise_protocol_resp.is_handshake_finished());
SessionsMock {
initiator: LpSession::new(
session_id,
1,
outer_aead_key.clone(),
init,
resp_remote,
crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret),
noise_protocol_init,
),
responder: LpSession::new(
session_id,
1,
outer_aead_key,
resp,
init_remote,
crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret),
noise_protocol_resp,
),
}
}
// we just need a dummy 'valid' session for simpler tests
pub fn mock_initiator() -> LpSession {
Self::mock_post_handshake(1234).initiator
}
}
#[cfg(any(feature = "mock", test))]
pub fn sessions_for_tests() -> (LpSession, LpSession) {
let (init, resp) = crate::peer::mock_peers();
let sessions = SessionsMock::mock_post_handshake(69);
(sessions.initiator, sessions.responder)
}
// Use a fixed receiver_index for deterministic tests
let receiver_index: u32 = 12345;
// Use consistent salt for deterministic tests
let salt = [1u8; 32];
let initiator_session = LpSession::new(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
packet::version::CURRENT,
)
.expect("Test session creation failed");
let responder_session = LpSession::new(
receiver_index,
false,
resp,
init.as_remote(),
&salt,
packet::version::CURRENT,
)
.expect("Test session creation failed");
(initiator_session, responder_session)
#[cfg(any(feature = "mock", test))]
pub fn mock_session_for_test() -> LpSession {
SessionsMock::mock_initiator()
}
#[cfg(test)]
mod tests {
use crate::message::LpMessage;
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN, version};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use crate::session_manager::SessionManager;
use crate::{LpError, sessions_for_tests};
use crate::{LpError, SessionsMock, mock_session_for_test};
use bytes::BytesMut;
// Import the new standalone functions
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::peer::mock_peers;
#[test]
fn test_replay_protection_integration() {
// Create session
let session = sessions_for_tests().0;
let mut session = mock_session_for_test();
// === Packet 1 (Counter 0 - Should succeed) ===
let packet1 = LpPacket {
@@ -175,40 +270,20 @@ mod tests {
#[test]
fn test_session_manager_integration() {
// Create session manager
let local_manager = SessionManager::new();
let remote_manager = SessionManager::new();
// Generate Ed25519 keypairs for PSQ authentication
let (init, resp) = mock_peers();
let mut local_manager = SessionManager::new();
let mut remote_manager = SessionManager::new();
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 54321;
// Test salt
let salt = [46u8; 32];
let sessions = SessionsMock::mock_post_handshake(receiver_index);
let local_session = sessions.initiator;
let remote_session = sessions.responder;
// Create a session via manager
let _ = local_manager
.create_session_state_machine(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let _ = local_manager.create_session_state_machine(local_session);
let _ = remote_manager.create_session_state_machine(remote_session);
let _ = remote_manager
.create_session_state_machine(
receiver_index,
false,
resp,
init.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// === Packet 1 (Counter 0 - Should succeed) ===
let packet1 = LpPacket {
header: LpHeader {
+141 -2
View File
@@ -1,8 +1,9 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::packet::LpHeader;
use crate::peer::LpRemotePeer;
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError};
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
use bytes::{BufMut, BytesMut};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use nym_crypto::asymmetric::{ed25519, x25519};
@@ -12,7 +13,7 @@ use std::fmt::{self, Display};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Data structure for the ClientHello message
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct ClientHelloData {
/// Client-proposed receiver index for session identification (4 bytes)
/// Auto-generated randomly by the client
@@ -29,6 +30,17 @@ impl ClientHelloData {
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
pub const LEN: usize = 100;
pub fn into_lp_packet(self, protocol_version: u8) -> LpPacket {
LpPacket::new(
LpHeader::new(
BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
0, // counter starts at 0
protocol_version,
),
LpMessage::ClientHello(self),
)
}
fn len(&self) -> usize {
Self::LEN
}
@@ -142,6 +154,8 @@ pub enum MessageType {
SubsessionReady = 0x000C,
/// Subsession abort - race winner tells loser to become responder
SubsessionAbort = 0x000D,
/// General error
Error = 0x00FF,
}
impl MessageType {
@@ -158,6 +172,9 @@ impl MessageType {
pub struct HandshakeData(pub Vec<u8>);
impl HandshakeData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -175,6 +192,11 @@ impl HandshakeData {
pub struct EncryptedDataPayload(pub Vec<u8>);
impl EncryptedDataPayload {
#[allow(dead_code)]
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -193,6 +215,10 @@ impl EncryptedDataPayload {
pub struct KKTRequestData(pub Vec<u8>);
impl KKTRequestData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -211,6 +237,10 @@ impl KKTRequestData {
pub struct KKTResponseData(pub Vec<u8>);
impl KKTResponseData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -224,6 +254,52 @@ impl KKTResponseData {
}
}
/// General human-readable error message
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorPacketData {
pub message: String,
}
impl ErrorPacketData {
pub(crate) fn new(message: impl Into<String>) -> Self {
ErrorPacketData {
message: message.into(),
}
}
fn len(&self) -> usize {
// length-encoding + message
4 + self.message.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_u32_le(self.message.len() as u32);
dst.put_slice(self.message.as_bytes());
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
if bytes.len() < 4 {
return Err(LpError::DeserializationError(format!(
"Too few bytes to deserialise ErrorPacketData. got {}",
bytes.len()
)));
}
let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
if bytes[4..].len() != message_len {
return Err(LpError::DeserializationError(format!(
"Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}",
bytes.len(),
4 + message_len
)));
}
let message = String::from_utf8_lossy(&bytes[4..]).to_string();
Ok(ErrorPacketData { message })
}
}
/// Packet forwarding request with embedded inner LP packet
#[derive(Debug, Clone)]
pub struct ForwardPacketData {
@@ -449,6 +525,62 @@ pub enum LpMessage {
SubsessionReady(SubsessionReadyData),
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
SubsessionAbort,
/// An error has occurred
Error(ErrorPacketData),
}
impl From<HandshakeData> for LpMessage {
fn from(value: HandshakeData) -> Self {
LpMessage::Handshake(value)
}
}
impl From<EncryptedDataPayload> for LpMessage {
fn from(value: EncryptedDataPayload) -> Self {
LpMessage::EncryptedData(value)
}
}
impl From<ClientHelloData> for LpMessage {
fn from(value: ClientHelloData) -> Self {
LpMessage::ClientHello(value)
}
}
impl From<KKTRequestData> for LpMessage {
fn from(value: KKTRequestData) -> Self {
LpMessage::KKTRequest(value)
}
}
impl From<KKTResponseData> for LpMessage {
fn from(value: KKTResponseData) -> Self {
LpMessage::KKTResponse(value)
}
}
impl From<ForwardPacketData> for LpMessage {
fn from(value: ForwardPacketData) -> Self {
LpMessage::ForwardPacket(value)
}
}
impl From<SubsessionKK1Data> for LpMessage {
fn from(value: SubsessionKK1Data) -> Self {
LpMessage::SubsessionKK1(value)
}
}
impl From<SubsessionKK2Data> for LpMessage {
fn from(value: SubsessionKK2Data) -> Self {
LpMessage::SubsessionKK2(value)
}
}
impl From<SubsessionReadyData> for LpMessage {
fn from(value: SubsessionReadyData) -> Self {
LpMessage::SubsessionReady(value)
}
}
impl Display for LpMessage {
@@ -468,6 +600,7 @@ impl Display for LpMessage {
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
LpMessage::Error(_) => write!(f, "Error"),
}
}
}
@@ -489,6 +622,7 @@ impl LpMessage {
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionAbort => &[],
LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?)
}
}
@@ -508,6 +642,7 @@ impl LpMessage {
LpMessage::SubsessionKK2(_) => false, // Always has payload
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
LpMessage::SubsessionAbort => true, // Empty signal
LpMessage::Error(_) => false,
}
}
@@ -527,6 +662,7 @@ impl LpMessage {
LpMessage::SubsessionKK2(payload) => payload.len(),
LpMessage::SubsessionReady(payload) => payload.len(),
LpMessage::SubsessionAbort => 0,
LpMessage::Error(payload) => payload.len(),
}
}
@@ -546,6 +682,7 @@ impl LpMessage {
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
LpMessage::Error(_) => MessageType::Error,
}
}
@@ -565,6 +702,7 @@ impl LpMessage {
LpMessage::SubsessionKK2(data) => data.encode(dst),
LpMessage::SubsessionReady(data) => data.encode(dst),
LpMessage::SubsessionAbort => { /* No content - signal only */ }
LpMessage::Error(data) => data.encode(dst),
}
}
@@ -617,6 +755,7 @@ impl LpMessage {
content.ensure_empty()?;
Ok(LpMessage::SubsessionAbort)
}
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
}
}
}
+47 -40
View File
@@ -82,6 +82,13 @@ pub enum ReadResult {
// --- Implementation ---
impl NoiseProtocol {
pub fn params() -> NoiseParams {
// SAFETY: the hardcoded pattern must be valid
// and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery
#[allow(clippy::unwrap_used)]
crate::NOISE_PATTERN.parse().unwrap()
}
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
///
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
@@ -91,6 +98,46 @@ impl NoiseProtocol {
}
}
fn prepare_handshake_state<'a>(
local_private_key: &'a [u8],
remote_public_key: &'a [u8],
psk: &'a [u8],
) -> snow::Builder<'a> {
let psk_index = crate::NOISE_PSK_INDEX;
let noise_params = NoiseProtocol::params();
snow::Builder::new(noise_params)
.local_private_key(local_private_key)
.remote_public_key(remote_public_key)
.psk(psk_index, psk)
}
/// Builds a new `NoiseProtocol` initiator instance with the provided local private key,
/// remote public key and psk
pub fn build_new_initiator(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<Self, NoiseError> {
let handshake_state =
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
.build_initiator()?;
Ok(Self::new(handshake_state))
}
/// Builds a new `NoiseProtocol` responder instance with the provided local private key,
/// remote public key and psk
pub fn build_new_responder(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<Self, NoiseError> {
let handshake_state =
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
.build_responder()?;
Ok(Self::new(handshake_state))
}
/// Processes a single, complete incoming Noise message frame.
///
/// Assumes the caller handles buffering and framing to provide one full message.
@@ -288,43 +335,3 @@ impl NoiseProtocol {
}
}
}
pub fn create_noise_state(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<NoiseProtocol, NoiseError> {
let pattern_name = crate::NOISE_PATTERN;
let psk_index = crate::NOISE_PSK_INDEX;
let noise_params: NoiseParams = pattern_name.parse().unwrap();
let builder = snow::Builder::new(noise_params.clone());
// Using dummy remote key as it's not needed for state creation itself
// In a real scenario, the key would depend on initiator/responder role
let handshake_state = builder
.local_private_key(local_private_key)
.remote_public_key(remote_public_key) // Use own public as dummy remote
.psk(psk_index, psk)
.build_initiator()?;
Ok(NoiseProtocol::new(handshake_state))
}
pub fn create_noise_state_responder(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<NoiseProtocol, NoiseError> {
let pattern_name = crate::NOISE_PATTERN;
let psk_index = crate::NOISE_PSK_INDEX;
let noise_params: NoiseParams = pattern_name.parse().unwrap();
let builder = snow::Builder::new(noise_params.clone());
// Using dummy remote key as it's not needed for state creation itself
// In a real scenario, the key would depend on initiator/responder role
let handshake_state = builder
.local_private_key(local_private_key)
.remote_public_key(remote_public_key) // Use own public as dummy remote
.psk(psk_index, psk)
.build_responder()?;
Ok(NoiseProtocol::new(handshake_state))
}
+5 -1
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::LpError;
use crate::message::LpMessage;
use crate::message::{LpMessage, MessageType};
use crate::replay::ReceivingKeyCounterValidator;
use bytes::{BufMut, BytesMut};
use nym_lp_common::format_debug_bytes;
@@ -53,6 +53,10 @@ impl LpPacket {
}
}
pub fn typ(&self) -> MessageType {
self.message.typ()
}
/// Compute a hash of the message payload
///
/// This can be used for message integrity verification or deduplication
+34 -5
View File
@@ -1,9 +1,9 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ClientHelloData;
use crate::{ClientHelloData, LpError};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::ciphersuite::{KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use std::collections::HashMap;
use std::sync::Arc;
@@ -52,6 +52,14 @@ impl LpLocalPeer {
&self.x25519
}
/// Returns the reference to the KEM Public key of the peer (if available).
pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> {
self.kem_psq
.as_ref()
.map(|kp| kp.public_key())
.ok_or(LpError::ResponderWithMissingKEMKey)
}
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
#[doc(hidden)]
pub fn as_remote(&self) -> LpRemotePeer {
@@ -137,16 +145,37 @@ impl LpRemotePeer {
self.expected_signing_key_digests = expected_signing_key_digests;
self
}
/// Attempt to retrieve expected KEM key hash of the remote
/// for [`nym_kkt::ciphersuite::KEM`] key type and [`nym_kkt::ciphersuite::HashFunction`]
/// specified by own [`nym_kkt::ciphersuite::Ciphersuite`]
pub(crate) fn expected_kem_key_hash(
&self,
ciphersuite: Ciphersuite,
) -> Result<Vec<u8>, LpError> {
let kem = ciphersuite.kem();
let hash_function = ciphersuite.hash_function();
let digests = self
.expected_kem_key_digests
.get(&kem)
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
digests
.get(&hash_function)
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
.cloned()
}
}
#[cfg(test)]
#[cfg(any(feature = "mock", test))]
pub fn mock_peer() -> LpLocalPeer {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
random_peer(&mut rng)
}
#[cfg(test)]
#[cfg(any(feature = "mock", test))]
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
let x25519 = Arc::new(ed25519.to_x25519());
@@ -159,7 +188,7 @@ pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPe
}
}
#[cfg(test)]
#[cfg(any(feature = "mock", test))]
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
use crate::{LpError, LpPacket};
use bytes::BytesMut;
use nym_lp_transport::traits::LpTransport;
#[cfg(test)]
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
#[cfg(not(test))]
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) fn current_timestamp() -> Result<u64, LpError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| LpError::Internal("System time before UNIX epoch".into()))
.map(|d| d.as_secs())
}
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransportHandshakeExt: LpTransport {
// the outer key is temporary until the algorithm is changed with psqv2
async fn receive_packet(
&mut self,
outer_key: Option<&OuterAeadKey>,
) -> Result<LpPacket, LpError>
where
Self: Unpin,
{
let raw = self.receive_raw_packet().await?;
parse_lp_packet(&raw, outer_key)
}
async fn send_packet(
&mut self,
packet: LpPacket,
outer_key: Option<&OuterAeadKey>,
) -> Result<(), LpError>
where
Self: Unpin,
{
let mut packet_buf = BytesMut::new();
serialize_lp_packet(&packet, &mut packet_buf, outer_key)?;
self.send_serialised_packet(&packet_buf).await?;
Ok(())
}
}
impl<T> LpTransportHandshakeExt for T where T: LpTransport {}
+391
View File
@@ -0,0 +1,391 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::OuterAeadKey;
use crate::message::{HandshakeData, KKTRequestData, MessageType};
use crate::noise_protocol::NoiseProtocol;
use crate::peer::LpRemotePeer;
use crate::psk::psq_initiator_create_message;
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
use crate::session::PqSharedSecret;
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
use nym_kkt::KKT_RESPONSE_AAD;
use nym_kkt::ciphersuite::EncapsulationKey;
use nym_kkt::context::KKTContext;
use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame};
use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response};
use nym_lp_transport::traits::LpTransport;
use rand09::rng;
use tracing::debug;
impl<'a, S> PSQHandshakeState<'a, S>
where
S: LpTransport + Unpin,
{
/// Generate and send client hello to the responder
pub(crate) async fn send_client_hello(&mut self) -> Result<ClientHelloData, LpError> {
let protocol = self.protocol_version()?;
// 1. Generate and send ClientHelloData with fresh salt and both public keys
let timestamp = current_timestamp()?;
let client_hello_data = self.local_peer.build_client_hello_data(timestamp);
self.connection
.send_packet(client_hello_data.into_lp_packet(protocol), None)
.await?;
Ok(client_hello_data)
}
/// Attempt to receive an ack to sent client hello. returns a boolean indicating
/// whether the request has been successful or whether there has been a collision in receiver
/// index requiring a retry
pub(crate) async fn receive_client_hello_ack(&mut self) -> Result<bool, LpError> {
match self.receive_non_error(None).await?.message {
LpMessage::Ack => Ok(true),
LpMessage::Collision => Ok(false),
other => {
// TODO: retry on collision
Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Ack,
))
}
}
}
/// Attempt to send KKT request to begin the handshake
pub(crate) async fn send_kkt_request(
&mut self,
session_id: u32,
remote_peer: &LpRemotePeer,
) -> Result<(KKTContext, KKTSessionSecret), LpError> {
let protocol = self.protocol_version()?;
let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?;
let (session_secret, encrypted_frame) =
encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?;
let lp_message = KKTRequestData::new(encrypted_frame).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection.send_packet(lp_packet, None).await?;
Ok((kkt_context, session_secret))
}
/// Attempt to receive a KKT response to the previously sent request and extract (and validate)
/// the received encapsulation key
pub(crate) async fn receive_kkt_response(
&mut self,
(kkt_context, session_secret): (KKTContext, KKTSessionSecret),
remote_peer: &LpRemotePeer,
) -> Result<EncapsulationKey<'static>, LpError> {
let kkt_response = match self.receive_non_error(None).await?.message {
LpMessage::KKTResponse(response) => response,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::KKTResponse,
));
}
};
debug!("received KKT response");
let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?;
let (response_frame, remote_context) =
decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?;
let encapsulation_key = initiator_ingest_response(
&kkt_context,
&response_frame,
&remote_context,
&remote_peer.ed25519_public,
&expected_kem_key_digest,
)?;
Ok(encapsulation_key)
}
/// Attempt to prepare and send initial PSQ msg1
pub(crate) async fn send_psq_initiator_message(
&mut self,
remote_peer: &LpRemotePeer,
encapsulation_key: &EncapsulationKey<'_>,
salt: &[u8; 32],
session_id_bytes: &[u8; 4],
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> {
let protocol = self.protocol_version()?;
let session_id = u32::from_le_bytes(*session_id_bytes);
let psq_initiator = psq_initiator_create_message(
self.local_peer.x25519.private_key(),
&remote_peer.x25519_public,
encapsulation_key,
self.local_peer.ed25519.private_key(),
self.local_peer.ed25519.public_key(),
salt,
session_id_bytes,
)?;
let psk = psq_initiator.psk;
let psq_payload = psq_initiator.payload;
// TEMP \/
let outer_aead_key = OuterAeadKey::from_psk(&psk);
// TEMP /\
// prepare noise state and msg1
let mut noise_protocol = NoiseProtocol::build_new_initiator(
self.local_peer.x25519().private_key().as_bytes(),
remote_peer.x25519_public.as_bytes(),
&psk,
)?;
// prepare noise msg1
let noise_msg1 = noise_protocol
.get_bytes_to_send()
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??;
let psq_len = psq_payload.len() as u16;
let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len());
combined.extend_from_slice(&psq_len.to_le_bytes());
combined.extend_from_slice(&psq_payload);
combined.extend_from_slice(&noise_msg1);
let lp_message = HandshakeData::new(combined).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection.send_packet(lp_packet, None).await?;
Ok((
outer_aead_key,
noise_protocol,
PqSharedSecret::new(psq_initiator.pq_shared_secret),
))
}
/// Attempt to receive and validate received PSQ msg2
pub(crate) async fn receive_psq_responder_message(
&mut self,
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let psq_msg2 = match self
.connection
.receive_packet(Some(outer_aead_key))
.await?
.message
{
LpMessage::Handshake(response) => response.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Handshake,
));
}
};
// Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg]
if psq_msg2.len() < 2 {
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
}
let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize;
if psq_msg2.len() < 2 + handle_len {
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
}
// Extract and "store" the PSK handle
let _psq_handle_bytes = &psq_msg2[2..2 + handle_len];
let noise_payload = &psq_msg2[2 + handle_len..];
// *sigh* ignore the message
let _noise_msg2 = noise_protocol.read_message(noise_payload)?;
Ok(())
}
/// Attempt to prepare and send final PSQ msg3
pub(crate) async fn send_final_psq_message(
&mut self,
session_id: u32,
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let noise_msg3 = noise_protocol
.get_bytes_to_send()
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??;
let lp_message = HandshakeData::new(noise_msg3).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection
.send_packet(lp_packet, Some(outer_aead_key))
.await?;
if !noise_protocol.is_handshake_finished() {
return Err(LpError::kkt_psq_handshake(
"noise handshake not finished after msg3",
));
}
Ok(())
}
/// Receive final ACK that indicates finalisation of the handshake
pub(crate) async fn receive_final_ack(
&mut self,
outer_aead_key: &OuterAeadKey,
) -> Result<(), LpError> {
match self
.connection
.receive_packet(Some(outer_aead_key))
.await?
.message
{
LpMessage::Ack => Ok(()),
other => Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Ack,
)),
}
}
async fn complete_as_initiator_inner(
&mut self,
) -> Result<LpSession, IntermediateHandshakeFailure>
where
S: LpTransport + Unpin,
{
// 0. retrieve the expected kem key hash. if we don't know it,
// there's no point in even trying to start the handshake
let Some(remote_peer) = self.remote_peer.take() else {
return Err(IntermediateHandshakeFailure::plain(
LpError::kkt_psq_handshake("initiator can't proceed without remote information"),
));
};
// 1. Generate and send ClientHelloData with fresh salt and both public keys
// and keep retrying until we manage to establish a receiver index without collisions
let mut attempt = 0;
let client_hello_data = loop {
attempt += 1;
debug!("sending client hello");
let client_hello = self
.send_client_hello()
.await
.map_err(IntermediateHandshakeFailure::plain)?;
if self
.receive_client_hello_ack()
.await
.map_err(IntermediateHandshakeFailure::plain)?
{
debug!("received client hello ACK");
break client_hello;
}
debug!("received client hello collision");
// TODO: make it configurable
if attempt > 3 {
return Err(IntermediateHandshakeFailure::plain(
LpError::kkt_psq_handshake(
"failed to establish receiver index without collision",
),
));
}
};
let session_id = client_hello_data.receiver_index;
let session_id_bytes = session_id.to_le_bytes();
let salt = client_hello_data.salt;
// 3. prepare and send KKT request
debug!("sending KKT request");
let kkt_data = self
.send_kkt_request(session_id, &remote_peer)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 4. receive and process KKT response
let encapsulation_key = self
.receive_kkt_response(kkt_data, &remote_peer)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
debug!("received KKT response");
// 5. prepare and send PSQ msg1
debug!("sending PSQ msg1");
let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self
.send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 6. receive and process PSQ msg2
debug!("received PSQ msg2");
if let Err(source) = self
.receive_psq_responder_message(&outer_aead_key, &mut noise_protocol)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 7. prepare and send PSQ msg3
debug!("sending PSQ msg3");
if let Err(source) = self
.send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 8. receive final ACK and finalise
debug!("received final ACK");
if let Err(source) = self.receive_final_ack(&outer_aead_key).await {
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
#[allow(clippy::expect_used)]
Ok(LpSession::new(
session_id,
self.protocol_version()
.expect("protocol version is known at this point"),
outer_aead_key,
self.local_peer.clone(),
remote_peer,
pq_shared_secret,
noise_protocol,
))
}
// TODO: missing: receive counter check
pub async fn complete_as_initiator(mut self) -> Result<LpSession, LpError>
where
S: LpTransport + Unpin,
{
match self.complete_as_initiator_inner().await {
Ok(res) => Ok(res),
Err(err) => Err(self.try_send_error_packet(err).await),
}
}
}
+340
View File
@@ -0,0 +1,340 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::OuterAeadKey;
use crate::message::ErrorPacketData;
use crate::packet::LpHeader;
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::psq::helpers::LpTransportHandshakeExt;
use crate::{LpError, LpMessage, LpPacket};
use nym_kkt::ciphersuite::Ciphersuite;
use nym_lp_transport::traits::LpTransport;
use tracing::debug;
mod helpers;
mod initiator;
mod responder;
pub(crate) struct IntermediateHandshakeFailure {
/// Session id established during exchange if we managed to derive it
session_id: Option<u32>,
/// Protocol version established during the exchange
protocol_version: Option<u8>,
/// Outer aead key established during exchange if we managed to derive it
outer_aead_key: Option<OuterAeadKey>,
/// The error source
source: LpError,
}
impl IntermediateHandshakeFailure {
fn plain(source: LpError) -> IntermediateHandshakeFailure {
IntermediateHandshakeFailure {
session_id: None,
protocol_version: None,
outer_aead_key: None,
source,
}
}
}
pub struct PSQHandshakeState<'a, S> {
/// The underlying connection established for the handshake
connection: &'a mut S,
/// Protocol version used for the exchange.
/// either known implicitly through the directory (initiator)
/// or established through client hello (responder)
protocol_version: Option<u8>,
/// Ciphersuite selected for the KKT/PSQ exchange
ciphersuite: Ciphersuite,
/// Representation of a local Lewes Protocol peer
/// encapsulating all the known information and keys.
local_peer: LpLocalPeer,
/// Representation of a remote Lewes Protocol peer
/// encapsulating all the known information and keys.
remote_peer: Option<LpRemotePeer>,
/// Counter for outgoing packets
sending_counter: u64,
}
impl<'a, S> PSQHandshakeState<'a, S>
where
S: LpTransport + Unpin,
{
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
PSQHandshakeState {
connection,
protocol_version: None,
ciphersuite,
local_peer,
remote_peer: None,
sending_counter: 0,
}
}
#[must_use]
pub fn with_protocol_version(mut self, protocol_version: u8) -> Self {
self.protocol_version = Some(protocol_version);
self
}
#[must_use]
pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self {
self.remote_peer = Some(remote_peer);
self
}
fn protocol_version(&self) -> Result<u8, LpError> {
self.protocol_version
.ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version"))
}
/// Generates the next counter value for outgoing packets.
pub fn next_counter(&mut self) -> u64 {
let counter = self.sending_counter;
self.sending_counter += 1;
counter
}
pub fn next_packet(
&mut self,
session_id: u32,
protocol_version: u8,
message: LpMessage,
) -> LpPacket {
let counter = self.next_counter();
let header = LpHeader::new(session_id, counter, protocol_version);
LpPacket::new(header, message)
}
pub(crate) async fn try_send_error_packet(
&mut self,
err: IntermediateHandshakeFailure,
) -> LpError {
// if session_id is not known, we can't send the packet back (with the current design)
let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else {
return err.source;
};
if let Err(err) = self
.send_error_packet(
session_id,
protocol,
err.source.to_string(),
err.outer_aead_key.as_ref(),
)
.await
{
debug!("failed to send back error response: {err}")
}
err.source
}
/// Attempt to send an error packet
pub(crate) async fn send_error_packet(
&mut self,
session_id: u32,
protocol_version: u8,
msg: impl Into<String>,
outer_aead_key: Option<&OuterAeadKey>,
) -> Result<(), LpError> {
let packet = self.next_packet(
session_id,
protocol_version,
LpMessage::Error(ErrorPacketData::new(msg)),
);
self.connection.send_packet(packet, outer_aead_key).await?;
Ok(())
}
/// Attempt to receive a packet from connection, explicitly checking for an error response
/// and returning corresponding message if received
pub(crate) async fn receive_non_error(
&mut self,
outer_aead_key: Option<&OuterAeadKey>,
) -> Result<LpPacket, LpError> {
let packet = self.connection.receive_packet(outer_aead_key).await?;
match &packet.message {
LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!(
"remote error: {}",
error_packet.message
))),
_ => Ok(packet),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peer::mock_peers;
use crate::psq::helpers::LpTransportHandshakeExt;
use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE;
use mock_instant::thread_local::MockClock;
use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme};
use nym_test_utils::mocks::async_read_write::MockIOStream;
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
use std::time::Duration;
use tokio::join;
#[allow(dead_code)]
async fn extract_error(conn: &mut MockIOStream) -> String {
let packet = conn.receive_packet(None).await.unwrap();
match packet.message {
LpMessage::Error(error) => error.message,
_ => panic!("non error packet"),
}
}
#[tokio::test]
async fn e2e_psq_handshake() -> anyhow::Result<()> {
let conn_init = MockIOStream::default();
let conn_resp = conn_init.try_get_remote_handle();
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
// so they'd get 'static lifetime
let conn_init = conn_init.leak();
let conn_resp = conn_resp.leak();
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
let (init, resp) = mock_peers();
let resp_remote = resp.as_remote();
let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init)
.with_protocol_version(1)
.with_remote_peer(resp_remote);
let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp);
let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed();
let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed();
let (session_init, session_resp) = join!(init_fut, resp_fut);
let session_init = session_init???;
let session_resp = session_resp???;
assert_eq!(session_init.id(), session_resp.id());
assert_eq!(
session_init.outer_aead_key().as_bytes(),
session_resp.outer_aead_key().as_bytes()
);
assert_eq!(
session_init.pq_shared_secret().as_bytes(),
session_resp.pq_shared_secret().as_bytes()
);
Ok(())
}
#[tokio::test]
async fn preparing_client_hello_initiator() -> anyhow::Result<()> {
let mut conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
let (init, resp) = mock_peers();
let resp_remote = resp.as_remote();
// as initiator
let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init)
.with_protocol_version(1)
.with_remote_peer(resp_remote);
// you can generate and send (valid) client hello as initiator
let client_hello = handshake_init.send_client_hello().await?;
let LpMessage::ClientHello(received_client_hello) =
conn_resp.receive_packet(None).await?.message
else {
panic!("wrong message type");
};
assert_eq!(client_hello, received_client_hello);
Ok(())
}
// essentially make sure you can't accidentally trigger the handshake as the responder
#[tokio::test]
async fn preparing_client_hello_responder() -> anyhow::Result<()> {
let conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
let (_, resp) = mock_peers();
// as initiator
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
// you can generate and send (valid) client hello as initiator
let sending_res = handshake_resp.send_client_hello().await;
assert!(sending_res.is_err());
Ok(())
}
#[tokio::test]
async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> {
let current_time = Duration::from_secs(10000);
MockClock::set_system_time(current_time);
let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1);
let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1);
let ciphersuite = Ciphersuite::new(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
// TOO OLD
let mut conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let (init, resp) = mock_peers();
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
let client_hello_too_old = init.build_client_hello_data(too_old.as_secs());
conn_init
.send_packet(client_hello_too_old.into_lp_packet(1), None)
.await?;
let err = handshake_resp.receive_client_hello().await.unwrap_err();
assert!(err.to_string().contains("too old"));
// TOO RECENT
let mut conn_init = MockIOStream::default();
let mut conn_resp = conn_init.try_get_remote_handle();
let (init, resp) = mock_peers();
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs());
conn_init
.send_packet(client_hello_too_recent.into_lp_packet(1), None)
.await?;
let err = handshake_resp.receive_client_hello().await.unwrap_err();
assert!(err.to_string().contains("too future"));
Ok(())
}
}
+461
View File
@@ -0,0 +1,461 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::OuterAeadKey;
use crate::message::{HandshakeData, KKTResponseData, MessageType};
use crate::noise_protocol::NoiseProtocol;
use crate::peer::LpRemotePeer;
use crate::psk::psq_responder_process_message;
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
use crate::session::PqSharedSecret;
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
use nym_kkt::KKT_RESPONSE_AAD;
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
use nym_kkt::context::KKTContext;
use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame};
use nym_kkt::frame::KKTSessionId;
use nym_kkt::session::{responder_ingest_message, responder_process};
use nym_lp_transport::traits::LpTransport;
use rand09::rng;
use std::time::Duration;
use tracing::debug;
pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30);
// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder
fn validate_client_hello_timestamp(
client_timestamp: u64,
tolerance: Duration,
) -> Result<(), LpError> {
let now = current_timestamp()?;
let age = now.abs_diff(client_timestamp);
if age > tolerance.as_secs() {
let direction = if now >= client_timestamp {
"old"
} else {
"future"
};
return Err(LpError::kkt_psq_handshake(format!(
"ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)",
tolerance.as_secs()
)));
}
Ok(())
}
impl<'a, S> PSQHandshakeState<'a, S>
where
S: LpTransport + Unpin,
{
pub(crate) fn encapsulated_kem_keys(
&self,
) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> {
let kem_keys = self
.local_peer
.kem_psq
.as_ref()
.ok_or(LpError::ResponderWithMissingKEMKey)?;
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.private_key().as_bytes(),
)
.map_err(|e| {
LpError::KKTError(format!(
"Failed to convert X25519 private key to libcrux PrivateKey: {e:?}",
))
})?;
let dec_key = DecapsulationKey::X25519(libcrux_private_key);
let libcrux_public_key = libcrux_kem::PublicKey::decode(
libcrux_kem::Algorithm::X25519,
kem_keys.public_key().as_bytes(),
)
.map_err(|e| {
LpError::KKTError(format!(
"Failed to convert X25519 public key to libcrux PublicKey: {e:?}",
))
})?;
let enc_key = EncapsulationKey::X25519(libcrux_public_key);
Ok((dec_key, enc_key))
}
/// Attempt to receive and validate ClientHello
pub(crate) async fn receive_client_hello(
&mut self,
) -> Result<(ClientHelloData, LpRemotePeer), LpError> {
let client_hello_packet = self.receive_non_error(None).await?;
let client_hello = match client_hello_packet.message {
LpMessage::ClientHello(client_hello) => client_hello,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::ClientHello,
));
}
};
validate_client_hello_timestamp(
client_hello.extract_timestamp(),
DEFAULT_TIMESTAMP_TOLERANCE,
)?;
// TODO: somehow check for collision
// set version and remote peer information
self.protocol_version = Some(client_hello_packet.header.protocol_version);
let remote_peer = LpRemotePeer::new(
client_hello.client_ed25519_public_key,
client_hello.client_lp_public_key,
);
Ok((client_hello, remote_peer))
}
/// Send client hello ACK
pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
self.connection.send_packet(ack, None).await?;
Ok(())
}
/// Attempt to receive and process a KKT request
pub(crate) async fn receive_kkt_request(
&mut self,
) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> {
let kkt_request = match self.receive_non_error(None).await?.message {
LpMessage::KKTRequest(request) => request.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::KKTRequest,
));
}
};
let (session_secret, request_frame, remote_context) =
decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?;
let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?;
Ok((context, session_secret, request_frame.session_id()))
}
/// Attempt to send KKT response to the previously received request
pub(crate) async fn send_kkt_response(
&mut self,
session_id: u32,
(kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId),
encapsulation_key: &EncapsulationKey<'_>,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let response_frame = responder_process(
&kkt_context,
kkt_session_id,
self.local_peer.ed25519().private_key(),
encapsulation_key,
)?;
let encrypted_frame = encrypt_kkt_frame(
&mut rng(),
&session_secret,
&response_frame,
KKT_RESPONSE_AAD,
)?;
let lp_message = KKTResponseData::new(encrypted_frame).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection.send_packet(lp_packet, None).await?;
Ok(())
}
/// Attempt to receive and process a PSQ msg1 request
pub(crate) async fn receive_psq_initiator_message(
&mut self,
remote_peer: &LpRemotePeer,
local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>),
salt: &[u8; 32],
session_id_bytes: &[u8; 4],
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec<u8>), LpError> {
let psq_msg1 = match self.receive_non_error(None).await?.message {
LpMessage::Handshake(response) => response.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Handshake,
));
}
};
// Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg]
if psq_msg1.len() < 2 {
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
}
let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize;
if psq_msg1.len() < 2 + handle_len {
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
}
let psq_payload = &psq_msg1[2..2 + handle_len];
let noise_payload = &psq_msg1[2 + handle_len..];
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
let psq_responder = psq_responder_process_message(
self.local_peer.x25519.private_key(),
&remote_peer.x25519_public,
local_kem_keypair,
&remote_peer.ed25519_public,
psq_payload,
salt,
session_id_bytes,
)?;
let psk = psq_responder.psk;
let psk_handle = psq_responder.psk_handle;
// TEMP \/
let outer_aead_key = OuterAeadKey::from_psk(&psk);
// TEMP /\
let mut noise_protocol = NoiseProtocol::build_new_responder(
self.local_peer.x25519().private_key().as_bytes(),
remote_peer.x25519_public.as_bytes(),
&psk,
)?;
noise_protocol.read_message(noise_payload)?;
Ok((
outer_aead_key,
noise_protocol,
PqSharedSecret::new(psq_responder.pq_shared_secret),
psk_handle,
))
}
/// Attempt to prepare and generate a responder PSQ msg2
pub(crate) async fn send_psq_responder_message(
&mut self,
session_id: u32,
psk_handle: &[u8],
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let msg2 = noise_protocol
.get_bytes_to_send()
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??;
// Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg]
let handle_len = psk_handle.len() as u16;
let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len());
combined.extend_from_slice(&handle_len.to_le_bytes());
combined.extend_from_slice(psk_handle);
combined.extend_from_slice(&msg2);
let lp_message = HandshakeData::new(combined).into();
let lp_packet = self.next_packet(session_id, protocol, lp_message);
self.connection
.send_packet(lp_packet, Some(outer_aead_key))
.await?;
Ok(())
}
/// Attempt to receive and process final PSQ msg3
pub(crate) async fn receive_final_psq_message(
&mut self,
outer_aead_key: &OuterAeadKey,
noise_protocol: &mut NoiseProtocol,
) -> Result<(), LpError> {
let psq_msg3 = match self
.connection
.receive_packet(Some(outer_aead_key))
.await?
.message
{
LpMessage::Handshake(response) => response.0,
other => {
return Err(LpError::unexpected_handshake_response(
other.typ(),
MessageType::Handshake,
));
}
};
noise_protocol.read_message(&psq_msg3)?;
if !noise_protocol.is_handshake_finished() {
return Err(LpError::kkt_psq_handshake(
"noise handshake not finished after msg3",
));
}
Ok(())
}
/// Send final ACK to indicate finalisation of the handshake
pub(crate) async fn send_final_ack(
&mut self,
session_id: u32,
outer_aead_key: &OuterAeadKey,
) -> Result<(), LpError> {
let protocol = self.protocol_version()?;
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
self.connection
.send_packet(ack, Some(outer_aead_key))
.await?;
Ok(())
}
async fn complete_as_responder_inner(
&mut self,
) -> Result<LpSession, IntermediateHandshakeFailure>
where
S: LpTransport + Unpin,
{
// 1. receive and validate ClientHello
let (client_hello_data, remote_peer) =
self.receive_client_hello()
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: None,
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
debug!("received client hello");
let session_id = client_hello_data.receiver_index;
let session_id_bytes = session_id.to_le_bytes();
let salt = client_hello_data.salt;
// 2. send ack
debug!("sending client hello ACK");
self.send_client_hello_ack(session_id)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 3. receive and process KKT request
let kkt_data =
self.receive_kkt_request()
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
debug!("received KKT request");
// TEMP: 'derive' KEM keys
let (dec_key, enc_key) =
self.encapsulated_kem_keys()
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 4. prepare and send KKT response
debug!("sending KKT response");
self.send_kkt_response(session_id, kkt_data, &enc_key)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 5. receive and process PSQ msg1
debug!("received PSQ msg1");
let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self
.receive_psq_initiator_message(
&remote_peer,
(&dec_key, &enc_key),
&salt,
&session_id_bytes,
)
.await
.map_err(|source| IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: None,
source,
})?;
// 6. prepare and send PSQ msg2
debug!("sending PSQ msg2");
if let Err(source) = self
.send_psq_responder_message(
session_id,
&psk_handle,
&outer_aead_key,
&mut noise_protocol,
)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 7. receive and process PSQ msg3
debug!("received PSQ msg3");
if let Err(source) = self
.receive_final_psq_message(&outer_aead_key, &mut noise_protocol)
.await
{
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
// 8. [optionally] send ACK to finalise
debug!("sending final ACK");
if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await {
return Err(IntermediateHandshakeFailure {
session_id: Some(session_id),
protocol_version: self.protocol_version,
outer_aead_key: Some(outer_aead_key),
source,
});
}
#[allow(clippy::expect_used)]
Ok(LpSession::new(
session_id,
self.protocol_version()
.expect("protocol version is known at this point"),
outer_aead_key,
self.local_peer.clone(),
remote_peer,
pq_shared_secret,
noise_protocol,
))
}
pub async fn complete_as_responder(mut self) -> Result<LpSession, LpError>
where
S: LpTransport + Unpin,
{
match self.complete_as_responder_inner().await {
Ok(res) => Ok(res),
Err(err) => Err(self.try_send_error_packet(err).await),
}
}
}
+165 -1790
View File
File diff suppressed because it is too large Load Diff
+54 -645
View File
@@ -2,7 +2,7 @@
mod tests {
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::{
LpError,
LpError, SessionsMock,
message::LpMessage,
packet::{LpHeader, LpPacket, TRAILER_LEN},
session_manager::SessionManager,
@@ -44,214 +44,21 @@ mod tests {
#[test]
fn test_full_session_flow() {
// 1. Initialize session manager
let session_manager_1 = SessionManager::new();
let session_manager_2 = SessionManager::new();
let mut session_manager_1 = SessionManager::new();
let mut session_manager_2 = SessionManager::new();
// 2. Generate Ed25519 keypairs for PSQ authentication
let (a, b) = mock_peers();
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 100001;
// Test salt
let salt = [42u8; 32];
// 4. Create sessions using the pre-built Noise states
let peer_a_sm = session_manager_1
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session A");
let peer_b_sm = session_manager_2
.create_session_state_machine(
receiver_index,
false,
b.clone(),
a.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session B");
// 2. Create sessions using the pre-built Noise states
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
// Verify session count
assert_eq!(session_manager_1.session_count(), 1);
assert_eq!(session_manager_2.session_count(), 1);
// Initialize KKT state for both sessions (test bypass)
session_manager_1
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
.expect("Failed to init KKT for peer A");
session_manager_2
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
.expect("Failed to init KKT for peer B");
// 5. Simulate Noise Handshake (Sans-IO)
println!("Starting handshake simulation...");
let mut i_msg_payload;
let mut r_msg_payload = None;
let mut rounds = 0;
const MAX_ROUNDS: usize = 10;
// Prime initiator's first message
i_msg_payload = session_manager_1
.prepare_handshake_message(peer_a_sm)
.transpose()
.unwrap();
assert!(
i_msg_payload.is_some(),
"Initiator did not produce initial message"
);
while rounds < MAX_ROUNDS {
rounds += 1;
let mut did_exchange = false;
// === Initiator -> Responder ===
if let Some(payload) = i_msg_payload.take() {
did_exchange = true;
println!(
" Round {}: Initiator -> Responder ({} bytes)",
rounds,
payload.len()
);
// A prepares packet
let counter = session_manager_1.next_counter(receiver_index).unwrap();
let message_a_to_b = create_test_packet(1, receiver_index, counter, payload);
let mut encoded_msg = BytesMut::new();
serialize_lp_packet(&message_a_to_b, &mut encoded_msg, None)
.expect("A serialize failed");
// B parses packet and checks replay
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("B parse failed");
assert_eq!(decoded_packet.header.counter, counter);
// Check replay before processing handshake
session_manager_2
.receiving_counter_quick_check(peer_b_sm, decoded_packet.header.counter)
.expect("B replay check failed (A->B)");
match session_manager_2
.process_handshake_message(peer_b_sm, &decoded_packet.message)
{
Ok(_) => {
// Mark counter only after successful processing
session_manager_2
.receiving_counter_mark(peer_b_sm, decoded_packet.header.counter)
.expect("B mark counter failed");
}
Err(e) => panic!("Responder processing failed: {:?}", e),
}
// Check if responder needs to send a reply
r_msg_payload = session_manager_2
.prepare_handshake_message(peer_b_sm)
.transpose()
.unwrap();
println!("{:?}", r_msg_payload);
}
// Check completion
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
{
println!("Handshake completed after Initiator->Responder message.");
break;
}
// === Responder -> Initiator ===
if let Some(payload) = r_msg_payload.take() {
did_exchange = true;
println!(
" Round {}: Responder -> Initiator ({} bytes)",
rounds,
payload.len()
);
// B prepares packet
let counter = session_manager_2.next_counter(peer_b_sm).unwrap();
let message_b_to_a = create_test_packet(1, receiver_index, counter, payload);
let mut encoded_msg = BytesMut::new();
serialize_lp_packet(&message_b_to_a, &mut encoded_msg, None)
.expect("B serialize failed");
// A parses packet and checks replay
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("A parse failed");
assert_eq!(decoded_packet.header.counter, counter);
// Check replay before processing handshake
session_manager_1
.receiving_counter_quick_check(peer_a_sm, decoded_packet.header.counter)
.expect("A replay check failed (B->A)");
match session_manager_1
.process_handshake_message(peer_a_sm, &decoded_packet.message)
{
Ok(_) => {
// Mark counter only after successful processing
session_manager_1
.receiving_counter_mark(peer_a_sm, decoded_packet.header.counter)
.expect("A mark counter failed");
}
Err(e) => panic!("Initiator processing failed: {:?}", e),
}
// Check if initiator needs to send a reply
i_msg_payload = session_manager_1
.prepare_handshake_message(peer_a_sm)
.transpose()
.unwrap();
}
// println!("Initiator state: {}", session_manager_1.get_state(peer_a_sm).unwrap());
// println!("Responder state: {}", session_manager_2.get_state(peer_b_sm).unwrap());
println!(
"Initiator state: {}",
session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
);
println!(
"Responder state: {}",
session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
);
// Check completion again
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
{
println!("Handshake completed after Responder->Initiator message.");
// Safety break if no messages were exchanged in a round
if !did_exchange {
println!("No messages exchanged in round {}, breaking.", rounds);
break;
}
}
assert!(rounds < MAX_ROUNDS, "Handshake loop exceeded max rounds");
}
assert!(
session_manager_1.is_handshake_complete(peer_a_sm).unwrap(),
"Initiator handshake did not complete"
);
assert!(
session_manager_2.is_handshake_complete(peer_b_sm).unwrap(),
"Responder handshake did not complete"
);
println!(
"Handshake simulation completed successfully in {} rounds.",
rounds
);
// --- Handshake Complete ---
// 7. Simulate Data Transfer (Post-Handshake)
// 3. Simulate Data Transfer (Post-Handshake)
println!("Starting data transfer simulation...");
let plaintext_a_to_b = b"Hello from A!";
@@ -328,7 +135,7 @@ mod tests {
println!("Data transfer simulation completed.");
// 8. Replay Protection Test (Data Packet)
// 4. Replay Protection Test (Data Packet)
println!("Testing data packet replay protection...");
// Try to replay the last message from B to A
// Need to re-encode because decode consumes the buffer
@@ -359,7 +166,7 @@ mod tests {
);
println!("Data packet replay protection test passed.");
// 9. Test out-of-order packet reception (send counter N+1 before counter N)
// 5. Test out-of-order packet reception (send counter N+1 before counter N)
println!("Testing out-of-order data packet reception...");
let counter_a_next = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 1
let counter_a_skip = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 2
@@ -405,7 +212,7 @@ mod tests {
String::from_utf8_lossy(&decrypted_payload)
);
// 10. Now send the skipped counter N message (should still work)
// 6. Now send the skipped counter N message (should still work)
println!("Testing delayed data packet reception...");
// Prepare data for counter_a_next (N)
let plaintext_delayed = b"Delayed message";
@@ -453,7 +260,7 @@ mod tests {
println!("Delayed data packet reception test passed.");
// 11. Try to replay message with counter N (should fail)
// 7. Try to replay message with counter N (should fail)
println!("Testing replay of delayed packet...");
let parsed_delayed_replay =
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
@@ -465,7 +272,7 @@ mod tests {
"Should be a replay protection error"
);
// 12. Session removal
// 8. Session removal
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
@@ -482,94 +289,21 @@ mod tests {
#[test]
fn test_bidirectional_communication() {
// 1. Initialize session manager
let session_manager_1 = SessionManager::new();
let session_manager_2 = SessionManager::new();
let mut session_manager_1 = SessionManager::new();
let mut session_manager_2 = SessionManager::new();
// 2. Generate Ed25519 keypairs for PSQ authentication
let (a, b) = mock_peers();
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
// Use fixed receiver_index for test
let receiver_index: u32 = 100002;
// 2. Create sessions using the pre-built Noise states
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
// Test salt
let salt = [43u8; 32];
// Counters after handshake
let mut counter_a = 0; // Next counter for A to send
let mut counter_b = 0; // Next counter for B to send
let peer_a_sm = session_manager_1
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session A");
let peer_b_sm = session_manager_2
.create_session_state_machine(
receiver_index,
false,
b.clone(),
a.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session B");
// Initialize KKT state for both sessions (test bypass)
session_manager_1
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
.expect("Failed to init KKT for peer A");
session_manager_2
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
.expect("Failed to init KKT for peer B");
// Drive handshake to completion (simplified)
let mut i_msg = session_manager_1
.prepare_handshake_message(peer_a_sm)
.transpose()
.unwrap()
.unwrap();
session_manager_2
.process_handshake_message(peer_b_sm, &i_msg)
.unwrap();
session_manager_2
.receiving_counter_mark(peer_b_sm, 0)
.unwrap(); // Assume counter 0 for first msg
let r_msg = session_manager_2
.prepare_handshake_message(peer_b_sm)
.transpose()
.unwrap()
.unwrap();
session_manager_1
.process_handshake_message(peer_a_sm, &r_msg)
.unwrap();
session_manager_1
.receiving_counter_mark(peer_a_sm, 0)
.unwrap(); // Assume counter 0 for first msg
i_msg = session_manager_1
.prepare_handshake_message(peer_a_sm)
.transpose()
.unwrap()
.unwrap();
session_manager_2
.process_handshake_message(peer_b_sm, &i_msg)
.unwrap();
session_manager_2
.receiving_counter_mark(peer_b_sm, 1)
.unwrap(); // Assume counter 1 for second msg from A
assert!(session_manager_1.is_handshake_complete(peer_a_sm).unwrap());
assert!(session_manager_2.is_handshake_complete(peer_b_sm).unwrap());
println!("Bidirectional test: Handshake complete.");
// Counters after handshake (A sent 2, B sent 1)
let mut counter_a = 2; // Next counter for A to send
let mut counter_b = 1; // Next counter for B to send
// 4. Send multiple encrypted messages both ways
// 3. Send multiple encrypted messages both ways
const NUM_MESSAGES: u64 = 5;
for i in 0..NUM_MESSAGES {
println!("Bidirectional test: Round {}", i);
@@ -634,36 +368,30 @@ mod tests {
// Peer A sent handshake(0), handshake(1) + 5 data packets = 7 total. Next send counter = 7.
// Peer A received handshake(0) + 5 data packets = 6 total. Next expected recv counter = 6.
assert_eq!(
counter_a,
2 + NUM_MESSAGES,
counter_a, NUM_MESSAGES,
"Peer A final send counter mismatch"
);
assert_eq!(
total_recv_a,
1 + NUM_MESSAGES,
total_recv_a, NUM_MESSAGES,
"Peer A total received count mismatch"
); // Received 1 handshake + 5 data
); // Received 5 data
assert_eq!(
next_recv_a,
1 + NUM_MESSAGES,
next_recv_a, NUM_MESSAGES,
"Peer A next expected receive counter mismatch"
); // Expected counter for msg from B
// Peer B sent handshake(0) + 5 data packets = 6 total. Next send counter = 6.
// Peer B received handshake(0), handshake(1) + 5 data packets = 7 total. Next expected recv counter = 7.
assert_eq!(
counter_b,
1 + NUM_MESSAGES,
counter_b, NUM_MESSAGES,
"Peer B final send counter mismatch"
);
assert_eq!(
total_recv_b,
2 + NUM_MESSAGES,
total_recv_b, NUM_MESSAGES,
"Peer B total received count mismatch"
); // Received 2 handshake + 5 data
); // Received 5 data
assert_eq!(
next_recv_b,
2 + NUM_MESSAGES,
next_recv_b, NUM_MESSAGES,
"Peer B next expected receive counter mismatch"
); // Expected counter for msg from A
@@ -674,28 +402,14 @@ mod tests {
#[test]
fn test_session_error_handling() {
// 1. Initialize session manager
let session_manager = SessionManager::new();
let mut session_manager = SessionManager::new();
// Generate Ed25519 keypair for PSQ authentication
let (a, b) = mock_peers();
// Use fixed receiver_index for test
let receiver_index: u32 = 100003;
// Test salt
let salt = [44u8; 32];
let receiver_index = 123;
let session1 = SessionsMock::mock_post_handshake(receiver_index).initiator;
let session2 = SessionsMock::mock_post_handshake(124).initiator;
// 2. Create a session (using real noise state)
let _session = session_manager
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session");
let _session = session_manager.create_session_state_machine(session1);
// 3. Try to get a non-existent session
let result = session_manager.state_machine_exists(999);
@@ -709,20 +423,10 @@ mod tests {
);
// 5. Create and immediately remove a session
let receiver_index_temp: u32 = 100004;
let _temp_session = session_manager
.create_session_state_machine(
receiver_index_temp,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create temp session");
let _temp_session = session_manager.create_session_state_machine(session2);
assert!(
session_manager.remove_state_machine(receiver_index_temp),
session_manager.remove_state_machine(124),
"Should remove the session"
);
@@ -769,15 +473,8 @@ mod tests {
}
// Remove unused imports if SessionManager methods are no longer direct dependencies
// use crate::noise_protocol::{create_noise_state, create_noise_state_responder};
use crate::packet::version;
use crate::peer::mock_peers;
use crate::state_machine::LpData;
use crate::{
// Bring in state machine types
state_machine::{LpAction, LpInput, LpStateBare},
// message::LpMessage, // LpMessage likely still needed for LpInput/LpAction
// packet::{LpHeader, LpPacket, TRAILER_LEN}, // LpPacket needed for LpAction/LpInput
};
use crate::state_machine::{LpAction, LpInput, LpStateBare};
// Use Bytes for SendData input
// Keep helper function for creating test packets if needed,
@@ -794,309 +491,22 @@ mod tests {
#[test]
fn test_full_session_flow_with_process_input() {
// 1. Initialize session managers
let session_manager_1 = SessionManager::new();
let session_manager_2 = SessionManager::new();
let mut session_manager_1 = SessionManager::new();
let mut session_manager_2 = SessionManager::new();
// 2. Generate Ed25519 keypairs for PSQ authentication
let (a, b) = mock_peers();
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
// Use fixed receiver_index for test
let receiver_index: u32 = 100005;
// Test salt
let salt = [45u8; 32];
// 3. Create sessions state machines
assert!(
session_manager_1
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT
) // Initiator
.is_ok()
);
assert!(
session_manager_2
.create_session_state_machine(
receiver_index,
false,
b,
a.as_remote(),
&salt,
version::CURRENT
) // Responder
.is_ok()
);
// 2. Create sessions state machines
session_manager_1.create_session_state_machine(sessions.initiator);
session_manager_2.create_session_state_machine(sessions.responder);
assert_eq!(session_manager_1.session_count(), 1);
assert_eq!(session_manager_2.session_count(), 1);
assert!(session_manager_1.state_machine_exists(receiver_index));
assert!(session_manager_2.state_machine_exists(receiver_index));
// Verify initial states are ReadyToHandshake
assert_eq!(
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::ReadyToHandshake
);
assert_eq!(
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::ReadyToHandshake
);
// --- 4. Simulate Noise Handshake via process_input ---
println!("Starting handshake simulation via process_input...");
let mut packet_a_to_b: Option<LpPacket>;
let mut packet_b_to_a: Option<LpPacket>;
let mut rounds = 0;
const MAX_ROUNDS: usize = 10; // KKT (2 messages) + XK handshake (3 messages) + PSQ = 6 rounds total
// --- Round 1: Initiator Starts ---
println!(" Round {}: Initiator starts handshake", rounds);
let action_a1 = session_manager_1
.process_input(receiver_index, LpInput::StartHandshake)
.expect("Initiator StartHandshake should produce an action")
.expect("Initiator StartHandshake failed");
if let LpAction::SendPacket(packet) = action_a1 {
println!(" Initiator produced SendPacket (KKT request)");
packet_a_to_b = Some(packet);
} else {
panic!("Initiator StartHandshake did not produce SendPacket");
}
// After StartHandshake, initiator should be in KKTExchange state (not Handshaking yet)
assert_eq!(
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::KKTExchange,
"Initiator state wrong after StartHandshake (should be KKTExchange)"
);
// *** ADD THIS BLOCK for Responder StartHandshake ***
println!(
" Round {}: Responder explicitly enters KKTExchange state",
rounds
);
let action_b_start =
session_manager_2.process_input(receiver_index, LpInput::StartHandshake);
// Responder's StartHandshake should not produce an action to send
assert!(
action_b_start.as_ref().unwrap().is_none(),
"Responder StartHandshake should produce None action, got {:?}",
action_b_start
);
// Verify responder transitions to KKTExchange state (not Handshaking yet)
assert_eq!(
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::KKTExchange, // Responder also enters KKTExchange state
"Responder state should be KKTExchange after its StartHandshake"
);
// *** END OF ADDED BLOCK ***
// --- Round 2: Responder Receives KKT Request, Sends KKT Response ---
rounds += 1;
println!(
" Round {}: Responder receives KKT request, sends KKT response",
rounds
);
let packet_to_process = packet_a_to_b
.take()
.expect("KKT request from A was missing");
// Simulate network: serialize -> parse (optional but good practice)
let mut buf_a = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a, None).unwrap();
let parsed_packet_a = parse_lp_packet(&buf_a, None).unwrap();
// Responder processes KKT request
let action_b1 = session_manager_2
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a))
.expect("Responder ReceivePacket should produce an action")
.expect("Responder ReceivePacket failed");
if let LpAction::SendPacket(packet) = action_b1 {
println!(" Responder received KKT request, produced KKT response");
packet_b_to_a = Some(packet);
} else {
panic!("Responder ReceivePacket did not produce SendPacket for KKT response");
}
// Responder transitions to Handshaking after KKT completes
assert_eq!(
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Responder state should be Handshaking after KKT exchange"
);
// --- Round 3: Initiator Receives KKT Response, Sends First Noise Message (with PSQ) ---
rounds += 1;
println!(
" Round {}: Initiator receives KKT response, sends first Noise message (with PSQ)",
rounds
);
let packet_to_process = packet_b_to_a
.take()
.expect("KKT response from B was missing");
// Simulate network
let mut buf_b = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_b, None).unwrap();
let parsed_packet_b = parse_lp_packet(&buf_b, None).unwrap();
// Initiator processes KKT response
let action_a2 = session_manager_1
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b))
.expect("Initiator ReceivePacket should produce an action")
.expect("Initiator ReceivePacket failed");
match action_a2 {
LpAction::SendPacket(packet) => {
println!(
" Initiator received KKT response, produced first Noise message (-> e)"
);
packet_a_to_b = Some(packet);
// Initiator transitions to Handshaking after KKT completes
assert_eq!(
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Initiator state should be Handshaking after receiving KKT response"
);
}
LpAction::KKTComplete => {
println!(
" Initiator received KKT response, produced KKTComplete (will send Noise in next step)"
);
// KKT completed, now need to explicitly trigger handshake message
// This might be the case if KKT completion doesn't automatically send the first Noise message
// Let's try to prepare the handshake message
if let Some(msg_result) =
session_manager_1.prepare_handshake_message(receiver_index)
{
let msg = msg_result.expect("Failed to prepare handshake message after KKT");
// Create a packet from the message
let packet = create_test_packet(1, receiver_index, 0, msg);
packet_a_to_b = Some(packet);
println!(" Prepared first Noise message after KKTComplete");
} else {
panic!("No handshake message available after KKT complete");
}
}
other => {
panic!(
"Initiator ReceivePacket produced unexpected action after KKT response: {:?}",
other
);
}
}
// --- Round 4: Responder Receives First Noise Message, Sends Second ---
rounds += 1;
println!(
" Round {}: Responder receives first Noise message, sends second",
rounds
);
let packet_to_process = packet_a_to_b
.take()
.expect("First Noise packet from A was missing");
// Simulate network
let mut buf_a2 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a2, None).unwrap();
let parsed_packet_a2 = parse_lp_packet(&buf_a2, None).unwrap();
// Responder processes first Noise message and sends second Noise message
let action_b2 = session_manager_2
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a2))
.expect("Responder ReceivePacket should produce an action")
.expect("Responder ReceivePacket failed");
if let LpAction::SendPacket(packet) = action_b2 {
println!(
" Responder received first Noise message, produced second Noise message (<- e, ee, s, es)"
);
packet_b_to_a = Some(packet);
} else {
panic!("Responder did not produce SendPacket for second Noise message");
}
// Responder still in Handshaking, waiting for final message
assert_eq!(
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Responder state should still be Handshaking after sending second message"
);
// --- Round 5: Initiator Receives Second Noise Message, Sends Third, Completes ---
rounds += 1;
println!(
" Round {}: Initiator receives second Noise message, sends third, completes",
rounds
);
let packet_to_process = packet_b_to_a
.take()
.expect("Second Noise packet from B was missing");
let mut buf_b2 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_b2, None).unwrap();
let parsed_packet_b2 = parse_lp_packet(&buf_b2, None).unwrap();
let action_a3 = session_manager_1
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b2))
.expect("Initiator ReceivePacket should produce an action")
.expect("Initiator ReceivePacket failed");
if let LpAction::SendPacket(packet) = action_a3 {
println!(
" Initiator received second Noise message, produced third Noise message (-> s, se)"
);
packet_a_to_b = Some(packet);
} else {
panic!("Initiator did not produce SendPacket for third Noise message");
}
// Initiator transitions to Transport after sending third message
assert_eq!(
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Transport,
"Initiator state should be Transport after sending third message"
);
// --- Round 6: Responder Receives Third Noise Message, Completes ---
rounds += 1;
println!(
" Round {}: Responder receives third Noise message, completes",
rounds
);
let packet_to_process = packet_a_to_b
.take()
.expect("Third Noise packet from A was missing");
let mut buf_a3 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a3, None).unwrap();
let parsed_packet_a3 = parse_lp_packet(&buf_a3, None).unwrap();
let action_b3 = session_manager_2
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a3))
.expect("Responder final ReceivePacket should produce an action")
.expect("Responder final ReceivePacket failed");
// Responder completes handshake
if let LpAction::HandshakeComplete = action_b3 {
println!(" Responder received third Noise message, produced HandshakeComplete");
} else {
println!(
" Responder received third Noise message (Action: {:?})",
action_b3
);
}
assert_eq!(
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Transport,
"Responder state should be Transport after processing third message"
);
// --- Verification ---
assert!(rounds < MAX_ROUNDS, "Handshake took too many rounds");
// Verify initial states are Transport
assert_eq!(
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Transport
@@ -1105,9 +515,8 @@ mod tests {
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Transport
);
println!("Handshake simulation completed successfully via process_input.");
// --- 5. Simulate Data Transfer via process_input ---
// --- 3. Simulate Data Transfer via process_input ---
println!("Starting data transfer simulation via process_input...");
let plaintext_a_to_b = LpData::new_opaque(b"Hello from A via process_input!".to_vec());
let plaintext_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec());
@@ -1185,7 +594,7 @@ mod tests {
}
println!("Data transfer simulation completed.");
// --- 6. Replay Protection Test ---
// --- 4. Replay Protection Test ---
println!("Testing data packet replay protection via process_input...");
let replay_result = session_manager_1
.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
@@ -1199,7 +608,7 @@ mod tests {
);
println!("Data packet replay protection test passed.");
// --- 7. Out-of-Order Test ---
// --- 5. Out-of-Order Test ---
println!("Testing out-of-order reception via process_input...");
// A prepares N+1 then N
@@ -1258,7 +667,7 @@ mod tests {
);
println!("Out-of-order test passed.");
// --- 8. Close Test ---
// --- 6. Close Test ---
println!("Testing close via process_input...");
// A closes
@@ -1306,7 +715,7 @@ mod tests {
));
println!("Close test passed.");
// --- 9. Session Removal ---
// --- 7. Session Removal ---
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
assert!(!session_manager_1.state_machine_exists(receiver_index));
+50 -191
View File
@@ -6,11 +6,9 @@
//! This module implements session lifecycle management functionality, handling
//! creation, retrieval, and storage of sessions.
use crate::noise_protocol::ReadResult;
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
use crate::state_machine::{LpAction, LpInput, LpStateBare};
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
use dashmap::DashMap;
use std::collections::HashMap;
/// Manages the lifecycle of Lewes Protocol sessions.
///
@@ -18,7 +16,7 @@ use dashmap::DashMap;
/// ensuring proper thread-safety for concurrent access.
pub struct SessionManager {
/// Manages state machines directly, keyed by lp_id
state_machines: DashMap<u32, LpStateMachine>,
state_machines: HashMap<u32, LpStateMachine>,
}
impl Default for SessionManager {
@@ -31,36 +29,18 @@ impl SessionManager {
/// Creates a new session manager with empty session storage.
pub fn new() -> Self {
Self {
state_machines: DashMap::new(),
state_machines: HashMap::new(),
}
}
pub fn process_input(&self, lp_id: u32, input: LpInput) -> Result<Option<LpAction>, LpError> {
pub fn process_input(
&mut self,
lp_id: u32,
input: LpInput,
) -> Result<Option<LpAction>, LpError> {
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
}
pub fn add(&self, session: LpSession) -> Result<(), LpError> {
let sm = LpStateMachine {
state: LpState::ReadyToHandshake {
session: Box::new(session),
},
};
self.state_machines.insert(sm.id()?, sm);
Ok(())
}
pub fn handshaking(&self, lp_id: u32) -> Result<bool, LpError> {
Ok(self.get_state(lp_id)? == LpStateBare::Handshaking)
}
pub fn should_initiate_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
Ok(self.ready_to_handshake(lp_id)? || self.closed(lp_id)?)
}
pub fn ready_to_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
Ok(self.get_state(lp_id)? == LpStateBare::ReadyToHandshake)
}
pub fn closed(&self, lp_id: u32) -> Result<bool, LpError> {
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
}
@@ -84,38 +64,27 @@ impl SessionManager {
})?
}
pub fn receiving_counter_mark(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
self.with_state_machine(lp_id, |sm| sm.session()?.receiving_counter_mark(counter))?
pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?.receiving_counter_mark(counter)
})?
}
pub fn start_handshake(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
self.prepare_handshake_message(lp_id)
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))?
}
pub fn prepare_handshake_message(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_handshake_message())
.ok()?
}
pub fn is_handshake_complete(&self, lp_id: u32) -> Result<bool, LpError> {
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.is_handshake_complete()))?
}
pub fn next_counter(&self, lp_id: u32) -> Result<u64, LpError> {
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.next_counter()))?
}
pub fn decrypt_data(&self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
self.with_state_machine(lp_id, |sm| {
sm.session()?
pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?
.decrypt_data(message)
.map_err(LpError::NoiseError)
})?
}
pub fn encrypt_data(&self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
self.with_state_machine(lp_id, |sm| {
sm.session()?
pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?
.encrypt_data(message)
.map_err(LpError::NoiseError)
})?
@@ -125,14 +94,6 @@ impl SessionManager {
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
}
pub fn process_handshake_message(
&self,
lp_id: u32,
message: &LpMessage,
) -> Result<ReadResult, LpError> {
self.with_state_machine(lp_id, |sm| sm.session()?.process_handshake_message(message))?
}
pub fn session_count(&self) -> usize {
self.state_machines.len()
}
@@ -146,7 +107,7 @@ impl SessionManager {
F: FnOnce(&LpStateMachine) -> R,
{
if let Some(sm) = self.state_machines.get(&lp_id) {
Ok(f(&sm))
Ok(f(sm))
} else {
Err(LpError::StateMachineNotFound { lp_id })
}
@@ -154,90 +115,48 @@ impl SessionManager {
}
// For mutable access (like running process_input)
pub fn with_state_machine_mut<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: u32, f: F) -> Result<R, LpError>
where
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
{
if let Some(mut sm) = self.state_machines.get_mut(&lp_id) {
Ok(f(&mut sm))
if let Some(sm) = self.state_machines.get_mut(&lp_id) {
Ok(f(sm))
} else {
Err(LpError::StateMachineNotFound { lp_id })
}
}
pub fn create_session_state_machine(
&self,
receiver_index: u32,
is_initiator: bool,
local_peer: LpLocalPeer,
remote_peer: LpRemotePeer,
salt: &[u8; 32],
protocol_version: u8,
) -> Result<u32, LpError> {
let sm = LpStateMachine::new(
receiver_index,
is_initiator,
local_peer,
remote_peer,
salt,
protocol_version,
)?;
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 {
let receiver_index = lp_session.id();
let sm = LpStateMachine::new(lp_session);
self.state_machines.insert(receiver_index, sm);
Ok(receiver_index)
receiver_index
}
/// Method to remove a state machine
pub fn remove_state_machine(&self, lp_id: u32) -> bool {
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
let removed = self.state_machines.remove(&lp_id);
removed.is_some()
}
/// Test-only method to initialize KKT state to Completed for a session.
/// This allows integration tests to bypass KKT exchange and directly test PSQ/handshake.
#[cfg(test)]
pub fn init_kkt_for_test(
&self,
lp_id: u32,
remote_x25519_pub: &nym_crypto::asymmetric::x25519::PublicKey,
) -> Result<(), LpError> {
self.with_state_machine(lp_id, |sm| {
sm.session()?.set_kkt_completed_for_test(remote_x25519_pub);
Ok(())
})?
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packet::version;
use crate::peer::{mock_peers, random_peer};
use nym_test_utils::helpers::deterministic_rng;
use crate::{SessionsMock, mock_session_for_test};
#[test]
fn test_session_manager_get() {
let manager = SessionManager::new();
let mut rng = deterministic_rng();
let local = random_peer(&mut rng);
let peer1 = random_peer(&mut rng);
let mut manager = SessionManager::new();
let salt = [47u8; 32];
let receiver_index: u32 = 1001;
let local_session = mock_session_for_test();
let id = local_session.id();
let sm_1_id = manager
.create_session_state_machine(
receiver_index,
true,
local,
peer1.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let sm_1_id = manager.create_session_state_machine(local_session);
assert_eq!(sm_1_id, id);
let retrieved = manager.state_machine_exists(sm_1_id);
let retrieved = manager.state_machine_exists(id);
assert!(retrieved);
let not_found = manager.state_machine_exists(99);
@@ -246,24 +165,10 @@ mod tests {
#[test]
fn test_session_manager_remove() {
let manager = SessionManager::new();
let mut rng = deterministic_rng();
let local = random_peer(&mut rng);
let peer1 = random_peer(&mut rng);
let mut manager = SessionManager::new();
let local_session = mock_session_for_test();
let salt = [48u8; 32];
let receiver_index: u32 = 2002;
let sm_1_id = manager
.create_session_state_machine(
receiver_index,
true,
local,
peer1.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let sm_1_id = manager.create_session_state_machine(local_session);
let removed = manager.remove_state_machine(sm_1_id);
assert!(removed);
@@ -275,47 +180,14 @@ mod tests {
#[test]
fn test_multiple_sessions() {
let manager = SessionManager::new();
let mut rng = deterministic_rng();
let local = random_peer(&mut rng);
let peer1 = random_peer(&mut rng);
let peer2 = random_peer(&mut rng);
let peer3 = random_peer(&mut rng);
let mut manager = SessionManager::new();
let session1 = SessionsMock::mock_post_handshake(123).initiator;
let session2 = SessionsMock::mock_post_handshake(124).initiator;
let session3 = SessionsMock::mock_post_handshake(125).initiator;
let salt = [49u8; 32];
let sm_1 = manager
.create_session_state_machine(
3001,
true,
local.clone(),
peer1.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let sm_2 = manager
.create_session_state_machine(
3002,
true,
local.clone(),
peer2.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let sm_3 = manager
.create_session_state_machine(
3003,
true,
local.clone(),
peer3.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let sm_1 = manager.create_session_state_machine(session1);
let sm_2 = manager.create_session_state_machine(session2);
let sm_3 = manager.create_session_state_machine(session3);
assert_eq!(manager.session_count(), 3);
@@ -330,24 +202,11 @@ mod tests {
#[test]
fn test_session_manager_create_session() {
let manager = SessionManager::new();
let (init, resp) = mock_peers();
let mut manager = SessionManager::new();
let salt = [50u8; 32];
let receiver_index: u32 = 4004;
let sm = manager.create_session_state_machine(
receiver_index,
true,
init,
resp.as_remote(),
&salt,
version::CURRENT,
);
assert!(sm.is_ok());
let sm = sm.unwrap();
let sesion = mock_session_for_test();
let sm = manager.create_session_state_machine(sesion);
assert_eq!(manager.session_count(), 1);
let retrieved = manager.get_state_machine_id(sm);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -18,7 +18,7 @@ rand_chacha = { workspace = true }
tokio = { workspace = true, features = ["sync", "time", "rt"] }
tracing = { workspace = true }
nym-bin-common = { workspace = true, features = ["tracing"] }
nym-bin-common = { workspace = true, features = ["basic_tracing"] }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
+1
View File
@@ -95,6 +95,7 @@ nym-gateway-storage = { workspace = true, features = ["mock"] }
nym-wireguard = { workspace = true, features = ["mock"] }
mock_instant = { workspace = true }
time = { workspace = true }
nym-lp = { path = "../common/nym-lp", features = ["mock"] }
[lints]
workspace = true
+3 -6
View File
@@ -155,14 +155,11 @@ impl LpDataHandler {
.value()
.state
.session()
.map_err(|e| GatewayError::LpProtocolError(format!("Session error: {}", e)))?
.outer_aead_key()
.ok_or_else(|| {
GatewayError::LpProtocolError("Session has no outer AEAD key".to_string())
})?;
.map_err(|e| GatewayError::LpProtocolError(format!("Session error: {e}")))?
.outer_aead_key();
// Parse full packet with outer AEAD decryption
let lp_packet = nym_lp::codec::parse_lp_packet(packet, Some(&outer_key)).map_err(|e| {
let lp_packet = nym_lp::codec::parse_lp_packet(packet, Some(outer_key)).map_err(|e| {
inc!("lp_data_decrypt_errors");
GatewayError::LpProtocolError(format!("Failed to decrypt LP packet: {}", e))
})?;
File diff suppressed because it is too large Load Diff
+5 -38
View File
@@ -362,15 +362,6 @@ pub struct LpHandlerState {
/// from LP clients into the mixnet for routing.
pub outbound_mix_sender: MixForwardingSender,
/// In-progress handshakes keyed by session_id
///
/// Session ID is deterministically computed from both parties' X25519 keys immediately
/// after ClientHello. Used during handshake phase. After handshake completes,
/// state moves to session_states map.
///
/// Wrapped in TimestampedState for TTL-based cleanup of stale handshakes.
pub handshake_states: Arc<DashMap<ReceiverIndex, TimestampedState<LpStateMachine>>>,
/// Established sessions keyed by session_id
///
/// Used after handshake completes (session_id is deterministically computed from
@@ -504,7 +495,7 @@ impl LpListener {
handler::LpConnectionHandler::new(stream, remote_addr, self.handler_state.clone());
let metrics = self.handler_state.metrics.clone();
self.shutdown.try_spawn_named(
self.shutdown.try_spawn_named_with_shutdown(
async move {
let result = handler.handle().await;
@@ -559,7 +550,6 @@ impl LpListener {
///
/// The task automatically stops when the shutdown signal is received.
fn spawn_state_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
let handshake_states = Arc::clone(&self.handler_state.handshake_states);
let session_states = Arc::clone(&self.handler_state.session_states);
let dbg_cfg = self.handler_state.lp_config.debug;
@@ -576,13 +566,7 @@ impl LpListener {
);
self.shutdown.try_spawn_named(
cleanup_task::cleanup_loop(
handshake_states,
session_states,
dbg_cfg,
shutdown,
metrics,
),
cleanup_task::cleanup_loop(session_states, dbg_cfg, shutdown, metrics),
"LP::StateCleanup",
)
}
@@ -606,29 +590,16 @@ pub(crate) mod cleanup_task {
use tracing::{debug, info};
async fn perform_cleanup(
handshake_states: &Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
session_states: &Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
cfg: LpDebug,
) {
let handshake_ttl = cfg.handshake_ttl;
let session_ttl = cfg.session_ttl;
let demoted_session_ttl = cfg.demoted_session_ttl;
let start = std::time::Instant::now();
let mut hs_removed = 0u64;
let mut ss_removed = 0u64;
let mut demoted_removed = 0u64;
// Remove stale handshakes (based on age since creation)
handshake_states.retain(|_, timestamped| {
if timestamped.age() > handshake_ttl {
hs_removed += 1;
false
} else {
true
}
});
// Remove stale sessions (based on time since last activity)
// Use shorter TTL for demoted (ReadOnlyTransport) sessions
session_states.retain(|_, timestamped| {
@@ -651,17 +622,14 @@ pub(crate) mod cleanup_task {
}
});
if hs_removed > 0 || ss_removed > 0 || demoted_removed > 0 {
if ss_removed > 0 || demoted_removed > 0 {
let duration = start.elapsed();
info!(
"LP state cleanup: removed {hs_removed} handshakes, {ss_removed} sessions, {demoted_removed} demoted (took {:.3}s)",
"LP state cleanup: {ss_removed} sessions, {demoted_removed} demoted (took {:.3}s)",
duration.as_secs_f64()
);
// Track metrics
if hs_removed > 0 {
inc_by!("lp_states_cleanup_handshake_removed", hs_removed as i64);
}
if ss_removed > 0 {
inc_by!("lp_states_cleanup_session_removed", ss_removed as i64);
}
@@ -679,7 +647,6 @@ pub(crate) mod cleanup_task {
/// Demoted sessions (ReadOnlyTransport) use shorter TTL since they
/// only need to drain in-flight packets after subsession promotion.
pub(crate) async fn cleanup_loop(
handshake_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
session_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
cfg: LpDebug,
shutdown: nym_task::ShutdownToken,
@@ -697,7 +664,7 @@ pub(crate) mod cleanup_task {
break;
}
_ = cleanup_interval.tick() => {
perform_cleanup(&handshake_states, &session_states, cfg).await;
perform_cleanup(&session_states, cfg).await;
}
}
}
-1
View File
@@ -362,7 +362,6 @@ impl GatewayTasksBuilder {
peer_registrator,
lp_config: self.config.lp,
outbound_mix_sender: self.mix_packet_sender.clone(),
handshake_states: Arc::new(dashmap::DashMap::new()),
session_states: Arc::new(dashmap::DashMap::new()),
forward_semaphore: Arc::new(Semaphore::new(
self.config.lp.debug.max_concurrent_forwards,
-3
View File
@@ -237,9 +237,6 @@ mod tests {
// TODO: might be needed later on for mixnet registration
outbound_mix_sender: mix_sender,
// we start with empty state
handshake_states: Arc::new(Default::default()),
// we start with empty state
session_states: Arc::new(Default::default()),
+85 -349
View File
@@ -8,19 +8,18 @@ use super::error::{LpClientError, Result};
use crate::lp_client::helpers::{
LpDataDeliverExt, LpDataSendExt, convert_forward_data, try_convert_forward_response,
};
use crate::lp_client::state_machine_helpers::{
extract_forwarded_response, get_recv_key, get_send_key, prepare_send_packet,
};
use crate::lp_client::nested_session::connection::NestedConnection;
use crate::lp_client::state_machine_helpers::{extract_forwarded_response, prepare_send_packet};
use bytes::BytesMut;
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_lp::LpPacket;
use nym_lp::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
use nym_lp::message::ForwardPacketData;
use nym_lp::packet::version;
use nym_lp::peer::{LpLocalPeer, LpRemotePeer};
use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine};
use nym_lp::{LpPacket, LpSession};
use nym_lp_transport::traits::LpTransport;
use nym_registration_common::dvpn::LpDvpnRegistrationResponseMessageContent;
use nym_registration_common::{
@@ -31,7 +30,7 @@ use nym_wireguard_types::PeerPublicKey;
use rand::{CryptoRng, RngCore};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::time::Duration;
use tokio::net::TcpStream;
use tracing::warn;
@@ -67,7 +66,7 @@ pub struct LpRegistrationClient<S = TcpStream> {
state_machine: Option<LpStateMachine>,
/// Configuration for timeouts and TCP parameters.
config: LpRegistrationConfig,
pub(crate) config: LpRegistrationConfig,
/// Persistent TCP stream for the connection.
/// Opened on first use, closed after registration.
@@ -119,6 +118,19 @@ where
}
}
/// Attempt to use this `LpRegistrationClient` as transport for `NestedSession`
pub fn as_nested_connection(
&mut self,
exit_identity: ed25519::PublicKey,
exit_address: SocketAddr,
) -> NestedConnection<'_, S> {
NestedConnection {
exit_identity,
exit_address,
outer_client: self,
}
}
/// Creates a new LP registration client with default configuration.
///
/// # Arguments
@@ -145,15 +157,7 @@ where
)
}
fn state_machine(&self) -> Result<&LpStateMachine> {
self.state_machine.as_ref().ok_or_else(|| {
LpClientError::transport(
"State machine not available - has the handshake been completed?",
)
})
}
fn state_machine_mut(&mut self) -> Result<&mut LpStateMachine> {
pub(crate) fn state_machine_mut(&mut self) -> Result<&mut LpStateMachine> {
self.state_machine.as_mut().ok_or_else(|| {
LpClientError::transport(
"State machine not available - has the handshake been completed?",
@@ -169,11 +173,7 @@ where
/// Returns whether the client has completed the handshake and is ready for registration.
pub fn is_handshake_complete(&self) -> bool {
self.state_machine
.as_ref()
.and_then(|sm| sm.session().ok())
.map(|s| s.is_handshake_complete())
.unwrap_or(false)
self.state_machine.is_some()
}
/// Returns the gateway LP address this client is configured for.
@@ -287,27 +287,20 @@ where
///
/// # Errors
/// Returns an error if not connected or if send fails.
async fn try_send_packet(&mut self, packet: &LpPacket) -> Result<()> {
let state_machine = self.state_machine()?;
let send_key = get_send_key(state_machine);
self.try_send_packet_with_key(packet, send_key.as_ref())
.await
}
pub(crate) async fn try_send_packet(&mut self, packet: &LpPacket) -> Result<()> {
// can't use getters due to borrow checker (i.e. requiring full borrows for function calls)
let stream = self
.stream
.as_mut()
.ok_or_else(|| LpClientError::transport("Cannot send: not connected"))?;
/// Sends an LP packet (and optionally encrypted) on the persistent stream.
///
/// # Arguments
/// * `packet` - The LP packet to send
/// * `outer_key` - Optional outer AEAD key for encryption
///
/// # Errors
/// Returns an error if not connected or if send fails.
async fn try_send_packet_with_key(
&mut self,
packet: &LpPacket,
outer_key: Option<&OuterAeadKey>,
) -> Result<()> {
let stream = self.stream_mut()?;
let state_machine = self.state_machine.as_ref().ok_or_else(|| {
LpClientError::transport(
"State machine not available - has the handshake been completed?",
)
})?;
let outer_key = state_machine.session()?.outer_aead_key();
Self::send_packet_with_key(stream, packet, outer_key).await
}
@@ -315,25 +308,19 @@ where
///
/// # Errors
/// Returns an error if not connected or if receive fails.
async fn try_receive_packet(&mut self) -> Result<LpPacket> {
let state_machine = self.state_machine()?;
let recv_key = get_recv_key(state_machine);
pub(crate) async fn try_receive_packet(&mut self) -> Result<LpPacket> {
let stream = self
.stream
.as_mut()
.ok_or_else(|| LpClientError::transport("Cannot send: not connected"))?;
self.try_receive_packet_with_key(recv_key.as_ref()).await
}
let state_machine = self.state_machine.as_ref().ok_or_else(|| {
LpClientError::transport(
"State machine not available - has the handshake been completed?",
)
})?;
/// Receives an LP packet from the persistent stream.
///
/// # Arguments
/// * `outer_key` - Optional outer AEAD key for decryption
///
/// # Errors
/// Returns an error if not connected or if receive fails.
async fn try_receive_packet_with_key(
&mut self,
outer_key: Option<&OuterAeadKey>,
) -> Result<LpPacket> {
let stream = self.stream_mut()?;
let outer_key = state_machine.session()?.outer_aead_key();
Self::receive_packet_with_key(stream, outer_key).await
}
@@ -420,256 +407,28 @@ where
// Ensure we have a TCP connection
self.ensure_connected().await?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| LpClientError::Other("System time before UNIX epoch".into()))?
.as_secs();
let local_peer = self.lp_local_peer.clone();
let remote_peer = self.gateway_lp_peer.clone();
let protocol_version = self.gateway_supported_lp_protocol_version;
let connection = self.stream_mut()?;
// Step 1: Generate ClientHelloData with fresh salt and both public keys
let client_hello_data = self.lp_local_peer.build_client_hello_data(timestamp);
let salt = client_hello_data.salt;
let receiver_index = client_hello_data.receiver_index;
tracing::trace!(
"Generated ClientHello with timestamp: {}, receiver_index: {}",
client_hello_data.extract_timestamp(),
receiver_index
);
// Step 2: Send ClientHello and receive Ack (persistent connection)
let client_hello_header = nym_lp::packet::LpHeader::new(
nym_lp::BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
0, // counter starts at 0
self.gateway_supported_lp_protocol_version,
);
let client_hello_packet = nym_lp::LpPacket::new(
client_hello_header,
nym_lp::LpMessage::ClientHello(client_hello_data),
);
// Send ClientHello (no outer key - before PSK)
self.try_send_packet_with_key(&client_hello_packet, None)
.await?;
// Receive Ack (no outer key - before PSK)
let ack_response = self.try_receive_packet_with_key(None).await?;
// Verify we received Ack
// this confirms that gateway is fine with our suggested protocol version
// in the future we probably have some fancier negotiation
match ack_response.message() {
nym_lp::LpMessage::Ack => {
tracing::debug!("Received Ack for ClientHello");
}
other => {
return Err(LpClientError::Transport(format!(
"Expected Ack for ClientHello, got: {:?}",
other
)));
}
}
// Step 3: Create state machine as initiator with Ed25519 keys
// PSK derivation happens internally in the state machine constructor
let mut state_machine = LpStateMachine::new(
receiver_index,
true, // is_initiator
self.lp_local_peer.clone(),
self.gateway_lp_peer.clone(),
&salt,
self.gateway_supported_lp_protocol_version,
)?;
// Step 4: Start handshake - get first packet to send (KKT request)
let mut pending_packet: Option<nym_lp::LpPacket> = None;
if let Some(action) = state_machine.process_input(LpInput::StartHandshake) {
match action? {
LpAction::SendPacket(packet) => {
pending_packet = Some(packet);
}
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action at handshake start: {:?}",
other
)));
}
}
}
// Step 5: Handshake loop - all packets on persistent connection
loop {
// Send pending packet if we have one
if let Some(packet) = pending_packet.take() {
// Get outer keys from session:
// - send_key: outer_aead_key_for_sending() returns None until PSQ complete
// - recv_key: outer_aead_key() returns key as soon as PSK is derived
let send_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key_for_sending());
let recv_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key());
tracing::trace!(
"Sending handshake packet (send_key={}, recv_key={})",
send_key.is_some(),
recv_key.is_some()
);
self.try_send_packet_with_key(&packet, send_key.as_ref())
.await?;
let response = self.try_receive_packet_with_key(recv_key.as_ref()).await?;
tracing::trace!("Received handshake response");
// Process the received packet
if let Some(action) = state_machine.process_input(LpInput::ReceivePacket(response))
{
match action? {
LpAction::SendPacket(response_packet) => {
// Queue the response packet to send on next iteration
pending_packet = Some(response_packet);
// Check if handshake completed after queueing this packet
if state_machine.session()?.is_handshake_complete() {
// Send the final packet before breaking
if let Some(final_packet) = pending_packet.take() {
let send_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key_for_sending());
let recv_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key());
tracing::trace!("Sending final handshake packet");
self.try_send_packet_with_key(&final_packet, send_key.as_ref())
.await?;
let ack_response =
self.try_receive_packet_with_key(recv_key.as_ref()).await?;
// Validate Ack response
match ack_response.message() {
nym_lp::LpMessage::Ack => {
tracing::debug!(
"Received Ack for final handshake packet"
);
}
other => {
return Err(LpClientError::Transport(format!(
"Expected Ack for final handshake packet, got: {:?}",
other
)));
}
}
}
tracing::info!("LP handshake completed after sending final packet");
break;
}
}
LpAction::HandshakeComplete => {
tracing::info!("LP handshake completed successfully");
break;
}
LpAction::KKTComplete => {
tracing::info!("KKT exchange completed, starting Noise handshake");
// After KKT completes, initiator must send first Noise handshake message
let noise_msg = state_machine
.session()?
.prepare_handshake_message()
.ok_or_else(|| {
LpClientError::transport("No handshake message available after KKT")
})??;
let noise_packet = state_machine.session()?.next_packet(noise_msg)?;
pending_packet = Some(noise_packet);
}
other => {
tracing::trace!("Received action during handshake: {:?}", other);
}
}
}
} else {
// No pending packet and not complete - something is wrong
return Err(LpClientError::transport(
"Handshake stalled: no packet to send",
));
}
}
// TODO:
let ciphersuite = LpSession::default_ciphersuite();
let session = LpSession::complete_as_initiator(
connection,
ciphersuite,
local_peer,
remote_peer,
protocol_version,
)
.complete_as_initiator()
.await?;
// Store the state machine (with established session) for later use
self.state_machine = Some(state_machine);
self.state_machine = Some(LpStateMachine::new(session));
Ok(())
}
/// Opens a TCP connection, sends one packet, receives one response, closes.
///
/// This implements the packet-per-connection model where each LP packet
/// exchange uses its own TCP connection. The connection is closed when
/// this method returns (stream dropped).
///
/// # Arguments
/// * `address` - Gateway LP listener address
/// * `packet` - The LP packet to send
/// * `outer_key` - Optional outer AEAD key (None before PSK, Some after)
/// * `config` - Configuration for timeouts and TCP parameters
///
/// # Errors
/// Returns an error if connection, send, or receive fails.
///
/// # Outer AEAD Keys
///
/// Send and receive use separate keys because during the PSQ handshake:
/// - Initiator derives PSK when preparing msg 1, but must send it cleartext
/// (responder hasn't derived PSK yet)
/// - Responder sends msg 2 encrypted (both have PSK now)
/// - Initiator can decrypt msg 2 (has had PSK since preparing msg 1)
///
/// Use `outer_aead_key_for_sending()` for `send_key` (gates on PSQ completion)
/// and `outer_aead_key()` for `recv_key` (available as soon as PSK derived).
///
/// # Note
/// This method is kept for reference but is no longer used. The persistent
/// connection model uses `send_packet()` and `receive_packet()` instead.
#[allow(dead_code)]
async fn connect_send_receive(
address: SocketAddr,
packet: &LpPacket,
send_key: Option<&OuterAeadKey>,
recv_key: Option<&OuterAeadKey>,
config: &LpRegistrationConfig,
) -> Result<LpPacket> {
// 1. Connect with timeout
let mut stream = tokio::time::timeout(config.connect_timeout, S::connect(address))
.await
.map_err(|_| LpClientError::TcpConnection {
address: address.to_string(),
source: std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("Connection timeout after {:?}", config.connect_timeout),
),
})?
.map_err(|source| LpClientError::TcpConnection {
address: address.to_string(),
source,
})?;
// 2. Set TCP_NODELAY
stream
.set_no_delay(config.tcp_nodelay)
.map_err(|source| LpClientError::TcpConnection {
address: address.to_string(),
source,
})?;
// 3. Send packet with send_key
Self::send_packet_with_key(&mut stream, packet, send_key).await?;
// 4. Receive response with recv_key
let response = Self::receive_packet_with_key(&mut stream, recv_key).await?;
// Connection drops when stream goes out of scope
Ok(response)
}
/// Sends an LP packet over a TCP stream with length-prefixed framing.
///
/// Format: 4-byte big-endian u32 length + packet bytes
@@ -684,10 +443,10 @@ where
async fn send_packet_with_key(
stream: &mut S,
packet: &LpPacket,
outer_key: Option<&OuterAeadKey>,
outer_key: &OuterAeadKey,
) -> Result<()> {
let mut packet_buf = BytesMut::new();
serialize_lp_packet(packet, &mut packet_buf, outer_key)
serialize_lp_packet(packet, &mut packet_buf, Some(outer_key))
.map_err(|e| LpClientError::Transport(format!("Failed to serialize packet: {e}")))?;
stream
@@ -709,16 +468,13 @@ where
/// - Network read fails
/// - Packet size exceeds maximum (64KB)
/// - Packet parsing/decryption fails
async fn receive_packet_with_key(
stream: &mut S,
outer_key: Option<&OuterAeadKey>,
) -> Result<LpPacket> {
async fn receive_packet_with_key(stream: &mut S, outer_key: &OuterAeadKey) -> Result<LpPacket> {
let packet_buf = stream
.receive_raw_packet()
.await
.map_err(|err| LpClientError::transport(err.to_string()))?;
let packet = parse_lp_packet(&packet_buf, outer_key)
let packet = parse_lp_packet(&packet_buf, Some(outer_key))
.map_err(|e| LpClientError::Transport(format!("Failed to parse packet: {e}")))?;
Ok(packet)
@@ -1061,47 +817,30 @@ where
// 1. Serialize the ForwardPacketData
let input = convert_forward_data(forward_data)?;
// 2. Encrypt and prepare packet via state machine (scoped borrow)
let (forward_packet, send_key, recv_key) = {
let state_machine = self.state_machine.as_mut().ok_or_else(|| {
LpClientError::transport("Cannot send forward packet: handshake not completed")
// 2. Encrypt and prepare packet via state machine
let state_machine = self.state_machine_mut()?;
let action = state_machine
.process_input(input)
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
.map_err(|e| {
LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}"))
})?;
let action = state_machine
.process_input(input)
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
.map_err(|e| {
LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}"))
})?;
let forward_packet = match action {
LpAction::SendPacket(packet) => packet,
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when sending ForwardPacket: {:?}",
other
)));
}
};
// Get outer keys from session
let send_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key_for_sending());
let recv_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key());
(forward_packet, send_key, recv_key)
}; // state_machine borrow ends here
let forward_packet = match action {
LpAction::SendPacket(packet) => packet,
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when sending ForwardPacket: {:?}",
other
)));
}
};
// 3. Send and receive on persistent connection with timeout
let response_packet = tokio::time::timeout(self.config.forward_timeout, async {
self.try_send_packet_with_key(&forward_packet, send_key.as_ref())
.await?;
self.try_receive_packet_with_key(recv_key.as_ref()).await
self.try_send_packet(&forward_packet).await?;
self.try_receive_packet().await
})
.await
.map_err(|_| {
@@ -1185,14 +924,11 @@ where
};
// Get outer AEAD key for encryption
let outer_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key_for_sending());
let outer_key = state_machine.session()?.outer_aead_key();
// Serialize the packet with outer AEAD encryption
let mut buf = BytesMut::new();
serialize_lp_packet(&packet, &mut buf, outer_key.as_ref())
serialize_lp_packet(&packet, &mut buf, Some(outer_key))
.map_err(|e| LpClientError::Transport(format!("Failed to serialize LP packet: {e}")))?;
Ok(buf.to_vec())
@@ -0,0 +1,135 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::lp_client::helpers::{convert_forward_data, try_convert_forward_response};
use crate::{LpClientError, LpRegistrationClient};
use nym_crypto::asymmetric::ed25519;
use nym_lp::message::ForwardPacketData;
use nym_lp::state_machine::{LpAction, LpInput};
use nym_lp_transport::traits::LpTransport;
use std::io;
use std::net::SocketAddr;
/// Attempt to treat the inner client as a LP connection
pub struct NestedConnection<'a, S> {
/// Remote Ed25519 public key
pub(crate) exit_identity: ed25519::PublicKey,
/// Exit gateway's LP address (e.g., "2.2.2.2:41264")
pub(crate) exit_address: SocketAddr,
pub(crate) outer_client: &'a mut LpRegistrationClient<S>,
}
impl<'a, S> NestedConnection<'a, S> {
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> Result<(), LpClientError>
where
S: LpTransport + Unpin,
{
let forward_packet_data =
ForwardPacketData::new(self.exit_identity, self.exit_address, packet_data.to_vec());
let target_address = self.exit_address;
tracing::debug!(
"Sending ForwardPacket to {target_address} ({} inner bytes, persistent connection)",
forward_packet_data.inner_packet_bytes.len()
);
// 1. Serialize the ForwardPacketData
let input = convert_forward_data(forward_packet_data)?;
// 2. Encrypt and prepare packet via state machine
let state_machine = self.outer_client.state_machine_mut()?;
let action = state_machine
.process_input(input)
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
.map_err(|e| {
LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}"))
})?;
let forward_packet = match action {
LpAction::SendPacket(packet) => packet,
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when sending ForwardPacket: {:?}",
other
)));
}
};
// 3. Send the packet with timeout
let timeout = self.outer_client.config.forward_timeout;
tokio::time::timeout(timeout, async {
self.outer_client.try_send_packet(&forward_packet).await
})
.await
.map_err(|_| {
LpClientError::Transport(format!("Forward packet timeout after {timeout:?}",))
})??;
Ok(())
}
async fn receive_raw_packet(&mut self) -> Result<Vec<u8>, LpClientError>
where
S: LpTransport + Unpin,
{
// 1. Receive the packet with timeout
let timeout = self.outer_client.config.forward_timeout;
let response_packet = tokio::time::timeout(timeout, async {
self.outer_client.try_receive_packet().await
})
.await
.map_err(|_| {
LpClientError::Transport(format!("Forward packet timeout after {timeout:?}",))
})??;
// 2. Decrypt via state machine (re-borrow)
let state_machine = self.outer_client.state_machine_mut()?;
let action = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
.map_err(|e| {
LpClientError::Transport(format!("Failed to decrypt forward response: {e}"))
})?;
// 3. Extract decrypted response data
let response_data = try_convert_forward_response(action)?;
tracing::debug!(
"Successfully received forward response from {} ({} bytes)",
self.exit_address,
response_data.len()
);
Ok(response_data)
}
}
impl<'a, S> LpTransport for NestedConnection<'a, S>
where
S: LpTransport + Unpin,
{
#[allow(clippy::unimplemented)]
async fn connect(_: SocketAddr) -> std::io::Result<Self> {
// this really breaks the pattern and should be refactored
// since this function should never be called
unimplemented!("cannot establish nested connection without an outer client")
}
fn set_no_delay(&mut self, _: bool) -> std::io::Result<()> {
Ok(())
}
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
self.send_serialised_packet(packet_data)
.await
.map_err(io::Error::other)
}
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
self.receive_raw_packet().await.map_err(io::Error::other)
}
}
@@ -22,8 +22,7 @@ use super::client::LpRegistrationClient;
use super::error::{LpClientError, Result};
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt};
use crate::lp_client::state_machine_helpers::{
extract_forwarded_response, get_recv_key, get_send_key, prepare_serialised_send_packet,
serialize_packet,
extract_forwarded_response, prepare_serialised_send_packet,
};
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
use nym_credentials_interface::TicketType;
@@ -32,8 +31,8 @@ use nym_lp::codec::{OuterAeadKey, parse_lp_packet};
use nym_lp::message::ForwardPacketData;
use nym_lp::packet::version;
use nym_lp::peer::{LpLocalPeer, LpRemotePeer};
use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine};
use nym_lp::{LpMessage, LpPacket};
use nym_lp::state_machine::{LpData, LpStateMachine};
use nym_lp::{LpPacket, LpSession};
use nym_lp_transport::traits::LpTransport;
use nym_registration_common::dvpn::LpDvpnRegistrationResponseMessageContent;
use nym_registration_common::{
@@ -44,9 +43,10 @@ use nym_wireguard_types::PeerPublicKey;
use rand::{CryptoRng, RngCore};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::warn;
pub(crate) mod connection;
/// Manages a nested LP session where the client establishes a handshake with
/// an exit gateway by forwarding packets through an entry gateway.
///
@@ -140,8 +140,8 @@ impl NestedLpSession {
/// Attempt to parse received bytes into an LpPacket
fn parse_received_lp_packet(&self, response_bytes: Vec<u8>) -> Result<LpPacket> {
let state_machine = self.state_machine()?;
let outer_key = get_recv_key(state_machine);
Self::parse_packet(&response_bytes, outer_key.as_ref())
let outer_key = state_machine.session()?.outer_aead_key();
Self::parse_packet(&response_bytes, Some(outer_key))
}
/// Attempt to wrap the provided `LpData` into a `ForwardPacketData`
@@ -193,155 +193,26 @@ impl NestedLpSession {
self.exit_address
);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| LpClientError::Other("System time before UNIX epoch".into()))?
.as_secs();
let mut nested_connection =
outer_client.as_nested_connection(self.gateway_lp_peer.ed25519(), self.exit_address);
// Step 1: Generate ClientHello for exit gateway
let client_hello_data = self.lp_local_peer.build_client_hello_data(timestamp);
let salt = client_hello_data.salt;
let receiver_index = client_hello_data.receiver_index;
let local_peer = self.lp_local_peer.clone();
let remote_peer = self.gateway_lp_peer.clone();
let protocol_version = self.gateway_supported_lp_protocol_version;
tracing::trace!(
"Generated ClientHello for exit gateway (timestamp: {})",
client_hello_data.extract_timestamp()
);
// Step 2: Send ClientHello to exit gateway via forwarding
let client_hello_header = nym_lp::packet::LpHeader::new(
nym_lp::BOOTSTRAP_RECEIVER_IDX, // Use constant for bootstrap session
0, // counter starts at 0
self.gateway_supported_lp_protocol_version,
);
let client_hello_packet = nym_lp::LpPacket::new(
client_hello_header,
LpMessage::ClientHello(client_hello_data),
);
// Serialize and forward ClientHello (no state machine yet, no outer key)
let client_hello_bytes = serialize_packet(&client_hello_packet, None)?;
let forward_packet_data = ForwardPacketData::new(
self.gateway_lp_peer.ed25519(),
self.exit_address,
client_hello_bytes,
);
let response_bytes = outer_client
.send_forward_packet_with_response(forward_packet_data)
.await?;
// Parse and validate Ack response (cleartext, no outer key before PSK derivation)
// this confirms that gateway is fine with our suggested protocol version
// in the future we probably have some fancier negotiation
let ack_response = Self::parse_packet(&response_bytes, None)?;
match ack_response.message() {
LpMessage::Ack => {
tracing::debug!("Received Ack for ClientHello from exit gateway");
}
LpMessage::Collision => {
return Err(LpClientError::Transport(format!(
"Exit gateway returned Collision - receiver_index {receiver_index} already in use",
)));
}
other => {
return Err(LpClientError::Transport(format!(
"Expected Ack for ClientHello from exit gateway, got: {:?}",
other
)));
}
}
// Step 3: Create state machine for exit gateway handshake
let mut state_machine = LpStateMachine::new(
receiver_index,
true, // is_initiator
self.lp_local_peer.clone(),
self.gateway_lp_peer.clone(),
&salt,
self.gateway_supported_lp_protocol_version,
)?;
// Step 4: Get initial packet from StartHandshake
let mut pending_packet: Option<LpPacket> = None;
if let Some(action) = state_machine.process_input(LpInput::StartHandshake) {
match action? {
LpAction::SendPacket(packet) => {
pending_packet = Some(packet);
}
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action at handshake start: {other:?}",
)));
}
}
}
// Step 5: Handshake loop - each packet on new connection via forwarding
loop {
if let Some(packet) = pending_packet.take() {
tracing::trace!("Sending handshake packet to exit via forwarding");
let response = self
.send_and_receive_via_forward(outer_client, &state_machine, &packet)
.await?;
tracing::trace!("Received handshake response from exit");
// Process the received packet
if let Some(action) = state_machine.process_input(LpInput::ReceivePacket(response))
{
match action? {
LpAction::SendPacket(response_packet) => {
pending_packet = Some(response_packet);
// Check if handshake completed - send final packet if so
if state_machine.session()?.is_handshake_complete() {
if let Some(final_packet) = pending_packet.take() {
tracing::trace!("Sending final handshake packet to exit");
let _ = self
.send_and_receive_via_forward(
outer_client,
&state_machine,
&final_packet,
)
.await?;
}
tracing::info!("Nested LP handshake completed with exit gateway");
break;
}
}
LpAction::HandshakeComplete => {
tracing::info!("Nested LP handshake completed with exit gateway");
break;
}
LpAction::KKTComplete => {
tracing::info!("KKT exchange completed with exit, starting Noise");
// After KKT completes, initiator must send first Noise handshake message
let noise_msg = state_machine
.session()?
.prepare_handshake_message()
.ok_or_else(|| {
LpClientError::Transport(
"No handshake message available after KKT".to_string(),
)
})??;
let noise_packet = state_machine.session()?.next_packet(noise_msg)?;
pending_packet = Some(noise_packet);
}
other => {
tracing::trace!("Received action during handshake: {:?}", other);
}
}
}
} else {
// No pending packet and not complete - something is wrong
return Err(LpClientError::Transport(
"Nested handshake stalled: no packet to send".to_string(),
));
}
}
let ciphersuite = LpSession::default_ciphersuite();
let session = LpSession::complete_as_initiator(
&mut nested_connection,
ciphersuite,
local_peer,
remote_peer,
protocol_version,
)
.complete_as_initiator()
.await?;
// Store the state machine (with established session) for later use
self.state_machine = Some(state_machine);
self.state_machine = Some(LpStateMachine::new(session));
Ok(())
}
@@ -660,36 +531,6 @@ impl NestedLpSession {
}))
}
/// Sends a packet via forwarding through the entry gateway and returns the parsed response.
///
/// This helper consolidates the send/receive pattern used throughout the handshake:
/// 1. Gets outer AEAD key from state machine (if available)
/// 2. Serializes the packet with outer encryption
/// 3. Forwards via entry gateway
/// 4. Parses and returns the response
async fn send_and_receive_via_forward<S>(
&self,
outer_client: &mut LpRegistrationClient<S>,
state_machine: &LpStateMachine,
packet: &LpPacket,
) -> Result<LpPacket>
where
S: LpTransport + Unpin,
{
let send_key = get_send_key(state_machine);
let packet_bytes = serialize_packet(packet, send_key.as_ref())?;
let forward_data = ForwardPacketData::new(
self.gateway_lp_peer.ed25519(),
self.exit_address,
packet_bytes,
);
let response_bytes = outer_client
.send_forward_packet_with_response(forward_data)
.await?;
let recv_key = get_recv_key(state_machine);
Self::parse_packet(&response_bytes, recv_key.as_ref())
}
/// Parses an LP packet from bytes.
///
/// # Arguments
@@ -7,26 +7,6 @@ use nym_lp::codec::{OuterAeadKey, serialize_lp_packet};
use nym_lp::state_machine::{LpAction, LpData, LpInput};
use nym_lp::{LpPacket, LpStateMachine};
/// Gets the outer AEAD key for sending (encryption) from the state machine.
///
/// Returns `None` during early handshake before PSK derivation.
pub(crate) fn get_send_key(state_machine: &LpStateMachine) -> Option<OuterAeadKey> {
state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key_for_sending())
}
/// Gets the outer AEAD key for receiving (decryption) from the state machine.
///
/// Returns `None` during early handshake before PSK derivation.
pub(crate) fn get_recv_key(state_machine: &LpStateMachine) -> Option<OuterAeadKey> {
state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key())
}
/// Serializes an LP packet to bytes.
///
/// # Arguments
@@ -79,9 +59,9 @@ pub(crate) fn prepare_serialised_send_packet(
state_machine: &mut LpStateMachine,
) -> Result<Vec<u8>, LpClientError> {
let packet = prepare_send_packet(data, state_machine)?;
let send_key = state_machine.session()?.outer_aead_key();
let send_key = get_send_key(state_machine);
serialize_packet(&packet, send_key.as_ref())
serialize_packet(&packet, Some(send_key))
}
/// Attempt to recover received `LpData` from the received `LpPacket`
+33 -164
View File
@@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array 0.14.7",
"generic-array",
]
[[package]]
@@ -106,7 +106,7 @@ dependencies = [
"cipher",
"ctr",
"ghash",
"subtle 2.6.1",
"subtle",
]
[[package]]
@@ -259,7 +259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2 0.10.6",
"blake2",
"cpufeatures",
"password-hash",
]
@@ -703,7 +703,7 @@ dependencies = [
"ripemd",
"secp256k1",
"sha2 0.10.9",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -752,18 +752,6 @@ dependencies = [
"serde",
]
[[package]]
name = "blake2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330"
dependencies = [
"byte-tools",
"crypto-mac",
"digest 0.8.1",
"opaque-debug 0.2.3",
]
[[package]]
name = "blake2"
version = "0.10.6"
@@ -790,7 +778,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array 0.14.7",
"generic-array",
]
[[package]]
@@ -799,7 +787,7 @@ version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array 0.14.7",
"generic-array",
]
[[package]]
@@ -876,12 +864,6 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]]
name = "bytemuck"
version = "1.22.0"
@@ -1051,16 +1033,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862"
dependencies = [
"byteorder",
"keystream",
]
[[package]]
name = "chrono"
version = "0.4.40"
@@ -1467,9 +1439,9 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array 0.14.7",
"generic-array",
"rand_core 0.6.4",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -1479,21 +1451,11 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array 0.14.7",
"generic-array",
"rand_core 0.6.4",
"typenum",
]
[[package]]
name = "crypto-mac"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
dependencies = [
"generic-array 0.12.4",
"subtle 1.0.0",
]
[[package]]
name = "cssparser"
version = "0.27.2"
@@ -1559,7 +1521,7 @@ dependencies = [
"fiat-crypto",
"rustc_version",
"serde",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -1800,22 +1762,13 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "digest"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
"generic-array 0.12.4",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.7",
"generic-array",
]
[[package]]
@@ -1827,7 +1780,7 @@ dependencies = [
"block-buffer 0.10.4",
"const-oid",
"crypto-common",
"subtle 2.6.1",
"subtle",
]
[[package]]
@@ -2019,7 +1972,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"sha2 0.10.9",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -2054,7 +2007,7 @@ dependencies = [
"crypto-bigint",
"digest 0.10.7",
"ff",
"generic-array 0.14.7",
"generic-array",
"group",
"hkdf",
"pem-rfc7468",
@@ -2062,7 +2015,7 @@ dependencies = [
"rand_core 0.6.4",
"sec1",
"serdect 0.2.0",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -2242,7 +2195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core 0.6.4",
"subtle 2.6.1",
"subtle",
]
[[package]]
@@ -2601,15 +2554,6 @@ dependencies = [
"windows 0.58.0",
]
[[package]]
name = "generic-array"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -2675,7 +2619,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug 0.3.1",
"opaque-debug",
"polyval",
]
@@ -2789,7 +2733,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle 2.6.1",
"subtle",
]
[[package]]
@@ -3547,7 +3491,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array 0.14.7",
"generic-array",
]
[[package]]
@@ -3808,12 +3752,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "keystream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
[[package]]
name = "kuchikiki"
version = "0.8.2"
@@ -3905,18 +3843,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
[[package]]
name = "lioness"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
dependencies = [
"arrayref",
"blake2 0.8.1",
"chacha",
"keystream",
]
[[package]]
name = "litemap"
version = "0.7.5"
@@ -4336,7 +4262,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"serdect 0.3.0",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -4370,7 +4296,7 @@ dependencies = [
"rand 0.8.5",
"serde",
"sha2 0.10.9",
"subtle 2.6.1",
"subtle",
"thiserror 2.0.12",
"zeroize",
]
@@ -4431,7 +4357,6 @@ dependencies = [
"ed25519-dalek",
"jwt-simple",
"nym-pemstore",
"nym-sphinx-types",
"rand 0.8.5",
"serde",
"serde_bytes",
@@ -4681,21 +4606,13 @@ dependencies = [
"time",
]
[[package]]
name = "nym-sphinx-types"
version = "1.20.4"
dependencies = [
"sphinx-packet",
"thiserror 2.0.12",
]
[[package]]
name = "nym-store-cipher"
version = "1.20.4"
dependencies = [
"aes-gcm",
"argon2",
"generic-array 0.14.7",
"generic-array",
"getrandom 0.2.15",
"rand 0.8.5",
"serde",
@@ -5113,12 +5030,6 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]]
name = "opaque-debug"
version = "0.3.1"
@@ -5322,7 +5233,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle 2.6.1",
"subtle",
]
[[package]]
@@ -5698,7 +5609,7 @@ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug 0.3.1",
"opaque-debug",
"universal-hash",
]
@@ -6031,16 +5942,6 @@ dependencies = [
"getrandom 0.3.2",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand 0.8.5",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -6290,7 +6191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle 2.6.1",
"subtle",
]
[[package]]
@@ -6364,7 +6265,7 @@ dependencies = [
"sha2 0.10.9",
"signature",
"spki",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -6439,7 +6340,7 @@ dependencies = [
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.9",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -6631,10 +6532,10 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array 0.14.7",
"generic-array",
"pkcs8",
"serdect 0.2.0",
"subtle 2.6.1",
"subtle",
"zeroize",
]
@@ -6939,7 +6840,7 @@ dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.9.0",
"opaque-debug 0.3.1",
"opaque-debug",
]
[[package]]
@@ -7098,32 +6999,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "sphinx-packet"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e"
dependencies = [
"aes",
"arrayref",
"blake2 0.8.1",
"bs58",
"byteorder",
"chacha",
"ctr",
"curve25519-dalek",
"digest 0.10.7",
"hkdf",
"hmac",
"lioness",
"rand 0.8.5",
"rand_distr",
"sha2 0.10.9",
"subtle 2.6.1",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "spin"
version = "0.9.8"
@@ -7204,12 +7079,6 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "subtle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]]
name = "subtle"
version = "2.6.1"
@@ -7798,7 +7667,7 @@ dependencies = [
"serde_repr",
"sha2 0.10.9",
"signature",
"subtle 2.6.1",
"subtle",
"subtle-encoding",
"tendermint-proto",
"time",
@@ -7853,7 +7722,7 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"subtle 2.6.1",
"subtle",
"subtle-encoding",
"tendermint",
"tendermint-config",
@@ -8437,7 +8306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle 2.6.1",
"subtle",
]
[[package]]