From bb694855d553d2f989fbe5779d520318df81076c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 10 Feb 2026 17:20:54 +0000 Subject: [PATCH] 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 --- Cargo.lock | 5 +- Cargo.toml | 7 +- common/bin-common/src/logging/mod.rs | 2 +- common/crypto/Cargo.toml | 4 +- common/nym-kkt/Cargo.toml | 3 +- common/nym-kkt/benches/benches.rs | 44 +- common/nym-kkt/src/encryption.rs | 7 +- common/nym-kkt/src/key_utils.rs | 3 +- common/nym-kkt/src/kkt.rs | 29 +- common/nym-kkt/src/lib.rs | 52 +- common/nym-kkt/src/session.rs | 6 +- common/nym-lp-transport/src/traits.rs | 130 +- common/nym-lp/Cargo.toml | 14 +- common/nym-lp/benches/replay_protection.rs | 6 +- common/nym-lp/src/codec.rs | 41 +- common/nym-lp/src/error.rs | 23 + common/nym-lp/src/lib.rs | 197 +- common/nym-lp/src/message.rs | 143 +- common/nym-lp/src/noise_protocol.rs | 87 +- common/nym-lp/src/packet.rs | 6 +- common/nym-lp/src/peer.rs | 39 +- common/nym-lp/src/psq/helpers.rs | 52 + common/nym-lp/src/psq/initiator.rs | 391 ++++ common/nym-lp/src/psq/mod.rs | 340 +++ common/nym-lp/src/psq/responder.rs | 461 ++++ common/nym-lp/src/session.rs | 1955 ++--------------- common/nym-lp/src/session_integration/mod.rs | 699 +----- common/nym-lp/src/session_manager.rs | 241 +- common/nym-lp/src/state_machine.rs | 1042 ++------- common/test-utils/Cargo.toml | 2 +- gateway/Cargo.toml | 1 + gateway/src/node/lp_listener/data_handler.rs | 9 +- gateway/src/node/lp_listener/handler.rs | 858 ++------ gateway/src/node/lp_listener/mod.rs | 43 +- gateway/src/node/mod.rs | 1 - integration-tests/src/lp_registration.rs | 3 - .../src/lp_client/client.rs | 434 +--- .../lp_client/nested_session/connection.rs | 135 ++ .../mod.rs} | 205 +- .../src/lp_client/state_machine_helpers.rs | 24 +- nym-wallet/Cargo.lock | 197 +- 41 files changed, 2815 insertions(+), 5126 deletions(-) create mode 100644 common/nym-lp/src/psq/helpers.rs create mode 100644 common/nym-lp/src/psq/initiator.rs create mode 100644 common/nym-lp/src/psq/mod.rs create mode 100644 common/nym-lp/src/psq/responder.rs create mode 100644 nym-registration-client/src/lp_client/nested_session/connection.rs rename nym-registration-client/src/lp_client/{nested_session.rs => nested_session/mod.rs} (71%) diff --git a/Cargo.lock b/Cargo.lock index 136e11bd65..83b806eb3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 2541455d35..5d4ba68b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/common/bin-common/src/logging/mod.rs b/common/bin-common/src/logging/mod.rs index b5a9c68e40..20d2e9e274 100644 --- a/common/bin-common/src/logging/mod.rs +++ b/common/bin-common/src/logging/mod.rs @@ -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; diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 8b08749512..1b5d62f85e 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -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 diff --git a/common/nym-kkt/Cargo.toml b/common/nym-kkt/Cargo.toml index fcc535016d..705395f712 100644 --- a/common/nym-kkt/Cargo.toml +++ b/common/nym-kkt/Cargo.toml @@ -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"] } diff --git a/common/nym-kkt/benches/benches.rs b/common/nym-kkt/benches/benches.rs index 2acb7ab7b5..4d4f0f7e13 100644 --- a/common/nym-kkt/benches/benches.rs +++ b/common/nym-kkt/benches/benches.rs @@ -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(), diff --git a/common/nym-kkt/src/encryption.rs b/common/nym-kkt/src/encryption.rs index 75257fc79c..c4189f9552 100644 --- a/common/nym-kkt/src/encryption.rs +++ b/common/nym-kkt/src/encryption.rs @@ -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"; diff --git a/common/nym-kkt/src/key_utils.rs b/common/nym-kkt/src/key_utils.rs index 535ba8b000..2ea88d231b 100644 --- a/common/nym-kkt/src/key_utils.rs +++ b/common/nym-kkt/src/key_utils.rs @@ -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( 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); diff --git a/common/nym-kkt/src/kkt.rs b/common/nym-kkt/src/kkt.rs index 05b55d575d..c1d21982f3 100644 --- a/common/nym-kkt/src/kkt.rs +++ b/common/nym-kkt/src/kkt.rs @@ -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( /// # 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! diff --git a/common/nym-kkt/src/lib.rs b/common/nym-kkt/src/lib.rs index e6a91a83ec..d879cf2286 100644 --- a/common/nym-kkt/src/lib.rs +++ b/common/nym-kkt/src/lib.rs @@ -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(), diff --git a/common/nym-kkt/src/session.rs b/common/nym-kkt/src/session.rs index 88a59500eb..40db3d275d 100644 --- a/common/nym-kkt/src/session.rs +++ b/common/nym-kkt/src/session.rs @@ -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>, diff --git a/common/nym-lp-transport/src/traits.rs b/common/nym-lp-transport/src/traits.rs index 888c800f64..045bf29498 100644 --- a/common/nym-lp-transport/src/traits.rs +++ b/common/nym-lp-transport/src/traits.rs @@ -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; 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> - 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>; +} - let packet_len = u32::from_be_bytes(len_buf) as usize; +async fn send_serialised_packet_async_write( + 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(reader: &mut R) -> std::io::Result> +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> { + 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> { + receive_raw_packet_async_read(self).await + } } diff --git a/common/nym-lp/Cargo.toml b/common/nym-lp/Cargo.toml index 58f376b1e5..22f11c279b 100644 --- a/common/nym-lp/Cargo.toml +++ b/common/nym-lp/Cargo.toml @@ -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" diff --git a/common/nym-lp/benches/replay_protection.rs b/common/nym-lp/benches/replay_protection.rs index 562982e527..a21a1092f0 100644 --- a/common/nym-lp/benches/replay_protection.rs +++ b/common/nym-lp/benches/replay_protection.rs @@ -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 = (0..size).map(|_| rng.gen_range(0..1024)).collect(); b.iter(|| { diff --git a/common/nym-lp/src/codec.rs b/common/nym-lp/src/codec.rs index d8eee41844..94eedc1199 100644 --- a/common/nym-lp/src/codec.rs +++ b/common/nym-lp/src/codec.rs @@ -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"), + } + } } diff --git a/common/nym-lp/src/error.rs b/common/nym-lp/src/error.rs index caedcc84e4..cf3b5e5085 100644 --- a/common/nym-lp/src/error.rs +++ b/common/nym-lp/src/error.rs @@ -1,9 +1,11 @@ // Copyright 2025 - Nym Technologies SA // 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) -> 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:?}" + )) + } } diff --git a/common/nym-lp/src/lib.rs b/common/nym-lp/src/lib.rs index e1198e7fdc..be5bb991af 100644 --- a/common/nym-lp/src/lib.rs +++ b/common/nym-lp/src/lib.rs @@ -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 { diff --git a/common/nym-lp/src/message.rs b/common/nym-lp/src/message.rs index 0796d0ffcc..fbd692175b 100644 --- a/common/nym-lp/src/message.rs +++ b/common/nym-lp/src/message.rs @@ -1,8 +1,9 @@ // Copyright 2025 - Nym Technologies SA // 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); impl HandshakeData { + pub(crate) fn new(bytes: Vec) -> Self { + Self(bytes) + } fn len(&self) -> usize { self.0.len() } @@ -175,6 +192,11 @@ impl HandshakeData { pub struct EncryptedDataPayload(pub Vec); impl EncryptedDataPayload { + #[allow(dead_code)] + pub(crate) fn new(bytes: Vec) -> Self { + Self(bytes) + } + fn len(&self) -> usize { self.0.len() } @@ -193,6 +215,10 @@ impl EncryptedDataPayload { pub struct KKTRequestData(pub Vec); impl KKTRequestData { + pub(crate) fn new(bytes: Vec) -> Self { + Self(bytes) + } + fn len(&self) -> usize { self.0.len() } @@ -211,6 +237,10 @@ impl KKTRequestData { pub struct KKTResponseData(pub Vec); impl KKTResponseData { + pub(crate) fn new(bytes: Vec) -> 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) -> 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 { + 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 for LpMessage { + fn from(value: HandshakeData) -> Self { + LpMessage::Handshake(value) + } +} + +impl From for LpMessage { + fn from(value: EncryptedDataPayload) -> Self { + LpMessage::EncryptedData(value) + } +} + +impl From for LpMessage { + fn from(value: ClientHelloData) -> Self { + LpMessage::ClientHello(value) + } +} + +impl From for LpMessage { + fn from(value: KKTRequestData) -> Self { + LpMessage::KKTRequest(value) + } +} + +impl From for LpMessage { + fn from(value: KKTResponseData) -> Self { + LpMessage::KKTResponse(value) + } +} + +impl From for LpMessage { + fn from(value: ForwardPacketData) -> Self { + LpMessage::ForwardPacket(value) + } +} + +impl From for LpMessage { + fn from(value: SubsessionKK1Data) -> Self { + LpMessage::SubsessionKK1(value) + } +} + +impl From for LpMessage { + fn from(value: SubsessionKK2Data) -> Self { + LpMessage::SubsessionKK2(value) + } +} + +impl From 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)?)), } } } diff --git a/common/nym-lp/src/noise_protocol.rs b/common/nym-lp/src/noise_protocol.rs index 293bd9ccd4..c56f7cd9b9 100644 --- a/common/nym-lp/src/noise_protocol.rs +++ b/common/nym-lp/src/noise_protocol.rs @@ -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 { + 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 { + 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 { - 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 { - 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)) -} diff --git a/common/nym-lp/src/packet.rs b/common/nym-lp/src/packet.rs index e93edfaad4..db2644b5fa 100644 --- a/common/nym-lp/src/packet.rs +++ b/common/nym-lp/src/packet.rs @@ -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 diff --git a/common/nym-lp/src/peer.rs b/common/nym-lp/src/peer.rs index fd6e5ab7f3..085ea8be5b 100644 --- a/common/nym-lp/src/peer.rs +++ b/common/nym-lp/src/peer.rs @@ -1,9 +1,9 @@ // Copyright 2026 - Nym Technologies SA // 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, 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(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(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(); diff --git a/common/nym-lp/src/psq/helpers.rs b/common/nym-lp/src/psq/helpers.rs new file mode 100644 index 0000000000..7c488187a6 --- /dev/null +++ b/common/nym-lp/src/psq/helpers.rs @@ -0,0 +1,52 @@ +// Copyright 2026 - Nym Technologies SA +// 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 { + 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 + 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 LpTransportHandshakeExt for T where T: LpTransport {} diff --git a/common/nym-lp/src/psq/initiator.rs b/common/nym-lp/src/psq/initiator.rs new file mode 100644 index 0000000000..61804124a9 --- /dev/null +++ b/common/nym-lp/src/psq/initiator.rs @@ -0,0 +1,391 @@ +// Copyright 2026 - Nym Technologies SA +// 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 { + 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 { + 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, 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 + 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 + 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), + } + } +} diff --git a/common/nym-lp/src/psq/mod.rs b/common/nym-lp/src/psq/mod.rs new file mode 100644 index 0000000000..06be6f26ec --- /dev/null +++ b/common/nym-lp/src/psq/mod.rs @@ -0,0 +1,340 @@ +// Copyright 2026 - Nym Technologies SA +// 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, + + /// Protocol version established during the exchange + protocol_version: Option, + + /// Outer aead key established during exchange if we managed to derive it + outer_aead_key: Option, + + /// 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, + + /// 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, + + /// 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 { + 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, + 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 { + 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(()) + } +} diff --git a/common/nym-lp/src/psq/responder.rs b/common/nym-lp/src/psq/responder.rs new file mode 100644 index 0000000000..8882e1269f --- /dev/null +++ b/common/nym-lp/src/psq/responder.rs @@ -0,0 +1,461 @@ +// Copyright 2026 - Nym Technologies SA +// 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), 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 + 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 + 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), + } + } +} diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index 0b8cee57a3..52a336cfca 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -7,33 +7,20 @@ //! and Noise protocol state handling. use crate::codec::OuterAeadKey; -use crate::message::{EncryptedDataPayload, HandshakeData}; +use crate::message::EncryptedDataPayload; // noiserm use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult}; use crate::packet::LpHeader; use crate::peer::{LpLocalPeer, LpRemotePeer}; -use crate::psk::{ - derive_subsession_psk, psq_initiator_create_message, psq_responder_process_message, -}; +use crate::psk::derive_subsession_psk; +use crate::psq::PSQHandshakeState; use crate::replay::ReceivingKeyCounterValidator; use crate::{LpError, LpMessage, LpPacket}; use nym_crypto::asymmetric::{ed25519, x25519}; -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, decrypt_kkt_frame, encrypt_initial_kkt_frame, - encrypt_kkt_frame, -}; -use nym_kkt::session::{ - anonymous_initiator_process, initiator_ingest_response, responder_ingest_message, - responder_process, -}; +use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme}; +use nym_lp_transport::traits::LpTransport; use parking_lot::Mutex; -use rand::RngCore; use snow::Builder; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::time::{SystemTime, UNIX_EPOCH}; use zeroize::{Zeroize, ZeroizeOnDrop}; /// PQ shared secret wrapper with automatic memory zeroization. @@ -59,140 +46,22 @@ impl std::fmt::Debug for PqSharedSecret { } } -/// KKT (KEM Key Transfer) exchange state. -/// -/// Tracks the KKT protocol for obtaining the responder's KEM public key -/// before PSQ can begin. This allows post-quantum KEM algorithms to be -/// used even when keys are not pre-published. -/// -/// # State Transitions -/// -/// **Initiator path:** -/// ```text -/// NotStarted → InitiatorWaiting → Completed -/// ``` -/// -/// **Responder path:** -/// ```text -/// NotStarted → ResponderProcessed -/// ``` -pub enum KKTState { - /// KKT exchange not started. - NotStarted, - - /// Initiator sent KKT request and is waiting for responder's KEM key. - InitiatorWaiting { - /// KKT context for verifying the response - context: nym_kkt::context::KKTContext, - session_secret: KKTSessionSecret, - }, - - /// KKT exchange completed (initiator received and validated KEM key). - Completed { - /// Responder's KEM public key for PSQ encapsulation - kem_pk: Box>, - }, - - /// Responder processed a KKT request and sent response. - /// Responder uses their own KEM keypair, not the one from KKT. - ResponderProcessed, -} - -impl std::fmt::Debug for KKTState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::NotStarted => write!(f, "KKTState::NotStarted"), - Self::InitiatorWaiting { context, .. } => f - .debug_struct("KKTState::InitiatorWaiting") - .field("context", context) - .finish(), - Self::Completed { .. } => write!(f, "KKTState::Completed {{ kem_pk: }}"), - Self::ResponderProcessed => write!(f, "KKTState::ResponderProcessed"), - } - } -} - -/// PSQ (Post-Quantum Secure PSK) handshake state. -/// -/// Tracks the PSQ protocol state machine through the session lifecycle. -/// -/// # State Transitions -/// -/// **Initiator path:** -/// ```text -/// NotStarted → InitiatorWaiting → Completed -/// ``` -/// -/// **Responder path:** -/// ```text -/// NotStarted → ResponderWaiting → Completed -/// ``` -#[derive(Debug)] -pub enum PSQState { - /// PSQ handshake not yet started. - NotStarted, - - /// Initiator has sent PSQ ciphertext and is waiting for confirmation. - /// PSK is already derived, but we don't encrypt outgoing packets yet - /// because the responder may not have processed our message yet. - InitiatorWaiting { - /// The derived PSK, stored until we transition to Completed - psk: [u8; 32], - }, - - /// Responder is ready to receive and decapsulate PSQ ciphertext. - ResponderWaiting, - - /// PSQ handshake completed successfully. - /// The PSK has been derived and registered with the Noise protocol. - Completed { - /// The derived post-quantum PSK - psk: [u8; 32], - }, -} - /// A session in the Lewes Protocol, handling connection state with Noise. /// -/// Sessions manage connection state, including LP replay protection and Noise cryptography. +/// Sessions manage connection state, including LP replay protection. /// Each session has a unique receiving index and sending index for connection identification. -/// -/// ## PSK Injection Lifecycle -/// -/// 1. Session created with dummy PSK `[0u8; 32]` in Noise HandshakeState -/// 2. During handshake, PSQ runs and derives real post-quantum PSK -/// 3. Real PSK injected via `set_psk()` - `psk_injected` flag set to `true` -/// 4. Handshake completes, transport mode available -/// 5. Transport operations (`encrypt_data`/`decrypt_data`) check `psk_injected` flag for safety #[derive(Debug)] pub struct LpSession { - id: u32, + /// Id of the established session + session_id: u32, - /// Flag indicating if this session acts as the Noise protocol initiator. - is_initiator: bool, + /// Negotiated protocol version from handshake. + /// Set during handshake completion from the ClientHello/ServerHello packet header. + /// Used for future version negotiation and compatibility checks. + version: u8, - // noiserm - /// Noise protocol state machine - noise_state: Mutex, - - /// KKT (KEM Key Transfer) exchange state - kkt_state: Mutex, - - /// PSQ (Post-Quantum Secure PSK) handshake state - psq_state: Mutex, - - /// PSK handle from responder (ctxt_B) for future re-registration - psk_handle: Mutex>>, - - /// Counter for outgoing packets - sending_counter: AtomicU64, - - /// Validator for incoming packet counters to prevent replay attacks - receiving_counter: Mutex, - - // noiserm - /// Safety flag: `true` if real PSK was injected via PSQ, `false` if still using dummy PSK. - /// This prevents transport mode operations from running with the insecure dummy `[0u8; 32]` PSK. - psk_injected: AtomicBool, + /// Outer AEAD key for packet encryption (derived from PSK after PSQ handshake). + outer_aead_key: OuterAeadKey, /// Representation of a local Lewes Protocol peer /// encapsulating all the known information and keys. @@ -202,83 +71,120 @@ pub struct LpSession { /// encapsulating all the known information and keys. remote_peer: LpRemotePeer, - /// Salt for PSK derivation - salt: [u8; 32], - - /// Outer AEAD key for packet encryption (derived from PSK after PSQ handshake). - /// None before PSK is available, Some after PSK injection. - outer_aead_key: Mutex>, - + // TODO: ALL BELOW maybe not needed after all? /// Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation. /// Stored after PSQ handshake completes for subsession PSK derivation. - /// This preserves PQ protection when creating subsessions via KKpsk0. - /// Wrapped in PqSharedSecret for automatic memory zeroization on drop. - pq_shared_secret: Mutex>, + pq_shared_secret: PqSharedSecret, + + /// Noise protocol state machine + noise_state: NoiseProtocol, + + /// Counter for outgoing packets + sending_counter: u64, + + /// Validator for incoming packet counters to prevent replay attacks + receiving_counter: ReceivingKeyCounterValidator, /// Monotonically increasing counter for subsession indices. /// Each subsession gets a unique index to ensure unique PSK derivation. /// Uses u64 to make overflow practically impossible (~585k years at 1M/sec). - subsession_counter: AtomicU64, + subsession_counter: u64, /// True if this session has been demoted to read-only mode. /// Demoted sessions can still receive/decrypt but cannot send/encrypt. - read_only: AtomicBool, + read_only: bool, /// ID of the successor session that replaced this one. /// Set when demote() is called. - successor_session_id: Mutex>, - - /// Negotiated protocol version from handshake. - /// Set during handshake completion from the ClientHello/ServerHello packet header. - /// Used for future version negotiation and compatibility checks. - negotiated_version: u8, -} - -// noiserm -/// Generates a fresh salt for PSK derivation. -/// -/// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce -/// -/// This ensures each session derives a unique PSK, even with the same key pairs. -/// The timestamp provides temporal uniqueness while the random nonce prevents collisions. -/// -/// # Returns -/// A 32-byte array containing fresh salt material -pub fn generate_fresh_salt() -> [u8; 32] { - let mut salt = [0u8; 32]; - - // First 8 bytes: current timestamp as u64 little-endian - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - salt[..8].copy_from_slice(×tamp.to_le_bytes()); - - // Last 24 bytes: random nonce - rand::thread_rng().fill_bytes(&mut salt[8..]); - - salt + successor_session_id: Option, } impl LpSession { + /// Creates a new session after completed KTT/PSQ exchange + /// + /// # Arguments + /// + /// * `session_id` - Session identifier + /// * `version` - Protocol version to attach in all `LpPacket`s + /// * `outer_aead_key` - Outer AEAD key for packet encryption + /// * `local_peer` - This side's LP peer's keys + /// * `remote_peer` - The remote's LP peer's keys + /// * `pq_shared_secret` - Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation. + /// * `noise_state` - Noise protocol state machine + pub fn new( + session_id: u32, + version: u8, + outer_aead_key: OuterAeadKey, + local_peer: LpLocalPeer, + remote_peer: LpRemotePeer, + pq_shared_secret: PqSharedSecret, + noise_state: NoiseProtocol, + ) -> Self { + LpSession { + session_id, + version, + outer_aead_key, + local_peer, + remote_peer, + pq_shared_secret, + noise_state, + sending_counter: 0, + receiving_counter: Default::default(), + subsession_counter: 0, + read_only: false, + successor_session_id: None, + } + } + + /// Create an instance of `Ciphersuite` using hardcoded defaults. + /// This is a temporary workaround until values can be properly inferred + /// from reported version + pub fn default_ciphersuite() -> Ciphersuite { + Ciphersuite::new( + KEM::X25519, + HashFunction::Blake3, + SignatureScheme::Ed25519, + HashLength::Default, + ) + } + + /// Helper function to create `PSQHandshakeState` for the handshake initiator + pub fn complete_as_initiator( + connection: &'_ mut S, + ciphersuite: Ciphersuite, + local_peer: LpLocalPeer, + remote_peer: LpRemotePeer, + remote_protocol_version: u8, + ) -> PSQHandshakeState<'_, S> + where + S: LpTransport + Unpin, + { + PSQHandshakeState::new(connection, ciphersuite, local_peer) + .with_protocol_version(remote_protocol_version) + .with_remote_peer(remote_peer) + } + + /// Helper function to create `PSQHandshakeState` for the handshake responder + pub fn psq_handshake_responder( + connection: &'_ mut S, + ciphersuite: Ciphersuite, + local_peer: LpLocalPeer, + ) -> PSQHandshakeState<'_, S> + where + S: LpTransport + Unpin, + { + PSQHandshakeState::new(connection, ciphersuite, local_peer) + } + pub fn id(&self) -> u32 { - self.id - } - - pub fn noise_state(&self) -> &Mutex { - &self.noise_state - } - - /// Returns true if this session was created as the initiator. - pub fn is_initiator(&self) -> bool { - self.is_initiator + self.session_id } /// Returns the negotiated protocol version from the handshake. /// /// Set during `LpSession` creation after sending / receiving `ClientHelloData` pub fn negotiated_version(&self) -> u8 { - self.negotiated_version + self.version } /// Returns the local X25519 public key. @@ -303,154 +209,22 @@ impl LpSession { } /// Returns the outer AEAD key for packet encryption/decryption. - /// - /// Returns `None` before PSK is derived (during initial handshake), - /// `Some(&OuterAeadKey)` after PSK injection via PSQ. - /// - /// Callers should use `None` for packet encryption/decryption during - /// the handshake phase, and use the returned key for transport phase. - /// - /// Note: For sending packets during handshake, use `outer_aead_key_for_sending()` - /// which checks PSQ state to avoid encrypting before the responder can decrypt. - pub fn outer_aead_key(&self) -> Option { - self.outer_aead_key.lock().clone() + pub fn outer_aead_key(&self) -> &OuterAeadKey { + &self.outer_aead_key } - /// Returns the outer AEAD key only if it's safe to use for sending. - /// - /// This method gates the key based on PSQ handshake state: - /// - Returns `None` if PSQ is NotStarted, InitiatorWaiting, or ResponderWaiting - /// - Returns `Some(key)` only if PSQ is Completed - /// - /// # Why This Matters - /// - /// The first Noise handshake message (containing PSQ payload from initiator) - /// must be sent in cleartext because the responder hasn't derived the PSK yet. - /// Only after the responder processes the PSQ and both sides have the PSK - /// can outer encryption be used for sending. - /// - /// For receiving, use `outer_aead_key()` which returns the key as soon as - /// it's derived (needed because the peer may start encrypting before we've - /// finished our send). - // This fixes a bug where the initiator encrypted the first Noise - // message with outer AEAD, but the responder couldn't decrypt because it - // hadn't processed the PSQ yet to derive the same PSK. - pub fn outer_aead_key_for_sending(&self) -> Option { - let psq_state = self.psq_state.lock(); - match &*psq_state { - PSQState::Completed { .. } => self.outer_aead_key.lock().clone(), - _ => None, - } - } - - /// Creates a new session and initializes the Noise protocol state. - /// - /// PSQ always runs during the handshake to derive the real PSK from X25519 DHKEM. - /// The Noise protocol is initialized with a dummy PSK that gets replaced during handshake. - /// - /// # Arguments - /// - /// * `id` - Session identifier - /// * `is_initiator` - True if this side initiates the Noise handshake. - /// * `local_peer` - This side's LP peer's keys - /// * `remote_peer` - The remote's LP peer's keys - /// * `salt` - Salt for PSK derivation - /// * `protocol_version` - Protocol version to attach in all `LpPacket`s - pub fn new( - id: u32, - is_initiator: bool, - local_peer: LpLocalPeer, - remote_peer: LpRemotePeer, - salt: &[u8; 32], - protocol_version: u8, - ) -> Result { - // noiserm - // if we're LP responder, we **must** set our kem key - // georgio: if the initiator is a client, this is ok. but if it's a node then this will block mutual kkt. - if !is_initiator && local_peer.kem_psq.is_none() { - return Err(LpError::ResponderWithMissingKEMKey); - } - - // XKpsk3 pattern requires remote static key known upfront (XK) - // and PSK mixed at position 3. This provides forward secrecy with PSK authentication. - let pattern_name = crate::NOISE_PATTERN; - let psk_index = crate::NOISE_PSK_INDEX; - - // noiserm - let params = pattern_name.parse()?; - let builder = Builder::new(params); - - let local_key_bytes = local_peer.x25519.private_key().as_bytes(); - // noiserm - let builder = builder.local_private_key(local_key_bytes); - - let remote_key_bytes = remote_peer.x25519_public.to_bytes(); - // noiserm - let builder = builder.remote_public_key(&remote_key_bytes); - - // noiserm - // Initialize with dummy PSK - real PSK will be injected via set_psk() during handshake - // when PSQ runs using X25519 as DHKEM - let dummy_psk = [0u8; 32]; - let builder = builder.psk(psk_index, &dummy_psk); - - // noiserm - let initial_state = if is_initiator { - builder.build_initiator().map_err(LpError::SnowKeyError)? - } else { - builder.build_responder().map_err(LpError::SnowKeyError)? - }; - - // noiserm - let noise_protocol = NoiseProtocol::new(initial_state); - - // Initialize KKT state - both roles start at NotStarted - let kkt_state = KKTState::NotStarted; - - // Initialize PSQ state based on role - // georgio: why PSQState::ResponderWaiting if responder? - // georgio: maybe because we can start straight with PSQ? - // georgio: either way, doesn't matter so much because a bad PSQ request will be rejected - let psq_state = if is_initiator { - PSQState::NotStarted - } else { - PSQState::ResponderWaiting - }; - - Ok(Self { - id, - is_initiator, - // noiserm - noise_state: Mutex::new(noise_protocol), - kkt_state: Mutex::new(kkt_state), - psq_state: Mutex::new(psq_state), - psk_handle: Mutex::new(None), - sending_counter: AtomicU64::new(0), - receiving_counter: Mutex::new(ReceivingKeyCounterValidator::default()), - // noiserm - psk_injected: AtomicBool::new(false), - local_peer, - remote_peer, - salt: *salt, - outer_aead_key: Mutex::new(None), - pq_shared_secret: Mutex::new(None), - subsession_counter: AtomicU64::new(0), - read_only: AtomicBool::new(false), - successor_session_id: Mutex::new(None), - negotiated_version: protocol_version, - }) - } - - pub fn next_packet(&self, message: LpMessage) -> Result { + pub fn next_packet(&mut self, message: LpMessage) -> Result { let counter = self.next_counter(); - let header = LpHeader::new(self.id(), counter, self.negotiated_version); + let header = LpHeader::new(self.id(), counter, self.version); let packet = LpPacket::new(header, message); Ok(packet) } /// Generates the next counter value for outgoing packets. - pub fn next_counter(&self) -> u64 { - self.sending_counter.fetch_add(1, Ordering::Relaxed) + pub fn next_counter(&mut self) -> u64 { + let counter = self.sending_counter; + self.sending_counter += 1; + counter } /// Performs a quick validation check for an incoming packet counter. @@ -469,8 +243,7 @@ impl LpSession { pub fn receiving_counter_quick_check(&self, counter: u64) -> Result<(), LpError> { // Branchless implementation uses SIMD when available for constant-time // operations, preventing timing attacks. Check before crypto to save CPU cycles. - let counter_validator = self.receiving_counter.lock(); - counter_validator + self.receiving_counter .will_accept_branchless(counter) .map_err(LpError::Replay) } @@ -488,9 +261,8 @@ impl LpSession { /// /// * `Ok(())` if the counter was successfully marked /// * `Err(LpError::Replay)` if the counter cannot be marked (duplicate, too old, etc.) - pub fn receiving_counter_mark(&self, counter: u64) -> Result<(), LpError> { - let mut counter_validator = self.receiving_counter.lock(); - counter_validator + pub fn receiving_counter_mark(&mut self, counter: u64) -> Result<(), LpError> { + self.receiving_counter .mark_did_receive_branchless(counter) .map_err(LpError::Replay) } @@ -503,601 +275,25 @@ impl LpSession { /// * The next expected counter value for incoming packets /// * The total number of received packets pub fn current_packet_cnt(&self) -> (u64, u64) { - let counter_validator = self.receiving_counter.lock(); - counter_validator.current_packet_cnt() + self.receiving_counter.current_packet_cnt() } - /// Returns the stored PSK handle (ctxt_B) if available. - /// - /// The PSK handle is received from the responder during handshake and can be - /// used for future PSK re-registration without running KEM encapsulation again. - /// - /// # Returns - /// - /// * `Some(Vec)` - The encrypted PSK handle from the responder - /// * `None` - PSK handle not yet received or session is initiator before handshake completion - pub fn get_psk_handle(&self) -> Option> { - self.psk_handle.lock().clone() - } - - /// 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.local_peer - .kem_psq - .as_ref() - .map(|kp| kp.public_key()) - .ok_or(LpError::ResponderWithMissingKEMKey) - } - - // TODO: missing Zeroize - /// Convert local KEM PSQ keys to typed EncapsulationKey / EncapsulationKey - pub fn encapsulated_kem_keys( - &self, - ) -> Result<(DecapsulationKey<'_>, EncapsulationKey<'_>), 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)) - } - - /// Prepares a KKT (KEM Key Transfer) request message. - /// Config: Encryption Enabled, One-Way, Anonymous - /// - /// This should be called by the initiator before starting PSQ - /// to obtain the responder's KEM public key. - /// - /// **Protocol Flow:** - /// 1. Initiator creates an Encrypted, Anonymous, One-Way, KKT request - /// 2. Responder responds with encrypted KEM public key + signature - /// 3. Initiator validates response and stores KEM key for PSQ use - /// - /// # Returns - /// - /// * `Some(Ok(LpMessage::KKTRequest))` - KKT request ready to send - /// * `Some(Err(LpError))` - Error creating KKT request - /// * `None` - KKT not applicable (responder, or already completed) - pub fn prepare_kkt_request(&self) -> Option> { - use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme}; - - let mut kkt_state = self.kkt_state.lock(); - - // Only initiator creates KKT requests, and only when not started - if !self.is_initiator || !matches!(*kkt_state, KKTState::NotStarted) { - return None; - } - - // georgio this needs to be moved one level up (maybe chosen in LPSession) - let ciphersuite = match Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) { - Ok(cs) => cs, - Err(e) => { - return Some(Err(LpError::Internal(format!( - "KKT ciphersuite error: {:?}", - e - )))); - } - }; - - let mut rng = rand09::rng(); - match anonymous_initiator_process(&mut rng, ciphersuite) { - Ok((context, kkt_frame)) => { - match encrypt_initial_kkt_frame( - &mut rng, - &self.remote_peer.x25519_public, - &kkt_frame, - ) { - Ok((session_secret, encrypted_request_bytes)) => { - // Store context for response validation - *kkt_state = KKTState::InitiatorWaiting { - context, - session_secret, - }; - - // Serialize KKT frame to bytes - Some(Ok(LpMessage::KKTRequest(crate::message::KKTRequestData( - encrypted_request_bytes, - )))) - } - Err(e) => Some(Err(LpError::Internal(format!( - "KKT request encryption failed: {:?}", - e - )))), - } - } - Err(e) => Some(Err(LpError::Internal(format!( - "KKT request creation failed: {:?}", - e - )))), - } - } - - /// Attempt to retrieve expected KEM key hash of the remote - /// for [KEM] key type and [HashFunction] specified by own [KKTContext] - fn expected_kem_key_hash(&self, context: KKTContext) -> Result<&Vec, LpError> { - let kem = context.ciphersuite().kem(); - let hash_function = context.ciphersuite().hash_function(); - - let digests = self - .remote_peer - .expected_kem_key_digests - .get(&kem) - .ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?; - - digests - .get(&hash_function) - .ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function }) - } - - /// Processes a KKT response from the responder. - /// - /// Decrypts and validates the response and stores the authenticated KEM public key - /// for use in PSQ encapsulation. - /// - /// # Arguments - /// - /// * `response_bytes` - Raw KKT response message from responder - /// * `expected_key_hash` - Expected hash of responder's KEM key (obtained from directory). - /// - /// # Returns - /// - /// * `Ok(())` - KKT exchange completed, KEM key stored - /// * `Err(LpError)` - Signature verification failed, hash mismatch, or invalid state - /// - pub fn process_kkt_response(&self, encrypted_response_bytes: &[u8]) -> Result<(), LpError> { - let mut kkt_state = self.kkt_state.lock(); - - // Extract context from waiting state - let (mut context, session_secret) = match &*kkt_state { - KKTState::InitiatorWaiting { - context, - session_secret, - } => (*context, *session_secret), - _ => { - return Err(LpError::Internal( - "KKT response received in invalid state".to_string(), - )); - } - }; - - let remote_encapsulation_key = - match decrypt_kkt_frame(&session_secret, encrypted_response_bytes, KKT_RESPONSE_AAD) { - Ok((response_frame, remote_context)) => { - let expected_kem_key_digest = self.expected_kem_key_hash(context)?; - match initiator_ingest_response( - &mut context, - &response_frame, - &remote_context, - &self.remote_peer.ed25519_public, - expected_kem_key_digest, - ) { - Ok(remote_encapsulation_key) => remote_encapsulation_key, - Err(e) => { - return Err(LpError::Internal(format!( - "KKT response handling failure: {:?}", - e - ))); - } - } - } - Err(e) => { - return Err(LpError::Internal(format!( - "KKT response decryption failure: {:?}", - e - ))); - } - }; - - // Store the authenticated KEM key - *kkt_state = KKTState::Completed { - kem_pk: Box::new(remote_encapsulation_key), - }; - Ok(()) - } - - /// Processes a KKT request from the initiator and prepares a signed response. - /// - /// Validates the initiator's signature and creates a response containing this - /// responder's KEM public key, signed with Ed25519. - /// - /// # Arguments - /// - /// * `request_bytes` - Raw KKT request message from initiator - /// * `responder_kem_pk` - This responder's KEM public key to send - /// - /// # Returns - /// - /// * `Ok(LpMessage::KKTResponse)` - Signed KKT response ready to send - /// * `Err(LpError)` - Signature verification failed or invalid request - pub fn process_kkt_request( - &self, - request_bytes: &[u8], - responder_kem_pk: &EncapsulationKey, - ) -> Result { - let mut rng = rand09::rng(); - - let mut kkt_state = self.kkt_state.lock(); - - let response_bytes = match decrypt_initial_kkt_frame( - self.local_peer.x25519().private_key(), - request_bytes, - ) { - Ok((session_secret, request_frame, remote_context)) => { - match responder_ingest_message(&remote_context, None, None, &request_frame) { - Ok((mut context, _)) => match responder_process( - &mut context, - request_frame.session_id(), - self.local_peer.ed25519().private_key(), - responder_kem_pk, - ) { - Ok(response_frame) => match encrypt_kkt_frame( - &mut rng, - &session_secret, - &response_frame, - KKT_RESPONSE_AAD, - ) { - Ok(response_bytes) => response_bytes, - Err(e) => { - return Err(LpError::Internal(format!( - "KKT response encryption failure : {:?}", - e - ))); - } - }, - Err(e) => { - return Err(LpError::Internal(format!( - "KKT response generation failure : {:?}", - e - ))); - } - }, - Err(e) => { - return Err(LpError::Internal(format!( - "KKT request handling failure: {:?}", - e - ))); - } - } - } - Err(e) => { - return Err(LpError::Internal(format!( - "KKT request decryption failure: {:?}", - e - ))); - } - }; - - // Mark KKT as processed - // Responder doesn't store the kem_pk since they already have their own KEM keypair - *kkt_state = KKTState::ResponderProcessed; - - Ok(LpMessage::KKTResponse(crate::message::KKTResponseData( - response_bytes, - ))) - } - - /// Prepares the next handshake message to be sent, if any. - /// - /// This should be called by the driver/IO layer to check if the Noise protocol - /// state machine requires a message to be sent to the peer. - /// - /// For initiators, PSQ always runs on the first message: - /// 1. Converts X25519 keys to DHKEM format - /// 2. Generates PSQ payload and derives PSK - /// 3. Injects PSK into Noise HandshakeState - /// 4. Embeds PSQ payload in first handshake message as: [u16 len][psq_payload][noise_msg] - /// - /// # Returns - /// - /// * `None` if no message needs to be sent currently (e.g., waiting for peer, or handshake complete). - /// * `Some(LpError)` if there's an error within the Noise protocol or PSQ. - pub fn prepare_handshake_message(&self) -> Option> { - let mut noise_state = self.noise_state.lock(); - - // PSQ always runs for initiator on first message - let mut psq_state = self.psq_state.lock(); - - if self.is_initiator && matches!(*psq_state, PSQState::NotStarted) { - // Extract KEM public key from completed KKT exchange - // PSQ requires the authenticated KEM key obtained via KKT protocol - let kkt_state = self.kkt_state.lock(); - let remote_kem = match &*kkt_state { - KKTState::Completed { kem_pk } => kem_pk, - _ => { - return Some(Err(LpError::KKTError( - "PSQ handshake requires completed KKT exchange".to_string(), - ))); - } - }; - - // Generate PSQ payload and PSK using KKT-authenticated KEM key - let session_context = self.id.to_le_bytes(); - - let psq_result = match psq_initiator_create_message( - self.local_peer.x25519.private_key(), - &self.remote_peer.x25519_public, - remote_kem, - self.local_peer.ed25519.private_key(), - self.local_peer.ed25519.public_key(), - &self.salt, - &session_context, - ) { - Ok(result) => result, - Err(e) => { - tracing::error!("PSQ handshake preparation failed, aborting: {:?}", e); - return Some(Err(e)); - } - }; - let psk = psq_result.psk; - let psq_payload = psq_result.payload; - - // Store PQ shared secret for subsession PSK derivation - *self.pq_shared_secret.lock() = Some(PqSharedSecret::new(psq_result.pq_shared_secret)); - - // georgio start psq v2 here - - // Inject PSK into Noise HandshakeState - if let Err(e) = noise_state.set_psk(3, &psk) { - return Some(Err(LpError::NoiseError(e))); - } - // noiserm - // Mark PSK as injected for safety checks in transport mode - self.psk_injected.store(true, Ordering::Release); - - // Derive and store outer AEAD key from PSK - { - let mut outer_key = self.outer_aead_key.lock(); - *outer_key = Some(OuterAeadKey::from_psk(&psk)); - } - - // Get the Noise handshake message - let noise_msg = match noise_state.get_bytes_to_send() { - Some(Ok(msg)) => msg, - Some(Err(e)) => return Some(Err(LpError::NoiseError(e))), - None => return None, // Should not happen if is_my_turn, but handle gracefully - }; - - // Combine: [u16 psq_len][psq_payload][noise_msg] - let psq_len = psq_payload.len() as u16; - let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg.len()); - combined.extend_from_slice(&psq_len.to_le_bytes()); - combined.extend_from_slice(&psq_payload); - combined.extend_from_slice(&noise_msg); - - // PSK is derived but we stay in InitiatorWaiting until we receive msg 2. - // This ensures we send msg 1 in cleartext (responder can't decrypt yet). - *psq_state = PSQState::InitiatorWaiting { psk }; - - return Some(Ok(LpMessage::Handshake(HandshakeData(combined)))); - } - - // Normal flow (no PSQ, or PSQ already completed) - drop(psq_state); // Release lock - - if let Some(message) = noise_state.get_bytes_to_send() { - match message { - Ok(noise_msg) => { - // Check if we have a PSK handle (ctxt_B) to embed (responder message 2 only) - // Only the responder should embed the handle, never the initiator - if !self.is_initiator { - let mut psk_handle_guard = self.psk_handle.lock(); - if let Some(handle_bytes) = psk_handle_guard.take() { - // Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg] - let handle_len = handle_bytes.len() as u16; - let mut combined = - Vec::with_capacity(2 + handle_bytes.len() + noise_msg.len()); - combined.extend_from_slice(&handle_len.to_le_bytes()); - combined.extend_from_slice(&handle_bytes); - combined.extend_from_slice(&noise_msg); - - tracing::debug!( - "Embedding PSK handle ({} bytes) in handshake message 2", - handle_bytes.len() - ); - - return Some(Ok(LpMessage::Handshake(HandshakeData(combined)))); - } - } - // No PSK handle to embed, send noise message as-is - Some(Ok(LpMessage::Handshake(HandshakeData(noise_msg)))) - } - Err(e) => Some(Err(LpError::NoiseError(e))), - } - } else { - None - } - } - - /// Processes a received handshake message from the peer. - /// - /// This should be called by the driver/IO layer after receiving a potential - /// handshake message payload from an LP packet. - /// - /// For responders, PSQ always runs on the first message: - /// 1. Extracts PSQ payload from the first handshake message: [u16 len][psq_payload][noise_msg] - /// 2. Converts X25519 keys to DHKEM format - /// 3. Decapsulates PSK from PSQ payload - /// 4. Injects PSK into Noise HandshakeState - /// 5. Processes the remaining Noise handshake message - /// - /// # Arguments - /// - /// * `message` - The LP message received from the peer, expected to be a Handshake message. - /// - /// # Returns - /// - /// * `Ok(ReadResult)` detailing the outcome (e.g., handshake complete, no-op). - /// * `Err(LpError)` if the message is invalid or causes a Noise/PSQ protocol error. - pub fn process_handshake_message(&self, message: &LpMessage) -> Result { - let mut noise_state = self.noise_state.lock(); - let mut psq_state = self.psq_state.lock(); - - match message { - LpMessage::Handshake(HandshakeData(payload)) => { - // PSQ always runs for responder on first message - if !self.is_initiator && matches!(*psq_state, PSQState::ResponderWaiting) { - // Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg] - if payload.len() < 2 { - return Err(LpError::NoiseError(NoiseError::Other( - "Payload too short for PSQ extraction".to_string(), - ))); - } - - let psq_len = u16::from_le_bytes([payload[0], payload[1]]) as usize; - - if payload.len() < 2 + psq_len { - return Err(LpError::NoiseError(NoiseError::Other( - "Payload length mismatch for PSQ extraction".to_string(), - ))); - } - - let psq_payload = &payload[2..2 + psq_len]; - let noise_payload = &payload[2 + psq_len..]; - - let (dec_key, enc_key) = self.encapsulated_kem_keys()?; - - // Decapsulate PSK from PSQ payload using X25519 as DHKEM - let session_context = self.id.to_le_bytes(); - - let psq_result = match psq_responder_process_message( - self.local_peer.x25519.private_key(), - &self.remote_peer.x25519_public, - (&dec_key, &enc_key), - &self.remote_peer.ed25519_public, - psq_payload, - &self.salt, - &session_context, - ) { - Ok(result) => result, - Err(e) => { - tracing::error!("PSQ handshake processing failed, aborting: {:?}", e); - return Err(e); - } - }; - let psk = psq_result.psk; - - // Store PQ shared secret for subsession PSK derivation - *self.pq_shared_secret.lock() = - Some(PqSharedSecret::new(psq_result.pq_shared_secret)); - - // Store the PSK handle (ctxt_B) for transmission in next message - { - let mut psk_handle = self.psk_handle.lock(); - *psk_handle = Some(psq_result.psk_handle); - } - - // Inject PSK into Noise HandshakeState - noise_state.set_psk(3, &psk)?; - // Mark PSK as injected for safety checks in transport mode - self.psk_injected.store(true, Ordering::Release); - - // Derive and store outer AEAD key from PSK - { - let mut outer_key = self.outer_aead_key.lock(); - *outer_key = Some(OuterAeadKey::from_psk(&psk)); - } - - // Update PSQ state to Completed - *psq_state = PSQState::Completed { psk }; - - // Process the Noise handshake message (without PSQ prefix) - drop(psq_state); // Release lock before processing - return noise_state - .read_message(noise_payload) - .map_err(LpError::NoiseError); - } - - // Check if initiator should extract PSK handle from message 2 - if let PSQState::InitiatorWaiting { psk } = *psq_state - && self.is_initiator - { - // Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg] - if payload.len() >= 2 { - let handle_len = u16::from_le_bytes([payload[0], payload[1]]) as usize; - - if handle_len > 0 && payload.len() >= 2 + handle_len { - // Extract and store the PSK handle - let handle_bytes = &payload[2..2 + handle_len]; - let noise_payload = &payload[2 + handle_len..]; - - tracing::debug!( - "Extracted PSK handle ({} bytes) from message 2", - handle_len - ); - - { - let mut psk_handle = self.psk_handle.lock(); - *psk_handle = Some(handle_bytes.to_vec()); - } - - // Transition to Completed - we've received confirmation from responder - *psq_state = PSQState::Completed { psk }; - drop(psq_state); - - // Process only the Noise message part - return noise_state - .read_message(noise_payload) - .map_err(LpError::NoiseError); - } - } - // If no valid handle found, fall through to normal processing - } - - // The sans-io NoiseProtocol::read_message expects only the payload. - noise_state - .read_message(payload) - .map_err(LpError::NoiseError) - } - _ => Err(LpError::NoiseError(NoiseError::IncorrectStateError)), - } - } - - /// Checks if the Noise handshake phase is complete. - pub fn is_handshake_complete(&self) -> bool { - self.noise_state.lock().is_handshake_finished() - } - - /// Returns the PQ shared secret (K_pq) if available. + /// Returns the PQ shared secret (K_pq). /// /// This is the raw KEM output from PSQ before Blake3 KDF combination. /// Used for deriving subsession PSKs to maintain PQ protection. - pub fn pq_shared_secret(&self) -> Option<[u8; 32]> { - self.pq_shared_secret.lock().as_ref().map(|s| *s.as_bytes()) + pub fn pq_shared_secret(&self) -> &PqSharedSecret { + &self.pq_shared_secret } /// Gets the next subsession index and increments the counter. /// /// Each subsession requires a unique index to ensure unique PSK derivation. /// The index is monotonically increasing per session. - pub fn next_subsession_index(&self) -> u64 { - self.subsession_counter.fetch_add(1, Ordering::Relaxed) + pub fn next_subsession_index(&mut self) -> u64 { + let next = self.subsession_counter; + self.subsession_counter += 1; + next } /// Returns true if this session is in read-only mode. @@ -1105,7 +301,7 @@ impl LpSession { /// Read-only sessions have been demoted after a subsession was promoted. /// They can still decrypt incoming messages but cannot encrypt outgoing ones. pub fn is_read_only(&self) -> bool { - self.read_only.load(Ordering::Acquire) + self.read_only } /// Demotes this session to read-only mode after a subsession replaces it. @@ -1117,20 +313,18 @@ impl LpSession { /// /// # Arguments /// * `successor_idx` - The receiver index of the session that replaced this one - pub fn demote(&self, successor_idx: u32) { - *self.successor_session_id.lock() = Some(successor_idx); - self.read_only.store(true, Ordering::Release); + pub fn demote(&mut self, successor_idx: u32) { + self.successor_session_id = Some(successor_idx); + self.read_only = true; } /// Returns the successor session ID if this session was demoted. pub fn successor_session_id(&self) -> Option { - *self.successor_session_id.lock() + self.successor_session_id } /// Encrypts application data payload using the established Noise transport session. /// - /// This should only be called after the handshake is complete (`is_handshake_complete` returns true). - /// /// # Arguments /// /// * `payload` - The application data to encrypt. @@ -1139,30 +333,18 @@ impl LpSession { /// /// * `Ok(Vec)` containing the encrypted Noise message ciphertext. /// * `Err(NoiseError)` if the session is not in transport mode or encryption fails. - pub fn encrypt_data(&self, payload: &[u8]) -> Result { + pub fn encrypt_data(&mut self, payload: &[u8]) -> Result { // Check if session is read-only (demoted) - if self.read_only.load(Ordering::Acquire) { + if self.read_only { return Err(NoiseError::SessionReadOnly); } - let mut noise_state = self.noise_state.lock(); - // Safety: Prevent transport mode with dummy PSK - if !self.psk_injected.load(Ordering::Acquire) { - return Err(NoiseError::PskNotInjected); - } - // Explicitly check if handshake is finished before trying to write - if !noise_state.is_handshake_finished() { - return Err(NoiseError::IncorrectStateError); - } - let payload = noise_state.write_message(payload)?; + let payload = self.noise_state.write_message(payload)?; Ok(LpMessage::EncryptedData(EncryptedDataPayload(payload))) } /// Decrypts an incoming Noise message containing application data. /// - /// This should only be called after the handshake is complete (`is_handshake_complete` returns true) - /// and when an `LPMessage::EncryptedData` is received. - /// /// # Arguments /// /// * `noise_ciphertext` - The encrypted Noise message received from the peer. @@ -1171,42 +353,15 @@ impl LpSession { /// /// * `Ok(Vec)` containing the decrypted application data payload. /// * `Err(NoiseError)` if the session is not in transport mode, decryption fails, or the message is not data. - pub fn decrypt_data(&self, noise_ciphertext: &LpMessage) -> Result, NoiseError> { - let mut noise_state = self.noise_state.lock(); - // Safety: Prevent transport mode with dummy PSK - if !self.psk_injected.load(Ordering::Acquire) { - return Err(NoiseError::PskNotInjected); - } - // Explicitly check if handshake is finished before trying to read - if !noise_state.is_handshake_finished() { - return Err(NoiseError::IncorrectStateError); - } - + pub fn decrypt_data(&mut self, noise_ciphertext: &LpMessage) -> Result, NoiseError> { let payload = noise_ciphertext.payload(); - match noise_state.read_message(payload)? { + match self.noise_state.read_message(payload)? { ReadResult::DecryptedData(data) => Ok(data), _ => Err(NoiseError::IncorrectStateError), } } - /// Test-only method to set KKT state to Completed with a mock KEM key. - /// This allows tests to bypass KKT exchange and directly test PSQ handshake. - #[cfg(test)] - pub(crate) fn set_kkt_completed_for_test(&self, remote_x25519_pub: &x25519::PublicKey) { - // Convert remote X25519 public key to EncapsulationKey for testing - let remote_kem_bytes = remote_x25519_pub.as_bytes(); - let libcrux_public_key = - libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, remote_kem_bytes) - .expect("Test KEM key conversion failed"); - let kem_pk = EncapsulationKey::X25519(libcrux_public_key); - - let mut kkt_state = self.kkt_state.lock(); - *kkt_state = KKTState::Completed { - kem_pk: Box::new(kem_pk), - }; - } - /// Creates a new subsession using Noise KKpsk0 pattern. /// /// KKpsk0 reuses parent's static X25519 keys (both parties know each other from parent session). @@ -1227,18 +382,11 @@ impl LpSession { subsession_index: u64, is_initiator: bool, ) -> Result { - // Verify parent handshake is complete - if !self.is_handshake_complete() { - return Err(LpError::Internal("Parent handshake not complete".into())); - } - // Get PQ shared secret - let pq_secret = self - .pq_shared_secret() - .ok_or_else(|| LpError::Internal("PQ shared secret not available".into()))?; + let pq_secret = self.pq_shared_secret(); // Derive subsession PSK from parent's PQ shared secret - let subsession_psk = derive_subsession_psk(&pq_secret, subsession_index); + let subsession_psk = derive_subsession_psk(pq_secret.as_bytes(), subsession_index); // Build KKpsk0 handshake // Pattern: Noise_KKpsk0_25519_ChaChaPoly_SHA256 @@ -1266,9 +414,9 @@ impl LpSession { is_initiator, local_peer: self.local_peer.clone(), remote_peer: self.remote_peer.clone(), - pq_shared_secret: PqSharedSecret::new(pq_secret), + pq_shared_secret: self.pq_shared_secret.clone(), subsession_psk, - negotiated_version: self.negotiated_version, + negotiated_version: self.version, }) } } @@ -1382,38 +530,23 @@ impl SubsessionHandshake { // Extract the noise state (now in transport mode) let noise_state = self.noise_state.into_inner(); - // Generate fresh salt for the new session - let salt = generate_fresh_salt(); - // Derive outer AEAD key from the subsession PSK let outer_key = OuterAeadKey::from_psk(&self.subsession_psk); Ok(LpSession { - id: receiver_index, - is_initiator: self.is_initiator, // noiserm - noise_state: Mutex::new(noise_state), - // KKT: subsession inherits from parent, mark as processed - kkt_state: Mutex::new(KKTState::ResponderProcessed), - // PSQ: subsession uses PSK derived from parent's PQ secret - psq_state: Mutex::new(PSQState::Completed { - psk: self.subsession_psk, - }), - psk_handle: Mutex::new(None), // Subsession doesn't have its own handle - sending_counter: AtomicU64::new(0), - receiving_counter: Mutex::new(ReceivingKeyCounterValidator::new(0)), - // noiserm - psk_injected: AtomicBool::new(true), // PSK was in KKpsk0 + session_id: receiver_index, + noise_state, + sending_counter: 0, + receiving_counter: ReceivingKeyCounterValidator::new(0), local_peer: self.local_peer, remote_peer: self.remote_peer, - salt, - outer_aead_key: Mutex::new(Some(outer_key)), - pq_shared_secret: Mutex::new(Some(self.pq_shared_secret)), - subsession_counter: AtomicU64::new(0), - read_only: AtomicBool::new(false), - successor_session_id: Mutex::new(None), - // Inherit parent's protocol version - negotiated_version: self.negotiated_version, + outer_aead_key: outer_key, + pq_shared_secret: self.pq_shared_secret, + subsession_counter: 0, + read_only: false, + successor_session_id: None, + version: self.negotiated_version, }) } } @@ -1421,10 +554,7 @@ impl SubsessionHandshake { #[cfg(test)] mod tests { use super::*; - use crate::packet::version; - use crate::peer::mock_peers; - use crate::{replay::ReplayError, sessions_for_tests}; - use nym_crypto::asymmetric::ed25519; + use crate::{SessionsMock, replay::ReplayError, sessions_for_tests}; use rand::thread_rng; // Helper function to generate keypairs for tests @@ -1432,41 +562,9 @@ mod tests { x25519::KeyPair::new(&mut thread_rng()) } - // Helper function to create a session with real keys for handshake tests - fn create_handshake_test_session(receiver_index: u32, is_initiator: bool) -> LpSession { - let (keys_1, keys_2) = mock_peers(); - - // Create Ed25519 keypairs that correspond to initiator/responder roles - // Initiator uses [1u8], Responder uses [2u8] - let (local, remote) = if is_initiator { - (keys_1, keys_2.as_remote()) - } else { - (keys_2, keys_1.as_remote()) - }; - - let salt = [0u8; 32]; // Test salt - - // PSQ will derive the PSK during handshake using X25519 as DHKEM - let session = LpSession::new( - receiver_index, - is_initiator, - local, - remote.clone(), - &salt, - version::CURRENT, - ) - .expect("Test session creation failed"); - - // Initialize KKT state to Completed for tests (bypasses KKT exchange) - // This simulates having already received the remote party's KEM key via KKT - session.set_kkt_completed_for_test(&remote.x25519_public); - - session - } - #[test] fn test_session_creation() { - let session = sessions_for_tests().0; + let mut session = sessions_for_tests().0; // Initial counter should be zero let counter = session.next_counter(); @@ -1494,7 +592,7 @@ mod tests { #[test] fn test_replay_protection_sequential() { - let session = sessions_for_tests().1; + let mut session = sessions_for_tests().1; // Sequential counters should be accepted assert!(session.receiving_counter_quick_check(0).is_ok()); @@ -1516,7 +614,7 @@ mod tests { #[test] fn test_replay_protection_out_of_order() { - let session = sessions_for_tests().1; + let mut session = sessions_for_tests().1; // Receive packets in order assert!(session.receiving_counter_mark(0).is_ok()); @@ -1537,7 +635,7 @@ mod tests { #[test] fn test_packet_stats() { - let session = sessions_for_tests().1; + let mut session = sessions_for_tests().1; // Initial stats let (next, received) = session.current_packet_cnt(); @@ -1553,235 +651,11 @@ mod tests { assert_eq!(received, 2); } - #[test] - fn test_prepare_handshake_message_initial_state() { - let receiver_index = 12345u32; - - let initiator_session = create_handshake_test_session(receiver_index, true); - let responder_session = create_handshake_test_session( - receiver_index, - false, - // Responder also needs initiator's key for XK - ); - - // Initiator should have a message to send immediately (-> e) - let initiator_msg_result = initiator_session.prepare_handshake_message(); - assert!(initiator_msg_result.is_some()); - let initiator_msg = initiator_msg_result - .unwrap() - .expect("Initiator msg prep failed"); - assert!(!initiator_msg.is_empty()); - - // Responder should have nothing to send initially (waits for <- e) - let responder_msg_result = responder_session.prepare_handshake_message(); - assert!(responder_msg_result.is_none()); - } - - #[test] - fn test_process_handshake_message_first_step() { - let receiver_index = 12345u32; - - let initiator_session = create_handshake_test_session(receiver_index, true); - let responder_session = create_handshake_test_session(receiver_index, false); - - // 1. Initiator prepares the first message (-> e) - let initiator_msg_result = initiator_session.prepare_handshake_message(); - let initiator_msg = initiator_msg_result - .unwrap() - .expect("Initiator msg prep failed"); - - // 2. Responder processes the message (<- e) - let process_result = responder_session.process_handshake_message(&initiator_msg); - - // Check the result of processing - match process_result { - Ok(ReadResult::NoOp) => { - // Expected for XK first message, responder doesn't decrypt data yet - } - Ok(other) => panic!("Unexpected process result: {:?}", other), - Err(e) => panic!("Responder processing failed: {:?}", e), - } - - // 3. After processing, responder should now have a message to send (-> e, es) - let responder_response_result = responder_session.prepare_handshake_message(); - assert!(responder_response_result.is_some()); - let responder_response = responder_response_result - .unwrap() - .expect("Responder response prep failed"); - assert!(!responder_response.is_empty()); - } - - #[test] - fn test_handshake_driver_simulation() { - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - let mut responder_to_initiator_msg = None; - let mut rounds = 0; - const MAX_ROUNDS: usize = 10; // Safety break for the loop - - // Start by priming the initiator message - let mut initiator_to_responder_msg = - initiator_session.prepare_handshake_message().unwrap().ok(); - assert!( - initiator_to_responder_msg.is_some(), - "Initiator did not produce initial message" - ); - - while rounds < MAX_ROUNDS { - rounds += 1; - - // === Initiator -> Responder === - if let Some(msg) = initiator_to_responder_msg.take() { - // Process message - match responder_session.process_handshake_message(&msg) { - Ok(_) => {} - Err(e) => panic!("Responder processing failed: {:?}", e), - } - - // Check if responder needs to send a reply - responder_to_initiator_msg = responder_session - .prepare_handshake_message() - .transpose() - .unwrap(); - } - - // Check completion after potentially processing responder's message below - if initiator_session.is_handshake_complete() - && responder_session.is_handshake_complete() - { - break; - } - - // === Responder -> Initiator === - if let Some(msg) = responder_to_initiator_msg.take() { - // Process message - match initiator_session.process_handshake_message(&msg) { - Ok(_) => {} - Err(e) => panic!("Initiator processing failed: {:?}", e), - } - - // Check if initiator needs to send a reply (should be last message in XK) - initiator_to_responder_msg = initiator_session - .prepare_handshake_message() - .transpose() - .unwrap(); - } - - // Check completion again after potentially processing initiator's message above - if initiator_session.is_handshake_complete() - && responder_session.is_handshake_complete() - { - break; - } - } - - assert!( - rounds < MAX_ROUNDS, - "Handshake did not complete within max rounds" - ); - assert!( - initiator_session.is_handshake_complete(), - "Initiator handshake did not complete" - ); - assert!( - responder_session.is_handshake_complete(), - "Responder handshake did not complete" - ); - - println!("Handshake completed in {} rounds.", rounds); - } - - #[test] - fn test_encrypt_decrypt_after_handshake() { - // --- Setup Handshake --- - - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - // Drive handshake to completion (simplified loop from previous test) - let mut i_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&i_msg).unwrap(); - let r_msg = responder_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - initiator_session.process_handshake_message(&r_msg).unwrap(); - i_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&i_msg).unwrap(); - - assert!(initiator_session.is_handshake_complete()); - assert!(responder_session.is_handshake_complete()); - - // --- Test Encryption/Decryption --- - let plaintext = b"Hello, Lewes Protocol!"; - - // Initiator encrypts - let ciphertext = initiator_session - .encrypt_data(plaintext) - .expect("Initiator encryption failed"); - assert_ne!(ciphertext.payload(), plaintext); // Ensure it's actually encrypted - - // Responder decrypts - let decrypted = responder_session - .decrypt_data(&ciphertext) - .expect("Responder decryption failed"); - assert_eq!(decrypted, plaintext); - - // --- Test other direction --- - let plaintext2 = b"Response from responder."; - - // Responder encrypts - let ciphertext2 = responder_session - .encrypt_data(plaintext2) - .expect("Responder encryption failed"); - assert_ne!(ciphertext2.payload(), plaintext2); - - // Initiator decrypts - let decrypted2 = initiator_session - .decrypt_data(&ciphertext2) - .expect("Initiator decryption failed"); - assert_eq!(decrypted2, plaintext2); - } - - #[test] - fn test_encrypt_decrypt_before_handshake() { - let initiator_session = create_handshake_test_session(12345u32, true); - - assert!(!initiator_session.is_handshake_complete()); - - // Attempt to encrypt before handshake - let plaintext = b"This should fail"; - let result = initiator_session.encrypt_data(plaintext); - assert!(result.is_err()); - match result.unwrap_err() { - NoiseError::PskNotInjected => {} // Expected - PSK check comes before handshake check - e => panic!("Expected PskNotInjected, got {:?}", e), - } - - // Attempt to decrypt before handshake (using dummy ciphertext) - let dummy_ciphertext = vec![0u8; 32]; - let result_decrypt = initiator_session.decrypt_data(&LpMessage::EncryptedData( - EncryptedDataPayload(dummy_ciphertext), - )); - assert!(result_decrypt.is_err()); - match result_decrypt.unwrap_err() { - NoiseError::PskNotInjected => {} // Expected - PSK check comes before handshake check - e => panic!("Expected PskNotInjected, got {:?}", e), - } - } - /* // These tests remain commented as they rely on the old mock crypto functions #[test] fn test_mock_crypto() { - let session = create_test_session(true); + let mut session = create_test_session(true); let data = [1, 2, 3, 4, 5]; let mut encrypted = [0; 5]; let mut decrypted = [0; 5]; @@ -1799,7 +673,7 @@ mod tests { #[test] fn test_mock_crypto_buffer_too_small() { - let session = create_test_session(true); + let mut session = create_test_session(true); let data = [1, 2, 3, 4, 5]; let mut too_small = [0; 3]; @@ -1813,72 +687,6 @@ mod tests { } */ - // ==================================================================== - // PSQ Handshake Integration Tests - // ==================================================================== - - /// Test that PSQ runs during handshake and derives a PSK - #[test] - fn test_psq_handshake_runs_with_psk_injection() { - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - // Drive the handshake - let mut i_msg = initiator_session - .prepare_handshake_message() - .expect("Initiator should have message") - .expect("Message prep should succeed"); - - // The first message should contain PSQ payload embedded - // Verify message is not empty and has reasonable size - assert!(!i_msg.is_empty(), "Initiator message should not be empty"); - assert!( - i_msg.len() > 100, - "Message should contain PSQ payload (actual: {})", - i_msg.len() - ); - - // Responder processes message (which includes PSQ decapsulation) - responder_session - .process_handshake_message(&i_msg) - .expect("Responder should process first message"); - - // Continue handshake - let r_msg = responder_session - .prepare_handshake_message() - .expect("Responder should have message") - .expect("Responder message prep should succeed"); - - initiator_session - .process_handshake_message(&r_msg) - .expect("Initiator should process responder message"); - - i_msg = initiator_session - .prepare_handshake_message() - .expect("Initiator should have final message") - .expect("Final message prep should succeed"); - - responder_session - .process_handshake_message(&i_msg) - .expect("Responder should process final message"); - - // Verify handshake completed - assert!(initiator_session.is_handshake_complete()); - assert!(responder_session.is_handshake_complete()); - - // Verify encryption works (implicitly tests PSK was correctly injected) - let plaintext = b"PSQ test message"; - let encrypted = initiator_session - .encrypt_data(plaintext) - .expect("Encryption should work after handshake"); - - let decrypted = responder_session - .decrypt_data(&encrypted) - .expect("Decryption should work with PSQ-derived PSK"); - - assert_eq!(decrypted, plaintext); - } - /// Test that X25519 keys are correctly converted to KEM format #[test] fn test_x25519_to_kem_conversion() { @@ -1905,404 +713,10 @@ mod tests { // (libcrux_kem::PrivateKey is an enum with no len() method, conversion success is enough) } - /// Test that PSQ actually derives a different PSK (not using dummy) - #[test] - fn test_psq_derived_psk_differs_from_dummy() { - // Create sessions - they start with dummy PSK [0u8; 32] - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - // Prepare first message (initiator runs PSQ and injects PSK) - let i_msg = initiator_session - .prepare_handshake_message() - .expect("Initiator should have message") - .expect("Message prep should succeed"); - - // Verify message is not empty (PSQ runs successfully) - assert!( - !i_msg.is_empty(), - "First message should contain PSQ payload" - ); - - // Complete handshake - responder_session - .process_handshake_message(&i_msg) - .expect("Responder should process message"); - - let r_msg = responder_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - - initiator_session.process_handshake_message(&r_msg).unwrap(); - - let final_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - - responder_session - .process_handshake_message(&final_msg) - .unwrap(); - - // Test that encryption produces non-trivial ciphertext - // (would fail if using dummy PSK incorrectly) - let plaintext = b"test"; - let encrypted = initiator_session.encrypt_data(plaintext).unwrap(); - - // Decrypt should work - let decrypted = responder_session.decrypt_data(&encrypted).unwrap(); - assert_eq!(decrypted, plaintext); - - // Verify ciphertext is not just plaintext (basic encryption sanity) - if let LpMessage::EncryptedData(payload) = encrypted { - assert_ne!( - &payload.0[..plaintext.len()], - plaintext, - "Ciphertext should differ from plaintext" - ); - } else { - panic!("Expected EncryptedData message"); - } - } - - /// Test full end-to-end handshake with PSQ integration - #[test] - fn test_handshake_with_psq_end_to_end() { - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - // Verify initial state - assert!(!initiator_session.is_handshake_complete()); - assert!(!responder_session.is_handshake_complete()); - assert!(initiator_session.is_initiator()); - assert!(!responder_session.is_initiator()); - - // Round 1: Initiator -> Responder (contains PSQ encapsulation) - let msg1 = initiator_session - .prepare_handshake_message() - .expect("Initiator should prepare message") - .expect("Message should succeed"); - - assert!(!msg1.is_empty()); - assert!(!initiator_session.is_handshake_complete()); - - responder_session - .process_handshake_message(&msg1) - .expect("Responder should process PSQ message"); - - assert!(!responder_session.is_handshake_complete()); - - // Round 2: Responder -> Initiator - let msg2 = responder_session - .prepare_handshake_message() - .expect("Responder should prepare message") - .expect("Message should succeed"); - - initiator_session - .process_handshake_message(&msg2) - .expect("Initiator should process message"); - - // Round 3: Initiator -> Responder (final) - let msg3 = initiator_session - .prepare_handshake_message() - .expect("Initiator should prepare final message") - .expect("Message should succeed"); - - responder_session - .process_handshake_message(&msg3) - .expect("Responder should process final message"); - - // Verify both sides completed - assert!(initiator_session.is_handshake_complete()); - assert!(responder_session.is_handshake_complete()); - - // Test bidirectional encrypted communication - let msg_i_to_r = b"Hello from initiator"; - let encrypted_i = initiator_session - .encrypt_data(msg_i_to_r) - .expect("Initiator encryption"); - let decrypted_i = responder_session - .decrypt_data(&encrypted_i) - .expect("Responder decryption"); - assert_eq!(decrypted_i, msg_i_to_r); - - let msg_r_to_i = b"Hello from responder"; - let encrypted_r = responder_session - .encrypt_data(msg_r_to_i) - .expect("Responder encryption"); - let decrypted_r = initiator_session - .decrypt_data(&encrypted_r) - .expect("Initiator decryption"); - assert_eq!(decrypted_r, msg_r_to_i); - - // Successfully completed end-to-end test with PSQ - } - - /// Test that Ed25519 keys are used in PSQ authentication - #[test] - fn test_psq_handshake_uses_ed25519_authentication() { - // Create sessions with explicit Ed25519 keys - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - // Verify sessions store Ed25519 keys - // (Internal verification - keys are used in PSQ calls) - assert_eq!(initiator_session.id(), responder_session.id()); - - // Complete handshake - let msg1 = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&msg1).unwrap(); - - let msg2 = responder_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - initiator_session.process_handshake_message(&msg2).unwrap(); - - let msg3 = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&msg3).unwrap(); - - // If Ed25519 authentication failed, handshake would not complete - assert!(initiator_session.is_handshake_complete()); - assert!(responder_session.is_handshake_complete()); - - // Verify encrypted communication works (proof of successful PSQ with auth) - let test_data = b"Authentication test"; - let encrypted = initiator_session.encrypt_data(test_data).unwrap(); - let decrypted = responder_session.decrypt_data(&encrypted).unwrap(); - assert_eq!(decrypted, test_data); - } - - #[test] - fn test_psq_deserialization_failure() { - // Test that corrupted PSQ payload causes clean abort - - let responder_session = create_handshake_test_session(12345u32, false); - - // Create a handshake message with corrupted PSQ payload - let corrupted_psq_data = vec![0xFF; 128]; // Random garbage - let bad_message = LpMessage::Handshake(HandshakeData(corrupted_psq_data)); - - // Attempt to process corrupted message - should fail - let result = responder_session.process_handshake_message(&bad_message); - - // Should return error (PSQ deserialization will fail) - assert!(result.is_err(), "Expected error for corrupted PSQ payload"); - - // Verify session state is unchanged - // PSQ state should still be ResponderWaiting (not modified) - // Noise PSK should still be dummy [0u8; 32] - assert!(!responder_session.is_handshake_complete()); - } - - #[test] - fn test_handshake_abort_on_psq_failure() { - let (init, resp) = mock_peers(); - - let mut bad_resp = resp.as_remote(); - let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key! - bad_resp.ed25519_public = *wrong_ed25519.public_key(); - - let mut bad_init = init.as_remote(); - bad_init.ed25519_public = *wrong_ed25519.public_key(); - - // Create sessions with MISMATCHED Ed25519 keys - // This simulates authentication failure - let receiver_index: u32 = 55555; - let salt = [0u8; 32]; - - let initiator_session = LpSession::new( - receiver_index, - true, - init.clone(), - bad_resp, - &salt, - version::CURRENT, - ) - .unwrap(); - - // Initialize KKT state for test - initiator_session.set_kkt_completed_for_test(resp.x25519.public_key()); - - let responder_session = LpSession::new( - receiver_index, - false, - resp, - bad_init, - &salt, - version::CURRENT, - ) - .unwrap(); - // Initialize KKT state for test - responder_session.set_kkt_completed_for_test(init.x25519.public_key()); - - // Initiator prepares message (should succeed - signing works) - let msg1 = initiator_session - .prepare_handshake_message() - .expect("Initiator should prepare message") - .expect("Initiator should have message"); - - // Responder processes message - should FAIL (signature verification fails) - let result = responder_session.process_handshake_message(&msg1); - - // Should return CredError due to Ed25519 signature mismatch - assert!( - result.is_err(), - "Expected error for Ed25519 authentication failure" - ); - - // Verify handshake aborted cleanly - assert!(!initiator_session.is_handshake_complete()); - assert!(!responder_session.is_handshake_complete()); - } - - #[test] - fn test_psq_invalid_signature() { - let (init, resp) = mock_peers(); - - let mut bad_init = init.as_remote(); - let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key! - bad_init.ed25519_public = *wrong_ed25519.public_key(); - - let receiver_index: u32 = 66666; - let salt = [0u8; 32]; - - let initiator_session = LpSession::new( - receiver_index, - true, - init.clone(), - resp.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - // Initialize KKT state for test - initiator_session.set_kkt_completed_for_test(resp.x25519.public_key()); - - let responder_session = LpSession::new( - receiver_index, - false, - resp, - bad_init, - &salt, - version::CURRENT, - ) - .unwrap(); - // Initialize KKT state for test - responder_session.set_kkt_completed_for_test(init.x25519.public_key()); - - // Initiator creates message with valid signature (signed with [1u8]) - let msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - - // Responder tries to verify with wrong public key [99u8] - // This should fail Ed25519 signature verification - let result = responder_session.process_handshake_message(&msg); - - assert!(result.is_err(), "Expected signature verification to fail"); - - // Verify error is related to PSQ/authentication - match result.unwrap_err() { - LpError::Internal(msg) if msg.contains("PSQ") => { - // Expected - PSQ v1 responder send failed due to CredError - } - e => panic!("Unexpected error type: {:?}", e), - } - } - - #[test] - fn test_psq_state_unchanged_on_error() { - // Verify that PSQ errors leave session in clean state - let responder_session = create_handshake_test_session(12345u32, false); - - // Capture initial PSQ state (should be ResponderWaiting) - // (We can't directly access psq_state, but we can verify behavior) - - // Send corrupted data - let corrupted_message = LpMessage::Handshake(HandshakeData(vec![0xFF; 100])); - - // Process should fail - let result = responder_session.process_handshake_message(&corrupted_message); - assert!(result.is_err()); - - // After error, session should still be in handshake mode (not complete) - assert!(!responder_session.is_handshake_complete()); - - // Session should still be functional - can process valid messages - // Create a proper initiator to send valid message - let initiator_session = create_handshake_test_session(12345u32, true); - - let valid_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - - // After the error, responder should still be able to process valid messages - let result2 = responder_session.process_handshake_message(&valid_msg); - - // Should succeed (session state was not corrupted by previous error) - assert!( - result2.is_ok(), - "Session should still be functional after PSQ error" - ); - } - - #[test] - fn test_transport_fails_without_psk_injection() { - // This test verifies the safety mechanism that prevents transport mode operations - // from running with the dummy PSK if PSQ injection fails or is skipped. - - // Create session but don't complete handshake (no PSK injection will occur) - let session = create_handshake_test_session(12345u32, true); - - // Verify session was created successfully - assert!(!session.is_handshake_complete()); - - // Attempt to encrypt data - should fail with PskNotInjected - let plaintext = b"test data"; - let encrypt_result = session.encrypt_data(plaintext); - - assert!( - encrypt_result.is_err(), - "encrypt_data should fail without PSK injection" - ); - match encrypt_result.unwrap_err() { - NoiseError::PskNotInjected => { - // Expected - this is the safety mechanism working - } - e => panic!("Expected PskNotInjected error, got: {:?}", e), - } - - // Create a dummy encrypted message to test decrypt - let dummy_ciphertext = LpMessage::EncryptedData(EncryptedDataPayload(vec![0u8; 48])); - - // Attempt to decrypt data - should also fail with PskNotInjected - let decrypt_result = session.decrypt_data(&dummy_ciphertext); - - assert!( - decrypt_result.is_err(), - "decrypt_data should fail without PSK injection" - ); - match decrypt_result.unwrap_err() { - NoiseError::PskNotInjected => { - // Expected - this is the safety mechanism working - } - e => panic!("Expected PskNotInjected error, got: {:?}", e), - } - } - #[test] fn test_demote_sets_read_only() { - let session = create_handshake_test_session(12345u32, true); + let sessions = SessionsMock::mock_post_handshake(12345); + let mut session = sessions.initiator; // Initially not read-only assert!(!session.is_read_only()); @@ -2318,29 +732,9 @@ mod tests { #[test] fn test_encrypt_fails_after_demotion() { - // --- Setup Handshake --- - - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - // Drive handshake to completion - let i_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&i_msg).unwrap(); - let r_msg = responder_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - initiator_session.process_handshake_message(&r_msg).unwrap(); - let i_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&i_msg).unwrap(); - - assert!(initiator_session.is_handshake_complete()); + let receiver_index = 12345; + let sessions = SessionsMock::mock_post_handshake(receiver_index); + let mut initiator_session = sessions.initiator; // Encryption works before demotion let plaintext = b"Hello before demotion"; @@ -2363,29 +757,10 @@ mod tests { #[test] fn test_decrypt_works_after_demotion() { // --- Setup Handshake --- - - let initiator_session = create_handshake_test_session(12345u32, true); - let responder_session = create_handshake_test_session(12345u32, false); - - // Drive handshake to completion - let i_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&i_msg).unwrap(); - let r_msg = responder_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - initiator_session.process_handshake_message(&r_msg).unwrap(); - let i_msg = initiator_session - .prepare_handshake_message() - .unwrap() - .unwrap(); - responder_session.process_handshake_message(&i_msg).unwrap(); - - assert!(initiator_session.is_handshake_complete()); - assert!(responder_session.is_handshake_complete()); + let receiver_index = 12345; + let sessions = SessionsMock::mock_post_handshake(receiver_index); + let mut initiator_session = sessions.initiator; + let mut responder_session = sessions.responder; // Responder encrypts a message let plaintext = b"Message to demoted initiator"; diff --git a/common/nym-lp/src/session_integration/mod.rs b/common/nym-lp/src/session_integration/mod.rs index 7e52d88b9c..e72242a507 100644 --- a/common/nym-lp/src/session_integration/mod.rs +++ b/common/nym-lp/src/session_integration/mod.rs @@ -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; - let mut packet_b_to_a: Option; - 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)); diff --git a/common/nym-lp/src/session_manager.rs b/common/nym-lp/src/session_manager.rs index 9988167164..9ddcb91d3f 100644 --- a/common/nym-lp/src/session_manager.rs +++ b/common/nym-lp/src/session_manager.rs @@ -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, + state_machines: HashMap, } 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, LpError> { + pub fn process_input( + &mut self, + lp_id: u32, + input: LpInput, + ) -> Result, 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 { - Ok(self.get_state(lp_id)? == LpStateBare::Handshaking) - } - - pub fn should_initiate_handshake(&self, lp_id: u32) -> Result { - Ok(self.ready_to_handshake(lp_id)? || self.closed(lp_id)?) - } - - pub fn ready_to_handshake(&self, lp_id: u32) -> Result { - Ok(self.get_state(lp_id)? == LpStateBare::ReadyToHandshake) - } - pub fn closed(&self, lp_id: u32) -> Result { 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> { - self.prepare_handshake_message(lp_id) + pub fn next_counter(&mut self, lp_id: u32) -> Result { + self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))? } - pub fn prepare_handshake_message(&self, lp_id: u32) -> Option> { - self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_handshake_message()) - .ok()? - } - - pub fn is_handshake_complete(&self, lp_id: u32) -> Result { - self.with_state_machine(lp_id, |sm| Ok(sm.session()?.is_handshake_complete()))? - } - - pub fn next_counter(&self, lp_id: u32) -> Result { - self.with_state_machine(lp_id, |sm| Ok(sm.session()?.next_counter()))? - } - - pub fn decrypt_data(&self, lp_id: u32, message: &LpMessage) -> Result, LpError> { - self.with_state_machine(lp_id, |sm| { - sm.session()? + pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result, 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 { - self.with_state_machine(lp_id, |sm| { - sm.session()? + pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result { + 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 { - 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(&self, lp_id: u32, f: F) -> Result + pub fn with_state_machine_mut(&mut self, lp_id: u32, f: F) -> Result 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 { - 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); diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index 91fa48cbe7..7892fbf017 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -13,7 +13,6 @@ //! State machine ensures protocol steps execute in correct order. Invalid transitions //! return LpError, preventing protocol violations. -use crate::peer::{LpLocalPeer, LpRemotePeer}; use crate::{ LpError, message::{LpMessage, SubsessionKK1Data, SubsessionKK2Data, SubsessionReadyData}, @@ -23,25 +22,12 @@ use crate::{ }; use bytes::{Buf, Bytes}; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use nym_kkt::ciphersuite::EncapsulationKey; use std::mem; use tracing::debug; /// Represents the possible states of the Lewes Protocol connection. #[derive(Debug, Default)] pub enum LpState { - /// Initial state: Ready to start the handshake. - /// State machine is created with keys, lp_id is derived, session is ready. - ReadyToHandshake { session: Box }, - - /// Performing KKT (KEM Key Transfer) exchange before Noise handshake. - /// Initiator requests responder's KEM public key, responder provides signed key. - KKTExchange { session: Box }, - - /// Actively performing the Noise handshake. - /// (We might be able to merge this with ReadyToHandshake if the first step always happens) - Handshaking { session: Box }, // Kept for now, logic might merge later - /// Handshake complete, ready for data transport. Transport { session: Box }, @@ -65,9 +51,6 @@ pub enum LpState { #[derive(Debug, Clone, PartialEq, Eq)] pub enum LpStateBare { - ReadyToHandshake, - KKTExchange, - Handshaking, Transport, SubsessionHandshaking, ReadOnlyTransport, @@ -78,9 +61,6 @@ pub enum LpStateBare { impl From<&LpState> for LpStateBare { fn from(state: &LpState) -> Self { match state { - LpState::ReadyToHandshake { .. } => LpStateBare::ReadyToHandshake, - LpState::KKTExchange { .. } => LpStateBare::KKTExchange, - LpState::Handshaking { .. } => LpStateBare::Handshaking, LpState::Transport { .. } => LpStateBare::Transport, LpState::SubsessionHandshaking { .. } => LpStateBare::SubsessionHandshaking, LpState::ReadOnlyTransport { .. } => LpStateBare::ReadOnlyTransport, @@ -94,8 +74,6 @@ impl From<&LpState> for LpStateBare { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum LpInput { - /// Explicitly trigger the start of the handshake (optional, could be implicit on creation) - StartHandshake, /// Received an LP Packet from the network. ReceivePacket(LpPacket), /// Application wants to send data (only valid in Transport state). @@ -114,10 +92,6 @@ pub enum LpAction { SendPacket(LpPacket), /// Deliver decrypted application data received from the peer. DeliverData(LpData), - /// Inform the environment that KKT exchange completed successfully. - KKTComplete, - /// Inform the environment that the handshake is complete. - HandshakeComplete, /// Inform the environment that the connection is closed. ConnectionClosed, /// Subsession KK handshake initiated by this side. @@ -215,12 +189,19 @@ impl LpStateMachine { LpStateBare::from(&self.state) } + pub fn session_mut(&mut self) -> Result<&mut LpSession, LpError> { + match &mut self.state { + LpState::Transport { session } + | LpState::SubsessionHandshaking { session, .. } + | LpState::ReadOnlyTransport { session } => Ok(session), + LpState::Closed { .. } => Err(LpError::LpSessionClosed), + LpState::Processing => Err(LpError::LpSessionProcessing), + } + } + pub fn session(&self) -> Result<&LpSession, LpError> { match &self.state { - LpState::ReadyToHandshake { session } - | LpState::KKTExchange { session } - | LpState::Handshaking { session } - | LpState::Transport { session } + LpState::Transport { session } | LpState::SubsessionHandshaking { session, .. } | LpState::ReadOnlyTransport { session } => Ok(session), LpState::Closed { .. } => Err(LpError::LpSessionClosed), @@ -233,10 +214,7 @@ impl LpStateMachine { /// ownership of the session to the caller. pub fn into_session(self) -> Result { match self.state { - LpState::ReadyToHandshake { session } - | LpState::KKTExchange { session } - | LpState::Handshaking { session } - | LpState::Transport { session } + LpState::Transport { session } | LpState::SubsessionHandshaking { session, .. } | LpState::ReadOnlyTransport { session } => Ok(*session), LpState::Closed { .. } => Err(LpError::LpSessionClosed), @@ -248,40 +226,13 @@ impl LpStateMachine { Ok(self.session()?.id()) } - /// Creates a new state machine from Ed25519 keys. - /// - /// # Arguments - /// - /// * `receiver_index` - Client-proposed session identifier (random 4 bytes) - /// * `is_initiator` - Whether this side initiates the handshake - /// * `local_peer` - This side's LP peer's keys - /// * `remote_peer` - The remote's LP peer's keys - /// * `salt` - Fresh salt for PSK derivation (must be unique per session) - /// * `protocol_version` - Protocol version to attach in all `LpPacket`s - pub fn new( - receiver_index: u32, - is_initiator: bool, - local_peer: LpLocalPeer, - remote_peer: LpRemotePeer, - salt: &[u8; 32], - protocol_version: u8, - ) -> Result { - // Create the session with both Ed25519 (for PSQ auth) and derived X25519 keys (for Noise) - // receiver_index is client-proposed, passed through directly - let session = LpSession::new( - receiver_index, - is_initiator, - local_peer, - remote_peer, - salt, - protocol_version, - )?; - - Ok(LpStateMachine { - state: LpState::ReadyToHandshake { + /// Creates a new state machine in `Transport` state post-KKT/PSQ handshake + pub fn new(session: LpSession) -> Self { + LpStateMachine { + state: LpState::Transport { session: Box::new(session), }, - }) + } } /// Creates a state machine in Transport state from a completed subsession handshake. @@ -318,258 +269,12 @@ impl LpStateMachine { // 2. Match on the owned current_state. Each arm calculates and returns the NEXT state. let next_state = match (current_state, input) { - // --- ReadyToHandshake State --- - (LpState::ReadyToHandshake { session }, LpInput::StartHandshake) => { - if session.is_initiator() { - // Initiator starts by requesting KEM key via KKT - match session.prepare_kkt_request() { - Some(Ok(kkt_message)) => { - match session.next_packet(kkt_message) { - Ok(kkt_packet) => { - result_action = Some(Ok(LpAction::SendPacket(kkt_packet))); - LpState::KKTExchange { session } // Transition to KKTExchange - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Some(Err(e)) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - None => { - // Should not happen for initiator - let err = LpError::Internal( - "prepare_kkt_request returned None for initiator".to_string(), - ); - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - } - } else { - // Responder waits for KKT request - LpState::KKTExchange { session } - // No action needed yet, result_action remains None. - } - } - - // --- KKTExchange State --- - (LpState::KKTExchange { session }, LpInput::ReceivePacket(packet)) => { - // Check if packet lp_id matches our session - if packet.header.receiver_idx() != session.id() { - result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); - LpState::KKTExchange { session } - } else { - - // Packet message is already parsed, match on it directly - match &packet.message { - LpMessage::KKTRequest(kkt_request) if !session.is_initiator() => { - // Responder processes KKT request - // Convert X25519 public key to KEM format for KKT response - - // Get local KEM key - match session.get_kem_key_handle() { - Err(err) => { - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - Ok(local_kem_psq_public) => { - // Convert to libcrux KEM public key - // V1 is X255519 - match libcrux_kem::PublicKey::decode( - libcrux_kem::Algorithm::X25519, - local_kem_psq_public.as_bytes(), - ) { - Ok(libcrux_public_key) => { - let responder_kem_pk = EncapsulationKey::X25519(libcrux_public_key); - - match session.process_kkt_request(&kkt_request.0, &responder_kem_pk) { - Ok(kkt_response_message) => { - match session.next_packet(kkt_response_message) { - Ok(response_packet) => { - result_action = Some(Ok(LpAction::SendPacket(response_packet))); - // After KKT exchange, move to Handshaking - LpState::Handshaking { session } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Err(e) => { - let reason = format!("Failed to convert X25519 to KEM: {:?}", e); - let err = LpError::Internal(reason.clone()); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - } - }, - } - } - LpMessage::KKTResponse(kkt_response) if session.is_initiator() => { - match session.process_kkt_response(&kkt_response.0) { - Ok(()) => { - result_action = Some(Ok(LpAction::KKTComplete)); - // After successful KKT, move to Handshaking - LpState::Handshaking { session } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - _ => { - // Wrong message type for KKT state - let err = LpError::InvalidStateTransition { - state: "KKTExchange".to_string(), - input: format!("Unexpected message type: {:?}", packet.message), - }; - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - } - } - } - - // Reject SendData during KKT exchange - (LpState::KKTExchange { session }, LpInput::SendData(_)) => { - result_action = Some(Err(LpError::InvalidStateTransition { - state: "KKTExchange".to_string(), - input: "SendData".to_string(), - })); - LpState::KKTExchange { session } - } - - // Reject StartHandshake if already in KKT exchange - (LpState::KKTExchange { session }, LpInput::StartHandshake) => { - result_action = Some(Err(LpError::InvalidStateTransition { - state: "KKTExchange".to_string(), - input: "StartHandshake".to_string(), - })); - LpState::KKTExchange { session } - } - - // --- Handshaking State --- - (LpState::Handshaking { session }, LpInput::ReceivePacket(packet)) => { - // Check if packet lp_id matches our session - if packet.header.receiver_idx() != session.id() { - result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); - // Don't change state, return the original state variant - LpState::Handshaking { session } - } else { - // --- Inline handle_handshake_packet logic --- - // 1. Check replay protection *before* processing - if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) { - let _reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Handshaking { session } - // LpState::Closed { reason } - } else { - // 2. Process the handshake message - match session.process_handshake_message(&packet.message) { - Ok(_) => { - // 3. Mark counter as received *after* successful processing - if let Err(e) = session.receiving_counter_mark(packet.header.counter) { - let _reason = e.to_string(); - result_action = Some(Err(e)); - // LpState::Closed { reason } - LpState::Handshaking { session } - } else { - // 4. First check if we need to send a handshake message (before checking completion) - match session.prepare_handshake_message() { - Some(Ok(message)) => { - match session.next_packet(message) { - Ok(response_packet) => { - result_action = Some(Ok(LpAction::SendPacket(response_packet))); - // Check if handshake became complete after preparing message - if session.is_handshake_complete() { - LpState::Transport { session } // Transition to Transport - } else { - LpState::Handshaking { session } // Remain Handshaking - } - } - Err(e) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - Some(Err(e)) => { - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - None => { - // 5. No message to send - check if handshake is complete - if session.is_handshake_complete() { - result_action = Some(Ok(LpAction::HandshakeComplete)); - LpState::Transport { session } // Transition to Transport - } else { - // Handshake stalled unexpectedly - let err = LpError::NoiseError(NoiseError::Other( - "Handshake stalled unexpectedly".to_string(), - )); - let reason = err.to_string(); - result_action = Some(Err(err)); - LpState::Closed { reason } - } - } - } - } - } - Err(e) => { // Error from process_handshake_message - let reason = e.to_string(); - result_action = Some(Err(e)); - LpState::Closed { reason } - } - } - } - // --- End inline handle_handshake_packet logic --- - } - } - // Reject SendData during handshake - (LpState::Handshaking { session }, LpInput::SendData(_)) => { // Keep session if returning to this state - result_action = Some(Err(LpError::InvalidStateTransition { - state: "Handshaking".to_string(), - input: "SendData".to_string(), - })); - // Invalid input, remain in Handshaking state - LpState::Handshaking { session } - } - // Reject StartHandshake if already handshaking - (LpState::Handshaking { session }, LpInput::StartHandshake) => { // Keep session - result_action = Some(Err(LpError::InvalidStateTransition { - state: "Handshaking".to_string(), - input: "StartHandshake".to_string(), - })); - // Invalid input, remain in Handshaking state - LpState::Handshaking { session } - } - // --- Transport State --- - (LpState::Transport { session }, LpInput::ReceivePacket(packet)) => { + (LpState::Transport { mut session }, LpInput::ReceivePacket(packet)) => { // Check if packet lp_id matches our session if packet.header.receiver_idx() != session.id() { - result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); + result_action = + Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); LpState::Transport { session } } else { // Check message type - handle subsession initiation from peer @@ -586,12 +291,22 @@ impl LpStateMachine { // Prepare KK2 response match subsession.prepare_message() { Ok(kk2_payload) => { - let kk2_msg = LpMessage::SubsessionKK2(SubsessionKK2Data { payload: kk2_payload }); + let kk2_msg = LpMessage::SubsessionKK2( + SubsessionKK2Data { + payload: kk2_payload, + }, + ); match session.next_packet(kk2_msg) { Ok(response_packet) => { - result_action = Some(Ok(LpAction::SendPacket(response_packet))); + result_action = + Some(Ok(LpAction::SendPacket( + response_packet, + ))); // Stay in SubsessionHandshaking, wait for SubsessionReady - LpState::SubsessionHandshaking { session, subsession: Box::new(subsession) } + LpState::SubsessionHandshaking { + session, + subsession: Box::new(subsession), + } } Err(e) => { let reason = e.to_string(); @@ -624,7 +339,9 @@ impl LpStateMachine { // Normal encrypted data LpMessage::EncryptedData(_) => { // 1. Check replay protection - if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) { + if let Err(e) = + session.receiving_counter_quick_check(packet.header.counter) + { result_action = Some(Err(e)); LpState::Transport { session } } else { @@ -632,16 +349,19 @@ impl LpStateMachine { match session.decrypt_data(&packet.message) { Ok(plaintext) => { // 3. Mark counter as received - if let Err(e) = session.receiving_counter_mark(packet.header.counter) { + if let Err(e) = + session.receiving_counter_mark(packet.header.counter) + { result_action = Some(Err(e)); LpState::Transport { session } } else { // 4. Deliver data - match plaintext.try_into() { + match plaintext.try_into() { Ok(data) => { - result_action = Some(Ok(LpAction::DeliverData(data))); + result_action = + Some(Ok(LpAction::DeliverData(data))); LpState::Transport { session } - }, + } Err(e) => { let reason = e.to_string(); result_action = Some(Err(e)); @@ -679,9 +399,9 @@ impl LpStateMachine { } } } - (LpState::Transport { session }, LpInput::SendData(data)) => { + (LpState::Transport { mut session }, LpInput::SendData(data)) => { // Encrypt and send application data - match self.prepare_data_packet(&session, data) { + match self.prepare_data_packet(&mut session, data) { Ok(packet) => result_action = Some(Ok(LpAction::SendPacket(packet))), Err(e) => { // If prepare fails, should we close? Let's report error and stay Transport for now. @@ -692,18 +412,9 @@ impl LpStateMachine { // Remain in transport state LpState::Transport { session } } - // Reject StartHandshake if already in transport - (LpState::Transport { session }, LpInput::StartHandshake) => { // Keep session - result_action = Some(Err(LpError::InvalidStateTransition { - state: "Transport".to_string(), - input: "StartHandshake".to_string(), - })); - // Invalid input, remain in Transport state - LpState::Transport { session } - } // --- Transport + InitiateSubsession → SubsessionHandshaking --- - (LpState::Transport { session }, LpInput::InitiateSubsession) => { + (LpState::Transport { mut session }, LpInput::InitiateSubsession) => { // Get next subsession index let subsession_index = session.next_subsession_index(); @@ -713,7 +424,9 @@ impl LpStateMachine { // Prepare KK1 message match subsession.prepare_message() { Ok(kk1_payload) => { - let kk1_msg = LpMessage::SubsessionKK1(SubsessionKK1Data { payload: kk1_payload }); + let kk1_msg = LpMessage::SubsessionKK1(SubsessionKK1Data { + payload: kk1_payload, + }); match session.next_packet(kk1_msg) { Ok(packet) => { // Emit SubsessionInitiated with packet and index @@ -721,7 +434,10 @@ impl LpStateMachine { packet, subsession_index, })); - LpState::SubsessionHandshaking { session, subsession: Box::new(subsession) } + LpState::SubsessionHandshaking { + session, + subsession: Box::new(subsession), + } } Err(e) => { let reason = e.to_string(); @@ -746,11 +462,21 @@ impl LpStateMachine { } // --- SubsessionHandshaking State --- - (LpState::SubsessionHandshaking { session, subsession }, LpInput::ReceivePacket(packet)) => { + ( + LpState::SubsessionHandshaking { + mut session, + subsession, + }, + LpInput::ReceivePacket(packet), + ) => { // Check if packet receiver_idx matches our session if packet.header.receiver_idx() != session.id() { - result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); - LpState::SubsessionHandshaking { session, subsession } + result_action = + Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); + LpState::SubsessionHandshaking { + session, + subsession, + } } else { match &packet.message { LpMessage::SubsessionKK1(kk1_data) if !subsession.is_initiator() => { @@ -761,12 +487,20 @@ impl LpStateMachine { Ok(_) => { match subsession.prepare_message() { Ok(kk2_payload) => { - let kk2_msg = LpMessage::SubsessionKK2(SubsessionKK2Data { payload: kk2_payload }); + let kk2_msg = + LpMessage::SubsessionKK2(SubsessionKK2Data { + payload: kk2_payload, + }); match session.next_packet(kk2_msg) { Ok(response_packet) => { - result_action = Some(Ok(LpAction::SendPacket(response_packet))); + result_action = Some(Ok(LpAction::SendPacket( + response_packet, + ))); // Stay in SubsessionHandshaking, wait for SubsessionReady - LpState::SubsessionHandshaking { session, subsession } + LpState::SubsessionHandshaking { + session, + subsession, + } } Err(e) => { let reason = e.to_string(); @@ -810,12 +544,24 @@ impl LpStateMachine { Ok(_) => { match new_subsession.prepare_message() { Ok(kk2_payload) => { - let kk2_msg = LpMessage::SubsessionKK2(SubsessionKK2Data { payload: kk2_payload }); + let kk2_msg = LpMessage::SubsessionKK2( + SubsessionKK2Data { + payload: kk2_payload, + }, + ); match session.next_packet(kk2_msg) { Ok(response_packet) => { - result_action = Some(Ok(LpAction::SendPacket(response_packet))); + result_action = + Some(Ok(LpAction::SendPacket( + response_packet, + ))); // Replace old initiator subsession with new responder subsession - LpState::SubsessionHandshaking { session, subsession: Box::new(new_subsession) } + LpState::SubsessionHandshaking { + session, + subsession: Box::new( + new_subsession, + ), + } } Err(e) => { let reason = e.to_string(); @@ -850,8 +596,12 @@ impl LpStateMachine { let abort_msg = LpMessage::SubsessionAbort; match session.next_packet(abort_msg) { Ok(abort_packet) => { - result_action = Some(Ok(LpAction::SendPacket(abort_packet))); - LpState::SubsessionHandshaking { session, subsession } + result_action = + Some(Ok(LpAction::SendPacket(abort_packet))); + LpState::SubsessionHandshaking { + session, + subsession, + } } Err(e) => { let reason = e.to_string(); @@ -872,16 +622,18 @@ impl LpStateMachine { session.demote(new_receiver_index); // Send SubsessionReady with new index - let ready_msg = LpMessage::SubsessionReady(SubsessionReadyData { - receiver_index: new_receiver_index, - }); + let ready_msg = + LpMessage::SubsessionReady(SubsessionReadyData { + receiver_index: new_receiver_index, + }); match session.next_packet(ready_msg) { Ok(ready_packet) => { - result_action = Some(Ok(LpAction::SubsessionComplete { - packet: Some(ready_packet), - subsession, - new_receiver_index, - })); + result_action = + Some(Ok(LpAction::SubsessionComplete { + packet: Some(ready_packet), + subsession, + new_receiver_index, + })); LpState::ReadOnlyTransport { session } } Err(e) => { @@ -893,7 +645,9 @@ impl LpStateMachine { } Ok(_) => { // Handshake not complete yet, shouldn't happen for KK - let err = LpError::Internal("Subsession handshake incomplete after KK2".to_string()); + let err = LpError::Internal( + "Subsession handshake incomplete after KK2".to_string(), + ); let reason = err.to_string(); result_action = Some(Err(err)); LpState::Closed { reason } @@ -908,24 +662,41 @@ impl LpStateMachine { LpMessage::EncryptedData(_) => { // Parent still processes normal traffic during subsession handshake // Same as Transport state handling - if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) { + if let Err(e) = + session.receiving_counter_quick_check(packet.header.counter) + { result_action = Some(Err(e)); - LpState::SubsessionHandshaking { session, subsession } + LpState::SubsessionHandshaking { + session, + subsession, + } } else { match session.decrypt_data(&packet.message) { Ok(plaintext) => { - if let Err(e) = session.receiving_counter_mark(packet.header.counter) { + if let Err(e) = + session.receiving_counter_mark(packet.header.counter) + { result_action = Some(Err(e)); - LpState::SubsessionHandshaking { session, subsession } + LpState::SubsessionHandshaking { + session, + subsession, + } } else { match plaintext.try_into() { Ok(data) => { - result_action = Some(Ok(LpAction::DeliverData(data))); - LpState::SubsessionHandshaking { session, subsession } + result_action = + Some(Ok(LpAction::DeliverData(data))); + LpState::SubsessionHandshaking { + session, + subsession, + } } Err(err) => { result_action = Some(Err(err)); - LpState::SubsessionHandshaking { session, subsession } + LpState::SubsessionHandshaking { + session, + subsession, + } } } } @@ -954,7 +725,8 @@ impl LpStateMachine { } else { // Shouldn't happen - handshake should be complete after KK2 let err = LpError::Internal( - "Received SubsessionReady but handshake not complete".to_string(), + "Received SubsessionReady but handshake not complete" + .to_string(), ); let reason = err.to_string(); result_action = Some(Err(err)); @@ -975,7 +747,10 @@ impl LpStateMachine { // The winner's abort message arrived after we processed their KK1. // Silently ignore it - we're in the correct state. result_action = None; - LpState::SubsessionHandshaking { session, subsession } + LpState::SubsessionHandshaking { + session, + subsession, + } } _ => { // Wrong message type for subsession handshake @@ -992,40 +767,52 @@ impl LpStateMachine { } // Parent can still send data during subsession handshake - (LpState::SubsessionHandshaking { session, subsession }, LpInput::SendData(data)) => { - match self.prepare_data_packet(&session, data) { + ( + LpState::SubsessionHandshaking { + mut session, + subsession, + }, + LpInput::SendData(data), + ) => { + match self.prepare_data_packet(&mut session, data) { Ok(packet) => result_action = Some(Ok(LpAction::SendPacket(packet))), Err(e) => { result_action = Some(Err(e.into())); } } - LpState::SubsessionHandshaking { session, subsession } + LpState::SubsessionHandshaking { + session, + subsession, + } } // Reject other inputs during subsession handshake - (LpState::SubsessionHandshaking { session, subsession }, LpInput::StartHandshake) => { - result_action = Some(Err(LpError::InvalidStateTransition { - state: "SubsessionHandshaking".to_string(), - input: "StartHandshake".to_string(), - })); - LpState::SubsessionHandshaking { session, subsession } - } - - (LpState::SubsessionHandshaking { session, subsession }, LpInput::InitiateSubsession) => { + ( + LpState::SubsessionHandshaking { + session, + subsession, + }, + LpInput::InitiateSubsession, + ) => { result_action = Some(Err(LpError::InvalidStateTransition { state: "SubsessionHandshaking".to_string(), input: "InitiateSubsession".to_string(), })); - LpState::SubsessionHandshaking { session, subsession } + LpState::SubsessionHandshaking { + session, + subsession, + } } // --- ReadOnlyTransport State --- - (LpState::ReadOnlyTransport { session }, LpInput::ReceivePacket(packet)) => { + (LpState::ReadOnlyTransport { mut session }, LpInput::ReceivePacket(packet)) => { // Can still receive and decrypt, but state stays ReadOnlyTransport if packet.header.receiver_idx() != session.id() { - result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); + result_action = + Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx()))); LpState::ReadOnlyTransport { session } - } else if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) { + } else if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) + { result_action = Some(Err(e)); LpState::ReadOnlyTransport { session } } else { @@ -1063,14 +850,6 @@ impl LpStateMachine { } // Reject other inputs in read-only mode - (LpState::ReadOnlyTransport { session }, LpInput::StartHandshake) => { - result_action = Some(Err(LpError::InvalidStateTransition { - state: "ReadOnlyTransport".to_string(), - input: "StartHandshake".to_string(), - })); - LpState::ReadOnlyTransport { session } - } - (LpState::ReadOnlyTransport { session }, LpInput::InitiateSubsession) => { result_action = Some(Err(LpError::InvalidStateTransition { state: "ReadOnlyTransport".to_string(), @@ -1081,17 +860,16 @@ impl LpStateMachine { // --- Close Transition (applies to ReadyToHandshake, KKTExchange, Handshaking, Transport, SubsessionHandshaking, ReadOnlyTransport) --- ( - LpState::ReadyToHandshake { .. } // We consume the session here - | LpState::KKTExchange { .. } - | LpState::Handshaking { .. } - | LpState::Transport { .. } + LpState::Transport { .. } | LpState::SubsessionHandshaking { .. } | LpState::ReadOnlyTransport { .. }, LpInput::Close, ) => { result_action = Some(Ok(LpAction::ConnectionClosed)); // Transition to Closed state - LpState::Closed { reason: "Closed by user".to_string() } + LpState::Closed { + reason: "Closed by user".to_string(), + } } // Ignore Close if already Closed (closed_state @ LpState::Closed { .. }, LpInput::Close) => { @@ -1147,7 +925,7 @@ impl LpStateMachine { // Kept as it doesn't mutate self.state fn prepare_data_packet( &self, - session: &LpSession, + session: &mut LpSession, data: LpData, ) -> Result { let encrypted_message = session.encrypt_data(Vec::::from(data).as_ref())?; @@ -1160,51 +938,19 @@ impl LpStateMachine { #[cfg(test)] mod tests { use super::*; - use crate::packet::version; - use crate::peer::mock_peers; + use crate::SessionsMock; #[test] fn test_state_machine_init() { - let (init, resp) = mock_peers(); + let mock_sessions = SessionsMock::mock_post_handshake(123); - // Test salt - let salt = [51u8; 32]; - - let receiver_index: u32 = 77777; - - let initiator_sm = LpStateMachine::new( - receiver_index, - true, - init.clone(), - resp.as_remote(), - &salt, - version::CURRENT, - ); - assert!(initiator_sm.is_ok()); - let initiator_sm = initiator_sm.unwrap(); - assert!(matches!( - initiator_sm.state, - LpState::ReadyToHandshake { .. } - )); + let initiator_sm = LpStateMachine::new(mock_sessions.initiator); + assert!(matches!(initiator_sm.state, LpState::Transport { .. })); let init_session = initiator_sm.session().unwrap(); - assert!(init_session.is_initiator()); - let responder_sm = LpStateMachine::new( - receiver_index, - false, - resp, - init.as_remote(), - &salt, - version::CURRENT, - ); - assert!(responder_sm.is_ok()); - let responder_sm = responder_sm.unwrap(); - assert!(matches!( - responder_sm.state, - LpState::ReadyToHandshake { .. } - )); + let responder_sm = LpStateMachine::new(mock_sessions.responder); + assert!(matches!(responder_sm.state, LpState::Transport { .. })); let resp_session = responder_sm.session().unwrap(); - assert!(!resp_session.is_initiator()); // Check both state machines use the same receiver_index assert_eq!(init_session.id(), resp_session.id()); @@ -1212,152 +958,17 @@ mod tests { #[test] fn test_state_machine_simplified_flow() { - let (init, resp) = mock_peers(); + let receiver_index: u32 = 123; + let mock_sessions = SessionsMock::mock_post_handshake(123); - // Test salt - let salt = [52u8; 32]; - let receiver_index: u32 = 88888; - - // Create state machines (already in ReadyToHandshake) - let mut initiator = LpStateMachine::new( - receiver_index, - true, // is_initiator - init.clone(), - resp.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - let mut responder = LpStateMachine::new( - receiver_index, - false, // is_initiator - resp, - init.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); + // Create state machines (already in Transport) + let mut initiator = LpStateMachine::new(mock_sessions.initiator); + let mut responder = LpStateMachine::new(mock_sessions.responder); assert_eq!(initiator.id().unwrap(), responder.id().unwrap()); - // --- KKT Exchange --- - println!("--- Step 1: Initiator starts handshake (sends KKT request) ---"); - let init_actions_1 = initiator.process_input(LpInput::StartHandshake); - let kkt_request_packet = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_1 { - packet.clone() - } else { - panic!("Initiator should send KKT request"); - }; - - assert!( - matches!(initiator.state, LpState::KKTExchange { .. }), - "Initiator should be in KKTExchange" - ); - assert_eq!( - kkt_request_packet.header.receiver_idx(), - receiver_index, - "KKT request packet has wrong receiver_index" - ); - - println!("--- Step 2: Responder starts handshake (waits for KKT) ---"); - let resp_actions_1 = responder.process_input(LpInput::StartHandshake); - assert!( - resp_actions_1.is_none(), - "Responder should produce 0 actions initially" - ); - assert!( - matches!(responder.state, LpState::KKTExchange { .. }), - "Responder should be in KKTExchange" - ); - - println!("--- Step 3: Responder receives KKT request, sends KKT response ---"); - let resp_actions_2 = responder.process_input(LpInput::ReceivePacket(kkt_request_packet)); - let kkt_response_packet = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_2 { - packet.clone() - } else { - panic!("Responder should send KKT response"); - }; - assert!( - matches!(responder.state, LpState::Handshaking { .. }), - "Responder should be Handshaking after KKT" - ); - - println!("--- Step 4: Initiator receives KKT response (KKT complete) ---"); - let init_actions_2 = initiator.process_input(LpInput::ReceivePacket(kkt_response_packet)); - assert!( - matches!(init_actions_2, Some(Ok(LpAction::KKTComplete))), - "Initiator should signal KKT complete" - ); - assert!( - matches!(initiator.state, LpState::Handshaking { .. }), - "Initiator should be Handshaking after KKT" - ); - - // --- Noise Handshake Message Exchange --- - println!("--- Step 5: Responder receives Noise msg 1, sends Noise msg 2 ---"); - // Now both sides are in Handshaking, continue with Noise handshake - // Initiator needs to send first Noise message - // (In real flow, this might happen automatically or via another process_input call) - // For this test, we'll simulate the responder receiving the first Noise message - // Actually, let me check if initiator automatically sends the first Noise message... - // Looking at the old test, it seems packet 1 was the first Noise message. - // With KKT, we need the initiator to send the first Noise message now. - - // Initiator prepares and sends first Noise handshake message - let init_noise_msg = initiator.session().unwrap().prepare_handshake_message(); - let init_packet_1 = if let Some(Ok(msg)) = init_noise_msg { - initiator.session().unwrap().next_packet(msg).unwrap() - } else { - panic!("Initiator should have first Noise message"); - }; - - let resp_actions_3 = responder.process_input(LpInput::ReceivePacket(init_packet_1)); - let resp_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_3 { - packet.clone() - } else { - panic!("Responder should send packet 2"); - }; - assert!( - matches!(responder.state, LpState::Handshaking { .. }), - "Responder still Handshaking" - ); - assert_eq!( - resp_packet_2.header.receiver_idx(), - receiver_index, - "Packet 2 has wrong receiver_index" - ); - - println!("--- Step 6: Initiator receives Noise msg 2, sends Noise msg 3 ---"); - let init_actions_3 = initiator.process_input(LpInput::ReceivePacket(resp_packet_2)); - let init_packet_3 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_3 { - packet.clone() - } else { - panic!("Initiator should send Noise packet 3"); - }; - assert!( - matches!(initiator.state, LpState::Transport { .. }), - "Initiator should be Transport" - ); - assert_eq!( - init_packet_3.header.receiver_idx(), - receiver_index, - "Noise packet 3 has wrong receiver_index" - ); - - println!("--- Step 7: Responder receives Noise msg 3, completes handshake ---"); - let resp_actions_4 = responder.process_input(LpInput::ReceivePacket(init_packet_3)); - assert!( - matches!(resp_actions_4, Some(Ok(LpAction::HandshakeComplete))), - "Responder should complete handshake" - ); - assert!( - matches!(responder.state, LpState::Transport { .. }), - "Responder should be Transport" - ); - // --- Transport Phase --- - println!("--- Step 8: Initiator sends data ---"); + println!("--- Step 1: Initiator sends data ---"); let data_to_send_1 = LpData::new_opaque(b"hello responder".to_vec()); let init_actions_4 = initiator.process_input(LpInput::SendData(data_to_send_1.clone())); let data_packet_1 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_4 { @@ -1367,7 +978,7 @@ mod tests { }; assert_eq!(data_packet_1.header.receiver_idx(), receiver_index); - println!("--- Step 9: Responder receives data ---"); + println!("--- Step 2: Responder receives data ---"); let resp_actions_5 = responder.process_input(LpInput::ReceivePacket(data_packet_1)); let resp_data_1 = if let Some(Ok(LpAction::DeliverData(data))) = resp_actions_5 { data @@ -1376,7 +987,7 @@ mod tests { }; assert_eq!(resp_data_1, data_to_send_1); - println!("--- Step 10: Responder sends data ---"); + println!("--- Step 3: Responder sends data ---"); let data_to_send_2 = LpData::new_opaque(b"hello initiator".to_vec()); let resp_actions_6 = responder.process_input(LpInput::SendData(data_to_send_2.clone())); let data_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_6 { @@ -1386,7 +997,7 @@ mod tests { }; assert_eq!(data_packet_2.header.receiver_idx(), receiver_index); - println!("--- Step 11: Initiator receives data ---"); + println!("--- Step 4: Initiator receives data ---"); let init_actions_5 = initiator.process_input(LpInput::ReceivePacket(data_packet_2)); if let Some(Ok(LpAction::DeliverData(data))) = init_actions_5 { assert_eq!(data, data_to_send_2); @@ -1395,7 +1006,7 @@ mod tests { } // --- Close --- - println!("--- Step 12: Initiator closes ---"); + println!("--- Step 5: Initiator closes ---"); let init_actions_6 = initiator.process_input(LpInput::Close); assert!(matches!( init_actions_6, @@ -1403,7 +1014,7 @@ mod tests { )); assert!(matches!(initiator.state, LpState::Closed { .. })); - println!("--- Step 13: Responder closes ---"); + println!("--- Step 6: Responder closes ---"); let resp_actions_7 = responder.process_input(LpInput::Close); assert!(matches!( resp_actions_7, @@ -1412,277 +1023,14 @@ mod tests { assert!(matches!(responder.state, LpState::Closed { .. })); } - #[test] - fn test_kkt_exchange_initiator_flow() { - let (init, resp) = mock_peers(); - - let salt = [53u8; 32]; - let receiver_index: u32 = 99901; - - // Create initiator state machine - let mut initiator = LpStateMachine::new( - receiver_index, - true, - init, - resp.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - // Verify initial state - assert!(matches!(initiator.state, LpState::ReadyToHandshake { .. })); - - // Step 1: Initiator starts handshake (should send KKT request) - let init_action = initiator.process_input(LpInput::StartHandshake); - assert!(matches!(init_action, Some(Ok(LpAction::SendPacket(_))))); - assert!(matches!(initiator.state, LpState::KKTExchange { .. })); - } - - #[test] - fn test_kkt_exchange_responder_flow() { - let (init, resp) = mock_peers(); - - let salt = [54u8; 32]; - let receiver_index: u32 = 99902; - - // Create responder state machine - let mut responder = LpStateMachine::new( - receiver_index, - false, - resp, - init.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - // Verify initial state - assert!(matches!(responder.state, LpState::ReadyToHandshake { .. })); - - // Step 1: Responder starts handshake (should transition to KKTExchange without sending) - let resp_action = responder.process_input(LpInput::StartHandshake); - assert!(resp_action.is_none()); - assert!(matches!(responder.state, LpState::KKTExchange { .. })); - } - - #[test] - fn test_kkt_exchange_full_roundtrip() { - // Ed25519 keypairs for PSQ authentication and X25519 derivation - let (init, resp) = mock_peers(); - - let salt = [55u8; 32]; - let receiver_index: u32 = 99903; - - // Create both state machines - let mut initiator = LpStateMachine::new( - receiver_index, - true, - init.clone(), - resp.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - let mut responder = LpStateMachine::new( - receiver_index, - false, - resp, - init.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - // Step 1: Initiator starts handshake, sends KKT request - let init_action = initiator.process_input(LpInput::StartHandshake); - let kkt_request_packet = if let Some(Ok(LpAction::SendPacket(packet))) = init_action { - packet.clone() - } else { - panic!("Initiator should send KKT request"); - }; - assert!(matches!(initiator.state, LpState::KKTExchange { .. })); - - // Step 2: Responder transitions to KKTExchange - let resp_action = responder.process_input(LpInput::StartHandshake); - assert!(resp_action.is_none()); - assert!(matches!(responder.state, LpState::KKTExchange { .. })); - - // Step 3: Responder receives KKT request, sends KKT response - let resp_action = responder.process_input(LpInput::ReceivePacket(kkt_request_packet)); - let kkt_response_packet = if let Some(Ok(LpAction::SendPacket(packet))) = resp_action { - packet.clone() - } else { - panic!("Responder should send KKT response"); - }; - // After sending KKT response, responder moves to Handshaking - assert!(matches!(responder.state, LpState::Handshaking { .. })); - - // Step 4: Initiator receives KKT response, completes KKT - let init_action = initiator.process_input(LpInput::ReceivePacket(kkt_response_packet)); - - assert!(matches!(init_action, Some(Ok(LpAction::KKTComplete)))); - // After KKT complete, initiator moves to Handshaking - assert!(matches!(initiator.state, LpState::Handshaking { .. })); - } - - #[test] - fn test_kkt_exchange_close() { - let (init, resp) = mock_peers(); - - let salt = [56u8; 32]; - let receiver_index: u32 = 99904; - - // Create initiator state machine - let mut initiator = LpStateMachine::new( - receiver_index, - true, - init.clone(), - resp.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - // Start handshake to enter KKTExchange state - initiator.process_input(LpInput::StartHandshake); - assert!(matches!(initiator.state, LpState::KKTExchange { .. })); - - // Close during KKT exchange - let close_action = initiator.process_input(LpInput::Close); - assert!(matches!(close_action, Some(Ok(LpAction::ConnectionClosed)))); - assert!(matches!(initiator.state, LpState::Closed { .. })); - } - - #[test] - fn test_kkt_exchange_rejects_invalid_inputs() { - let (init, resp) = mock_peers(); - - let salt = [57u8; 32]; - let receiver_index: u32 = 99905; - - // Create initiator state machine - let mut initiator = LpStateMachine::new( - receiver_index, - true, - init.clone(), - resp.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - // Start handshake to enter KKTExchange state - initiator.process_input(LpInput::StartHandshake); - assert!(matches!(initiator.state, LpState::KKTExchange { .. })); - - // Try SendData during KKT exchange (should be rejected) - let send_data = LpData::new_opaque(vec![1, 2, 3]); - let send_action = initiator.process_input(LpInput::SendData(send_data)); - assert!(matches!( - send_action, - Some(Err(LpError::InvalidStateTransition { .. })) - )); - assert!(matches!(initiator.state, LpState::KKTExchange { .. })); // Still in KKTExchange - - // Try StartHandshake again during KKT exchange (should be rejected) - let start_action = initiator.process_input(LpInput::StartHandshake); - assert!(matches!( - start_action, - Some(Err(LpError::InvalidStateTransition { .. })) - )); - assert!(matches!(initiator.state, LpState::KKTExchange { .. })); // Still in KKTExchange - } - /// Helper function to complete a full handshake between initiator and responder, /// returning both in Transport state ready for subsession testing. fn setup_transport_sessions() -> (LpStateMachine, LpStateMachine) { - let (a, b) = mock_peers(); - - let salt = [60u8; 32]; - let receiver_index: u32 = 111111; - - // Create state machines - Alice is initiator, Bob is responder - let mut alice = LpStateMachine::new( - receiver_index, - true, - a.clone(), - b.as_remote(), - &salt, - version::CURRENT, + let sessions = SessionsMock::mock_post_handshake(12345); + ( + LpStateMachine::new(sessions.initiator), + LpStateMachine::new(sessions.responder), ) - .unwrap(); - - let mut bob = LpStateMachine::new( - receiver_index, - false, - b, - a.as_remote(), - &salt, - version::CURRENT, - ) - .unwrap(); - - // --- Complete KKT Exchange --- - // Alice starts handshake - let kkt_request = if let Some(Ok(LpAction::SendPacket(p))) = - alice.process_input(LpInput::StartHandshake) - { - p - } else { - panic!("Alice should send KKT request"); - }; - - // Bob starts handshake - let _ = bob.process_input(LpInput::StartHandshake); - - // Bob receives KKT request, sends response - let kkt_response = if let Some(Ok(LpAction::SendPacket(p))) = - bob.process_input(LpInput::ReceivePacket(kkt_request)) - { - p - } else { - panic!("Bob should send KKT response"); - }; - - // Alice receives KKT response - let _ = alice.process_input(LpInput::ReceivePacket(kkt_response)); - - // --- Complete Noise Handshake --- - // Alice prepares first Noise message - let noise1_msg = alice - .session() - .unwrap() - .prepare_handshake_message() - .unwrap() - .unwrap(); - let noise1_packet = alice.session().unwrap().next_packet(noise1_msg).unwrap(); - - // Bob receives noise1, sends noise2 - let noise2_packet = if let Some(Ok(LpAction::SendPacket(p))) = - bob.process_input(LpInput::ReceivePacket(noise1_packet)) - { - p - } else { - panic!("Bob should send Noise packet 2"); - }; - - // Alice receives noise2, sends noise3 - let noise3_packet = if let Some(Ok(LpAction::SendPacket(p))) = - alice.process_input(LpInput::ReceivePacket(noise2_packet)) - { - p - } else { - panic!("Alice should send Noise packet 3"); - }; - assert!(matches!(alice.state, LpState::Transport { .. })); - - // Bob receives noise3, completes handshake - let _ = bob.process_input(LpInput::ReceivePacket(noise3_packet)); - assert!(matches!(bob.state, LpState::Transport { .. })); - - (alice, bob) } #[test] diff --git a/common/test-utils/Cargo.toml b/common/test-utils/Cargo.toml index f89073afc7..ab509eb8f9 100644 --- a/common/test-utils/Cargo.toml +++ b/common/test-utils/Cargo.toml @@ -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"] } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index e912053e34..ca508afdac 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -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 diff --git a/gateway/src/node/lp_listener/data_handler.rs b/gateway/src/node/lp_listener/data_handler.rs index 2b2ce11c64..cbeb46b058 100644 --- a/gateway/src/node/lp_listener/data_handler.rs +++ b/gateway/src/node/lp_listener/data_handler.rs @@ -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)) })?; diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs index c29f9298f1..c65efc8443 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/gateway/src/node/lp_listener/handler.rs @@ -1,20 +1,21 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use super::{LpHandlerState, ReceiverIndex}; +use super::{LpHandlerState, ReceiverIndex, TimestampedState}; use crate::error::GatewayError; +use bytes::BytesMut; use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_lp::codec::serialize_lp_packet; use nym_lp::state_machine::{LpAction, LpData, LpDataKind, LpInput}; use nym_lp::{ - codec::OuterAeadKey, message::ForwardPacketData, packet::LpHeader, LpMessage, LpPacket, - OuterHeader, + message::ForwardPacketData, LpMessage, LpPacket, LpSession, LpStateMachine, OuterHeader, }; use nym_lp_transport::traits::LpTransport; use nym_metrics::{add_histogram_obs, inc}; use nym_registration_common::{LpRegistrationRequest, RegistrationStatus}; use std::net::SocketAddr; use std::time::Duration; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::timeout; use tracing::*; @@ -79,6 +80,7 @@ pub struct LpConnectionHandler { remote_addr: SocketAddr, state: LpHandlerState, stats: ConnectionStats, + /// Bound receiver_idx for this connection (set after first packet). /// All subsequent packets on this connection must use this receiver_idx. /// Set from ClientHello's proposed receiver_index, or from header for non-bootstrap packets. @@ -92,7 +94,7 @@ pub struct LpConnectionHandler { impl LpConnectionHandler where - S: LpTransport + Unpin, + S: LpTransport + AsyncRead + AsyncWrite + Unpin, { pub fn new(stream: S, remote_addr: SocketAddr, state: LpHandlerState) -> Self { Self { @@ -122,6 +124,49 @@ where // State persists in LpHandlerState maps across packets // ============================================================ + // 1. complete KKT/PSQ handshake before doing anything else. + // bail if it takes too long + let timeout = self.state.lp_config.debug.handshake_ttl; + let local_peer = self.state.local_lp_peer.clone(); + let stream = &mut self.stream; + + // TODO: + let ciphersuite = LpSession::default_ciphersuite(); + let session = match tokio::time::timeout(timeout, async move { + LpSession::psq_handshake_responder(stream, ciphersuite, local_peer) + .complete_as_responder() + .await + }) + .await + { + Err(_timeout) => { + debug!( + "timed out attempting to complete KTT/PSQ handshake with {}", + self.remote_addr + ); + self.emit_lifecycle_metrics(false); + return Ok(()); + } + Ok(Err(handshake_failure)) => { + debug!( + "failed to complete KKT/PSQ handshake with {}: {handshake_failure}", + self.remote_addr + ); + self.emit_lifecycle_metrics(false); + return Ok(()); + } + Ok(Ok(session)) => session, + }; + let receiver_idx = session.id(); + + // 2. insert the state machine into the shared state + let state_machine = LpStateMachine::new(session); + self.state + .session_states + .insert(receiver_idx, TimestampedState::new(state_machine)); + self.bound_receiver_idx = Some(receiver_idx); + + // 3. handle any new incoming packet loop { // Step 1: Receive raw packet bytes and parse header only (for routing) let (raw_bytes, header) = match self.receive_raw_packet().await { @@ -140,9 +185,8 @@ where let receiver_idx = header.receiver_idx; - // Step 2: Validate or set binding (session-affine connection) - // Note: ClientHello (receiver_idx=0) defers binding to handle_client_hello() - if let Err(e) = self.validate_or_set_binding(receiver_idx) { + // Step 2: Validate the binding + if let Err(e) = self.validate_binding(receiver_idx) { self.emit_lifecycle_metrics(false); return Err(e); } @@ -158,6 +202,14 @@ where Ok(()) } + fn bound_receiver_index(&self) -> Result { + self.bound_receiver_idx.ok_or_else(|| { + GatewayError::LpProtocolError( + "missing bound receiver index after KKT/PSQ handshake".into(), + ) + }) + } + /// Check if an error indicates the connection was closed (EOF). /// AIDEV-NOTE: Uses string matching on error messages. Tokio's read_exact /// returns UnexpectedEof which gets formatted into the error message. @@ -172,43 +224,28 @@ where } } - /// Validate that the receiver_idx matches the bound session, or set binding if first packet. + /// Validate that the receiver_idx matches the bound session. /// /// Binding rules: /// - ClientHello (receiver_idx=0): binding deferred to handle_client_hello() which /// extracts receiver_index from payload /// - First non-bootstrap packet: sets binding from header's receiver_idx /// - Subsequent packets: must match bound receiver_idx - fn validate_or_set_binding(&mut self, receiver_idx: u32) -> Result<(), GatewayError> { - match self.bound_receiver_idx { - None => { - // First packet - don't bind if bootstrap (handle_client_hello sets binding) - if receiver_idx != nym_lp::BOOTSTRAP_RECEIVER_IDX { - self.bound_receiver_idx = Some(receiver_idx); - trace!( - "Bound connection from {} to receiver_idx={}", - self.remote_addr, - receiver_idx - ); - } - Ok(()) - } - Some(bound) => { - if receiver_idx == bound { - Ok(()) - } else { - warn!( - "Receiver_idx mismatch from {}: expected {}, got {}", - self.remote_addr, bound, receiver_idx - ); - inc!("lp_errors_receiver_idx_mismatch"); - Err(GatewayError::LpProtocolError(format!( - "receiver_idx mismatch: connection bound to {}, packet has {}", - bound, receiver_idx - ))) - } - } + fn validate_binding(&self, receiver_idx: u32) -> Result<(), GatewayError> { + let bound_receiver_idx = self.bound_receiver_index()?; + + if bound_receiver_idx != receiver_idx { + warn!( + "Receiver_idx mismatch from {}: expected {bound_receiver_idx}, got {receiver_idx}", + self.remote_addr + ); + inc!("lp_errors_receiver_idx_mismatch"); + return Err(GatewayError::LpProtocolError(format!( + "receiver_idx mismatch: connection bound to {bound_receiver_idx}, packet has {receiver_idx}", + ))); } + + Ok(()) } /// Process a single packet: lookup session, parse, route to handler. @@ -220,287 +257,35 @@ where ) -> Result<(), GatewayError> { // Get outer_aead_key based on receiver_idx // Header is always cleartext for routing. Payload is encrypted after PSK. - let outer_key: Option = if receiver_idx == nym_lp::BOOTSTRAP_RECEIVER_IDX { - // ClientHello - no encryption (PSK not yet derived) - None - } else if let Some(state_entry) = self.state.handshake_states.get(&receiver_idx) { - // Handshake in progress - check if PSK has been injected yet - state_entry - .value() - .state - .session() - .ok() - .and_then(|session| session.outer_aead_key()) - } else if let Some(session_entry) = self.state.session_states.get(&receiver_idx) { - // Established session - should always have PSK - session_entry - .value() - .state - .session() - .ok() - .and_then(|s| s.outer_aead_key()) - } else { - // Unknown session - will error during routing, parse cleartext - None + let Some(state_machine) = self.state.session_states.get(&receiver_idx) else { + // session might have gotten removed due to inactivity + return Err(GatewayError::LpConnectionError(format!( + "missing session state for {receiver_idx} - has it been removed due to inactivity?" + ))); }; + let outer_key = state_machine + .state + .session() + .map_err(|err| GatewayError::LpProtocolError(err.to_string()))? + .outer_aead_key(); + // Parse full packet with outer AEAD key - let packet = - nym_lp::codec::parse_lp_packet(&raw_bytes, outer_key.as_ref()).map_err(|e| { - inc!("lp_errors_parse_packet"); - GatewayError::LpProtocolError(format!("Failed to parse LP packet: {}", e)) - })?; + let packet = nym_lp::codec::parse_lp_packet(&raw_bytes, Some(outer_key)).map_err(|e| { + inc!("lp_errors_parse_packet"); + GatewayError::LpProtocolError(format!("Failed to parse LP packet: {e}")) + })?; + + drop(state_machine); trace!( - "Received packet from {} (receiver_idx={}, counter={}, encrypted={})", + "Received packet from {} (receiver_idx={}, counter={})", self.remote_addr, receiver_idx, packet.header().counter, - outer_key.is_some() ); - // Route packet based on receiver_idx - if receiver_idx == nym_lp::BOOTSTRAP_RECEIVER_IDX { - // ClientHello - first packet in handshake - self.handle_client_hello(packet).await - } else { - // Check if this is an in-progress handshake or established session - if self.state.handshake_states.contains_key(&receiver_idx) { - // Handshake in progress - self.handle_handshake_packet(receiver_idx, packet).await - } else if self.state.session_states.contains_key(&receiver_idx) { - // Established session - transport mode - self.handle_transport_packet(receiver_idx, packet).await - } else { - // Unknown session - possibly stale or client error - warn!( - "Received packet for unknown session {} from {}", - receiver_idx, self.remote_addr - ); - inc!("lp_errors_unknown_session"); - Err(GatewayError::LpProtocolError(format!( - "Unknown session ID: {}", - receiver_idx - ))) - } - } - } - - /// Handle ClientHello packet (receiver_idx=0, first packet) - async fn handle_client_hello(&mut self, packet: LpPacket) -> Result<(), GatewayError> { - use nym_lp::packet::LpHeader; - use nym_lp::state_machine::{LpInput, LpStateMachine}; - let remote = self.remote_addr; - - // if we managed to parse out the header, i.e. we have code supporting whatever - // protocol version has been sent - use that one instead - let protocol_version = packet.header().protocol_version; - - // Extract ClientHello data - let hello_data = match packet.message() { - LpMessage::ClientHello(hello_data) => hello_data, - other => { - inc!("lp_client_hello_failed"); - return Err(GatewayError::LpProtocolError(format!( - "Expected ClientHello, got {other}", - ))); - } - }; - - // Validate timestamp - let timestamp = hello_data.extract_timestamp(); - Self::validate_timestamp(timestamp, self.state.lp_config.debug.timestamp_tolerance)?; - - // Extract client-proposed receiver_index - let receiver_index = hello_data.receiver_index; - - // let client_ed25519_pubkey = hello_data.client_ed25519_public_key; - - debug!("Processing ClientHello from {remote} (proposed receiver_index={receiver_index})",); - - // Collision check for client-proposed receiver_index - // Check both handshake_states (in-progress) and session_states (established) - if self.state.handshake_states.contains_key(&receiver_index) - || self.state.session_states.contains_key(&receiver_index) - { - warn!("Receiver index collision: {receiver_index} from {remote}",); - inc!("lp_receiver_index_collision"); - - // Send Collision response to tell client to retry with new receiver_index - // No outer key - this is before PSK derivation - // Note: Do NOT set binding on collision - client may retry with new receiver_index - let collision_packet = LpPacket::new( - LpHeader::new(receiver_index, 0, protocol_version), - LpMessage::Collision, - ); - self.send_lp_packet(collision_packet, None).await?; - - return Ok(()); - } - - // Collision check passed - bind this connection to the receiver_index - // All subsequent packets on this connection must use this receiver_index - self.bound_receiver_idx = Some(receiver_index); - trace!("Bound connection from {remote} to receiver_idx={receiver_index} (via ClientHello)",); - - // Create state machine for this handshake using client-proposed receiver_index - let mut state_machine = LpStateMachine::new( - receiver_index, - false, // responder - self.state.local_lp_peer.clone(), - hello_data.to_remote_peer(), - &hello_data.salt, - protocol_version, - ) - .map_err(|e| { - inc!("lp_client_hello_failed"); - GatewayError::LpHandshakeError(format!("Failed to create state machine: {}", e)) - })?; - - debug!("Created handshake state for {remote} (receiver_index={receiver_index})",); - - // Transition state machine to KKTExchange (responder waits for client's KKT request) - // For responder, StartHandshake returns None (just transitions state) - // For initiator, StartHandshake returns SendPacket (KKT request) - if let Some(Err(e)) = state_machine.process_input(LpInput::StartHandshake) { - inc!("lp_client_hello_failed"); - return Err(GatewayError::LpHandshakeError(format!( - "StartHandshake failed: {e}", - ))); - // Responder (gateway) gets Ok but no packet to send - we just wait for client's next packet - } - - // Store state machine for subsequent handshake packets (KKT request with receiver_index=X) - self.state - .handshake_states - .insert(receiver_index, super::TimestampedState::new(state_machine)); - - debug!( - "Stored handshake state for {remote} (receiver_index={receiver_index}) - waiting for KKT request", - ); - - // Send Ack to confirm ClientHello received - // No outer key - this is before PSK derivation - let ack_packet = LpPacket::new( - LpHeader::new(receiver_index, 0, protocol_version), - LpMessage::Ack, - ); - self.send_lp_packet(ack_packet, None).await?; - - Ok(()) - } - - /// Handle handshake packet (receiver_idx!=0, handshake not complete) - async fn handle_handshake_packet( - &mut self, - receiver_idx: u32, - packet: LpPacket, - ) -> Result<(), GatewayError> { - debug!( - "Processing handshake packet from {} (receiver_idx={})", - self.remote_addr, receiver_idx - ); - - // Get mutable reference to state machine - let mut state_entry = self - .state - .handshake_states - .get_mut(&receiver_idx) - .ok_or_else(|| { - GatewayError::LpProtocolError(format!( - "Handshake state not found for session {}", - receiver_idx - )) - })?; - - let state_machine = &mut state_entry.value_mut().state; - - // Process packet through state machine - let action = state_machine - .process_input(LpInput::ReceivePacket(packet)) - .ok_or_else(|| { - GatewayError::LpHandshakeError("State machine returned no action".to_string()) - })? - .map_err(|e| GatewayError::LpHandshakeError(format!("Handshake error: {}", e)))?; - - // Get outer_aead_key from session (if PSK has been derived) - // PSK is derived after Noise msg 1 processing, so msg 2+ are encrypted - let should_send = match action { - LpAction::SendPacket(response_packet) => { - // Get key before dropping borrow - let outer_key = state_machine - .session() - .ok() - .and_then(|s| s.outer_aead_key()); - drop(state_entry); // Release borrow before send - Some((response_packet, outer_key)) - } - LpAction::HandshakeComplete => { - info!( - "Handshake completed for {} (receiver_idx={})", - self.remote_addr, receiver_idx - ); - - let session = state_entry.value().state.session().map_err(|err| { - GatewayError::LpHandshakeError(format!("no session available: {err}")) - })?; - - // Get outer key for Ack encryption before releasing borrow - let outer_key = session.outer_aead_key(); - - // Get previously negotiated protocol version for header creation - let negotiated_version = session.negotiated_version(); - - // Move state machine to session_states (already in Transport state) - // We keep the state machine (not just session) to enable - // subsession/rekeying support during transport phase - drop(state_entry); // Release mutable borrow - - let (_receiver_idx, timestamped_state) = self - .state - .handshake_states - .remove(&receiver_idx) - .ok_or_else(|| { - GatewayError::LpHandshakeError( - "Failed to remove handshake state".to_string(), - ) - })?; - - self.state - .session_states - .insert(receiver_idx, timestamped_state); - - inc!("lp_handshakes_success"); - - // Send Ack to confirm handshake completion to the client - let ack_packet = LpPacket::new( - LpHeader::new(receiver_idx, 0, negotiated_version), - LpMessage::Ack, - ); - trace!( - "Moved session {} to transport mode, sending Ack", - receiver_idx - ); - Some((ack_packet, outer_key)) - } - other => { - debug!("Received action during handshake: {:?}", other); - drop(state_entry); - None - } - }; - - // Send response packet if needed - if let Some((packet, outer_key)) = should_send { - self.send_lp_packet(packet, outer_key.as_ref()).await?; - trace!( - "Sent handshake response to {} (encrypted={})", - self.remote_addr, - outer_key.is_some() - ); - } - - Ok(()) + self.handle_transport_packet(receiver_idx, packet).await } /// Handle transport packet (receiver_idx!=0, session established) @@ -520,12 +305,8 @@ where receiver_idx: u32, packet: LpPacket, ) -> Result<(), GatewayError> { - use nym_lp::state_machine::{LpAction, LpInput}; - - debug!( - "Processing transport packet from {} (receiver_idx={})", - self.remote_addr, receiver_idx - ); + let remote = self.remote_addr; + debug!("Processing transport packet from {remote} (receiver_idx={receiver_idx})",); // Get state machine and process packet let mut state_entry = self @@ -533,7 +314,7 @@ where .session_states .get_mut(&receiver_idx) .ok_or_else(|| { - GatewayError::LpProtocolError(format!("Session not found: {}", receiver_idx)) + GatewayError::LpProtocolError(format!("Session not found: {receiver_idx}")) })?; // Update last activity timestamp @@ -547,31 +328,33 @@ where .ok_or_else(|| { GatewayError::LpProtocolError("No action from state machine".to_string()) })? - .map_err(|e| GatewayError::LpProtocolError(format!("State machine error: {}", e)))?; + .map_err(|e| GatewayError::LpProtocolError(format!("State machine error: {e}")))?; let lp_session = state_machine.session().map_err(|e| { - GatewayError::LpProtocolError(format!("Session unavailable after processing: {}", e)) + GatewayError::LpProtocolError(format!("Session unavailable after processing: {e}")) })?; // Get outer key before releasing borrow let outer_key = lp_session.outer_aead_key(); - drop(state_entry); match action { LpAction::SendPacket(response_packet) => { // Subsession KK2 response - gateway is responder // This means we received SubsessionKK1 and are responding - debug!( - "Sending subsession KK2 response to {} (receiver_idx={})", - self.remote_addr, receiver_idx - ); + debug!("Sending subsession KK2 response to {remote} (receiver_idx={receiver_idx})",); inc!("lp_subsession_kk2_sent"); - self.send_lp_packet(response_packet, outer_key.as_ref()) - .await?; + let mut packet_buf = BytesMut::new(); + serialize_lp_packet(&response_packet, &mut packet_buf, Some(outer_key)).map_err( + |e| GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e)), + )?; + + drop(state_entry); + self.send_serialised_packet(&packet_buf).await?; Ok(()) } LpAction::DeliverData(data) => { // Decrypted application data - process as registration/forwarding + drop(state_entry); self.handle_decrypted_payload(receiver_idx, data).await } LpAction::SubsessionComplete { @@ -580,23 +363,29 @@ where new_receiver_index, } => { // Subsession complete - promote to new session - self.handle_subsession_complete( - receiver_idx, - ready_packet, - *subsession, - new_receiver_index, - outer_key, - ) - .await + + // Send SubsessionReady packet if present (for initiator - gateway is responder, so typically None) + if let Some(ready_packet) = ready_packet { + let mut packet_buf = BytesMut::new(); + serialize_lp_packet(&ready_packet, &mut packet_buf, Some(outer_key)).map_err( + |e| { + GatewayError::LpProtocolError(format!( + "Failed to serialize packet: {e}", + )) + }, + )?; + drop(state_entry); + self.send_serialised_packet(&packet_buf).await?; + } else { + drop(state_entry); + } + self.handle_subsession_complete(receiver_idx, *subsession, new_receiver_index) + .await } other => { - warn!( - "Unexpected action in transport from {}: {:?}", - self.remote_addr, other - ); + warn!("Unexpected action in transport from {remote}: {other:?}",); Err(GatewayError::LpProtocolError(format!( - "Unexpected action: {:?}", - other + "Unexpected action: {other:?}", ))) } } @@ -653,10 +442,8 @@ where async fn handle_subsession_complete( &mut self, old_receiver_idx: u32, - ready_packet: Option, subsession: nym_lp::session::SubsessionHandshake, new_receiver_index: u32, - outer_key: Option, ) -> Result<(), GatewayError> { use nym_lp::state_machine::LpStateMachine; @@ -665,29 +452,18 @@ where self.remote_addr, old_receiver_idx, new_receiver_index ); - // Send SubsessionReady packet if present (for initiator - gateway is responder, so typically None) - if let Some(packet) = ready_packet { - self.send_lp_packet(packet, outer_key.as_ref()).await?; - } - // Create new state machine from completed subsession let new_state_machine = LpStateMachine::from_subsession(subsession, new_receiver_index) .map_err(|e| { GatewayError::LpProtocolError(format!( - "Failed to create session from subsession: {}", - e + "Failed to create session from subsession: {e}", )) })?; // Check for receiver_index collision before inserting // new_receiver_index is client-generated (rand::random() in state machine). // Collisions are statistically unlikely (1 in 4 billion) but could cause DoS if exploited. - if self.state.session_states.contains_key(&new_receiver_index) - || self - .state - .handshake_states - .contains_key(&new_receiver_index) - { + if self.state.session_states.contains_key(&new_receiver_index) { warn!( "Subsession receiver_index collision: {} from {}", new_receiver_index, self.remote_addr @@ -718,19 +494,19 @@ where serialised_response: Vec, response_kind: LpDataKind, ) -> Result<(), GatewayError> { - let session_entry = self + let mut session_entry = self .state .session_states - .get(&receiver_idx) + .get_mut(&receiver_idx) .ok_or_else(|| { GatewayError::LpProtocolError(format!("Session not found: {receiver_idx}")) })?; // Access session via state machine for subsession support let session = session_entry - .value() + .value_mut() .state - .session() + .session_mut() .map_err(|e| GatewayError::LpProtocolError(format!("Session error: {e}")))?; let wrapped_lp_data = LpData::new(response_kind, serialised_response); @@ -745,11 +521,17 @@ where })?; let outer_key = session.outer_aead_key(); + + // make sure to drop the entry before the .await call + // Serialize the packet (encrypted if outer_key provided) + let mut packet_buf = BytesMut::new(); + serialize_lp_packet(&response_packet, &mut packet_buf, Some(outer_key)).map_err(|e| { + GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e)) + })?; drop(session_entry); // Send response (encrypted with outer AEAD) - self.send_lp_packet(response_packet, outer_key.as_ref()) - .await?; + self.send_serialised_packet(&packet_buf).await?; Ok(()) } @@ -1163,42 +945,54 @@ where Ok((packet_buf, header)) } - /// Send an LP packet over the stream with proper length-prefixed framing. - /// - /// # Arguments - /// * `packet` - The LP packet to send - /// * `outer_key` - Optional outer AEAD key for encryption (None for cleartext, Some for encrypted) - async fn send_lp_packet( - &mut self, - packet: LpPacket, - outer_key: Option<&OuterAeadKey>, - ) -> Result<(), GatewayError> { - use bytes::BytesMut; - use nym_lp::codec::serialize_lp_packet; - - // Serialize the packet (encrypted if outer_key provided) - let mut packet_buf = BytesMut::new(); - serialize_lp_packet(&packet, &mut packet_buf, outer_key).map_err(|e| { - GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e)) - })?; - - // Send 4-byte length prefix (u32 big-endian) - let len = packet_buf.len() as u32; + /// Send a serialised LP packet over the stream with proper length-prefixed framing. + async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> Result<(), GatewayError> { self.stream - .write_all(&len.to_be_bytes()) + .send_serialised_packet(packet_data) .await - .map_err(|e| { - GatewayError::LpConnectionError(format!("Failed to send packet length: {}", e)) + .map_err(|err| { + GatewayError::LpConnectionError(format!("failed to send LP packet: {err}")) })?; - // Send the actual packet data - self.stream.write_all(&packet_buf).await.map_err(|e| { - GatewayError::LpConnectionError(format!("Failed to send packet data: {}", e)) + // Track bytes sent (4 byte header + packet data) + self.stats.record_bytes_sent(4 + packet_data.len()); + + Ok(()) + } + + // only used in tests + #[cfg(test)] + async fn send_lp_packet(&mut self, packet: LpPacket) -> Result<(), GatewayError> { + let receiver_idx = self.bound_receiver_index()?; + + let mut session_entry = self + .state + .session_states + .get_mut(&receiver_idx) + .ok_or_else(|| { + GatewayError::LpProtocolError(format!("Session not found: {receiver_idx}")) + })?; + + // Access session via state machine for subsession support + let session = session_entry + .value_mut() + .state + .session_mut() + .map_err(|e| GatewayError::LpProtocolError(format!("Session error: {e}")))?; + + let outer_key = session.outer_aead_key(); + + let mut packet_buf = BytesMut::new(); + serialize_lp_packet(&packet, &mut packet_buf, Some(outer_key)).map_err(|e| { + GatewayError::LpProtocolError(format!("Failed to serialize packet: {e}",)) })?; - self.stream.flush().await.map_err(|e| { - GatewayError::LpConnectionError(format!("Failed to flush stream: {}", e)) - })?; + self.stream + .send_serialised_packet(&packet_buf) + .await + .map_err(|err| { + GatewayError::LpConnectionError(format!("failed to send LP packet: {err}")) + })?; // Track bytes sent (4 byte header + packet data) self.stats.record_bytes_sent(4 + packet_buf.len()); @@ -1243,12 +1037,12 @@ mod tests { use crate::node::lp_listener::{LpConfig, LpDebug}; use crate::node::ActiveClientsStore; use bytes::BytesMut; - use nym_lp::codec::{parse_lp_packet, serialize_lp_packet}; - use nym_lp::message::{ClientHelloData, EncryptedDataPayload, HandshakeData, LpMessage}; + use nym_lp::codec::{parse_lp_packet, serialize_lp_packet, OuterAeadKey}; + use nym_lp::message::{EncryptedDataPayload, LpMessage}; use nym_lp::packet::{LpHeader, LpPacket}; use nym_lp::peer::LpLocalPeer; + use nym_lp::SessionsMock; use std::sync::Arc; - use std::time::{SystemTime, UNIX_EPOCH}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt}; // ==================== Test Helpers ==================== @@ -1294,13 +1088,23 @@ mod tests { metrics: nym_node_metrics::NymNodeMetrics::default(), active_clients_store: ActiveClientsStore::new(), outbound_mix_sender: mix_sender, - handshake_states: Arc::new(dashmap::DashMap::new()), session_states: Arc::new(dashmap::DashMap::new()), forward_semaphore, peer_registrator: None, } } + fn add_dummy_lp_state(handler: &mut LpConnectionHandler, session: LpSession) { + let id = session.id(); + let state_machine = LpStateMachine::new(session); + handler.bound_receiver_idx = Some(id); + + handler + .state + .session_states + .insert(id, TimestampedState::new(state_machine)); + } + /// Helper to write an LP packet to a stream with proper framing async fn write_lp_packet_to_stream( stream: &mut W, @@ -1324,6 +1128,7 @@ mod tests { /// Helper to read an LP packet from a stream with proper framing async fn read_lp_packet_from_stream( stream: &mut R, + outer_aead_key: &OuterAeadKey, ) -> Result { // Read length prefix let mut len_buf = [0u8; 4]; @@ -1335,7 +1140,8 @@ mod tests { stream.read_exact(&mut packet_buf).await?; // Parse packet - parse_lp_packet(&packet_buf, None).map_err(|e| std::io::Error::other(e.to_string())) + parse_lp_packet(&packet_buf, Some(outer_aead_key)) + .map_err(|e| std::io::Error::other(e.to_string())) } // ==================== Existing Tests ==================== @@ -1534,21 +1340,27 @@ mod tests { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); + let receiver_idx = 99; + let sessions = SessionsMock::mock_post_handshake(receiver_idx); + let init = sessions.initiator; + let resp = sessions.responder; + let server_task = tokio::spawn(async move { let (stream, remote_addr) = listener.accept().await.unwrap(); let state = create_minimal_test_state().await; let mut handler = LpConnectionHandler::new(stream, remote_addr, state); + add_dummy_lp_state(&mut handler, resp); let packet = LpPacket::new( LpHeader { protocol_version: 1, reserved: [0u8; 3], - receiver_idx: 99, + receiver_idx, counter: 5, }, LpMessage::Busy, ); - handler.send_lp_packet(packet, None).await + handler.send_lp_packet(packet).await }); let mut client_stream = TcpStream::connect(addr).await.unwrap(); @@ -1557,54 +1369,13 @@ mod tests { server_task.await.unwrap().unwrap(); // Client should receive it correctly - let received = read_lp_packet_from_stream(&mut client_stream) + let received = read_lp_packet_from_stream(&mut client_stream, init.outer_aead_key()) .await .unwrap(); - assert_eq!(received.header().receiver_idx, 99); + assert_eq!(received.header().receiver_idx, receiver_idx); assert_eq!(received.header().counter, 5); } - #[tokio::test] - async fn test_send_receive_handshake_message() { - use tokio::net::{TcpListener, TcpStream}; - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let handshake_data = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let expected_data = handshake_data.clone(); - - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - - let packet = LpPacket::new( - LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 100, - counter: 10, - }, - LpMessage::Handshake(HandshakeData(handshake_data)), - ); - handler.send_lp_packet(packet, None).await - }); - - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - server_task.await.unwrap().unwrap(); - - let received = read_lp_packet_from_stream(&mut client_stream) - .await - .unwrap(); - assert_eq!(received.header().receiver_idx, 100); - assert_eq!(received.header().counter, 10); - match received.message() { - LpMessage::Handshake(data) => assert_eq!(data, &HandshakeData(expected_data)), - _ => panic!("Expected Handshake message"), - } - } - #[tokio::test] async fn test_send_receive_encrypted_data_message() { use tokio::net::{TcpListener, TcpStream}; @@ -1612,6 +1383,11 @@ mod tests { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); + let receiver_idx = 200; + let sessions = SessionsMock::mock_post_handshake(receiver_idx); + let init = sessions.initiator; + let resp = sessions.responder; + let encrypted_payload = vec![42u8; 256]; let expected_payload = encrypted_payload.clone(); @@ -1619,23 +1395,24 @@ mod tests { let (stream, remote_addr) = listener.accept().await.unwrap(); let state = create_minimal_test_state().await; let mut handler = LpConnectionHandler::new(stream, remote_addr, state); + add_dummy_lp_state(&mut handler, resp); let packet = LpPacket::new( LpHeader { protocol_version: 1, reserved: [0u8; 3], - receiver_idx: 200, + receiver_idx, counter: 20, }, LpMessage::EncryptedData(EncryptedDataPayload(encrypted_payload)), ); - handler.send_lp_packet(packet, None).await + handler.send_lp_packet(packet).await }); let mut client_stream = TcpStream::connect(addr).await.unwrap(); server_task.await.unwrap().unwrap(); - let received = read_lp_packet_from_stream(&mut client_stream) + let received = read_lp_packet_from_stream(&mut client_stream, init.outer_aead_key()) .await .unwrap(); assert_eq!(received.header().receiver_idx, 200); @@ -1647,199 +1424,4 @@ mod tests { _ => panic!("Expected EncryptedData message"), } } - - #[tokio::test] - async fn test_send_receive_client_hello_message() { - use nym_lp::message::ClientHelloData; - use tokio::net::{TcpListener, TcpStream}; - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let mut rng = rand::thread_rng(); - let ed25519 = ed25519::KeyPair::new(&mut rng); - let x25519 = ed25519.to_x25519(); - - let client_key = *x25519.public_key(); - let client_ed25519_key = *ed25519.public_key(); - - let hello_data = - ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp); - let expected_salt = hello_data.salt; // Clone salt before moving hello_data - - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - - let packet = LpPacket::new( - LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 300, - counter: 30, - }, - LpMessage::ClientHello(hello_data), - ); - handler.send_lp_packet(packet, None).await - }); - - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - server_task.await.unwrap().unwrap(); - - let received = read_lp_packet_from_stream(&mut client_stream) - .await - .unwrap(); - assert_eq!(received.header().receiver_idx, 300); - assert_eq!(received.header().counter, 30); - match received.message() { - LpMessage::ClientHello(data) => { - assert_eq!(data.client_lp_public_key, client_key); - assert_eq!(data.salt, expected_salt); - } - _ => panic!("Expected ClientHello message"), - } - } - - // ==================== receive_client_hello Tests ==================== - - #[tokio::test] - async fn test_receive_client_hello_valid() { - use tokio::net::{TcpListener, TcpStream}; - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - handler.receive_client_hello().await - }); - - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - - // Create and send valid ClientHello - // Create separate Ed25519 keypair and derive X25519 from it (like production code) - use nym_crypto::asymmetric::ed25519; - use rand::rngs::OsRng; - - let client_ed25519_keypair = ed25519::KeyPair::new(&mut OsRng); - let client_x25519_public = client_ed25519_keypair.public_key().to_x25519().unwrap(); - - let hello_data = ClientHelloData::new_with_fresh_salt( - client_x25519_public, - *client_ed25519_keypair.public_key(), - timestamp, - ); - let packet = LpPacket::new( - LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 0, - counter: 0, - }, - LpMessage::ClientHello(hello_data.clone()), - ); - write_lp_packet_to_stream(&mut client_stream, &packet) - .await - .unwrap(); - - // Handler should receive and parse it - let result = server_task.await.unwrap(); - assert!(result.is_ok(), "Expected Ok, got: {:?}", result); - - let (x25519_pubkey, ed25519_pubkey, salt) = result.unwrap(); - assert_eq!(x25519_pubkey.as_bytes(), &client_x25519_public.to_bytes()); - assert_eq!( - ed25519_pubkey.to_bytes(), - client_ed25519_keypair.public_key().to_bytes() - ); - assert_eq!(salt, hello_data.salt); - } - - #[tokio::test] - async fn test_receive_client_hello_timestamp_too_old() { - use std::time::{SystemTime, UNIX_EPOCH}; - use tokio::net::{TcpListener, TcpStream}; - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("System time before UNIX epoch") - .as_secs(); - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let server_task = tokio::spawn(async move { - let (stream, remote_addr) = listener.accept().await.unwrap(); - let state = create_minimal_test_state().await; - let mut handler = LpConnectionHandler::new(stream, remote_addr, state); - handler.receive_client_hello().await - }); - - let mut client_stream = TcpStream::connect(addr).await.unwrap(); - - // Create ClientHello with old timestamp - // Use proper separate Ed25519 and X25519 keys (like production code) - use nym_crypto::asymmetric::ed25519; - use rand::rngs::OsRng; - - let client_ed25519_keypair = ed25519::KeyPair::new(&mut OsRng); - let client_x25519_public = client_ed25519_keypair.public_key().to_x25519().unwrap(); - - let mut hello_data = ClientHelloData::new_with_fresh_salt( - client_x25519_public, - *client_ed25519_keypair.public_key(), - timestamp, - ); - - // Manually set timestamp to be very old (100 seconds ago) - let old_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - - 100; - hello_data.salt[..8].copy_from_slice(&old_timestamp.to_le_bytes()); - - let packet = LpPacket::new( - LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 0, - counter: 0, - }, - LpMessage::ClientHello(hello_data), - ); - write_lp_packet_to_stream(&mut client_stream, &packet) - .await - .unwrap(); - - // Should fail with timestamp error - let result = server_task.await.unwrap(); - assert!(result.is_err()); - // Note: Can't use unwrap_err() directly because PublicKey doesn't implement Debug - // Just check that it failed - match result { - Err(e) => { - let err_msg = format!("{}", e); - assert!( - err_msg.contains("too old"), - "Expected 'too old' in error, got: {}", - err_msg - ); - } - Ok(_) => panic!("Expected error but got success"), - } - } } diff --git a/gateway/src/node/lp_listener/mod.rs b/gateway/src/node/lp_listener/mod.rs index 367e16e9bf..976eb2afa7 100644 --- a/gateway/src/node/lp_listener/mod.rs +++ b/gateway/src/node/lp_listener/mod.rs @@ -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>>, - /// 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>>, session_states: &Arc>>, 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>>, session_states: Arc>>, 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; } } } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index d8376289a3..cf8d303300 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -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, diff --git a/integration-tests/src/lp_registration.rs b/integration-tests/src/lp_registration.rs index 15074efc0e..cf3658f8fe 100644 --- a/integration-tests/src/lp_registration.rs +++ b/integration-tests/src/lp_registration.rs @@ -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()), diff --git a/nym-registration-client/src/lp_client/client.rs b/nym-registration-client/src/lp_client/client.rs index 1ae732dfb6..25f3265878 100644 --- a/nym-registration-client/src/lp_client/client.rs +++ b/nym-registration-client/src/lp_client/client.rs @@ -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 { state_machine: Option, /// 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 { - let state_machine = self.state_machine()?; - let recv_key = get_recv_key(state_machine); + pub(crate) async fn try_receive_packet(&mut self) -> Result { + 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 { - 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 = 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 { - // 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 { + async fn receive_packet_with_key(stream: &mut S, outer_key: &OuterAeadKey) -> Result { 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()) diff --git a/nym-registration-client/src/lp_client/nested_session/connection.rs b/nym-registration-client/src/lp_client/nested_session/connection.rs new file mode 100644 index 0000000000..fd0f673677 --- /dev/null +++ b/nym-registration-client/src/lp_client/nested_session/connection.rs @@ -0,0 +1,135 @@ +// Copyright 2026 - Nym Technologies SA +// 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, +} + +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, 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 { + // 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> { + self.receive_raw_packet().await.map_err(io::Error::other) + } +} diff --git a/nym-registration-client/src/lp_client/nested_session.rs b/nym-registration-client/src/lp_client/nested_session/mod.rs similarity index 71% rename from nym-registration-client/src/lp_client/nested_session.rs rename to nym-registration-client/src/lp_client/nested_session/mod.rs index 631206785c..7ee62c844c 100644 --- a/nym-registration-client/src/lp_client/nested_session.rs +++ b/nym-registration-client/src/lp_client/nested_session/mod.rs @@ -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) -> Result { 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 = 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( - &self, - outer_client: &mut LpRegistrationClient, - state_machine: &LpStateMachine, - packet: &LpPacket, - ) -> Result - 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 diff --git a/nym-registration-client/src/lp_client/state_machine_helpers.rs b/nym-registration-client/src/lp_client/state_machine_helpers.rs index 47da7c1255..b8be242621 100644 --- a/nym-registration-client/src/lp_client/state_machine_helpers.rs +++ b/nym-registration-client/src/lp_client/state_machine_helpers.rs @@ -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 { - 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 { - 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, 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` diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 423c497a0c..dd7d19c7f7 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -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]]