move key generation to peer

This commit is contained in:
Jędrzej Stuczyński
2026-02-13 17:10:21 +00:00
parent ad389c4fba
commit d313e4ef05
11 changed files with 226 additions and 162 deletions
Generated
+1
View File
@@ -8019,6 +8019,7 @@ dependencies = [
"futures",
"nym-bin-common",
"rand_chacha 0.3.1",
"rand_chacha 0.9.0",
"tokio",
"tracing",
]
+1
View File
@@ -326,6 +326,7 @@ quote = "1"
rand = "0.8.5"
rand09 = { package = "rand", version = "=0.9.2" }
rand_chacha = "0.3"
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
rand_core = "0.6.3"
rand_distr = "0.4"
rayon = "1.5.1"
+30
View File
@@ -209,9 +209,11 @@ impl SignatureScheme {
#[strum(serialize_all = "lowercase")]
#[repr(u8)]
pub enum KEM {
#[deprecated]
XWing = 0,
MlKem768 = 1,
McEliece = 2,
#[deprecated]
X25519 = 255,
}
@@ -268,6 +270,34 @@ impl Ciphersuite {
}
}
#[must_use]
pub fn with_kem(mut self, kem: KEM) -> Self {
self.kem = kem;
self.encapsulation_key_length = kem.encapsulation_key_length();
self
}
#[must_use]
pub fn with_signature_scheme(mut self, signature_scheme: SignatureScheme) -> Self {
self.signature_scheme = signature_scheme;
self.signing_key_length = signature_scheme.signing_key_length();
self.verification_key_length = signature_scheme.verification_key_length();
self.signature_length = signature_scheme.signature_length();
self
}
#[must_use]
pub fn with_hash_function(mut self, hash_function: HashFunction) -> Self {
self.hash_function = hash_function;
self
}
#[must_use]
pub fn with_hash_length(mut self, hash_length: HashLength) -> Self {
self.hash_length = hash_length;
self
}
pub fn kem_key_len(&self) -> usize {
self.encapsulation_key_length
}
+5 -12
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
use libcrux_ml_kem::mlkem768::MlKem768KeyPair;
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KeyDigests};
use rand09::{CryptoRng, RngCore};
@@ -12,27 +13,19 @@ where
DHKeyPair::new(rng)
}
pub fn generate_keypair_mlkem<R>(rng: &mut R) -> (MlKem768PrivateKey, MlKem768PublicKey)
pub fn generate_keypair_mlkem<R>(rng: &mut R) -> MlKem768KeyPair
where
R: RngCore + CryptoRng,
{
libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng).into_parts()
libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng)
}
// (decapsulation_key, encapsulation_key)
pub fn generate_keypair_mceliece<R>(
rng: &mut R,
) -> (
libcrux_psq::classic_mceliece::SecretKey,
libcrux_psq::classic_mceliece::PublicKey,
)
pub fn generate_keypair_mceliece<R>(rng: &mut R) -> libcrux_psq::classic_mceliece::KeyPair
where
// this is annoying because mceliece lib uses rand 0.8.5...
R: RngCore + CryptoRng,
{
let kp = libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng);
(kp.sk, kp.pk)
libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng)
}
pub fn hash_key_bytes(
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use libcrux_ml_kem::mlkem768::{MlKem768KeyPair, MlKem768PrivateKey, MlKem768PublicKey};
use libcrux_psq::classic_mceliece;
use nym_kkt_ciphersuite::KEM;
use std::fmt::{Debug, Formatter};
/// Wrapper around keys used for the KEM exchange
pub struct KEMKeys {
mc_eliece: classic_mceliece::KeyPair,
ml_kem768: MlKem768KeyPair,
}
impl Debug for KEMKeys {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KEMKeys")
.field("mc_eliece", &"<redacted>")
.field("ml_kem768", &"<redacted>")
.finish()
}
}
impl KEMKeys {
pub fn new(mc_eliece: classic_mceliece::KeyPair, ml_kem768: MlKem768KeyPair) -> Self {
Self {
mc_eliece,
ml_kem768,
}
}
pub fn encoded_encapsulation_key(&self, kem: KEM) -> Option<&[u8]> {
match kem {
KEM::McEliece => Some(self.mc_eliece.pk.as_ref()),
KEM::MlKem768 => Some(self.ml_kem768.pk()),
_ => None,
}
}
pub fn mc_eliece_encapsulation_key(&self) -> &classic_mceliece::PublicKey {
&self.mc_eliece.pk
}
pub fn ml_kem768_encapsulation_key(&self) -> &MlKem768PublicKey {
self.ml_kem768.public_key()
}
pub fn mc_eliece_decapsulation_key(&self) -> &classic_mceliece::SecretKey {
&self.mc_eliece.sk
}
pub fn ml_kem768_decapsulation_key(&self) -> &MlKem768PrivateKey {
self.ml_kem768.private_key()
}
}
+2
View File
@@ -7,6 +7,7 @@ pub mod error;
pub mod frame;
pub mod initiator;
pub mod key_utils;
pub mod keys;
pub mod masked_byte;
pub mod rekey;
pub mod responder;
@@ -44,6 +45,7 @@ mod test {
// generate kem public keys
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
let r_dir_hash_mlkem = hash_encapsulation_key(
+32 -32
View File
@@ -8,6 +8,7 @@ use crate::{
};
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme};
pub struct KKTResponder<'a> {
x25519_keypair: &'a DHKeyPair,
mlkem_encapsulation_key: Option<&'a libcrux_kem::MlKem768PublicKey>,
@@ -29,41 +30,40 @@ impl<'a> KKTResponder<'a> {
supported_hash_functions.iter().copied().collect();
if hash_functions.is_empty() {
Err(KKTError::FunctionInputError {
return Err(KKTError::FunctionInputError {
info: "Did not provide a supported HashFunction when instaciating a KKTResponder",
});
}
let signature_schemes: HashSet<SignatureScheme> =
supported_signature_schemes.iter().copied().collect();
if signature_schemes.is_empty() {
return Err(KKTError::FunctionInputError {
info: "Did not provide a supported SignatureScheme when instaciating 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 if mlkem_encapsulation_key.is_none() && mceliece_encapsulation_key.is_none() {
Err(KKTError::FunctionInputError {
info: "Did not provide an encapsulation key when instanciating a KKTResponder.",
})
} else {
let signature_schemes: HashSet<SignatureScheme> =
supported_signature_schemes.iter().copied().collect();
if signature_schemes.is_empty() {
Err(KKTError::FunctionInputError {
info: "Did not provide a supported SignatureScheme when instaciating a KKTResponder",
})
} else {
let outer_protocol_versions: HashSet<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 if mlkem_encapsulation_key.is_none() && mceliece_encapsulation_key.is_none()
{
Err(KKTError::FunctionInputError {
info: "Did not provide an encapsulation key when instanciating a KKTResponder.",
})
} else {
Ok(Self {
x25519_keypair,
mlkem_encapsulation_key,
mceliece_encapsulation_key,
supported_hash_functions: hash_functions,
supported_signature_schemes: signature_schemes,
supported_outer_protocol_versions: outer_protocol_versions,
})
}
}
Ok(Self {
x25519_keypair,
mlkem_encapsulation_key,
mceliece_encapsulation_key,
supported_hash_functions: hash_functions,
supported_signature_schemes: signature_schemes,
supported_outer_protocol_versions: outer_protocol_versions,
})
}
}
fn supported_protocol_versions(&self) -> Vec<u8> {
+58 -88
View File
@@ -2,11 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{ClientHelloData, LpError};
use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt_ciphersuite::{
Ciphersuite, HashFunction, HashLength, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests,
use nym_kkt::key_utils::{
generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519,
};
use nym_kkt::keys::KEMKeys;
use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use nym_test_utils::helpers::{deterministic_rng, seeded_rng_09};
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
@@ -24,25 +27,19 @@ pub struct LpLocalPeer {
pub(crate) x25519: Arc<DHKeyPair>,
/// Local KEM keys used for PSQ
// pub(crate) kem_keypairs: HashMap<KEM, Arc<KemKeyPair>>,
pub(crate) kem_keypairs: HashMap<KEM, Arc<()>>,
pub(crate) kem_keypairs: Option<Arc<KEMKeys>>,
}
impl LpLocalPeer {
pub fn new(
ciphersuite: Ciphersuite,
ed25519: Arc<ed25519::KeyPair>,
x25519: Arc<x25519::KeyPair>,
x25519: Arc<DHKeyPair>,
) -> Self {
// TODO: make nicer conversion (without cloning) + error handling
let initiator_libcrux_x25519_private_key =
DHPrivateKey::from_bytes(x25519.private_key().as_bytes()).unwrap();
let initiator_x25519_keypair = DHKeyPair::from(initiator_libcrux_x25519_private_key);
LpLocalPeer {
ciphersuite,
ed25519,
x25519: Arc::new(initiator_x25519_keypair),
x25519,
kem_keypairs: Default::default(),
}
}
@@ -56,11 +53,10 @@ impl LpLocalPeer {
// )
}
pub fn with_kem_keypair(mut self, keypair: Arc<()>) -> Self {
todo!()
// let kem = keypair.kem();
// self.kem_keypairs.insert(kem, keypair);
// self
#[must_use]
pub fn with_kem_keys(mut self, kem_keys: Arc<KEMKeys>) -> Self {
self.kem_keypairs = Some(kem_keys);
self
}
pub fn ed25519(&self) -> &Arc<ed25519::KeyPair> {
@@ -79,34 +75,33 @@ impl LpLocalPeer {
// .ok_or(LpError::ResponderWithMissingKEMKey)
// }
pub fn kem_key(&self, kem: KEM) -> Option<&Arc<()>> {
self.kem_keypairs.get(&kem)
}
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
#[doc(hidden)]
pub fn as_remote(&self) -> LpRemotePeer {
todo!()
// let mut expected_signing_key_digests = HashMap::new();
// expected_signing_key_digests.insert(
// SignatureScheme::Ed25519,
// nym_kkt::key_utils::produce_key_digests(self.ed25519.public_key().as_bytes()),
// );
//
// let mut expected_kem_key_digests = HashMap::new();
// for (kem, kem_key) in &self.kem_keypairs {
// expected_kem_key_digests.insert(
// *kem,
// nym_kkt::key_utils::produce_key_digests(&kem_key.encoded_encapsulation_key()),
// );
// }
//
// LpRemotePeer {
// ed25519_public: *self.ed25519.public_key(),
// x25519_public: self.x25519.pk,
// expected_kem_key_digests,
// expected_signing_key_digests,
// }
let mut expected_signing_key_digests = HashMap::new();
expected_signing_key_digests.insert(
SignatureScheme::Ed25519,
nym_kkt::key_utils::produce_key_digests(self.ed25519.public_key().as_bytes()),
);
let mut expected_kem_key_digests = HashMap::new();
if let Some(keys) = &self.kem_keypairs {
for kem in [KEM::MlKem768, KEM::McEliece] {
expected_kem_key_digests.insert(
kem,
nym_kkt::key_utils::produce_key_digests(
keys.encoded_encapsulation_key(kem).unwrap(),
),
);
}
}
LpRemotePeer {
ed25519_public: *self.ed25519.public_key(),
x25519_public: self.x25519.pk,
expected_kem_key_digests,
expected_signing_key_digests,
}
}
}
@@ -192,63 +187,38 @@ impl LpRemotePeer {
}
#[cfg(any(feature = "mock", test))]
pub fn mock_peer(kem: KEM) -> LpLocalPeer {
pub fn mock_peer() -> LpLocalPeer {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
let ciphersuite = Ciphersuite::new(
kem,
HashFunction::Blake3,
SignatureScheme::Ed25519,
HashLength::Default,
);
random_peer(&mut rng, ciphersuite)
random_peer(&mut rng)
}
#[cfg(any(feature = "mock", test))]
pub fn random_peer<'a, R: rand::CryptoRng + rand::RngCore>(
rng: &mut R,
ciphersuite: Ciphersuite,
) -> LpLocalPeer {
use nym_kkt::key_utils::{generate_keypair_mceliece, generate_keypair_mlkem};
pub fn random_peer<'a, R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
let mut sk = [0u8; 32];
rng.fill_bytes(&mut sk);
// disgusting conversion between rng08 and rng09
let mut seed = [0u8; 32];
rng.fill_bytes(&mut seed);
let TODO = "";
// clamp
sk[0] &= 248u8;
sk[31] &= 127u8;
sk[31] |= 64u8;
let mut rng09 = seeded_rng_09(seed);
let x25519 = Arc::new(DHKeyPair::from(DHPrivateKey::from_bytes(&sk).unwrap()));
let x25519 = Arc::new(generate_keypair_x25519(&mut rng09));
todo!()
// let default_peer = LpLocalPeer {
// ciphersuite: Arc::new(ciphersuite),
// ed25519,
// x25519,
// mlkem: None,
// mceliece: None,
// };
//
// match ciphersuite.kem() {
// KEM::MlKem768 => {
// let mlkem_keypair = generate_keypair_mlkem(&mut rand09::rng());
// default_peer.with_mlkem_keypair(&mlkem_keypair.0, &mlkem_keypair.1)
// }
// KEM::McEliece => {
// let mceliece_keypair = generate_keypair_mceliece(&mut rand09::rng());
// default_peer.with_mceliece_keypair(mceliece_keypair.0, mceliece_keypair.1)
// }
// _ => unreachable!(),
// }
LpLocalPeer {
ciphersuite: Ciphersuite::default(),
ed25519,
x25519,
kem_keypairs: Some(Arc::new(KEMKeys::new(
generate_keypair_mceliece(&mut rng09),
generate_keypair_mlkem(&mut rng09),
))),
}
}
#[cfg(any(feature = "mock", test))]
pub fn mock_peers(kem: KEM) -> (LpLocalPeer, LpLocalPeer) {
println!("KEM: {:?}", kem);
(mock_peer(kem), mock_peer(kem))
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
let mut rng = deterministic_rng();
(random_peer(&mut rng), random_peer(&mut rng))
}
+23 -30
View File
@@ -29,10 +29,10 @@ use rand09::rngs::ThreadRng;
use std::fmt::Debug;
pub(crate) const AAD_INITIATOR_OUTER: &[u8] = b"NYM-PSQ-AAD-INIT-OUTER-V1";
pub(crate) const AAD_INITIATOR_INNER: &[u8] = b"NYM-PSQ-AAD-INIT-INNER-V1";
pub(crate) const AAD_RESPONDER: &[u8] = b"NYM-PSQ-AAD-RESP-V1";
pub(crate) const SESSION_CONTEXT: &[u8] = b"NYM-PSQ-SESSION-CONTEXT-V1";
pub(crate) const AAD_INITIATOR_OUTER: &[u8] = b"NYM-PQ-AAD-INIT-OUTER-V1";
pub(crate) const AAD_INITIATOR_INNER: &[u8] = b"NYM-PQ-AAD-INIT-INNER-V1";
pub(crate) const AAD_RESPONDER: &[u8] = b"NYM-PQ-AAD-RESP-V1";
pub(crate) const SESSION_CONTEXT: &[u8] = b"NYM-PQ-SESSION-CONTEXT-V1";
pub enum PSQState<'a> {
Initiator(RegistrationInitiator<'a, ThreadRng>),
@@ -324,6 +324,7 @@ mod tests {
};
use nym_kkt::responder::KKTResponder;
use nym_kkt_ciphersuite::{HashFunction, HashLength, SignatureScheme};
use nym_test_utils::helpers::deterministic_rng_09;
use nym_test_utils::mocks::async_read_write::MockIOStream;
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
use std::time::Duration;
@@ -490,7 +491,15 @@ mod tests {
// plain test without any wrappers
#[test]
fn e2e_test_plain() {
let mut rng = rand09::rng();
let mut rng = deterministic_rng_09();
let (mut init, resp) = mock_peers();
init.ciphersuite = Ciphersuite::default().with_kem(KEM::MlKem768);
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();
// we should add these as consts
let aad_initiator_outer = b"Test Data I Outer";
@@ -503,29 +512,10 @@ mod tests {
let hash_function = HashFunction::Blake3;
// generate kem public keys
let responder_mlkem_keypair = generate_keypair_mlkem(&mut rng);
let responder_mceliece_keypair = generate_keypair_mceliece(&mut rng);
let r_dir_hash_mlkem = hash_encapsulation_key(
// &ciphersuite.hash_function(),
hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
responder_mlkem_keypair.1.as_slice().as_slice(),
);
let _r_dir_hash_mceliece = hash_encapsulation_key(
// &ciphersuite.hash_function(),
hash_function,
// ciphersuite.hash_len(),
HashLength::Default.value(),
responder_mceliece_keypair.1.as_ref(),
);
let kkt_responder = KKTResponder::new(
&responder_x25519_keypair,
Some(&responder_mlkem_keypair.1),
Some(&responder_mceliece_keypair.1),
Some(resp_keys.ml_kem768_encapsulation_key()),
Some(resp_keys.mc_eliece_encapsulation_key()),
&[
HashFunction::Blake3,
HashFunction::SHA256,
@@ -542,8 +532,8 @@ mod tests {
let responder_ciphersuite = CiphersuiteBuilder::new(psq_ciphersuite)
.longterm_x25519_keys(&responder_x25519_keypair)
.longterm_mlkem_encapsulation_key(&responder_mlkem_keypair.1)
.longterm_mlkem_decapsulation_key(&responder_mlkem_keypair.0)
.longterm_mlkem_encapsulation_key(resp_keys.ml_kem768_encapsulation_key())
.longterm_mlkem_decapsulation_key(resp_keys.ml_kem768_decapsulation_key())
.build_responder_ciphersuite()
.unwrap();
@@ -566,7 +556,7 @@ mod tests {
&mut rng,
&ciphersuite,
&responder_x25519_keypair.pk,
&r_dir_hash_mlkem,
&dir_hash,
1u8,
)
.unwrap();
@@ -577,7 +567,10 @@ mod tests {
assert_eq!(
i_obtained_key,
responder_mlkem_keypair.1.as_slice().as_slice(),
resp_keys
.ml_kem768_encapsulation_key()
.as_slice()
.as_slice(),
);
let mlkem_key =
+1
View File
@@ -15,6 +15,7 @@ description = "Helpers, traits, and mock definitions for tests"
anyhow = { workspace = true }
futures = { workspace = true }
rand_chacha = { workspace = true }
rand_chacha09 = { workspace = true }
tokio = { workspace = true, features = ["sync", "time", "rt"] }
tracing = { workspace = true }
+18
View File
@@ -7,13 +7,19 @@ use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
use nym_bin_common::logging::{default_tracing_fmt_layer, tracing_subscriber};
use rand_chacha::rand_core::SeedableRng;
use rand_chacha09::rand_core::SeedableRng as SeedableRng09;
use std::future::Future;
use tokio::task::JoinHandle;
use tokio::time::error::Elapsed;
// 'current' rand crate
pub use rand_chacha::ChaCha20Rng as DeterministicRng;
pub use rand_chacha::rand_core::{CryptoRng, RngCore};
// rand09 compat
pub use rand_chacha09::ChaChaRng as DeterministicRng09;
pub use rand_chacha09::rand_core::{CryptoRng as CryptoRng09, RngCore as RngCore09};
pub fn leak<T>(val: T) -> &'static mut T {
Box::leak(Box::new(val))
}
@@ -26,6 +32,10 @@ where
tokio::spawn(async move { fut.timeboxed().await })
}
pub fn deterministic_rng_09() -> DeterministicRng09 {
seeded_rng_09([42u8; 32])
}
pub fn deterministic_rng() -> DeterministicRng {
seeded_rng([42u8; 32])
}
@@ -34,10 +44,18 @@ pub fn seeded_rng(seed: [u8; 32]) -> DeterministicRng {
DeterministicRng::from_seed(seed)
}
pub fn seeded_rng_09(seed: [u8; 32]) -> DeterministicRng09 {
DeterministicRng09::from_seed(seed)
}
pub fn u64_seeded_rng(seed: u64) -> DeterministicRng {
DeterministicRng::seed_from_u64(seed)
}
pub fn u64_seeded_rng_09(seed: u64) -> DeterministicRng09 {
DeterministicRng09::seed_from_u64(seed)
}
// test logger to use during debugging
#[allow(clippy::unwrap_used)]
pub fn setup_test_logger() {