Blake3 hkdf + integration

This commit is contained in:
Georgio Nicolas
2026-01-29 02:43:27 +01:00
parent 6256b04cef
commit 5146ca92f5
9 changed files with 72 additions and 67 deletions
Generated
+2
View File
@@ -4137,6 +4137,7 @@ dependencies = [
"nym-credentials-interface",
"nym-crypto",
"nym-gateway",
"nym-kkt-ciphersuite",
"nym-lp-transport",
"nym-registration-client",
"nym-test-utils",
@@ -6224,6 +6225,7 @@ dependencies = [
"nym-sphinx-types",
"nym-test-utils",
"rand 0.8.5",
"rand 0.9.2",
"rand_chacha 0.3.1",
"serde",
"serde_bytes",
+1
View File
@@ -320,6 +320,7 @@ publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand09 = { package = "rand", version = "0.9.2" }
rand_chacha = "0.3"
rand_core = "0.6.3"
rand_distr = "0.4"
+2 -2
View File
@@ -13,14 +13,14 @@ num_enum = { workspace = true }
strum = { workspace = true }
# internal
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
nym-crypto = { path = "../crypto", features = ["asymmetric", "hashing", "serde"] }
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
libcrux-kem = { workspace = true }
libcrux-ecdh = { workspace = true, features = ["codec"] }
libcrux-chacha20poly1305 = { workspace = true }
rand = "0.9.2"
rand09 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
libcrux-psq = { workspace = true, features = ["classic-mceliece"] }
+4 -4
View File
@@ -21,13 +21,13 @@ use nym_kkt::{
responder_ingest_message, responder_process,
},
};
use rand::prelude::*;
use rand09::prelude::*;
pub fn gen_ed25519_keypair(c: &mut Criterion) {
c.bench_function("Generate Ed25519 Keypair", |b| {
b.iter(|| {
let mut s: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut s);
rand09::rng().fill_bytes(&mut s);
ed25519::KeyPair::from_secret(s, 0)
});
});
@@ -36,13 +36,13 @@ pub fn gen_ed25519_keypair(c: &mut Criterion) {
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
c.bench_function("Generate MlKem768 Keypair", |b| {
b.iter(|| {
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
});
});
}
pub fn kkt_benchmark(c: &mut Criterion) {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
+8 -8
View File
@@ -5,8 +5,9 @@ use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::
use blake3::Hasher;
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
use nym_crypto::hkdf::blake3::derive_key_blake3;
use nym_kkt_ciphersuite::x25519;
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
use zeroize::Zeroize;
#[derive(Clone, Copy, Zeroize)]
@@ -49,15 +50,14 @@ impl KKTSessionSecret {
.map_err(|_| KKTError::X25519Error {
info: "Key Derivation Error",
})?;
let mut hasher = Hasher::new();
hasher.update(shared_secret.as_ref());
Ok(Self(derive_key_blake3(
"KKT_KDF",
shared_secret.as_ref(),
&[],
)))
// TODO: zeroize
// shared_secret.zeroize();
Ok(Self(hasher.finalize().as_bytes().to_owned()))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
@@ -191,8 +191,8 @@ mod test {
encryption::{KKTSessionSecret, decrypt, encrypt},
key_utils::generate_keypair_x25519,
};
use rand::{RngCore, SeedableRng, rng};
use rand_chacha::ChaCha20Rng;
use rand09::{RngCore, SeedableRng, rng};
#[test]
fn test_keygen() {
+1 -1
View File
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
pub fn generate_keypair_ed25519<R>(
rng: &mut R,
+2 -12
View File
@@ -8,16 +8,6 @@ pub mod error;
pub mod frame;
pub mod key_utils;
// pub mod kkt;
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
/// We currently support MlKem768 and XWing.
///
/// This protocol is safe if it runs under a trusted secure channel.
///
/// Bandwidth costs:
/// Request (MlKem768): 1216 bytes
/// Response (MlKem768): 1088 bytes
/// Request (XWing): 1248 bytes
/// Response (XWing): 1120 bytes
pub mod rekey;
pub mod session;
@@ -49,7 +39,7 @@ mod test {
#[test]
fn test_kkt_psq_e2e_clear() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// generate ed25519 keys
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
@@ -248,7 +238,7 @@ mod test {
}
#[test]
fn test_kkt_psq_e2e_encrypted() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
// generate ed25519 keys
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
+51 -39
View File
@@ -1,24 +1,39 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Post-Quantum Re-Key Protocol
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
/// We currently support MlKem768 and XWing.
///
/// This protocol is safe if it runs under a trusted secure channel.
///
/// Bandwidth costs:
/// Request (MlKem768): 1216 bytes
/// Response (MlKem768): 1088 bytes
/// Request (XWing): 1248 bytes
/// Response (XWing): 1120 bytes
use libcrux_kem::*;
use nym_crypto::hkdf::blake3::derive_key_blake3;
use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing};
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
use zeroize::Zeroize;
use crate::error::KKTError;
/// Context string to be used with the Blake3 KDF.
const REKEY_CONTEXT: &str = "NYM_REKEY_v1";
const REKEY_CONTEXT: &str = "NYM_PQ_REKEY_v1";
pub struct RekeyInitiator {
algorithm: Algorithm,
decapsulation_key: PrivateKey,
randomness: [u8; 32],
salt: [u8; 32],
}
impl RekeyInitiator {
/// The Initiator generates an ephemeral KEM keypair and 32 bytes of randomness.
/// The Initiator generates an ephemeral KEM keypair and a 32-byte salt.
/// The Initiator keeps the decapsulation key and generates a request message.
/// The request message contains the random 32 bytes and an encoding of the encapsulation key as follows
/// randomness encapsulation_key
/// The request message contains the salt and an encoding of the encapsulation key as follows
/// salt encapsulation_key
/// [0 ........ 32 | 32 .............. ]
///
/// Inputs:
@@ -26,22 +41,15 @@ impl RekeyInitiator {
/// kem: a KEM algorithm (we currently support MlKem768 and XWing)
///
/// Outputs:
/// RekeyInitiator: A struct which contains the decapsulation key, the generated randomness and the kem algorithm in use.
/// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use.
/// Vec<u8>: The request message as explained above. This is to be sent to the responder as-is.
pub fn generate_request<R>(rng: &mut R, kem: KEM) -> Result<(RekeyInitiator, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
// Generate the Initiator's randomness.
let mut randomness = [0u8; 32];
rng.fill_bytes(&mut randomness);
// Create the buffer for the request message and copy the randomness bytes into it.
let mut request_buffer: Vec<u8> = randomness.to_vec();
let algorithm = match kem {
KEM::XWing => Algorithm::XWingKemDraft06,
KEM::MlKem768 => Algorithm::MlKem768,
let (algorithm, buffer_size) = match kem {
KEM::XWing => (Algorithm::XWingKemDraft06, 32 + xwing::PUBLIC_KEY_LENGTH),
KEM::MlKem768 => (Algorithm::MlKem768, 32 + ml_kem768::PUBLIC_KEY_LENGTH),
// We don't support McEliece because the keys are massive.
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
KEM::McEliece => {
@@ -57,6 +65,14 @@ impl RekeyInitiator {
}
};
// Generate the Initiator's salt
let mut salt = [0u8; 32];
rng.fill_bytes(&mut salt);
// Create the buffer for the request message and copy the salt into it.
let mut request_buffer = Vec::with_capacity(buffer_size);
request_buffer.extend_from_slice(&salt);
// Generate the ephemeral KEM keypair based on the algorithm from the function's input.
let (decapsulation_key, encapsulation_key) = key_gen(algorithm, rng)?;
@@ -68,7 +84,7 @@ impl RekeyInitiator {
RekeyInitiator {
algorithm,
decapsulation_key,
randomness,
salt,
},
// This is to be sent to the responder.
request_buffer,
@@ -76,7 +92,7 @@ impl RekeyInitiator {
}
/// The Initiator will attempt to decapsulate the `pre_key` generated by the responder
/// secret. This `pre_key` will be combined with the Initiator's previously generated randomness
/// secret. This `pre_key` will be combined with the Initiator's previously generated salt
/// as input to a Blake3 KDF call to generate the new shared secret.
///
/// This function fails if the ciphertext cannot be decoded or decapsulated.
@@ -92,14 +108,12 @@ impl RekeyInitiator {
let pre_key = ciphertext.decapsulate(&self.decapsulation_key)?;
// Encode the `pre_key` into bytes
let mut to_hash = pre_key.encode();
// Concatenate the Initiator's randomness to those bytes.
to_hash.extend_from_slice(&self.randomness);
// Derive a the new secret key: Blake3_KDF(pre_key || initiator_randomess)
let new_secret: [u8; 32] = blake3::derive_key(REKEY_CONTEXT, &to_hash);
let pre_key_bytes = pre_key.encode();
// Zeroize the Initiator's randomness
self.randomness.zeroize();
let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, &self.salt);
// Zeroize the Initiator's salt
self.salt.zeroize();
// TODO: zeroize the decapsulation key
@@ -108,7 +122,7 @@ impl RekeyInitiator {
}
/// The responder parses the request message.
/// The first 32 bytes are the Initiator's randomness,
/// The first 32 bytes are the Initiator's salt,
/// and the remainder is the encoding of the public key.
/// Given that XWing and MlKem768 have different key lengths,
/// we could deduce the algorithm from that.
@@ -117,12 +131,12 @@ impl RekeyInitiator {
/// this function will produce an error.
///
/// If everything is alright, the responder generates and encapsulates a key `pre_key` to send to the Initiator.
/// Then, the responder calls a Blake3 KDF over `pre_key` and the Initiator's randomness to obtain
/// Then, the responder calls a Blake3 KDF over `pre_key` and the Initiator's salt to obtain
/// the new shared secret.
///
/// Inputs:
/// rng: something that implements CryptoRng + RngCore
/// request_message: the Initiator's request message (contains randomness and an encapsulation key)
/// request_message: the Initiator's request message (contains the salt and encapsulation key)
///
/// Outputs:
/// [u8; 32]: new shared secret
@@ -171,9 +185,9 @@ where
}
};
// Split the message to get the Initiator's randomness (first 32 bytes)
// Split the message to get the Initiator's salt (first 32 bytes)
// and the encoding of the Initiator's public key.
let (remote_randomness, remote_encapsulation_key_bytes) = request_message.split_at_mut(32);
let (remote_salt, remote_encapsulation_key_bytes) = request_message.split_at_mut(32);
// Attempt to decode the Initiator's encapsulation key.
let remote_encapsulation_key = PublicKey::decode(algorithm, remote_encapsulation_key_bytes)?;
@@ -184,14 +198,12 @@ where
let message = ciphertext.encode();
// Encode the `pre_key` into bytes
let mut to_hash = pre_key.encode();
// Concatenate the Initiator's randomness to those bytes.
to_hash.extend_from_slice(remote_randomness);
// Derive a the new secret key: Blake3_KDF(pre_key || initiator_randomness)
let new_secret: [u8; 32] = blake3::derive_key(REKEY_CONTEXT, &to_hash);
let pre_key_bytes = pre_key.encode();
// Zeroize the Initiator's randomness
remote_randomness.zeroize();
let new_secret: [u8; 32] = derive_key_blake3(REKEY_CONTEXT, &pre_key_bytes, &remote_salt);
// Zeroize the Initiator's salt
remote_salt.zeroize();
Ok((new_secret, message))
}
@@ -203,7 +215,7 @@ mod tests {
#[test]
fn rekey_test() {
let mut rng = rand::rng();
let mut rng = rand09::rng();
for kem in [KEM::MlKem768, KEM::XWing] {
let (rekey_state, request_message) =
+1 -1
View File
@@ -1,5 +1,5 @@
use nym_crypto::asymmetric::ed25519::{self, Signature};
use rand::{CryptoRng, RngCore};
use rand09::{CryptoRng, RngCore};
use crate::frame::KKTSessionId;
use crate::{