diff --git a/common/nym-kkt/Cargo.toml b/common/nym-kkt/Cargo.toml index 64af80cd91..14b3f2f9fc 100644 --- a/common/nym-kkt/Cargo.toml +++ b/common/nym-kkt/Cargo.toml @@ -20,7 +20,6 @@ libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" } libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] } libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" } -#rand = "0.9.2" rand = "0.9.2" zeroize = { workspace = true, features = ["zeroize_derive"] } classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] } diff --git a/common/nym-kkt/src/encryption.rs b/common/nym-kkt/src/encryption.rs index 7bb7610622..ef8bc56342 100644 --- a/common/nym-kkt/src/encryption.rs +++ b/common/nym-kkt/src/encryption.rs @@ -1,9 +1,9 @@ // Copyright 2025-2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::kkt::KKT_INITIAL_FRAME_AAD; use crate::{ - ciphersuite::CURVE25519_KEY_LEN, context::KKTContext, error::KKTError, frame::KKTFrame, + KKT_INITIAL_FRAME_AAD, ciphersuite::CURVE25519_KEY_LEN, context::KKTContext, error::KKTError, + frame::KKTFrame, }; use blake3::Hasher; use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN}; diff --git a/common/nym-kkt/src/lib.rs b/common/nym-kkt/src/lib.rs index bdfd643cb7..354241254d 100644 --- a/common/nym-kkt/src/lib.rs +++ b/common/nym-kkt/src/lib.rs @@ -7,17 +7,20 @@ pub mod encryption; pub mod error; pub mod frame; pub mod key_utils; -pub mod kkt; +// pub mod kkt; pub mod session; // 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 crate::kkt::KKT_RESPONSE_AAD; + // use crate::kkt::KKT_RESPONSE_AAD; use crate::{ + KKT_RESPONSE_AAD, ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM}, encryption::{ decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame, @@ -495,3 +498,11 @@ mod test { } } } + +// #[test] +// fn test_keys() { +// let a = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rand::rng()); +// println!("{:?}", a.private_key().as_bytes()); +// println!("{:?}", a.public_key().as_bytes()); +// } +// } diff --git a/common/nym-lp/src/lib.rs b/common/nym-lp/src/lib.rs index 7e8eae8bd1..ddaff3968d 100644 --- a/common/nym-lp/src/lib.rs +++ b/common/nym-lp/src/lib.rs @@ -4,7 +4,7 @@ pub mod codec; pub mod config; pub mod error; -pub mod kkt_orchestrator; +// pub mod kkt_orchestrator; pub mod message; pub mod noise_protocol; pub mod packet; diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index 774f148146..ad819444a0 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -8,6 +8,7 @@ use crate::codec::OuterAeadKey; use crate::message::{EncryptedDataPayload, HandshakeData}; +// noiserm use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult}; use crate::packet::LpHeader; use crate::psk::{ @@ -16,9 +17,16 @@ use crate::psk::{ use crate::replay::ReceivingKeyCounterValidator; use crate::{LpError, LpMessage, LpPacket}; use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_kkt::KKT_RESPONSE_AAD; use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; -use nym_kkt::encryption::KKTSessionSecret; -use nym_kkt::kkt::decrypt_kkt_response_frame; +use nym_kkt::encryption::{ + KKTSessionSecret, decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame, + encrypt_kkt_frame, +}; +use nym_kkt::session::{ + anonymous_initiator_process, initiator_ingest_response, responder_ingest_message, + responder_process, +}; use parking_lot::Mutex; use rand::RngCore; use snow::Builder; @@ -161,6 +169,7 @@ pub struct LpSession { /// Flag indicating if this session acts as the Noise protocol initiator. is_initiator: bool, + // noiserm /// Noise protocol state machine noise_state: Mutex, @@ -179,6 +188,7 @@ pub struct LpSession { /// Validator for incoming packet counters to prevent replay attacks receiving_counter: Mutex, + // noiserm /// Safety flag: `true` if real PSK was injected via PSQ, `false` if still using dummy PSK. /// This prevents transport mode operations from running with the insecure dummy `[0u8; 32]` PSK. psk_injected: AtomicBool, @@ -228,6 +238,7 @@ pub struct LpSession { negotiated_version: std::sync::atomic::AtomicU8, } +// noiserm /// Generates a fresh salt for PSK derivation. /// /// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce @@ -363,37 +374,47 @@ impl LpSession { remote_x25519_key: &x25519::PublicKey, salt: &[u8; 32], ) -> Result { + // noiserm // XKpsk3 pattern requires remote static key known upfront (XK) // and PSK mixed at position 3. This provides forward secrecy with PSK authentication. let pattern_name = crate::NOISE_PATTERN; let psk_index = crate::NOISE_PSK_INDEX; + // noiserm let params = pattern_name.parse()?; let builder = Builder::new(params); let local_key_bytes = local_x25519_keypair.private_key().as_bytes(); + // noiserm let builder = builder.local_private_key(local_key_bytes); let remote_key_bytes = remote_x25519_key.to_bytes(); + // noiserm let builder = builder.remote_public_key(&remote_key_bytes); + // noiserm // Initialize with dummy PSK - real PSK will be injected via set_psk() during handshake // when PSQ runs using X25519 as DHKEM let dummy_psk = [0u8; 32]; let builder = builder.psk(psk_index, &dummy_psk); + // noiserm let initial_state = if is_initiator { builder.build_initiator().map_err(LpError::SnowKeyError)? } else { builder.build_responder().map_err(LpError::SnowKeyError)? }; + // noiserm let noise_protocol = NoiseProtocol::new(initial_state); // Initialize KKT state - both roles start at NotStarted let kkt_state = KKTState::NotStarted; // Initialize PSQ state based on role + // georgio: why PSQState::ResponderWaiting if responder? + // georgio: maybe because we can start straight with PSQ? + // georgio: either way, doesn't matter so much because a bad PSQ request will be rejected let psq_state = if is_initiator { PSQState::NotStarted } else { @@ -403,12 +424,14 @@ impl LpSession { Ok(Self { id, is_initiator, + // noiserm noise_state: Mutex::new(noise_protocol), kkt_state: Mutex::new(kkt_state), psq_state: Mutex::new(psq_state), psk_handle: Mutex::new(None), sending_counter: AtomicU64::new(0), receiving_counter: Mutex::new(ReceivingKeyCounterValidator::default()), + // noiserm psk_injected: AtomicBool::new(false), local_ed25519: local_ed25519_keypair.clone(), remote_ed25519_public: *remote_ed25519_key, @@ -503,16 +526,16 @@ impl LpSession { self.psk_handle.lock().clone() } - /// Prepares a KKT (KEM Key Transfer) request message. + /// Prepares an KKT (KEM Key Transfer) request message. + /// Config: Encryption Enabled, One-Way, Anonymous /// - /// This should be called by the initiator before starting the Noise handshake - /// to obtain the responder's KEM public key. The KKT protocol authenticates - /// the exchange using Ed25519 signatures. + /// This should be called by the initiator before starting PSQ + /// to obtain the responder's KEM public key. /// /// **Protocol Flow:** - /// 1. Initiator creates KKT request with Ed25519 signature - /// 2. Responder validates signature and responds with KEM public key + signature - /// 3. Initiator validates response and stores KEM key for PSQ + /// 1. Initiator creates an Encrypted, Anonymous, One-Way, KKT request + /// 2. Responder responds with encrypted KEM public key + signature + /// 3. Initiator validates response and stores KEM key for PSQ use /// /// # Returns /// @@ -520,10 +543,7 @@ impl LpSession { /// * `Some(Err(LpError))` - Error creating KKT request /// * `None` - KKT not applicable (responder, or already completed) pub fn prepare_kkt_request(&self) -> Option> { - use nym_kkt::{ - ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme}, - kkt::request_kem_key, - }; + use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme}; let mut kkt_state = self.kkt_state.lock(); @@ -532,9 +552,9 @@ impl LpSession { return None; } - // Use X25519 as KEM for now (can extend to ML-KEM-768 later) + // georgio this needs to be moved one level up (maybe chosen in LPSession) let ciphersuite = match Ciphersuite::resolve_ciphersuite( - KEM::X25519, + KEM::MlKem768, HashFunction::Blake3, SignatureScheme::Ed25519, None, @@ -549,23 +569,26 @@ impl LpSession { }; let mut rng = rand09::rng(); - match request_kem_key( - &mut rng, - ciphersuite, - self.local_ed25519.private_key(), - &self.remote_x25519_public, - ) { - Ok((session_secret, context, request_bytes)) => { - // Store context for response validation - *kkt_state = KKTState::InitiatorWaiting { - context, - session_secret, - }; + match anonymous_initiator_process(&mut rng, ciphersuite) { + Ok((context, kkt_frame)) => { + match encrypt_initial_kkt_frame(&mut rng, &self.remote_x25519_public, &kkt_frame) { + Ok((session_secret, encrypted_request_bytes)) => { + // Store context for response validation + *kkt_state = KKTState::InitiatorWaiting { + context, + session_secret, + }; - // Serialize KKT frame to bytes - Some(Ok(LpMessage::KKTRequest(crate::message::KKTRequestData( - request_bytes, - )))) + // Serialize KKT frame to bytes + Some(Ok(LpMessage::KKTRequest(crate::message::KKTRequestData( + encrypted_request_bytes, + )))) + } + Err(e) => Some(Err(LpError::Internal(format!( + "KKT request encryption failed: {:?}", + e + )))), + } } Err(e) => Some(Err(LpError::Internal(format!( "KKT request creation failed: {:?}", @@ -576,35 +599,24 @@ impl LpSession { /// Processes a KKT response from the responder. /// - /// Validates the responder's signature and stores the authenticated KEM public key + /// Decrypts and validates the response and stores the authenticated KEM public key /// for use in PSQ encapsulation. /// /// # Arguments /// /// * `response_bytes` - Raw KKT response message from responder - /// * `expected_key_hash` - Optional expected hash of responder's KEM key. - /// - `Some(hash)`: Full KKT validation (signature + hash) - use when directory service available - /// - `None`: Signature-only validation (hash computed from received key) - temporary mode + /// * `expected_key_hash` - Expected hash of responder's KEM key (obtained from directory). /// /// # Returns /// /// * `Ok(())` - KKT exchange completed, KEM key stored /// * `Err(LpError)` - Signature verification failed, hash mismatch, or invalid state /// - /// # Note - /// - /// When None is passed, the function computes the hash from the received key and validates against - /// that (effectively signature-only mode). This allows easy upgrade: just pass Some(directory_hash) - /// when directory service becomes available. The full KKT protocol with hash pinning provides - /// protection against key substitution attacks. pub fn process_kkt_response( &self, - response_bytes: &[u8], - expected_key_hash: Option<&[u8]>, + encrypted_response_bytes: &[u8], + expected_key_hash: &[u8], ) -> Result<(), LpError> { - use nym_kkt::key_utils::hash_encapsulation_key; - use nym_kkt::kkt::validate_kem_response; - let mut kkt_state = self.kkt_state.lock(); // Extract context from waiting state @@ -620,42 +632,37 @@ impl LpSession { } }; - // Determine hash to validate against - let hash_for_validation: Vec; - let hash_ref = match expected_key_hash { - Some(hash) => hash, - None => { - // Signature-only mode: extract key from response and compute its hash - // This effectively bypasses hash validation while keeping signature validation - let (frame, _) = decrypt_kkt_response_frame(&session_secret, response_bytes) - .map_err(|e| { - LpError::Internal(format!("Failed to decrypt KKT response: {:?}", e)) - })?; - - hash_for_validation = hash_encapsulation_key( - &context.ciphersuite().hash_function(), - context.ciphersuite().hash_len(), - frame.body_ref(), - ); - &hash_for_validation - } - }; - - // Validate response and extract KEM key - let kem_pk = validate_kem_response( - &mut context, - &session_secret, - &self.remote_ed25519_public, - hash_ref, - response_bytes, - ) - .map_err(|e| LpError::Internal(format!("KKT response validation failed: {:?}", e)))?; + let remote_encapsulation_key = + match decrypt_kkt_frame(&session_secret, encrypted_response_bytes, KKT_RESPONSE_AAD) { + Ok((response_frame, remote_context)) => { + match initiator_ingest_response( + &mut context, + &response_frame, + &remote_context, + &self.remote_ed25519_public, + expected_key_hash, + ) { + Ok(remote_encapsulation_key) => remote_encapsulation_key, + Err(e) => { + return Err(LpError::Internal(format!( + "KKT response handling failure: {:?}", + e + ))); + } + } + } + Err(e) => { + return Err(LpError::Internal(format!( + "KKT response decryption failure: {:?}", + e + ))); + } + }; // Store the authenticated KEM key *kkt_state = KKTState::Completed { - kem_pk: Box::new(kem_pk), + kem_pk: Box::new(remote_encapsulation_key), }; - Ok(()) } @@ -678,22 +685,38 @@ impl LpSession { request_bytes: &[u8], responder_kem_pk: &EncapsulationKey, ) -> Result { - use nym_kkt::kkt::handle_kem_request; - let mut rng = rand09::rng(); let mut kkt_state = self.kkt_state.lock(); - // Handle request and create signed response - let response_bytes = handle_kem_request( - &mut rng, - request_bytes, - Some(&self.remote_ed25519_public), // Verify initiator signature - self.local_ed25519.private_key(), // Sign response - self.local_x25519.private_key(), - responder_kem_pk, + let (session_secret, request_frame, remote_context) = + decrypt_initial_kkt_frame(self.local_x25519.private_key(), &request_bytes).unwrap(); + + let (mut context, _) = + responder_ingest_message(&remote_context, None, None, &request_frame).unwrap(); + + let response_frame = responder_process( + &mut context, + request_frame.session_id(), + self.local_ed25519.private_key(), + &responder_kem_pk, ) - .map_err(|e| LpError::Internal(format!("KKT request handling failed: {:?}", e)))?; + .unwrap(); + + let response_bytes = + encrypt_kkt_frame(&mut rng, &session_secret, &response_frame, KKT_RESPONSE_AAD) + .unwrap(); + + // Handle request and create signed response + // let response_bytes = handle_kem_request( + // &mut rng, + // request_bytes, + // Some(&self.remote_ed25519_public), // Verify initiator signature + // self.local_ed25519.private_key(), // Sign response + // self.local_x25519.private_key(), + // responder_kem_pk, + // ) + // .map_err(|e| LpError::Internal(format!("KKT request handling failed: {:?}", e)))?; // Mark KKT as processed // Responder doesn't store the kem_pk since they already have their own KEM keypair @@ -762,10 +785,13 @@ impl LpSession { // Store PQ shared secret for subsession PSK derivation *self.pq_shared_secret.lock() = Some(PqSharedSecret::new(psq_result.pq_shared_secret)); + // georgio start psq v2 here + // Inject PSK into Noise HandshakeState if let Err(e) = noise_state.set_psk(3, &psk) { return Some(Err(LpError::NoiseError(e))); } + // noiserm // Mark PSK as injected for safety checks in transport mode self.psk_injected.store(true, Ordering::Release); @@ -1316,6 +1342,7 @@ impl SubsessionHandshake { Ok(LpSession { id: receiver_index, is_initiator: self.is_initiator, + // noiserm noise_state: Mutex::new(noise_state), // KKT: subsession inherits from parent, mark as processed kkt_state: Mutex::new(KKTState::ResponderProcessed), @@ -1326,6 +1353,7 @@ impl SubsessionHandshake { psk_handle: Mutex::new(None), // Subsession doesn't have its own handle sending_counter: AtomicU64::new(0), receiving_counter: Mutex::new(ReceivingKeyCounterValidator::new(0)), + // noiserm psk_injected: AtomicBool::new(true), // PSK was in KKpsk0 local_ed25519: self.local_ed25519, remote_ed25519_public: self.remote_ed25519_public,