Adding concrete types within KKT exchange
This commit is contained in:
@@ -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)]
|
||||
|
||||
+50
-82
@@ -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<u8>,
|
||||
}
|
||||
|
||||
@@ -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<Self, KKTError> {
|
||||
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<R>(
|
||||
&self,
|
||||
self,
|
||||
rng: &mut R,
|
||||
responder_public_key: &DHPublicKey,
|
||||
version_byte: u8,
|
||||
) -> Result<(Carrier, Vec<u8>), 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<DecryptedRequestFrame, KKTError> {
|
||||
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, KKTError> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
pub fn try_to_bytes(&self) -> Result<Vec<u8>, 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<Self, KKTError> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<u8>), 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<u8>), 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<u8>), 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<KKTResponse, KKTError> {
|
||||
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<ProcessedKKTResponse, KKTError> {
|
||||
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<KKTFrame, KKTError> {
|
||||
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<u8>, bool), KKTError> {
|
||||
match remote_context.status() {
|
||||
) -> Result<ProcessedKKTResponse, KKTError> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Self, Self::Error> {
|
||||
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::<MASKED_BYTE_LEN>().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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<u8>,
|
||||
}
|
||||
|
||||
impl KKTRequest {
|
||||
pub(crate) fn into_bytes(mut self) -> Vec<u8> {
|
||||
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<KKTRequest, KKTError> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
Self::create_version_mask(&self.dh_pubkey, responder_pubkey)
|
||||
}
|
||||
|
||||
pub(crate) fn derive_initiator_carrier(
|
||||
&self,
|
||||
initiator_sk: &DHPrivateKey,
|
||||
responder_pubkey: &DHPublicKey,
|
||||
) -> Result<Carrier, KKTError> {
|
||||
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<Carrier, KKTError> {
|
||||
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<EncapsulationKey>,
|
||||
}
|
||||
|
||||
pub struct KKTResponse {
|
||||
/// Encrypted KKT frame that is going to be sent back to the initiator
|
||||
pub encrypted_frame: Vec<u8>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
@@ -1,21 +1,33 @@
|
||||
use std::collections::HashSet;
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<HashFunction>,
|
||||
supported_signature_schemes: HashSet<SignatureScheme>,
|
||||
supported_outer_protocol_versions: HashSet<u8>,
|
||||
|
||||
/// List of supported Hash Functions by this Responder
|
||||
supported_hash_functions: Vec<HashFunction>,
|
||||
|
||||
/// List of supported Signature Schemes by this Responder
|
||||
supported_signature_schemes: Vec<SignatureScheme>,
|
||||
|
||||
/// List of supported outer (LP) protocol version by this Responder
|
||||
supported_outer_protocol_versions: Vec<u8>,
|
||||
}
|
||||
|
||||
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<Self, KKTError> {
|
||||
let hash_functions: HashSet<HashFunction> =
|
||||
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<SignatureScheme> =
|
||||
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<u8> =
|
||||
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<u8> {
|
||||
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<u8>, Option<Vec<u8>>), KKTError> {
|
||||
let (mut carrier, remote_frame, remote_context) = KKTFrame::decrypt_initiator_frame(
|
||||
pub fn process_request(&self, request: KKTRequest) -> Result<ProcessedKKTRequest, KKTError> {
|
||||
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<Vec<u8>>), KKTError> {
|
||||
remote_frame: KKTFrame,
|
||||
) -> Result<(KKTContext, Option<EncapsulationKey>), 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user