From 5536d49f658a98e1639c7c5cbb86b8bc2fa4e623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Feb 2026 09:40:14 +0000 Subject: [PATCH] Adding concrete types within KKT exchange --- common/nym-kkt/src/error.rs | 14 +++ common/nym-kkt/src/frame.rs | 132 +++++++++------------- common/nym-kkt/src/initiator.rs | 101 ++++++++--------- common/nym-kkt/src/lib.rs | 1 + common/nym-kkt/src/masked_byte.rs | 21 ++-- common/nym-kkt/src/message.rs | 168 ++++++++++++++++++++++++++++ common/nym-kkt/src/responder.rs | 177 ++++++++++++++---------------- common/nym-lp/src/psq/mod.rs | 32 +++--- 8 files changed, 387 insertions(+), 259 deletions(-) create mode 100644 common/nym-kkt/src/message.rs diff --git a/common/nym-kkt/src/error.rs b/common/nym-kkt/src/error.rs index 8ace2a9007..68982aec91 100644 --- a/common/nym-kkt/src/error.rs +++ b/common/nym-kkt/src/error.rs @@ -55,6 +55,20 @@ pub enum KKTError { #[error("Generic libcrux error")] LibcruxError, + + #[error("failed to derive shared secret: {inner:?}")] + SharedSecretDerivationFailure { + inner: libcrux_psq::handshake::HandshakeError, + }, + + #[error("the received encapsulation key hash does not match the expected value")] + MismatchedKEMHash, +} + +impl KKTError { + pub fn shared_secret_derivation_failure(inner: libcrux_psq::handshake::HandshakeError) -> Self { + KKTError::SharedSecretDerivationFailure { inner } + } } #[derive(Error, Debug)] diff --git a/common/nym-kkt/src/frame.rs b/common/nym-kkt/src/frame.rs index 6e43178ef5..f079605c4c 100644 --- a/common/nym-kkt/src/frame.rs +++ b/common/nym-kkt/src/frame.rs @@ -7,22 +7,23 @@ // [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::message::{DecryptedRequestFrame, KKTRequest, KKTRequestPlaintext}; use crate::{ carrier::Carrier, context::{KKT_CONTEXT_LEN, KKTContext}, error::KKTError, masked_byte::{MASKED_BYTE_LEN, MaskedByte}, }; +use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey}; +use nym_kkt_ciphersuite::x25519; +use nym_kkt_ciphersuite::x25519::PUBLIC_KEY_LENGTH; +use rand09::{CryptoRng, RngCore}; -const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF"; +pub(crate) const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF"; #[derive(Debug, PartialEq, Clone)] pub struct KKTFrame { - context: [u8; KKT_CONTEXT_LEN], + context: KKTContext, body: Vec, } @@ -31,124 +32,92 @@ pub struct KKTFrame { // if coming from responder => body has the responder's kem public key. impl KKTFrame { - pub fn new(context: &KKTContext, body: &[u8]) -> Result { - let context_bytes = context.encode()?; - Ok(Self { - context: context_bytes, + pub fn new(context: KKTContext, body: &[u8]) -> Self { + Self { + context, body: Vec::from(body), - }) + } + } + + pub fn context(&self) -> &KKTContext { + &self.context } pub fn encrypt_initiator_frame( - &self, + self, rng: &mut R, responder_public_key: &DHPublicKey, version_byte: u8, - ) -> Result<(Carrier, Vec), KKTError> + ) -> Result<(Carrier, KKTRequest), 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 plaintext = + KKTRequestPlaintext::new(ephemeral_keypair.pk, responder_public_key, version_byte); - 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); + let mut carrier = + plaintext.derive_initiator_carrier(ephemeral_keypair.sk(), responder_public_key)?; + let full_kkt_message = plaintext.into_message(&mut carrier, self)?; Ok((carrier, full_kkt_message)) } pub fn decrypt_initiator_frame( responder_keypair: &DHKeyPair, - message: &[u8], + message: KKTRequest, 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]); + ) -> Result { + let mask = message.plaintext.version_mask(&responder_keypair.pk); // 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)?; + let outer_protocol_version = message + .plaintext + .masked_version_bytes + .unmask_check_version(&mask, supported_versions)?; - // now that the version is ok, we can try dh + // after verifying the version, we can perform the DH and continue processing the request + let mut carrier = message + .plaintext + .derive_responder_carrier(responder_keypair)?; - let initiator_public_key = DHPublicKey::from_bytes(&initiator_public_key_bytes); + let decrypted_message = carrier.decrypt(&message.encrypted_frame)?; + let frame = KKTFrame::from_bytes(&decrypted_message)?; - 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 - } - - pub fn context(&self) -> Result { - KKTContext::try_decode(self.context) + Ok(DecryptedRequestFrame { + carrier, + remote_frame: frame, + outer_protocol_version, + }) } pub fn body_ref(&self) -> &[u8] { &self.body } + pub fn body(self) -> Vec { + self.body + } + pub fn body_mut(&mut self) -> &mut [u8] { &mut self.body } pub fn frame_length(&self) -> usize { - self.context.len() + self.body.len() + KKT_CONTEXT_LEN + self.body.len() } - pub fn to_bytes(&self) -> Vec { + pub fn try_to_bytes(&self) -> Result, KKTError> { let mut bytes = Vec::with_capacity(self.frame_length()); - bytes.extend_from_slice(&self.context); + bytes.extend_from_slice(&self.context.encode()?); bytes.extend_from_slice(&self.body); - bytes + Ok(bytes) } - pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> { + pub fn from_bytes(bytes: &[u8]) -> Result { let len = bytes.len(); if bytes.len() < KKT_CONTEXT_LEN { return Err(KKTError::FrameDecodingError { @@ -180,7 +149,6 @@ impl KKTFrame { body.extend_from_slice(body_bytes); } - let frame = KKTFrame::new(&context, &body)?; - Ok((frame, context)) + Ok(KKTFrame::new(context, &body)) } } diff --git a/common/nym-kkt/src/initiator.rs b/common/nym-kkt/src/initiator.rs index 19fff37202..ac9c2b1193 100644 --- a/common/nym-kkt/src/initiator.rs +++ b/common/nym-kkt/src/initiator.rs @@ -1,9 +1,13 @@ +// Copyright 2025-2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use libcrux_psq::handshake::types::DHPublicKey; -use nym_kkt_ciphersuite::{Ciphersuite, KEM}; +use nym_kkt_ciphersuite::Ciphersuite; use rand09::{CryptoRng, RngCore}; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::keys::EncapsulationKey; +use crate::message::{KKTRequest, KKTResponse, ProcessedKKTResponse}; use crate::{ carrier::Carrier, context::{KKTContext, KKTMode, KKTRole, KKTStatus}, @@ -12,25 +16,16 @@ use crate::{ key_utils::validate_encapsulation_key, }; -pub struct KKTResponse { - /// The obtained encapsulation key of the remote - pub encapsulation_key: EncapsulationKey, - - /// Indicates whether responder was able to verify the initiator's kem key, - pub verified_initiator_kem_key: bool, -} - +#[derive(Zeroize, ZeroizeOnDrop)] pub struct KKTInitiator<'a> { carrier: Carrier, + + #[zeroize(skip)] context: KKTContext, + + #[zeroize(skip)] 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 @@ -40,7 +35,7 @@ impl<'a> KKTInitiator<'a> { responder_dh_public_key: &DHPublicKey, expected_hash: &'a [u8], outer_protocol_version: u8, - ) -> Result<(Self, Vec), KKTError> + ) -> Result<(Self, KKTRequest), KKTError> where R: CryptoRng + RngCore, { @@ -63,7 +58,7 @@ impl<'a> KKTInitiator<'a> { responder_dh_public_key: &DHPublicKey, expected_hash: &'a [u8], outer_protocol_version: u8, - ) -> Result<(Self, Vec), KKTError> + ) -> Result<(Self, KKTRequest), KKTError> where R: CryptoRng + RngCore, { @@ -86,11 +81,12 @@ impl<'a> KKTInitiator<'a> { responder_dh_public_key: &DHPublicKey, expected_hash: &'a [u8], outer_protocol_version: u8, - ) -> Result<(Self, Vec), KKTError> + ) -> Result<(Self, KKTRequest), KKTError> where R: CryptoRng + RngCore, { - let (context, frame) = initiator_process(mode, ciphersuite, local_encapsulation_key)?; + let frame = initiator_process(mode, ciphersuite, local_encapsulation_key)?; + let context = *frame.context(); let (carrier, message_bytes) = frame.encrypt_initiator_frame(rng, responder_dh_public_key, outer_protocol_version)?; @@ -104,25 +100,13 @@ impl<'a> KKTInitiator<'a> { )) } - // 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 { - let decrypted_response_bytes = self.carrier.decrypt(response_bytes)?; - let (response_frame, remote_context) = KKTFrame::from_bytes(&decrypted_response_bytes)?; - let (kem_bytes, verified_initiator_kem_key) = initiator_ingest_response( - &mut self.context, - &response_frame, - &remote_context, - self.expected_hash, - )?; - - let encapsulation_key = - EncapsulationKey::try_from_bytes(kem_bytes, self.context.ciphersuite().kem())?; - - Ok(KKTResponse { - encapsulation_key, - verified_initiator_kem_key, - }) + pub fn process_response( + &mut self, + response: KKTResponse, + ) -> Result { + let decrypted_response_bytes = self.carrier.decrypt(&response.encrypted_frame)?; + let response_frame = KKTFrame::from_bytes(&decrypted_response_bytes)?; + initiator_ingest_response(&mut self.context, &response_frame, self.expected_hash) } } @@ -130,7 +114,7 @@ pub fn initiator_process( mode: KKTMode, ciphersuite: Ciphersuite, own_encapsulation_key: Option<&[u8]>, -) -> Result<(KKTContext, KKTFrame), KKTError> { +) -> Result { let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite); let body: &[u8] = match mode { @@ -147,18 +131,16 @@ pub fn initiator_process( }, }; - let frame = KKTFrame::new(&context, body)?; - - Ok((context, frame)) + Ok(KKTFrame::new(context, body)) } pub fn initiator_ingest_response( - own_context: &mut KKTContext, + own_context: &KKTContext, remote_frame: &KKTFrame, - remote_context: &KKTContext, expected_hash: &[u8], -) -> Result<(Vec, bool), KKTError> { - match remote_context.status() { +) -> Result { + let remote_context = remote_frame.context(); + let verified_initiator_kem_key = match remote_context.status() { KKTStatus::Ok | KKTStatus::UnverifiedKEMKey => { match validate_encapsulation_key( own_context.ciphersuite().hash_function(), @@ -166,19 +148,24 @@ pub fn initiator_ingest_response( remote_frame.body_ref(), expected_hash, ) { - true => Ok(( - remote_frame.body_ref().to_vec(), - remote_context.status() != KKTStatus::UnverifiedKEMKey, - )), + true => 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.", - }), + false => return Err(KKTError::MismatchedKEMHash), } } - _ => Err(KKTError::ResponderFlaggedError { - status: remote_context.status(), - }), - } + _ => { + return Err(KKTError::ResponderFlaggedError { + status: remote_context.status(), + }); + } + }; + + let kem = own_context.ciphersuite().kem(); + let kem_bytes = remote_frame.body_ref(); + let encapsulation_key = EncapsulationKey::try_from_bytes(kem_bytes.to_vec(), kem)?; + Ok(ProcessedKKTResponse { + encapsulation_key, + verified_initiator_kem_key, + }) } diff --git a/common/nym-kkt/src/lib.rs b/common/nym-kkt/src/lib.rs index 1c853d815b..0dbe1e9a7f 100644 --- a/common/nym-kkt/src/lib.rs +++ b/common/nym-kkt/src/lib.rs @@ -9,6 +9,7 @@ pub mod initiator; pub mod key_utils; pub mod keys; pub mod masked_byte; +pub mod message; pub mod rekey; pub mod responder; diff --git a/common/nym-kkt/src/masked_byte.rs b/common/nym-kkt/src/masked_byte.rs index 20dab743b4..2bdd29edae 100644 --- a/common/nym-kkt/src/masked_byte.rs +++ b/common/nym-kkt/src/masked_byte.rs @@ -78,7 +78,7 @@ impl MaskedByte { &self.0 } - pub fn to_bytes(self) -> [u8; 16] { + pub fn to_bytes(self) -> [u8; MASKED_BYTE_LEN] { self.0 } } @@ -91,7 +91,7 @@ impl From<[u8; MASKED_BYTE_LEN]> for MaskedByte { impl From<&[u8; MASKED_BYTE_LEN]> for MaskedByte { fn from(value: &[u8; MASKED_BYTE_LEN]) -> Self { - MaskedByte(value.to_owned()) + MaskedByte(*value) } } @@ -99,14 +99,13 @@ impl TryFrom<&[u8]> for MaskedByte { type Error = MaskedByteError; fn try_from(value: &[u8]) -> Result { - if value.len() != MASKED_BYTE_LEN { - Err(InvalidLength { + let Ok(inner) = value.try_into() else { + return Err(InvalidLength { expected: MASKED_BYTE_LEN, actual: value.len(), - }) - } else { - Ok(Self::from(value.as_chunks::().0[0])) - } + }); + }; + Ok(MaskedByte(inner)) } } @@ -171,19 +170,19 @@ mod test { // add more one more byte wire_bytes_messy.push(0x42); - assert!(wire_bytes_messy.len() == MASKED_BYTE_LEN + 1); + assert_eq!(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); + assert_eq!(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); + assert_eq!(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/message.rs b/common/nym-kkt/src/message.rs new file mode 100644 index 0000000000..8a7b89cdf8 --- /dev/null +++ b/common/nym-kkt/src/message.rs @@ -0,0 +1,168 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::carrier::Carrier; +use crate::context::KKTContext; +use crate::error::KKTError; +use crate::frame::KKTFrame; +use crate::keys::EncapsulationKey; +use crate::masked_byte::{MASKED_BYTE_LEN, MaskedByte}; +use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey}; +use nym_kkt_ciphersuite::x25519; + +pub struct KKTRequest { + /// The plaintext part of the request + pub(crate) plaintext: KKTRequestPlaintext, + + /// Ciphertext of an initial request `KKTFrame` + pub(crate) encrypted_frame: Vec, +} + +impl KKTRequest { + pub(crate) fn into_bytes(mut self) -> Vec { + let mut out = self.plaintext.to_bytes(); + out.append(&mut self.encrypted_frame); + out + } +} + +pub(crate) struct KKTRequestPlaintext { + /// Ephemeral Diffie-Hellman public key of the initiator + pub(crate) dh_pubkey: DHPublicKey, + + /// Masked bytes representing the outer protocol version information + pub(crate) masked_version_bytes: MaskedByte, +} + +impl KKTRequestPlaintext { + pub(crate) fn new( + initiator_pubkey: DHPublicKey, + responder_pubkey: &DHPublicKey, + outer_protocol_version: u8, + ) -> Self { + let mask = Self::create_version_mask(&initiator_pubkey, responder_pubkey); + let masked_version_bytes = MaskedByte::new(outer_protocol_version, &mask); + KKTRequestPlaintext { + dh_pubkey: initiator_pubkey, + masked_version_bytes, + } + } + + pub(crate) fn into_message( + self, + carrier: &mut Carrier, + frame: KKTFrame, + ) -> Result { + let frame_bytes = frame.try_to_bytes()?; + let frame_ciphertext = carrier.encrypt(&frame_bytes)?; + Ok(KKTRequest { + plaintext: self, + encrypted_frame: frame_ciphertext, + }) + } + + pub(crate) fn create_version_mask( + initiator_pubkey: &DHPublicKey, + responder_pubkey: &DHPublicKey, + ) -> Vec { + let mut mask = Vec::with_capacity(2 * x25519::PUBLIC_KEY_LENGTH); + mask.extend_from_slice(&initiator_pubkey.as_ref()); + mask.extend_from_slice(&responder_pubkey.as_ref()); + mask + } + + fn create_carrier_ctx( + masked_version: &MaskedByte, + initiator_pubkey: &DHPublicKey, + responder_pubkey: &DHPublicKey, + ) -> Vec { + let mut context = Vec::new(); + context.extend_from_slice(masked_version.as_slice()); + context.extend_from_slice(crate::frame::KKT_CARRIER_CONTEXT); + context.extend_from_slice(initiator_pubkey.as_ref()); + context.extend_from_slice(responder_pubkey.as_ref()); + context + } + + pub(crate) fn to_bytes(&self) -> Vec { + let mut out = Vec::with_capacity(x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN); + out.extend_from_slice(self.dh_pubkey.as_ref()); + out.extend_from_slice(&self.masked_version_bytes.as_slice()); + out + } + + pub(crate) fn version_mask(&self, responder_pubkey: &DHPublicKey) -> Vec { + Self::create_version_mask(&self.dh_pubkey, responder_pubkey) + } + + pub(crate) fn derive_initiator_carrier( + &self, + initiator_sk: &DHPrivateKey, + responder_pubkey: &DHPublicKey, + ) -> Result { + let ctx = Self::create_carrier_ctx( + &self.masked_version_bytes, + &self.dh_pubkey, + responder_pubkey, + ); + + let shared_secret = initiator_sk + .diffie_hellman(responder_pubkey) + .map_err(KKTError::shared_secret_derivation_failure)?; + + Ok(Carrier::from_secret_slice(shared_secret.as_ref(), &ctx)) + } + + pub(crate) fn derive_responder_carrier( + &self, + responder_keys: &DHKeyPair, + ) -> Result { + let ctx = Self::create_carrier_ctx( + &self.masked_version_bytes, + &self.dh_pubkey, + &responder_keys.pk, + ); + let shared_secret = responder_keys + .sk() + .diffie_hellman(&self.dh_pubkey) + .map_err(KKTError::shared_secret_derivation_failure)?; + Ok(Carrier::from_secret_slice(shared_secret.as_ref(), &ctx).flip_keys()) + } +} + +pub(crate) struct DecryptedRequestFrame { + /// Derived carrier used for decrypting this frame and encrypting the response + pub(crate) carrier: Carrier, + + /// The remote frame sent in the message + pub(crate) remote_frame: KKTFrame, + + /// The unmasked byte representing the outer protocol version sent by the initiator + pub(crate) outer_protocol_version: u8, +} + +impl DecryptedRequestFrame { + pub(crate) fn remote_context(&self) -> &KKTContext { + self.remote_frame.context() + } +} + +pub struct ProcessedKKTRequest { + pub response: KKTResponse, + + /// The obtained encapsulation key of the remote + pub remote_encapsulation_key: Option, +} + +pub struct KKTResponse { + /// Encrypted KKT frame that is going to be sent back to the initiator + pub encrypted_frame: Vec, +} + +pub struct ProcessedKKTResponse { + /// The obtained encapsulation key of the remote + pub encapsulation_key: EncapsulationKey, + + /// Indicates whether responder was able to verify the initiator's kem key, + pub verified_initiator_kem_key: bool, +} diff --git a/common/nym-kkt/src/responder.rs b/common/nym-kkt/src/responder.rs index f9457cc8a9..bc91f1cac6 100644 --- a/common/nym-kkt/src/responder.rs +++ b/common/nym-kkt/src/responder.rs @@ -1,21 +1,33 @@ -use std::collections::HashSet; +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use crate::key_utils::validate_encapsulation_key; -use crate::keys::KEMKeys; +use crate::keys::{EncapsulationKey, KEMKeys}; +use crate::message::{KKTRequest, KKTResponse, ProcessedKKTRequest}; 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}; +use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, SignatureScheme}; +/// Representation of a KKT Responder pub struct KKTResponder<'a> { + /// Long-term x25519 DH key pair of this Responder x25519_keypair: &'a DHKeyPair, + + /// KEM keys of this responder kem_keys: &'a KEMKeys, - supported_hash_functions: HashSet, - supported_signature_schemes: HashSet, - supported_outer_protocol_versions: HashSet, + + /// List of supported Hash Functions by this Responder + supported_hash_functions: Vec, + + /// List of supported Signature Schemes by this Responder + supported_signature_schemes: Vec, + + /// List of supported outer (LP) protocol version by this Responder + supported_outer_protocol_versions: Vec, } impl<'a> KKTResponder<'a> { @@ -23,68 +35,50 @@ impl<'a> KKTResponder<'a> { x25519_keypair: &'a DHKeyPair, kem_keys: &'a KEMKeys, supported_hash_functions: &[HashFunction], - supported_outer_protocol_versions: &[u8], supported_signature_schemes: &[SignatureScheme], + supported_outer_protocol_versions: &[u8], ) -> Result { - let hash_functions: HashSet = - supported_hash_functions.iter().copied().collect(); - - if hash_functions.is_empty() { + if supported_hash_functions.is_empty() { return Err(KKTError::FunctionInputError { - info: "Did not provide a supported HashFunction when instaciating a KKTResponder", + info: "Did not provide a supported HashFunction when instantiating a KKTResponder", }); } - let signature_schemes: HashSet = - supported_signature_schemes.iter().copied().collect(); - - if signature_schemes.is_empty() { + if supported_signature_schemes.is_empty() { return Err(KKTError::FunctionInputError { - info: "Did not provide a supported SignatureScheme when instaciating a KKTResponder", + info: "Did not provide a supported SignatureScheme when instantiating a KKTResponder", }); } - 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 { - Ok(Self { - x25519_keypair, - kem_keys, - supported_hash_functions: hash_functions, - supported_signature_schemes: signature_schemes, - supported_outer_protocol_versions: outer_protocol_versions, - }) + if supported_outer_protocol_versions.is_empty() { + return Err(KKTError::FunctionInputError { + info: "Did not provide a supported outer protocol version when instantiating a KKTResponder", + }); } - } - fn supported_protocol_versions(&self) -> Vec { - self.supported_outer_protocol_versions - .iter() - .copied() - .collect() + + Ok(Self { + x25519_keypair, + kem_keys, + supported_hash_functions: supported_hash_functions.to_vec(), + supported_signature_schemes: supported_signature_schemes.to_vec(), + supported_outer_protocol_versions: supported_outer_protocol_versions.to_vec(), + }) } fn check_ciphersuite_compatiblity( &self, remote_ciphersuite: Ciphersuite, ) -> Result<(), KKTError> { - if !self - .supported_hash_functions - .contains(&remote_ciphersuite.hash_function()) - { + let r_hash = remote_ciphersuite.hash_function(); + let r_sig = remote_ciphersuite.signature_scheme(); + + if !self.supported_hash_functions.contains(&r_hash) { return Err(KKTError::IncompatibilityError { info: "Unsupported HashFunction", }); } - if !self - .supported_signature_schemes - .contains(&remote_ciphersuite.signature_scheme()) - { + if !self.supported_signature_schemes.contains(&r_sig) { return Err(KKTError::IncompatibilityError { info: "Unsupported SignatureScheme", }); @@ -93,29 +87,30 @@ impl<'a> KKTResponder<'a> { Ok(()) } - // When this function fails, we do that silently (i.e. we dont generate a response to the initiator). + // When this function fails, we do that silently (i.e. we don't 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( + pub fn process_request(&self, request: KKTRequest) -> Result { + let processed_req = KKTFrame::decrypt_initiator_frame( self.x25519_keypair, - request_bytes, - &self.supported_protocol_versions(), + request, + &self.supported_outer_protocol_versions, )?; + let remote_context = *processed_req.remote_context(); + let remote_frame = processed_req.remote_frame; + let mut carrier = processed_req.carrier; + 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::OneWay => responder_ingest_message(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)? + responder_ingest_message(None, remote_frame)? } }; @@ -125,20 +120,24 @@ impl<'a> KKTResponder<'a> { info: "Unsupported KEM", }); }; - let frame = KKTFrame::new(&local_context, kem_key)?; + let frame = KKTFrame::new(local_context, kem_key); // encryption - responder frame - let response_bytes = carrier.encrypt(&frame.to_bytes())?; - Ok((response_bytes, remote_encapsulation_key)) + let encrypted_frame = carrier.encrypt(&frame.try_to_bytes()?)?; + Ok(ProcessedKKTRequest { + response: KKTResponse { encrypted_frame }, + remote_encapsulation_key, + }) } } pub fn responder_ingest_message( - remote_context: &KKTContext, expected_hash: Option<&[u8]>, - remote_frame: &KKTFrame, -) -> Result<(KKTContext, Option>), KKTError> { + remote_frame: KKTFrame, +) -> Result<(KKTContext, Option), KKTError> { + let remote_context = remote_frame.context(); let mut own_context = remote_context.derive_responder_header()?; + let cs = own_context.ciphersuite(); match remote_context.role() { KKTRole::Initiator => { @@ -146,39 +145,33 @@ pub fn responder_ingest_message( 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 + let Some(expected_hash) = expected_hash else { + 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))) - // + // 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))) + // + return Ok((own_context, None)); + }; - Ok((own_context, None)) - } + if !validate_encapsulation_key( + cs.hash_function(), + cs.hash_len(), + remote_frame.body_ref(), + expected_hash, + ) { + // The key does not match the hash obtained from the directory + return Err(KKTError::MismatchedKEMHash); } + let remote_key = + EncapsulationKey::try_from_bytes(remote_frame.body(), cs.kem())?; + Ok((own_context, Some(remote_key))) } } } diff --git a/common/nym-lp/src/psq/mod.rs b/common/nym-lp/src/psq/mod.rs index b58e7e898f..528dc4d1e3 100644 --- a/common/nym-lp/src/psq/mod.rs +++ b/common/nym-lp/src/psq/mod.rs @@ -494,39 +494,37 @@ mod tests { fn e2e_test_plain() { let mut rng = deterministic_rng_09(); + // SETUP START: let kem = KEM::MlKem768; let protocol_version = 1; let (mut init, resp) = mock_peers(); - init.ciphersuite = Ciphersuite::default().with_kem(kem); let resp_remote = resp.as_remote(); let dir_hash = resp_remote.expected_kem_key_hash(init.ciphersuite).unwrap(); let resp_keys = resp.kem_keypairs.as_ref().unwrap(); - - // generate responder x25519 keys let responder_x25519_keypair = resp.x25519(); - let hash_function = HashFunction::Blake3; - // generate kem public keys + let supported_sigs = [SignatureScheme::Ed25519]; + let supported_hash = [ + HashFunction::Blake3, + HashFunction::Shake256, + HashFunction::Shake128, + HashFunction::SHA256, + ]; let kkt_responder = KKTResponder::new( &responder_x25519_keypair, &resp_keys, - &[ - HashFunction::Blake3, - HashFunction::SHA256, - HashFunction::Shake128, - HashFunction::Shake256, - ], + &supported_hash, + &supported_sigs, &[protocol_version], - &[SignatureScheme::Ed25519], ) .unwrap(); - // OneWay - MlKem - let psq_ciphersuite = CiphersuiteName::X25519_MLKEM768_X25519_AESGCM128_HKDFSHA256; + // SETUP END - let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request( + // OneWay - MlKem + let (mut initiator, request) = KKTInitiator::generate_one_way_request( &mut rng, init.ciphersuite, &responder_x25519_keypair.pk, @@ -535,9 +533,9 @@ mod tests { ) .unwrap(); - let (response_bytes, _) = kkt_responder.process_request(&request_bytes).unwrap(); + let processed_req = kkt_responder.process_request(request).unwrap(); - let response = initiator.process_response(&response_bytes).unwrap(); + let response = initiator.process_response(processed_req.response).unwrap(); let encapsulation_key = response.encapsulation_key; let mut msg_channel = vec![0u8; 8192];