Compare commits

...

11 Commits

Author SHA1 Message Date
Jędrzej Stuczyński d8958126e8 tmp: try mlkem 2026-01-26 15:04:28 +00:00
Georgio Nicolas fe45b856b7 make clippy happy 2026-01-26 15:16:29 +01:00
Georgio Nicolas 5c50b760a8 enable kkt with mceliece (kkt tests pass) 2026-01-26 15:11:08 +01:00
Georgio Nicolas aafa99ea47 kkt should work with different keys now 2026-01-26 15:01:43 +01:00
Georgio Nicolas 4767a719f7 impl hashing of different key types 2026-01-26 14:38:25 +01:00
Georgio Nicolas 89d167de08 add key input helper functions 2026-01-26 14:25:12 +01:00
Georgio Nicolas 05d5f4ae83 Merge remote-tracking branch 'origin' into georgio/lp-psqv2-integration 2026-01-26 13:28:40 +01:00
Georgio Nicolas 2109beeef6 Update keytypes and remove lifetime specifier for mceliece keys 2026-01-26 13:20:18 +01:00
Georgio Nicolas d1a5342625 Merge remote-tracking branch 'origin' into georgio/lp-psqv2-integration 2026-01-25 01:20:43 +01:00
Georgio Nicolas f0645bad57 Merge develop 2026-01-23 13:56:37 +01:00
Georgio Nicolas 60f8fe09a7 Introduce newer mlkem key types 2026-01-23 13:48:55 +01:00
13 changed files with 754 additions and 524 deletions
Generated
+470 -365
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -23,9 +23,15 @@ libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
rand = "0.9.2" rand = "0.9.2"
zeroize = { workspace = true, features = ["zeroize_derive"] } zeroize = { workspace = true, features = ["zeroize_derive"] }
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] } # classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
classic-mceliece-rust = { version = "3.1.0", features = [
"mceliece460896f",
"zeroize"] }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["classic-mceliece"] }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux"}
[dev-dependencies] [dev-dependencies]
rand_chacha = "0.9.0" rand_chacha = "0.9.0"
anyhow = { workspace = true } anyhow = { workspace = true }
+6 -3
View File
@@ -12,7 +12,10 @@ use nym_kkt::{
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM, SignatureScheme}, ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM, SignatureScheme},
context::KKTMode, context::KKTMode,
frame::KKTFrame, 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::{ session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process, anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process, responder_ingest_message, responder_process,
@@ -69,8 +72,8 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let (responder_kem_public_key, initiator_kem_public_key) = match kem { let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => ( KEM::MlKem768 => (
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1), EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1), EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
), ),
KEM::XWing => ( KEM::XWing => (
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
+38 -14
View File
@@ -3,23 +3,27 @@
use crate::error::KKTError; use crate::error::KKTError;
use libcrux_kem::Algorithm; use libcrux_kem::Algorithm;
use std::fmt::Debug;
pub use nym_kkt_ciphersuite::*; pub use nym_kkt_ciphersuite::*;
pub enum EncapsulationKey<'a> { pub enum EncapsulationKey {
MlKem768(libcrux_kem::PublicKey), MlKem768(libcrux_kem::PublicKey),
XWing(libcrux_kem::PublicKey), XWing(libcrux_kem::PublicKey),
X25519(libcrux_kem::PublicKey), X25519(libcrux_kem::PublicKey),
McEliece(classic_mceliece_rust::PublicKey<'a>), McEliece(libcrux_psq::classic_mceliece::PublicKey),
} }
impl Debug for EncapsulationKey {
pub enum DecapsulationKey<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
MlKem768(libcrux_kem::PrivateKey), match self {
XWing(libcrux_kem::PrivateKey), Self::MlKem768(_) => f.debug_tuple("MlKem768").finish(),
X25519(libcrux_kem::PrivateKey), Self::XWing(_) => f.debug_tuple("XWing").finish(),
McEliece(classic_mceliece_rust::SecretKey<'a>), Self::X25519(_) => f.debug_tuple("X25519").finish(),
Self::McEliece(_) => f.debug_tuple("McEliece").finish(),
}
}
} }
impl<'a> EncapsulationKey<'a> { impl EncapsulationKey {
pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> { pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> {
match kem { match kem {
KEM::McEliece => { KEM::McEliece => {
@@ -33,7 +37,7 @@ impl<'a> EncapsulationKey<'a> {
// Size must be correct due to KKTFrame::from_bytes(message_bytes)? // Size must be correct due to KKTFrame::from_bytes(message_bytes)?
public_key_bytes.clone_from_slice(bytes); public_key_bytes.clone_from_slice(bytes);
Ok(EncapsulationKey::McEliece( Ok(EncapsulationKey::McEliece(
classic_mceliece_rust::PublicKey::from(public_key_bytes), libcrux_psq::classic_mceliece::PublicKey::from(public_key_bytes),
)) ))
} }
} }
@@ -54,10 +58,30 @@ impl<'a> EncapsulationKey<'a> {
pub fn encode(&self) -> Vec<u8> { pub fn encode(&self) -> Vec<u8> {
match self { match self {
EncapsulationKey::XWing(public_key) EncapsulationKey::XWing(public_key) | EncapsulationKey::X25519(public_key) => {
| EncapsulationKey::MlKem768(public_key) public_key.encode()
| EncapsulationKey::X25519(public_key) => public_key.encode(), }
EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()), EncapsulationKey::McEliece(public_key) => {
let bytes_ref: &[u8] = public_key.as_ref();
Vec::from(bytes_ref)
}
EncapsulationKey::MlKem768(public_key) => Vec::from(public_key.encode()),
}
}
}
pub enum DecapsulationKey {
MlKem768(libcrux_kem::MlKem768PrivateKey),
XWing(libcrux_kem::PrivateKey),
X25519(libcrux_kem::PrivateKey),
McEliece(libcrux_psq::classic_mceliece::SecretKey),
}
impl Debug for DecapsulationKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MlKem768(_) => f.debug_tuple("MlKem768").finish(),
Self::XWing(_) => f.debug_tuple("XWing").finish(),
Self::X25519(_) => f.debug_tuple("X25519").finish(),
Self::McEliece(_) => f.debug_tuple("McEliece").finish(),
} }
} }
} }
+3
View File
@@ -48,6 +48,9 @@ pub enum KKTError {
#[error("{}", info)] #[error("{}", info)]
AEADError { info: &'static str }, AEADError { info: &'static str },
#[error("{}", info)]
DecodingError { info: &'static str },
#[error("Generic libcrux error")] #[error("Generic libcrux error")]
LibcruxError, LibcruxError,
} }
+15 -10
View File
@@ -1,8 +1,7 @@
use crate::ciphersuite::HashFunction; use crate::ciphersuite::HashFunction;
use std::collections::HashMap; use std::collections::HashMap;
use classic_mceliece_rust::keypair_boxed; use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KEMKeyDigests}; use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KEMKeyDigests};
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
@@ -38,9 +37,6 @@ where
R: RngCore + CryptoRng, R: RngCore + CryptoRng,
{ {
match kem { 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( crate::ciphersuite::KEM::XWing => Ok(libcrux_kem::key_gen(
libcrux_kem::Algorithm::XWingKemDraft06, libcrux_kem::Algorithm::XWingKemDraft06,
rng, rng,
@@ -53,19 +49,28 @@ where
}), }),
} }
} }
pub fn generate_keypair_mlkem<R>(rng: &mut R) -> (MlKem768PrivateKey, MlKem768PublicKey)
where
R: RngCore + CryptoRng,
{
libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng).into_parts()
}
// (decapsulation_key, encapsulation_key) // (decapsulation_key, encapsulation_key)
pub fn generate_keypair_mceliece<'a, R>( pub fn generate_keypair_mceliece<R>(
rng: &mut R, rng: &mut R,
) -> ( ) -> (
classic_mceliece_rust::SecretKey<'a>, libcrux_psq::classic_mceliece::SecretKey,
classic_mceliece_rust::PublicKey<'a>, libcrux_psq::classic_mceliece::PublicKey,
) )
where where
// this is annoying because mceliece lib uses rand 0.8.5... // this is annoying because mceliece lib uses rand 0.8.5...
R: RngCore + CryptoRng, R: RngCore + CryptoRng,
{ {
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng); let kp = libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng);
(decapsulation_key, encapsulation_key)
(kp.sk, kp.pk)
} }
pub fn hash_key_bytes( pub fn hash_key_bytes(
+3 -3
View File
@@ -98,13 +98,13 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
/// )?; /// )?;
/// // Use gateway_kem_key for PSQ /// // Use gateway_kem_key for PSQ
/// ``` /// ```
pub fn validate_kem_response<'a>( pub fn validate_kem_response(
context: &mut KKTContext, context: &mut KKTContext,
session_secret: &KKTSessionSecret, session_secret: &KKTSessionSecret,
responder_vk: &ed25519::PublicKey, responder_vk: &ed25519::PublicKey,
expected_key_hash: &[u8], expected_key_hash: &[u8],
encrypted_response_bytes: &[u8], encrypted_response_bytes: &[u8],
) -> Result<EncapsulationKey<'a>, KKTError> { ) -> Result<EncapsulationKey, KKTError> {
let (responder_frame, responder_context) = let (responder_frame, responder_context) =
decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?; decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?;
@@ -159,7 +159,7 @@ pub fn handle_kem_request<'a, R>(
initiator_vk: Option<&ed25519::PublicKey>, initiator_vk: Option<&ed25519::PublicKey>,
responder_signing_key: &ed25519::PrivateKey, responder_signing_key: &ed25519::PrivateKey,
responder_dh_private_key: &x25519::PrivateKey, responder_dh_private_key: &x25519::PrivateKey,
responder_kem_key: &EncapsulationKey<'a>, responder_kem_key: &EncapsulationKey,
) -> Result<Vec<u8>, KKTError> ) -> Result<Vec<u8>, KKTError>
where where
R: RngCore + CryptoRng, R: RngCore + CryptoRng,
+5 -13
View File
@@ -28,7 +28,7 @@ mod test {
frame::KKTFrame, frame::KKTFrame,
key_utils::{ key_utils::{
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece, generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece,
generate_keypair_x25519, hash_encapsulation_key, generate_keypair_mlkem, generate_keypair_x25519, hash_encapsulation_key,
}, },
session::{ session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process, anonymous_initiator_process, initiator_ingest_response, initiator_process,
@@ -63,12 +63,8 @@ mod test {
let (responder_kem_public_key, initiator_kem_public_key) = match kem { let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => ( KEM::MlKem768 => (
EncapsulationKey::MlKem768( EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
generate_keypair_libcrux(&mut rng, kem).unwrap().1, EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
),
EncapsulationKey::MlKem768(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
), ),
KEM::XWing => ( KEM::XWing => (
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
@@ -269,12 +265,8 @@ mod test {
let (responder_kem_public_key, initiator_kem_public_key) = match kem { let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => ( KEM::MlKem768 => (
EncapsulationKey::MlKem768( EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
generate_keypair_libcrux(&mut rng, kem).unwrap().1, EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
),
EncapsulationKey::MlKem768(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
), ),
KEM::XWing => ( KEM::XWing => (
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1), EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
+8 -8
View File
@@ -10,12 +10,12 @@ use crate::{
key_utils::validate_encapsulation_key, key_utils::validate_encapsulation_key,
}; };
pub fn initiator_process<'a, R>( pub fn initiator_process<R>(
rng: &mut R, rng: &mut R,
mode: KKTMode, mode: KKTMode,
ciphersuite: Ciphersuite, ciphersuite: Ciphersuite,
signing_key: &ed25519::PrivateKey, signing_key: &ed25519::PrivateKey,
own_encapsulation_key: Option<&EncapsulationKey<'a>>, own_encapsulation_key: Option<&EncapsulationKey>,
) -> Result<(KKTContext, KKTFrame), KKTError> ) -> Result<(KKTContext, KKTFrame), KKTError>
where where
R: CryptoRng + RngCore, R: CryptoRng + RngCore,
@@ -72,13 +72,13 @@ where
Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[]))) Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[])))
} }
pub fn initiator_ingest_response<'a>( pub fn initiator_ingest_response(
own_context: &mut KKTContext, own_context: &mut KKTContext,
remote_frame: &KKTFrame, remote_frame: &KKTFrame,
remote_context: &KKTContext, remote_context: &KKTContext,
remote_verification_key: &ed25519::PublicKey, remote_verification_key: &ed25519::PublicKey,
expected_hash: &[u8], expected_hash: &[u8],
) -> Result<EncapsulationKey<'a>, KKTError> { ) -> Result<EncapsulationKey, KKTError> {
check_compatibility(own_context, remote_context)?; check_compatibility(own_context, remote_context)?;
match remote_context.status() { match remote_context.status() {
KKTStatus::Ok => { KKTStatus::Ok => {
@@ -124,12 +124,12 @@ pub fn initiator_ingest_response<'a>(
// todo: figure out how to handle errors using status codes // todo: figure out how to handle errors using status codes
pub fn responder_ingest_message<'a>( pub fn responder_ingest_message(
remote_context: &KKTContext, remote_context: &KKTContext,
remote_verification_key: Option<&ed25519::PublicKey>, remote_verification_key: Option<&ed25519::PublicKey>,
expected_hash: Option<&[u8]>, expected_hash: Option<&[u8]>,
remote_frame: &KKTFrame, remote_frame: &KKTFrame,
) -> Result<(KKTContext, Option<EncapsulationKey<'a>>), KKTError> { ) -> Result<(KKTContext, Option<EncapsulationKey>), KKTError> {
let own_context = remote_context.derive_responder_header()?; let own_context = remote_context.derive_responder_header()?;
match remote_context.role() { match remote_context.role() {
@@ -200,11 +200,11 @@ pub fn responder_ingest_message<'a>(
} }
} }
pub fn responder_process<'a>( pub fn responder_process(
own_context: &mut KKTContext, own_context: &mut KKTContext,
session_id: KKTSessionId, session_id: KKTSessionId,
signing_key: &ed25519::PrivateKey, signing_key: &ed25519::PrivateKey,
encapsulation_key: &EncapsulationKey<'a>, encapsulation_key: &EncapsulationKey,
) -> Result<KKTFrame, KKTError> { ) -> Result<KKTFrame, KKTError> {
let body = encapsulation_key.encode(); let body = encapsulation_key.encode();
+92 -16
View File
@@ -1,14 +1,16 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net> // Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
use nym_crypto::asymmetric::{ed25519, x25519}; use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::ciphersuite::{HashFunction, KEM}; use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey, HashFunction, KEM};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
/// Representation of a local Lewes Protocol peer /// Representation of a local Lewes Protocol peer
/// encapsulating all the known information and keys. /// encapsulating all the known information and keys.
#[derive(Debug, Clone)] #[derive(Clone)]
pub struct LpLocalPeer { pub struct LpLocalPeer {
/// Local Ed25519 keys for PSQ authentication /// Local Ed25519 keys for PSQ authentication
pub(crate) ed25519: Arc<ed25519::KeyPair>, pub(crate) ed25519: Arc<ed25519::KeyPair>,
@@ -16,8 +18,14 @@ pub struct LpLocalPeer {
/// Local x25519 keys (Noise static key) /// Local x25519 keys (Noise static key)
pub(crate) x25519: Arc<x25519::KeyPair>, pub(crate) x25519: Arc<x25519::KeyPair>,
/// Local KEM key used for PSQ /// Local KEM key used for PSQ (x25519: to deprecate)
pub(crate) kem_psq: Option<Arc<x25519::KeyPair>>, pub(crate) kem_psq: Option<Arc<x25519::KeyPair>>,
/// Local MlKem keypair used for PSQ
pub(crate) mlkem: Option<(Arc<DecapsulationKey>, Arc<EncapsulationKey>)>,
/// Local McEliece keypair used for PSQ
pub(crate) mceliece: Option<(Arc<DecapsulationKey>, Arc<EncapsulationKey>)>,
} }
impl LpLocalPeer { impl LpLocalPeer {
@@ -26,15 +34,43 @@ impl LpLocalPeer {
ed25519, ed25519,
x25519, x25519,
kem_psq: None, kem_psq: None,
mlkem: None,
mceliece: None,
} }
} }
#[must_use] // #[must_use]
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self { pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
self.kem_psq = Some(key); self.kem_psq = Some(key);
self self
} }
pub fn with_mlkem_keypair(
mut self,
decapsulation_key: &MlKem768PrivateKey,
encapsulation_key: &MlKem768PublicKey,
) -> Self {
self.mlkem = Some((
Arc::new(DecapsulationKey::MlKem768(decapsulation_key.clone())),
Arc::new(EncapsulationKey::MlKem768(
libcrux_kem::PublicKey::MlKem768(encapsulation_key.clone()),
)),
));
self
}
pub fn with_mceliece_keypair(
mut self,
decapsulation_key: libcrux_psq::classic_mceliece::SecretKey,
encapsulation_key: libcrux_psq::classic_mceliece::PublicKey,
) -> Self {
self.mceliece = Some((
Arc::new(DecapsulationKey::McEliece(decapsulation_key)),
Arc::new(EncapsulationKey::McEliece(encapsulation_key)),
));
self
}
pub fn ed25519(&self) -> &Arc<ed25519::KeyPair> { pub fn ed25519(&self) -> &Arc<ed25519::KeyPair> {
&self.ed25519 &self.ed25519
} }
@@ -46,17 +82,28 @@ impl LpLocalPeer {
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests /// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
#[doc(hidden)] #[doc(hidden)]
pub fn as_remote(&self) -> LpRemotePeer { pub fn as_remote(&self) -> LpRemotePeer {
let expected_kem_key_digests = match &self.kem_psq { let mut expected_kem_key_digests = HashMap::new();
None => HashMap::new(),
Some(kem_keys) => { if let Some(x25519_key) = &self.kem_psq {
let hashes = expected_kem_key_digests.insert(
nym_kkt::key_utils::produce_key_digests(kem_keys.public_key().as_bytes()); KEM::X25519,
nym_kkt::key_utils::produce_key_digests(x25519_key.public_key().as_bytes()),
);
}
if let Some(mlkem_key) = &self.mlkem {
expected_kem_key_digests.insert(
KEM::MlKem768,
nym_kkt::key_utils::produce_key_digests(&mlkem_key.1.encode()),
);
}
if let Some(mceliece_key) = &self.mceliece {
expected_kem_key_digests.insert(
KEM::McEliece,
nym_kkt::key_utils::produce_key_digests(&mceliece_key.1.encode()),
);
}
let mut digests = HashMap::new();
digests.insert(KEM::X25519, hashes);
digests
}
};
LpRemotePeer { LpRemotePeer {
ed25519_public: *self.ed25519.public_key(), ed25519_public: *self.ed25519.public_key(),
x25519_public: *self.x25519.public_key(), x25519_public: *self.x25519.public_key(),
@@ -66,7 +113,7 @@ impl LpLocalPeer {
// this is only exposed in tests as ideally we should be storing the proper types to begin with // this is only exposed in tests as ideally we should be storing the proper types to begin with
#[cfg(test)] #[cfg(test)]
pub fn encapsulate_kem_key(&self) -> Option<nym_kkt::ciphersuite::EncapsulationKey<'_>> { pub fn encapsulate_kem_key(&self) -> Option<nym_kkt::ciphersuite::EncapsulationKey> {
let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes(); let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes();
let libcrux_pk = let libcrux_pk =
libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?; libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?;
@@ -75,6 +122,26 @@ impl LpLocalPeer {
} }
} }
impl Debug for LpLocalPeer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LpLocalPeer")
.field("ed25519", &self.ed25519)
.field("x25519", &self.x25519)
.field("kem_psq", &self.kem_psq)
.field(
"mlkem",
&format!(
"mlkem_public_key: {}",
match &self.mlkem {
Some(keypair) => format!("{:?}", keypair.1.as_ref()),
None => "None".to_string(),
}
),
)
.finish()
}
}
/// Representation of a remote Lewes Protocol peer /// Representation of a remote Lewes Protocol peer
/// encapsulating all the known information and keys. /// encapsulating all the known information and keys.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -124,16 +191,25 @@ pub fn mock_peer() -> LpLocalPeer {
} }
#[cfg(test)] #[cfg(test)]
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer { pub fn random_peer<'a, R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
use nym_kkt::key_utils::{generate_keypair_mceliece, generate_keypair_mlkem};
let ed25519 = Arc::new(ed25519::KeyPair::new(rng)); let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
let x25519 = Arc::new(ed25519.to_x25519()); let x25519 = Arc::new(ed25519.to_x25519());
let kem_psq = Some(x25519.clone()); let kem_psq = Some(x25519.clone());
let mlkem_keypair = generate_keypair_mlkem(&mut rand09::rng());
let mceliece_keypair = generate_keypair_mceliece(&mut rand09::rng());
LpLocalPeer { LpLocalPeer {
ed25519, ed25519,
x25519, x25519,
kem_psq, kem_psq,
mlkem: None,
mceliece: None,
} }
.with_mlkem_keypair(&mlkem_keypair.0, &mlkem_keypair.1)
.with_mceliece_keypair(mceliece_keypair.0, mceliece_keypair.1)
} }
#[cfg(test)] #[cfg(test)]
+57 -36
View File
@@ -48,7 +48,9 @@
use crate::LpError; use crate::LpError;
use libcrux_psq::v1::cred::{Authenticator, Ed25519}; use libcrux_psq::v1::cred::{Authenticator, Ed25519};
use libcrux_psq::v1::impls::MlKem768 as PsqMlKem768;
use libcrux_psq::v1::impls::X25519 as PsqX25519; use libcrux_psq::v1::impls::X25519 as PsqX25519;
use libcrux_psq::v1::impls::XWingKemDraft06 as PsqXwing;
use libcrux_psq::v1::psk_registration::{Initiator, InitiatorMsg, Responder}; use libcrux_psq::v1::psk_registration::{Initiator, InitiatorMsg, Responder};
use libcrux_psq::v1::traits::{Ciphertext as PsqCiphertext, PSQ}; use libcrux_psq::v1::traits::{Ciphertext as PsqCiphertext, PSQ};
use nym_crypto::asymmetric::{ed25519, x25519}; use nym_crypto::asymmetric::{ed25519, x25519};
@@ -290,64 +292,83 @@ pub fn psq_initiator_create_message(
// Step 1: Classical ECDH for baseline security // 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);
// Step 2: PSQ v1 with Ed25519 authentication
// Extract X25519 KEM key from EncapsulationKey
let kem_pk = match remote_kem_public {
EncapsulationKey::X25519(pk) => pk,
_ => {
return Err(LpError::KKTError(
"Only X25519 KEM is currently supported for PSQ".to_string(),
));
}
};
// Convert nym Ed25519 keys to libcrux format // Convert nym Ed25519 keys to libcrux format
type Ed25519VerificationKey = <Ed25519 as Authenticator>::VerificationKey; type Ed25519VerificationKey = <Ed25519 as Authenticator>::VerificationKey;
let ed25519_sk_bytes = client_ed25519_sk.to_bytes(); let ed25519_sk_bytes = client_ed25519_sk.to_bytes();
let ed25519_pk_bytes = client_ed25519_pk.to_bytes(); let ed25519_pk_bytes = client_ed25519_pk.to_bytes();
let ed25519_verification_key = Ed25519VerificationKey::from_bytes(ed25519_pk_bytes); let ed25519_verification_key = Ed25519VerificationKey::from_bytes(ed25519_pk_bytes);
// Use PSQ v1 API with Ed25519 authentication let init_msg_err = |e: libcrux_psq::v1::Error| {
let mut rng = rand09::rng();
let (state, initiator_msg) = Initiator::send_initial_message::<Ed25519, PsqX25519>(
session_context,
Duration::from_secs(3600), // 1 hour expiry
kem_pk,
&ed25519_sk_bytes,
&ed25519_verification_key,
&mut rng,
)
.map_err(|e| {
tracing::error!( tracing::error!(
"PSQ initiator failed - KEM encapsulation or signing error: {:?}", "PSQ initiator failed - KEM encapsulation or signing error: {:?}",
e e
); );
LpError::Internal(format!("PSQ v1 send_initial_message failed: {:?}", e)) LpError::Internal(format!("PSQ v1 send_initial_message failed: {:?}", e))
})?; };
// Extract PSQ shared secret (unregistered PSK) - this is K_pq let serialisation_err =
let psq_psk = state.unregistered_psk(); |e| LpError::Internal(format!("InitiatorMsg serialization failed: {:?}", e));
// pq_shared_secret is the raw K_pq from KEM encapsulation. let ctx = session_context;
// Store it for subsession derivation before it's combined with ECDH. let sk = &ed25519_sk_bytes;
let pq_shared_secret: [u8; 32] = *psq_psk; let vk = &ed25519_verification_key;
let exp = Duration::from_secs(3600); // 1 hour expiry
// Step 2: PSQ v1 with Ed25519 authentication
// Use PSQ v1 API with Ed25519 authentication
let mut rng = rand09::rng();
// psq_psk - this is K_pq
let (pq_shared_secret, payload) = match remote_kem_public {
EncapsulationKey::X25519(kem_pk) => {
let (state, initiator_msg) = Initiator::send_initial_message::<Ed25519, PsqX25519>(
ctx, exp, kem_pk, sk, vk, &mut rng,
)
.map_err(init_msg_err)?;
let msg_bytes = initiator_msg
.tls_serialize_detached()
.map_err(serialisation_err)?;
(*state.unregistered_psk(), msg_bytes)
}
EncapsulationKey::MlKem768(kem_pk) => {
let (state, initiator_msg) = Initiator::send_initial_message::<Ed25519, PsqMlKem768>(
ctx, exp, kem_pk, sk, vk, &mut rng,
)
.map_err(init_msg_err)?;
let msg_bytes = initiator_msg
.tls_serialize_detached()
.map_err(serialisation_err)?;
(*state.unregistered_psk(), msg_bytes)
}
EncapsulationKey::XWing(kem_pk) => {
let (state, initiator_msg) = Initiator::send_initial_message::<Ed25519, PsqXwing>(
ctx, exp, kem_pk, sk, vk, &mut rng,
)
.map_err(init_msg_err)?;
let msg_bytes = initiator_msg
.tls_serialize_detached()
.map_err(serialisation_err)?;
(*state.unregistered_psk(), msg_bytes)
}
EncapsulationKey::McEliece(pk) => {
todo!("unsupported mceliece for PSQ")
}
};
// Step 3: Combine ECDH + PSQ via Blake3 KDF // Step 3: Combine ECDH + PSQ via Blake3 KDF
let mut combined = Vec::with_capacity(64 + psq_psk.len()); let mut combined = Vec::with_capacity(64 + pq_shared_secret.len());
combined.extend_from_slice(&ecdh_secret); combined.extend_from_slice(&ecdh_secret);
combined.extend_from_slice(psq_psk); // psq_psk is already a &[u8; 32] combined.extend_from_slice(&pq_shared_secret); // psq_psk is already a &[u8; 32]
combined.extend_from_slice(salt); combined.extend_from_slice(salt);
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]); let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
// Serialize InitiatorMsg with TLS encoding for transport
let msg_bytes = initiator_msg
.tls_serialize_detached()
.map_err(|e| LpError::Internal(format!("InitiatorMsg serialization failed: {:?}", e)))?;
Ok(PsqInitiatorResult { Ok(PsqInitiatorResult {
psk: final_psk, psk: final_psk,
payload: msg_bytes, payload,
pq_shared_secret, pq_shared_secret,
}) })
} }
+47 -31
View File
@@ -19,7 +19,7 @@ use crate::replay::ReceivingKeyCounterValidator;
use crate::{LpError, LpMessage, LpPacket}; use crate::{LpError, LpMessage, LpPacket};
use nym_crypto::asymmetric::{ed25519, x25519}; use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::KKT_RESPONSE_AAD; use nym_kkt::KKT_RESPONSE_AAD;
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey, KEM};
use nym_kkt::context::KKTContext; use nym_kkt::context::KKTContext;
use nym_kkt::encryption::{ use nym_kkt::encryption::{
KKTSessionSecret, decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame, KKTSessionSecret, decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
@@ -32,6 +32,7 @@ use nym_kkt::session::{
use parking_lot::Mutex; use parking_lot::Mutex;
use rand::RngCore; use rand::RngCore;
use snow::Builder; use snow::Builder;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use zeroize::{Zeroize, ZeroizeOnDrop}; use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -90,7 +91,7 @@ pub enum KKTState {
/// KKT exchange completed (initiator received and validated KEM key). /// KKT exchange completed (initiator received and validated KEM key).
Completed { Completed {
/// Responder's KEM public key for PSQ encapsulation /// Responder's KEM public key for PSQ encapsulation
kem_pk: Box<EncapsulationKey<'static>>, kem_pk: Box<EncapsulationKey>,
}, },
/// Responder processed a KKT request and sent response. /// Responder processed a KKT request and sent response.
@@ -303,6 +304,14 @@ impl LpSession {
self.remote_peer.ed25519_public self.remote_peer.ed25519_public
} }
pub fn local_kem_encapsulation_key(&self, kem: &KEM) -> Option<Arc<EncapsulationKey>> {
match kem {
KEM::MlKem768 => self.local_peer.mlkem.as_ref().map(|x| x.1.clone()),
KEM::McEliece => self.local_peer.mceliece.as_ref().map(|x| x.1.clone()),
_ => None,
}
}
/// Returns the remote X25519 public key. /// Returns the remote X25519 public key.
/// ///
/// Used for tie-breaking in simultaneous subsession initiation. /// Used for tie-breaking in simultaneous subsession initiation.
@@ -538,9 +547,7 @@ impl LpSession {
// TODO: missing Zeroize // TODO: missing Zeroize
/// Convert local KEM PSQ keys to typed EncapsulationKey / EncapsulationKey /// Convert local KEM PSQ keys to typed EncapsulationKey / EncapsulationKey
pub fn encapsulated_kem_keys( pub fn encapsulated_kem_keys(&self) -> Result<(DecapsulationKey, EncapsulationKey), LpError> {
&self,
) -> Result<(DecapsulationKey<'_>, EncapsulationKey<'_>), LpError> {
let kem_keys = self let kem_keys = self
.local_peer .local_peer
.kem_psq .kem_psq
@@ -599,7 +606,7 @@ impl LpSession {
// georgio this needs to be moved one level up (maybe chosen in LPSession) // georgio this needs to be moved one level up (maybe chosen in LPSession)
let ciphersuite = match Ciphersuite::resolve_ciphersuite( let ciphersuite = match Ciphersuite::resolve_ciphersuite(
KEM::X25519, KEM::MlKem768,
HashFunction::Blake3, HashFunction::Blake3,
SignatureScheme::Ed25519, SignatureScheme::Ed25519,
None, None,
@@ -743,11 +750,7 @@ impl LpSession {
/// ///
/// * `Ok(LpMessage::KKTResponse)` - Signed KKT response ready to send /// * `Ok(LpMessage::KKTResponse)` - Signed KKT response ready to send
/// * `Err(LpError)` - Signature verification failed or invalid request /// * `Err(LpError)` - Signature verification failed or invalid request
pub fn process_kkt_request( pub fn process_kkt_request(&self, request_bytes: &[u8]) -> Result<LpMessage, LpError> {
&self,
request_bytes: &[u8],
responder_kem_pk: &EncapsulationKey,
) -> Result<LpMessage, LpError> {
let mut rng = rand09::rng(); let mut rng = rand09::rng();
let mut kkt_state = self.kkt_state.lock(); let mut kkt_state = self.kkt_state.lock();
@@ -758,33 +761,46 @@ impl LpSession {
) { ) {
Ok((session_secret, request_frame, remote_context)) => { Ok((session_secret, request_frame, remote_context)) => {
match responder_ingest_message(&remote_context, None, None, &request_frame) { match responder_ingest_message(&remote_context, None, None, &request_frame) {
Ok((mut context, _)) => match responder_process( Ok((mut context, _)) => {
&mut context, let local_kem_key: Arc<EncapsulationKey> =
request_frame.session_id(), match self.local_kem_encapsulation_key(&context.ciphersuite().kem()) {
self.local_peer.ed25519().private_key(), Some(key) => key.clone(),
responder_kem_pk, None => {
) { return Err(LpError::Internal(format!(
Ok(response_frame) => match encrypt_kkt_frame( "KEM key for algorithm ({:?}) is not available.",
&mut rng, &context.ciphersuite().kem()
&session_secret, )));
&response_frame, }
KKT_RESPONSE_AAD, };
match responder_process(
&mut context,
request_frame.session_id(),
self.local_peer.ed25519().private_key(),
&local_kem_key,
) { ) {
Ok(response_bytes) => response_bytes, Ok(response_frame) => match encrypt_kkt_frame(
&mut rng,
&session_secret,
&response_frame,
KKT_RESPONSE_AAD,
) {
Ok(response_bytes) => response_bytes,
Err(e) => {
return Err(LpError::Internal(format!(
"KKT response encryption failure : {:?}",
e
)));
}
},
Err(e) => { Err(e) => {
return Err(LpError::Internal(format!( return Err(LpError::Internal(format!(
"KKT response encryption failure : {:?}", "KKT response generation failure : {:?}",
e e
))); )));
} }
},
Err(e) => {
return Err(LpError::Internal(format!(
"KKT response generation failure : {:?}",
e
)));
} }
}, }
Err(e) => { Err(e) => {
return Err(LpError::Internal(format!( return Err(LpError::Internal(format!(
"KKT request handling failure: {:?}", "KKT request handling failure: {:?}",
+3 -24
View File
@@ -360,22 +360,7 @@ impl LpStateMachine {
// Packet message is already parsed, match on it directly // Packet message is already parsed, match on it directly
match &packet.message { match &packet.message {
LpMessage::KKTRequest(kkt_request) if !session.is_initiator() => { LpMessage::KKTRequest(kkt_request) if !session.is_initiator() => {
// Responder processes KKT request match session.process_kkt_request(&kkt_request.0) {
// Convert X25519 public key to KEM format for KKT response
use nym_kkt::ciphersuite::EncapsulationKey;
// Get local X25519 public key by deriving from private key
let local_x25519_public = session.local_x25519_public();
// Convert to libcrux KEM public key
match libcrux_kem::PublicKey::decode(
libcrux_kem::Algorithm::X25519,
local_x25519_public.as_bytes(),
) {
Ok(libcrux_public_key) => {
let responder_kem_pk = EncapsulationKey::X25519(libcrux_public_key);
match session.process_kkt_request(&kkt_request.0, &responder_kem_pk) {
Ok(kkt_response_message) => { Ok(kkt_response_message) => {
match session.next_packet(kkt_response_message) { match session.next_packet(kkt_response_message) {
Ok(response_packet) => { Ok(response_packet) => {
@@ -396,14 +381,8 @@ impl LpStateMachine {
LpState::Closed { reason } LpState::Closed { reason }
} }
} }
}
Err(e) => {
let reason = format!("Failed to convert X25519 to KEM: {:?}", e);
let err = LpError::Internal(reason.clone());
result_action = Some(Err(err));
LpState::Closed { reason }
}
}
} }
LpMessage::KKTResponse(kkt_response) if session.is_initiator() => { LpMessage::KKTResponse(kkt_response) if session.is_initiator() => {
match session.process_kkt_response(&kkt_response.0) { match session.process_kkt_response(&kkt_response.0) {