Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc13fa2c27 | |||
| f35818e75a | |||
| aea48bd30a | |||
| 72a81449be | |||
| d5c4097dff | |||
| 65df76efea | |||
| 6489ed5dd5 | |||
| 66ecccbfe5 | |||
| 854ab369a6 | |||
| e24eff6945 | |||
| 38f2d8eca2 | |||
| 064718a75a | |||
| c385d14342 | |||
| 3ef986d6ce | |||
| 261a36e792 | |||
| d1acd5b591 | |||
| d8ca227a5f | |||
| 41fae0cb03 | |||
| 5146ca92f5 | |||
| 6256b04cef | |||
| 511e8a4649 | |||
| defbdc8a40 | |||
| 1d4977e536 | |||
| a01bb3d9bd | |||
| dc98a2aed0 | |||
| 06a6df1365 | |||
| 31df6b55b0 | |||
| cef9534dce | |||
| 0629e37c1b | |||
| ac9166a401 | |||
| 8e1a776b7d | |||
| 9a88018cf2 | |||
| fe45b856b7 | |||
| 5c50b760a8 | |||
| aafa99ea47 | |||
| 4767a719f7 | |||
| 89d167de08 | |||
| 05d5f4ae83 | |||
| 2109beeef6 | |||
| d1a5342625 | |||
| f0645bad57 | |||
| 60f8fe09a7 |
Generated
+527
-545
File diff suppressed because it is too large
Load Diff
+16
-1
@@ -173,8 +173,9 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
# "nym-gateway-probe",
|
||||
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
|
||||
"common/nym-lp-sandbox"
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -320,6 +321,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"
|
||||
@@ -390,6 +392,18 @@ zeroize = "1.7.0"
|
||||
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
|
||||
# libcrux
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
|
||||
|
||||
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.1", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" }
|
||||
@@ -540,6 +554,7 @@ wasm-bindgen-test = "0.3.49"
|
||||
wasmtimer = "0.4.1"
|
||||
web-sys = "0.3.76"
|
||||
|
||||
|
||||
# for local development:
|
||||
#[patch.crates-io]
|
||||
#sphinx-packet = { path = "../sphinx" }
|
||||
|
||||
@@ -25,6 +25,7 @@ cipher = { workspace = true, optional = true }
|
||||
x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true }
|
||||
ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
rand09 = { workspace = true }
|
||||
serde_bytes = { workspace = true, optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
|
||||
@@ -109,3 +109,152 @@ impl DerivationMaterial {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod blake3 {
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
use blake3::Hasher;
|
||||
|
||||
use rand09::{RngCore, rng};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub fn derive_key_blake3_multi_input(
|
||||
info: &str,
|
||||
input_key_material: &[&[u8]],
|
||||
salt: &[u8],
|
||||
) -> [u8; 32] {
|
||||
let mut hasher = Hasher::new_derive_key(info);
|
||||
|
||||
for input_key in input_key_material {
|
||||
hasher.update(input_key);
|
||||
}
|
||||
|
||||
hasher.update(salt);
|
||||
|
||||
hasher.finalize().as_bytes().to_owned()
|
||||
}
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(info: &str, input_key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
derive_key_blake3_multi_input(info, &[input_key_material], salt)
|
||||
}
|
||||
|
||||
pub fn derive_fresh_key_blake3_multi_input(
|
||||
info: &str,
|
||||
input_key_material: &[&[u8]],
|
||||
) -> [u8; 32] {
|
||||
let mut salt = [0u8; 32];
|
||||
rng().fill_bytes(&mut salt);
|
||||
|
||||
let derived_key = derive_key_blake3_multi_input(info, input_key_material, &salt);
|
||||
|
||||
// Zeroize salt
|
||||
salt.zeroize();
|
||||
|
||||
derived_key
|
||||
}
|
||||
|
||||
/// Derives a fresh 32-byte key using Blake3's key derivation mode.
|
||||
/// The function calls a random number generator to generate a fresh salt.
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `info` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `input_key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_fresh_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes());
|
||||
/// ```
|
||||
pub fn derive_fresh_key_blake3(info: &str, input_key_material: &[u8]) -> [u8; 32] {
|
||||
derive_fresh_key_blake3_multi_input(info, &[input_key_material])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (timestamp + nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
// Concatenate key_material and salt as input
|
||||
let input = [key_material, salt].concat();
|
||||
|
||||
// Use Blake3's derive_key with context for domain separation
|
||||
// blake3::derive_key returns [u8; 32] directly
|
||||
blake3::derive_key(context, &input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ pub mod crypto_hash;
|
||||
pub mod hkdf;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod hmac;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod kdf;
|
||||
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
|
||||
pub mod shared_key;
|
||||
pub mod symmetric;
|
||||
|
||||
@@ -17,7 +17,7 @@ strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
|
||||
blake3 = { workspace = true, optional = true }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true }
|
||||
libcrux-sha3 = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
digests = ["blake3", "libcrux-sha3"]
|
||||
|
||||
@@ -226,6 +226,17 @@ impl KEM {
|
||||
}
|
||||
}
|
||||
|
||||
// pub const fn map_kem_to_libcrux_kem(kem: &KEM) -> Result<Algorithm, KKTError> {
|
||||
// match kem {
|
||||
// KEM::MlKem768 => Ok(Algorithm::MlKem768),
|
||||
// KEM::XWing => Ok(Algorithm::XWingKemDraft06),
|
||||
// KEM::X25519 => Ok(Algorithm::X25519),
|
||||
// KEM::McEliece => Err(KKTError::KEMMapping {
|
||||
// info: "attempted to map McEliece KEM to libcrux_kem",
|
||||
// }),
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub struct Ciphersuite {
|
||||
hash_function: HashFunction,
|
||||
@@ -273,16 +284,16 @@ impl Ciphersuite {
|
||||
self.verification_key_length
|
||||
}
|
||||
|
||||
pub fn hash_function(&self) -> HashFunction {
|
||||
self.hash_function
|
||||
pub fn hash_function(&self) -> &HashFunction {
|
||||
&self.hash_function
|
||||
}
|
||||
|
||||
pub fn kem(&self) -> KEM {
|
||||
self.kem
|
||||
pub fn kem(&self) -> &KEM {
|
||||
&self.kem
|
||||
}
|
||||
|
||||
pub fn signature_scheme(&self) -> SignatureScheme {
|
||||
self.signature_scheme
|
||||
pub fn signature_scheme(&self) -> &SignatureScheme {
|
||||
&self.signature_scheme
|
||||
}
|
||||
|
||||
pub fn hash_len(&self) -> usize {
|
||||
@@ -351,6 +362,17 @@ impl Display for Ciphersuite {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Ciphersuite {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
KEM::MlKem768,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -7,24 +7,23 @@ license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
blake3 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
|
||||
nym-crypto = { path = "../crypto", features = ["hashing"] }
|
||||
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
|
||||
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||
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"] }
|
||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
||||
|
||||
libcrux-psq = { workspace = true, features = ["classic-mceliece"] }
|
||||
libcrux-ml-kem = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.9.0"
|
||||
|
||||
@@ -7,49 +7,37 @@
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_kkt::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM, SignatureScheme},
|
||||
context::KKTMode,
|
||||
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::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
initiator_ingest_response, initiator_process, responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
use rand::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);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
use rand09::prelude::*;
|
||||
|
||||
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];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_responder);
|
||||
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
@@ -69,8 +57,8 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
|
||||
EncapsulationKey::MlKem768(generate_keypair_mlkem(&mut rng).1),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
@@ -107,12 +95,14 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
|
||||
b.iter(|| {
|
||||
initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
@@ -137,14 +127,12 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
"{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame",
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap()
|
||||
});
|
||||
b.iter(|| responder_ingest_message(&r_context, None, &i_frame_r).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
responder_ingest_message(&r_context, None, &i_frame_r).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
@@ -155,7 +143,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -165,7 +152,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -187,7 +173,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -199,7 +184,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -208,27 +192,14 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut i_context, i_frame) =
|
||||
initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Generate Request",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
initiator_process(&mut rng, KKTMode::OneWay, ciphersuite, None).unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -250,25 +221,12 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
c.bench_function(
|
||||
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
b.iter(|| responder_ingest_message(&r_context, None, &i_frame_r).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut r_context, r_obtained_key) =
|
||||
responder_ingest_message(&r_context, None, &i_frame_r).unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
@@ -281,7 +239,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -292,7 +249,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -314,7 +270,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -326,7 +281,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -344,7 +298,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap()
|
||||
@@ -356,7 +309,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -383,24 +335,14 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
responder_ingest_message(&r_context, Some(&i_dir_hash), &i_frame_r)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut r_context, r_obtained_key) =
|
||||
responder_ingest_message(&r_context, Some(&i_dir_hash), &i_frame_r).unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
@@ -413,7 +355,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -424,7 +365,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -448,7 +388,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -460,7 +399,6 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
&mut i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -471,10 +409,5 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
gen_ed25519_keypair,
|
||||
gen_mlkem768_keypair,
|
||||
kkt_benchmark
|
||||
);
|
||||
criterion_group!(benches, gen_mlkem768_keypair, kkt_benchmark);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
use libcrux_chacha20poly1305::TAG_LEN;
|
||||
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
|
||||
use nym_crypto::hkdf::blake3::derive_key_blake3;
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
// This is arbitrary
|
||||
pub const MAX_PAYLOAD_LEN: usize = 1_000_000;
|
||||
const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_RX";
|
||||
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_TX";
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Carrier {
|
||||
tx_key: [u8; 32],
|
||||
rx_key: [u8; 32],
|
||||
tx_counter: u64,
|
||||
rx_counter: u64,
|
||||
}
|
||||
|
||||
pub enum CarrierRole {
|
||||
Initiator,
|
||||
Responder,
|
||||
}
|
||||
|
||||
fn increment_nonce(nonce: &mut u64) -> Result<(), KKTError> {
|
||||
match nonce.checked_add(1) {
|
||||
Some(incremented_nonce) => {
|
||||
*nonce = incremented_nonce;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(KKTError::AEADError {
|
||||
info: "Nonce maxed out.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_nonce_bytes(nonce: u64) -> [u8; 12] {
|
||||
let mut bytes = [0u8; 12];
|
||||
let nonce_bytes = nonce.to_le_bytes();
|
||||
bytes[4..].clone_from_slice(&nonce_bytes);
|
||||
bytes
|
||||
}
|
||||
|
||||
impl Carrier {
|
||||
fn init(tx_key: [u8; 32], rx_key: [u8; 32]) -> Self {
|
||||
Self {
|
||||
tx_key,
|
||||
rx_key,
|
||||
tx_counter: 1,
|
||||
rx_counter: 1,
|
||||
}
|
||||
}
|
||||
pub fn new<R>(
|
||||
rng: &mut R,
|
||||
remote_public_key: &DHPublicKey,
|
||||
context: &[u8],
|
||||
) -> Result<(Self, DHPublicKey), KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let ephemeral_keypair = DHKeyPair::new(rng);
|
||||
let shared_secret = ephemeral_keypair
|
||||
.sk()
|
||||
.diffie_hellman(remote_public_key)
|
||||
.map_err(|_| KKTError::X25519Error {
|
||||
info: "Key Derivation Error",
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
Self::from_secret_slice(shared_secret.as_ref(), context),
|
||||
ephemeral_keypair.pk,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn from_secret_slice(secret: &[u8], context: &[u8]) -> Self {
|
||||
let tx_key = derive_key_blake3(CARRIER_KDF_INFO_TX, secret, context);
|
||||
let rx_key = derive_key_blake3(CARRIER_KDF_INFO_RX, secret, context);
|
||||
Self::init(tx_key, rx_key)
|
||||
}
|
||||
|
||||
pub fn from_secret(mut secret: [u8; 32], context: &[u8]) -> Self {
|
||||
let tx_key = derive_key_blake3(CARRIER_KDF_INFO_TX, secret.as_ref(), context);
|
||||
let rx_key = derive_key_blake3(CARRIER_KDF_INFO_RX, secret.as_ref(), context);
|
||||
secret.zeroize();
|
||||
Self::init(tx_key, rx_key)
|
||||
}
|
||||
|
||||
pub(crate) fn flip_keys(self) -> Self {
|
||||
Self {
|
||||
tx_key: self.rx_key,
|
||||
rx_key: self.tx_key,
|
||||
tx_counter: self.rx_counter,
|
||||
rx_counter: self.tx_counter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, KKTError> {
|
||||
if plaintext.len() > MAX_PAYLOAD_LEN {
|
||||
return Err(KKTError::AEADError {
|
||||
info: "Plaintext too large",
|
||||
});
|
||||
}
|
||||
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
|
||||
libcrux_chacha20poly1305::encrypt(
|
||||
&self.tx_key,
|
||||
plaintext,
|
||||
&mut output_buffer,
|
||||
b"kkt-carrier-v1",
|
||||
&as_nonce_bytes(self.tx_counter),
|
||||
)?;
|
||||
|
||||
increment_nonce(&mut self.tx_counter)?;
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, KKTError> {
|
||||
if ciphertext.len() > MAX_PAYLOAD_LEN + TAG_LEN {
|
||||
return Err(KKTError::AEADError {
|
||||
info: "Ciphertext too large",
|
||||
});
|
||||
}
|
||||
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
|
||||
libcrux_chacha20poly1305::decrypt(
|
||||
&self.rx_key,
|
||||
&mut output_buffer,
|
||||
ciphertext,
|
||||
b"kkt-carrier-v1",
|
||||
&as_nonce_bytes(self.rx_counter),
|
||||
)?;
|
||||
|
||||
increment_nonce(&mut self.rx_counter)?;
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{carrier::Carrier, key_utils::generate_keypair_x25519};
|
||||
use rand09::RngCore;
|
||||
|
||||
#[test]
|
||||
fn test_e2e() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate responder x25519 keys
|
||||
let r_x25519 = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let mut context: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut context);
|
||||
|
||||
let ephemeral_keypair = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let i_shared_secret = ephemeral_keypair.sk().diffie_hellman(&r_x25519.pk).unwrap();
|
||||
|
||||
let r_shared_secret = r_x25519.sk().diffie_hellman(&ephemeral_keypair.pk).unwrap();
|
||||
|
||||
let mut i_carrier = Carrier::from_secret_slice(i_shared_secret.as_ref(), &context);
|
||||
let mut r_carrier =
|
||||
Carrier::from_secret_slice(r_shared_secret.as_ref(), &context).flip_keys();
|
||||
|
||||
let test1 = b"test1: i>r #1";
|
||||
let ct1 = i_carrier.encrypt(test1).unwrap();
|
||||
let pt1 = r_carrier.decrypt(&ct1).unwrap();
|
||||
assert_eq!(pt1, test1);
|
||||
|
||||
let test2 = b"test2: r>i #1";
|
||||
let ct2 = i_carrier.encrypt(test2).unwrap();
|
||||
let pt2 = r_carrier.decrypt(&ct2).unwrap();
|
||||
assert_eq!(pt2, test2);
|
||||
let test3 = b"test3: i>r #2";
|
||||
|
||||
let ct3 = i_carrier.encrypt(test3).unwrap();
|
||||
let pt3 = r_carrier.decrypt(&ct3).unwrap();
|
||||
assert_eq!(pt3, test3);
|
||||
|
||||
let test4 = b"test4: i>r #3";
|
||||
let ct4 = i_carrier.encrypt(test4).unwrap();
|
||||
let pt4 = r_carrier.decrypt(&ct4).unwrap();
|
||||
assert_eq!(pt4, test4);
|
||||
|
||||
let test5 = b"test5: r>i #2";
|
||||
let ct5 = i_carrier.encrypt(test5).unwrap();
|
||||
let pt5 = r_carrier.decrypt(&ct5).unwrap();
|
||||
assert_eq!(pt5, test5);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::KKTError;
|
||||
use libcrux_kem::Algorithm;
|
||||
|
||||
pub use nym_kkt_ciphersuite::*;
|
||||
|
||||
pub enum EncapsulationKey<'a> {
|
||||
MlKem768(libcrux_kem::PublicKey),
|
||||
XWing(libcrux_kem::PublicKey),
|
||||
X25519(libcrux_kem::PublicKey),
|
||||
McEliece(classic_mceliece_rust::PublicKey<'a>),
|
||||
}
|
||||
|
||||
pub enum DecapsulationKey<'a> {
|
||||
MlKem768(libcrux_kem::PrivateKey),
|
||||
XWing(libcrux_kem::PrivateKey),
|
||||
X25519(libcrux_kem::PrivateKey),
|
||||
McEliece(classic_mceliece_rust::SecretKey<'a>),
|
||||
}
|
||||
impl<'a> EncapsulationKey<'a> {
|
||||
pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> {
|
||||
match kem {
|
||||
KEM::McEliece => {
|
||||
if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES {
|
||||
Err(KKTError::KEMError {
|
||||
info: "Received McEliece Encapsulation Key with Invalid Length",
|
||||
})
|
||||
} else {
|
||||
let mut public_key_bytes =
|
||||
Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]);
|
||||
// Size must be correct due to KKTFrame::from_bytes(message_bytes)?
|
||||
public_key_bytes.clone_from_slice(bytes);
|
||||
Ok(EncapsulationKey::McEliece(
|
||||
classic_mceliece_rust::PublicKey::from(public_key_bytes),
|
||||
))
|
||||
}
|
||||
}
|
||||
KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem)?,
|
||||
bytes,
|
||||
)?)),
|
||||
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem)?,
|
||||
bytes,
|
||||
)?)),
|
||||
KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem)?,
|
||||
bytes,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
EncapsulationKey::XWing(public_key)
|
||||
| EncapsulationKey::MlKem768(public_key)
|
||||
| EncapsulationKey::X25519(public_key) => public_key.encode(),
|
||||
EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Result<Algorithm, KKTError> {
|
||||
match kem {
|
||||
KEM::MlKem768 => Ok(Algorithm::MlKem768),
|
||||
KEM::XWing => Ok(Algorithm::XWingKemDraft06),
|
||||
KEM::X25519 => Ok(Algorithm::X25519),
|
||||
KEM::McEliece => Err(KKTError::KEMMapping {
|
||||
info: "attempted to map McEliece KEM to libcrux_kem",
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::ciphersuite::CIPHERSUITE_ENCODING_LEN;
|
||||
use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN};
|
||||
use crate::{KKT_VERSION, error::KKTError};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_kkt_ciphersuite::{CIPHERSUITE_ENCODING_LEN, Ciphersuite};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN;
|
||||
@@ -15,11 +15,11 @@ pub enum KKTStatus {
|
||||
Ok = 0b0000_0000,
|
||||
InvalidRequestFormat = 0b0010_0000,
|
||||
InvalidResponseFormat = 0b0100_0000,
|
||||
InvalidSignature = 0b0110_0000,
|
||||
UnsupportedCiphersuite = 0b1000_0000,
|
||||
UnsupportedKKTVersion = 0b1010_0000,
|
||||
InvalidKey = 0b1100_0000,
|
||||
Timeout = 0b1110_0000,
|
||||
UnsupportedCiphersuite = 0b0110_0000,
|
||||
UnsupportedKKTVersion = 0b1000_0000,
|
||||
InvalidKey = 0b1010_0000,
|
||||
Timeout = 0b1100_0000,
|
||||
UnverifiedKEMKey = 0b1110_0000,
|
||||
}
|
||||
|
||||
impl Display for KKTStatus {
|
||||
@@ -28,10 +28,10 @@ impl Display for KKTStatus {
|
||||
KKTStatus::Ok => "Ok",
|
||||
KKTStatus::InvalidRequestFormat => "Invalid Request Format",
|
||||
KKTStatus::InvalidResponseFormat => "Invalid Response Format",
|
||||
KKTStatus::InvalidSignature => "Invalid Signature",
|
||||
KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite",
|
||||
KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version",
|
||||
KKTStatus::InvalidKey => "Invalid Key",
|
||||
KKTStatus::UnverifiedKEMKey => "Could not verify received encapsulation key",
|
||||
KKTStatus::Timeout => "Timeout",
|
||||
})
|
||||
}
|
||||
@@ -43,7 +43,6 @@ impl Display for KKTStatus {
|
||||
pub enum KKTRole {
|
||||
Initiator = 0b0000_0000,
|
||||
Responder = 0b0000_0001,
|
||||
AnonymousInitiator = 0b0000_0010,
|
||||
}
|
||||
|
||||
// bitmask used: 0b0001_1100
|
||||
@@ -64,20 +63,15 @@ pub struct KKTContext {
|
||||
ciphersuite: Ciphersuite,
|
||||
}
|
||||
impl KKTContext {
|
||||
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result<Self, KKTError> {
|
||||
if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Anonymous Initiator can only use OneWay mode",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: &Ciphersuite) -> Self {
|
||||
Self {
|
||||
version: KKT_VERSION,
|
||||
message_sequence: 0,
|
||||
status: KKTStatus::Ok,
|
||||
mode,
|
||||
role,
|
||||
ciphersuite,
|
||||
})
|
||||
ciphersuite: *ciphersuite,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_responder_header(&self) -> Result<Self, KKTError> {
|
||||
@@ -107,8 +101,8 @@ impl KKTContext {
|
||||
pub fn status(&self) -> KKTStatus {
|
||||
self.status
|
||||
}
|
||||
pub fn ciphersuite(&self) -> Ciphersuite {
|
||||
self.ciphersuite
|
||||
pub fn ciphersuite(&self) -> &Ciphersuite {
|
||||
&self.ciphersuite
|
||||
}
|
||||
pub fn role(&self) -> KKTRole {
|
||||
self.role
|
||||
@@ -118,9 +112,10 @@ impl KKTContext {
|
||||
}
|
||||
|
||||
pub fn body_len(&self) -> usize {
|
||||
if self.status != KKTStatus::Ok
|
||||
|| (self.mode == KKTMode::OneWay
|
||||
&& (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator))
|
||||
if (self.status != KKTStatus::Ok && self.status != KKTStatus::UnverifiedKEMKey)
|
||||
||
|
||||
// no payload
|
||||
(self.mode == KKTMode::OneWay && self.role == KKTRole::Initiator)
|
||||
{
|
||||
0
|
||||
} else {
|
||||
@@ -128,31 +123,12 @@ impl KKTContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_len(&self) -> usize {
|
||||
match self.role {
|
||||
KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(),
|
||||
KKTRole::AnonymousInitiator => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn header_len(&self) -> usize {
|
||||
KKT_CONTEXT_LEN
|
||||
}
|
||||
|
||||
pub const fn session_id_len(&self) -> usize {
|
||||
// note: if anyone decides to update this function and changes the constant value,
|
||||
// you will have to adjust encoding/decoding functions
|
||||
|
||||
// match self.role {
|
||||
// KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH,
|
||||
// It doesn't make sense to send a session_id if we send messages in the clear
|
||||
// KKTRole::AnonymousInitiator => 0,
|
||||
// }
|
||||
KKT_SESSION_ID_LEN
|
||||
}
|
||||
|
||||
pub fn full_message_len(&self) -> usize {
|
||||
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len()
|
||||
self.body_len() + self.header_len()
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> {
|
||||
@@ -228,9 +204,8 @@ mod tests {
|
||||
let valid_context = KKTContext::new(
|
||||
KKTRole::Initiator,
|
||||
KKTMode::Mutual,
|
||||
Ciphersuite::decode([255, 1, 0, 0]).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
&Ciphersuite::decode([255, 1, 0, 0]).unwrap(),
|
||||
);
|
||||
let encoded = valid_context.encode().unwrap();
|
||||
let decoded = KKTContext::try_decode(encoded).unwrap();
|
||||
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::KKTFrame};
|
||||
use blake3::Hasher;
|
||||
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Copy, Zeroize)]
|
||||
pub struct KKTSessionSecret([u8; 32]);
|
||||
|
||||
impl KKTSessionSecret {
|
||||
pub fn new<R>(rng: &mut R, remote_public_key: &x25519::PublicKey) -> (Self, x25519::PublicKey)
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut private_key_bytes = [0u8; x25519::PRIVATE_KEY_SIZE];
|
||||
rng.fill_bytes(&mut private_key_bytes);
|
||||
|
||||
let ephemeral_private_key = x25519::PrivateKey::from_secret(private_key_bytes);
|
||||
let ephemeral_public_key = x25519::PublicKey::from(&ephemeral_private_key);
|
||||
|
||||
(
|
||||
Self::derive(&ephemeral_private_key, remote_public_key),
|
||||
ephemeral_public_key,
|
||||
)
|
||||
}
|
||||
pub fn from_bytes(secret: [u8; 32]) -> Self {
|
||||
Self(secret)
|
||||
}
|
||||
|
||||
fn try_derive(private_key: &x25519::PrivateKey, public_key: &[u8]) -> Result<Self, KKTError> {
|
||||
let mut pub_key: [u8; 32] = [0u8; 32];
|
||||
pub_key.copy_from_slice(&public_key[0..x25519::PUBLIC_KEY_SIZE]);
|
||||
|
||||
// Todo: check validity of pk...
|
||||
let pk = x25519::PublicKey::from(pub_key);
|
||||
Ok(Self::derive(private_key, &pk))
|
||||
}
|
||||
|
||||
pub fn derive(private_key: &x25519::PrivateKey, public_key: &x25519::PublicKey) -> Self {
|
||||
let mut shared_secret = private_key.diffie_hellman(public_key);
|
||||
|
||||
let mut hasher = Hasher::new();
|
||||
|
||||
hasher.update(&shared_secret);
|
||||
shared_secret.zeroize();
|
||||
|
||||
Self(hasher.finalize().as_bytes().to_owned())
|
||||
}
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_initial_kkt_frame<R>(
|
||||
rng: &mut R,
|
||||
remote_public_key: &x25519::PublicKey,
|
||||
kkt_frame: &KKTFrame,
|
||||
) -> Result<(KKTSessionSecret, Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(rng, remote_public_key);
|
||||
|
||||
let mut encrypted_frame =
|
||||
encrypt_kkt_frame(rng, &session_secret_key, kkt_frame, KKT_INITIAL_FRAME_AAD)?;
|
||||
|
||||
let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + x25519::PUBLIC_KEY_SIZE);
|
||||
output_buffer.extend_from_slice(ephemeral_public_key.as_bytes());
|
||||
output_buffer.append(&mut encrypted_frame);
|
||||
|
||||
// [ 32 | 12 | ciphertext | 16];
|
||||
// [eph_pub_key | nonce | ciphertext | tag];
|
||||
Ok((session_secret_key, output_buffer))
|
||||
}
|
||||
|
||||
pub fn decrypt_initial_kkt_frame(
|
||||
responder_private_key: &x25519::PrivateKey,
|
||||
encrypted_frame_bytes: &[u8],
|
||||
) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> {
|
||||
if encrypted_frame_bytes.len() < x25519::PUBLIC_KEY_SIZE + TAG_LEN + NONCE_LEN {
|
||||
Err(KKTError::AEADError {
|
||||
info: "Encrypted KKT Frame is too short.",
|
||||
})
|
||||
} else {
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
responder_private_key,
|
||||
&encrypted_frame_bytes[0..x25519::PUBLIC_KEY_SIZE],
|
||||
)?;
|
||||
|
||||
let (kkt_frame, kkt_context) = decrypt_kkt_frame(
|
||||
&shared_secret,
|
||||
&encrypted_frame_bytes[x25519::PUBLIC_KEY_SIZE..],
|
||||
KKT_INITIAL_FRAME_AAD,
|
||||
)?;
|
||||
Ok((shared_secret, kkt_frame, kkt_context))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_kkt_frame<R>(
|
||||
rng: &mut R,
|
||||
secret_key: &KKTSessionSecret,
|
||||
kkt_frame: &KKTFrame,
|
||||
aad: &[u8],
|
||||
) -> Result<Vec<u8>, KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let kkt_frame_bytes = kkt_frame.to_bytes();
|
||||
|
||||
// generate nonce
|
||||
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut ciphertext = encrypt(secret_key.as_bytes(), &kkt_frame_bytes, aad, &nonce)?;
|
||||
|
||||
// [ 12 | ciphertext | 16];
|
||||
// [nonce | ciphertext | tag];
|
||||
let mut output_buffer: Vec<u8> =
|
||||
Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN);
|
||||
|
||||
output_buffer.extend_from_slice(&nonce);
|
||||
output_buffer.append(&mut ciphertext);
|
||||
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
// kkt_frame_bytes should look like this
|
||||
// [ 12 | ciphertext | 16];
|
||||
// [nonce | ciphertext | tag];
|
||||
pub fn decrypt_kkt_frame(
|
||||
secret_key: &KKTSessionSecret,
|
||||
kkt_frame_bytes: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<(KKTFrame, KKTContext), KKTError> {
|
||||
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
|
||||
nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]);
|
||||
|
||||
let plaintext = decrypt(
|
||||
secret_key.as_bytes(),
|
||||
&kkt_frame_bytes[NONCE_LEN..],
|
||||
aad,
|
||||
&nonce,
|
||||
)?;
|
||||
|
||||
KKTFrame::from_bytes(&plaintext)
|
||||
}
|
||||
|
||||
fn encrypt(
|
||||
secret_key: &[u8; 32],
|
||||
plaintext: &[u8],
|
||||
aad: &[u8],
|
||||
nonce: &[u8; NONCE_LEN],
|
||||
) -> Result<Vec<u8>, KKTError> {
|
||||
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
|
||||
libcrux_chacha20poly1305::encrypt(secret_key, plaintext, &mut output_buffer, aad, nonce)?;
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
fn decrypt(
|
||||
secret_key: &[u8; 32],
|
||||
ciphertext: &[u8],
|
||||
aad: &[u8],
|
||||
nonce: &[u8; NONCE_LEN],
|
||||
) -> Result<Vec<u8>, KKTError> {
|
||||
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
|
||||
libcrux_chacha20poly1305::decrypt(secret_key, &mut output_buffer, ciphertext, aad, nonce)?;
|
||||
Ok(output_buffer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::ciphersuite::Ciphersuite;
|
||||
use crate::context::{KKTContext, KKTMode, KKTRole};
|
||||
use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame};
|
||||
use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame};
|
||||
use crate::{
|
||||
ciphersuite::DEFAULT_HASH_LEN,
|
||||
encryption::{KKTSessionSecret, decrypt, encrypt},
|
||||
key_utils::generate_keypair_x25519,
|
||||
};
|
||||
use rand::{RngCore, SeedableRng, rng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
#[test]
|
||||
fn test_keygen() {
|
||||
let mut rng = rng();
|
||||
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
|
||||
|
||||
let (session_secret_key, ephemeral_public_key) =
|
||||
KKTSessionSecret::new(&mut rng, responder_x25519_keypair.public_key());
|
||||
|
||||
let shared_secret = KKTSessionSecret::try_derive(
|
||||
responder_x25519_keypair.private_key(),
|
||||
ephemeral_public_key.as_bytes().as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption() {
|
||||
let mut rng = rng();
|
||||
|
||||
let mut secret_key = [0u8; DEFAULT_HASH_LEN];
|
||||
rng.fill_bytes(&mut secret_key);
|
||||
|
||||
let mut plaintext = vec![0; 100];
|
||||
rng.fill_bytes(&mut plaintext);
|
||||
|
||||
let mut nonce = [0; 12];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut aad = vec![0; 124];
|
||||
rng.fill_bytes(&mut aad);
|
||||
|
||||
let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap();
|
||||
|
||||
let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap();
|
||||
|
||||
assert_eq!(o_plaintext, plaintext)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
||||
let mut rng = ChaCha20Rng::seed_from_u64(42);
|
||||
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
||||
let aad = b"my-amazing-aad";
|
||||
|
||||
let valid_context = KKTContext::new(
|
||||
KKTRole::Initiator,
|
||||
KKTMode::Mutual,
|
||||
Ciphersuite::decode([255, 1, 0, 0])?,
|
||||
)?;
|
||||
let dummy_frame = KKTFrame::new(
|
||||
valid_context.encode()?,
|
||||
&[2u8; 32],
|
||||
[3u8; KKT_SESSION_ID_LEN],
|
||||
&[4u8; 64],
|
||||
);
|
||||
|
||||
let ciphertext = encrypt_kkt_frame(&mut rng, &session_key, &dummy_frame, aad.as_slice())?;
|
||||
|
||||
let (frame, context) = decrypt_kkt_frame(&session_key, &ciphertext, aad.as_slice())?;
|
||||
|
||||
assert_eq!(dummy_frame, frame);
|
||||
assert_eq!(context, valid_context);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,12 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KKTError {
|
||||
#[error("Signature constructor error")]
|
||||
SigConstructorError,
|
||||
#[error("Signature verification error")]
|
||||
SigVerifError,
|
||||
#[error(transparent)]
|
||||
CiphersuiteDecodingError(#[from] KKTCiphersuiteError),
|
||||
|
||||
#[error(transparent)]
|
||||
MaskedByteError(#[from] MaskedByteError),
|
||||
|
||||
#[error("KEM mapping failure: {}", info)]
|
||||
KEMMapping { info: &'static str },
|
||||
|
||||
@@ -48,10 +47,28 @@ pub enum KKTError {
|
||||
#[error("{}", info)]
|
||||
AEADError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
DecodingError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
UnsupportedAlgorithm { info: &'static str },
|
||||
|
||||
#[error("Generic libcrux error")]
|
||||
LibcruxError,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MaskedByteError {
|
||||
#[error(
|
||||
"Invalid Masked Byte Length: Expected({}), Actual({}).",
|
||||
expected,
|
||||
actual
|
||||
)]
|
||||
InvalidLength { expected: usize, actual: usize },
|
||||
#[error("Failed to Unmask Byte.")]
|
||||
Failure,
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> for KKTError {
|
||||
fn from(err: libcrux_kem::Error) -> Self {
|
||||
match err {
|
||||
|
||||
+101
-70
@@ -7,42 +7,120 @@
|
||||
// [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::{
|
||||
carrier::Carrier,
|
||||
context::{KKT_CONTEXT_LEN, KKTContext},
|
||||
error::KKTError,
|
||||
masked_byte::{MASKED_BYTE_LEN, MaskedByte},
|
||||
};
|
||||
|
||||
pub const KKT_SESSION_ID_LEN: usize = 16;
|
||||
|
||||
pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN];
|
||||
const KKT_CARRIER_CONTEXT: &[u8] = b"CARRIER_V1_KKT_V1_KDF";
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct KKTFrame {
|
||||
context: [u8; KKT_CONTEXT_LEN],
|
||||
session_id: KKTSessionId,
|
||||
body: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes).
|
||||
// if message coming from anonymous initiator => body is empty, there is no signature.
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id.
|
||||
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
|
||||
// if oneway and message coming from initiator => body is empty.
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key.
|
||||
// if coming from responder => body has the responder's kem public key.
|
||||
|
||||
impl KKTFrame {
|
||||
pub fn new(
|
||||
context: [u8; KKT_CONTEXT_LEN],
|
||||
body: &[u8],
|
||||
session_id: [u8; KKT_SESSION_ID_LEN],
|
||||
signature: &[u8],
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
pub fn new(context: &KKTContext, body: &[u8]) -> Result<Self, KKTError> {
|
||||
let context_bytes = context.encode()?;
|
||||
Ok(Self {
|
||||
context: context_bytes,
|
||||
body: Vec::from(body),
|
||||
session_id,
|
||||
signature: Vec::from(signature),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt_initiator_frame<R>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
responder_public_key: &DHPublicKey,
|
||||
version_byte: u8,
|
||||
) -> Result<(Carrier, Vec<u8>), 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 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);
|
||||
|
||||
Ok((carrier, full_kkt_message))
|
||||
}
|
||||
|
||||
pub fn decrypt_initiator_frame(
|
||||
responder_keypair: &DHKeyPair,
|
||||
message: &[u8],
|
||||
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]);
|
||||
|
||||
// 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)?;
|
||||
|
||||
// now that the version is ok, we can try dh
|
||||
|
||||
let initiator_public_key = DHPublicKey::from_bytes(&initiator_public_key_bytes);
|
||||
|
||||
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
|
||||
}
|
||||
@@ -51,42 +129,22 @@ impl KKTFrame {
|
||||
KKTContext::try_decode(self.context)
|
||||
}
|
||||
|
||||
pub fn signature_ref(&self) -> &[u8] {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
pub fn body_ref(&self) -> &[u8] {
|
||||
&self.body
|
||||
}
|
||||
|
||||
pub fn session_id_ref(&self) -> &[u8] {
|
||||
&self.session_id
|
||||
}
|
||||
pub fn session_id(&self) -> [u8; KKT_SESSION_ID_LEN] {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn signature_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.signature
|
||||
}
|
||||
pub fn body_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.body
|
||||
}
|
||||
|
||||
pub fn session_id_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.session_id
|
||||
}
|
||||
|
||||
pub fn frame_length(&self) -> usize {
|
||||
self.context.len() + self.session_id.len() + self.body.len() + self.signature.len()
|
||||
self.context.len() + self.body.len()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(self.frame_length());
|
||||
bytes.extend_from_slice(&self.context);
|
||||
bytes.extend_from_slice(&self.body);
|
||||
bytes.extend_from_slice(&self.session_id);
|
||||
bytes.extend_from_slice(&self.signature);
|
||||
bytes
|
||||
}
|
||||
|
||||
@@ -115,7 +173,6 @@ impl KKTFrame {
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
let mut signature = Vec::new();
|
||||
|
||||
// decode body
|
||||
if context.body_len() > 0 {
|
||||
@@ -123,33 +180,7 @@ impl KKTFrame {
|
||||
body.extend_from_slice(body_bytes);
|
||||
}
|
||||
|
||||
let session_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len()
|
||||
..KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN];
|
||||
// SAFETY: we're using exactly KKT_SESSION_ID_LEN bytes and we checked for sufficient bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let session_id = session_bytes.try_into().unwrap();
|
||||
|
||||
// // old code left for reference if session id becomes variable in length:
|
||||
// if context.session_id_len() > 0 {
|
||||
// session_id.extend_from_slice(
|
||||
// &bytes[KKT_CONTEXT_LEN + context.body_len()
|
||||
// ..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()],
|
||||
// );
|
||||
// }
|
||||
|
||||
// decode signature
|
||||
if context.signature_len() > 0 {
|
||||
let signature_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN
|
||||
..KKT_CONTEXT_LEN
|
||||
+ context.body_len()
|
||||
+ KKT_SESSION_ID_LEN
|
||||
+ context.signature_len()];
|
||||
signature.extend_from_slice(signature_bytes);
|
||||
}
|
||||
|
||||
Ok((
|
||||
KKTFrame::new(context_bytes, &body, session_id, &signature),
|
||||
context,
|
||||
))
|
||||
let frame = KKTFrame::new(&context, &body)?;
|
||||
Ok((frame, context))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
use libcrux_psq::handshake::types::DHPublicKey;
|
||||
use nym_kkt_ciphersuite::Ciphersuite;
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use crate::{
|
||||
carrier::Carrier,
|
||||
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
|
||||
error::KKTError,
|
||||
frame::KKTFrame,
|
||||
key_utils::validate_encapsulation_key,
|
||||
};
|
||||
|
||||
pub struct KKTInitiator<'a> {
|
||||
carrier: Carrier,
|
||||
context: KKTContext,
|
||||
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
|
||||
pub fn generate_one_way_request<R>(
|
||||
rng: &mut R,
|
||||
ciphersuite: &Ciphersuite,
|
||||
responder_dh_public_key: &DHPublicKey,
|
||||
expected_hash: &'a [u8],
|
||||
outer_protocol_version: u8,
|
||||
) -> Result<(Self, Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
Self::generate_encrypted_request(
|
||||
rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
None,
|
||||
responder_dh_public_key,
|
||||
expected_hash,
|
||||
outer_protocol_version,
|
||||
)
|
||||
}
|
||||
|
||||
// to be used by nodes
|
||||
pub fn generate_mutual_request<'b, R>(
|
||||
rng: &mut R,
|
||||
ciphersuite: &Ciphersuite,
|
||||
local_encapsulation_key: &'b [u8],
|
||||
responder_dh_public_key: &DHPublicKey,
|
||||
expected_hash: &'a [u8],
|
||||
outer_protocol_version: u8,
|
||||
) -> Result<(Self, Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
Self::generate_encrypted_request(
|
||||
rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
Some(local_encapsulation_key),
|
||||
responder_dh_public_key,
|
||||
expected_hash,
|
||||
outer_protocol_version,
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_encrypted_request<'b, R>(
|
||||
rng: &mut R,
|
||||
mode: KKTMode,
|
||||
ciphersuite: &Ciphersuite,
|
||||
local_encapsulation_key: Option<&'b [u8]>,
|
||||
responder_dh_public_key: &DHPublicKey,
|
||||
expected_hash: &'a [u8],
|
||||
outer_protocol_version: u8,
|
||||
) -> Result<(Self, Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let (context, frame) = initiator_process(mode, ciphersuite, local_encapsulation_key)?;
|
||||
let (carrier, message_bytes) =
|
||||
frame.encrypt_initiator_frame(rng, responder_dh_public_key, outer_protocol_version)?;
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
carrier,
|
||||
context,
|
||||
expected_hash,
|
||||
},
|
||||
message_bytes,
|
||||
))
|
||||
}
|
||||
|
||||
// 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<(Vec<u8>, bool), KKTError> {
|
||||
let decrypted_response_bytes = self.carrier.decrypt(response_bytes)?;
|
||||
let (response_frame, remote_context) = KKTFrame::from_bytes(&decrypted_response_bytes)?;
|
||||
initiator_ingest_response(
|
||||
&mut self.context,
|
||||
&response_frame,
|
||||
&remote_context,
|
||||
self.expected_hash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initiator_process<'a>(
|
||||
mode: KKTMode,
|
||||
ciphersuite: &Ciphersuite,
|
||||
own_encapsulation_key: Option<&'a [u8]>,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError> {
|
||||
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite);
|
||||
|
||||
let body: &[u8] = match mode {
|
||||
KKTMode::OneWay => &[],
|
||||
KKTMode::Mutual => match own_encapsulation_key {
|
||||
Some(encaps_key) => encaps_key,
|
||||
|
||||
// Missing key
|
||||
None => {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "KEM Key Not Provided",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let frame = KKTFrame::new(&context, body)?;
|
||||
|
||||
Ok((context, frame))
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response(
|
||||
own_context: &mut KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
remote_context: &KKTContext,
|
||||
expected_hash: &[u8],
|
||||
) -> Result<(Vec<u8>, bool), KKTError> {
|
||||
match remote_context.status() {
|
||||
KKTStatus::Ok | KKTStatus::UnverifiedKEMKey => {
|
||||
match validate_encapsulation_key(
|
||||
own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
true => Ok((
|
||||
remote_frame.body_ref().to_vec(),
|
||||
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.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
_ => Err(KKTError::ResponderFlaggedError {
|
||||
status: remote_context.status(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,38 @@
|
||||
use crate::ciphersuite::HashFunction;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
|
||||
use libcrux_psq::handshake::types::DHKeyPair;
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KeyDigests};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
pub fn generate_keypair_ed25519<R>(
|
||||
rng: &mut R,
|
||||
index: Option<u32>,
|
||||
) -> nym_crypto::asymmetric::ed25519::KeyPair
|
||||
pub fn generate_keypair_x25519<R>(rng: &mut R) -> DHKeyPair
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
nym_crypto::asymmetric::ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0))
|
||||
DHKeyPair::new(rng)
|
||||
}
|
||||
|
||||
pub fn generate_keypair_x25519<R>(rng: &mut R) -> nym_crypto::asymmetric::x25519::KeyPair
|
||||
pub fn generate_keypair_mlkem<R>(rng: &mut R) -> (MlKem768PrivateKey, MlKem768PublicKey)
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
|
||||
let private_key = nym_crypto::asymmetric::x25519::PrivateKey::from_secret(secret_initiator);
|
||||
private_key.into()
|
||||
libcrux_ml_kem::mlkem768::rand::generate_key_pair(rng).into_parts()
|
||||
}
|
||||
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_libcrux<R>(
|
||||
rng: &mut R,
|
||||
kem: crate::ciphersuite::KEM,
|
||||
) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), crate::error::KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
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(
|
||||
libcrux_kem::Algorithm::XWingKemDraft06,
|
||||
rng,
|
||||
)?),
|
||||
crate::ciphersuite::KEM::X25519 => {
|
||||
Ok(libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, rng)?)
|
||||
}
|
||||
_ => Err(crate::error::KKTError::KEMError {
|
||||
info: "Key Generation Error: Unsupported Libcrux Algorithm",
|
||||
}),
|
||||
}
|
||||
}
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_mceliece<'a, R>(
|
||||
pub fn generate_keypair_mceliece<R>(
|
||||
rng: &mut R,
|
||||
) -> (
|
||||
classic_mceliece_rust::SecretKey<'a>,
|
||||
classic_mceliece_rust::PublicKey<'a>,
|
||||
libcrux_psq::classic_mceliece::SecretKey,
|
||||
libcrux_psq::classic_mceliece::PublicKey,
|
||||
)
|
||||
where
|
||||
// this is annoying because mceliece lib uses rand 0.8.5...
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
|
||||
(decapsulation_key, encapsulation_key)
|
||||
let kp = libcrux_psq::classic_mceliece::KeyPair::generate_key_pair(rng);
|
||||
|
||||
(kp.sk, kp.pk)
|
||||
}
|
||||
|
||||
pub fn hash_key_bytes(
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Convenience wrappers around KKT protocol functions for easier integration.
|
||||
//!
|
||||
//! This module provides simplified APIs for the common use case of exchanging
|
||||
//! KEM public keys between a client (initiator) and gateway (responder).
|
||||
//!
|
||||
//! The underlying KKT protocol is implemented in the `session` module.
|
||||
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
context::{KKTContext, KKTMode},
|
||||
encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame},
|
||||
error::KKTError,
|
||||
};
|
||||
|
||||
// Re-export core session functions for advanced use cases
|
||||
pub use crate::session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
};
|
||||
|
||||
use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame};
|
||||
use crate::frame::KKTFrame;
|
||||
|
||||
/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode).
|
||||
///
|
||||
/// This is the client-side operation that initiates a KKT exchange.
|
||||
/// The request will be signed with the provided signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - Random number generator
|
||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses
|
||||
/// * `KKTContext` - Context to use when validating the response
|
||||
/// * `Vec<u8>` - Contains the client's ephemeral public key and encrypted and signed bytes to send to responder
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let (session_secret, context, request_frame) = request_kem_key(
|
||||
/// &mut rng,
|
||||
/// ciphersuite,
|
||||
/// client_signing_key,
|
||||
/// responder_dh_public_key,
|
||||
/// )?;
|
||||
/// // Send request_frame to gateway
|
||||
/// ```
|
||||
pub fn request_kem_key<R: CryptoRng + RngCore>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_public_key: &x25519::PublicKey,
|
||||
) -> Result<(KKTSessionSecret, KKTContext, Vec<u8>), KKTError> {
|
||||
// OneWay mode: client only wants responder's KEM key
|
||||
// None: client doesn't send their own KEM key
|
||||
let (initiator_context, initiator_frame) =
|
||||
initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)?;
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
encrypt_initial_kkt_frame(rng, responder_dh_public_key, &initiator_frame)?;
|
||||
|
||||
Ok((session_secret, initiator_context, encrypted_request_bytes))
|
||||
}
|
||||
|
||||
/// Decrypt, validate an *Encrypted* KKT response and extract the responder's KEM public key.
|
||||
///
|
||||
/// This is the client-side operation that processes the gateway's response.
|
||||
/// It verifies the signature and validates the key hash against the expected value
|
||||
/// (typically retrieved from a directory service).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context from the initial request
|
||||
/// * `session_secret` - Session Secret Key (generated with request)
|
||||
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
|
||||
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
|
||||
/// * `response_bytes` - Serialized response frame from responder
|
||||
///
|
||||
/// # Returns
|
||||
/// * `EncapsulationKey` - Authenticated KEM public key of the responder
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let gateway_kem_key = validate_kem_response(
|
||||
/// &mut context,
|
||||
/// &session_secret,
|
||||
/// &gateway_verification_key,
|
||||
/// &expected_hash_from_directory,
|
||||
/// &response_bytes,
|
||||
/// )?;
|
||||
/// // Use gateway_kem_key for PSQ
|
||||
/// ```
|
||||
pub fn validate_kem_response<'a>(
|
||||
context: &mut KKTContext,
|
||||
session_secret: &KKTSessionSecret,
|
||||
responder_vk: &ed25519::PublicKey,
|
||||
expected_key_hash: &[u8],
|
||||
encrypted_response_bytes: &[u8],
|
||||
) -> Result<EncapsulationKey<'a>, KKTError> {
|
||||
let (responder_frame, responder_context) =
|
||||
decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?;
|
||||
|
||||
initiator_ingest_response(
|
||||
context,
|
||||
&responder_frame,
|
||||
&responder_context,
|
||||
responder_vk,
|
||||
expected_key_hash,
|
||||
)
|
||||
}
|
||||
|
||||
/// Decrypts and validates an *Encrypted* KKT response
|
||||
///
|
||||
/// This is the client-side operation that processes the gateway's response.
|
||||
pub fn decrypt_kkt_response_frame(
|
||||
session_secret: &KKTSessionSecret,
|
||||
frame_ciphertext: &[u8],
|
||||
) -> Result<(KKTFrame, KKTContext), KKTError> {
|
||||
decrypt_kkt_frame(session_secret, frame_ciphertext, KKT_RESPONSE_AAD)
|
||||
}
|
||||
|
||||
/// Handle an *Encrypted* KKT request and generate a signed response with the responder's KEM key.
|
||||
///
|
||||
/// This is the gateway-side operation that processes a client's KKT request.
|
||||
/// It validates the request signature (if authenticated) and responds with
|
||||
/// the gateway's KEM public key, signed for authenticity.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `encrypted_request_bytes` - encrypted KEM request
|
||||
/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous)
|
||||
/// * `responder_signing_key` - Gateway's Ed25519 signing key
|
||||
/// * `responder_dh_public_key` - Gateway's long-term x25519 Diffie-Hellman private key
|
||||
/// * `responder_kem_key` - Gateway's KEM public key to send
|
||||
///
|
||||
/// # Returns
|
||||
/// * `KKTFrame` - Signed response frame containing the KEM public key
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let response_frame = handle_kem_request(
|
||||
/// &request_frame,
|
||||
/// Some(client_verification_key), // or None for anonymous
|
||||
/// gateway_signing_key,
|
||||
/// &gateway_kem_public_key,
|
||||
/// )?;
|
||||
/// // Send response_frame back to client
|
||||
/// ```
|
||||
pub fn handle_kem_request<'a, R>(
|
||||
rng: &mut R,
|
||||
encrypted_request_bytes: &[u8],
|
||||
initiator_vk: Option<&ed25519::PublicKey>,
|
||||
responder_signing_key: &ed25519::PrivateKey,
|
||||
responder_dh_private_key: &x25519::PrivateKey,
|
||||
responder_kem_key: &EncapsulationKey<'a>,
|
||||
) -> Result<Vec<u8>, KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
// Compute the session's shared secret, decrypt and parse context from the request frame
|
||||
|
||||
let (session_secret, request_frame, initiator_context) =
|
||||
decrypt_initial_kkt_frame(responder_dh_private_key, encrypted_request_bytes)?;
|
||||
|
||||
// Validate the request (verifies signature if initiator_vk provided)
|
||||
let (mut response_context, _) = responder_ingest_message(
|
||||
&initiator_context,
|
||||
initiator_vk,
|
||||
None, // Not checking initiator's KEM key in OneWay mode
|
||||
&request_frame,
|
||||
)?;
|
||||
|
||||
// Generate signed response with our KEM public key
|
||||
let responder_frame = responder_process(
|
||||
&mut response_context,
|
||||
request_frame.session_id(),
|
||||
responder_signing_key,
|
||||
responder_kem_key,
|
||||
)?;
|
||||
|
||||
// Encrypt the responder's response with the session's shared secret
|
||||
encrypt_kkt_frame(rng, &session_secret, &responder_frame, KKT_RESPONSE_AAD)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
ciphersuite::{HashFunction, KEM, SignatureScheme},
|
||||
key_utils::{generate_keypair_libcrux, hash_encapsulation_key},
|
||||
};
|
||||
|
||||
fn random_x25519_key() -> x25519::PrivateKey {
|
||||
let mut bytes = [0u8; 32];
|
||||
let mut rng = rand::rng();
|
||||
rng.fill_bytes(&mut bytes);
|
||||
x25519::PrivateKey::from_secret(bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_oneway_authenticated() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let ed25519_init = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let ed25519_resp = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
// Generate responder's KEM keypair (X25519 for testing)
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
// Create ciphersuite
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Hash the KEM key (simulating directory storage)
|
||||
let key_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&responder_kem_key.encode(),
|
||||
);
|
||||
|
||||
// Client: Request KEM key
|
||||
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
ed25519_init.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway: Handle request
|
||||
let response_frame_ciphertext = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame_ciphertext,
|
||||
Some(ed25519_init.public_key()), // Authenticated
|
||||
ed25519_resp.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&session_key,
|
||||
ed25519_resp.public_key(),
|
||||
&key_hash,
|
||||
&response_frame_ciphertext,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Verify we got the correct KEM key
|
||||
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_anonymous() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Only responder has keys
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&responder_kem_key.encode(),
|
||||
);
|
||||
|
||||
// Anonymous initiator
|
||||
let (mut context, request_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
encrypt_initial_kkt_frame(&mut rng, &x25519_resp_pub, &request_frame).unwrap();
|
||||
|
||||
// Gateway: Handle anonymous request
|
||||
let response_frame = handle_kem_request(
|
||||
&mut rng,
|
||||
&encrypted_request_bytes,
|
||||
None, // Anonymous - no verification key
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Initiator: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&session_secret,
|
||||
responder_keypair.public_key(),
|
||||
&key_hash,
|
||||
&response_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
// Different keypair for wrong signature
|
||||
let mut wrong_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut wrong_secret);
|
||||
let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_session_key, _context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Gateway handles request but we provide WRONG verification key
|
||||
let result = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame_ciphertext,
|
||||
Some(wrong_keypair.public_key()), // Wrong key!
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
);
|
||||
|
||||
// Should fail signature verification
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
|
||||
|
||||
let mut responder_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut responder_secret);
|
||||
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
|
||||
|
||||
let x25519_resp_priv = random_x25519_key();
|
||||
let x25519_resp_pub = x25519::PublicKey::from(&x25519_resp_priv);
|
||||
|
||||
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Use WRONG hash
|
||||
let wrong_hash = [0u8; 32];
|
||||
|
||||
let (session_key, mut context, request_frame) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
&x25519_resp_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response_frame = handle_kem_request(
|
||||
&mut rng,
|
||||
&request_frame,
|
||||
Some(initiator_keypair.public_key()),
|
||||
responder_keypair.private_key(),
|
||||
&x25519_resp_priv,
|
||||
&responder_kem_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Client validates with WRONG hash
|
||||
let result = validate_kem_response(
|
||||
&mut context,
|
||||
&session_key,
|
||||
responder_keypair.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
&response_frame,
|
||||
);
|
||||
|
||||
// Should fail hash validation
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
+163
-451
@@ -1,498 +1,210 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod ciphersuite;
|
||||
pub mod carrier;
|
||||
pub mod context;
|
||||
pub mod encryption;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod initiator;
|
||||
pub mod key_utils;
|
||||
// pub mod kkt;
|
||||
pub mod session;
|
||||
pub mod masked_byte;
|
||||
pub mod rekey;
|
||||
pub mod responder;
|
||||
|
||||
// This must be less than 4 bits
|
||||
pub const KKT_VERSION: u8 = 1;
|
||||
const _: () = assert!(KKT_VERSION < 1 << 4);
|
||||
pub const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response";
|
||||
pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_FRAME";
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
|
||||
|
||||
use crate::{
|
||||
KKT_RESPONSE_AAD,
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
|
||||
encryption::{
|
||||
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
|
||||
encrypt_kkt_frame,
|
||||
},
|
||||
frame::KKTFrame,
|
||||
initiator::KKTInitiator,
|
||||
key_utils::{
|
||||
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece,
|
||||
generate_keypair_x25519, hash_encapsulation_key,
|
||||
},
|
||||
session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519,
|
||||
hash_encapsulation_key,
|
||||
},
|
||||
responder::KKTResponder,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
hash_function,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_encrypted() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
|
||||
fn test_kkt_psq_e2e_encrypted_carrier() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate responder x25519 keys
|
||||
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
|
||||
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
] {
|
||||
// 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 initiator_mlkem_keypair = generate_keypair_mlkem(&mut rng);
|
||||
let initiator_mceliece_keypair = generate_keypair_mceliece(&mut rng);
|
||||
|
||||
let _i_dir_hash_mlkem = hash_encapsulation_key(
|
||||
// &ciphersuite.hash_function(),
|
||||
&hash_function,
|
||||
// ciphersuite.hash_len(),
|
||||
HashLength::Default.value(),
|
||||
initiator_mlkem_keypair.1.as_slice().as_slice(),
|
||||
);
|
||||
|
||||
let _i_dir_hash_mceliece = hash_encapsulation_key(
|
||||
// &ciphersuite.hash_function(),
|
||||
&hash_function,
|
||||
// ciphersuite.hash_len(),
|
||||
HashLength::Default.value(),
|
||||
initiator_mceliece_keypair.1.as_ref(),
|
||||
);
|
||||
|
||||
let responder = KKTResponder::new(
|
||||
&responder_x25519_keypair,
|
||||
Some(&responder_mlkem_keypair.1),
|
||||
Some(&responder_mceliece_keypair.1),
|
||||
&[
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
],
|
||||
&[1],
|
||||
&[SignatureScheme::Ed25519],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// OneWay - MlKem
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
KEM::MlKem768,
|
||||
hash_function,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
&ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mlkem,
|
||||
1u8,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
let (response_bytes, _) = responder.process_request(&request_bytes).unwrap();
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
assert_eq!(
|
||||
i_obtained_key,
|
||||
responder_mlkem_keypair.1.as_slice().as_slice(),
|
||||
)
|
||||
}
|
||||
// Mutual - MlKem
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::MlKem768,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
&ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mlkem,
|
||||
1u8,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
let (response_bytes, r_obtained_key) =
|
||||
responder.process_request(&request_bytes).unwrap();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
// if we keep unverified keys, this should change
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
assert_eq!(
|
||||
i_obtained_key,
|
||||
responder_mlkem_keypair.1.as_slice().as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
// encryption - initiator frame
|
||||
// OneWay - McEliece
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::McEliece,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
&ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mceliece,
|
||||
1u8,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
let (response_bytes, _) = responder.process_request(&request_bytes).unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
assert_eq!(i_obtained_key, responder_mceliece_keypair.1.as_ref(),)
|
||||
}
|
||||
// Mutual - MlKem
|
||||
{
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::McEliece,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
&ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mceliece,
|
||||
1u8,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
||||
let (response_bytes, r_obtained_key) =
|
||||
responder.process_request(&request_bytes).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
// if we keep unverified keys, this should change
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, r_context) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
|
||||
let (i_session_secret, i_bytes) = encrypt_initial_kkt_frame(
|
||||
&mut rng,
|
||||
responder_x25519_keypair.public_key(),
|
||||
&i_frame,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// decryption - initiator frame
|
||||
|
||||
let (r_session_secret, i_frame_r, i_context_r) =
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&i_context_r,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// encryption - responder frame
|
||||
let r_bytes =
|
||||
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
|
||||
.unwrap();
|
||||
|
||||
// decryption - responder frame
|
||||
|
||||
let (i_frame_r, i_context_r) =
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
assert_eq!(i_obtained_key, responder_mceliece_keypair.1.as_ref(),)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
use nym_crypto::{blake3, hmac::hmac::digest::ExtendableOutput};
|
||||
|
||||
use crate::error::{
|
||||
MaskedByteError,
|
||||
MaskedByteError::{Failure, InvalidLength},
|
||||
};
|
||||
|
||||
pub const MASKED_BYTE_LEN: usize = 16;
|
||||
pub const MASKED_BYTE_CONTEXT_STR: &[u8] = b"NYM_MASKED_BYTE_V1";
|
||||
|
||||
const U8_RANGE: [u8; 256] = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
|
||||
50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
|
||||
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
|
||||
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
|
||||
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
|
||||
136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
|
||||
155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173,
|
||||
174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192,
|
||||
193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
|
||||
212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
|
||||
231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
|
||||
250, 251, 252, 253, 254, 255,
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MaskedByte([u8; MASKED_BYTE_LEN]);
|
||||
|
||||
impl MaskedByte {
|
||||
/// Mask a byte by hashing it with some mask.
|
||||
/// Outputs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF || byte)
|
||||
pub fn new(byte: u8, mask: &[u8]) -> Self {
|
||||
let mut output: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(MASKED_BYTE_CONTEXT_STR);
|
||||
hasher.update(mask);
|
||||
// avoid zero update
|
||||
hasher.update(&[0xFF, byte]);
|
||||
hasher.finalize_xof_into(&mut output);
|
||||
|
||||
Self(output)
|
||||
}
|
||||
/// Unmasks a byte by trial hashing.
|
||||
/// This function runs Blake3_Hash(MASKED_BYTE_CONTEXT_STR || mask || 0xFF).
|
||||
/// This Hasher state is then cloned updated with `i: u8` in (0..=u8::max).
|
||||
/// If we find an `i` which yields back the hash input, then we found the masked byte.
|
||||
/// Otherwise, the function returns an error.
|
||||
pub fn unmask(&self, mask: &[u8]) -> Result<u8, MaskedByteError> {
|
||||
self.unmask_check_version(mask, &U8_RANGE)
|
||||
}
|
||||
|
||||
// This could be more efficient than unmask,
|
||||
// because we just could check against a smaller list of supported versions.
|
||||
pub fn unmask_check_version(
|
||||
&self,
|
||||
mask: &[u8],
|
||||
supported_versions: &[u8],
|
||||
) -> Result<u8, MaskedByteError> {
|
||||
let mut buf: [u8; MASKED_BYTE_LEN] = [0u8; MASKED_BYTE_LEN];
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(MASKED_BYTE_CONTEXT_STR);
|
||||
hasher.update(mask);
|
||||
// avoid zero update
|
||||
hasher.update(&[0xFF]);
|
||||
for i in supported_versions {
|
||||
let mut t_hasher = hasher.clone();
|
||||
t_hasher.update(&[*i]);
|
||||
t_hasher.finalize_xof_into(&mut buf);
|
||||
if buf == self.0 {
|
||||
return Ok(*i);
|
||||
}
|
||||
}
|
||||
Err(Failure)
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; 16] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; MASKED_BYTE_LEN]> for MaskedByte {
|
||||
fn from(value: [u8; MASKED_BYTE_LEN]) -> Self {
|
||||
MaskedByte(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8; MASKED_BYTE_LEN]> for MaskedByte {
|
||||
fn from(value: &[u8; MASKED_BYTE_LEN]) -> Self {
|
||||
MaskedByte(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
expected: MASKED_BYTE_LEN,
|
||||
actual: value.len(),
|
||||
})
|
||||
} else {
|
||||
Ok(Self::from(value.as_chunks::<MASKED_BYTE_LEN>().0[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use crate::masked_byte::MASKED_BYTE_LEN;
|
||||
|
||||
use super::MaskedByte;
|
||||
use rand09::{Rng, RngCore, rng};
|
||||
|
||||
#[test]
|
||||
fn test_masking() {
|
||||
let mut mask: [u8; 256] = [0u8; 256];
|
||||
let mut wire_bytes: [u8; MASKED_BYTE_LEN];
|
||||
|
||||
// why not
|
||||
for i in 0..=u8::MAX {
|
||||
// gen mask
|
||||
rng().fill_bytes(&mut mask);
|
||||
let masked_byte = MaskedByte::new(i, &mask);
|
||||
wire_bytes = masked_byte.to_bytes();
|
||||
|
||||
let decoded_masked_byte = MaskedByte::from(wire_bytes);
|
||||
let output = decoded_masked_byte.unmask(&mask).unwrap();
|
||||
|
||||
assert_eq!(i, output);
|
||||
|
||||
// flip bit
|
||||
let mut with_flipped_bit = decoded_masked_byte.to_bytes();
|
||||
|
||||
let byte_idx: usize = rng().random_range(0..MASKED_BYTE_LEN);
|
||||
let bit_idx = rng().random_range(0..8);
|
||||
with_flipped_bit[byte_idx] ^= 1 << bit_idx;
|
||||
|
||||
let decoded_masked_byte = MaskedByte::from(with_flipped_bit);
|
||||
assert!(decoded_masked_byte.unmask(&mask).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoding() {
|
||||
let mut mask: [u8; 256] = [0u8; 256];
|
||||
|
||||
// gen mask
|
||||
rng().fill_bytes(&mut mask);
|
||||
let byte = rng().random();
|
||||
let masked_byte = MaskedByte::new(byte, &mask);
|
||||
let wire_bytes: [u8; MASKED_BYTE_LEN] = masked_byte.to_bytes();
|
||||
|
||||
// should succeed
|
||||
let decoded_masked_byte = MaskedByte::try_from(wire_bytes.as_slice()).unwrap();
|
||||
let output = decoded_masked_byte.unmask(&mask).unwrap();
|
||||
|
||||
assert_eq!(byte, output);
|
||||
|
||||
let empty_slice: &[u8] = &[];
|
||||
// should fail
|
||||
assert!(MaskedByte::try_from(empty_slice).is_err());
|
||||
|
||||
let mut wire_bytes_messy = Vec::from(wire_bytes);
|
||||
|
||||
// add more one more byte
|
||||
wire_bytes_messy.push(0x42);
|
||||
assert!(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);
|
||||
// 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);
|
||||
// should fail
|
||||
assert!(MaskedByte::try_from(wire_bytes_messy.as_slice()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// 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 rand09::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
/// Context string to be used with the Blake3 KDF.
|
||||
const REKEY_CONTEXT: &str = "NYM_PQ_REKEY_v1";
|
||||
|
||||
pub struct RekeyInitiator {
|
||||
algorithm: Algorithm,
|
||||
decapsulation_key: PrivateKey,
|
||||
salt: [u8; 32],
|
||||
}
|
||||
|
||||
impl RekeyInitiator {
|
||||
/// 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 salt and an encoding of the encapsulation key as follows
|
||||
/// salt encapsulation_key
|
||||
/// [0 ........ 32 | 32 .............. ]
|
||||
///
|
||||
/// Inputs:
|
||||
/// rng: something that implements CryptoRng + RngCore
|
||||
/// kem: a KEM algorithm (we currently support MlKem768 and XWing)
|
||||
///
|
||||
/// Outputs:
|
||||
/// 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,
|
||||
{
|
||||
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 => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
// We don't support X25519 because it's not post-quantum secure.
|
||||
KEM::X25519 => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "X25519 is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 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)?;
|
||||
|
||||
// Append the encoding of the KEM encapsulation key to the initiator's randomness.
|
||||
request_buffer.extend(encapsulation_key.encode());
|
||||
|
||||
Ok((
|
||||
// The Initiator should store this until they use `RekeyInitiator::finalize`.
|
||||
RekeyInitiator {
|
||||
algorithm,
|
||||
decapsulation_key,
|
||||
salt,
|
||||
},
|
||||
// This is to be sent to the responder.
|
||||
request_buffer,
|
||||
))
|
||||
}
|
||||
|
||||
/// 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 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.
|
||||
///
|
||||
/// Input:
|
||||
/// response_message: the responder's message which contains an encapsulation of `pre_key`.
|
||||
/// Output:
|
||||
/// [u8; 32]: the new shared secret.
|
||||
pub fn finalize(mut self, response_message: &[u8]) -> Result<[u8; 32], KKTError> {
|
||||
// Decode the responder's ciphertext.
|
||||
let ciphertext = Ct::decode(self.algorithm, response_message)?;
|
||||
// Decapsulate the `pre_key` using the Initiator's decapsulation key.
|
||||
let pre_key = ciphertext.decapsulate(&self.decapsulation_key)?;
|
||||
|
||||
// Encode the `pre_key` into bytes
|
||||
let pre_key_bytes = pre_key.encode();
|
||||
|
||||
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
|
||||
|
||||
Ok(new_secret)
|
||||
}
|
||||
}
|
||||
|
||||
/// The responder parses the request message.
|
||||
/// 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.
|
||||
///
|
||||
/// If the message is badly formatted, or the encapsulation received is invalid,
|
||||
/// 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 salt to obtain
|
||||
/// the new shared secret.
|
||||
///
|
||||
/// Inputs:
|
||||
/// rng: something that implements CryptoRng + RngCore
|
||||
/// request_message: the Initiator's request message (contains the salt and encapsulation key)
|
||||
///
|
||||
/// Outputs:
|
||||
/// [u8; 32]: new shared secret
|
||||
/// Vec<u8>: response which contains an encapsulation of a secret value generated by the responder.
|
||||
/// This is to be sent back to the Initiator as-is.
|
||||
pub fn responder_process<R>(
|
||||
rng: &mut R,
|
||||
mut request_message: Vec<u8>,
|
||||
) -> Result<([u8; 32], Vec<u8>), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
// Deduce the KEM algorithm from the message length
|
||||
let algorithm = match request_message.len().checked_sub(32) {
|
||||
//
|
||||
Some(num) => match num {
|
||||
// If message length is 1216 (32 + 1184) then the algorithm should be MlKem768
|
||||
ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768,
|
||||
// If message length is 1248 (32 + 1216) then the algorithm should be MlKem768
|
||||
xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06,
|
||||
// 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.
|
||||
mceliece::PUBLIC_KEY_LENGTH => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
// We don't support X25519 because it's not post-quantum secure.
|
||||
x25519::PUBLIC_KEY_LENGTH => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "McEliece is not supported for re-keying",
|
||||
});
|
||||
}
|
||||
// Reject if the size does not match any of the above.
|
||||
_ => {
|
||||
return Err(KKTError::UnsupportedAlgorithm {
|
||||
info: "Unknown Algorithm",
|
||||
});
|
||||
}
|
||||
},
|
||||
// Reject if message length is less than 32.
|
||||
None => {
|
||||
return Err(KKTError::DecodingError {
|
||||
info: "Invalid rekey request: size is too small",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Split the message to get the Initiator's salt (first 32 bytes)
|
||||
// and the encoding of the Initiator's public key.
|
||||
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)?;
|
||||
|
||||
// Encapsulate a fresh `pre_key` using the Initiator's encapsulation key into `ciphertext`.
|
||||
let (pre_key, ciphertext) = remote_encapsulation_key.encapsulate(rng)?;
|
||||
// Encode the ciphertext into bytes to send back to the initiator.
|
||||
let message = ciphertext.encode();
|
||||
|
||||
// Encode the `pre_key` into bytes
|
||||
let pre_key_bytes = pre_key.encode();
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::rekey::{RekeyInitiator, responder_process};
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
|
||||
#[test]
|
||||
fn rekey_test() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
for kem in [KEM::MlKem768, KEM::XWing] {
|
||||
let (rekey_state, request_message) =
|
||||
RekeyInitiator::generate_request(&mut rng, kem).unwrap();
|
||||
|
||||
let (responder_secret, response_message) =
|
||||
responder_process(&mut rng, request_message).unwrap();
|
||||
|
||||
let initiator_secret = rekey_state.finalize(&response_message).unwrap();
|
||||
|
||||
assert_eq!(initiator_secret, responder_secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::key_utils::validate_encapsulation_key;
|
||||
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};
|
||||
pub struct KKTResponder<'a> {
|
||||
x25519_keypair: &'a DHKeyPair,
|
||||
mlkem_encapsulation_key: Option<&'a libcrux_kem::MlKem768PublicKey>,
|
||||
mceliece_encapsulation_key: Option<&'a libcrux_psq::classic_mceliece::PublicKey>,
|
||||
supported_hash_functions: HashSet<HashFunction>,
|
||||
supported_signature_schemes: HashSet<SignatureScheme>,
|
||||
supported_outer_protocol_versions: HashSet<u8>,
|
||||
}
|
||||
impl<'a> KKTResponder<'a> {
|
||||
pub fn new(
|
||||
x25519_keypair: &'a DHKeyPair,
|
||||
mlkem_encapsulation_key: Option<&'a libcrux_kem::MlKem768PublicKey>,
|
||||
mceliece_encapsulation_key: Option<&'a libcrux_psq::classic_mceliece::PublicKey>,
|
||||
supported_hash_functions: &[HashFunction],
|
||||
supported_outer_protocol_versions: &[u8],
|
||||
supported_signature_schemes: &[SignatureScheme],
|
||||
) -> Result<Self, KKTError> {
|
||||
let hash_functions: HashSet<HashFunction> =
|
||||
supported_hash_functions.iter().copied().collect();
|
||||
|
||||
if hash_functions.is_empty() {
|
||||
Err(KKTError::FunctionInputError {
|
||||
info: "Did not provide a supported HashFunction when instaciating 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() {
|
||||
return 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn supported_protocol_versions(&self) -> Vec<u8> {
|
||||
self.supported_outer_protocol_versions
|
||||
.iter()
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn check_ciphersuite_compatiblity(
|
||||
&self,
|
||||
remote_ciphersuite: &Ciphersuite,
|
||||
) -> Result<(), KKTError> {
|
||||
if !self
|
||||
.supported_hash_functions
|
||||
.contains(remote_ciphersuite.hash_function())
|
||||
{
|
||||
Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported HashFunction",
|
||||
})
|
||||
} else {
|
||||
if !self
|
||||
.supported_signature_schemes
|
||||
.contains(remote_ciphersuite.signature_scheme())
|
||||
{
|
||||
Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported SignatureScheme",
|
||||
})
|
||||
} else {
|
||||
if match remote_ciphersuite.kem() {
|
||||
KEM::MlKem768 => self.mlkem_encapsulation_key.is_some(),
|
||||
KEM::McEliece => self.mceliece_encapsulation_key.is_some(),
|
||||
_ => false,
|
||||
} {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KKTError::IncompatibilityError {
|
||||
info: "Unsupported KEM",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When this function fails, we do that silently (i.e. we dont 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(
|
||||
self.x25519_keypair,
|
||||
request_bytes,
|
||||
&self.supported_protocol_versions(),
|
||||
)?;
|
||||
|
||||
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::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)?
|
||||
}
|
||||
};
|
||||
|
||||
let frame = if local_context.ciphersuite().kem() == &KEM::MlKem768 {
|
||||
KKTFrame::new(
|
||||
&local_context,
|
||||
// SAFETY: the self.check_ciphersuite_compatibility call above guarantees that we will have a key in the right place
|
||||
#[allow(clippy::unwrap_used)]
|
||||
&self.mlkem_encapsulation_key.unwrap().as_slice().as_slice(),
|
||||
)?
|
||||
} else {
|
||||
KKTFrame::new(
|
||||
&local_context,
|
||||
// SAFETY: the self.check_ciphersuite_compatibility call above guarantees that we will have a key in the right place
|
||||
#[allow(clippy::unwrap_used)]
|
||||
&self.mceliece_encapsulation_key.unwrap().as_ref(),
|
||||
)?
|
||||
};
|
||||
|
||||
// encryption - responder frame
|
||||
let response_bytes = carrier.encrypt(&frame.to_bytes())?;
|
||||
Ok((response_bytes, remote_encapsulation_key))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn responder_ingest_message(
|
||||
remote_context: &KKTContext,
|
||||
expected_hash: Option<&[u8]>,
|
||||
remote_frame: &KKTFrame,
|
||||
) -> Result<(KKTContext, Option<Vec<u8>>), KKTError> {
|
||||
let mut own_context = remote_context.derive_responder_header()?;
|
||||
|
||||
match remote_context.role() {
|
||||
KKTRole::Initiator => {
|
||||
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
|
||||
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
|
||||
|
||||
// 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)))
|
||||
//
|
||||
|
||||
Ok((own_context, None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KKTRole::Responder => Err(KKTError::IncompatibilityError {
|
||||
info: "Responder received a request from another responder.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::frame::KKTSessionId;
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
|
||||
error::KKTError,
|
||||
frame::{KKT_SESSION_ID_LEN, KKTFrame},
|
||||
key_utils::validate_encapsulation_key,
|
||||
};
|
||||
|
||||
pub fn initiator_process<'a, R>(
|
||||
rng: &mut R,
|
||||
mode: KKTMode,
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
own_encapsulation_key: Option<&EncapsulationKey<'a>>,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?;
|
||||
|
||||
let context_bytes = context.encode()?;
|
||||
|
||||
let mut session_id = [0; KKT_SESSION_ID_LEN];
|
||||
// Generate Session ID
|
||||
rng.fill_bytes(&mut session_id);
|
||||
|
||||
let body: &[u8] = match mode {
|
||||
KKTMode::OneWay => &[],
|
||||
KKTMode::Mutual => match own_encapsulation_key {
|
||||
Some(encaps_key) => &encaps_key.encode(),
|
||||
|
||||
// Missing key
|
||||
None => {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "KEM Key Not Provided",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut bytes_to_sign =
|
||||
Vec::with_capacity(context.full_message_len() - context.signature_len());
|
||||
bytes_to_sign.extend_from_slice(&context_bytes);
|
||||
bytes_to_sign.extend_from_slice(body);
|
||||
bytes_to_sign.extend_from_slice(&session_id);
|
||||
|
||||
let signature = signing_key.sign(bytes_to_sign).to_bytes();
|
||||
|
||||
Ok((
|
||||
context,
|
||||
KKTFrame::new(context_bytes, body, session_id, &signature),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn anonymous_initiator_process<R>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let context = KKTContext::new(KKTRole::AnonymousInitiator, KKTMode::OneWay, ciphersuite)?;
|
||||
let context_bytes = context.encode()?;
|
||||
|
||||
let mut session_id = [0u8; KKT_SESSION_ID_LEN];
|
||||
rng.fill_bytes(&mut session_id);
|
||||
|
||||
Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[])))
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: &ed25519::PublicKey,
|
||||
expected_hash: &[u8],
|
||||
) -> Result<EncapsulationKey<'a>, KKTError> {
|
||||
check_compatibility(own_context, remote_context)?;
|
||||
match remote_context.status() {
|
||||
KKTStatus::Ok => {
|
||||
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
|
||||
remote_context.full_message_len() - remote_context.signature_len(),
|
||||
);
|
||||
bytes_to_verify.extend_from_slice(&remote_context.encode()?);
|
||||
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
|
||||
|
||||
match Signature::from_bytes(remote_frame.signature_ref()) {
|
||||
Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) {
|
||||
Ok(()) => {
|
||||
let received_encapsulation_key = EncapsulationKey::decode(
|
||||
own_context.ciphersuite().kem(),
|
||||
remote_frame.body_ref(),
|
||||
)?;
|
||||
|
||||
match validate_encapsulation_key(
|
||||
&own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
true => Ok(received_encapsulation_key),
|
||||
|
||||
// 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.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
_ => Err(KKTError::ResponderFlaggedError {
|
||||
status: remote_context.status(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// todo: figure out how to handle errors using status codes
|
||||
|
||||
pub fn responder_ingest_message<'a>(
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: Option<&ed25519::PublicKey>,
|
||||
expected_hash: Option<&[u8]>,
|
||||
remote_frame: &KKTFrame,
|
||||
) -> Result<(KKTContext, Option<EncapsulationKey<'a>>), KKTError> {
|
||||
let own_context = remote_context.derive_responder_header()?;
|
||||
|
||||
match remote_context.role() {
|
||||
KKTRole::AnonymousInitiator => Ok((own_context, None)),
|
||||
|
||||
KKTRole::Initiator => {
|
||||
match remote_verification_key {
|
||||
Some(remote_verif_key) => {
|
||||
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
|
||||
own_context.full_message_len() - own_context.signature_len(),
|
||||
);
|
||||
bytes_to_verify.extend_from_slice(remote_frame.context_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
|
||||
|
||||
match Signature::from_bytes(remote_frame.signature_ref()) {
|
||||
Ok(sig) => match remote_verif_key.verify(bytes_to_verify, &sig) {
|
||||
Ok(()) => {
|
||||
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
|
||||
match own_context.mode() {
|
||||
KKTMode::OneWay => Ok((own_context, None)),
|
||||
KKTMode::Mutual => {
|
||||
match expected_hash {
|
||||
Some(expected_hash) => {
|
||||
let received_encapsulation_key =
|
||||
EncapsulationKey::decode(
|
||||
own_context.ciphersuite().kem(),
|
||||
remote_frame.body_ref(),
|
||||
)?;
|
||||
if validate_encapsulation_key(
|
||||
&own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
Ok((
|
||||
own_context,
|
||||
Some(received_encapsulation_key),
|
||||
))
|
||||
}
|
||||
// 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 => Err(KKTError::FunctionInputError {
|
||||
info: "Expected hash of the remote encapsulation key is not provided.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
None => Err(KKTError::FunctionInputError {
|
||||
info: "Remote Signature Verification Key Not Provided",
|
||||
}),
|
||||
}
|
||||
}
|
||||
KKTRole::Responder => Err(KKTError::IncompatibilityError {
|
||||
info: "Responder received a request from another responder.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn responder_process<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
session_id: KKTSessionId,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
encapsulation_key: &EncapsulationKey<'a>,
|
||||
) -> Result<KKTFrame, KKTError> {
|
||||
let body = encapsulation_key.encode();
|
||||
|
||||
let context_bytes = own_context.encode()?;
|
||||
|
||||
let mut bytes_to_sign =
|
||||
Vec::with_capacity(own_context.full_message_len() - own_context.signature_len());
|
||||
bytes_to_sign.extend_from_slice(&own_context.encode()?);
|
||||
bytes_to_sign.extend_from_slice(&body);
|
||||
bytes_to_sign.extend_from_slice(&session_id);
|
||||
|
||||
let signature = signing_key.sign(bytes_to_sign).to_bytes();
|
||||
|
||||
Ok(KKTFrame::new(context_bytes, &body, session_id, &signature))
|
||||
}
|
||||
|
||||
fn check_compatibility(
|
||||
_own_context: &KKTContext,
|
||||
_remote_context: &KKTContext,
|
||||
) -> Result<(), KKTError> {
|
||||
// todo: check ciphersuite/context compatibility
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "nym-lp-sandbox"
|
||||
version = "0.1.0"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand09 = { workspace = true }
|
||||
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-kkt = { path = "../nym-kkt" }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
|
||||
nym-kkt-ciphersuite = { path ="../nym-kkt-ciphersuite" }
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
libcrux-psq = { workspace = true, features = ["test-utils"] }
|
||||
libcrux-kem = { workspace = true }
|
||||
tls_codec = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
rand_chacha = "0.3"
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
nym-test-utils = { workspace = true }
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use libcrux_psq::{
|
||||
Channel, IntoSession,
|
||||
handshake::{
|
||||
builders::{CiphersuiteBuilder, PrincipalBuilder},
|
||||
ciphersuites::CiphersuiteName,
|
||||
types::{Authenticator, PQEncapsulationKey},
|
||||
},
|
||||
session::{Session, SessionBinding},
|
||||
};
|
||||
use nym_kkt::{
|
||||
initiator::KKTInitiator,
|
||||
key_utils::{
|
||||
generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519,
|
||||
hash_encapsulation_key,
|
||||
},
|
||||
responder::KKTResponder,
|
||||
};
|
||||
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
|
||||
|
||||
#[test]
|
||||
fn test_e2e_client_node() {
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// we should add these as consts
|
||||
let aad_initiator_outer = b"Test Data I Outer";
|
||||
let aad_initiator_inner = b"Test Data I Inner";
|
||||
let aad_responder = b"Test Data R";
|
||||
let ctx = b"Test Context";
|
||||
|
||||
// generate responder x25519 keys
|
||||
let responder_x25519_keypair = generate_keypair_x25519(&mut rng);
|
||||
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),
|
||||
&[
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::Shake128,
|
||||
HashFunction::Shake256,
|
||||
],
|
||||
&[1],
|
||||
&[SignatureScheme::Ed25519],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// OneWay - MlKem
|
||||
let psq_ciphersuite = CiphersuiteName::X25519_MLKEM768_X25519_AESGCM128_HKDFSHA256;
|
||||
|
||||
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)
|
||||
.build_responder_ciphersuite()
|
||||
.unwrap();
|
||||
|
||||
let mut responder = PrincipalBuilder::new(rand09::rng())
|
||||
.context(ctx)
|
||||
.outer_aad(aad_responder)
|
||||
.recent_keys_upper_bound(30)
|
||||
.build_responder(responder_ciphersuite)
|
||||
.unwrap();
|
||||
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
KEM::MlKem768,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (mut initiator, request_bytes) = KKTInitiator::generate_one_way_request(
|
||||
&mut rng,
|
||||
&ciphersuite,
|
||||
&responder_x25519_keypair.pk,
|
||||
&r_dir_hash_mlkem,
|
||||
1u8,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (response_bytes, _) = kkt_responder.process_request(&request_bytes).unwrap();
|
||||
|
||||
let (i_obtained_key, _) = initiator.process_response(&response_bytes).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
i_obtained_key,
|
||||
responder_mlkem_keypair.1.as_slice().as_slice(),
|
||||
);
|
||||
|
||||
let mlkem_key =
|
||||
libcrux_kem::MlKem768PublicKey::try_from(i_obtained_key.as_slice()).unwrap();
|
||||
|
||||
let initiator_psq_keys = generate_keypair_x25519(&mut rng);
|
||||
let initiator_cbuilder = CiphersuiteBuilder::new(psq_ciphersuite)
|
||||
.longterm_x25519_keys(&initiator_psq_keys)
|
||||
.peer_longterm_x25519_pk(&responder_x25519_keypair.pk)
|
||||
.peer_longterm_mlkem_pk(&mlkem_key);
|
||||
|
||||
let initiator_ciphersuite = initiator_cbuilder.build_initiator_ciphersuite().unwrap();
|
||||
|
||||
let mut msg_channel = vec![0u8; 8192];
|
||||
let mut payload_buf_responder = vec![0u8; 4096];
|
||||
let mut payload_buf_initiator = vec![0u8; 4096];
|
||||
|
||||
let mut initiator = PrincipalBuilder::new(rand09::rng())
|
||||
.outer_aad(aad_initiator_outer)
|
||||
.inner_aad(aad_initiator_inner)
|
||||
.context(ctx)
|
||||
.build_registration_initiator(initiator_ciphersuite)
|
||||
.unwrap();
|
||||
|
||||
// Send first message
|
||||
let registration_payload_initiator = b"Registration_init";
|
||||
let len_i = initiator
|
||||
.write_message(registration_payload_initiator, &mut msg_channel)
|
||||
.unwrap();
|
||||
|
||||
// Read first message
|
||||
let (len_r_deserialized, len_r_payload) = responder
|
||||
.read_message(&msg_channel, &mut payload_buf_responder)
|
||||
.unwrap();
|
||||
|
||||
// We read the same amount of data.
|
||||
assert_eq!(len_r_deserialized, len_i);
|
||||
assert_eq!(len_r_payload, registration_payload_initiator.len());
|
||||
assert_eq!(
|
||||
&payload_buf_responder[0..len_r_payload],
|
||||
registration_payload_initiator
|
||||
);
|
||||
|
||||
// Get the authenticator out here, so we can deserialize the session later.
|
||||
let Some(initiator_authenticator) = responder.initiator_authenticator() else {
|
||||
panic!("No initiator authenticator found")
|
||||
};
|
||||
|
||||
// Respond
|
||||
let registration_payload_responder = b"Registration_respond";
|
||||
let len_r = responder
|
||||
.write_message(registration_payload_responder, &mut msg_channel)
|
||||
.unwrap();
|
||||
|
||||
// Finalize on registration initiator
|
||||
let (len_i_deserialized, len_i_payload) = initiator
|
||||
.read_message(&msg_channel, &mut payload_buf_initiator)
|
||||
.unwrap();
|
||||
|
||||
// We read the same amount of data.
|
||||
assert_eq!(len_r, len_i_deserialized);
|
||||
assert_eq!(registration_payload_responder.len(), len_i_payload);
|
||||
assert_eq!(
|
||||
&payload_buf_initiator[0..len_i_payload],
|
||||
registration_payload_responder
|
||||
);
|
||||
|
||||
// Ready for transport mode
|
||||
assert!(initiator.is_handshake_finished());
|
||||
assert!(responder.is_handshake_finished());
|
||||
|
||||
let i_transport = initiator.into_session().unwrap();
|
||||
let r_transport = responder.into_session().unwrap();
|
||||
|
||||
// test serialization, deserialization
|
||||
let mut session_storage = vec![0u8; 4096];
|
||||
i_transport
|
||||
.serialize(
|
||||
&mut session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &Authenticator::Dh(initiator_psq_keys.pk),
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut i_transport = Session::deserialize(
|
||||
&session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &Authenticator::Dh(initiator_psq_keys.pk),
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
r_transport
|
||||
.serialize(
|
||||
&mut session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &initiator_authenticator,
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut r_transport = Session::deserialize(
|
||||
&session_storage,
|
||||
SessionBinding {
|
||||
initiator_authenticator: &initiator_authenticator,
|
||||
responder_ecdh_pk: &responder_x25519_keypair.pk,
|
||||
responder_pq_pk: Some(PQEncapsulationKey::MlKem(&mlkem_key)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut channel_i = i_transport.transport_channel().unwrap();
|
||||
let mut channel_r = r_transport.transport_channel().unwrap();
|
||||
|
||||
assert_eq!(channel_i.identifier(), channel_r.identifier());
|
||||
|
||||
let app_data_i = b"Derived session hey".as_slice();
|
||||
let app_data_r = b"Derived session ho".as_slice();
|
||||
|
||||
let len_i = channel_i
|
||||
.write_message(app_data_i, &mut msg_channel)
|
||||
.unwrap();
|
||||
|
||||
let (len_r_deserialized, len_r_payload) = channel_r
|
||||
.read_message(&msg_channel, &mut payload_buf_responder)
|
||||
.unwrap();
|
||||
|
||||
// We read the same amount of data.
|
||||
assert_eq!(len_r_deserialized, len_i);
|
||||
assert_eq!(len_r_payload, app_data_i.len());
|
||||
assert_eq!(&payload_buf_responder[0..len_r_payload], app_data_i);
|
||||
|
||||
let len_r = channel_r
|
||||
.write_message(app_data_r, &mut msg_channel)
|
||||
.unwrap();
|
||||
|
||||
let (len_i_deserialized, len_i_payload) = channel_i
|
||||
.read_message(&msg_channel, &mut payload_buf_initiator)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(len_r, len_i_deserialized);
|
||||
assert_eq!(app_data_r.len(), len_i_payload);
|
||||
assert_eq!(&payload_buf_initiator[0..len_i_payload], app_data_r);
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,16 @@ dashmap = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
|
||||
rand09 = { package = "rand", version = "0.9.2" }
|
||||
rand09 = { workspace = true }
|
||||
ouroboros = "0.18.5"
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-kkt = { path = "../nym-kkt" }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
|
||||
"test-utils",
|
||||
] }
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-psq = { workspace = true, features = ["test-utils"] }
|
||||
libcrux-kem = { workspace = true }
|
||||
tls_codec = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
|
||||
@@ -14,6 +14,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
/// Size of outer header (receiver_idx + counter) - always cleartext
|
||||
pub const OUTER_HEADER_SIZE: usize = OuterHeader::SIZE; // 12 bytes
|
||||
|
||||
// georgio: maybe remove this?
|
||||
/// Size of inner prefix (proto + reserved) - cleartext or encrypted depending on mode
|
||||
const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3)
|
||||
|
||||
@@ -38,6 +39,7 @@ const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3)
|
||||
/// 3. **No PSK persistence**: PSK handles are not stored/reused across sessions.
|
||||
/// Each connection performs fresh KKT+PSQ handshake.
|
||||
///
|
||||
// noiserm
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct OuterAeadKey {
|
||||
key: [u8; 32],
|
||||
@@ -52,7 +54,7 @@ impl OuterAeadKey {
|
||||
/// Uses Blake3 KDF with domain separation to avoid key reuse
|
||||
/// between the outer AEAD layer and the inner Noise layer.
|
||||
pub fn from_psk(psk: &[u8; 32]) -> Self {
|
||||
let key = nym_crypto::kdf::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]);
|
||||
let key = nym_crypto::hkdf::blake3::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]);
|
||||
Self { key }
|
||||
}
|
||||
|
||||
@@ -70,6 +72,7 @@ impl std::fmt::Debug for OuterAeadKey {
|
||||
}
|
||||
}
|
||||
|
||||
// noiserm
|
||||
/// Build 12-byte nonce from 8-byte counter (zero-padded).
|
||||
///
|
||||
/// Format: counter (8 bytes LE) || 0x00000000 (4 bytes)
|
||||
@@ -177,6 +180,7 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<L
|
||||
trailer,
|
||||
})
|
||||
}
|
||||
// noiserm (potentially)
|
||||
Some(key) => {
|
||||
// AEAD decryption mode
|
||||
// AAD is the outer header (12 bytes)
|
||||
@@ -224,6 +228,7 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<L
|
||||
}
|
||||
}
|
||||
|
||||
// georgio: start with no outer_key
|
||||
/// Serializes an LpPacket into the provided BytesMut buffer.
|
||||
///
|
||||
/// ## Unified Packet Format
|
||||
@@ -318,7 +323,7 @@ mod tests {
|
||||
use super::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
|
||||
// Keep necessary imports
|
||||
use crate::LpError;
|
||||
use crate::message::{EncryptedDataPayload, HandshakeData, LpMessage, MessageType};
|
||||
use crate::message::{EncryptedDataPayload, LpMessage, MessageType, PSQRequestData};
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use bytes::BytesMut;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
@@ -373,7 +378,7 @@ mod tests {
|
||||
receiver_idx: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::Handshake(HandshakeData(payload.clone())),
|
||||
message: LpMessage::PSQRequest(PSQRequestData(payload.clone())),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -390,8 +395,8 @@ mod tests {
|
||||
|
||||
// Verify message type and data
|
||||
match decoded.message {
|
||||
LpMessage::Handshake(decoded_payload) => {
|
||||
assert_eq!(decoded_payload, HandshakeData(payload));
|
||||
LpMessage::PSQRequest(decoded_payload) => {
|
||||
assert_eq!(decoded_payload, PSQRequestData(payload));
|
||||
}
|
||||
_ => panic!("Expected Handshake message"),
|
||||
}
|
||||
@@ -1050,7 +1055,7 @@ mod tests {
|
||||
receiver_idx: 99999,
|
||||
counter: 2,
|
||||
},
|
||||
message: LpMessage::Handshake(HandshakeData(handshake_data.clone())),
|
||||
message: LpMessage::PSQRequest(PSQRequestData(handshake_data.clone())),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -1060,7 +1065,7 @@ mod tests {
|
||||
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
|
||||
|
||||
match decoded.message {
|
||||
LpMessage::Handshake(data) => {
|
||||
LpMessage::PSQResponse(data) => {
|
||||
assert_eq!(data.0, handshake_data);
|
||||
}
|
||||
_ => panic!("Expected Handshake message"),
|
||||
|
||||
@@ -18,7 +18,7 @@ pub const DEFAULT_PSK_TTL_SECS: u64 = 3600;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LpConfig {
|
||||
/// KEM algorithm for PSQ key encapsulation.
|
||||
/// X25519 = classical (testing), MlKem768 = PQ, XWing = hybrid.
|
||||
/// Supported KEMs: MlKem768, McEliece
|
||||
#[serde(with = "kem_serde")]
|
||||
pub kem_algorithm: KEM,
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct LpConfig {
|
||||
impl Default for LpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kem_algorithm: KEM::X25519,
|
||||
kem_algorithm: KEM::MlKem768,
|
||||
psk_ttl_secs: DEFAULT_PSK_TTL_SECS,
|
||||
enable_kkt: true,
|
||||
}
|
||||
@@ -55,10 +55,10 @@ mod kem_serde {
|
||||
S: Serializer,
|
||||
{
|
||||
match kem {
|
||||
KEM::X25519 => "X25519",
|
||||
KEM::MlKem768 => "MlKem768",
|
||||
KEM::XWing => "XWing",
|
||||
KEM::McEliece => "McEliece",
|
||||
KEM::X25519 => return Err(serde::ser::Error::custom("Unsupported KEM: X25519")),
|
||||
KEM::XWing => return Err(serde::ser::Error::custom("Unsupported KEM: XWing")),
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
@@ -69,10 +69,10 @@ mod kem_serde {
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"X25519" => Ok(KEM::X25519),
|
||||
"MlKem768" => Ok(KEM::MlKem768),
|
||||
"XWing" => Ok(KEM::XWing),
|
||||
"McEliece" => Ok(KEM::McEliece),
|
||||
"X25519" => Err(serde::de::Error::custom("Unsupported KEM: X25519")),
|
||||
"XWing" => Err(serde::de::Error::custom("Unsupported KEM: XWing")),
|
||||
_ => Err(serde::de::Error::custom(format!("Unknown KEM: {}", s))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
||||
use libcrux_psq::handshake::builders::BuilderError;
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_kkt::ciphersuite::{HashFunction, KEM};
|
||||
use nym_kkt::{
|
||||
ciphersuite::{HashFunction, KEM},
|
||||
error::KKTError,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -11,15 +15,21 @@ pub enum LpError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
// noiserm
|
||||
#[error("Snow Error: {0}")]
|
||||
SnowKeyError(#[from] snow::Error),
|
||||
|
||||
// noiserm
|
||||
#[error("Snow Pattern Error: {0}")]
|
||||
SnowPatternError(String),
|
||||
|
||||
// noiserm
|
||||
#[error("Noise Protocol Error: {0}")]
|
||||
NoiseError(#[from] NoiseError),
|
||||
|
||||
#[error("PSQ Error: {0}")]
|
||||
PSQError(String),
|
||||
|
||||
#[error("Replay detected: {0}")]
|
||||
Replay(#[from] ReplayError),
|
||||
|
||||
@@ -92,8 +102,8 @@ pub enum LpError {
|
||||
#[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")]
|
||||
IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 },
|
||||
|
||||
#[error("attempted to create an LP responder without providing a valid KEM key")]
|
||||
ResponderWithMissingKEMKey,
|
||||
#[error("attempted to create an LP responder without providing a valid KEM key for {kem} ")]
|
||||
ResponderWithMissingKEMKey { kem: KEM },
|
||||
|
||||
#[error(
|
||||
"there are no known digests for remote's KEM key with {kem} KEM and {hash_function} hash function"
|
||||
@@ -103,3 +113,9 @@ pub enum LpError {
|
||||
hash_function: HashFunction,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<KKTError> for LpError {
|
||||
fn from(err: KKTError) -> Self {
|
||||
LpError::KKTError(format!("KKT Error: {:?}", err))
|
||||
}
|
||||
}
|
||||
|
||||
+211
-192
@@ -2,42 +2,49 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod codec;
|
||||
pub mod config;
|
||||
// georgio: config does not seem to be used anywhere
|
||||
// pub mod config;
|
||||
// pub use config::LpConfig;
|
||||
|
||||
pub mod error;
|
||||
// georgio: no use for this
|
||||
// pub mod kkt_orchestrator;
|
||||
pub mod message;
|
||||
pub mod noise_protocol;
|
||||
pub mod packet;
|
||||
pub mod peer;
|
||||
pub mod psk;
|
||||
pub mod psq;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
mod session_integration;
|
||||
pub mod session_manager;
|
||||
pub mod state_machine;
|
||||
|
||||
pub use config::LpConfig;
|
||||
pub use error::LpError;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::{LpSession, generate_fresh_salt};
|
||||
pub use session::LpSession;
|
||||
pub use session_manager::SessionManager;
|
||||
pub use state_machine::LpStateMachine;
|
||||
|
||||
// noiserm
|
||||
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
pub const NOISE_PSK_INDEX: u8 = 3;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
let (init, resp) = crate::peer::mock_peers();
|
||||
pub fn kem_list() -> Vec<nym_kkt::ciphersuite::KEM> {
|
||||
use nym_kkt::ciphersuite::KEM;
|
||||
vec![KEM::MlKem768, KEM::McEliece, KEM::X25519]
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn sessions_for_tests<'a>(kem: nym_kkt::ciphersuite::KEM) -> (LpSession<'a>, LpSession<'a>) {
|
||||
let (init, resp) = crate::peer::mock_peers(kem);
|
||||
|
||||
// Use a fixed receiver_index for deterministic tests
|
||||
let receiver_index: u32 = 12345;
|
||||
|
||||
// Use consistent salt for deterministic tests
|
||||
let salt = [1u8; 32];
|
||||
|
||||
let initiator_session =
|
||||
LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt)
|
||||
.expect("Test session creation failed");
|
||||
@@ -53,8 +60,9 @@ mod tests {
|
||||
use crate::message::LpMessage;
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::session_manager::SessionManager;
|
||||
use crate::{LpError, sessions_for_tests};
|
||||
use crate::{LpError, kem_list, sessions_for_tests};
|
||||
use bytes::BytesMut;
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, HashFunction, SignatureScheme};
|
||||
|
||||
// Import the new standalone functions
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
@@ -62,222 +70,233 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_integration() {
|
||||
// Create session
|
||||
let session = sessions_for_tests().0;
|
||||
for kem in kem_list() {
|
||||
// Create session
|
||||
let session = sessions_for_tests(kem).0;
|
||||
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42, // Matches session's sending_index assumption for this test
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42, // Matches session's sending_index assumption for this test
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
|
||||
// Serialize packet
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
|
||||
// Parse packet
|
||||
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
|
||||
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet1.header.counter)
|
||||
.expect("Initial packet failed replay check");
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet1.header.counter)
|
||||
.expect("Initial packet failed replay check");
|
||||
|
||||
// Mark received (simulating successful processing)
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet1.header.counter)
|
||||
.expect("Failed to mark initial packet received");
|
||||
// Mark received (simulating successful processing)
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet1.header.counter)
|
||||
.expect("Failed to mark initial packet received");
|
||||
|
||||
// === Packet 2 (Counter 0 - Replay, should fail check) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 0, // Same counter as before (replay)
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
// === Packet 2 (Counter 0 - Replay, should fail check) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 0, // Same counter as before (replay)
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
|
||||
// Serialize packet
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
|
||||
// Parse packet
|
||||
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
|
||||
|
||||
// Perform replay check (should fail)
|
||||
let replay_result = session.receiving_counter_quick_check(parsed_packet2.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
// Perform replay check (should fail)
|
||||
let replay_result =
|
||||
session.receiving_counter_quick_check(parsed_packet2.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
}
|
||||
e => panic!("Expected replay error, got {:?}", e),
|
||||
}
|
||||
e => panic!("Expected replay error, got {:?}", e),
|
||||
// Do not mark received as it failed validation
|
||||
|
||||
// === Packet 3 (Counter 1 - Should succeed) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 1, // Incremented counter
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
|
||||
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet3.header.counter)
|
||||
.expect("Packet 3 failed replay check");
|
||||
|
||||
// Mark received
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet3.header.counter)
|
||||
.expect("Failed to mark packet 3 received");
|
||||
|
||||
// Verify validator state directly on the session
|
||||
let state = session.current_packet_cnt();
|
||||
assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2)
|
||||
assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3)
|
||||
}
|
||||
// Do not mark received as it failed validation
|
||||
|
||||
// === Packet 3 (Counter 1 - Should succeed) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 1, // Incremented counter
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
|
||||
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet3.header.counter)
|
||||
.expect("Packet 3 failed replay check");
|
||||
|
||||
// Mark received
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet3.header.counter)
|
||||
.expect("Failed to mark packet 3 received");
|
||||
|
||||
// Verify validator state directly on the session
|
||||
let state = session.current_packet_cnt();
|
||||
assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2)
|
||||
assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_integration() {
|
||||
// Create session manager
|
||||
let local_manager = SessionManager::new();
|
||||
let remote_manager = SessionManager::new();
|
||||
let mut local_manager = SessionManager::new();
|
||||
let mut remote_manager = SessionManager::new();
|
||||
|
||||
// Generate Ed25519 keypairs for PSQ authentication
|
||||
let (init, resp) = mock_peers();
|
||||
for kem in kem_list() {
|
||||
// Generate Ed25519 keypairs for PSQ authentication
|
||||
let (init, resp) = mock_peers(kem);
|
||||
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 54321;
|
||||
let mut ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
);
|
||||
|
||||
// Test salt
|
||||
let salt = [46u8; 32];
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 54321;
|
||||
|
||||
// Create a session via manager
|
||||
let _ = local_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init.clone(),
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
// Test salt
|
||||
let salt = [46u8; 32];
|
||||
|
||||
let _ = remote_manager
|
||||
.create_session_state_machine(receiver_index, false, resp, init.as_remote(), &salt)
|
||||
.unwrap();
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
// Create a session via manager
|
||||
let _ = local_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init.clone(),
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Serialize
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
|
||||
let _ = remote_manager
|
||||
.create_session_state_machine(receiver_index, false, resp, init.as_remote(), &salt)
|
||||
.unwrap();
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Parse
|
||||
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
|
||||
// Serialize
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
|
||||
|
||||
// Process via SessionManager method (which should handle checks + marking)
|
||||
// NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet`
|
||||
// that encapsulates parse -> check -> process_noise -> mark.
|
||||
// For now, we simulate the steps using the retrieved session.
|
||||
// Parse
|
||||
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
|
||||
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(receiver_index, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 mark failed");
|
||||
// Process via SessionManager method (which should handle checks + marking)
|
||||
// NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet`
|
||||
// that encapsulates parse -> check -> process_noise -> mark.
|
||||
// For now, we simulate the steps using the retrieved session.
|
||||
|
||||
// === Packet 2 (Counter 1 - Should succeed on same session) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 1,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(receiver_index, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 mark failed");
|
||||
|
||||
// Serialize
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
|
||||
// === Packet 2 (Counter 1 - Should succeed on same session) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 1,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Parse
|
||||
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
|
||||
// Serialize
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
|
||||
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(receiver_index, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 mark failed");
|
||||
// Parse
|
||||
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
|
||||
|
||||
// === Packet 3 (Counter 0 - Replay, should fail check) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 0, // Replay of first packet
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(receiver_index, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 mark failed");
|
||||
|
||||
// Serialize
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
|
||||
// === Packet 3 (Counter 0 - Replay, should fail check) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: receiver_index,
|
||||
counter: 0, // Replay of first packet
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Parse
|
||||
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
|
||||
// Serialize
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
|
||||
|
||||
// Perform replay check (should fail)
|
||||
let replay_result = local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
// Parse
|
||||
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
|
||||
|
||||
// Perform replay check (should fail)
|
||||
let replay_result = local_manager
|
||||
.receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
}
|
||||
e => panic!("Expected replay error for packet 3, got {:?}", e),
|
||||
}
|
||||
e => panic!("Expected replay error for packet 3, got {:?}", e),
|
||||
// Do not mark received
|
||||
}
|
||||
// Do not mark received
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
use std::fmt::{self, Display, write};
|
||||
|
||||
/// Data structure for the ClientHello message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -20,11 +20,13 @@ pub struct ClientHelloData {
|
||||
pub client_lp_public_key: x25519::PublicKey,
|
||||
/// Client's Ed25519 public key (32 bytes) - for PSQ authentication
|
||||
pub client_ed25519_public_key: ed25519::PublicKey,
|
||||
// noiserm
|
||||
/// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce)
|
||||
pub salt: [u8; 32],
|
||||
}
|
||||
|
||||
impl ClientHelloData {
|
||||
// noiserm (remove 32 bytes for salt)
|
||||
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
|
||||
pub const LEN: usize = 100;
|
||||
|
||||
@@ -41,6 +43,7 @@ impl ClientHelloData {
|
||||
}
|
||||
}
|
||||
|
||||
// noiserm
|
||||
/// Generates a new ClientHelloData with fresh salt.
|
||||
///
|
||||
/// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce
|
||||
@@ -69,7 +72,7 @@ impl ClientHelloData {
|
||||
salt,
|
||||
}
|
||||
}
|
||||
|
||||
// noiserm
|
||||
/// Extracts the timestamp from the salt.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -84,6 +87,7 @@ impl ClientHelloData {
|
||||
dst.put_u32_le(self.receiver_index);
|
||||
dst.put_slice(self.client_lp_public_key.as_bytes());
|
||||
dst.put_slice(self.client_ed25519_public_key.as_bytes());
|
||||
// noiserm
|
||||
dst.put_slice(&self.salt);
|
||||
}
|
||||
|
||||
@@ -107,6 +111,7 @@ impl ClientHelloData {
|
||||
client_ed25519_public_key: ed25519::PublicKey::from_byte_array(
|
||||
client_ed25519_public_key_bytes,
|
||||
)?,
|
||||
// noiserm
|
||||
salt: b[68..].try_into().unwrap(),
|
||||
})
|
||||
}
|
||||
@@ -133,6 +138,8 @@ pub enum MessageType {
|
||||
Ack = 0x0008,
|
||||
/// Subsession request - client initiates subsession creation
|
||||
SubsessionRequest = 0x0009,
|
||||
|
||||
// georgio: this should be the psq msg
|
||||
/// Subsession KK1 - first message of Noise KK handshake
|
||||
SubsessionKK1 = 0x000A,
|
||||
/// Subsession KK2 - second message of Noise KK handshake
|
||||
@@ -223,6 +230,42 @@ impl KKTResponseData {
|
||||
}
|
||||
}
|
||||
|
||||
/// PSQ request frame data (serialized bytes)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PSQRequestData(pub Vec<u8>);
|
||||
|
||||
impl PSQRequestData {
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.0);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(PSQRequestData(bytes.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
/// PSQ response frame data (serialized bytes)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PSQResponseData(pub Vec<u8>);
|
||||
|
||||
impl PSQResponseData {
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.0);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
Ok(PSQResponseData(bytes.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Packet forwarding request with embedded inner LP packet
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForwardPacketData {
|
||||
@@ -312,6 +355,7 @@ impl ForwardPacketData {
|
||||
}
|
||||
}
|
||||
|
||||
// georgio: swap with psq
|
||||
/// Subsession KK1 message - first message of Noise KK handshake
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubsessionKK1Data {
|
||||
@@ -392,7 +436,8 @@ impl SubsessionReadyData {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LpMessage {
|
||||
Busy,
|
||||
Handshake(HandshakeData),
|
||||
PSQRequest(PSQRequestData),
|
||||
PSQResponse(PSQResponseData),
|
||||
EncryptedData(EncryptedDataPayload),
|
||||
ClientHello(ClientHelloData),
|
||||
KKTRequest(KKTRequestData),
|
||||
@@ -402,6 +447,7 @@ pub enum LpMessage {
|
||||
Collision,
|
||||
/// Acknowledgment - gateway confirms receipt of message
|
||||
Ack,
|
||||
// georgio: this should become psq stuff
|
||||
/// Subsession request - client initiates subsession creation (empty, signal only)
|
||||
SubsessionRequest,
|
||||
/// Subsession KK1 - first message of Noise KK handshake
|
||||
@@ -418,7 +464,6 @@ impl Display for LpMessage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LpMessage::Busy => write!(f, "Busy"),
|
||||
LpMessage::Handshake(_) => write!(f, "Handshake"),
|
||||
LpMessage::EncryptedData(_) => write!(f, "EncryptedData"),
|
||||
LpMessage::ClientHello(_) => write!(f, "ClientHello"),
|
||||
LpMessage::KKTRequest(_) => write!(f, "KKTRequest"),
|
||||
@@ -431,34 +476,16 @@ impl Display for LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
|
||||
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
|
||||
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
|
||||
LpMessage::PSQRequest(_) => write!(f, "PSQRequest"),
|
||||
LpMessage::PSQResponse(_) => write!(f, "PSQResponse"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LpMessage {
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
match self {
|
||||
LpMessage::Busy => &[],
|
||||
LpMessage::Handshake(payload) => payload.0.as_slice(),
|
||||
LpMessage::EncryptedData(payload) => payload.0.as_slice(),
|
||||
LpMessage::ClientHello(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::KKTRequest(payload) => payload.0.as_slice(),
|
||||
LpMessage::KKTResponse(payload) => payload.0.as_slice(),
|
||||
LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::Collision => &[],
|
||||
LpMessage::Ack => &[],
|
||||
LpMessage::SubsessionRequest => &[],
|
||||
LpMessage::SubsessionKK1(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionAbort => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
LpMessage::Busy => true,
|
||||
LpMessage::Handshake(payload) => payload.0.is_empty(),
|
||||
LpMessage::EncryptedData(payload) => payload.0.is_empty(),
|
||||
LpMessage::ClientHello(_) => false, // Always has data
|
||||
LpMessage::KKTRequest(payload) => payload.0.is_empty(),
|
||||
@@ -471,13 +498,16 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => false, // Always has payload
|
||||
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
|
||||
LpMessage::SubsessionAbort => true, // Empty signal
|
||||
LpMessage::PSQRequest(payload) => true, // Always had data (?)
|
||||
LpMessage::PSQResponse(payload) => true, // Always had data (?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
LpMessage::Busy => 0,
|
||||
LpMessage::Handshake(payload) => payload.len(),
|
||||
LpMessage::PSQRequest(payload) => payload.len(),
|
||||
LpMessage::PSQResponse(payload) => payload.len(),
|
||||
LpMessage::EncryptedData(payload) => payload.len(),
|
||||
LpMessage::ClientHello(payload) => payload.len(),
|
||||
LpMessage::KKTRequest(payload) => payload.len(),
|
||||
@@ -496,7 +526,8 @@ impl LpMessage {
|
||||
pub fn typ(&self) -> MessageType {
|
||||
match self {
|
||||
LpMessage::Busy => MessageType::Busy,
|
||||
LpMessage::Handshake(_) => MessageType::Handshake,
|
||||
LpMessage::PSQRequest(_) => todo!(),
|
||||
LpMessage::PSQResponse(_) => todo!(),
|
||||
LpMessage::EncryptedData(_) => MessageType::EncryptedData,
|
||||
LpMessage::ClientHello(_) => MessageType::ClientHello,
|
||||
LpMessage::KKTRequest(_) => MessageType::KKTRequest,
|
||||
@@ -515,7 +546,8 @@ impl LpMessage {
|
||||
pub fn encode_content(&self, dst: &mut BytesMut) {
|
||||
match self {
|
||||
LpMessage::Busy => { /* No content */ }
|
||||
LpMessage::Handshake(payload) => payload.encode(dst),
|
||||
LpMessage::PSQRequest(payload) => payload.encode(dst),
|
||||
LpMessage::PSQResponse(payload) => payload.encode(dst),
|
||||
LpMessage::EncryptedData(payload) => payload.encode(dst),
|
||||
LpMessage::ClientHello(data) => data.encode(dst),
|
||||
LpMessage::KKTRequest(payload) => payload.encode(dst),
|
||||
@@ -541,7 +573,7 @@ impl LpMessage {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::Busy)
|
||||
}
|
||||
MessageType::Handshake => Ok(LpMessage::Handshake(HandshakeData::decode(content)?)),
|
||||
MessageType::Handshake => todo!(),
|
||||
MessageType::EncryptedData => Ok(LpMessage::EncryptedData(
|
||||
EncryptedDataPayload::decode(content)?,
|
||||
)),
|
||||
|
||||
+108
-50
@@ -1,37 +1,59 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use libcrux_kem::{MlKem768PrivateKey, MlKem768PublicKey};
|
||||
use libcrux_psq::handshake::Responder;
|
||||
use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||
use nym_kkt::ciphersuite::{
|
||||
Ciphersuite, DecapsulationKey, EncapsulationKey, KEM, KEMKeyDigests, KemKeyPair,
|
||||
SignatureScheme, SigningKeyDigests,
|
||||
};
|
||||
use rand::rngs::ThreadRng;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::psq::build_responder;
|
||||
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct LpLocalPeer {
|
||||
pub(crate) ciphersuite: Ciphersuite,
|
||||
|
||||
/// Local Ed25519 keys for PSQ authentication
|
||||
pub(crate) ed25519: Arc<ed25519::KeyPair>,
|
||||
|
||||
/// Local x25519 keys (Noise static key)
|
||||
pub(crate) x25519: Arc<x25519::KeyPair>,
|
||||
pub(crate) x25519: Arc<DHKeyPair>,
|
||||
|
||||
/// Local KEM key used for PSQ
|
||||
pub(crate) kem_psq: Option<Arc<x25519::KeyPair>>,
|
||||
/// Local KEM keys used for PSQ
|
||||
pub(crate) kem_keypairs: HashMap<KEM, Arc<KemKeyPair>>,
|
||||
}
|
||||
|
||||
impl LpLocalPeer {
|
||||
pub fn new(ed25519: Arc<ed25519::KeyPair>, x25519: Arc<x25519::KeyPair>) -> Self {
|
||||
pub fn new(
|
||||
ciphersuite: Ciphersuite,
|
||||
ed25519: Arc<ed25519::KeyPair>,
|
||||
x25519: Arc<x25519::KeyPair>,
|
||||
) -> 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,
|
||||
kem_psq: None,
|
||||
x25519: Arc::new(initiator_x25519_keypair),
|
||||
kem_keypairs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
|
||||
self.kem_psq = Some(key);
|
||||
pub fn with_kem_keypair(mut self, keypair: Arc<KemKeyPair>) -> Self {
|
||||
let kem = keypair.kem();
|
||||
self.kem_keypairs.insert(kem, keypair);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -39,47 +61,48 @@ impl LpLocalPeer {
|
||||
&self.ed25519
|
||||
}
|
||||
|
||||
pub fn x25519(&self) -> &Arc<x25519::KeyPair> {
|
||||
pub fn x25519(&self) -> &Arc<DHKeyPair> {
|
||||
&self.x25519
|
||||
}
|
||||
|
||||
pub fn kem_key(&self, kem: KEM) -> Option<&Arc<KemKeyPair>> {
|
||||
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 {
|
||||
let expected_kem_key_digests = match &self.kem_psq {
|
||||
None => HashMap::new(),
|
||||
Some(kem_keys) => {
|
||||
let mut digests = HashMap::new();
|
||||
digests.insert(
|
||||
KEM::X25519,
|
||||
nym_kkt::key_utils::produce_key_digests(kem_keys.public_key().as_bytes()),
|
||||
);
|
||||
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();
|
||||
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.public_key(),
|
||||
x25519_public: self.x25519.pk,
|
||||
expected_kem_key_digests,
|
||||
expected_signing_key_digests,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is only exposed in tests as ideally we should be storing the proper types to begin with
|
||||
#[cfg(test)]
|
||||
pub fn encapsulate_kem_key(&self) -> Option<nym_kkt::ciphersuite::EncapsulationKey<'_>> {
|
||||
let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes();
|
||||
let libcrux_pk =
|
||||
libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?;
|
||||
|
||||
Some(nym_kkt::ciphersuite::EncapsulationKey::X25519(libcrux_pk))
|
||||
impl Debug for LpLocalPeer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LpLocalPeer")
|
||||
.field("ciphersuite", &self.ciphersuite)
|
||||
.field("ed25519", &self.ed25519)
|
||||
.field("x25519", &self.x25519.pk)
|
||||
.field("kem_keypairs", &self.kem_keypairs)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +114,7 @@ pub struct LpRemotePeer {
|
||||
pub(crate) ed25519_public: ed25519::PublicKey,
|
||||
|
||||
/// Remote X25519 public key (Noise static key)
|
||||
pub(crate) x25519_public: x25519::PublicKey,
|
||||
pub(crate) x25519_public: DHPublicKey,
|
||||
|
||||
/// Expected digests of the remote's KEM key
|
||||
pub(crate) expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
|
||||
@@ -102,9 +125,11 @@ pub struct LpRemotePeer {
|
||||
|
||||
impl LpRemotePeer {
|
||||
pub fn new(ed25519_public: ed25519::PublicKey, x25519_public: x25519::PublicKey) -> Self {
|
||||
// TODO: make nicer conversion (without cloning) + error handling
|
||||
let responder_x25519_public_key = DHPublicKey::from_bytes(x25519_public.as_bytes());
|
||||
LpRemotePeer {
|
||||
ed25519_public,
|
||||
x25519_public,
|
||||
x25519_public: responder_x25519_public_key,
|
||||
expected_kem_key_digests: Default::default(),
|
||||
expected_signing_key_digests: Default::default(),
|
||||
}
|
||||
@@ -114,8 +139,8 @@ impl LpRemotePeer {
|
||||
self.ed25519_public
|
||||
}
|
||||
|
||||
pub fn x25519(&self) -> x25519::PublicKey {
|
||||
self.x25519_public
|
||||
pub fn x25519(&self) -> &DHPublicKey {
|
||||
&self.x25519_public
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -131,29 +156,62 @@ impl LpRemotePeer {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn mock_peer() -> LpLocalPeer {
|
||||
pub fn mock_peer(kem: KEM) -> LpLocalPeer {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
random_peer(&mut rng)
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
kem,
|
||||
nym_kkt::ciphersuite::HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
nym_kkt::ciphersuite::HashLength::Default,
|
||||
);
|
||||
|
||||
random_peer(&mut rng, ciphersuite)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
|
||||
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
||||
let x25519 = Arc::new(ed25519.to_x25519());
|
||||
let kem_psq = Some(x25519.clone());
|
||||
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};
|
||||
|
||||
LpLocalPeer {
|
||||
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
||||
|
||||
let mut sk = [0u8; 32];
|
||||
rng.fill_bytes(&mut sk);
|
||||
|
||||
// clamp
|
||||
sk[0] &= 248u8;
|
||||
sk[31] &= 127u8;
|
||||
sk[31] |= 64u8;
|
||||
|
||||
let x25519 = Arc::new(DHKeyPair::from(DHPrivateKey::from_bytes(&sk).unwrap()));
|
||||
|
||||
let default_peer = LpLocalPeer {
|
||||
ciphersuite: Arc::new(ciphersuite),
|
||||
ed25519,
|
||||
x25519,
|
||||
kem_psq,
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
|
||||
(random_peer(&mut rng), random_peer(&mut rng))
|
||||
pub fn mock_peers(kem: KEM) -> (LpLocalPeer, LpLocalPeer) {
|
||||
println!("KEM: {:?}", kem);
|
||||
(mock_peer(kem), mock_peer(kem))
|
||||
}
|
||||
|
||||
+63
-80
@@ -47,11 +47,12 @@
|
||||
//! - **No cleanup needed**: No state was mutated
|
||||
|
||||
use crate::LpError;
|
||||
use libcrux_psq::handshake::types::{DHPrivateKey, DHPublicKey};
|
||||
use libcrux_psq::v1::cred::{Authenticator, Ed25519};
|
||||
use libcrux_psq::v1::impls::X25519 as PsqX25519;
|
||||
use libcrux_psq::v1::psk_registration::{Initiator, InitiatorMsg, Responder};
|
||||
use libcrux_psq::v1::traits::{Ciphertext as PsqCiphertext, PSQ};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
use std::time::Duration;
|
||||
use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait};
|
||||
@@ -136,13 +137,15 @@ pub struct PsqResponderResult {
|
||||
/// // Send ciphertext to gateway
|
||||
/// ```
|
||||
pub fn derive_psk_with_psq_initiator(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
local_x25519_private: &DHPrivateKey,
|
||||
remote_x25519_public: &DHPublicKey,
|
||||
remote_kem_public: &EncapsulationKey,
|
||||
salt: &[u8; 32],
|
||||
) -> Result<([u8; 32], Vec<u8>), LpError> {
|
||||
// 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);
|
||||
|
||||
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
|
||||
|
||||
// Step 2: PSQ encapsulation for post-quantum security
|
||||
// KEM algorithm migration path:
|
||||
@@ -171,7 +174,7 @@ pub fn derive_psk_with_psq_initiator(
|
||||
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
// Serialize ciphertext using TLS encoding for transport
|
||||
let ct_bytes = ciphertext
|
||||
@@ -219,14 +222,16 @@ pub fn derive_psk_with_psq_initiator(
|
||||
/// )?;
|
||||
/// ```
|
||||
pub fn derive_psk_with_psq_responder(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
local_x25519_private: &DHPrivateKey,
|
||||
remote_x25519_public: &DHPublicKey,
|
||||
local_kem_keypair: (&DecapsulationKey, &EncapsulationKey),
|
||||
ciphertext: &[u8],
|
||||
salt: &[u8; 32],
|
||||
) -> Result<[u8; 32], LpError> {
|
||||
// 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);
|
||||
|
||||
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
|
||||
|
||||
// Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey
|
||||
let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) {
|
||||
@@ -252,7 +257,7 @@ pub fn derive_psk_with_psq_responder(
|
||||
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
Ok(final_psk)
|
||||
}
|
||||
@@ -279,8 +284,8 @@ pub fn derive_psk_with_psq_responder(
|
||||
/// # Returns
|
||||
/// `PsqInitiatorResult` containing PSK, payload, and raw PQ shared secret
|
||||
pub fn psq_initiator_create_message(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
local_x25519_private: &DHPrivateKey,
|
||||
remote_x25519_public: &DHPublicKey,
|
||||
remote_kem_public: &EncapsulationKey,
|
||||
client_ed25519_sk: &ed25519::PrivateKey,
|
||||
client_ed25519_pk: &ed25519::PublicKey,
|
||||
@@ -288,7 +293,9 @@ pub fn psq_initiator_create_message(
|
||||
session_context: &[u8],
|
||||
) -> Result<PsqInitiatorResult, LpError> {
|
||||
// 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);
|
||||
|
||||
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
|
||||
|
||||
// Step 2: PSQ v1 with Ed25519 authentication
|
||||
// Extract X25519 KEM key from EncapsulationKey
|
||||
@@ -338,7 +345,7 @@ pub fn psq_initiator_create_message(
|
||||
combined.extend_from_slice(psq_psk); // psq_psk is already a &[u8; 32]
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
// Serialize InitiatorMsg with TLS encoding for transport
|
||||
let msg_bytes = initiator_msg
|
||||
@@ -374,8 +381,8 @@ pub fn psq_initiator_create_message(
|
||||
/// # Returns
|
||||
/// `PsqResponderResult` containing PSK, PSK handle, and raw PQ shared secret
|
||||
pub fn psq_responder_process_message(
|
||||
local_x25519_private: &x25519::PrivateKey,
|
||||
remote_x25519_public: &x25519::PublicKey,
|
||||
local_x25519_private: &DHPrivateKey,
|
||||
remote_x25519_public: &DHPublicKey,
|
||||
local_kem_keypair: (&DecapsulationKey, &EncapsulationKey),
|
||||
initiator_ed25519_pk: &ed25519::PublicKey,
|
||||
psq_payload: &[u8],
|
||||
@@ -383,7 +390,8 @@ pub fn psq_responder_process_message(
|
||||
session_context: &[u8],
|
||||
) -> Result<PsqResponderResult, LpError> {
|
||||
// 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);
|
||||
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
|
||||
|
||||
// Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey
|
||||
let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) {
|
||||
@@ -447,7 +455,7 @@ pub fn psq_responder_process_message(
|
||||
combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
|
||||
combined.extend_from_slice(salt);
|
||||
|
||||
let final_psk = nym_crypto::kdf::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
|
||||
|
||||
// Step 7: Serialize ResponderMsg (contains ctxt_B - encrypted PSK handle)
|
||||
use tls_codec::Serialize;
|
||||
@@ -482,7 +490,7 @@ pub fn psq_responder_process_message(
|
||||
/// # Returns
|
||||
/// 32-byte PSK for Noise KKpsk0 handshake
|
||||
pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64) -> [u8; 32] {
|
||||
nym_crypto::kdf::derive_key_blake3(
|
||||
nym_crypto::hkdf::blake3::derive_key_blake3(
|
||||
SUBSESSION_PSK_CONTEXT,
|
||||
pq_shared_secret,
|
||||
&subsession_index.to_le_bytes(),
|
||||
@@ -492,10 +500,10 @@ pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::thread_rng;
|
||||
use libcrux_psq::handshake::types::DHKeyPair;
|
||||
|
||||
fn generate_x25519_keypair() -> x25519::KeyPair {
|
||||
x25519::KeyPair::new(&mut thread_rng())
|
||||
fn generate_x25519_keypair() -> DHKeyPair {
|
||||
DHKeyPair::new(&mut rand09::rng())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -510,18 +518,13 @@ mod tests {
|
||||
let dec_key = DecapsulationKey::X25519(_kem_sk);
|
||||
|
||||
// Client derives PSK
|
||||
let (client_psk, ciphertext) = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
let (client_psk, ciphertext) =
|
||||
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt).unwrap();
|
||||
|
||||
// Gateway derives PSK from their perspective
|
||||
let gateway_psk = derive_psk_with_psq_responder(
|
||||
keypair_2.private_key(),
|
||||
keypair_1.public_key(),
|
||||
keypair_2.sk(),
|
||||
&keypair_1.pk,
|
||||
(&dec_key, &enc_key),
|
||||
&ciphertext,
|
||||
&salt,
|
||||
@@ -545,20 +548,10 @@ mod tests {
|
||||
let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
|
||||
let psk1 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt1,
|
||||
)
|
||||
.unwrap();
|
||||
let psk2 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt2,
|
||||
)
|
||||
.unwrap();
|
||||
let psk1 =
|
||||
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt1).unwrap();
|
||||
let psk2 =
|
||||
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt2).unwrap();
|
||||
|
||||
assert_ne!(psk1, psk2, "Different salts should produce different PSKs");
|
||||
}
|
||||
@@ -574,20 +567,10 @@ mod tests {
|
||||
let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
|
||||
let enc_key = EncapsulationKey::X25519(kem_pk);
|
||||
|
||||
let psk1 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_2.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
let psk2 = derive_psk_with_psq_initiator(
|
||||
keypair_1.private_key(),
|
||||
keypair_3.public_key(),
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
let psk1 =
|
||||
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt).unwrap();
|
||||
let psk2 =
|
||||
derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_3.pk, &enc_key, &salt).unwrap();
|
||||
|
||||
assert_ne!(
|
||||
psk1, psk2,
|
||||
@@ -616,16 +599,16 @@ mod tests {
|
||||
|
||||
// Derive PSK twice with same inputs (initiator side)
|
||||
let (_psk1, ct1) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (_psk2, _ct2) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
@@ -634,8 +617,8 @@ mod tests {
|
||||
// PSKs will be different due to randomness in PSQ, but ciphertexts too
|
||||
// This test verifies the function is deterministic given the SAME ciphertext
|
||||
let psk_responder1 = derive_psk_with_psq_responder(
|
||||
gateway_keypair.private_key(),
|
||||
client_keypair.public_key(),
|
||||
gateway_keypair.sk(),
|
||||
&client_keypair.pk,
|
||||
(&dec_key, &enc_key),
|
||||
&ct1,
|
||||
&salt,
|
||||
@@ -643,8 +626,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let psk_responder2 = derive_psk_with_psq_responder(
|
||||
gateway_keypair.private_key(),
|
||||
client_keypair.public_key(),
|
||||
gateway_keypair.sk(),
|
||||
&client_keypair.pk,
|
||||
(&dec_key, &enc_key),
|
||||
&ct1, // Same ciphertext
|
||||
&salt,
|
||||
@@ -674,8 +657,8 @@ mod tests {
|
||||
|
||||
// Client derives PSK (initiator)
|
||||
let (client_psk, ciphertext) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
@@ -683,8 +666,8 @@ mod tests {
|
||||
|
||||
// Gateway derives PSK from ciphertext (responder)
|
||||
let gateway_psk = derive_psk_with_psq_responder(
|
||||
gateway_keypair.private_key(),
|
||||
client_keypair.public_key(),
|
||||
gateway_keypair.sk(),
|
||||
&client_keypair.pk,
|
||||
(&dec_key, &enc_key),
|
||||
&ciphertext,
|
||||
&salt,
|
||||
@@ -714,16 +697,16 @@ mod tests {
|
||||
let salt = [3u8; 32];
|
||||
|
||||
let (psk1, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key1,
|
||||
&salt,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (psk2, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key2,
|
||||
&salt,
|
||||
)
|
||||
@@ -748,8 +731,8 @@ mod tests {
|
||||
let salt = [4u8; 32];
|
||||
|
||||
let (psk, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key,
|
||||
&salt,
|
||||
)
|
||||
@@ -772,16 +755,16 @@ mod tests {
|
||||
let salt2 = [2u8; 32];
|
||||
|
||||
let (psk1, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key,
|
||||
&salt1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (psk2, _) = derive_psk_with_psq_initiator(
|
||||
client_keypair.private_key(),
|
||||
gateway_keypair.public_key(),
|
||||
client_keypair.sk(),
|
||||
&gateway_keypair.pk,
|
||||
&enc_key,
|
||||
&salt2,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
use libcrux_psq::{
|
||||
Channel,
|
||||
handshake::{
|
||||
RegistrationInitiator, Responder,
|
||||
builders::{CiphersuiteBuilder, PrincipalBuilder},
|
||||
ciphersuites::CiphersuiteName,
|
||||
types::{DHKeyPair, DHPublicKey},
|
||||
},
|
||||
};
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, DecapsulationKey, EncapsulationKey, KEM, KemKeyPair};
|
||||
use rand09::rngs::ThreadRng;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
const AAD_INITIATOR_OUTER: &[u8] = b"Test Data I Outer";
|
||||
const AAD_INITIATOR_INNER: &[u8] = b"Test Data I Inner";
|
||||
const AAD_RESPONDER: &[u8] = b"Test Data R";
|
||||
const SESSION_CONTEXT: &[u8] = b"Test Context";
|
||||
|
||||
pub enum PSQState<'a> {
|
||||
Initiator(RegistrationInitiator<'a, ThreadRng>),
|
||||
Responder(Responder<'a, ThreadRng>),
|
||||
}
|
||||
impl Debug for PSQState<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Initiator(_) => f.debug_tuple("PSQ Initiator").finish(),
|
||||
Self::Responder(_) => f.debug_tuple("PSQ Responder").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initiator_process(initiator: &mut RegistrationInitiator<ThreadRng>) -> Vec<u8> {
|
||||
let mut buffer = vec![0u8; 4096];
|
||||
let msg_len = initiator.write_message(b"", &mut buffer).unwrap();
|
||||
buffer.resize(msg_len, 0);
|
||||
buffer
|
||||
}
|
||||
|
||||
pub fn build_initiator<'a>(
|
||||
ciphersuite: &'a Ciphersuite,
|
||||
session_context: &'a [u8],
|
||||
local_x25519_keys: &'a DHKeyPair,
|
||||
remote_x25519_public: &'a DHPublicKey,
|
||||
remote_kem_public: &'a EncapsulationKey,
|
||||
) -> RegistrationInitiator<'a, rand09::rngs::ThreadRng> {
|
||||
//georgio: handle errors
|
||||
|
||||
let initiator_cbuilder = match ciphersuite.kem() {
|
||||
nym_kkt::ciphersuite::KEM::MlKem768 => match remote_kem_public {
|
||||
EncapsulationKey::MlKem768(ml_kem_public_key) => CiphersuiteBuilder::new(
|
||||
CiphersuiteName::X25519_MLKEM768_X25519_CHACHA20POLY1305_HKDFSHA256,
|
||||
)
|
||||
.peer_longterm_mlkem_pk(ml_kem_public_key),
|
||||
_ => panic!(
|
||||
"wrong key type passed (remote_kem_public should be EncapsulationKey::MlKem768)"
|
||||
),
|
||||
},
|
||||
nym_kkt::ciphersuite::KEM::McEliece => match remote_kem_public {
|
||||
EncapsulationKey::McEliece(mceliece_public_key) => CiphersuiteBuilder::new(
|
||||
CiphersuiteName::X25519_CLASSICMCELIECE_X25519_CHACHA20POLY1305_HKDFSHA256,
|
||||
)
|
||||
.peer_longterm_cmc_pk(mceliece_public_key),
|
||||
_ => panic!(
|
||||
"wrong key type passed (remote_kem_public should be EncapsulationKey::McEliece)"
|
||||
),
|
||||
},
|
||||
_ => panic!("undefined"),
|
||||
};
|
||||
let initiator_ciphersuite = initiator_cbuilder
|
||||
.longterm_x25519_keys(local_x25519_keys)
|
||||
.peer_longterm_x25519_pk(remote_x25519_public)
|
||||
.build_initiator_ciphersuite()
|
||||
.unwrap();
|
||||
|
||||
PrincipalBuilder::new(rand09::rng())
|
||||
.outer_aad(AAD_INITIATOR_OUTER)
|
||||
.inner_aad(AAD_INITIATOR_INNER)
|
||||
.context(session_context)
|
||||
.build_registration_initiator(initiator_ciphersuite)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// JS: I have removed the `ciphersuite` argument as it was only matching on the key types,
|
||||
// which we already obtained matching on the ciphersuite kem type in `LpSession::new`
|
||||
pub fn build_responder<'a>(
|
||||
local_x25519_keys: &'a DHKeyPair,
|
||||
local_kem_keys: &'a KemKeyPair,
|
||||
) -> Responder<'a, rand09::rngs::ThreadRng> {
|
||||
let responder_ciphersuite = match local_kem_keys {
|
||||
KemKeyPair::MlKem768 {
|
||||
encapsulation_key,
|
||||
decapsulation_key,
|
||||
} => CiphersuiteBuilder::new(
|
||||
CiphersuiteName::X25519_MLKEM768_X25519_CHACHA20POLY1305_HKDFSHA256,
|
||||
)
|
||||
.longterm_mlkem_encapsulation_key(encapsulation_key)
|
||||
.longterm_mlkem_decapsulation_key(decapsulation_key),
|
||||
KemKeyPair::McEliece {
|
||||
encapsulation_key,
|
||||
decapsulation_key,
|
||||
} => CiphersuiteBuilder::new(
|
||||
CiphersuiteName::X25519_CLASSICMCELIECE_X25519_CHACHA20POLY1305_HKDFSHA256,
|
||||
)
|
||||
.longterm_cmc_encapsulation_key(encapsulation_key)
|
||||
.longterm_cmc_decapsulation_key(decapsulation_key),
|
||||
KemKeyPair::XWing { .. } => panic!("unsupported"),
|
||||
KemKeyPair::X25519 { .. } => panic!("unsupported"),
|
||||
}
|
||||
.longterm_x25519_keys(local_x25519_keys)
|
||||
.build_responder_ciphersuite()
|
||||
.unwrap();
|
||||
|
||||
PrincipalBuilder::new(rand09::rng())
|
||||
.outer_aad(AAD_RESPONDER)
|
||||
.context(SESSION_CONTEXT)
|
||||
.build_responder(responder_ciphersuite)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn psq_responder_process<'a>(
|
||||
responder: &'a mut Responder<ThreadRng>,
|
||||
initiator_message: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut payload = vec![0u8; 4096];
|
||||
responder
|
||||
.read_message(initiator_message, &mut payload)
|
||||
.unwrap();
|
||||
|
||||
let mut buffer = vec![0u8; 4096];
|
||||
let msg_len = responder.write_message(b"", &mut buffer).unwrap();
|
||||
buffer.resize(msg_len, 0);
|
||||
buffer
|
||||
}
|
||||
+1098
-1273
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,21 @@
|
||||
//! This module implements session lifecycle management functionality, handling
|
||||
//! creation, retrieval, and storage of sessions.
|
||||
|
||||
use crate::noise_protocol::ReadResult;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
|
||||
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
|
||||
use dashmap::DashMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(test)]
|
||||
use libcrux_psq::handshake::types::DHPublicKey;
|
||||
use nym_kkt::ciphersuite::Ciphersuite;
|
||||
|
||||
/// Manages the lifecycle of Lewes Protocol sessions.
|
||||
///
|
||||
/// The SessionManager is responsible for creating, storing, and retrieving sessions,
|
||||
/// ensuring proper thread-safety for concurrent access.
|
||||
/// The SessionManager is responsible for creating, storing, and retrieving sessions
|
||||
pub struct SessionManager {
|
||||
/// Manages state machines directly, keyed by lp_id
|
||||
state_machines: DashMap<u32, LpStateMachine>,
|
||||
state_machines: HashMap<u32, LpStateMachine>,
|
||||
}
|
||||
|
||||
impl Default for SessionManager {
|
||||
@@ -31,15 +33,21 @@ impl SessionManager {
|
||||
/// Creates a new session manager with empty session storage.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_machines: DashMap::new(),
|
||||
state_machines: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_input(&self, lp_id: u32, input: LpInput) -> Result<Option<LpAction>, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
|
||||
pub fn process_input(
|
||||
&mut self,
|
||||
lp_id: u32,
|
||||
input: LpInput,
|
||||
) -> Result<Option<LpAction>, LpError> {
|
||||
self.state_machine_mut(lp_id)?
|
||||
.process_input(input)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn add(&self, session: LpSession) -> Result<(), LpError> {
|
||||
pub fn add(&mut self, session: LpSession) -> Result<(), LpError> {
|
||||
let sm = LpStateMachine {
|
||||
state: LpState::ReadyToHandshake {
|
||||
session: Box::new(session),
|
||||
@@ -71,67 +79,76 @@ impl SessionManager {
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_state_machine_id(&self, lp_id: u32) -> Result<u32, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.id())?
|
||||
self.state_machine(lp_id)?.id()
|
||||
}
|
||||
|
||||
pub fn get_state(&self, lp_id: u32) -> Result<LpStateBare, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.bare_state()))?
|
||||
Ok(self.state_machine(lp_id)?.bare_state())
|
||||
}
|
||||
|
||||
pub fn receiving_counter_quick_check(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?.receiving_counter_quick_check(counter)
|
||||
})?
|
||||
pub fn receiving_counter_quick_check(
|
||||
&mut self,
|
||||
lp_id: u32,
|
||||
counter: u64,
|
||||
) -> Result<(), LpError> {
|
||||
self.state_machine_mut(lp_id)?
|
||||
.session()?
|
||||
.receiving_counter_quick_check(counter)
|
||||
}
|
||||
|
||||
pub fn receiving_counter_mark(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session()?.receiving_counter_mark(counter))?
|
||||
self.state_machine(lp_id)?
|
||||
.session()?
|
||||
.receiving_counter_mark(counter)
|
||||
}
|
||||
|
||||
pub fn start_handshake(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
self.prepare_handshake_message(lp_id)
|
||||
}
|
||||
|
||||
pub fn prepare_handshake_message(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_handshake_message())
|
||||
.ok()?
|
||||
}
|
||||
// pub fn start_handshake(& self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
// self.prepare_psq_request(lp_id)
|
||||
// }
|
||||
//
|
||||
// pub fn prepare_psq_request(& self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
// self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_psq_request())
|
||||
// .ok()?
|
||||
// }
|
||||
|
||||
pub fn is_handshake_complete(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.is_handshake_complete()))?
|
||||
Ok(self
|
||||
.state_machine(lp_id)?
|
||||
.session()?
|
||||
.is_handshake_complete())
|
||||
}
|
||||
|
||||
pub fn next_counter(&self, lp_id: u32) -> Result<u64, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.next_counter()))?
|
||||
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
|
||||
Ok(self.state_machine_mut(lp_id)?.session()?.next_counter())
|
||||
}
|
||||
|
||||
pub fn decrypt_data(&self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?
|
||||
.decrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
})?
|
||||
self.state_machine(lp_id)?
|
||||
.session()?
|
||||
.decrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
}
|
||||
|
||||
pub fn encrypt_data(&self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?
|
||||
.encrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
})?
|
||||
self.state_machine(lp_id)?
|
||||
.session()?
|
||||
.encrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
}
|
||||
|
||||
pub fn current_packet_cnt(&self, lp_id: u32) -> Result<(u64, u64), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
|
||||
Ok(self.state_machine(lp_id)?.session()?.current_packet_cnt())
|
||||
}
|
||||
|
||||
pub fn process_handshake_message(
|
||||
&self,
|
||||
lp_id: u32,
|
||||
message: &LpMessage,
|
||||
) -> Result<ReadResult, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session()?.process_handshake_message(message))?
|
||||
}
|
||||
// pub fn process_handshake_message(
|
||||
// &self,
|
||||
// lp_id: u32,
|
||||
// message: &LpMessage,
|
||||
// ) -> Result<ReadResult, LpError> {
|
||||
// self.state_machine(lp_id)?
|
||||
// .session()?
|
||||
// .process_handshake_message(message)
|
||||
// }
|
||||
|
||||
pub fn session_count(&self) -> usize {
|
||||
self.state_machines.len()
|
||||
@@ -141,46 +158,66 @@ impl SessionManager {
|
||||
self.state_machines.contains_key(&lp_id)
|
||||
}
|
||||
|
||||
pub fn with_state_machine<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
where
|
||||
F: FnOnce(&LpStateMachine) -> R,
|
||||
{
|
||||
if let Some(sm) = self.state_machines.get(&lp_id) {
|
||||
Ok(f(&sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
// self.state_machines.get(&lp_id).map(|sm_ref| f(&*sm_ref)) // Lock held only during closure execution
|
||||
fn state_machine(&self, lp_id: u32) -> Result<&LpStateMachine, LpError> {
|
||||
self.state_machines
|
||||
.get(&lp_id)
|
||||
.ok_or_else(|| LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
|
||||
// For mutable access (like running process_input)
|
||||
pub fn with_state_machine_mut<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
where
|
||||
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
|
||||
{
|
||||
if let Some(mut sm) = self.state_machines.get_mut(&lp_id) {
|
||||
Ok(f(&mut sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
fn state_machine_mut(&mut self, lp_id: u32) -> Result<&mut LpStateMachine, LpError> {
|
||||
self.state_machines
|
||||
.get_mut(&lp_id)
|
||||
.ok_or_else(|| LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
|
||||
// pub fn with_state_machine<F, R>(& self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
// where
|
||||
// F: FnOnce(&LpStateMachine) -> R,
|
||||
// {
|
||||
// if let Some(sm) = self.state_machines.get(&lp_id) {
|
||||
// Ok(f(&sm))
|
||||
// } else {
|
||||
// Err(LpError::StateMachineNotFound { lp_id })
|
||||
// }
|
||||
// // self.state_machines.get(&lp_id).map(|sm_ref| f(&*sm_ref)) // Lock held only during closure execution
|
||||
// }
|
||||
//
|
||||
// // For mutable access (like running process_input)
|
||||
// pub fn with_state_machine_mut<F, R>(& self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
// where
|
||||
// F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
|
||||
// {
|
||||
// if let Some(mut sm) = self.state_machines.get_mut(&lp_id) {
|
||||
// Ok(f(&mut sm))
|
||||
// } else {
|
||||
// Err(LpError::StateMachineNotFound { lp_id })
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn create_session_state_machine(
|
||||
&self,
|
||||
&mut self,
|
||||
receiver_index: u32,
|
||||
is_initiator: bool,
|
||||
ciphersuite: Ciphersuite,
|
||||
local_peer: LpLocalPeer,
|
||||
remote_peer: LpRemotePeer,
|
||||
salt: &[u8; 32],
|
||||
) -> Result<u32, LpError> {
|
||||
let sm = LpStateMachine::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?;
|
||||
let sm = LpStateMachine::new(
|
||||
receiver_index,
|
||||
is_initiator,
|
||||
ciphersuite,
|
||||
local_peer,
|
||||
remote_peer,
|
||||
salt,
|
||||
)?;
|
||||
|
||||
self.state_machines.insert(receiver_index, sm);
|
||||
Ok(receiver_index)
|
||||
}
|
||||
|
||||
/// Method to remove a state machine
|
||||
pub fn remove_state_machine(&self, lp_id: u32) -> bool {
|
||||
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
|
||||
let removed = self.state_machines.remove(&lp_id);
|
||||
|
||||
removed.is_some()
|
||||
@@ -190,123 +227,127 @@ impl SessionManager {
|
||||
/// This allows integration tests to bypass KKT exchange and directly test PSQ/handshake.
|
||||
#[cfg(test)]
|
||||
pub fn init_kkt_for_test(
|
||||
&self,
|
||||
&mut self,
|
||||
lp_id: u32,
|
||||
remote_x25519_pub: &nym_crypto::asymmetric::x25519::PublicKey,
|
||||
remote_x25519_pub: &DHPublicKey,
|
||||
) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?.set_kkt_completed_for_test(remote_x25519_pub);
|
||||
Ok(())
|
||||
})?
|
||||
self.state_machine_mut(lp_id)?
|
||||
.session()?
|
||||
.set_kkt_completed_for_test(remote_x25519_pub);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::peer::{mock_peers, random_peer};
|
||||
use crate::{
|
||||
kem_list,
|
||||
peer::{mock_peers, random_peer},
|
||||
};
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_get() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
|
||||
let salt = [47u8; 32];
|
||||
let receiver_index: u32 = 1001;
|
||||
for kem in kem_list() {
|
||||
let (local, peer1) = mock_peers(kem);
|
||||
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
|
||||
.unwrap();
|
||||
let salt = [47u8; 32];
|
||||
let receiver_index: u32 = 1001;
|
||||
|
||||
let retrieved = manager.state_machine_exists(sm_1_id);
|
||||
assert!(retrieved);
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
|
||||
.unwrap();
|
||||
|
||||
let not_found = manager.state_machine_exists(99);
|
||||
assert!(!not_found);
|
||||
let retrieved = manager.state_machine_exists(sm_1_id);
|
||||
assert!(retrieved);
|
||||
|
||||
let not_found = manager.state_machine_exists(99);
|
||||
assert!(!not_found);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_remove() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
for kem in kem_list() {
|
||||
let (local, peer1) = mock_peers(kem);
|
||||
|
||||
let salt = [48u8; 32];
|
||||
let receiver_index: u32 = 2002;
|
||||
let salt = [48u8; 32];
|
||||
let receiver_index: u32 = 2002;
|
||||
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
|
||||
.unwrap();
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
|
||||
.unwrap();
|
||||
|
||||
let removed = manager.remove_state_machine(sm_1_id);
|
||||
assert!(removed);
|
||||
assert_eq!(manager.session_count(), 0);
|
||||
let removed = manager.remove_state_machine(sm_1_id);
|
||||
assert!(removed);
|
||||
assert_eq!(manager.session_count(), 0);
|
||||
|
||||
let removed_again = manager.remove_state_machine(sm_1_id);
|
||||
assert!(!removed_again);
|
||||
let removed_again = manager.remove_state_machine(sm_1_id);
|
||||
assert!(!removed_again);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_sessions() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let peer2 = random_peer(&mut rng);
|
||||
let peer3 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
for kem in kem_list() {
|
||||
let (local, peer1) = mock_peers(kem);
|
||||
let (peer2, peer3) = mock_peers(kem);
|
||||
|
||||
let salt = [49u8; 32];
|
||||
let salt = [49u8; 32];
|
||||
|
||||
let sm_1 = manager
|
||||
.create_session_state_machine(3001, true, local.clone(), peer1.as_remote(), &salt)
|
||||
.unwrap();
|
||||
let sm_1 = manager
|
||||
.create_session_state_machine(3001, true, local.clone(), peer1.as_remote(), &salt)
|
||||
.unwrap();
|
||||
|
||||
let sm_2 = manager
|
||||
.create_session_state_machine(3002, true, local.clone(), peer2.as_remote(), &salt)
|
||||
.unwrap();
|
||||
let sm_2 = manager
|
||||
.create_session_state_machine(3002, true, local.clone(), peer2.as_remote(), &salt)
|
||||
.unwrap();
|
||||
|
||||
let sm_3 = manager
|
||||
.create_session_state_machine(3003, true, local.clone(), peer3.as_remote(), &salt)
|
||||
.unwrap();
|
||||
let sm_3 = manager
|
||||
.create_session_state_machine(3003, true, local.clone(), peer3.as_remote(), &salt)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manager.session_count(), 3);
|
||||
assert_eq!(manager.session_count(), 3);
|
||||
|
||||
let retrieved1 = manager.get_state_machine_id(sm_1).unwrap();
|
||||
let retrieved2 = manager.get_state_machine_id(sm_2).unwrap();
|
||||
let retrieved3 = manager.get_state_machine_id(sm_3).unwrap();
|
||||
let retrieved1 = manager.get_state_machine_id(sm_1).unwrap();
|
||||
let retrieved2 = manager.get_state_machine_id(sm_2).unwrap();
|
||||
let retrieved3 = manager.get_state_machine_id(sm_3).unwrap();
|
||||
|
||||
assert_eq!(retrieved1, sm_1);
|
||||
assert_eq!(retrieved2, sm_2);
|
||||
assert_eq!(retrieved3, sm_3);
|
||||
assert_eq!(retrieved1, sm_1);
|
||||
assert_eq!(retrieved2, sm_2);
|
||||
assert_eq!(retrieved3, sm_3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_create_session() {
|
||||
let manager = SessionManager::new();
|
||||
let (init, resp) = mock_peers();
|
||||
let mut manager = SessionManager::new();
|
||||
for kem in kem_list() {
|
||||
let (init, resp) = mock_peers(kem);
|
||||
|
||||
let salt = [50u8; 32];
|
||||
let receiver_index: u32 = 4004;
|
||||
let salt = [50u8; 32];
|
||||
let receiver_index: u32 = 4004;
|
||||
|
||||
let sm = manager.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init,
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
);
|
||||
let sm = manager.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init,
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
);
|
||||
|
||||
assert!(sm.is_ok());
|
||||
let sm = sm.unwrap();
|
||||
assert!(sm.is_ok());
|
||||
let sm = sm.unwrap();
|
||||
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
|
||||
let retrieved = manager.get_state_machine_id(sm);
|
||||
assert!(retrieved.is_ok());
|
||||
assert_eq!(retrieved.unwrap(), sm);
|
||||
let retrieved = manager.get_state_machine_id(sm);
|
||||
assert!(retrieved.is_ok());
|
||||
assert_eq!(retrieved.unwrap(), sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+525
-539
File diff suppressed because it is too large
Load Diff
Generated
+90
-27
@@ -163,7 +163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
@@ -386,7 +386,7 @@ dependencies = [
|
||||
"k256",
|
||||
"num-traits",
|
||||
"p256",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"rayon",
|
||||
"sha2",
|
||||
"thiserror 1.0.64",
|
||||
@@ -441,7 +441,7 @@ dependencies = [
|
||||
"cosmwasm-derive",
|
||||
"derive_more",
|
||||
"hex",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"rmp-serde",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -492,7 +492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -863,7 +863,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle 2.4.1",
|
||||
@@ -880,7 +880,7 @@ dependencies = [
|
||||
"ed25519",
|
||||
"hashbrown 0.14.5",
|
||||
"hex",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"sha2",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -903,7 +903,7 @@ dependencies = [
|
||||
"ff",
|
||||
"generic-array 0.14.7",
|
||||
"group",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
"subtle 2.4.1",
|
||||
"zeroize",
|
||||
@@ -921,7 +921,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
@@ -962,6 +962,18 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
@@ -969,7 +981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
@@ -1137,9 +1149,9 @@ checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -1177,7 +1189,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract",
|
||||
"nym-vesting-contract-common",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1282,8 +1294,8 @@ dependencies = [
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"nym-contracts-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1297,7 +1309,8 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"sha2",
|
||||
"subtle-encoding",
|
||||
"thiserror 2.0.12",
|
||||
@@ -1325,7 +1338,7 @@ dependencies = [
|
||||
"nym-ecash-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-network-defaults",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.3.1",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -1376,8 +1389,8 @@ dependencies = [
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"semver",
|
||||
"serde",
|
||||
]
|
||||
@@ -1516,7 +1529,7 @@ dependencies = [
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
@@ -1687,6 +1700,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -1694,8 +1713,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1705,7 +1734,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1714,7 +1753,16 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1724,7 +1772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1965,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1986,7 +2034,7 @@ dependencies = [
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"lioness",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"rand_distr",
|
||||
"sha2",
|
||||
"subtle 2.4.1",
|
||||
@@ -2289,6 +2337,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.2"
|
||||
@@ -2298,6 +2355,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
|
||||
[[package]]
|
||||
name = "x25519-dalek"
|
||||
version = "2.0.1"
|
||||
@@ -2305,7 +2368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -26,7 +26,7 @@ nym-lp-transport = { path = "../common/nym-lp-transport", features = ["io-mocks"
|
||||
nym-gateway = { path = "../gateway" }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-kkt-ciphersuite = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -15,6 +15,7 @@ mod tests {
|
||||
WireguardGatewayData, mix_forwarding_channels,
|
||||
};
|
||||
use nym_gateway::node::{ActiveClientsStore, GatewayStorage, LpConfig};
|
||||
use nym_kkt_ciphersuite::Ciphersuite;
|
||||
use nym_registration_client::{LpClientError, LpRegistrationClient};
|
||||
use nym_test_utils::helpers::{CryptoRng, RngCore, u64_seeded_rng};
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
@@ -63,8 +64,10 @@ mod tests {
|
||||
|
||||
let lp_x25519_keys = Arc::new(ed25519_keys.to_x25519());
|
||||
|
||||
let ciphersuite = Ciphersuite::Default();
|
||||
|
||||
Party {
|
||||
peer: LpLocalPeer::new(ed25519_keys, lp_x25519_keys.clone())
|
||||
peer: LpLocalPeer::new(ciphersuite, ed25519_keys, lp_x25519_keys.clone())
|
||||
.with_kem_psq_key(lp_x25519_keys),
|
||||
x25519_wg_keys,
|
||||
socket_addr: SocketAddr::from((ip, u16::from_le_bytes(port))),
|
||||
|
||||
Generated
+6
-6
@@ -2955,7 +2955,7 @@ dependencies = [
|
||||
"idna",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustls 0.23.25",
|
||||
"thiserror 2.0.12",
|
||||
@@ -2980,7 +2980,7 @@ dependencies = [
|
||||
"moka",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.2",
|
||||
"resolv-conf",
|
||||
"rustls 0.23.25",
|
||||
"smallvec",
|
||||
@@ -4374,6 +4374,7 @@ dependencies = [
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"sha2 0.10.9",
|
||||
@@ -5831,7 +5832,7 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.25",
|
||||
@@ -5899,13 +5900,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"zerocopy 0.8.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user