diff --git a/Cargo.toml b/Cargo.toml index 99451a3484..4940179019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,8 +173,9 @@ members = [ "wasm/mix-fetch", "wasm/node-tester", "wasm/zknym-lib", - "nym-gateway-probe", + # "nym-gateway-probe", "integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite", + "common/nym-lp-sandbox" ] default-members = [ @@ -392,6 +393,16 @@ zeroize = "1.7.0" prometheus = { version = "0.14.0" } + +# libcrux +libcrux-kem = { git = "https://github.com/cryspen/libcrux" } +libcrux-ecdh = { git = "https://github.com/cryspen/libcrux" } +libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" } +libcrux-psq = { git = "https://github.com/cryspen/libcrux" } +libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" } +libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" } +libcrux-traits = { git = "https://github.com/cryspen/libcrux" } + # Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" } nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" } @@ -436,7 +447,7 @@ nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", def nym-id = { version = "1.20.4", path = "common/nym-id" } nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" } nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" } -nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" } +nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" } nym-metrics = { version = "1.20.4", path = "common/nym-metrics" } nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" } nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" } diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 1b5d62f85e..abe5b922d8 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -25,6 +25,7 @@ cipher = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true } ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true } rand = { workspace = true, optional = true } +rand09 = { workspace = true } serde_bytes = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } sha2 = { workspace = true, optional = true } diff --git a/common/crypto/src/hkdf.rs b/common/crypto/src/hkdf.rs index c343002430..0cd4650083 100644 --- a/common/crypto/src/hkdf.rs +++ b/common/crypto/src/hkdf.rs @@ -109,3 +109,152 @@ impl DerivationMaterial { } } } + +pub mod blake3 { + + //! Key Derivation Functions using Blake3. + + use blake3::Hasher; + + use rand09::{RngCore, rng}; + use zeroize::Zeroize; + + pub fn derive_key_blake3_multi_input( + info: &str, + input_key_material: &[&[u8]], + salt: &[u8], + ) -> [u8; 32] { + let mut hasher = Hasher::new_derive_key(info); + + for input_key in input_key_material { + hasher.update(input_key); + } + + hasher.update(salt); + + hasher.finalize().as_bytes().to_owned() + } + + /// Derives a 32-byte key using Blake3's key derivation mode. + /// + /// Uses Blake3's built-in `derive_key` function with domain separation via context string. + /// + /// # Arguments + /// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1") + /// * `input_key_material` - Input key material (shared secret from ECDH, etc.) + /// * `salt` - Additional salt for freshness (nonce) + /// + /// # Returns + /// 32-byte derived key suitable for use as PSK + /// + /// # Example + /// ```ignore + /// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt); + /// ``` + pub fn derive_key_blake3(info: &str, input_key_material: &[u8], salt: &[u8]) -> [u8; 32] { + derive_key_blake3_multi_input(info, &[input_key_material], salt) + } + + pub fn derive_fresh_key_blake3_multi_input( + info: &str, + input_key_material: &[&[u8]], + ) -> [u8; 32] { + let mut salt = [0u8; 32]; + rng().fill_bytes(&mut salt); + + let derived_key = derive_key_blake3_multi_input(info, input_key_material, &salt); + + // Zeroize salt + salt.zeroize(); + + derived_key + } + + /// Derives a fresh 32-byte key using Blake3's key derivation mode. + /// The function calls a random number generator to generate a fresh salt. + /// Uses Blake3's built-in `derive_key` function with domain separation via context string. + /// + /// # Arguments + /// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1") + /// * `input_key_material` - Input key material (shared secret from ECDH, etc.) + /// + /// # Returns + /// 32-byte derived key suitable for use as PSK + /// + /// # Example + /// ```ignore + /// let psk = derive_fresh_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes()); + /// ``` + pub fn derive_fresh_key_blake3(info: &str, input_key_material: &[u8]) -> [u8; 32] { + derive_fresh_key_blake3_multi_input(info, &[input_key_material]) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_deterministic_derivation() { + let context = "test-context"; + let key_material = b"shared_secret_12345"; + let salt = b"salt_67890"; + + let key1 = derive_key_blake3(context, key_material, salt); + let key2 = derive_key_blake3(context, key_material, salt); + + assert_eq!(key1, key2, "Same inputs should produce same output"); + } + + #[test] + fn test_different_contexts_produce_different_keys() { + let key_material = b"shared_secret"; + let salt = b"salt"; + + let key1 = derive_key_blake3("context1", key_material, salt); + let key2 = derive_key_blake3("context2", key_material, salt); + + assert_ne!( + key1, key2, + "Different contexts should produce different keys" + ); + } + + #[test] + fn test_different_salts_produce_different_keys() { + let context = "test-context"; + let key_material = b"shared_secret"; + + let key1 = derive_key_blake3(context, key_material, b"salt1"); + let key2 = derive_key_blake3(context, key_material, b"salt2"); + + assert_ne!(key1, key2, "Different salts should produce different keys"); + } + + #[test] + fn test_different_key_material_produces_different_keys() { + let context = "test-context"; + let salt = b"salt"; + + let key1 = derive_key_blake3(context, b"secret1", salt); + let key2 = derive_key_blake3(context, b"secret2", salt); + + assert_ne!( + key1, key2, + "Different key material should produce different keys" + ); + } + + #[test] + fn test_output_length() { + let key = derive_key_blake3("test", b"key", b"salt"); + assert_eq!(key.len(), 32, "Output should be exactly 32 bytes"); + } + + #[test] + fn test_empty_inputs() { + // Should not panic with empty inputs + let key = derive_key_blake3("test", b"", b""); + assert_eq!(key.len(), 32); + } + } +} diff --git a/common/crypto/src/kdf.rs b/common/crypto/src/kdf.rs deleted file mode 100644 index 3edb257299..0000000000 --- a/common/crypto/src/kdf.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! Key Derivation Functions using Blake3. - -/// Derives a 32-byte key using Blake3's key derivation mode. -/// -/// Uses Blake3's built-in `derive_key` function with domain separation via context string. -/// -/// # Arguments -/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1") -/// * `key_material` - Input key material (shared secret from ECDH, etc.) -/// * `salt` - Additional salt for freshness (timestamp + nonce) -/// -/// # Returns -/// 32-byte derived key suitable for use as PSK -/// -/// # Example -/// ```ignore -/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt); -/// ``` -pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] { - // Concatenate key_material and salt as input - let input = [key_material, salt].concat(); - - // Use Blake3's derive_key with context for domain separation - // blake3::derive_key returns [u8; 32] directly - blake3::derive_key(context, &input) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_deterministic_derivation() { - let context = "test-context"; - let key_material = b"shared_secret_12345"; - let salt = b"salt_67890"; - - let key1 = derive_key_blake3(context, key_material, salt); - let key2 = derive_key_blake3(context, key_material, salt); - - assert_eq!(key1, key2, "Same inputs should produce same output"); - } - - #[test] - fn test_different_contexts_produce_different_keys() { - let key_material = b"shared_secret"; - let salt = b"salt"; - - let key1 = derive_key_blake3("context1", key_material, salt); - let key2 = derive_key_blake3("context2", key_material, salt); - - assert_ne!( - key1, key2, - "Different contexts should produce different keys" - ); - } - - #[test] - fn test_different_salts_produce_different_keys() { - let context = "test-context"; - let key_material = b"shared_secret"; - - let key1 = derive_key_blake3(context, key_material, b"salt1"); - let key2 = derive_key_blake3(context, key_material, b"salt2"); - - assert_ne!(key1, key2, "Different salts should produce different keys"); - } - - #[test] - fn test_different_key_material_produces_different_keys() { - let context = "test-context"; - let salt = b"salt"; - - let key1 = derive_key_blake3(context, b"secret1", salt); - let key2 = derive_key_blake3(context, b"secret2", salt); - - assert_ne!( - key1, key2, - "Different key material should produce different keys" - ); - } - - #[test] - fn test_output_length() { - let key = derive_key_blake3("test", b"key", b"salt"); - assert_eq!(key.len(), 32, "Output should be exactly 32 bytes"); - } - - #[test] - fn test_empty_inputs() { - // Should not panic with empty inputs - let key = derive_key_blake3("test", b"", b""); - assert_eq!(key.len(), 32); - } -} diff --git a/common/crypto/src/lib.rs b/common/crypto/src/lib.rs index 3875fa7f81..1dff7b82be 100644 --- a/common/crypto/src/lib.rs +++ b/common/crypto/src/lib.rs @@ -10,8 +10,6 @@ pub mod crypto_hash; pub mod hkdf; #[cfg(feature = "hashing")] pub mod hmac; -#[cfg(feature = "hashing")] -pub mod kdf; #[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))] pub mod shared_key; pub mod symmetric; diff --git a/common/nym-kkt-ciphersuite/Cargo.toml b/common/nym-kkt-ciphersuite/Cargo.toml index 079325f625..efd52f7030 100644 --- a/common/nym-kkt-ciphersuite/Cargo.toml +++ b/common/nym-kkt-ciphersuite/Cargo.toml @@ -17,7 +17,7 @@ strum = { workspace = true } strum_macros = { workspace = true } blake3 = { workspace = true, optional = true } -libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true } +libcrux-sha3 = { workspace = true, optional = true } [features] digests = ["blake3", "libcrux-sha3"] diff --git a/common/nym-kkt-ciphersuite/src/lib.rs b/common/nym-kkt-ciphersuite/src/lib.rs index e72d873a87..c5773e98a0 100644 --- a/common/nym-kkt-ciphersuite/src/lib.rs +++ b/common/nym-kkt-ciphersuite/src/lib.rs @@ -226,6 +226,17 @@ impl KEM { } } +// pub const fn map_kem_to_libcrux_kem(kem: &KEM) -> Result { +// match kem { +// KEM::MlKem768 => Ok(Algorithm::MlKem768), +// KEM::XWing => Ok(Algorithm::XWingKemDraft06), +// KEM::X25519 => Ok(Algorithm::X25519), +// KEM::McEliece => Err(KKTError::KEMMapping { +// info: "attempted to map McEliece KEM to libcrux_kem", +// }), +// } +// } + #[derive(Clone, Copy, PartialEq, Debug)] pub struct Ciphersuite { hash_function: HashFunction, @@ -273,16 +284,16 @@ impl Ciphersuite { self.verification_key_length } - pub fn hash_function(&self) -> HashFunction { - self.hash_function + pub fn hash_function(&self) -> &HashFunction { + &self.hash_function } - pub fn kem(&self) -> KEM { - self.kem + pub fn kem(&self) -> &KEM { + &self.kem } - pub fn signature_scheme(&self) -> SignatureScheme { - self.signature_scheme + pub fn signature_scheme(&self) -> &SignatureScheme { + &self.signature_scheme } pub fn hash_len(&self) -> usize { @@ -351,6 +362,17 @@ impl Display for Ciphersuite { } } +impl Default for Ciphersuite { + fn default() -> Self { + Self::new( + KEM::MlKem768, + HashFunction::Blake3, + SignatureScheme::Ed25519, + HashLength::Default, + ) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/nym-kkt/Cargo.toml b/common/nym-kkt/Cargo.toml index 705395f712..773c1f4d25 100644 --- a/common/nym-kkt/Cargo.toml +++ b/common/nym-kkt/Cargo.toml @@ -7,25 +7,23 @@ license.workspace = true publish = false [dependencies] -blake3 = { workspace = true } thiserror = { workspace = true } num_enum = { workspace = true } strum = { workspace = true } - # internal -nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] } +nym-crypto = { path = "../crypto", features = ["hashing"] } nym-kkt-ciphersuite = { workspace = true, features = ["digests"] } -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" } +libcrux-kem = { workspace = true } +libcrux-ecdh = { workspace = true, features = ["codec"] } +libcrux-chacha20poly1305 = { workspace = true } -# 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"] } +libcrux-psq = { workspace = true, features = ["classic-mceliece"] } +libcrux-ml-kem = { workspace = true } [dev-dependencies] rand_chacha = "0.9.0" diff --git a/common/nym-kkt/benches/benches.rs b/common/nym-kkt/benches/benches.rs index 4d4f0f7e13..16820b192b 100644 --- a/common/nym-kkt/benches/benches.rs +++ b/common/nym-kkt/benches/benches.rs @@ -7,29 +7,20 @@ use criterion::{Criterion, criterion_group, criterion_main}; -use nym_crypto::asymmetric::ed25519; use nym_kkt::{ ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM, SignatureScheme}, context::KKTMode, frame::KKTFrame, - key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key}, + key_utils::{ + generate_keypair_libcrux, generate_keypair_mceliece, generate_keypair_mlkem, + hash_encapsulation_key, + }, session::{ - anonymous_initiator_process, initiator_ingest_response, initiator_process, - responder_ingest_message, responder_process, + initiator_ingest_response, initiator_process, responder_ingest_message, responder_process, }, }; 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]; - rand09::rng().fill_bytes(&mut s); - ed25519::KeyPair::from_secret(s, 0) - }); - }); -} - pub fn gen_mlkem768_keypair(c: &mut Criterion) { c.bench_function("Generate MlKem768 Keypair", |b| { b.iter(|| { @@ -41,15 +32,12 @@ pub fn gen_mlkem768_keypair(c: &mut Criterion) { pub fn kkt_benchmark(c: &mut Criterion) { let mut rng = rand09::rng(); - // generate ed25519 keys let mut secret_initiator: [u8; 32] = [0u8; 32]; rng.fill_bytes(&mut secret_initiator); - let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0); let mut secret_responder: [u8; 32] = [0u8; 32]; rng.fill_bytes(&mut secret_responder); - let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1); for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] { for hash_function in [ HashFunction::Blake3, @@ -69,8 +57,8 @@ pub fn kkt_benchmark(c: &mut Criterion) { let (responder_kem_public_key, initiator_kem_public_key) = match kem { KEM::MlKem768 => ( - EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1), + EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1), + EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1), ), KEM::XWing => ( EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), @@ -107,12 +95,14 @@ pub fn kkt_benchmark(c: &mut Criterion) { c.bench_function( &format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",), |b| { - b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap()); + b.iter(|| { + initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap() + }); }, ); - let (i_context, i_frame) = - anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); + let (mut i_context, i_frame) = + initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap(); c.bench_function( &format!( @@ -137,14 +127,12 @@ pub fn kkt_benchmark(c: &mut Criterion) { "{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame", ), |b| { - b.iter(|| { - responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap() - }); + b.iter(|| responder_ingest_message(&r_context, None, &i_frame_r).unwrap()); }, ); - let (r_context, _) = - responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap(); + let (mut r_context, _) = + responder_ingest_message(&r_context, None, &i_frame_r).unwrap(); c.bench_function( &format!( @@ -153,9 +141,8 @@ pub fn kkt_benchmark(c: &mut Criterion) { |b| { b.iter(|| { responder_process( - &r_context, + &mut r_context, i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), &responder_kem_public_key, ) .unwrap() @@ -163,9 +150,8 @@ pub fn kkt_benchmark(c: &mut Criterion) { }, ); let r_frame = responder_process( - &r_context, + &mut r_context, i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), &responder_kem_public_key, ) .unwrap(); @@ -184,10 +170,9 @@ pub fn kkt_benchmark(c: &mut Criterion) { |b| { b.iter(|| { initiator_ingest_response( - &i_context, + &mut i_context, &r_frame, &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), &r_dir_hash, ) .unwrap() @@ -196,10 +181,9 @@ pub fn kkt_benchmark(c: &mut Criterion) { ); let obtained_key = initiator_ingest_response( - &i_context, + &mut i_context, &r_frame, &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), &r_dir_hash, ) .unwrap(); @@ -208,27 +192,14 @@ pub fn kkt_benchmark(c: &mut Criterion) { } // Initiator, OneWay { - let (i_context, i_frame) = initiator_process( - &mut rng, - KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) - .unwrap(); + let (mut i_context, i_frame) = + initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap(); c.bench_function( &format!("{kem}, {hash_function} | Initiator OneWay: Generate Request",), |b| { b.iter(|| { - initiator_process( - &mut rng, - KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) - .unwrap() + initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap() }); }, ); @@ -250,25 +221,12 @@ pub fn kkt_benchmark(c: &mut Criterion) { c.bench_function( &format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",), |b| { - b.iter(|| { - responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap() - }); + b.iter(|| responder_ingest_message(&r_context, None, &i_frame_r).unwrap()); }, ); - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap(); + let (mut r_context, r_obtained_key) = + responder_ingest_message(&r_context, None, &i_frame_r).unwrap(); assert!(r_obtained_key.is_none()); @@ -279,9 +237,8 @@ pub fn kkt_benchmark(c: &mut Criterion) { |b| { b.iter(|| { responder_process( - &r_context, + &mut r_context, i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), &responder_kem_public_key, ) .unwrap() @@ -290,9 +247,8 @@ pub fn kkt_benchmark(c: &mut Criterion) { ); let r_frame = responder_process( - &r_context, + &mut r_context, i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), &responder_kem_public_key, ) .unwrap(); @@ -311,10 +267,9 @@ pub fn kkt_benchmark(c: &mut Criterion) { |b| { b.iter(|| { initiator_ingest_response( - &i_context, + &mut i_context, &r_frame, &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), &r_dir_hash, ) .unwrap() @@ -323,10 +278,9 @@ pub fn kkt_benchmark(c: &mut Criterion) { ); let i_obtained_key = initiator_ingest_response( - &i_context, + &mut i_context, &r_frame, &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), &r_dir_hash, ) .unwrap(); @@ -344,7 +298,6 @@ pub fn kkt_benchmark(c: &mut Criterion) { &mut rng, KKTMode::Mutual, ciphersuite, - initiator_ed25519_keypair.private_key(), Some(&initiator_kem_public_key), ) .unwrap() @@ -352,11 +305,10 @@ pub fn kkt_benchmark(c: &mut Criterion) { }, ); - let (i_context, i_frame) = initiator_process( + let (mut i_context, i_frame) = initiator_process( &mut rng, KKTMode::Mutual, ciphersuite, - initiator_ed25519_keypair.private_key(), Some(&initiator_kem_public_key), ) .unwrap(); @@ -383,24 +335,14 @@ pub fn kkt_benchmark(c: &mut Criterion) { &format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",), |b| { b.iter(|| { - responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap() + responder_ingest_message(&r_context, Some(&i_dir_hash), &i_frame_r) + .unwrap() }); }, ); - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap(); + let (mut r_context, r_obtained_key) = + responder_ingest_message(&r_context, Some(&i_dir_hash), &i_frame_r).unwrap(); assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes); @@ -411,9 +353,8 @@ pub fn kkt_benchmark(c: &mut Criterion) { |b| { b.iter(|| { responder_process( - &r_context, + &mut r_context, i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), &responder_kem_public_key, ) .unwrap() @@ -422,9 +363,8 @@ pub fn kkt_benchmark(c: &mut Criterion) { ); let r_frame = responder_process( - &r_context, + &mut r_context, i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), &responder_kem_public_key, ) .unwrap(); @@ -445,10 +385,9 @@ pub fn kkt_benchmark(c: &mut Criterion) { |b| { b.iter(|| { initiator_ingest_response( - &i_context, + &mut i_context, &r_frame, &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), &r_dir_hash, ) .unwrap() @@ -457,10 +396,9 @@ pub fn kkt_benchmark(c: &mut Criterion) { ); let obtained_key = initiator_ingest_response( - &i_context, + &mut i_context, &r_frame, &r_frame.context().unwrap(), - responder_ed25519_keypair.public_key(), &r_dir_hash, ) .unwrap(); @@ -471,10 +409,5 @@ pub fn kkt_benchmark(c: &mut Criterion) { } } -criterion_group!( - benches, - gen_ed25519_keypair, - gen_mlkem768_keypair, - kkt_benchmark -); +criterion_group!(benches, gen_mlkem768_keypair, kkt_benchmark); criterion_main!(benches); diff --git a/common/nym-kkt/src/carrier.rs b/common/nym-kkt/src/carrier.rs new file mode 100644 index 0000000000..38686e97f9 --- /dev/null +++ b/common/nym-kkt/src/carrier.rs @@ -0,0 +1,189 @@ +use libcrux_chacha20poly1305::TAG_LEN; +use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey}; +use nym_crypto::hkdf::blake3::derive_key_blake3; +use rand09::{CryptoRng, RngCore}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::error::KKTError; + +// This is arbitrary +pub const MAX_PAYLOAD_LEN: usize = 1_000_000; +const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_RX"; +const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_TX"; + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct Carrier { + tx_key: [u8; 32], + rx_key: [u8; 32], + tx_counter: u64, + rx_counter: u64, +} + +pub enum CarrierRole { + Initiator, + Responder, +} + +fn increment_nonce(nonce: &mut u64) -> Result<(), KKTError> { + match nonce.checked_add(1) { + Some(incremented_nonce) => { + *nonce = incremented_nonce; + Ok(()) + } + None => Err(KKTError::AEADError { + info: "Nonce maxed out.", + }), + } +} + +fn as_nonce_bytes(nonce: u64) -> [u8; 12] { + let mut bytes = [0u8; 12]; + let nonce_bytes = nonce.to_le_bytes(); + bytes[4..].clone_from_slice(&nonce_bytes); + bytes +} + +impl Carrier { + fn init(tx_key: [u8; 32], rx_key: [u8; 32]) -> Self { + Self { + tx_key, + rx_key, + tx_counter: 1, + rx_counter: 1, + } + } + pub fn new( + rng: &mut R, + remote_public_key: &DHPublicKey, + context: &[u8], + ) -> Result<(Self, DHPublicKey), KKTError> + where + R: RngCore + CryptoRng, + { + let ephemeral_keypair = DHKeyPair::new(rng); + let shared_secret = ephemeral_keypair + .sk() + .diffie_hellman(remote_public_key) + .map_err(|_| KKTError::X25519Error { + info: "Key Derivation Error", + })?; + + Ok(( + Self::from_secret_slice(shared_secret.as_ref(), context), + ephemeral_keypair.pk, + )) + } + + pub(crate) fn from_secret_slice(secret: &[u8], context: &[u8]) -> Self { + let tx_key = derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context); + let rx_key = derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context); + Self::init(tx_key, rx_key) + } + + pub fn from_secret(mut secret: [u8; 32], context: &[u8]) -> Self { + let tx_key = derive_key_blake3(CARRIER_KDF_INFO_TX, secret.as_ref(), context); + let rx_key = derive_key_blake3(CARRIER_KDF_INFO_RX, secret.as_ref(), context); + secret.zeroize(); + Self::init(tx_key, rx_key) + } + + pub(crate) fn flip_keys(self) -> Self { + Self { + tx_key: self.rx_key, + rx_key: self.tx_key, + tx_counter: self.rx_counter, + rx_counter: self.tx_counter, + } + } + + pub fn encrypt(&mut self, plaintext: &[u8]) -> Result, KKTError> { + if plaintext.len() > MAX_PAYLOAD_LEN { + return Err(KKTError::AEADError { + info: "Plaintext too large", + }); + } + let mut output_buffer = vec![0; plaintext.len() + TAG_LEN]; + libcrux_chacha20poly1305::encrypt( + &self.tx_key, + plaintext, + &mut output_buffer, + b"kkt-carrier-v1", + &as_nonce_bytes(self.tx_counter), + )?; + + increment_nonce(&mut self.tx_counter)?; + + Ok(output_buffer) + } + pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result, KKTError> { + if ciphertext.len() > MAX_PAYLOAD_LEN + TAG_LEN { + return Err(KKTError::AEADError { + info: "Ciphertext too large", + }); + } + let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN]; + libcrux_chacha20poly1305::decrypt( + &self.rx_key, + &mut output_buffer, + ciphertext, + b"kkt-carrier-v1", + &as_nonce_bytes(self.rx_counter), + )?; + + increment_nonce(&mut self.rx_counter)?; + + Ok(output_buffer) + } +} + +#[cfg(test)] +mod tests { + use crate::{carrier::Carrier, key_utils::generate_keypair_x25519}; + use rand09::RngCore; + + #[test] + fn test_e2e() { + let mut rng = rand09::rng(); + + // generate responder x25519 keys + let r_x25519 = generate_keypair_x25519(&mut rng); + + let mut context: [u8; 32] = [0u8; 32]; + rng.fill_bytes(&mut context); + + let ephemeral_keypair = generate_keypair_x25519(&mut rng); + + let i_shared_secret = ephemeral_keypair.sk().diffie_hellman(&r_x25519.pk).unwrap(); + + let r_shared_secret = r_x25519.sk().diffie_hellman(&ephemeral_keypair.pk).unwrap(); + + let mut i_carrier = Carrier::from_secret_slice(i_shared_secret.as_ref(), &context); + let mut r_carrier = + Carrier::from_secret_slice(r_shared_secret.as_ref(), &context).flip_keys(); + + let test1 = b"test1: i>r #1"; + let ct1 = i_carrier.encrypt(test1).unwrap(); + let pt1 = r_carrier.decrypt(&ct1).unwrap(); + assert_eq!(pt1, test1); + + let test2 = b"test2: r>i #1"; + let ct2 = i_carrier.encrypt(test2).unwrap(); + let pt2 = r_carrier.decrypt(&ct2).unwrap(); + assert_eq!(pt2, test2); + let test3 = b"test3: i>r #2"; + + let ct3 = i_carrier.encrypt(test3).unwrap(); + let pt3 = r_carrier.decrypt(&ct3).unwrap(); + assert_eq!(pt3, test3); + + let test4 = b"test4: i>r #3"; + let ct4 = i_carrier.encrypt(test4).unwrap(); + let pt4 = r_carrier.decrypt(&ct4).unwrap(); + assert_eq!(pt4, test4); + + let test5 = b"test5: r>i #2"; + let ct5 = i_carrier.encrypt(test5).unwrap(); + let pt5 = r_carrier.decrypt(&ct5).unwrap(); + assert_eq!(pt5, test5); + } +} diff --git a/common/nym-kkt/src/ciphersuite.rs b/common/nym-kkt/src/ciphersuite.rs deleted file mode 100644 index c87915881c..0000000000 --- a/common/nym-kkt/src/ciphersuite.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::KKTError; -use libcrux_kem::Algorithm; - -pub use nym_kkt_ciphersuite::*; - -pub enum EncapsulationKey<'a> { - MlKem768(libcrux_kem::PublicKey), - XWing(libcrux_kem::PublicKey), - X25519(libcrux_kem::PublicKey), - McEliece(classic_mceliece_rust::PublicKey<'a>), -} - -pub enum DecapsulationKey<'a> { - MlKem768(libcrux_kem::PrivateKey), - XWing(libcrux_kem::PrivateKey), - X25519(libcrux_kem::PrivateKey), - McEliece(classic_mceliece_rust::SecretKey<'a>), -} -impl<'a> EncapsulationKey<'a> { - pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result { - match kem { - KEM::McEliece => { - if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES { - Err(KKTError::KEMError { - info: "Received McEliece Encapsulation Key with Invalid Length", - }) - } else { - let mut public_key_bytes = - Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]); - // Size must be correct due to KKTFrame::from_bytes(message_bytes)? - public_key_bytes.clone_from_slice(bytes); - Ok(EncapsulationKey::McEliece( - classic_mceliece_rust::PublicKey::from(public_key_bytes), - )) - } - } - KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode( - map_kem_to_libcrux_kem(kem)?, - bytes, - )?)), - KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode( - map_kem_to_libcrux_kem(kem)?, - bytes, - )?)), - KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode( - map_kem_to_libcrux_kem(kem)?, - bytes, - )?)), - } - } - - pub fn encode(&self) -> Vec { - match self { - EncapsulationKey::XWing(public_key) - | EncapsulationKey::MlKem768(public_key) - | EncapsulationKey::X25519(public_key) => public_key.encode(), - EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()), - } - } -} - -pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Result { - match kem { - KEM::MlKem768 => Ok(Algorithm::MlKem768), - KEM::XWing => Ok(Algorithm::XWingKemDraft06), - KEM::X25519 => Ok(Algorithm::X25519), - KEM::McEliece => Err(KKTError::KEMMapping { - info: "attempted to map McEliece KEM to libcrux_kem", - }), - } -} diff --git a/common/nym-kkt/src/context.rs b/common/nym-kkt/src/context.rs index 8034c52af7..8eeb695523 100644 --- a/common/nym-kkt/src/context.rs +++ b/common/nym-kkt/src/context.rs @@ -1,9 +1,9 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::ciphersuite::CIPHERSUITE_ENCODING_LEN; -use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN}; +use crate::{KKT_VERSION, error::KKTError}; use num_enum::{IntoPrimitive, TryFromPrimitive}; +use nym_kkt_ciphersuite::{CIPHERSUITE_ENCODING_LEN, Ciphersuite}; use std::fmt::Display; pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN; @@ -15,11 +15,11 @@ pub enum KKTStatus { Ok = 0b0000_0000, InvalidRequestFormat = 0b0010_0000, InvalidResponseFormat = 0b0100_0000, - InvalidSignature = 0b0110_0000, - UnsupportedCiphersuite = 0b1000_0000, - UnsupportedKKTVersion = 0b1010_0000, - InvalidKey = 0b1100_0000, - Timeout = 0b1110_0000, + UnsupportedCiphersuite = 0b0110_0000, + UnsupportedKKTVersion = 0b1000_0000, + InvalidKey = 0b1010_0000, + Timeout = 0b1100_0000, + UnverifiedKEMKey = 0b1110_0000, } impl Display for KKTStatus { @@ -28,10 +28,10 @@ impl Display for KKTStatus { KKTStatus::Ok => "Ok", KKTStatus::InvalidRequestFormat => "Invalid Request Format", KKTStatus::InvalidResponseFormat => "Invalid Response Format", - KKTStatus::InvalidSignature => "Invalid Signature", KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite", KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version", KKTStatus::InvalidKey => "Invalid Key", + KKTStatus::UnverifiedKEMKey => "Could not verify received encapsulation key", KKTStatus::Timeout => "Timeout", }) } @@ -43,7 +43,6 @@ impl Display for KKTStatus { pub enum KKTRole { Initiator = 0b0000_0000, Responder = 0b0000_0001, - AnonymousInitiator = 0b0000_0010, } // bitmask used: 0b0001_1100 @@ -64,20 +63,15 @@ pub struct KKTContext { ciphersuite: Ciphersuite, } impl KKTContext { - pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result { - if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay { - return Err(KKTError::IncompatibilityError { - info: "Anonymous Initiator can only use OneWay mode", - }); - } - Ok(Self { + pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: &Ciphersuite) -> Self { + Self { version: KKT_VERSION, message_sequence: 0, status: KKTStatus::Ok, mode, role, - ciphersuite, - }) + ciphersuite: *ciphersuite, + } } pub fn derive_responder_header(&self) -> Result { @@ -107,8 +101,8 @@ impl KKTContext { pub fn status(&self) -> KKTStatus { self.status } - pub fn ciphersuite(&self) -> Ciphersuite { - self.ciphersuite + pub fn ciphersuite(&self) -> &Ciphersuite { + &self.ciphersuite } pub fn role(&self) -> KKTRole { self.role @@ -118,9 +112,10 @@ impl KKTContext { } pub fn body_len(&self) -> usize { - if self.status != KKTStatus::Ok - || (self.mode == KKTMode::OneWay - && (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator)) + if (self.status != KKTStatus::Ok && self.status != KKTStatus::UnverifiedKEMKey) + || + // no payload + (self.mode == KKTMode::OneWay && self.role == KKTRole::Initiator) { 0 } else { @@ -128,31 +123,12 @@ impl KKTContext { } } - pub fn signature_len(&self) -> usize { - match self.role { - KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(), - KKTRole::AnonymousInitiator => 0, - } - } - pub const fn header_len(&self) -> usize { KKT_CONTEXT_LEN } - pub const fn session_id_len(&self) -> usize { - // note: if anyone decides to update this function and changes the constant value, - // you will have to adjust encoding/decoding functions - - // match self.role { - // KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH, - // It doesn't make sense to send a session_id if we send messages in the clear - // KKTRole::AnonymousInitiator => 0, - // } - KKT_SESSION_ID_LEN - } - pub fn full_message_len(&self) -> usize { - self.body_len() + self.signature_len() + self.header_len() + self.session_id_len() + self.body_len() + self.header_len() } pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> { @@ -228,9 +204,8 @@ mod tests { let valid_context = KKTContext::new( KKTRole::Initiator, KKTMode::Mutual, - Ciphersuite::decode([255, 1, 0, 0]).unwrap(), - ) - .unwrap(); + &Ciphersuite::decode([255, 1, 0, 0]).unwrap(), + ); let encoded = valid_context.encode().unwrap(); let decoded = KKTContext::try_decode(encoded).unwrap(); diff --git a/common/nym-kkt/src/encryption.rs b/common/nym-kkt/src/encryption.rs deleted file mode 100644 index c4189f9552..0000000000 --- a/common/nym-kkt/src/encryption.rs +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright 2025-2026 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::KKTFrame}; -use blake3::Hasher; -use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN}; -use nym_crypto::asymmetric::x25519; -use rand09::{CryptoRng, RngCore}; -use zeroize::Zeroize; - -#[derive(Clone, Copy, Zeroize)] -pub struct KKTSessionSecret([u8; 32]); - -impl KKTSessionSecret { - pub fn new(rng: &mut R, remote_public_key: &x25519::PublicKey) -> (Self, x25519::PublicKey) - where - R: RngCore + CryptoRng, - { - let mut private_key_bytes = [0u8; x25519::PRIVATE_KEY_SIZE]; - rng.fill_bytes(&mut private_key_bytes); - - let ephemeral_private_key = x25519::PrivateKey::from_secret(private_key_bytes); - let ephemeral_public_key = x25519::PublicKey::from(&ephemeral_private_key); - - ( - Self::derive(&ephemeral_private_key, remote_public_key), - ephemeral_public_key, - ) - } - pub fn from_bytes(secret: [u8; 32]) -> Self { - Self(secret) - } - - fn try_derive(private_key: &x25519::PrivateKey, public_key: &[u8]) -> Result { - let mut pub_key: [u8; 32] = [0u8; 32]; - pub_key.copy_from_slice(&public_key[0..x25519::PUBLIC_KEY_SIZE]); - - // Todo: check validity of pk... - let pk = x25519::PublicKey::from(pub_key); - Ok(Self::derive(private_key, &pk)) - } - - pub fn derive(private_key: &x25519::PrivateKey, public_key: &x25519::PublicKey) -> Self { - let mut shared_secret = private_key.diffie_hellman(public_key); - - let mut hasher = Hasher::new(); - - hasher.update(&shared_secret); - shared_secret.zeroize(); - - Self(hasher.finalize().as_bytes().to_owned()) - } - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } -} - -pub fn encrypt_initial_kkt_frame( - rng: &mut R, - remote_public_key: &x25519::PublicKey, - kkt_frame: &KKTFrame, -) -> Result<(KKTSessionSecret, Vec), KKTError> -where - R: CryptoRng + RngCore, -{ - let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(rng, remote_public_key); - - let mut encrypted_frame = - encrypt_kkt_frame(rng, &session_secret_key, kkt_frame, KKT_INITIAL_FRAME_AAD)?; - - let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + x25519::PUBLIC_KEY_SIZE); - output_buffer.extend_from_slice(ephemeral_public_key.as_bytes()); - output_buffer.append(&mut encrypted_frame); - - // [ 32 | 12 | ciphertext | 16]; - // [eph_pub_key | nonce | ciphertext | tag]; - Ok((session_secret_key, output_buffer)) -} - -pub fn decrypt_initial_kkt_frame( - responder_private_key: &x25519::PrivateKey, - encrypted_frame_bytes: &[u8], -) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> { - if encrypted_frame_bytes.len() < x25519::PUBLIC_KEY_SIZE + TAG_LEN + NONCE_LEN { - Err(KKTError::AEADError { - info: "Encrypted KKT Frame is too short.", - }) - } else { - let shared_secret = KKTSessionSecret::try_derive( - responder_private_key, - &encrypted_frame_bytes[0..x25519::PUBLIC_KEY_SIZE], - )?; - - let (kkt_frame, kkt_context) = decrypt_kkt_frame( - &shared_secret, - &encrypted_frame_bytes[x25519::PUBLIC_KEY_SIZE..], - KKT_INITIAL_FRAME_AAD, - )?; - Ok((shared_secret, kkt_frame, kkt_context)) - } -} - -pub fn encrypt_kkt_frame( - rng: &mut R, - secret_key: &KKTSessionSecret, - kkt_frame: &KKTFrame, - aad: &[u8], -) -> Result, KKTError> -where - R: CryptoRng + RngCore, -{ - let kkt_frame_bytes = kkt_frame.to_bytes(); - - // generate nonce - let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN]; - rng.fill_bytes(&mut nonce); - - let mut ciphertext = encrypt(secret_key.as_bytes(), &kkt_frame_bytes, aad, &nonce)?; - - // [ 12 | ciphertext | 16]; - // [nonce | ciphertext | tag]; - let mut output_buffer: Vec = - Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN); - - output_buffer.extend_from_slice(&nonce); - output_buffer.append(&mut ciphertext); - - Ok(output_buffer) -} - -// kkt_frame_bytes should look like this -// [ 12 | ciphertext | 16]; -// [nonce | ciphertext | tag]; -pub fn decrypt_kkt_frame( - secret_key: &KKTSessionSecret, - kkt_frame_bytes: &[u8], - aad: &[u8], -) -> Result<(KKTFrame, KKTContext), KKTError> { - let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN]; - nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]); - - let plaintext = decrypt( - secret_key.as_bytes(), - &kkt_frame_bytes[NONCE_LEN..], - aad, - &nonce, - )?; - - KKTFrame::from_bytes(&plaintext) -} - -fn encrypt( - secret_key: &[u8; 32], - plaintext: &[u8], - aad: &[u8], - nonce: &[u8; NONCE_LEN], -) -> Result, KKTError> { - let mut output_buffer = vec![0; plaintext.len() + TAG_LEN]; - libcrux_chacha20poly1305::encrypt(secret_key, plaintext, &mut output_buffer, aad, nonce)?; - Ok(output_buffer) -} - -fn decrypt( - secret_key: &[u8; 32], - ciphertext: &[u8], - aad: &[u8], - nonce: &[u8; NONCE_LEN], -) -> Result, KKTError> { - let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN]; - libcrux_chacha20poly1305::decrypt(secret_key, &mut output_buffer, ciphertext, aad, nonce)?; - Ok(output_buffer) -} - -#[cfg(test)] -mod test { - use crate::ciphersuite::Ciphersuite; - use crate::context::{KKTContext, KKTMode, KKTRole}; - use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame}; - use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame}; - use crate::{ - ciphersuite::DEFAULT_HASH_LEN, - encryption::{KKTSessionSecret, decrypt, encrypt}, - key_utils::generate_keypair_x25519, - }; - use rand09::{RngCore, SeedableRng, rng}; - - #[test] - fn test_keygen() { - let mut rng = rng(); - let responder_x25519_keypair = generate_keypair_x25519(&mut rng); - - let (session_secret_key, ephemeral_public_key) = - KKTSessionSecret::new(&mut rng, responder_x25519_keypair.public_key()); - - let shared_secret = KKTSessionSecret::try_derive( - responder_x25519_keypair.private_key(), - ephemeral_public_key.as_bytes().as_slice(), - ) - .unwrap(); - - assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes()) - } - - #[test] - fn test_encryption() { - let mut rng = rng(); - - let mut secret_key = [0u8; DEFAULT_HASH_LEN]; - rng.fill_bytes(&mut secret_key); - - let mut plaintext = vec![0; 100]; - rng.fill_bytes(&mut plaintext); - - let mut nonce = [0; 12]; - rng.fill_bytes(&mut nonce); - - let mut aad = vec![0; 124]; - rng.fill_bytes(&mut aad); - - let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap(); - - let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap(); - - assert_eq!(o_plaintext, plaintext) - } - - #[test] - fn kkt_frame_encryption() -> anyhow::Result<()> { - let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42); - let session_key = KKTSessionSecret::from_bytes([42u8; 32]); - let aad = b"my-amazing-aad"; - - let valid_context = KKTContext::new( - KKTRole::Initiator, - KKTMode::Mutual, - Ciphersuite::decode([255, 1, 0, 0])?, - )?; - let dummy_frame = KKTFrame::new( - valid_context.encode()?, - &[2u8; 32], - [3u8; KKT_SESSION_ID_LEN], - &[4u8; 64], - ); - - let ciphertext = encrypt_kkt_frame(&mut rng, &session_key, &dummy_frame, aad.as_slice())?; - - let (frame, context) = decrypt_kkt_frame(&session_key, &ciphertext, aad.as_slice())?; - - assert_eq!(dummy_frame, frame); - assert_eq!(context, valid_context); - Ok(()) - } -} diff --git a/common/nym-kkt/src/error.rs b/common/nym-kkt/src/error.rs index 1defc1b1e0..8ace2a9007 100644 --- a/common/nym-kkt/src/error.rs +++ b/common/nym-kkt/src/error.rs @@ -8,13 +8,12 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum KKTError { - #[error("Signature constructor error")] - SigConstructorError, - #[error("Signature verification error")] - SigVerifError, #[error(transparent)] CiphersuiteDecodingError(#[from] KKTCiphersuiteError), + #[error(transparent)] + MaskedByteError(#[from] MaskedByteError), + #[error("KEM mapping failure: {}", info)] KEMMapping { info: &'static str }, @@ -48,10 +47,28 @@ pub enum KKTError { #[error("{}", info)] AEADError { info: &'static str }, + #[error("{}", info)] + DecodingError { info: &'static str }, + + #[error("{}", info)] + UnsupportedAlgorithm { info: &'static str }, + #[error("Generic libcrux error")] LibcruxError, } +#[derive(Error, Debug)] +pub enum MaskedByteError { + #[error( + "Invalid Masked Byte Length: Expected({}), Actual({}).", + expected, + actual + )] + InvalidLength { expected: usize, actual: usize }, + #[error("Failed to Unmask Byte.")] + Failure, +} + impl From for KKTError { fn from(err: libcrux_kem::Error) -> Self { match err { diff --git a/common/nym-kkt/src/frame.rs b/common/nym-kkt/src/frame.rs index 6502af882c..6e43178ef5 100644 --- a/common/nym-kkt/src/frame.rs +++ b/common/nym-kkt/src/frame.rs @@ -7,42 +7,120 @@ // [2..=5] => Ciphersuite // [6] => Reserved +use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey}; +use nym_kkt_ciphersuite::x25519::PUBLIC_KEY_LENGTH; +use rand09::{CryptoRng, RngCore}; + use crate::{ + carrier::Carrier, context::{KKT_CONTEXT_LEN, KKTContext}, error::KKTError, + masked_byte::{MASKED_BYTE_LEN, MaskedByte}, }; -pub const KKT_SESSION_ID_LEN: usize = 16; - -pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN]; +const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF"; #[derive(Debug, PartialEq, Clone)] pub struct KKTFrame { context: [u8; KKT_CONTEXT_LEN], - session_id: KKTSessionId, body: Vec, - signature: Vec, } -// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes). -// if message coming from anonymous initiator => body is empty, there is no signature. -// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id. -// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id. +// if oneway and message coming from initiator => body is empty. +// if mutual and message coming from initiator => body has the initiator's kem public key. +// if coming from responder => body has the responder's kem public key. impl KKTFrame { - pub fn new( - context: [u8; KKT_CONTEXT_LEN], - body: &[u8], - session_id: [u8; KKT_SESSION_ID_LEN], - signature: &[u8], - ) -> Self { - Self { - context, + pub fn new(context: &KKTContext, body: &[u8]) -> Result { + let context_bytes = context.encode()?; + Ok(Self { + context: context_bytes, body: Vec::from(body), - session_id, - signature: Vec::from(signature), - } + }) } + + pub fn encrypt_initiator_frame( + &self, + rng: &mut R, + responder_public_key: &DHPublicKey, + version_byte: u8, + ) -> Result<(Carrier, Vec), KKTError> + where + R: CryptoRng + RngCore, + { + let ephemeral_keypair = DHKeyPair::new(rng); + let shared_secret = ephemeral_keypair + .sk() + .diffie_hellman(responder_public_key) + .map_err(|_| KKTError::X25519Error { + info: "Key Derivation Error", + })?; + + let mut mask = Vec::from(ephemeral_keypair.pk.as_ref()); + mask.extend_from_slice(responder_public_key.as_ref()); + + let masked_byte = MaskedByte::new(version_byte, &mask); + + let mut context = Vec::from(masked_byte.as_slice()); + context.extend_from_slice(KKT_CARRIER_CONTEXT); + context.extend_from_slice(ephemeral_keypair.pk.as_ref()); + context.extend_from_slice(responder_public_key.as_ref()); + + let mut carrier = Carrier::from_secret_slice(shared_secret.as_ref(), &context); + + let mut full_kkt_message = Vec::from(ephemeral_keypair.pk.as_ref()); + full_kkt_message.extend_from_slice(masked_byte.as_slice()); + let encrypted_kkt_frame = carrier.encrypt(&self.to_bytes())?; + full_kkt_message.extend_from_slice(&encrypted_kkt_frame); + + Ok((carrier, full_kkt_message)) + } + + pub fn decrypt_initiator_frame( + responder_keypair: &DHKeyPair, + message: &[u8], + supported_versions: &[u8], + ) -> Result<(Carrier, KKTFrame, KKTContext), KKTError> { + let mut initiator_public_key_bytes: [u8; PUBLIC_KEY_LENGTH] = [0; PUBLIC_KEY_LENGTH]; + initiator_public_key_bytes.clone_from_slice(&message[0..PUBLIC_KEY_LENGTH]); + + // check mask + + let masked_byte = + MaskedByte::try_from(&message[PUBLIC_KEY_LENGTH..PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN])?; + + let mut mask = Vec::from(&initiator_public_key_bytes); + mask.extend_from_slice(responder_keypair.pk.as_ref()); + + // this could be used later when we have multiple versions + // if this call fails, it does before the server has to run a DH + let _outer_protocol_version = + masked_byte.unmask_check_version(&mask, supported_versions)?; + + // now that the version is ok, we can try dh + + let initiator_public_key = DHPublicKey::from_bytes(&initiator_public_key_bytes); + + let shared_secret = responder_keypair + .sk() + .diffie_hellman(&initiator_public_key) + .map_err(|_| KKTError::X25519Error { + info: "Key Derivation Error", + })?; + + let mut context = Vec::from(masked_byte.as_slice()); + context.extend_from_slice(KKT_CARRIER_CONTEXT); + context.extend_from_slice(initiator_public_key.as_ref()); + context.extend_from_slice(responder_keypair.pk.as_ref()); + + let mut carrier = Carrier::from_secret_slice(shared_secret.as_ref(), &context).flip_keys(); + + let decrypted_message = carrier.decrypt(&message[PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN..])?; + let (frame, context) = KKTFrame::from_bytes(&decrypted_message)?; + + Ok((carrier, frame, context)) + } + pub fn context_ref(&self) -> &[u8] { &self.context } @@ -51,42 +129,22 @@ impl KKTFrame { KKTContext::try_decode(self.context) } - pub fn signature_ref(&self) -> &[u8] { - &self.signature - } - pub fn body_ref(&self) -> &[u8] { &self.body } - pub fn session_id_ref(&self) -> &[u8] { - &self.session_id - } - pub fn session_id(&self) -> [u8; KKT_SESSION_ID_LEN] { - self.session_id - } - - pub fn signature_mut(&mut self) -> &mut [u8] { - &mut self.signature - } pub fn body_mut(&mut self) -> &mut [u8] { &mut self.body } - pub fn session_id_mut(&mut self) -> &mut [u8] { - &mut self.session_id - } - pub fn frame_length(&self) -> usize { - self.context.len() + self.session_id.len() + self.body.len() + self.signature.len() + self.context.len() + self.body.len() } pub fn to_bytes(&self) -> Vec { let mut bytes = Vec::with_capacity(self.frame_length()); bytes.extend_from_slice(&self.context); bytes.extend_from_slice(&self.body); - bytes.extend_from_slice(&self.session_id); - bytes.extend_from_slice(&self.signature); bytes } @@ -115,7 +173,6 @@ impl KKTFrame { } let mut body = Vec::new(); - let mut signature = Vec::new(); // decode body if context.body_len() > 0 { @@ -123,33 +180,7 @@ impl KKTFrame { body.extend_from_slice(body_bytes); } - let session_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() - ..KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN]; - // SAFETY: we're using exactly KKT_SESSION_ID_LEN bytes and we checked for sufficient bytes - #[allow(clippy::unwrap_used)] - let session_id = session_bytes.try_into().unwrap(); - - // // old code left for reference if session id becomes variable in length: - // if context.session_id_len() > 0 { - // session_id.extend_from_slice( - // &bytes[KKT_CONTEXT_LEN + context.body_len() - // ..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()], - // ); - // } - - // decode signature - if context.signature_len() > 0 { - let signature_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN - ..KKT_CONTEXT_LEN - + context.body_len() - + KKT_SESSION_ID_LEN - + context.signature_len()]; - signature.extend_from_slice(signature_bytes); - } - - Ok(( - KKTFrame::new(context_bytes, &body, session_id, &signature), - context, - )) + let frame = KKTFrame::new(&context, &body)?; + Ok((frame, context)) } } diff --git a/common/nym-kkt/src/initiator.rs b/common/nym-kkt/src/initiator.rs new file mode 100644 index 0000000000..2cdc3ead4c --- /dev/null +++ b/common/nym-kkt/src/initiator.rs @@ -0,0 +1,167 @@ +use libcrux_psq::handshake::types::DHPublicKey; +use nym_kkt_ciphersuite::Ciphersuite; +use rand09::{CryptoRng, RngCore}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::{ + carrier::Carrier, + context::{KKTContext, KKTMode, KKTRole, KKTStatus}, + error::KKTError, + frame::KKTFrame, + key_utils::validate_encapsulation_key, +}; + +pub struct KKTInitiator<'a> { + carrier: Carrier, + context: KKTContext, + expected_hash: &'a [u8], +} +impl<'a> Zeroize for KKTInitiator<'a> { + fn zeroize(&mut self) { + self.carrier.zeroize(); + } +} +impl<'a> ZeroizeOnDrop for KKTInitiator<'a> {} + +impl<'a> KKTInitiator<'a> { + // to be used by clients + pub fn generate_one_way_request( + rng: &mut R, + ciphersuite: &Ciphersuite, + responder_dh_public_key: &DHPublicKey, + expected_hash: &'a [u8], + outer_protocol_version: u8, + ) -> Result<(Self, Vec), KKTError> + where + R: CryptoRng + RngCore, + { + Self::generate_encrypted_request( + rng, + KKTMode::OneWay, + ciphersuite, + None, + responder_dh_public_key, + expected_hash, + outer_protocol_version, + ) + } + + // to be used by nodes + pub fn generate_mutual_request<'b, R>( + rng: &mut R, + ciphersuite: &Ciphersuite, + local_encapsulation_key: &'b [u8], + responder_dh_public_key: &DHPublicKey, + expected_hash: &'a [u8], + outer_protocol_version: u8, + ) -> Result<(Self, Vec), KKTError> + where + R: CryptoRng + RngCore, + { + Self::generate_encrypted_request( + rng, + KKTMode::Mutual, + ciphersuite, + Some(local_encapsulation_key), + responder_dh_public_key, + expected_hash, + outer_protocol_version, + ) + } + + fn generate_encrypted_request<'b, R>( + rng: &mut R, + mode: KKTMode, + ciphersuite: &Ciphersuite, + local_encapsulation_key: Option<&'b [u8]>, + responder_dh_public_key: &DHPublicKey, + expected_hash: &'a [u8], + outer_protocol_version: u8, + ) -> Result<(Self, Vec), KKTError> + where + R: CryptoRng + RngCore, + { + let (context, frame) = initiator_process(mode, ciphersuite, local_encapsulation_key)?; + let (carrier, message_bytes) = + frame.encrypt_initiator_frame(rng, responder_dh_public_key, outer_protocol_version)?; + + Ok(( + Self { + carrier, + context, + expected_hash, + }, + message_bytes, + )) + } + + // bool would be true if the initiator was using mutual mode + // and the responder was able to verify the initiator's kem key + pub fn process_response(&mut self, response_bytes: &[u8]) -> Result<(Vec, bool), KKTError> { + let decrypted_response_bytes = self.carrier.decrypt(response_bytes)?; + let (response_frame, remote_context) = KKTFrame::from_bytes(&decrypted_response_bytes)?; + initiator_ingest_response( + &mut self.context, + &response_frame, + &remote_context, + self.expected_hash, + ) + } +} + +pub fn initiator_process<'a>( + mode: KKTMode, + ciphersuite: &Ciphersuite, + own_encapsulation_key: Option<&'a [u8]>, +) -> Result<(KKTContext, KKTFrame), KKTError> { + let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite); + + let body: &[u8] = match mode { + KKTMode::OneWay => &[], + KKTMode::Mutual => match own_encapsulation_key { + Some(encaps_key) => encaps_key, + + // Missing key + None => { + return Err(KKTError::FunctionInputError { + info: "KEM Key Not Provided", + }); + } + }, + }; + + let frame = KKTFrame::new(&context, body)?; + + Ok((context, frame)) +} + +pub fn initiator_ingest_response( + own_context: &mut KKTContext, + remote_frame: &KKTFrame, + remote_context: &KKTContext, + expected_hash: &[u8], +) -> Result<(Vec, bool), KKTError> { + match remote_context.status() { + KKTStatus::Ok | KKTStatus::UnverifiedKEMKey => { + match validate_encapsulation_key( + own_context.ciphersuite().hash_function(), + own_context.ciphersuite().hash_len(), + remote_frame.body_ref(), + expected_hash, + ) { + true => Ok(( + remote_frame.body_ref().to_vec(), + remote_context.status() != KKTStatus::UnverifiedKEMKey, + )), + + // The key does not match the hash obtained from the directory + false => Err(KKTError::KEMError { + info: "Hash of received encapsulation key does not match the value stored on the directory.", + }), + } + } + _ => Err(KKTError::ResponderFlaggedError { + status: remote_context.status(), + }), + } +} diff --git a/common/nym-kkt/src/key_utils.rs b/common/nym-kkt/src/key_utils.rs index 2ea88d231b..a5c5b11aca 100644 --- a/common/nym-kkt/src/key_utils.rs +++ b/common/nym-kkt/src/key_utils.rs @@ -1,70 +1,38 @@ -use crate::ciphersuite::HashFunction; use std::collections::HashMap; -use classic_mceliece_rust::keypair_boxed; - -use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests}; +use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey}; +use libcrux_psq::handshake::types::DHKeyPair; +use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KeyDigests}; use rand09::{CryptoRng, RngCore}; -pub fn generate_keypair_ed25519( - rng: &mut R, - index: Option, -) -> nym_crypto::asymmetric::ed25519::KeyPair +pub fn generate_keypair_x25519(rng: &mut R) -> DHKeyPair where R: RngCore + CryptoRng, { - let mut secret_initiator: [u8; 32] = [0u8; 32]; - rng.fill_bytes(&mut secret_initiator); - nym_crypto::asymmetric::ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0)) + DHKeyPair::new(rng) } -pub fn generate_keypair_x25519(rng: &mut R) -> nym_crypto::asymmetric::x25519::KeyPair +pub fn generate_keypair_mlkem(rng: &mut R) -> (MlKem768PrivateKey, MlKem768PublicKey) where R: RngCore + CryptoRng, { - let mut secret_initiator: [u8; 32] = [0u8; 32]; - rng.fill_bytes(&mut secret_initiator); - - let private_key = nym_crypto::asymmetric::x25519::PrivateKey::from_secret(secret_initiator); - private_key.into() + libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng).into_parts() } // (decapsulation_key, encapsulation_key) -pub fn generate_keypair_libcrux( - rng: &mut R, - kem: crate::ciphersuite::KEM, -) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), crate::error::KKTError> -where - R: RngCore + CryptoRng, -{ - match kem { - crate::ciphersuite::KEM::MlKem768 => { - Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, rng)?) - } - crate::ciphersuite::KEM::XWing => Ok(libcrux_kem::key_gen( - libcrux_kem::Algorithm::XWingKemDraft06, - rng, - )?), - crate::ciphersuite::KEM::X25519 => { - Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, rng)?) - } - _ => Err(crate::error::KKTError::KEMError { - info: "Key Generation Error: Unsupported Libcrux Algorithm", - }), - } -} -// (decapsulation_key, encapsulation_key) -pub fn generate_keypair_mceliece<'a, R>( +pub fn generate_keypair_mceliece( rng: &mut R, ) -> ( - classic_mceliece_rust::SecretKey<'a>, - classic_mceliece_rust::PublicKey<'a>, + libcrux_psq::classic_mceliece::SecretKey, + libcrux_psq::classic_mceliece::PublicKey, ) where + // this is annoying because mceliece lib uses rand 0.8.5... R: RngCore + CryptoRng, { - let (encapsulation_key, decapsulation_key) = keypair_boxed(rng); - (decapsulation_key, encapsulation_key) + let kp = libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng); + + (kp.sk, kp.pk) } pub fn hash_key_bytes( diff --git a/common/nym-kkt/src/kkt.rs b/common/nym-kkt/src/kkt.rs deleted file mode 100644 index c1d21982f3..0000000000 --- a/common/nym-kkt/src/kkt.rs +++ /dev/null @@ -1,449 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -//! Convenience wrappers around KKT protocol functions for easier integration. -//! -//! This module provides simplified APIs for the common use case of exchanging -//! KEM public keys between a client (initiator) and gateway (responder). -//! -//! The underlying KKT protocol is implemented in the `session` module. - -use nym_crypto::asymmetric::{ed25519, x25519}; -use rand09::{CryptoRng, RngCore}; - -use crate::{ - ciphersuite::{Ciphersuite, EncapsulationKey}, - context::{KKTContext, KKTMode}, - encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame}, - error::KKTError, -}; - -// Re-export core session functions for advanced use cases -pub use crate::session::{ - anonymous_initiator_process, initiator_ingest_response, initiator_process, - responder_ingest_message, responder_process, -}; - -use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame}; -use crate::frame::KKTFrame; - -/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode). -/// -/// This is the client-side operation that initiates a KKT exchange. -/// The request will be signed with the provided signing key. -/// -/// # Arguments -/// * `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 -/// -/// # Returns -/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses -/// * `KKTContext` - Context to use when validating the response -/// * `Vec` - Contains the client's ephemeral public key and encrypted and signed bytes to send to responder -/// -/// # Example -/// ```ignore -/// let (session_secret, context, request_frame) = request_kem_key( -/// &mut rng, -/// ciphersuite, -/// client_signing_key, -/// responder_dh_public_key, -/// )?; -/// // Send request_frame to gateway -/// ``` -pub fn request_kem_key( - rng: &mut R, - ciphersuite: Ciphersuite, - signing_key: &ed25519::PrivateKey, - responder_dh_public_key: &x25519::PublicKey, -) -> Result<(KKTSessionSecret, KKTContext, Vec), KKTError> { - // OneWay mode: client only wants responder's KEM key - // None: client doesn't send their own KEM key - let (initiator_context, initiator_frame) = - initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)?; - - // Generate the session's shared secret and encrypt the Initiator's request - let (session_secret, encrypted_request_bytes) = - encrypt_initial_kkt_frame(rng, responder_dh_public_key, &initiator_frame)?; - - Ok((session_secret, initiator_context, encrypted_request_bytes)) -} - -/// Decrypt, validate an *Encrypted* KKT response and extract the responder's KEM public key. -/// -/// This is the client-side operation that processes the gateway's response. -/// It verifies the signature and validates the key hash against the expected value -/// (typically retrieved from a directory service). -/// -/// # Arguments -/// * `context` - Context from the initial request -/// * `session_secret` - Session Secret Key (generated with request) -/// * `responder_vk` - Responder's Ed25519 verification key (from directory) -/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory) -/// * `response_bytes` - Serialized response frame from responder -/// -/// # Returns -/// * `EncapsulationKey` - Authenticated KEM public key of the responder -/// -/// # Example -/// ```ignore -/// let gateway_kem_key = validate_kem_response( -/// &context, -/// &session_secret, -/// &gateway_verification_key, -/// &expected_hash_from_directory, -/// &response_bytes, -/// )?; -/// // Use gateway_kem_key for PSQ -/// ``` -pub fn validate_kem_response<'a>( - context: &mut KKTContext, - session_secret: &KKTSessionSecret, - responder_vk: &ed25519::PublicKey, - expected_key_hash: &[u8], - encrypted_response_bytes: &[u8], -) -> Result, KKTError> { - let (responder_frame, responder_context) = - decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?; - - initiator_ingest_response( - context, - &responder_frame, - &responder_context, - responder_vk, - expected_key_hash, - ) -} - -/// Decrypts and validates an *Encrypted* KKT response -/// -/// This is the client-side operation that processes the gateway's response. -pub fn decrypt_kkt_response_frame( - session_secret: &KKTSessionSecret, - frame_ciphertext: &[u8], -) -> Result<(KKTFrame, KKTContext), KKTError> { - decrypt_kkt_frame(session_secret, frame_ciphertext, KKT_RESPONSE_AAD) -} - -/// Handle an *Encrypted* KKT request and generate a signed response with the responder's KEM key. -/// -/// This is the gateway-side operation that processes a client's KKT request. -/// It validates the request signature (if authenticated) and responds with -/// the gateway's KEM public key, signed for authenticity. -/// -/// # Arguments -/// * `encrypted_request_bytes` - encrypted KEM request -/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous) -/// * `responder_signing_key` - Gateway's Ed25519 signing key -/// * `responder_dh_public_key` - Gateway's long-term x25519 Diffie-Hellman private key -/// * `responder_kem_key` - Gateway's KEM public key to send -/// -/// # Returns -/// * `KKTFrame` - Signed response frame containing the KEM public key -/// -/// # Example -/// ```ignore -/// let response_frame = handle_kem_request( -/// &request_frame, -/// Some(client_verification_key), // or None for anonymous -/// gateway_signing_key, -/// &gateway_kem_public_key, -/// )?; -/// // Send response_frame back to client -/// ``` -pub fn handle_kem_request<'a, R>( - rng: &mut R, - encrypted_request_bytes: &[u8], - initiator_vk: Option<&ed25519::PublicKey>, - responder_signing_key: &ed25519::PrivateKey, - responder_dh_private_key: &x25519::PrivateKey, - responder_kem_key: &EncapsulationKey<'a>, -) -> Result, KKTError> -where - R: RngCore + CryptoRng, -{ - // Compute the session's shared secret, decrypt and parse context from the request frame - - let (session_secret, request_frame, initiator_context) = - decrypt_initial_kkt_frame(responder_dh_private_key, encrypted_request_bytes)?; - - // Validate the request (verifies signature if initiator_vk provided) - let (mut response_context, _) = responder_ingest_message( - &initiator_context, - initiator_vk, - None, // Not checking initiator's KEM key in OneWay mode - &request_frame, - )?; - - // Generate signed response with our KEM public key - let responder_frame = responder_process( - &mut response_context, - request_frame.session_id(), - responder_signing_key, - responder_kem_key, - )?; - - // Encrypt the responder's response with the session's shared secret - encrypt_kkt_frame(rng, &session_secret, &responder_frame, KKT_RESPONSE_AAD) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - ciphersuite::{HashFunction, KEM, SignatureScheme}, - key_utils::{generate_keypair_libcrux, hash_encapsulation_key}, - }; - - fn random_x25519_key() -> x25519::PrivateKey { - let mut bytes = [0u8; 32]; - 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 = rand09::rng(); - - // Generate Ed25519 keypairs for both parties - let mut initiator_secret = [0u8; 32]; - rng.fill_bytes(&mut initiator_secret); - let ed25519_init = ed25519::KeyPair::from_secret(initiator_secret, 0); - - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let ed25519_resp = ed25519::KeyPair::from_secret(responder_secret, 1); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - // Generate responder's KEM keypair (X25519 for testing) - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - // Create ciphersuite - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // Hash the KEM key (simulating directory storage) - let key_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &responder_kem_key.encode(), - ); - - // Client: Request KEM key - let (session_key, context, request_frame_ciphertext) = request_kem_key( - &mut rng, - ciphersuite, - ed25519_init.private_key(), - &x25519_resp_pub, - ) - .unwrap(); - - // Gateway: Handle request - let response_frame_ciphertext = handle_kem_request( - &mut rng, - &request_frame_ciphertext, - Some(ed25519_init.public_key()), // Authenticated - ed25519_resp.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ) - .unwrap(); - - // Client: Validate response - let obtained_key = validate_kem_response( - &context, - &session_key, - ed25519_resp.public_key(), - &key_hash, - &response_frame_ciphertext, - ) - .unwrap(); - - // Verify we got the correct KEM key - assert_eq!(obtained_key.encode(), responder_kem_key.encode()); - } - - #[test] - fn test_kkt_wrappers_anonymous() { - let mut rng = rand09::rng(); - - // Only responder has keys - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - let key_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &responder_kem_key.encode(), - ); - - // Anonymous initiator - 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) = - encrypt_initial_kkt_frame(&mut rng, &x25519_resp_pub, &request_frame).unwrap(); - - // Gateway: Handle anonymous request - let response_frame = handle_kem_request( - &mut rng, - &encrypted_request_bytes, - None, // Anonymous - no verification key - responder_keypair.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ) - .unwrap(); - - // Initiator: Validate response - let obtained_key = validate_kem_response( - &context, - &session_secret, - responder_keypair.public_key(), - &key_hash, - &response_frame, - ) - .unwrap(); - - assert_eq!(obtained_key.encode(), responder_kem_key.encode()); - } - - #[test] - fn test_invalid_signature_rejected() { - let mut rng = rand09::rng(); - - let mut initiator_secret = [0u8; 32]; - rng.fill_bytes(&mut initiator_secret); - let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0); - - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - // Different keypair for wrong signature - let mut wrong_secret = [0u8; 32]; - rng.fill_bytes(&mut wrong_secret); - let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - let (_session_key, _context, request_frame_ciphertext) = request_kem_key( - &mut rng, - ciphersuite, - initiator_keypair.private_key(), - &x25519_resp_pub, - ) - .unwrap(); - - // Gateway handles request but we provide WRONG verification key - let result = handle_kem_request( - &mut rng, - &request_frame_ciphertext, - Some(wrong_keypair.public_key()), // Wrong key! - responder_keypair.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ); - - // Should fail signature verification - assert!(result.is_err()); - } - - #[test] - fn test_hash_mismatch_rejected() { - let mut rng = rand09::rng(); - - let mut initiator_secret = [0u8; 32]; - rng.fill_bytes(&mut initiator_secret); - let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0); - - let mut responder_secret = [0u8; 32]; - rng.fill_bytes(&mut responder_secret); - let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1); - - let x25519_resp_priv = random_x25519_key(); - let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); - - let ciphersuite = Ciphersuite::resolve_ciphersuite( - KEM::X25519, - HashFunction::Blake3, - SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // Use WRONG hash - let wrong_hash = [0u8; 32]; - - let (session_key, context, request_frame) = request_kem_key( - &mut rng, - ciphersuite, - initiator_keypair.private_key(), - &x25519_resp_pub, - ) - .unwrap(); - - let response_frame = handle_kem_request( - &mut rng, - &request_frame, - Some(initiator_keypair.public_key()), - responder_keypair.private_key(), - &x25519_resp_priv, - &responder_kem_key, - ) - .unwrap(); - - // Client validates with WRONG hash - let result = validate_kem_response( - &context, - &session_key, - responder_keypair.public_key(), - &wrong_hash, // Wrong! - &response_frame, - ); - - // Should fail hash validation - assert!(result.is_err()); - } -} diff --git a/common/nym-kkt/src/lib.rs b/common/nym-kkt/src/lib.rs index d879cf2286..066df57daf 100644 --- a/common/nym-kkt/src/lib.rs +++ b/common/nym-kkt/src/lib.rs @@ -1,498 +1,210 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod ciphersuite; +pub mod carrier; pub mod context; -pub mod encryption; pub mod error; pub mod frame; +pub mod initiator; pub mod key_utils; -// pub mod kkt; -pub mod session; +pub mod masked_byte; +pub mod rekey; +pub mod responder; // This must be less than 4 bits pub const KKT_VERSION: u8 = 1; const _: () = assert!(KKT_VERSION < 1 << 4); -pub const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response"; -pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_FRAME"; #[cfg(test)] mod test { + use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme}; + use crate::{ - KKT_RESPONSE_AAD, - ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM}, - encryption::{ - decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame, - encrypt_kkt_frame, - }, - frame::KKTFrame, + initiator::KKTInitiator, key_utils::{ - generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece, - generate_keypair_x25519, hash_encapsulation_key, - }, - session::{ - anonymous_initiator_process, initiator_ingest_response, initiator_process, - responder_ingest_message, responder_process, + generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519, + hash_encapsulation_key, }, + responder::KKTResponder, }; #[test] - fn test_kkt_psq_e2e_clear() { + fn test_kkt_psq_e2e_encrypted_carrier() { let mut rng = rand09::rng(); - // generate ed25519 keys - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] { - for hash_function in [ - HashFunction::Blake3, - HashFunction::SHA256, - HashFunction::Shake128, - HashFunction::Shake256, - ] { - let ciphersuite = Ciphersuite::resolve_ciphersuite( - kem, - hash_function, - crate::ciphersuite::SignatureScheme::Ed25519, - None, - ) - .unwrap(); - - // generate kem public keys - - let (responder_kem_public_key, initiator_kem_public_key) = match kem { - KEM::MlKem768 => ( - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::XWing => ( - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - ), - KEM::X25519 => ( - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::McEliece => ( - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - ), - }; - - let i_kem_key_bytes = initiator_kem_public_key.encode(); - - let r_kem_key_bytes = responder_kem_public_key.encode(); - - let i_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &i_kem_key_bytes, - ); - - let r_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &r_kem_key_bytes, - ); - - // Anonymous Initiator, OneWay - { - 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 (r_context, _) = - responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap(); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - let r_bytes = r_frame.to_bytes(); - - let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - // Initiator, OneWay - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) - .unwrap(); - - let i_frame_bytes = i_frame.to_bytes(); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap(); - - assert!(r_obtained_key.is_none()); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - let r_bytes = r_frame.to_bytes(); - - let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - - // Initiator, Mutual - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::Mutual, - ciphersuite, - initiator_ed25519_keypair.private_key(), - Some(&initiator_kem_public_key), - ) - .unwrap(); - - let i_frame_bytes = i_frame.to_bytes(); - - let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap(); - - assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - let r_bytes = r_frame.to_bytes(); - - let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - } - } - } - #[test] - fn test_kkt_psq_e2e_encrypted() { - let mut rng = rand09::rng(); - - // generate ed25519 keys - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - // generate responder x25519 keys let responder_x25519_keypair = generate_keypair_x25519(&mut rng); - for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] { - for hash_function in [ - HashFunction::Blake3, - HashFunction::SHA256, - HashFunction::Shake128, - HashFunction::Shake256, - ] { + for hash_function in [ + HashFunction::Blake3, + HashFunction::SHA256, + HashFunction::Shake128, + HashFunction::Shake256, + ] { + // generate kem public keys + + let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng); + let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng); + + let r_dir_hash_mlkem = hash_encapsulation_key( + // &ciphersuite.hash_function(), + &hash_function, + // ciphersuite.hash_len(), + HashLength::Default.value(), + responder_mlkem_keypair.1.as_slice().as_slice(), + ); + + let r_dir_hash_mceliece = hash_encapsulation_key( + // &ciphersuite.hash_function(), + &hash_function, + // ciphersuite.hash_len(), + HashLength::Default.value(), + responder_mceliece_keypair.1.as_ref(), + ); + let initiator_mlkem_keypair = generate_keypair_mlkem(&mut rng); + let initiator_mceliece_keypair = generate_keypair_mceliece(&mut rng); + + let _i_dir_hash_mlkem = hash_encapsulation_key( + // &ciphersuite.hash_function(), + &hash_function, + // ciphersuite.hash_len(), + HashLength::Default.value(), + initiator_mlkem_keypair.1.as_slice().as_slice(), + ); + + let _i_dir_hash_mceliece = hash_encapsulation_key( + // &ciphersuite.hash_function(), + &hash_function, + // ciphersuite.hash_len(), + HashLength::Default.value(), + initiator_mceliece_keypair.1.as_ref(), + ); + + let responder = KKTResponder::new( + &responder_x25519_keypair, + Some(&responder_mlkem_keypair.1), + Some(&responder_mceliece_keypair.1), + &[ + HashFunction::Blake3, + HashFunction::SHA256, + HashFunction::Shake128, + HashFunction::Shake256, + ], + &[1], + &[SignatureScheme::Ed25519], + ) + .unwrap(); + + // OneWay - MlKem + { let ciphersuite = Ciphersuite::resolve_ciphersuite( - kem, + KEM::MlKem768, hash_function, - crate::ciphersuite::SignatureScheme::Ed25519, + SignatureScheme::Ed25519, None, ) .unwrap(); + let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request( + &mut rng, + &ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mlkem, + 1u8, + ) + .unwrap(); - // generate kem public keys + let (response_bytes, _) = responder.process_request(&request_bytes).unwrap(); - let (responder_kem_public_key, initiator_kem_public_key) = match kem { - KEM::MlKem768 => ( - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::MlKem768( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::XWing => ( - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), - ), - KEM::X25519 => ( - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - EncapsulationKey::X25519( - generate_keypair_libcrux(&mut rng, kem).unwrap().1, - ), - ), - KEM::McEliece => ( - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1), - ), - }; + let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap(); - let i_kem_key_bytes = initiator_kem_public_key.encode(); + assert_eq!( + i_obtained_key, + responder_mlkem_keypair.1.as_slice().as_slice(), + ) + } + // Mutual - MlKem + { + let ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::MlKem768, + hash_function, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request( + &mut rng, + &ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mlkem, + 1u8, + ) + .unwrap(); - let r_kem_key_bytes = responder_kem_public_key.encode(); + let (response_bytes, r_obtained_key) = + responder.process_request(&request_bytes).unwrap(); - let i_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &i_kem_key_bytes, - ); + // if we keep unverified keys, this should change + assert!(r_obtained_key.is_none()); - let r_dir_hash = hash_encapsulation_key( - &ciphersuite.hash_function(), - ciphersuite.hash_len(), - &r_kem_key_bytes, - ); + let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap(); - // Anonymous Initiator, OneWay - { - let (i_context, i_frame) = - anonymous_initiator_process(&mut rng, ciphersuite).unwrap(); + assert_eq!( + i_obtained_key, + responder_mlkem_keypair.1.as_slice().as_slice(), + ) + } - // encryption - initiator frame + // OneWay - McEliece + { + let ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::McEliece, + hash_function, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request( + &mut rng, + &ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mceliece, + 1u8, + ) + .unwrap(); - let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame( - &mut rng, - responder_x25519_keypair.public_key(), - &i_frame, - ) - .unwrap(); + let (response_bytes, _) = responder.process_request(&request_bytes).unwrap(); - // decryption - initiator frame + let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap(); - let (r_session_secret, i_frame_r, i_context_r) = - decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes) - .unwrap(); + assert_eq!(i_obtained_key, responder_mceliece_keypair.1.as_ref(),) + } + // Mutual - MlKem + { + let ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::McEliece, + hash_function, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request( + &mut rng, + &ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mceliece, + 1u8, + ) + .unwrap(); - let (r_context, _) = - responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap(); + let (response_bytes, r_obtained_key) = + responder.process_request(&request_bytes).unwrap(); - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); + // if we keep unverified keys, this should change + assert!(r_obtained_key.is_none()); - // encryption - responder frame - let r_bytes = - encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD) - .unwrap(); + let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap(); - // decryption - responder frame - - let (i_frame_r, i_context_r) = - decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - // Initiator, OneWay - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::OneWay, - ciphersuite, - initiator_ed25519_keypair.private_key(), - None, - ) - .unwrap(); - - // encryption - initiator frame - - let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame( - &mut rng, - responder_x25519_keypair.public_key(), - &i_frame, - ) - .unwrap(); - - // decryption - initiator frame - - let (r_session_secret, i_frame_r, r_context) = - decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes) - .unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &r_context, - Some(initiator_ed25519_keypair.public_key()), - None, - &i_frame_r, - ) - .unwrap(); - - assert!(r_obtained_key.is_none()); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - // encryption - responder frame - let r_bytes = - encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD) - .unwrap(); - - // decryption - responder frame - - let (i_frame_r, i_context_r) = - decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } - - // Initiator, Mutual - { - let (i_context, i_frame) = initiator_process( - &mut rng, - crate::context::KKTMode::Mutual, - ciphersuite, - initiator_ed25519_keypair.private_key(), - Some(&initiator_kem_public_key), - ) - .unwrap(); - - // encryption - initiator frame - - let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame( - &mut rng, - responder_x25519_keypair.public_key(), - &i_frame, - ) - .unwrap(); - - // decryption - initiator frame - - let (r_session_secret, i_frame_r, i_context_r) = - decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes) - .unwrap(); - - let (r_context, r_obtained_key) = responder_ingest_message( - &i_context_r, - Some(initiator_ed25519_keypair.public_key()), - Some(&i_dir_hash), - &i_frame_r, - ) - .unwrap(); - - assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes); - - let r_frame = responder_process( - &r_context, - i_frame_r.session_id(), - responder_ed25519_keypair.private_key(), - &responder_kem_public_key, - ) - .unwrap(); - - // encryption - responder frame - let r_bytes = - encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD) - .unwrap(); - - // decryption - responder frame - - let (i_frame_r, i_context_r) = - decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap(); - - let i_obtained_key = initiator_ingest_response( - &i_context, - &i_frame_r, - &i_context_r, - responder_ed25519_keypair.public_key(), - &r_dir_hash, - ) - .unwrap(); - - assert_eq!(i_obtained_key.encode(), r_kem_key_bytes) - } + assert_eq!(i_obtained_key, responder_mceliece_keypair.1.as_ref(),) } } } diff --git a/common/nym-kkt/src/masked_byte.rs b/common/nym-kkt/src/masked_byte.rs new file mode 100644 index 0000000000..20dab743b4 --- /dev/null +++ b/common/nym-kkt/src/masked_byte.rs @@ -0,0 +1,190 @@ +use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput}; + +use crate::error::{ + MaskedByteError, + MaskedByteError::{Failure, InvalidLength}, +}; + +pub const MASKED_BYTE_LEN: usize = 16; +pub const MASKED_BYTE_CONTEXT_STR: &[u8] = b"NYM_MASKED_BYTE_V1"; + +const U8_RANGE: [u8; 256] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, + 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, + 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, + 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, + 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, + 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, + 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, + 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, + 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, + 250, 251, 252, 253, 254, 255, +]; + +#[derive(Clone, Copy)] +pub struct MaskedByte([u8; MASKED_BYTE_LEN]); + +impl MaskedByte { + /// Mask a byte by hashing it with some mask. + /// Outputs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF || byte) + pub fn new(byte: u8, mask: &[u8]) -> Self { + let mut output: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN]; + let mut hasher = blake3::Hasher::new(); + hasher.update(MASKED_BYTE_CONTEXT_STR); + hasher.update(mask); + // avoid zero update + hasher.update(&[0xFF, byte]); + hasher.finalize_xof_into(&mut output); + + Self(output) + } + /// Unmasks a byte by trial hashing. + /// This function runs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF). + /// This Hasher state is then cloned updated with `i: u8` in (0..=u8::max). + /// If we find an `i` which yields back the hash input, then we found the masked byte. + /// Otherwise, the function returns an error. + pub fn unmask(&self, mask: &[u8]) -> Result { + self.unmask_check_version(mask, &U8_RANGE) + } + + // This could be more efficient than unmask, + // because we just could check against a smaller list of supported versions. + pub fn unmask_check_version( + &self, + mask: &[u8], + supported_versions: &[u8], + ) -> Result { + let mut buf: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN]; + let mut hasher = blake3::Hasher::new(); + hasher.update(MASKED_BYTE_CONTEXT_STR); + hasher.update(mask); + // avoid zero update + hasher.update(&[0xFF]); + for i in supported_versions { + let mut t_hasher = hasher.clone(); + t_hasher.update(&[*i]); + t_hasher.finalize_xof_into(&mut buf); + if buf == self.0 { + return Ok(*i); + } + } + Err(Failure) + } + + pub fn as_slice(&self) -> &[u8] { + &self.0 + } + + pub fn to_bytes(self) -> [u8; 16] { + self.0 + } +} + +impl From<[u8; MASKED_BYTE_LEN]> for MaskedByte { + fn from(value: [u8; MASKED_BYTE_LEN]) -> Self { + MaskedByte(value) + } +} + +impl From<&[u8; MASKED_BYTE_LEN]> for MaskedByte { + fn from(value: &[u8; MASKED_BYTE_LEN]) -> Self { + MaskedByte(value.to_owned()) + } +} + +impl TryFrom<&[u8]> for MaskedByte { + type Error = MaskedByteError; + + fn try_from(value: &[u8]) -> Result { + if value.len() != MASKED_BYTE_LEN { + Err(InvalidLength { + expected: MASKED_BYTE_LEN, + actual: value.len(), + }) + } else { + Ok(Self::from(value.as_chunks::().0[0])) + } + } +} + +#[cfg(test)] +mod test { + + use crate::masked_byte::MASKED_BYTE_LEN; + + use super::MaskedByte; + use rand09::{Rng, RngCore, rng}; + + #[test] + fn test_masking() { + let mut mask: [u8; 256] = [0u8; 256]; + let mut wire_bytes: [u8; MASKED_BYTE_LEN]; + + // why not + for i in 0..=u8::MAX { + // gen mask + rng().fill_bytes(&mut mask); + let masked_byte = MaskedByte::new(i, &mask); + wire_bytes = masked_byte.to_bytes(); + + let decoded_masked_byte = MaskedByte::from(wire_bytes); + let output = decoded_masked_byte.unmask(&mask).unwrap(); + + assert_eq!(i, output); + + // flip bit + let mut with_flipped_bit = decoded_masked_byte.to_bytes(); + + let byte_idx: usize = rng().random_range(0..MASKED_BYTE_LEN); + let bit_idx = rng().random_range(0..8); + with_flipped_bit[byte_idx] ^= 1 << bit_idx; + + let decoded_masked_byte = MaskedByte::from(with_flipped_bit); + assert!(decoded_masked_byte.unmask(&mask).is_err()); + } + } + + #[test] + fn test_decoding() { + let mut mask: [u8; 256] = [0u8; 256]; + + // gen mask + rng().fill_bytes(&mut mask); + let byte = rng().random(); + let masked_byte = MaskedByte::new(byte, &mask); + let wire_bytes: [u8; MASKED_BYTE_LEN] = masked_byte.to_bytes(); + + // should succeed + let decoded_masked_byte = MaskedByte::try_from(wire_bytes.as_slice()).unwrap(); + let output = decoded_masked_byte.unmask(&mask).unwrap(); + + assert_eq!(byte, output); + + let empty_slice: &[u8] = &[]; + // should fail + assert!(MaskedByte::try_from(empty_slice).is_err()); + + let mut wire_bytes_messy = Vec::from(wire_bytes); + + // add more one more byte + wire_bytes_messy.push(0x42); + assert!(wire_bytes_messy.len() == MASKED_BYTE_LEN + 1); + // should fail + assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err()); + + // pop the added byte + _ = wire_bytes_messy.pop(); + assert!(wire_bytes_messy.len() == MASKED_BYTE_LEN); + // should succeed + assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_ok()); + + // pop one more byte + _ = wire_bytes_messy.pop(); + assert!(wire_bytes_messy.len() == MASKED_BYTE_LEN - 1); + // should fail + assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err()); + } +} diff --git a/common/nym-kkt/src/rekey.rs b/common/nym-kkt/src/rekey.rs new file mode 100644 index 0000000000..d41f7e2a5b --- /dev/null +++ b/common/nym-kkt/src/rekey.rs @@ -0,0 +1,232 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Post-Quantum Re-Key Protocol +/// This module implements a stateless post-quantum re-keying protocol in one round-trip. +/// We currently support MlKem768 and XWing. +/// +/// This protocol is safe if it runs under a trusted secure channel. +/// +/// Bandwidth costs: +/// Request (MlKem768): 1216 bytes +/// Response (MlKem768): 1088 bytes +/// Request (XWing): 1248 bytes +/// Response (XWing): 1120 bytes +use libcrux_kem::*; +use nym_crypto::hkdf::blake3::derive_key_blake3; +use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing}; +use rand09::{CryptoRng, RngCore}; +use zeroize::Zeroize; + +use crate::error::KKTError; + +/// Context string to be used with the Blake3 KDF. +const REKEY_CONTEXT: &str = "NYM_PQ_REKEY_v1"; + +pub struct RekeyInitiator { + algorithm: Algorithm, + decapsulation_key: PrivateKey, + salt: [u8; 32], +} + +impl RekeyInitiator { + /// The Initiator generates an ephemeral KEM keypair and a 32-byte salt. + /// The Initiator keeps the decapsulation key and generates a request message. + /// The request message contains the salt and an encoding of the encapsulation key as follows + /// salt encapsulation_key + /// [0 ........ 32 | 32 .............. ] + /// + /// Inputs: + /// rng: something that implements CryptoRng + RngCore + /// kem: a KEM algorithm (we currently support MlKem768 and XWing) + /// + /// Outputs: + /// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use. + /// Vec: The request message as explained above. This is to be sent to the responder as-is. + pub fn generate_request(rng: &mut R, kem: KEM) -> Result<(RekeyInitiator, Vec), KKTError> + where + R: CryptoRng + RngCore, + { + let (algorithm, buffer_size) = match kem { + KEM::XWing => (Algorithm::XWingKemDraft06, 32 + xwing::PUBLIC_KEY_LENGTH), + KEM::MlKem768 => (Algorithm::MlKem768, 32 + ml_kem768::PUBLIC_KEY_LENGTH), + // We don't support McEliece because the keys are massive. + // If this is a deal-breaker, users can start a new session with PSQ which can use McEliece. + KEM::McEliece => { + return Err(KKTError::UnsupportedAlgorithm { + info: "McEliece is not supported for re-keying", + }); + } + // We don't support X25519 because it's not post-quantum secure. + KEM::X25519 => { + return Err(KKTError::UnsupportedAlgorithm { + info: "X25519 is not supported for re-keying", + }); + } + }; + + // Generate the Initiator's salt + let mut salt = [0u8; 32]; + rng.fill_bytes(&mut salt); + + // Create the buffer for the request message and copy the salt into it. + let mut request_buffer = Vec::with_capacity(buffer_size); + request_buffer.extend_from_slice(&salt); + + // Generate the ephemeral KEM keypair based on the algorithm from the function's input. + let (decapsulation_key, encapsulation_key) = key_gen(algorithm, rng)?; + + // Append the encoding of the KEM encapsulation key to the initiator's randomness. + request_buffer.extend(encapsulation_key.encode()); + + Ok(( + // The Initiator should store this until they use `RekeyInitiator::finalize`. + RekeyInitiator { + algorithm, + decapsulation_key, + salt, + }, + // This is to be sent to the responder. + request_buffer, + )) + } + + /// The Initiator will attempt to decapsulate the `pre_key` generated by the responder + /// secret. This `pre_key` will be combined with the Initiator's previously generated salt + /// as input to a Blake3 KDF call to generate the new shared secret. + /// + /// This function fails if the ciphertext cannot be decoded or decapsulated. + /// + /// Input: + /// response_message: the responder's message which contains an encapsulation of `pre_key`. + /// Output: + /// [u8; 32]: the new shared secret. + pub fn finalize(mut self, response_message: &[u8]) -> Result<[u8; 32], KKTError> { + // Decode the responder's ciphertext. + let ciphertext = Ct::decode(self.algorithm, response_message)?; + // Decapsulate the `pre_key` using the Initiator's decapsulation key. + let pre_key = ciphertext.decapsulate(&self.decapsulation_key)?; + + // Encode the `pre_key` into bytes + let pre_key_bytes = pre_key.encode(); + + let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, &self.salt); + + // Zeroize the Initiator's salt + self.salt.zeroize(); + + // TODO: zeroize the decapsulation key + + Ok(new_secret) + } +} + +/// The responder parses the request message. +/// The first 32 bytes are the Initiator's salt, +/// and the remainder is the encoding of the public key. +/// Given that XWing and MlKem768 have different key lengths, +/// we could deduce the algorithm from that. +/// +/// If the message is badly formatted, or the encapsulation received is invalid, +/// this function will produce an error. +/// +/// If everything is alright, the responder generates and encapsulates a key `pre_key` to send to the Initiator. +/// Then, the responder calls a Blake3 KDF over `pre_key` and the Initiator's salt to obtain +/// the new shared secret. +/// +/// Inputs: +/// rng: something that implements CryptoRng + RngCore +/// request_message: the Initiator's request message (contains the salt and encapsulation key) +/// +/// Outputs: +/// [u8; 32]: new shared secret +/// Vec: response which contains an encapsulation of a secret value generated by the responder. +/// This is to be sent back to the Initiator as-is. +pub fn responder_process( + rng: &mut R, + mut request_message: Vec, +) -> Result<([u8; 32], Vec), KKTError> +where + R: CryptoRng + RngCore, +{ + // Deduce the KEM algorithm from the message length + let algorithm = match request_message.len().checked_sub(32) { + // + Some(num) => match num { + // If message length is 1216 (32 + 1184) then the algorithm should be MlKem768 + ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768, + // If message length is 1248 (32 + 1216) then the algorithm should be MlKem768 + xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06, + // We don't support McEliece because the keys are massive. + // If this is a deal-breaker, users can start a new session with PSQ which can use McEliece. + mceliece::PUBLIC_KEY_LENGTH => { + return Err(KKTError::UnsupportedAlgorithm { + info: "McEliece is not supported for re-keying", + }); + } + // We don't support X25519 because it's not post-quantum secure. + x25519::PUBLIC_KEY_LENGTH => { + return Err(KKTError::UnsupportedAlgorithm { + info: "McEliece is not supported for re-keying", + }); + } + // Reject if the size does not match any of the above. + _ => { + return Err(KKTError::UnsupportedAlgorithm { + info: "Unknown Algorithm", + }); + } + }, + // Reject if message length is less than 32. + None => { + return Err(KKTError::DecodingError { + info: "Invalid rekey request: size is too small", + }); + } + }; + + // Split the message to get the Initiator's salt (first 32 bytes) + // and the encoding of the Initiator's public key. + let (remote_salt, remote_encapsulation_key_bytes) = request_message.split_at_mut(32); + + // Attempt to decode the Initiator's encapsulation key. + let remote_encapsulation_key = PublicKey::decode(algorithm, remote_encapsulation_key_bytes)?; + + // Encapsulate a fresh `pre_key` using the Initiator's encapsulation key into `ciphertext`. + let (pre_key, ciphertext) = remote_encapsulation_key.encapsulate(rng)?; + // Encode the ciphertext into bytes to send back to the initiator. + let message = ciphertext.encode(); + + // Encode the `pre_key` into bytes + let pre_key_bytes = pre_key.encode(); + + let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, remote_salt); + + // Zeroize the Initiator's salt + remote_salt.zeroize(); + + Ok((new_secret, message)) +} + +#[cfg(test)] +mod tests { + use crate::rekey::{RekeyInitiator, responder_process}; + use nym_kkt_ciphersuite::KEM; + + #[test] + fn rekey_test() { + let mut rng = rand09::rng(); + + for kem in [KEM::MlKem768, KEM::XWing] { + let (rekey_state, request_message) = + RekeyInitiator::generate_request(&mut rng, kem).unwrap(); + + let (responder_secret, response_message) = + responder_process(&mut rng, request_message).unwrap(); + + let initiator_secret = rekey_state.finalize(&response_message).unwrap(); + + assert_eq!(initiator_secret, responder_secret); + } + } +} diff --git a/common/nym-kkt/src/responder.rs b/common/nym-kkt/src/responder.rs new file mode 100644 index 0000000000..a4b645f12e --- /dev/null +++ b/common/nym-kkt/src/responder.rs @@ -0,0 +1,214 @@ +use std::collections::HashSet; + +use crate::key_utils::validate_encapsulation_key; +use crate::{ + context::{KKTContext, KKTMode, KKTRole, KKTStatus}, + error::KKTError, + frame::KKTFrame, +}; +use libcrux_psq::handshake::types::DHKeyPair; +use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme}; +pub struct KKTResponder<'a> { + x25519_keypair: &'a DHKeyPair, + mlkem_encapsulation_key: Option<&'a libcrux_kem::MlKem768PublicKey>, + mceliece_encapsulation_key: Option<&'a libcrux_psq::classic_mceliece::PublicKey>, + supported_hash_functions: HashSet, + supported_signature_schemes: HashSet, + supported_outer_protocol_versions: HashSet, +} +impl<'a> KKTResponder<'a> { + pub fn new( + x25519_keypair: &'a DHKeyPair, + mlkem_encapsulation_key: Option<&'a libcrux_kem::MlKem768PublicKey>, + mceliece_encapsulation_key: Option<&'a libcrux_psq::classic_mceliece::PublicKey>, + supported_hash_functions: &[HashFunction], + supported_outer_protocol_versions: &[u8], + supported_signature_schemes: &[SignatureScheme], + ) -> Result { + let hash_functions: HashSet = + supported_hash_functions.iter().copied().collect(); + + if hash_functions.is_empty() { + Err(KKTError::FunctionInputError { + info: "Did not provide a supported HashFunction when instaciating a KKTResponder", + }) + } else { + let signature_schemes: HashSet = + supported_signature_schemes.iter().copied().collect(); + + if signature_schemes.is_empty() { + Err(KKTError::FunctionInputError { + info: "Did not provide a supported SignatureScheme when instaciating a KKTResponder", + }) + } else { + let outer_protocol_versions: HashSet = + supported_outer_protocol_versions.iter().copied().collect(); + + if outer_protocol_versions.is_empty() { + Err(KKTError::FunctionInputError { + info: "Did not provide a supported outer protocol version when instaciating a KKTResponder", + }) + } else { + if mlkem_encapsulation_key.is_none() && mceliece_encapsulation_key.is_none() { + return Err(KKTError::FunctionInputError { + info: "Did not provide an encapsulation key when instanciating a KKTResponder.", + }); + } else { + Ok(Self { + x25519_keypair, + mlkem_encapsulation_key, + mceliece_encapsulation_key, + supported_hash_functions: hash_functions, + supported_signature_schemes: signature_schemes, + supported_outer_protocol_versions: outer_protocol_versions, + }) + } + } + } + } + } + fn supported_protocol_versions(&self) -> Vec { + self.supported_outer_protocol_versions + .iter() + .copied() + .collect() + } + + fn check_ciphersuite_compatiblity( + &self, + remote_ciphersuite: &Ciphersuite, + ) -> Result<(), KKTError> { + if !self + .supported_hash_functions + .contains(remote_ciphersuite.hash_function()) + { + Err(KKTError::IncompatibilityError { + info: "Unsupported HashFunction", + }) + } else { + if !self + .supported_signature_schemes + .contains(remote_ciphersuite.signature_scheme()) + { + Err(KKTError::IncompatibilityError { + info: "Unsupported SignatureScheme", + }) + } else { + if match remote_ciphersuite.kem() { + KEM::MlKem768 => self.mlkem_encapsulation_key.is_some(), + KEM::McEliece => self.mceliece_encapsulation_key.is_some(), + _ => false, + } { + Ok(()) + } else { + Err(KKTError::IncompatibilityError { + info: "Unsupported KEM", + }) + } + } + } + } + + // When this function fails, we do that silently (i.e. we dont generate a response to the initiator). + + pub fn process_request( + &self, + request_bytes: &[u8], + ) -> Result<(Vec, Option>), KKTError> { + let (mut carrier, remote_frame, remote_context) = KKTFrame::decrypt_initiator_frame( + self.x25519_keypair, + request_bytes, + &self.supported_protocol_versions(), + )?; + + self.check_ciphersuite_compatiblity(remote_context.ciphersuite())?; + + let (local_context, remote_encapsulation_key) = match remote_context.mode() { + KKTMode::OneWay => responder_ingest_message(&remote_context, None, &remote_frame)?, + KKTMode::Mutual => { + // So we can either fetch the remote hash here using some async call to the directory, + // which might make registration hang or accept the sent key then verify later. + + // If we choose to not accept, the response's status will be KKTStatus::UnverifiedKEMKey. + // The response would still contain the responder's encapsulation key. + responder_ingest_message(&remote_context, None, &remote_frame)? + } + }; + + let frame = if local_context.ciphersuite().kem() == &KEM::MlKem768 { + KKTFrame::new( + &local_context, + // SAFETY: the self.check_ciphersuite_compatibility call above guarantees that we will have a key in the right place + #[allow(clippy::unwrap_used)] + &self.mlkem_encapsulation_key.unwrap().as_slice().as_slice(), + )? + } else { + KKTFrame::new( + &local_context, + // SAFETY: the self.check_ciphersuite_compatibility call above guarantees that we will have a key in the right place + #[allow(clippy::unwrap_used)] + &self.mceliece_encapsulation_key.unwrap().as_ref(), + )? + }; + + // encryption - responder frame + let response_bytes = carrier.encrypt(&frame.to_bytes())?; + Ok((response_bytes, remote_encapsulation_key)) + } +} + +pub fn responder_ingest_message( + remote_context: &KKTContext, + expected_hash: Option<&[u8]>, + remote_frame: &KKTFrame, +) -> Result<(KKTContext, Option>), KKTError> { + let mut own_context = remote_context.derive_responder_header()?; + + match remote_context.role() { + KKTRole::Initiator => { + // using own_context here because maybe for whatever reason we want to ignore the remote kem key + match own_context.mode() { + KKTMode::OneWay => Ok((own_context, None)), + KKTMode::Mutual => { + match expected_hash { + Some(expected_hash) => { + if validate_encapsulation_key( + own_context.ciphersuite().hash_function(), + own_context.ciphersuite().hash_len(), + remote_frame.body_ref(), + expected_hash, + ) { + Ok((own_context, Some(remote_frame.body_ref().to_vec()))) + } + // The key does not match the hash obtained from the directory + else { + Err(KKTError::KEMError { + info: "Hash of received encapsulation key does not match the value stored on the directory.", + }) + } + } + None => { + own_context.update_status(KKTStatus::UnverifiedKEMKey); + // we don't store an unverified key + // changing the status notifies the initiator that we didn't + + // we could still keep it here and then verify later... + // let received_encapsulation_key = EncapsulationKey::decode( + // own_context.ciphersuite().kem(), + // remote_frame.body_ref(), + // )?; + // Ok((own_context, Some(received_encapsulation_key))) + // + + Ok((own_context, None)) + } + } + } + } + } + + KKTRole::Responder => Err(KKTError::IncompatibilityError { + info: "Responder received a request from another responder.", + }), + } +} diff --git a/common/nym-kkt/src/session.rs b/common/nym-kkt/src/session.rs deleted file mode 100644 index 40db3d275d..0000000000 --- a/common/nym-kkt/src/session.rs +++ /dev/null @@ -1,230 +0,0 @@ -use nym_crypto::asymmetric::ed25519::{self, Signature}; -use rand09::{CryptoRng, RngCore}; - -use crate::frame::KKTSessionId; -use crate::{ - ciphersuite::{Ciphersuite, EncapsulationKey}, - context::{KKTContext, KKTMode, KKTRole, KKTStatus}, - error::KKTError, - frame::{KKT_SESSION_ID_LEN, KKTFrame}, - key_utils::validate_encapsulation_key, -}; - -pub fn initiator_process<'a, R>( - rng: &mut R, - mode: KKTMode, - ciphersuite: Ciphersuite, - signing_key: &ed25519::PrivateKey, - own_encapsulation_key: Option<&EncapsulationKey<'a>>, -) -> Result<(KKTContext, KKTFrame), KKTError> -where - R: CryptoRng + RngCore, -{ - let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?; - - let context_bytes = context.encode()?; - - let mut session_id = [0; KKT_SESSION_ID_LEN]; - // Generate Session ID - rng.fill_bytes(&mut session_id); - - let body: &[u8] = match mode { - KKTMode::OneWay => &[], - KKTMode::Mutual => match own_encapsulation_key { - Some(encaps_key) => &encaps_key.encode(), - - // Missing key - None => { - return Err(KKTError::FunctionInputError { - info: "KEM Key Not Provided", - }); - } - }, - }; - - let mut bytes_to_sign = - Vec::with_capacity(context.full_message_len() - context.signature_len()); - bytes_to_sign.extend_from_slice(&context_bytes); - bytes_to_sign.extend_from_slice(body); - bytes_to_sign.extend_from_slice(&session_id); - - let signature = signing_key.sign(bytes_to_sign).to_bytes(); - - Ok(( - context, - KKTFrame::new(context_bytes, body, session_id, &signature), - )) -} - -pub fn anonymous_initiator_process( - rng: &mut R, - ciphersuite: Ciphersuite, -) -> Result<(KKTContext, KKTFrame), KKTError> -where - R: CryptoRng + RngCore, -{ - let context = KKTContext::new(KKTRole::AnonymousInitiator, KKTMode::OneWay, ciphersuite)?; - let context_bytes = context.encode()?; - - let mut session_id = [0u8; KKT_SESSION_ID_LEN]; - rng.fill_bytes(&mut session_id); - - Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[]))) -} - -pub fn initiator_ingest_response<'a>( - own_context: &KKTContext, - remote_frame: &KKTFrame, - remote_context: &KKTContext, - remote_verification_key: &ed25519::PublicKey, - expected_hash: &[u8], -) -> Result, KKTError> { - check_compatibility(own_context, remote_context)?; - match remote_context.status() { - KKTStatus::Ok => { - let mut bytes_to_verify: Vec = Vec::with_capacity( - remote_context.full_message_len() - remote_context.signature_len(), - ); - bytes_to_verify.extend_from_slice(&remote_context.encode()?); - bytes_to_verify.extend_from_slice(remote_frame.body_ref()); - bytes_to_verify.extend_from_slice(remote_frame.session_id_ref()); - - match Signature::from_bytes(remote_frame.signature_ref()) { - Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) { - Ok(()) => { - let received_encapsulation_key = EncapsulationKey::decode( - own_context.ciphersuite().kem(), - remote_frame.body_ref(), - )?; - - match validate_encapsulation_key( - &own_context.ciphersuite().hash_function(), - own_context.ciphersuite().hash_len(), - remote_frame.body_ref(), - expected_hash, - ) { - true => Ok(received_encapsulation_key), - - // The key does not match the hash obtained from the directory - false => Err(KKTError::KEMError { - info: "Hash of received encapsulation key does not match the value stored on the directory.", - }), - } - } - Err(_) => Err(KKTError::SigVerifError), - }, - Err(_) => Err(KKTError::SigConstructorError), - } - } - _ => Err(KKTError::ResponderFlaggedError { - status: remote_context.status(), - }), - } -} - -// todo: figure out how to handle errors using status codes - -pub fn responder_ingest_message<'a>( - remote_context: &KKTContext, - remote_verification_key: Option<&ed25519::PublicKey>, - expected_hash: Option<&[u8]>, - remote_frame: &KKTFrame, -) -> Result<(KKTContext, Option>), KKTError> { - let own_context = remote_context.derive_responder_header()?; - - match remote_context.role() { - KKTRole::AnonymousInitiator => Ok((own_context, None)), - - KKTRole::Initiator => { - match remote_verification_key { - Some(remote_verif_key) => { - let mut bytes_to_verify: Vec = Vec::with_capacity( - own_context.full_message_len() - own_context.signature_len(), - ); - bytes_to_verify.extend_from_slice(remote_frame.context_ref()); - bytes_to_verify.extend_from_slice(remote_frame.body_ref()); - bytes_to_verify.extend_from_slice(remote_frame.session_id_ref()); - - match Signature::from_bytes(remote_frame.signature_ref()) { - Ok(sig) => match remote_verif_key.verify(bytes_to_verify, &sig) { - Ok(()) => { - // using own_context here because maybe for whatever reason we want to ignore the remote kem key - match own_context.mode() { - KKTMode::OneWay => Ok((own_context, None)), - KKTMode::Mutual => { - match expected_hash { - Some(expected_hash) => { - let received_encapsulation_key = - EncapsulationKey::decode( - own_context.ciphersuite().kem(), - remote_frame.body_ref(), - )?; - if validate_encapsulation_key( - &own_context.ciphersuite().hash_function(), - own_context.ciphersuite().hash_len(), - remote_frame.body_ref(), - expected_hash, - ) { - Ok(( - own_context, - Some(received_encapsulation_key), - )) - } - // The key does not match the hash obtained from the directory - else { - Err(KKTError::KEMError { - info: "Hash of received encapsulation key does not match the value stored on the directory.", - }) - } - } - None => Err(KKTError::FunctionInputError { - info: "Expected hash of the remote encapsulation key is not provided.", - }), - } - } - } - } - Err(_) => Err(KKTError::SigVerifError), - }, - Err(_) => Err(KKTError::SigConstructorError), - } - } - None => Err(KKTError::FunctionInputError { - info: "Remote Signature Verification Key Not Provided", - }), - } - } - KKTRole::Responder => Err(KKTError::IncompatibilityError { - info: "Responder received a request from another responder.", - }), - } -} - -pub fn responder_process<'a>( - own_context: &KKTContext, - session_id: KKTSessionId, - signing_key: &ed25519::PrivateKey, - encapsulation_key: &EncapsulationKey<'a>, -) -> Result { - let body = encapsulation_key.encode(); - - let context_bytes = own_context.encode()?; - - let mut bytes_to_sign = - Vec::with_capacity(own_context.full_message_len() - own_context.signature_len()); - bytes_to_sign.extend_from_slice(&own_context.encode()?); - bytes_to_sign.extend_from_slice(&body); - bytes_to_sign.extend_from_slice(&session_id); - - let signature = signing_key.sign(bytes_to_sign).to_bytes(); - - Ok(KKTFrame::new(context_bytes, &body, session_id, &signature)) -} - -fn check_compatibility( - _own_context: &KKTContext, - _remote_context: &KKTContext, -) -> Result<(), KKTError> { - // todo: check ciphersuite/context compatibility - Ok(()) -} diff --git a/common/nym-lp-sandbox/Cargo.toml b/common/nym-lp-sandbox/Cargo.toml new file mode 100644 index 0000000000..eb04b78f51 --- /dev/null +++ b/common/nym-lp-sandbox/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "nym-lp-sandbox" +version = "0.1.0" +edition = { workspace = true } +license = { workspace = true } +publish = false + +[dependencies] +thiserror = { workspace = true } +parking_lot = { workspace = true } +snow = { workspace = true } +bs58 = { workspace = true } +serde = { workspace = true } +bytes = { workspace = true } +dashmap = { workspace = true } +sha2 = { workspace = true } +tracing = { workspace = true } +rand = { workspace = true } +rand09 = { workspace = true } + + +nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] } +nym-kkt = { path = "../nym-kkt" } +nym-lp-common = { path = "../nym-lp-common" } + +nym-kkt-ciphersuite = { path ="../nym-kkt-ciphersuite" } + +# libcrux dependencies for PSQ (Post-Quantum PSK derivation) +libcrux-psq = { workspace = true, features = ["test-utils"] } +libcrux-kem = { workspace = true } +tls_codec = { workspace = true } +num_enum = { workspace = true } +chacha20poly1305 = { workspace = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } +rand_chacha = "0.3" +nym-crypto = { path = "../crypto", features = ["rand"] } +nym-test-utils = { workspace = true } + + diff --git a/common/nym-lp-sandbox/src/lib.rs b/common/nym-lp-sandbox/src/lib.rs new file mode 100644 index 0000000000..888e01653d --- /dev/null +++ b/common/nym-lp-sandbox/src/lib.rs @@ -0,0 +1,262 @@ +#[cfg(test)] +mod tests { + + use libcrux_psq::{ + Channel, IntoSession, + handshake::{ + builders::{CiphersuiteBuilder, PrincipalBuilder}, + ciphersuites::CiphersuiteName, + types::{Authenticator, PQEncapsulationKey}, + }, + session::{Session, SessionBinding}, + }; + use nym_kkt::{ + initiator::KKTInitiator, + key_utils::{ + generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519, + hash_encapsulation_key, + }, + responder::KKTResponder, + }; + use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme}; + + #[test] + fn test_e2e_client_node() { + let mut rng = rand09::rng(); + + // we should add these as consts + let aad_initiator_outer = b"Test Data I Outer"; + let aad_initiator_inner = b"Test Data I Inner"; + let aad_responder = b"Test Data R"; + let ctx = b"Test Context"; + + // generate responder x25519 keys + let responder_x25519_keypair = generate_keypair_x25519(&mut rng); + let hash_function = HashFunction::Blake3; + // generate kem public keys + + let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng); + let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng); + + let r_dir_hash_mlkem = hash_encapsulation_key( + // &ciphersuite.hash_function(), + &hash_function, + // ciphersuite.hash_len(), + HashLength::Default.value(), + responder_mlkem_keypair.1.as_slice().as_slice(), + ); + + let r_dir_hash_mceliece = hash_encapsulation_key( + // &ciphersuite.hash_function(), + &hash_function, + // ciphersuite.hash_len(), + HashLength::Default.value(), + responder_mceliece_keypair.1.as_ref(), + ); + + let kkt_responder = KKTResponder::new( + &responder_x25519_keypair, + Some(&responder_mlkem_keypair.1), + Some(&responder_mceliece_keypair.1), + &[ + HashFunction::Blake3, + HashFunction::SHA256, + HashFunction::Shake128, + HashFunction::Shake256, + ], + &[1], + &[SignatureScheme::Ed25519], + ) + .unwrap(); + + // OneWay - MlKem + let psq_ciphersuite = CiphersuiteName::X25519_MLKEM768_X25519_AESGCM128_HKDFSHA256; + + let responder_ciphersuite = CiphersuiteBuilder::new(psq_ciphersuite) + .longterm_x25519_keys(&responder_x25519_keypair) + .longterm_mlkem_encapsulation_key(&responder_mlkem_keypair.1) + .longterm_mlkem_decapsulation_key(&responder_mlkem_keypair.0) + .build_responder_ciphersuite() + .unwrap(); + + let mut responder = PrincipalBuilder::new(rand09::rng()) + .context(ctx) + .outer_aad(aad_responder) + .recent_keys_upper_bound(30) + .build_responder(responder_ciphersuite) + .unwrap(); + + let ciphersuite = Ciphersuite::resolve_ciphersuite( + KEM::MlKem768, + hash_function, + SignatureScheme::Ed25519, + None, + ) + .unwrap(); + + let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request( + &mut rng, + &ciphersuite, + &responder_x25519_keypair.pk, + &r_dir_hash_mlkem, + 1u8, + ) + .unwrap(); + + let (response_bytes, _) = kkt_responder.process_request(&request_bytes).unwrap(); + + let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap(); + + assert_eq!( + i_obtained_key, + responder_mlkem_keypair.1.as_slice().as_slice(), + ); + + let mlkem_key = + libcrux_kem::MlKem768PublicKey::try_from(i_obtained_key.as_slice()).unwrap(); + + let initiator_psq_keys = generate_keypair_x25519(&mut rng); + let initiator_cbuilder = CiphersuiteBuilder::new(psq_ciphersuite) + .longterm_x25519_keys(&initiator_psq_keys) + .peer_longterm_x25519_pk(&responder_x25519_keypair.pk) + .peer_longterm_mlkem_pk(&mlkem_key); + + let initiator_ciphersuite = initiator_cbuilder.build_initiator_ciphersuite().unwrap(); + + let mut msg_channel = vec![0u8; 8192]; + let mut payload_buf_responder = vec![0u8; 4096]; + let mut payload_buf_initiator = vec![0u8; 4096]; + + let mut initiator = PrincipalBuilder::new(rand09::rng()) + .outer_aad(aad_initiator_outer) + .inner_aad(aad_initiator_inner) + .context(ctx) + .build_registration_initiator(initiator_ciphersuite) + .unwrap(); + + // Send first message + let registration_payload_initiator = b"Registration_init"; + let len_i = initiator + .write_message(registration_payload_initiator, &mut msg_channel) + .unwrap(); + + // Read first message + let (len_r_deserialized, len_r_payload) = responder + .read_message(&msg_channel, &mut payload_buf_responder) + .unwrap(); + + // We read the same amount of data. + assert_eq!(len_r_deserialized, len_i); + assert_eq!(len_r_payload, registration_payload_initiator.len()); + assert_eq!( + &payload_buf_responder[0..len_r_payload], + registration_payload_initiator + ); + + // Get the authenticator out here, so we can deserialize the session later. + let Some(initiator_authenticator) = responder.initiator_authenticator() else { + panic!("No initiator authenticator found") + }; + + // Respond + let registration_payload_responder = b"Registration_respond"; + let len_r = responder + .write_message(registration_payload_responder, &mut msg_channel) + .unwrap(); + + // Finalize on registration initiator + let (len_i_deserialized, len_i_payload) = initiator + .read_message(&msg_channel, &mut payload_buf_initiator) + .unwrap(); + + // We read the same amount of data. + assert_eq!(len_r, len_i_deserialized); + assert_eq!(registration_payload_responder.len(), len_i_payload); + assert_eq!( + &payload_buf_initiator[0..len_i_payload], + registration_payload_responder + ); + + // Ready for transport mode + assert!(initiator.is_handshake_finished()); + assert!(responder.is_handshake_finished()); + + let i_transport = initiator.into_session().unwrap(); + let r_transport = responder.into_session().unwrap(); + + // test serialization, deserialization + let mut session_storage = vec![0u8; 4096]; + i_transport + .serialize( + &mut session_storage, + SessionBinding { + initiator_authenticator: &Authenticator::Dh(initiator_psq_keys.pk), + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)), + }, + ) + .unwrap(); + let mut i_transport = Session::deserialize( + &session_storage, + SessionBinding { + initiator_authenticator: &Authenticator::Dh(initiator_psq_keys.pk), + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)), + }, + ) + .unwrap(); + + r_transport + .serialize( + &mut session_storage, + SessionBinding { + initiator_authenticator: &Authenticator::Dh(initiator_psq_keys.pk), + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)), + }, + ) + .unwrap(); + let mut r_transport = Session::deserialize( + &session_storage, + SessionBinding { + initiator_authenticator: &Authenticator::Dh(initiator_psq_keys.pk), + responder_ecdh_pk: &responder_x25519_keypair.pk, + responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)), + }, + ) + .unwrap(); + + let mut channel_i = i_transport.transport_channel().unwrap(); + let mut channel_r = r_transport.transport_channel().unwrap(); + + assert_eq!(channel_i.identifier(), channel_r.identifier()); + + let app_data_i = b"Derived session hey".as_slice(); + let app_data_r = b"Derived session ho".as_slice(); + + let len_i = channel_i + .write_message(app_data_i, &mut msg_channel) + .unwrap(); + + let (len_r_deserialized, len_r_payload) = channel_r + .read_message(&msg_channel, &mut payload_buf_responder) + .unwrap(); + + // We read the same amount of data. + assert_eq!(len_r_deserialized, len_i); + assert_eq!(len_r_payload, app_data_i.len()); + assert_eq!(&payload_buf_responder[0..len_r_payload], app_data_i); + + let len_r = channel_r + .write_message(app_data_r, &mut msg_channel) + .unwrap(); + + let (len_i_deserialized, len_i_payload) = channel_i + .read_message(&msg_channel, &mut payload_buf_initiator) + .unwrap(); + + assert_eq!(len_r, len_i_deserialized); + assert_eq!(app_data_r.len(), len_i_payload); + assert_eq!(&payload_buf_initiator[0..len_i_payload], app_data_r); + } +} diff --git a/common/nym-lp/Cargo.toml b/common/nym-lp/Cargo.toml index 22f11c279b..0bc8f6c4db 100644 --- a/common/nym-lp/Cargo.toml +++ b/common/nym-lp/Cargo.toml @@ -16,7 +16,6 @@ dashmap = { workspace = true } sha2 = { workspace = true } tracing = { workspace = true } rand = { workspace = true } -# rand 0.9 for KKT integration (nym-kkt uses rand 0.9) rand09 = { workspace = true } nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] } @@ -25,11 +24,8 @@ 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 = [ - "test-utils", -] } -libcrux-kem = { git = "https://github.com/cryspen/libcrux" } -libcrux-traits = { git = "https://github.com/cryspen/libcrux" } +libcrux-psq = { workspace = true, features = ["test-utils"] } +libcrux-kem = { workspace = true } tls_codec = { workspace = true } num_enum = { workspace = true } chacha20poly1305 = { workspace = true } diff --git a/common/nym-lp/src/codec.rs b/common/nym-lp/src/codec.rs index 94eedc1199..95a6e2da3a 100644 --- a/common/nym-lp/src/codec.rs +++ b/common/nym-lp/src/codec.rs @@ -14,6 +14,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; /// Size of outer header (receiver_idx + counter) - always cleartext pub const OUTER_HEADER_SIZE: usize = OuterHeader::SIZE; // 12 bytes +// georgio: maybe remove this? /// Size of inner prefix (proto + reserved) - cleartext or encrypted depending on mode const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3) @@ -38,6 +39,7 @@ const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3) /// 3. **No PSK persistence**: PSK handles are not stored/reused across sessions. /// Each connection performs fresh KKT+PSQ handshake. /// +// noiserm #[derive(Clone, Zeroize, ZeroizeOnDrop)] pub struct OuterAeadKey { key: [u8; 32], @@ -52,7 +54,7 @@ impl OuterAeadKey { /// Uses Blake3 KDF with domain separation to avoid key reuse /// between the outer AEAD layer and the inner Noise layer. pub fn from_psk(psk: &[u8; 32]) -> Self { - let key = nym_crypto::kdf::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]); + let key = nym_crypto::hkdf::blake3::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]); Self { key } } @@ -70,6 +72,7 @@ impl std::fmt::Debug for OuterAeadKey { } } +// noiserm /// Build 12-byte nonce from 8-byte counter (zero-padded). /// /// Format: counter (8 bytes LE) || 0x00000000 (4 bytes) @@ -177,6 +180,7 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result { // AEAD decryption mode // AAD is the outer header (12 bytes) @@ -224,6 +228,7 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result { - assert_eq!(decoded_payload, HandshakeData(payload)); + LpMessage::PSQRequest(decoded_payload) => { + assert_eq!(decoded_payload, PSQRequestData(payload)); } _ => panic!("Expected Handshake message"), } @@ -1093,7 +1098,7 @@ mod tests { receiver_idx: 99999, counter: 2, }, - message: LpMessage::Handshake(HandshakeData(handshake_data.clone())), + message: LpMessage::PSQRequest(PSQRequestData(handshake_data.clone())), trailer: [0; TRAILER_LEN], }; @@ -1103,7 +1108,7 @@ mod tests { let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap(); match decoded.message { - LpMessage::Handshake(data) => { + LpMessage::PSQResponse(data) => { assert_eq!(data.0, handshake_data); } _ => panic!("Expected Handshake message"), diff --git a/common/nym-lp/src/config.rs b/common/nym-lp/src/config.rs index 4b22ad331d..bc6a0ab987 100644 --- a/common/nym-lp/src/config.rs +++ b/common/nym-lp/src/config.rs @@ -18,7 +18,7 @@ pub const DEFAULT_PSK_TTL_SECS: u64 = 3600; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LpConfig { /// KEM algorithm for PSQ key encapsulation. - /// X25519 = classical (testing), MlKem768 = PQ, XWing = hybrid. + /// Supported KEMs: MlKem768, McEliece #[serde(with = "kem_serde")] pub kem_algorithm: KEM, @@ -32,7 +32,7 @@ pub struct LpConfig { impl Default for LpConfig { fn default() -> Self { Self { - kem_algorithm: KEM::X25519, + kem_algorithm: KEM::MlKem768, psk_ttl_secs: DEFAULT_PSK_TTL_SECS, enable_kkt: true, } @@ -55,10 +55,10 @@ mod kem_serde { S: Serializer, { match kem { - KEM::X25519 => "X25519", KEM::MlKem768 => "MlKem768", - KEM::XWing => "XWing", KEM::McEliece => "McEliece", + KEM::X25519 => return Err(serde::ser::Error::custom("Unsupported KEM: X25519")), + KEM::XWing => return Err(serde::ser::Error::custom("Unsupported KEM: XWing")), } .serialize(serializer) } @@ -69,10 +69,10 @@ mod kem_serde { { let s = String::deserialize(deserializer)?; match s.as_str() { - "X25519" => Ok(KEM::X25519), "MlKem768" => Ok(KEM::MlKem768), - "XWing" => Ok(KEM::XWing), "McEliece" => Ok(KEM::McEliece), + "X25519" => Err(serde::de::Error::custom("Unsupported KEM: X25519")), + "XWing" => Err(serde::de::Error::custom("Unsupported KEM: XWing")), _ => Err(serde::de::Error::custom(format!("Unknown KEM: {}", s))), } } diff --git a/common/nym-lp/src/error.rs b/common/nym-lp/src/error.rs index cf3b5e5085..fbd6672b95 100644 --- a/common/nym-lp/src/error.rs +++ b/common/nym-lp/src/error.rs @@ -3,9 +3,12 @@ use crate::message::MessageType; use crate::{noise_protocol::NoiseError, replay::ReplayError}; +use libcrux_psq::handshake::builders::BuilderError; use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; -use nym_kkt::ciphersuite::{HashFunction, KEM}; -use nym_kkt::error::KKTError; +use nym_kkt::{ + ciphersuite::{HashFunction, KEM}, + error::KKTError, +}; use thiserror::Error; #[derive(Error, Debug)] @@ -13,15 +16,21 @@ pub enum LpError { #[error("IO Error: {0}")] IoError(#[from] std::io::Error), + // noiserm #[error("Snow Error: {0}")] SnowKeyError(#[from] snow::Error), + // noiserm #[error("Snow Pattern Error: {0}")] SnowPatternError(String), + // noiserm #[error("Noise Protocol Error: {0}")] NoiseError(#[from] NoiseError), + #[error("PSQ Error: {0}")] + PSQError(String), + #[error("Replay detected: {0}")] Replay(#[from] ReplayError), @@ -52,9 +61,6 @@ pub enum LpError { #[error("Deserialization error: {0}")] DeserializationError(String), - #[error("KKT protocol error: {0}")] - KKTError(String), - #[error(transparent)] InvalidBase58String(#[from] bs58::decode::Error), @@ -94,8 +100,8 @@ pub enum LpError { #[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")] IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 }, - #[error("attempted to create an LP responder without providing a valid KEM key")] - ResponderWithMissingKEMKey, + #[error("attempted to create an LP responder without providing a valid KEM key for {kem} ")] + ResponderWithMissingKEMKey { kem: KEM }, #[error( "there are no known digests for remote's KEM key with {kem} KEM and {hash_function} hash function" diff --git a/common/nym-lp/src/lib.rs b/common/nym-lp/src/lib.rs index be5bb991af..cd7bc34340 100644 --- a/common/nym-lp/src/lib.rs +++ b/common/nym-lp/src/lib.rs @@ -2,22 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 pub mod codec; -pub mod config; +// georgio: config does not seem to be used anywhere +// pub mod config; +// pub use config::LpConfig; + pub mod error; +// georgio: no use for this // pub mod kkt_orchestrator; pub mod message; pub mod noise_protocol; pub mod packet; pub mod peer; pub mod psk; -mod psq; +pub mod psq; pub mod replay; pub mod session; mod session_integration; pub mod session_manager; pub mod state_machine; -pub use config::LpConfig; pub use error::LpError; pub use message::{ClientHelloData, LpMessage}; pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader}; @@ -26,9 +29,16 @@ pub use session::LpSession; pub use session_manager::SessionManager; pub use state_machine::LpStateMachine; +// noiserm pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256"; pub const NOISE_PSK_INDEX: u8 = 3; +#[cfg(test)] +pub fn kem_list() -> Vec { + use nym_kkt::ciphersuite::KEM; + vec![KEM::MlKem768, KEM::McEliece, KEM::X25519] +} + #[cfg(any(feature = "mock", test))] pub struct SessionsMock { pub initiator: LpSession, @@ -162,109 +172,113 @@ mod tests { use crate::message::LpMessage; use crate::packet::{LpHeader, LpPacket, TRAILER_LEN}; use crate::session_manager::SessionManager; - use crate::{LpError, SessionsMock, mock_session_for_test}; + use crate::{LpError, SessionsMock, kem_list, mock_session_for_test}; use bytes::BytesMut; + use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, SignatureScheme}; // Import the new standalone functions use crate::codec::{parse_lp_packet, serialize_lp_packet}; #[test] fn test_replay_protection_integration() { - // Create session - let mut session = mock_session_for_test(); + for kem in kem_list() { + // Create session + let mut session = mock_session_for_test(); - // === Packet 1 (Counter 0 - Should succeed) === - let packet1 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, // Matches session's sending_index assumption for this test - counter: 0, - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; + // === Packet 1 (Counter 0 - Should succeed) === + let packet1 = LpPacket { + header: LpHeader { + protocol_version: 1, + reserved: [0u8; 3], + receiver_idx: 42, // Matches session's sending_index assumption for this test + counter: 0, + }, + message: LpMessage::Busy, + trailer: [0u8; TRAILER_LEN], + }; - // Serialize packet - let mut buf1 = BytesMut::new(); - serialize_lp_packet(&packet1, &mut buf1, None).unwrap(); + // Serialize packet + let mut buf1 = BytesMut::new(); + serialize_lp_packet(&packet1, &mut buf1, None).unwrap(); - // Parse packet - let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap(); + // Parse packet + let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap(); - // Perform replay check (should pass) - session - .receiving_counter_quick_check(parsed_packet1.header.counter) - .expect("Initial packet failed replay check"); + // Perform replay check (should pass) + session + .receiving_counter_quick_check(parsed_packet1.header.counter) + .expect("Initial packet failed replay check"); - // Mark received (simulating successful processing) - session - .receiving_counter_mark(parsed_packet1.header.counter) - .expect("Failed to mark initial packet received"); + // Mark received (simulating successful processing) + session + .receiving_counter_mark(parsed_packet1.header.counter) + .expect("Failed to mark initial packet received"); - // === Packet 2 (Counter 0 - Replay, should fail check) === - let packet2 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 0, // Same counter as before (replay) - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; + // === Packet 2 (Counter 0 - Replay, should fail check) === + let packet2 = LpPacket { + header: LpHeader { + protocol_version: 1, + reserved: [0u8; 3], + receiver_idx: 42, + counter: 0, // Same counter as before (replay) + }, + message: LpMessage::Busy, + trailer: [0u8; TRAILER_LEN], + }; - // Serialize packet - let mut buf2 = BytesMut::new(); - serialize_lp_packet(&packet2, &mut buf2, None).unwrap(); + // Serialize packet + let mut buf2 = BytesMut::new(); + serialize_lp_packet(&packet2, &mut buf2, None).unwrap(); - // Parse packet - let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap(); + // Parse packet + let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap(); - // Perform replay check (should fail) - let replay_result = session.receiving_counter_quick_check(parsed_packet2.header.counter); - assert!(replay_result.is_err()); - match replay_result.unwrap_err() { - LpError::Replay(e) => { - assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter)); + // Perform replay check (should fail) + let replay_result = + session.receiving_counter_quick_check(parsed_packet2.header.counter); + assert!(replay_result.is_err()); + match replay_result.unwrap_err() { + LpError::Replay(e) => { + assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter)); + } + e => panic!("Expected replay error, got {:?}", e), } - e => panic!("Expected replay error, got {:?}", e), + // Do not mark received as it failed validation + + // === Packet 3 (Counter 1 - Should succeed) === + let packet3 = LpPacket { + header: LpHeader { + protocol_version: 1, + reserved: [0u8; 3], + receiver_idx: 42, + counter: 1, // Incremented counter + }, + message: LpMessage::Busy, + trailer: [0u8; TRAILER_LEN], + }; + + // Serialize packet + let mut buf3 = BytesMut::new(); + serialize_lp_packet(&packet3, &mut buf3, None).unwrap(); + + // Parse packet + let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap(); + + // Perform replay check (should pass) + session + .receiving_counter_quick_check(parsed_packet3.header.counter) + .expect("Packet 3 failed replay check"); + + // Mark received + session + .receiving_counter_mark(parsed_packet3.header.counter) + .expect("Failed to mark packet 3 received"); + + // Verify validator state directly on the session + let state = session.current_packet_cnt(); + assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2) + assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3) } - // Do not mark received as it failed validation - - // === Packet 3 (Counter 1 - Should succeed) === - let packet3 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: 42, - counter: 1, // Incremented counter - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; - - // Serialize packet - let mut buf3 = BytesMut::new(); - serialize_lp_packet(&packet3, &mut buf3, None).unwrap(); - - // Parse packet - let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap(); - - // Perform replay check (should pass) - session - .receiving_counter_quick_check(parsed_packet3.header.counter) - .expect("Packet 3 failed replay check"); - - // Mark received - session - .receiving_counter_mark(parsed_packet3.header.counter) - .expect("Failed to mark packet 3 received"); - - // Verify validator state directly on the session - let state = session.current_packet_cnt(); - assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2) - assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3) } #[test] @@ -273,107 +287,119 @@ mod tests { let mut local_manager = SessionManager::new(); let mut remote_manager = SessionManager::new(); - // Use fixed receiver_index for deterministic test - let receiver_index: u32 = 54321; + for kem in kem_list() { + // Generate Ed25519 keypairs for PSQ authentication + let (init, resp) = mock_peers(kem); - let sessions = SessionsMock::mock_post_handshake(receiver_index); - let local_session = sessions.initiator; - let remote_session = sessions.responder; + let mut ciphersuite = Ciphersuite::resolve_ciphersuite( + kem, + HashFunction::Blake3, + SignatureScheme::Ed25519, + None, + ); - // Create a session via manager - let _ = local_manager.create_session_state_machine(local_session); - let _ = remote_manager.create_session_state_machine(remote_session); + // Use fixed receiver_index for deterministic test + let receiver_index: u32 = 54321; - // === Packet 1 (Counter 0 - Should succeed) === - let packet1 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: receiver_index, - counter: 0, - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; + let sessions = SessionsMock::mock_post_handshake(receiver_index); + let local_session = sessions.initiator; + let remote_session = sessions.responder; - // Serialize - let mut buf1 = BytesMut::new(); - serialize_lp_packet(&packet1, &mut buf1, None).unwrap(); + // Create a session via manager + let _ = local_manager.create_session_state_machine(local_session); + let _ = remote_manager.create_session_state_machine(remote_session); - // Parse - let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap(); + // === Packet 1 (Counter 0 - Should succeed) === + let packet1 = LpPacket { + header: LpHeader { + protocol_version: 1, + reserved: [0u8; 3], + receiver_idx: receiver_index, + counter: 0, + }, + message: LpMessage::Busy, + trailer: [0u8; TRAILER_LEN], + }; - // Process via SessionManager method (which should handle checks + marking) - // NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet` - // that encapsulates parse -> check -> process_noise -> mark. - // For now, we simulate the steps using the retrieved session. + // Serialize + let mut buf1 = BytesMut::new(); + serialize_lp_packet(&packet1, &mut buf1, None).unwrap(); - // Perform replay check - local_manager - .receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter) - .expect("Packet 1 check failed"); - // Mark received - local_manager - .receiving_counter_mark(receiver_index, parsed_packet1.header.counter) - .expect("Packet 1 mark failed"); + // Parse + let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap(); - // === Packet 2 (Counter 1 - Should succeed on same session) === - let packet2 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: receiver_index, - counter: 1, - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; + // Process via SessionManager method (which should handle checks + marking) + // NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet` + // that encapsulates parse -> check -> process_noise -> mark. + // For now, we simulate the steps using the retrieved session. - // Serialize - let mut buf2 = BytesMut::new(); - serialize_lp_packet(&packet2, &mut buf2, None).unwrap(); + // Perform replay check + local_manager + .receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter) + .expect("Packet 1 check failed"); + // Mark received + local_manager + .receiving_counter_mark(receiver_index, parsed_packet1.header.counter) + .expect("Packet 1 mark failed"); - // Parse - let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap(); + // === Packet 2 (Counter 1 - Should succeed on same session) === + let packet2 = LpPacket { + header: LpHeader { + protocol_version: 1, + reserved: [0u8; 3], + receiver_idx: receiver_index, + counter: 1, + }, + message: LpMessage::Busy, + trailer: [0u8; TRAILER_LEN], + }; - // Perform replay check - local_manager - .receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter) - .expect("Packet 2 check failed"); - // Mark received - local_manager - .receiving_counter_mark(receiver_index, parsed_packet2.header.counter) - .expect("Packet 2 mark failed"); + // Serialize + let mut buf2 = BytesMut::new(); + serialize_lp_packet(&packet2, &mut buf2, None).unwrap(); - // === Packet 3 (Counter 0 - Replay, should fail check) === - let packet3 = LpPacket { - header: LpHeader { - protocol_version: 1, - reserved: [0u8; 3], - receiver_idx: receiver_index, - counter: 0, // Replay of first packet - }, - message: LpMessage::Busy, - trailer: [0u8; TRAILER_LEN], - }; + // Parse + let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap(); - // Serialize - let mut buf3 = BytesMut::new(); - serialize_lp_packet(&packet3, &mut buf3, None).unwrap(); + // Perform replay check + local_manager + .receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter) + .expect("Packet 2 check failed"); + // Mark received + local_manager + .receiving_counter_mark(receiver_index, parsed_packet2.header.counter) + .expect("Packet 2 mark failed"); - // Parse - let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap(); + // === Packet 3 (Counter 0 - Replay, should fail check) === + let packet3 = LpPacket { + header: LpHeader { + protocol_version: 1, + reserved: [0u8; 3], + receiver_idx: receiver_index, + counter: 0, // Replay of first packet + }, + message: LpMessage::Busy, + trailer: [0u8; TRAILER_LEN], + }; - // Perform replay check (should fail) - let replay_result = local_manager - .receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter); - assert!(replay_result.is_err()); - match replay_result.unwrap_err() { - LpError::Replay(e) => { - assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter)); + // Serialize + let mut buf3 = BytesMut::new(); + serialize_lp_packet(&packet3, &mut buf3, None).unwrap(); + + // Parse + let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap(); + + // Perform replay check (should fail) + let replay_result = local_manager + .receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter); + assert!(replay_result.is_err()); + match replay_result.unwrap_err() { + LpError::Replay(e) => { + assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter)); + } + e => panic!("Expected replay error for packet 3, got {:?}", e), } - e => panic!("Expected replay error for packet 3, got {:?}", e), + // Do not mark received } - // Do not mark received } } diff --git a/common/nym-lp/src/message.rs b/common/nym-lp/src/message.rs index fbd692175b..a71812bc05 100644 --- a/common/nym-lp/src/message.rs +++ b/common/nym-lp/src/message.rs @@ -9,7 +9,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use nym_crypto::asymmetric::{ed25519, x25519}; use rand::RngCore; use serde::{Deserialize, Serialize}; -use std::fmt::{self, Display}; +use std::fmt::{self, Display, write}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; /// Data structure for the ClientHello message @@ -22,11 +22,13 @@ pub struct ClientHelloData { pub client_lp_public_key: x25519::PublicKey, /// Client's Ed25519 public key (32 bytes) - for PSQ authentication pub client_ed25519_public_key: ed25519::PublicKey, + // noiserm /// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce) pub salt: [u8; 32], } impl ClientHelloData { + // noiserm (remove 32 bytes for salt) // 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; @@ -54,6 +56,7 @@ impl ClientHelloData { } } + // noiserm /// Generates a new ClientHelloData with fresh salt. /// /// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce @@ -82,7 +85,7 @@ impl ClientHelloData { salt, } } - + // noiserm /// Extracts the timestamp from the salt. /// /// # Returns @@ -97,6 +100,7 @@ impl ClientHelloData { dst.put_u32_le(self.receiver_index); dst.put_slice(self.client_lp_public_key.as_bytes()); dst.put_slice(self.client_ed25519_public_key.as_bytes()); + // noiserm dst.put_slice(&self.salt); } @@ -120,6 +124,7 @@ impl ClientHelloData { client_ed25519_public_key: ed25519::PublicKey::from_byte_array( client_ed25519_public_key_bytes, )?, + // noiserm salt: b[68..].try_into().unwrap(), }) } @@ -146,6 +151,8 @@ pub enum MessageType { Ack = 0x0008, /// Subsession request - client initiates subsession creation SubsessionRequest = 0x0009, + + // georgio: this should be the psq msg /// Subsession KK1 - first message of Noise KK handshake SubsessionKK1 = 0x000A, /// Subsession KK2 - second message of Noise KK handshake @@ -300,6 +307,42 @@ impl ErrorPacketData { } } +/// PSQ request frame data (serialized bytes) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PSQRequestData(pub Vec); + +impl PSQRequestData { + fn len(&self) -> usize { + self.0.len() + } + + fn encode(&self, dst: &mut BytesMut) { + dst.put_slice(&self.0); + } + + fn decode(bytes: &[u8]) -> Result { + Ok(PSQRequestData(bytes.to_vec())) + } +} + +/// PSQ response frame data (serialized bytes) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PSQResponseData(pub Vec); + +impl PSQResponseData { + fn len(&self) -> usize { + self.0.len() + } + + fn encode(&self, dst: &mut BytesMut) { + dst.put_slice(&self.0); + } + + fn decode(bytes: &[u8]) -> Result { + Ok(PSQResponseData(bytes.to_vec())) + } +} + /// Packet forwarding request with embedded inner LP packet #[derive(Debug, Clone)] pub struct ForwardPacketData { @@ -425,6 +468,7 @@ impl ForwardPacketData { } } +// georgio: swap with psq /// Subsession KK1 message - first message of Noise KK handshake #[derive(Debug, Clone, PartialEq, Eq)] pub struct SubsessionKK1Data { @@ -505,7 +549,8 @@ impl SubsessionReadyData { #[derive(Debug, Clone)] pub enum LpMessage { Busy, - Handshake(HandshakeData), + PSQRequest(PSQRequestData), + PSQResponse(PSQResponseData), EncryptedData(EncryptedDataPayload), ClientHello(ClientHelloData), KKTRequest(KKTRequestData), @@ -515,6 +560,7 @@ pub enum LpMessage { Collision, /// Acknowledgment - gateway confirms receipt of message Ack, + // georgio: this should become psq stuff /// Subsession request - client initiates subsession creation (empty, signal only) SubsessionRequest, /// Subsession KK1 - first message of Noise KK handshake @@ -587,7 +633,6 @@ impl Display for LpMessage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { LpMessage::Busy => write!(f, "Busy"), - LpMessage::Handshake(_) => write!(f, "Handshake"), LpMessage::EncryptedData(_) => write!(f, "EncryptedData"), LpMessage::ClientHello(_) => write!(f, "ClientHello"), LpMessage::KKTRequest(_) => write!(f, "KKTRequest"), @@ -601,6 +646,8 @@ impl Display for LpMessage { LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"), LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"), LpMessage::Error(_) => write!(f, "Error"), + LpMessage::PSQRequest(_) => write!(f, "PSQRequest"), + LpMessage::PSQResponse(_) => write!(f, "PSQResponse"), } } } @@ -629,7 +676,6 @@ impl LpMessage { pub fn is_empty(&self) -> bool { match self { LpMessage::Busy => true, - LpMessage::Handshake(payload) => payload.0.is_empty(), LpMessage::EncryptedData(payload) => payload.0.is_empty(), LpMessage::ClientHello(_) => false, // Always has data LpMessage::KKTRequest(payload) => payload.0.is_empty(), @@ -642,6 +688,8 @@ impl LpMessage { LpMessage::SubsessionKK2(_) => false, // Always has payload LpMessage::SubsessionReady(_) => false, // Always has receiver_index LpMessage::SubsessionAbort => true, // Empty signal + LpMessage::PSQRequest(payload) => true, // Always had data (?) + LpMessage::PSQResponse(payload) => true, // Always had data (?) LpMessage::Error(_) => false, } } @@ -649,7 +697,8 @@ impl LpMessage { pub fn len(&self) -> usize { match self { LpMessage::Busy => 0, - LpMessage::Handshake(payload) => payload.len(), + LpMessage::PSQRequest(payload) => payload.len(), + LpMessage::PSQResponse(payload) => payload.len(), LpMessage::EncryptedData(payload) => payload.len(), LpMessage::ClientHello(payload) => payload.len(), LpMessage::KKTRequest(payload) => payload.len(), @@ -669,7 +718,8 @@ impl LpMessage { pub fn typ(&self) -> MessageType { match self { LpMessage::Busy => MessageType::Busy, - LpMessage::Handshake(_) => MessageType::Handshake, + LpMessage::PSQRequest(_) => todo!(), + LpMessage::PSQResponse(_) => todo!(), LpMessage::EncryptedData(_) => MessageType::EncryptedData, LpMessage::ClientHello(_) => MessageType::ClientHello, LpMessage::KKTRequest(_) => MessageType::KKTRequest, @@ -689,7 +739,8 @@ impl LpMessage { pub fn encode_content(&self, dst: &mut BytesMut) { match self { LpMessage::Busy => { /* No content */ } - LpMessage::Handshake(payload) => payload.encode(dst), + LpMessage::PSQRequest(payload) => payload.encode(dst), + LpMessage::PSQResponse(payload) => payload.encode(dst), LpMessage::EncryptedData(payload) => payload.encode(dst), LpMessage::ClientHello(data) => data.encode(dst), LpMessage::KKTRequest(payload) => payload.encode(dst), @@ -716,7 +767,7 @@ impl LpMessage { content.ensure_empty()?; Ok(LpMessage::Busy) } - MessageType::Handshake => Ok(LpMessage::Handshake(HandshakeData::decode(content)?)), + MessageType::Handshake => todo!(), MessageType::EncryptedData => Ok(LpMessage::EncryptedData( EncryptedDataPayload::decode(content)?, )), diff --git a/common/nym-lp/src/peer.rs b/common/nym-lp/src/peer.rs index 085ea8be5b..2aa2a1df26 100644 --- a/common/nym-lp/src/peer.rs +++ b/common/nym-lp/src/peer.rs @@ -1,32 +1,53 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{ClientHelloData, LpError}; +use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey}; +use libcrux_psq::handshake::Responder; +use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey}; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests}; +use nym_kkt::ciphersuite::{ + Ciphersuite, DecapsulationKey, EncapsulationKey, KEM, KEMKeyDigests, KemKeyPair, + SignatureScheme, SigningKeyDigests, +}; +use rand::rngs::ThreadRng; use std::collections::HashMap; +use std::fmt::Debug; use std::sync::Arc; +use crate::psq::build_responder; + /// Representation of a local Lewes Protocol peer /// encapsulating all the known information and keys. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct LpLocalPeer { + pub(crate) ciphersuite: Ciphersuite, + /// Local Ed25519 keys for PSQ authentication pub(crate) ed25519: Arc, /// Local x25519 keys (Noise static key) - pub(crate) x25519: Arc, + pub(crate) x25519: Arc, - /// Local KEM key used for PSQ - pub(crate) kem_psq: Option>, + /// Local KEM keys used for PSQ + pub(crate) kem_keypairs: HashMap>, } impl LpLocalPeer { - pub fn new(ed25519: Arc, x25519: Arc) -> Self { + pub fn new( + ciphersuite: Ciphersuite, + ed25519: Arc, + x25519: Arc, + ) -> Self { + // TODO: make nicer conversion (without cloning) + error handling + let initiator_libcrux_x25519_private_key = + DHPrivateKey::from_bytes(x25519.private_key().as_bytes()).unwrap(); + let initiator_x25519_keypair = DHKeyPair::from(initiator_libcrux_x25519_private_key); + LpLocalPeer { + ciphersuite, ed25519, - x25519, - kem_psq: None, + x25519: Arc::new(initiator_x25519_keypair), + kem_keypairs: Default::default(), } } @@ -38,9 +59,9 @@ impl LpLocalPeer { ) } - #[must_use] - pub fn with_kem_psq_key(mut self, key: Arc) -> Self { - self.kem_psq = Some(key); + pub fn with_kem_keypair(mut self, keypair: Arc) -> Self { + let kem = keypair.kem(); + self.kem_keypairs.insert(kem, keypair); self } @@ -48,55 +69,56 @@ impl LpLocalPeer { &self.ed25519 } - pub fn x25519(&self) -> &Arc { + pub fn x25519(&self) -> &Arc { &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) + // /// 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) + // } + + pub fn kem_key(&self, kem: KEM) -> Option<&Arc> { + self.kem_keypairs.get(&kem) } /// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests #[doc(hidden)] pub fn as_remote(&self) -> LpRemotePeer { - let expected_kem_key_digests = match &self.kem_psq { - None => HashMap::new(), - Some(kem_keys) => { - let mut digests = HashMap::new(); - digests.insert( - KEM::X25519, - nym_kkt::key_utils::produce_key_digests(kem_keys.public_key().as_bytes()), - ); - digests - } - }; - let mut expected_signing_key_digests = HashMap::new(); expected_signing_key_digests.insert( SignatureScheme::Ed25519, nym_kkt::key_utils::produce_key_digests(self.ed25519.public_key().as_bytes()), ); + let mut expected_kem_key_digests = HashMap::new(); + for (kem, kem_key) in &self.kem_keypairs { + expected_kem_key_digests.insert( + *kem, + nym_kkt::key_utils::produce_key_digests(&kem_key.encoded_encapsulation_key()), + ); + } + LpRemotePeer { ed25519_public: *self.ed25519.public_key(), - x25519_public: *self.x25519.public_key(), + x25519_public: self.x25519.pk, expected_kem_key_digests, expected_signing_key_digests, } } +} - // this is only exposed in tests as ideally we should be storing the proper types to begin with - #[cfg(test)] - pub fn encapsulate_kem_key(&self) -> Option> { - let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes(); - let libcrux_pk = - libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?; - - Some(nym_kkt::ciphersuite::EncapsulationKey::X25519(libcrux_pk)) +impl Debug for LpLocalPeer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LpLocalPeer") + .field("ciphersuite", &self.ciphersuite) + .field("ed25519", &self.ed25519) + .field("x25519", &self.x25519.pk) + .field("kem_keypairs", &self.kem_keypairs) + .finish() } } @@ -108,7 +130,7 @@ pub struct LpRemotePeer { pub(crate) ed25519_public: ed25519::PublicKey, /// Remote X25519 public key (Noise static key) - pub(crate) x25519_public: x25519::PublicKey, + pub(crate) x25519_public: DHPublicKey, /// Expected digests of the remote's KEM key pub(crate) expected_kem_key_digests: HashMap, @@ -119,9 +141,11 @@ pub struct LpRemotePeer { impl LpRemotePeer { pub fn new(ed25519_public: ed25519::PublicKey, x25519_public: x25519::PublicKey) -> Self { + // TODO: make nicer conversion (without cloning) + error handling + let responder_x25519_public_key = DHPublicKey::from_bytes(x25519_public.as_bytes()); LpRemotePeer { ed25519_public, - x25519_public, + x25519_public: responder_x25519_public_key, expected_kem_key_digests: Default::default(), expected_signing_key_digests: Default::default(), } @@ -131,8 +155,8 @@ impl LpRemotePeer { self.ed25519_public } - pub fn x25519(&self) -> x25519::PublicKey { - self.x25519_public + pub fn x25519(&self) -> &DHPublicKey { + &self.x25519_public } #[must_use] @@ -169,29 +193,62 @@ impl LpRemotePeer { } #[cfg(any(feature = "mock", test))] -pub fn mock_peer() -> LpLocalPeer { +pub fn mock_peer(kem: KEM) -> LpLocalPeer { // use deterministic rng let mut rng = nym_test_utils::helpers::deterministic_rng(); - random_peer(&mut rng) + + let ciphersuite = Ciphersuite::new( + kem, + nym_kkt::ciphersuite::HashFunction::Blake3, + SignatureScheme::Ed25519, + nym_kkt::ciphersuite::HashLength::Default, + ); + + random_peer(&mut rng, ciphersuite) } #[cfg(any(feature = "mock", test))] -pub fn random_peer(rng: &mut R) -> LpLocalPeer { +pub fn random_peer<'a, R: rand::CryptoRng + rand::RngCore>( + rng: &mut R, + ciphersuite: Ciphersuite, +) -> LpLocalPeer { + use nym_kkt::key_utils::{generate_keypair_mceliece, generate_keypair_mlkem}; let ed25519 = Arc::new(ed25519::KeyPair::new(rng)); - let x25519 = Arc::new(ed25519.to_x25519()); - let kem_psq = Some(x25519.clone()); - LpLocalPeer { + let mut sk = [0u8; 32]; + rng.fill_bytes(&mut sk); + + let TODO = ""; + // clamp + sk[0] &= 248u8; + sk[31] &= 127u8; + sk[31] |= 64u8; + + let x25519 = Arc::new(DHKeyPair::from(DHPrivateKey::from_bytes(&sk).unwrap())); + + let default_peer = LpLocalPeer { + ciphersuite: Arc::new(ciphersuite), ed25519, x25519, - kem_psq, + mlkem: None, + mceliece: None, + }; + + match ciphersuite.kem() { + KEM::MlKem768 => { + let mlkem_keypair = generate_keypair_mlkem(&mut rand09::rng()); + default_peer.with_mlkem_keypair(&mlkem_keypair.0, &mlkem_keypair.1) + } + KEM::McEliece => { + let mceliece_keypair = generate_keypair_mceliece(&mut rand09::rng()); + default_peer.with_mceliece_keypair(mceliece_keypair.0, mceliece_keypair.1) + } + _ => unreachable!(), } } #[cfg(any(feature = "mock", test))] -pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) { - // use deterministic rng - let mut rng = nym_test_utils::helpers::deterministic_rng(); - - (random_peer(&mut rng), random_peer(&mut rng)) +pub fn mock_peers(kem: KEM) -> (LpLocalPeer, LpLocalPeer) { + println!("KEM: {:?}", kem); + (mock_peer(kem), mock_peer(kem)) } diff --git a/common/nym-lp/src/psk.rs b/common/nym-lp/src/psk.rs index 45c352f490..6bb22b659c 100644 --- a/common/nym-lp/src/psk.rs +++ b/common/nym-lp/src/psk.rs @@ -47,11 +47,12 @@ //! - **No cleanup needed**: No state was mutated use crate::LpError; +use libcrux_psq::handshake::types::{DHPrivateKey, DHPublicKey}; use libcrux_psq::v1::cred::{Authenticator, Ed25519}; use libcrux_psq::v1::impls::X25519 as PsqX25519; use libcrux_psq::v1::psk_registration::{Initiator, InitiatorMsg, Responder}; use libcrux_psq::v1::traits::{Ciphertext as PsqCiphertext, PSQ}; -use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_crypto::asymmetric::ed25519; use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; use std::time::Duration; use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait}; @@ -136,13 +137,15 @@ pub struct PsqResponderResult { /// // Send ciphertext to gateway /// ``` pub fn derive_psk_with_psq_initiator( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, + local_x25519_private: &DHPrivateKey, + remote_x25519_public: &DHPublicKey, remote_kem_public: &EncapsulationKey, salt: &[u8; 32], ) -> Result<([u8; 32], Vec), LpError> { // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + // let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + + let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux"); // Step 2: PSQ encapsulation for post-quantum security // KEM algorithm migration path: @@ -171,7 +174,7 @@ pub fn derive_psk_with_psq_initiator( combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need & combined.extend_from_slice(salt); - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); + let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); // Serialize ciphertext using TLS encoding for transport let ct_bytes = ciphertext @@ -219,14 +222,16 @@ pub fn derive_psk_with_psq_initiator( /// )?; /// ``` pub fn derive_psk_with_psq_responder( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, + local_x25519_private: &DHPrivateKey, + remote_x25519_public: &DHPublicKey, local_kem_keypair: (&DecapsulationKey, &EncapsulationKey), ciphertext: &[u8], salt: &[u8; 32], ) -> Result<[u8; 32], LpError> { // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + // let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + + let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux"); // Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) { @@ -252,7 +257,7 @@ pub fn derive_psk_with_psq_responder( combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need & combined.extend_from_slice(salt); - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); + let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); Ok(final_psk) } @@ -279,8 +284,8 @@ pub fn derive_psk_with_psq_responder( /// # Returns /// `PsqInitiatorResult` containing PSK, payload, and raw PQ shared secret pub fn psq_initiator_create_message( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, + local_x25519_private: &DHPrivateKey, + remote_x25519_public: &DHPublicKey, remote_kem_public: &EncapsulationKey, client_ed25519_sk: &ed25519::PrivateKey, client_ed25519_pk: &ed25519::PublicKey, @@ -288,7 +293,9 @@ pub fn psq_initiator_create_message( session_context: &[u8], ) -> Result { // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + // let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + + let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux"); // Step 2: PSQ v1 with Ed25519 authentication // Extract X25519 KEM key from EncapsulationKey @@ -338,7 +345,7 @@ pub fn psq_initiator_create_message( combined.extend_from_slice(psq_psk); // psq_psk is already a &[u8; 32] combined.extend_from_slice(salt); - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); + let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); // Serialize InitiatorMsg with TLS encoding for transport let msg_bytes = initiator_msg @@ -374,8 +381,8 @@ pub fn psq_initiator_create_message( /// # Returns /// `PsqResponderResult` containing PSK, PSK handle, and raw PQ shared secret pub fn psq_responder_process_message( - local_x25519_private: &x25519::PrivateKey, - remote_x25519_public: &x25519::PublicKey, + local_x25519_private: &DHPrivateKey, + remote_x25519_public: &DHPublicKey, local_kem_keypair: (&DecapsulationKey, &EncapsulationKey), initiator_ed25519_pk: &ed25519::PublicKey, psq_payload: &[u8], @@ -383,7 +390,8 @@ pub fn psq_responder_process_message( session_context: &[u8], ) -> Result { // Step 1: Classical ECDH for baseline security - let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + // let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public); + let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux"); // Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) { @@ -447,7 +455,7 @@ pub fn psq_responder_process_message( combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need & combined.extend_from_slice(salt); - let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); + let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); // Step 7: Serialize ResponderMsg (contains ctxt_B - encrypted PSK handle) use tls_codec::Serialize; @@ -482,7 +490,7 @@ pub fn psq_responder_process_message( /// # Returns /// 32-byte PSK for Noise KKpsk0 handshake pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64) -> [u8; 32] { - nym_crypto::kdf::derive_key_blake3( + nym_crypto::hkdf::blake3::derive_key_blake3( SUBSESSION_PSK_CONTEXT, pq_shared_secret, &subsession_index.to_le_bytes(), @@ -492,10 +500,10 @@ pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64) #[cfg(test)] mod tests { use super::*; - use rand::thread_rng; + use libcrux_psq::handshake::types::DHKeyPair; - fn generate_x25519_keypair() -> x25519::KeyPair { - x25519::KeyPair::new(&mut thread_rng()) + fn generate_x25519_keypair() -> DHKeyPair { + DHKeyPair::new(&mut rand09::rng()) } #[test] @@ -510,18 +518,13 @@ mod tests { let dec_key = DecapsulationKey::X25519(_kem_sk); // Client derives PSK - let (client_psk, ciphertext) = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt, - ) - .unwrap(); + let (client_psk, ciphertext) = + derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt).unwrap(); // Gateway derives PSK from their perspective let gateway_psk = derive_psk_with_psq_responder( - keypair_2.private_key(), - keypair_1.public_key(), + keypair_2.sk(), + &keypair_1.pk, (&dec_key, &enc_key), &ciphertext, &salt, @@ -545,20 +548,10 @@ mod tests { let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); let enc_key = EncapsulationKey::X25519(kem_pk); - let psk1 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt1, - ) - .unwrap(); - let psk2 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt2, - ) - .unwrap(); + let psk1 = + derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt1).unwrap(); + let psk2 = + derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt2).unwrap(); assert_ne!(psk1, psk2, "Different salts should produce different PSKs"); } @@ -574,20 +567,10 @@ mod tests { let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); let enc_key = EncapsulationKey::X25519(kem_pk); - let psk1 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_2.public_key(), - &enc_key, - &salt, - ) - .unwrap(); - let psk2 = derive_psk_with_psq_initiator( - keypair_1.private_key(), - keypair_3.public_key(), - &enc_key, - &salt, - ) - .unwrap(); + let psk1 = + derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt).unwrap(); + let psk2 = + derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_3.pk, &enc_key, &salt).unwrap(); assert_ne!( psk1, psk2, @@ -616,16 +599,16 @@ mod tests { // Derive PSK twice with same inputs (initiator side) let (_psk1, ct1) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key, &salt, ) .unwrap(); let (_psk2, _ct2) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key, &salt, ) @@ -634,8 +617,8 @@ mod tests { // PSKs will be different due to randomness in PSQ, but ciphertexts too // This test verifies the function is deterministic given the SAME ciphertext let psk_responder1 = derive_psk_with_psq_responder( - gateway_keypair.private_key(), - client_keypair.public_key(), + gateway_keypair.sk(), + &client_keypair.pk, (&dec_key, &enc_key), &ct1, &salt, @@ -643,8 +626,8 @@ mod tests { .unwrap(); let psk_responder2 = derive_psk_with_psq_responder( - gateway_keypair.private_key(), - client_keypair.public_key(), + gateway_keypair.sk(), + &client_keypair.pk, (&dec_key, &enc_key), &ct1, // Same ciphertext &salt, @@ -674,8 +657,8 @@ mod tests { // Client derives PSK (initiator) let (client_psk, ciphertext) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key, &salt, ) @@ -683,8 +666,8 @@ mod tests { // Gateway derives PSK from ciphertext (responder) let gateway_psk = derive_psk_with_psq_responder( - gateway_keypair.private_key(), - client_keypair.public_key(), + gateway_keypair.sk(), + &client_keypair.pk, (&dec_key, &enc_key), &ciphertext, &salt, @@ -714,16 +697,16 @@ mod tests { let salt = [3u8; 32]; let (psk1, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key1, &salt, ) .unwrap(); let (psk2, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key2, &salt, ) @@ -748,8 +731,8 @@ mod tests { let salt = [4u8; 32]; let (psk, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key, &salt, ) @@ -772,16 +755,16 @@ mod tests { let salt2 = [2u8; 32]; let (psk1, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key, &salt1, ) .unwrap(); let (psk2, _) = derive_psk_with_psq_initiator( - client_keypair.private_key(), - gateway_keypair.public_key(), + client_keypair.sk(), + &gateway_keypair.pk, &enc_key, &salt2, ) diff --git a/common/nym-lp/src/psq/mod.rs b/common/nym-lp/src/psq/mod.rs index 06be6f26ec..6c0b62a36b 100644 --- a/common/nym-lp/src/psq/mod.rs +++ b/common/nym-lp/src/psq/mod.rs @@ -15,6 +15,143 @@ mod helpers; mod initiator; mod responder; +// IMPORT START: +use libcrux_psq::{ + Channel, + handshake::{ + RegistrationInitiator, Responder, + builders::{CiphersuiteBuilder, PrincipalBuilder}, + ciphersuites::CiphersuiteName, + types::{DHKeyPair, DHPublicKey}, + }, +}; +use nym_kkt::ciphersuite::{Ciphersuite, DecapsulationKey, EncapsulationKey, KEM, KemKeyPair}; +use rand09::rngs::ThreadRng; + +use std::fmt::Debug; + +const AAD_INITIATOR_OUTER: &[u8] = b"Test Data I Outer"; +const AAD_INITIATOR_INNER: &[u8] = b"Test Data I Inner"; +const AAD_RESPONDER: &[u8] = b"Test Data R"; +const SESSION_CONTEXT: &[u8] = b"Test Context"; + +pub enum PSQState<'a> { + Initiator(RegistrationInitiator<'a, ThreadRng>), + Responder(Responder<'a, ThreadRng>), +} +impl Debug for PSQState<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Initiator(_) => f.debug_tuple("PSQ Initiator").finish(), + Self::Responder(_) => f.debug_tuple("PSQ Responder").finish(), + } + } +} + +pub fn initiator_process(initiator: &mut RegistrationInitiator) -> Vec { + let mut buffer = vec![0u8; 4096]; + let msg_len = initiator.write_message(b"", &mut buffer).unwrap(); + buffer.resize(msg_len, 0); + buffer +} + +pub fn build_initiator<'a>( + ciphersuite: &'a Ciphersuite, + session_context: &'a [u8], + local_x25519_keys: &'a DHKeyPair, + remote_x25519_public: &'a DHPublicKey, + remote_kem_public: &'a EncapsulationKey, +) -> RegistrationInitiator<'a, rand09::rngs::ThreadRng> { + //georgio: handle errors + + let initiator_cbuilder = match ciphersuite.kem() { + nym_kkt::ciphersuite::KEM::MlKem768 => match remote_kem_public { + EncapsulationKey::MlKem768(ml_kem_public_key) => CiphersuiteBuilder::new( + CiphersuiteName::X25519_MLKEM768_X25519_CHACHA20POLY1305_HKDFSHA256, + ) + .peer_longterm_mlkem_pk(ml_kem_public_key), + _ => panic!( + "wrong key type passed (remote_kem_public should be EncapsulationKey::MlKem768)" + ), + }, + nym_kkt::ciphersuite::KEM::McEliece => match remote_kem_public { + EncapsulationKey::McEliece(mceliece_public_key) => CiphersuiteBuilder::new( + CiphersuiteName::X25519_CLASSICMCELIECE_X25519_CHACHA20POLY1305_HKDFSHA256, + ) + .peer_longterm_cmc_pk(mceliece_public_key), + _ => panic!( + "wrong key type passed (remote_kem_public should be EncapsulationKey::McEliece)" + ), + }, + _ => panic!("undefined"), + }; + let initiator_ciphersuite = initiator_cbuilder + .longterm_x25519_keys(local_x25519_keys) + .peer_longterm_x25519_pk(remote_x25519_public) + .build_initiator_ciphersuite() + .unwrap(); + + PrincipalBuilder::new(rand09::rng()) + .outer_aad(AAD_INITIATOR_OUTER) + .inner_aad(AAD_INITIATOR_INNER) + .context(session_context) + .build_registration_initiator(initiator_ciphersuite) + .unwrap() +} + +// JS: I have removed the `ciphersuite` argument as it was only matching on the key types, +// which we already obtained matching on the ciphersuite kem type in `LpSession::new` +pub fn build_responder<'a>( + local_x25519_keys: &'a DHKeyPair, + local_kem_keys: &'a KemKeyPair, +) -> Responder<'a, rand09::rngs::ThreadRng> { + let responder_ciphersuite = match local_kem_keys { + KemKeyPair::MlKem768 { + encapsulation_key, + decapsulation_key, + } => CiphersuiteBuilder::new( + CiphersuiteName::X25519_MLKEM768_X25519_CHACHA20POLY1305_HKDFSHA256, + ) + .longterm_mlkem_encapsulation_key(encapsulation_key) + .longterm_mlkem_decapsulation_key(decapsulation_key), + KemKeyPair::McEliece { + encapsulation_key, + decapsulation_key, + } => CiphersuiteBuilder::new( + CiphersuiteName::X25519_CLASSICMCELIECE_X25519_CHACHA20POLY1305_HKDFSHA256, + ) + .longterm_cmc_encapsulation_key(encapsulation_key) + .longterm_cmc_decapsulation_key(decapsulation_key), + KemKeyPair::XWing { .. } => panic!("unsupported"), + KemKeyPair::X25519 { .. } => panic!("unsupported"), + } + .longterm_x25519_keys(local_x25519_keys) + .build_responder_ciphersuite() + .unwrap(); + + PrincipalBuilder::new(rand09::rng()) + .outer_aad(AAD_RESPONDER) + .context(SESSION_CONTEXT) + .build_responder(responder_ciphersuite) + .unwrap() +} + +pub fn psq_responder_process<'a>( + responder: &'a mut Responder, + initiator_message: &[u8], +) -> Vec { + let mut payload = vec![0u8; 4096]; + responder + .read_message(initiator_message, &mut payload) + .unwrap(); + + let mut buffer = vec![0u8; 4096]; + let msg_len = responder.write_message(b"", &mut buffer).unwrap(); + buffer.resize(msg_len, 0); + buffer +} +// IMPORT END + pub(crate) struct IntermediateHandshakeFailure { /// Session id established during exchange if we managed to derive it session_id: Option, diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index 52a336cfca..954517ac0f 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -16,6 +16,14 @@ use crate::psk::derive_subsession_psk; use crate::psq::PSQHandshakeState; use crate::replay::ReceivingKeyCounterValidator; use crate::{LpError, LpMessage, LpPacket}; +use libcrux_psq::v1::traits::PSQ; +use libcrux_psq::{Channel, IntoSession}; +use rand09::rngs::ThreadRng; + +use libcrux_psq::{ + handshake::{RegistrationInitiator, Responder, types::DHPublicKey}, + session::Session, +}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme}; use nym_lp_transport::traits::LpTransport; @@ -60,6 +68,9 @@ pub struct LpSession { /// Used for future version negotiation and compatibility checks. version: u8, + /// Carrier + carrier: Option, + /// Outer AEAD key for packet encryption (derived from PSK after PSQ handshake). outer_aead_key: OuterAeadKey, @@ -180,6 +191,7 @@ impl LpSession { self.session_id } + // noiserm /// Returns the negotiated protocol version from the handshake. /// /// Set during `LpSession` creation after sending / receiving `ClientHelloData` @@ -200,6 +212,17 @@ impl LpSession { self.remote_peer.ed25519_public } + pub fn local_kem_keys(&self) -> Result<&KemKeyPair, LpError> { + todo!() + // let kem = self.base.local_peer.ciphersuite.kem(); + // + // self.base + // .local_peer + // .kem_key(kem) + // .ok_or_else(|| LpError::ResponderWithMissingKEMKey { kem }) + // .map(|keys| keys.deref()) + } + /// Returns the remote X25519 public key. /// /// Used for tie-breaking in simultaneous subsession initiation. @@ -208,9 +231,20 @@ impl LpSession { &self.remote_peer.x25519_public } + // noiserm /// 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) -> &OuterAeadKey { - &self.outer_aead_key + todo!() + // &self.outer_aead_key } pub fn next_packet(&mut self, message: LpMessage) -> Result { @@ -296,6 +330,25 @@ impl LpSession { next } + /// 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> { + todo!() + // let kem = context.ciphersuite().kem(); + // let hash_function = context.ciphersuite().hash_function(); + // + // let digests = self + // .base + // .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 }) + } + /// Returns true if this session is in read-only mode. /// /// Read-only sessions have been demoted after a subsession was promoted. @@ -441,6 +494,7 @@ pub struct SubsessionHandshake { /// Subsession index (unique per parent session) pub index: u64, /// Noise KKpsk0 handshake state + // georgio: replace with psq noise_state: Mutex, /// Is this side the initiator? is_initiator: bool, @@ -565,14 +619,17 @@ mod tests { #[test] fn test_session_creation() { let mut session = sessions_for_tests().0; + for kem in kem_list() { + let session = sessions_for_tests(kem).0; - // Initial counter should be zero - let counter = session.next_counter(); - assert_eq!(counter, 0); + // Initial counter should be zero + let counter = session.next_counter(); + assert_eq!(counter, 0); - // Counter should increment - let counter = session.next_counter(); - assert_eq!(counter, 1); + // Counter should increment + let counter = session.next_counter(); + assert_eq!(counter, 1); + } } // NOTE: These tests are obsolete after removing optional KEM parameters. @@ -592,63 +649,69 @@ mod tests { #[test] fn test_replay_protection_sequential() { - let mut session = sessions_for_tests().1; + for kem in kem_list() { + let mut session = sessions_for_tests(kem).1; - // Sequential counters should be accepted - assert!(session.receiving_counter_quick_check(0).is_ok()); - assert!(session.receiving_counter_mark(0).is_ok()); + // Sequential counters should be accepted + assert!(session.receiving_counter_quick_check(0).is_ok()); + assert!(session.receiving_counter_mark(0).is_ok()); - assert!(session.receiving_counter_quick_check(1).is_ok()); - assert!(session.receiving_counter_mark(1).is_ok()); + assert!(session.receiving_counter_quick_check(1).is_ok()); + assert!(session.receiving_counter_mark(1).is_ok()); - // Duplicates should be rejected - assert!(session.receiving_counter_quick_check(0).is_err()); - let err = session.receiving_counter_mark(0).unwrap_err(); - match err { - LpError::Replay(replay_error) => { - assert!(matches!(replay_error, ReplayError::DuplicateCounter)); + // Duplicates should be rejected + assert!(session.receiving_counter_quick_check(0).is_err()); + let err = session.receiving_counter_mark(0).unwrap_err(); + match err { + LpError::Replay(replay_error) => { + assert!(matches!(replay_error, ReplayError::DuplicateCounter)); + } + _ => panic!("Expected replay error"), } - _ => panic!("Expected replay error"), } } #[test] fn test_replay_protection_out_of_order() { - let mut session = sessions_for_tests().1; + for kem in kem_list() { + let mut session = sessions_for_tests(kem).1; - // Receive packets in order - assert!(session.receiving_counter_mark(0).is_ok()); - assert!(session.receiving_counter_mark(1).is_ok()); - assert!(session.receiving_counter_mark(2).is_ok()); + // Receive packets in order + assert!(session.receiving_counter_mark(0).is_ok()); + assert!(session.receiving_counter_mark(1).is_ok()); + assert!(session.receiving_counter_mark(2).is_ok()); - // Skip ahead - assert!(session.receiving_counter_mark(10).is_ok()); + // Skip ahead + assert!(session.receiving_counter_mark(10).is_ok()); - // Can still receive out-of-order packets within window - assert!(session.receiving_counter_quick_check(5).is_ok()); - assert!(session.receiving_counter_mark(5).is_ok()); + // Can still receive out-of-order packets within window + assert!(session.receiving_counter_quick_check(5).is_ok()); + assert!(session.receiving_counter_mark(5).is_ok()); - // But duplicates are still rejected - assert!(session.receiving_counter_quick_check(5).is_err()); - assert!(session.receiving_counter_mark(5).is_err()); + // But duplicates are still rejected + assert!(session.receiving_counter_quick_check(5).is_err()); + assert!(session.receiving_counter_mark(5).is_err()); + } } #[test] fn test_packet_stats() { - let mut session = sessions_for_tests().1; + for kem in kem_list() { + let mut session = sessions_for_tests(kem).1; - // Initial stats - let (next, received) = session.current_packet_cnt(); - assert_eq!(next, 0); - assert_eq!(received, 0); + // Initial stats + let (next, received) = session.current_packet_cnt(); + assert_eq!(next, 0); + assert_eq!(received, 0); - // After receiving packets - assert!(session.receiving_counter_mark(0).is_ok()); - assert!(session.receiving_counter_mark(1).is_ok()); + // After receiving packets + assert!(session.receiving_counter_mark(0).is_ok()); + assert!(session.receiving_counter_mark(1).is_ok()); - let (next, received) = session.current_packet_cnt(); - assert_eq!(next, 2); - assert_eq!(received, 2); + let (next, received) = session.current_packet_cnt(); + assert_eq!(next, 2); + assert_eq!(received, 2); + } } /* @@ -717,21 +780,25 @@ mod tests { fn test_demote_sets_read_only() { let sessions = SessionsMock::mock_post_handshake(12345); let mut session = sessions.initiator; + for kem in kem_list() { + let session = create_handshake_test_session(kem, 12345u32, true); - // Initially not read-only - assert!(!session.is_read_only()); - assert!(session.successor_session_id().is_none()); + // Initially not read-only + assert!(!session.is_read_only()); + assert!(session.successor_session_id().is_none()); - // Demote the session - session.demote(99999); + // Demote the session + session.demote(99999); - // Now read-only with successor - assert!(session.is_read_only()); - assert_eq!(session.successor_session_id(), Some(99999)); + // Now read-only with successor + assert!(session.is_read_only()); + assert_eq!(session.successor_session_id(), Some(99999)); + } } #[test] fn test_encrypt_fails_after_demotion() { + let TODO = " for kem in kem_list() {"; let receiver_index = 12345; let sessions = SessionsMock::mock_post_handshake(receiver_index); let mut initiator_session = sessions.initiator; @@ -757,6 +824,7 @@ mod tests { #[test] fn test_decrypt_works_after_demotion() { // --- Setup Handshake --- + let TODO = " for kem in kem_list() {"; let receiver_index = 12345; let sessions = SessionsMock::mock_post_handshake(receiver_index); let mut initiator_session = sessions.initiator; diff --git a/common/nym-lp/src/session_integration/mod.rs b/common/nym-lp/src/session_integration/mod.rs index e72242a507..4fbc9a171f 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, SessionsMock, + LpError, SessionsMock, kem_list, message::LpMessage, packet::{LpHeader, LpPacket, TRAILER_LEN}, session_manager::SessionManager, @@ -47,6 +47,8 @@ mod tests { let mut session_manager_1 = SessionManager::new(); let mut session_manager_2 = SessionManager::new(); + let TODO = " for kem in kem_list() {"; + let receiver_index = 12345; let sessions = SessionsMock::mock_post_handshake(receiver_index); @@ -292,6 +294,8 @@ mod tests { let mut session_manager_1 = SessionManager::new(); let mut session_manager_2 = SessionManager::new(); + let TODO = " for kem in kem_list() {"; + let receiver_index = 12345; let sessions = SessionsMock::mock_post_handshake(receiver_index); @@ -404,6 +408,7 @@ mod tests { // 1. Initialize session manager let mut session_manager = SessionManager::new(); + let TODO = " for kem in kem_list() {"; let receiver_index = 123; let session1 = SessionsMock::mock_post_handshake(receiver_index).initiator; let session2 = SessionsMock::mock_post_handshake(124).initiator; @@ -494,6 +499,8 @@ mod tests { let mut session_manager_1 = SessionManager::new(); let mut session_manager_2 = SessionManager::new(); + let TODO = " for kem in kem_list() {"; + let receiver_index = 12345; let sessions = SessionsMock::mock_post_handshake(receiver_index); diff --git a/common/nym-lp/src/session_manager.rs b/common/nym-lp/src/session_manager.rs index 9ddcb91d3f..e6279c643e 100644 --- a/common/nym-lp/src/session_manager.rs +++ b/common/nym-lp/src/session_manager.rs @@ -6,14 +6,18 @@ //! This module implements session lifecycle management functionality, handling //! creation, retrieval, and storage of sessions. -use crate::state_machine::{LpAction, LpInput, LpStateBare}; +use crate::peer::{LpLocalPeer, LpRemotePeer}; +use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare}; use crate::{LpError, LpMessage, LpSession, LpStateMachine}; use std::collections::HashMap; +#[cfg(test)] +use libcrux_psq::handshake::types::DHPublicKey; +use nym_kkt::ciphersuite::Ciphersuite; + /// Manages the lifecycle of Lewes Protocol sessions. /// -/// The SessionManager is responsible for creating, storing, and retrieving sessions, -/// ensuring proper thread-safety for concurrent access. +/// The SessionManager is responsible for creating, storing, and retrieving sessions pub struct SessionManager { /// Manages state machines directly, keyed by lp_id state_machines: HashMap, @@ -111,7 +115,6 @@ impl SessionManager { } else { Err(LpError::StateMachineNotFound { lp_id }) } - // self.state_machines.get(&lp_id).map(|sm_ref| f(&*sm_ref)) // Lock held only during closure execution } // For mutable access (like running process_input) @@ -150,6 +153,8 @@ mod tests { fn test_session_manager_get() { let mut manager = SessionManager::new(); + let TODO = " for kem in kem_list() {"; + let local_session = mock_session_for_test(); let id = local_session.id(); @@ -168,6 +173,8 @@ mod tests { let mut manager = SessionManager::new(); let local_session = mock_session_for_test(); + let TODO = " for kem in kem_list() {"; + let sm_1_id = manager.create_session_state_machine(local_session); let removed = manager.remove_state_machine(sm_1_id); @@ -181,6 +188,9 @@ mod tests { #[test] fn test_multiple_sessions() { let mut manager = SessionManager::new(); + + let TODO = " for kem in kem_list() {"; + let session1 = SessionsMock::mock_post_handshake(123).initiator; let session2 = SessionsMock::mock_post_handshake(124).initiator; let session3 = SessionsMock::mock_post_handshake(125).initiator; @@ -204,6 +214,8 @@ mod tests { fn test_session_manager_create_session() { let mut manager = SessionManager::new(); + let TODO = " for kem in kem_list() {"; + let sesion = mock_session_for_test(); let sm = manager.create_session_state_machine(sesion); diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index 7892fbf017..4bdb5d4ef1 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -22,6 +22,7 @@ use crate::{ }; use bytes::{Buf, Bytes}; use num_enum::{IntoPrimitive, TryFromPrimitive}; +use nym_kkt::ciphersuite::Ciphersuite; use std::mem; use tracing::debug; @@ -531,7 +532,7 @@ impl LpStateMachine { let local_key = session.local_x25519_public(); let remote_key = session.remote_x25519_public(); - if local_key.as_bytes() < remote_key.as_bytes() { + if local_key.as_ref() < remote_key.as_ref() { // We LOSE - become responder // Use the same index as our initiator subsession, which should // match the winner's index if subsession counters are in sync. @@ -942,6 +943,8 @@ mod tests { #[test] fn test_state_machine_init() { + let TODO = " for kem in kem_list() {"; + let mock_sessions = SessionsMock::mock_post_handshake(123); let initiator_sm = LpStateMachine::new(mock_sessions.initiator); @@ -958,6 +961,8 @@ mod tests { #[test] fn test_state_machine_simplified_flow() { + let TODO = " for kem in kem_list() {"; + let receiver_index: u32 = 123; let mock_sessions = SessionsMock::mock_post_handshake(123); @@ -1025,7 +1030,7 @@ mod tests { /// 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) { + fn setup_transport_sessions(kem: KEM) -> (LpStateMachine, LpStateMachine) { let sessions = SessionsMock::mock_post_handshake(12345); ( LpStateMachine::new(sessions.initiator), @@ -1038,138 +1043,140 @@ mod tests { // Test for simultaneous subsession initiation race condition. // Both sides call InitiateSubsession at the same time, sending KK1 to each other. // The tie-breaker uses X25519 public key comparison: lower key becomes responder. + for kem in kem_list() { + let (mut alice, mut bob) = setup_transport_sessions(kem); - let (mut alice, mut bob) = setup_transport_sessions(); + // Get X25519 public keys to determine expected winner + let alice_x25519 = alice.session().unwrap().local_x25519_public(); + let bob_x25519 = bob.session().unwrap().local_x25519_public(); - // Get X25519 public keys to determine expected winner - let alice_x25519 = alice.session().unwrap().local_x25519_public(); - let bob_x25519 = bob.session().unwrap().local_x25519_public(); + // Determine who should win (higher key stays initiator) + let alice_wins = alice_x25519.as_ref() > bob_x25519.as_ref(); - // Determine who should win (higher key stays initiator) - let alice_wins = alice_x25519.as_bytes() > bob_x25519.as_bytes(); + // --- Both sides initiate subsession simultaneously --- + // Alice initiates subsession + let alice_kk1_packet = + if let Some(Ok(LpAction::SubsessionInitiated { packet, .. })) = + alice.process_input(LpInput::InitiateSubsession) + { + packet + } else { + panic!("Alice should initiate subsession with KK1"); + }; + assert!(matches!(alice.state, LpState::SubsessionHandshaking { .. })); - // --- Both sides initiate subsession simultaneously --- - // Alice initiates subsession - let alice_kk1_packet = if let Some(Ok(LpAction::SubsessionInitiated { packet, .. })) = - alice.process_input(LpInput::InitiateSubsession) - { - packet - } else { - panic!("Alice should initiate subsession with KK1"); - }; - assert!(matches!(alice.state, LpState::SubsessionHandshaking { .. })); - - // Bob initiates subsession (simultaneously) - let bob_kk1_packet = if let Some(Ok(LpAction::SubsessionInitiated { packet, .. })) = - bob.process_input(LpInput::InitiateSubsession) - { - packet - } else { - panic!("Bob should initiate subsession with KK1"); - }; - assert!(matches!(bob.state, LpState::SubsessionHandshaking { .. })); - - // --- Cross-delivery of KK1 packets (race resolution) --- - // Alice receives Bob's KK1 - let alice_response = alice.process_input(LpInput::ReceivePacket(bob_kk1_packet)); - - // Bob receives Alice's KK1 - let bob_response = bob.process_input(LpInput::ReceivePacket(alice_kk1_packet)); - - // --- Verify tie-breaker worked correctly --- - if alice_wins { - // Alice has higher key - she stays initiator, sends SubsessionAbort - assert!( - matches!(alice_response, Some(Ok(LpAction::SendPacket(_)))), - "Alice (winner) should send SubsessionAbort" - ); - assert!( - matches!(alice.state, LpState::SubsessionHandshaking { .. }), - "Alice should still be SubsessionHandshaking as initiator" - ); - - // Bob has lower key - he becomes responder, sends KK2 - let bob_kk2_packet = if let Some(Ok(LpAction::SendPacket(p))) = bob_response { - p + // Bob initiates subsession (simultaneously) + let bob_kk1_packet = if let Some(Ok(LpAction::SubsessionInitiated { packet, .. })) = + bob.process_input(LpInput::InitiateSubsession) + { + packet } else { - panic!("Bob (loser) should send KK2 as new responder"); + panic!("Bob should initiate subsession with KK1"); }; - assert!( - matches!(bob.state, LpState::SubsessionHandshaking { .. }), - "Bob should be SubsessionHandshaking as responder" - ); + assert!(matches!(bob.state, LpState::SubsessionHandshaking { .. })); - // Complete the handshake: Alice receives KK2 - let alice_completion = alice.process_input(LpInput::ReceivePacket(bob_kk2_packet)); - match alice_completion { - Some(Ok(LpAction::SubsessionComplete { - packet: Some(ready_packet), - .. - })) => { - assert!( - matches!(alice.state, LpState::ReadOnlyTransport { .. }), - "Alice should be ReadOnlyTransport after SubsessionComplete" - ); + // --- Cross-delivery of KK1 packets (race resolution) --- + // Alice receives Bob's KK1 + let alice_response = alice.process_input(LpInput::ReceivePacket(bob_kk1_packet)); - // Bob receives SubsessionReady - let bob_final = bob.process_input(LpInput::ReceivePacket(ready_packet)); - assert!( - matches!(bob_final, Some(Ok(LpAction::SubsessionComplete { .. }))), - "Bob should complete with SubsessionComplete" - ); - assert!( - matches!(bob.state, LpState::ReadOnlyTransport { .. }), - "Bob should be ReadOnlyTransport" - ); + // Bob receives Alice's KK1 + let bob_response = bob.process_input(LpInput::ReceivePacket(alice_kk1_packet)); + + // --- Verify tie-breaker worked correctly --- + if alice_wins { + // Alice has higher key - she stays initiator, sends SubsessionAbort + assert!( + matches!(alice_response, Some(Ok(LpAction::SendPacket(_)))), + "Alice (winner) should send SubsessionAbort" + ); + assert!( + matches!(alice.state, LpState::SubsessionHandshaking { .. }), + "Alice should still be SubsessionHandshaking as initiator" + ); + + // Bob has lower key - he becomes responder, sends KK2 + let bob_kk2_packet = if let Some(Ok(LpAction::SendPacket(p))) = bob_response { + p + } else { + panic!("Bob (loser) should send KK2 as new responder"); + }; + assert!( + matches!(bob.state, LpState::SubsessionHandshaking { .. }), + "Bob should be SubsessionHandshaking as responder" + ); + + // Complete the handshake: Alice receives KK2 + let alice_completion = alice.process_input(LpInput::ReceivePacket(bob_kk2_packet)); + match alice_completion { + Some(Ok(LpAction::SubsessionComplete { + packet: Some(ready_packet), + .. + })) => { + assert!( + matches!(alice.state, LpState::ReadOnlyTransport { .. }), + "Alice should be ReadOnlyTransport after SubsessionComplete" + ); + + // Bob receives SubsessionReady + let bob_final = bob.process_input(LpInput::ReceivePacket(ready_packet)); + assert!( + matches!(bob_final, Some(Ok(LpAction::SubsessionComplete { .. }))), + "Bob should complete with SubsessionComplete" + ); + assert!( + matches!(bob.state, LpState::ReadOnlyTransport { .. }), + "Bob should be ReadOnlyTransport" + ); + } + other => panic!("Alice should complete subsession, got: {:?}", other), } - other => panic!("Alice should complete subsession, got: {:?}", other), - } - } else { - // Bob has higher key - he stays initiator, sends SubsessionAbort - assert!( - matches!(bob_response, Some(Ok(LpAction::SendPacket(_)))), - "Bob (winner) should send SubsessionAbort" - ); - assert!( - matches!(bob.state, LpState::SubsessionHandshaking { .. }), - "Bob should still be SubsessionHandshaking as initiator" - ); - - // Alice has lower key - she becomes responder, sends KK2 - let alice_kk2_packet = if let Some(Ok(LpAction::SendPacket(p))) = alice_response { - p } else { - panic!("Alice (loser) should send KK2 as new responder"); - }; - assert!( - matches!(alice.state, LpState::SubsessionHandshaking { .. }), - "Alice should be SubsessionHandshaking as responder" - ); + // Bob has higher key - he stays initiator, sends SubsessionAbort + assert!( + matches!(bob_response, Some(Ok(LpAction::SendPacket(_)))), + "Bob (winner) should send SubsessionAbort" + ); + assert!( + matches!(bob.state, LpState::SubsessionHandshaking { .. }), + "Bob should still be SubsessionHandshaking as initiator" + ); - // Complete the handshake: Bob receives KK2 - let bob_completion = bob.process_input(LpInput::ReceivePacket(alice_kk2_packet)); - match bob_completion { - Some(Ok(LpAction::SubsessionComplete { - packet: Some(ready_packet), - .. - })) => { - assert!( - matches!(bob.state, LpState::ReadOnlyTransport { .. }), - "Bob should be ReadOnlyTransport after SubsessionComplete" - ); + // Alice has lower key - she becomes responder, sends KK2 + let alice_kk2_packet = if let Some(Ok(LpAction::SendPacket(p))) = alice_response { + p + } else { + panic!("Alice (loser) should send KK2 as new responder"); + }; + assert!( + matches!(alice.state, LpState::SubsessionHandshaking { .. }), + "Alice should be SubsessionHandshaking as responder" + ); - // Alice receives SubsessionReady - let alice_final = alice.process_input(LpInput::ReceivePacket(ready_packet)); - assert!( - matches!(alice_final, Some(Ok(LpAction::SubsessionComplete { .. }))), - "Alice should complete with SubsessionComplete" - ); - assert!( - matches!(alice.state, LpState::ReadOnlyTransport { .. }), - "Alice should be ReadOnlyTransport" - ); + // Complete the handshake: Bob receives KK2 + let bob_completion = bob.process_input(LpInput::ReceivePacket(alice_kk2_packet)); + match bob_completion { + Some(Ok(LpAction::SubsessionComplete { + packet: Some(ready_packet), + .. + })) => { + assert!( + matches!(bob.state, LpState::ReadOnlyTransport { .. }), + "Bob should be ReadOnlyTransport after SubsessionComplete" + ); + + // Alice receives SubsessionReady + let alice_final = alice.process_input(LpInput::ReceivePacket(ready_packet)); + assert!( + matches!(alice_final, Some(Ok(LpAction::SubsessionComplete { .. }))), + "Alice should complete with SubsessionComplete" + ); + assert!( + matches!(alice.state, LpState::ReadOnlyTransport { .. }), + "Alice should be ReadOnlyTransport" + ); + } + other => panic!("Bob should complete subsession, got: {:?}", other), } - other => panic!("Bob should complete subsession, got: {:?}", other), } } } diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 5468e50112..84a73def30 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1137,9 +1137,9 @@ checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 3d825c7e73..703d2b0a9e 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -27,6 +27,7 @@ nym-gateway = { path = "../gateway" } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } tracing = { workspace = true } futures = { workspace = true } +nym-kkt-ciphersuite = { workspace = true } [lints] workspace = true diff --git a/integration-tests/src/lp_registration.rs b/integration-tests/src/lp_registration.rs index cf3658f8fe..295e663df6 100644 --- a/integration-tests/src/lp_registration.rs +++ b/integration-tests/src/lp_registration.rs @@ -17,6 +17,7 @@ mod tests { }; use nym_gateway::node::wireguard::{PeerManager, PeerRegistrator}; use nym_gateway::node::{ActiveClientsStore, GatewayStorage, LpConfig}; + use nym_kkt_ciphersuite::Ciphersuite; use nym_registration_client::{LpClientError, LpRegistrationClient}; use nym_test_utils::helpers::{CryptoRng, RngCore, u64_seeded_rng}; use nym_test_utils::mocks::async_read_write::MockIOStream; @@ -66,8 +67,10 @@ mod tests { let lp_x25519_keys = Arc::new(ed25519_keys.to_x25519()); + let ciphersuite = Ciphersuite::Default(); + Party { - peer: LpLocalPeer::new(ed25519_keys, lp_x25519_keys.clone()) + peer: LpLocalPeer::new(ciphersuite, ed25519_keys, lp_x25519_keys.clone()) .with_kem_psq_key(lp_x25519_keys), x25519_wg_keys, socket_addr: SocketAddr::from((ip, u16::from_le_bytes(port))), diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index dd7d19c7f7..a551f7d995 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2943,7 +2943,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.0", + "rand 0.9.2", "ring", "rustls 0.23.36", "thiserror 2.0.12", @@ -2968,7 +2968,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.0", + "rand 0.9.2", "resolv-conf", "rustls 0.23.36", "smallvec", @@ -4358,6 +4358,7 @@ dependencies = [ "jwt-simple", "nym-pemstore", "rand 0.8.5", + "rand 0.9.2", "serde", "serde_bytes", "sha2 0.10.9", @@ -5808,7 +5809,7 @@ dependencies = [ "aws-lc-rs", "bytes", "getrandom 0.3.2", - "rand 0.9.0", + "rand 0.9.2", "ring", "rustc-hash", "rustls 0.23.36", @@ -5876,13 +5877,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.24", ] [[package]]