LP: modified LPRemotePeer to dynamically choose required KEM key hash (#6358)

* LP: modified LPRemotePeer to dynamically choose required KEM key hash

* nym-lp-client fixes
This commit is contained in:
Jędrzej Stuczyński
2026-01-23 11:41:55 +00:00
committed by GitHub
parent 43d1c61b70
commit a63a1e745e
24 changed files with 568 additions and 313 deletions
Generated
+17 -1
View File
@@ -6796,15 +6796,28 @@ dependencies = [
"libcrux-chacha20poly1305",
"libcrux-ecdh",
"libcrux-kem",
"libcrux-sha3",
"num_enum",
"nym-crypto",
"nym-kkt-ciphersuite",
"rand 0.9.2",
"rand_chacha 0.9.0",
"strum",
"thiserror 2.0.17",
"zeroize",
]
[[package]]
name = "nym-kkt-ciphersuite"
version = "1.20.1"
dependencies = [
"blake3",
"libcrux-sha3",
"num_enum",
"strum",
"strum_macros",
"thiserror 2.0.17",
]
[[package]]
name = "nym-ledger"
version = "1.20.1"
@@ -6858,6 +6871,7 @@ dependencies = [
"nym-http-api-client",
"nym-ip-packet-requests",
"nym-kcp",
"nym-kkt-ciphersuite",
"nym-lp",
"nym-registration-client",
"nym-sphinx",
@@ -7197,6 +7211,7 @@ dependencies = [
"nym-crypto",
"nym-exit-policy",
"nym-http-api-client",
"nym-kkt-ciphersuite",
"nym-noise-keys",
"nym-upgrade-mode-check",
"nym-wireguard-types",
@@ -7503,6 +7518,7 @@ dependencies = [
"nym-credentials-interface",
"nym-crypto",
"nym-ip-packet-requests",
"nym-kkt-ciphersuite",
"nym-sphinx",
"nym-wireguard-types",
"serde",
+1 -1
View File
@@ -174,7 +174,7 @@ members = [
"wasm/node-tester",
"wasm/zknym-lib",
"nym-gateway-probe",
"integration-tests", "common/nym-lp-transport",
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
]
default-members = [
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "nym-kkt-ciphersuite"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
version.workspace = true
[dependencies]
thiserror = { workspace = true }
num_enum = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
blake3 = { workspace = true, optional = true }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", optional = true }
[features]
digests = ["blake3", "libcrux-sha3"]
[lints]
workspace = true
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Debug, Error)]
pub enum KKTCiphersuiteError {
#[error(
"attempted to use an insecure encapsulation key hash length. requested: {requested}. minimum: {minimum}"
)]
InsecureHashLen { requested: u8, minimum: u8 },
#[error("{raw} does not correspond to any known KEM type encoding")]
UnknownKEMType { raw: u8 },
#[error("{raw} does not correspond to any known Hash Function type encoding")]
UnknownHashFunctionType { raw: u8 },
#[error("{raw} does not correspond to any known Signature Scheme type encoding")]
UnknownSignatureSchemeType { raw: u8 },
}
+337
View File
@@ -0,0 +1,337 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::KKTCiphersuiteError;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::collections::HashMap;
use std::fmt::Display;
use strum_macros::EnumIter;
pub mod error;
pub const DEFAULT_HASH_LEN: usize = 32;
const _: () = assert!(DEFAULT_HASH_LEN <= u8::MAX as usize);
pub const MINIMUM_SECURE_HASH_LEN: u8 = 16;
const _: () = assert!(MINIMUM_SECURE_HASH_LEN <= DEFAULT_HASH_LEN as u8);
pub const CIPHERSUITE_ENCODING_LEN: usize = 4;
// no point in importing curve libraries for well-defined constants
pub mod ed25519 {
pub const SECRET_KEY_LENGTH: usize = 32;
pub const PUBLIC_KEY_LENGTH: usize = 32;
pub const SIGNATURE_LENGTH: usize = 64;
}
pub mod x25519 {
pub const PUBLIC_KEY_LENGTH: usize = 32;
pub const SECRET_KEY_LENGTH: usize = 32;
}
pub mod ml_kem768 {
pub const PUBLIC_KEY_LENGTH: usize = 1184;
}
pub mod mceliece {
pub const PUBLIC_KEY_LENGTH: usize = 524160;
pub const SECRET_KEY_LENGTH: usize = 13608;
pub const CIPHERTEXT_LENGTH: usize = 156;
}
pub mod xwing {
use crate::{ml_kem768, x25519};
pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH;
}
pub type KEMKeyDigests = HashMap<HashFunction, Vec<u8>>;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive, EnumIter)]
#[repr(u8)]
pub enum HashFunction {
Blake3 = 0,
SHAKE256 = 1,
SHAKE128 = 2,
SHA256 = 3,
}
impl HashFunction {
#[cfg(feature = "digests")]
pub fn digest<M: AsRef<[u8]>>(&self, data: M, output_length: usize) -> Vec<u8> {
let mut out = vec![0u8; output_length];
match self {
HashFunction::Blake3 => {
let mut hasher = blake3::Hasher::new();
hasher.update(data.as_ref());
hasher.finalize_xof().fill(&mut out);
hasher.reset();
}
HashFunction::SHAKE256 => libcrux_sha3::shake256_ema(&mut out, data.as_ref()),
HashFunction::SHAKE128 => libcrux_sha3::shake128_ema(&mut out, data.as_ref()),
HashFunction::SHA256 => libcrux_sha3::sha256_ema(&mut out, data.as_ref()),
}
out
}
}
impl Display for HashFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
HashFunction::Blake3 => "blake3",
HashFunction::SHAKE128 => "shake128",
HashFunction::SHAKE256 => "shake256",
HashFunction::SHA256 => "sha256",
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive)]
#[repr(u8)]
pub enum HashLength {
Default = 0,
#[num_enum(catch_all)]
Custom(u8),
}
impl Display for HashLength {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HashLength::Default => DEFAULT_HASH_LEN.fmt(f),
HashLength::Custom(custom_len) => custom_len.fmt(f),
}
}
}
impl HashLength {
pub fn decode(raw: u8) -> Result<Self, KKTCiphersuiteError> {
// check if we're using encoding for 'default' value
if raw == u8::from(Self::Default) {
return Ok(Self::Default);
}
// otherwise, we treat it as a custom length, and we have to validate its security
let custom_len = raw;
if custom_len < MINIMUM_SECURE_HASH_LEN {
return Err(KKTCiphersuiteError::InsecureHashLen {
requested: custom_len,
minimum: MINIMUM_SECURE_HASH_LEN,
});
}
Ok(Self::Custom(custom_len))
}
}
impl TryFrom<Option<u8>> for HashLength {
type Error = KKTCiphersuiteError;
fn try_from(value: Option<u8>) -> Result<Self, Self::Error> {
match value {
None => Ok(Self::Default),
Some(custom_len) => Self::decode(custom_len),
}
}
}
impl HashLength {
pub const fn value(&self) -> usize {
match self {
HashLength::Default => DEFAULT_HASH_LEN,
HashLength::Custom(custom_len) => *custom_len as usize,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum SignatureScheme {
Ed25519 = 0,
}
impl SignatureScheme {
pub const fn signing_key_length(&self) -> usize {
match self {
// 32 bytes
SignatureScheme::Ed25519 => ed25519::SECRET_KEY_LENGTH,
}
}
pub const fn verification_key_length(&self) -> usize {
match self {
// 32 bytes
SignatureScheme::Ed25519 => ed25519::PUBLIC_KEY_LENGTH,
}
}
pub const fn signature_length(&self) -> usize {
match self {
// 64 bytes
SignatureScheme::Ed25519 => ed25519::SIGNATURE_LENGTH,
}
}
}
impl Display for SignatureScheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SignatureScheme::Ed25519 => "ed25519",
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum KEM {
XWing = 0,
MlKem768 = 1,
McEliece = 2,
X25519 = 255,
}
impl KEM {
pub fn encapsulation_key_length(&self) -> usize {
match self {
KEM::MlKem768 => ml_kem768::PUBLIC_KEY_LENGTH,
KEM::XWing => xwing::PUBLIC_KEY_LENGTH,
KEM::X25519 => x25519::PUBLIC_KEY_LENGTH,
KEM::McEliece => mceliece::PUBLIC_KEY_LENGTH,
}
}
}
impl Display for KEM {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
KEM::MlKem768 => "mlkem768",
KEM::XWing => "xwing",
KEM::X25519 => "x25519",
KEM::McEliece => "mceliece",
})
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Ciphersuite {
hash_function: HashFunction,
signature_scheme: SignatureScheme,
kem: KEM,
hash_length: HashLength,
encapsulation_key_length: usize,
signing_key_length: usize,
verification_key_length: usize,
signature_length: usize,
}
impl Ciphersuite {
pub fn new(
kem: KEM,
hash_function: HashFunction,
signature_scheme: SignatureScheme,
hash_length: HashLength,
) -> Self {
Self {
hash_function,
signature_scheme,
kem,
hash_length,
encapsulation_key_length: kem.encapsulation_key_length(),
signing_key_length: signature_scheme.signing_key_length(),
verification_key_length: signature_scheme.verification_key_length(),
signature_length: signature_scheme.signature_length(),
}
}
pub fn kem_key_len(&self) -> usize {
self.encapsulation_key_length
}
pub fn signature_len(&self) -> usize {
self.signature_length
}
pub fn signing_key_len(&self) -> usize {
self.signing_key_length
}
pub fn verification_key_len(&self) -> usize {
self.verification_key_length
}
pub fn hash_function(&self) -> HashFunction {
self.hash_function
}
pub fn kem(&self) -> KEM {
self.kem
}
pub fn signature_scheme(&self) -> SignatureScheme {
self.signature_scheme
}
pub fn hash_len(&self) -> usize {
self.hash_length.value()
}
pub fn resolve_ciphersuite(
kem: KEM,
hash_function: HashFunction,
signature_scheme: SignatureScheme,
// This should be None 99.9999% of the time
custom_hash_length: Option<u8>,
) -> Result<Self, KKTCiphersuiteError> {
let hash_length = HashLength::try_from(custom_hash_length)?;
Ok(Ciphersuite::new(
kem,
hash_function,
signature_scheme,
hash_length,
))
}
pub fn encode(&self) -> [u8; CIPHERSUITE_ENCODING_LEN] {
// [kem, hash, hashlen, sig]
[
self.kem.into(),
self.hash_function.into(),
self.hash_length.into(),
self.signature_scheme.into(),
]
}
pub fn decode(encoding: [u8; CIPHERSUITE_ENCODING_LEN]) -> Result<Self, KKTCiphersuiteError> {
let raw_kem = encoding[0];
let raw_hash_function = encoding[1];
let hash_len = encoding[2];
let raw_signature_scheme = encoding[3];
let kem = KEM::try_from(raw_kem)
.map_err(|_| KKTCiphersuiteError::UnknownKEMType { raw: raw_kem })?;
let hash_function = HashFunction::try_from(raw_hash_function).map_err(|_| {
KKTCiphersuiteError::UnknownHashFunctionType {
raw: raw_hash_function,
}
})?;
let hash_length = HashLength::decode(hash_len)?;
let signature_scheme = SignatureScheme::try_from(raw_signature_scheme).map_err(|_| {
KKTCiphersuiteError::UnknownSignatureSchemeType {
raw: raw_signature_scheme,
}
})?;
Ok(Self::new(kem, hash_function, signature_scheme, hash_length))
}
}
impl Display for Ciphersuite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&format!(
"{}_{}({})_{}",
self.kem, self.hash_function, self.hash_length, self.signature_scheme
)
.to_ascii_lowercase(),
)
}
}
+2 -1
View File
@@ -10,13 +10,14 @@ publish = false
blake3 = { workspace = true }
thiserror = { workspace = true }
num_enum = { workspace = true }
strum = { workspace = true }
# internal
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
nym-kkt-ciphersuite = { path = "../nym-kkt-ciphersuite", features = ["digests"] }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
+2 -223
View File
@@ -1,37 +1,10 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use libcrux_kem::{Algorithm, MlKem768PublicKey};
use nym_crypto::asymmetric::ed25519;
use std::fmt::Display;
use crate::error::KKTError;
use libcrux_kem::Algorithm;
pub const DEFAULT_HASH_LEN: usize = 32;
const _: () = assert!(DEFAULT_HASH_LEN <= u8::MAX as usize);
pub const CIPHERSUITE_ENCODING_LEN: usize = 4;
pub const CURVE25519_KEY_LEN: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum HashFunction {
Blake3,
SHAKE128,
SHAKE256,
SHA256,
}
impl Display for HashFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
HashFunction::Blake3 => "Blake3",
HashFunction::SHAKE128 => "SHAKE128",
HashFunction::SHAKE256 => "SHAKE256",
HashFunction::SHA256 => "SHA256",
})
}
}
pub use nym_kkt_ciphersuite::*;
pub enum EncapsulationKey<'a> {
MlKem768(libcrux_kem::PublicKey),
@@ -89,200 +62,6 @@ impl<'a> EncapsulationKey<'a> {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SignatureScheme {
Ed25519,
}
impl Display for SignatureScheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SignatureScheme::Ed25519 => "Ed25519",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum KEM {
MlKem768,
XWing,
X25519,
McEliece,
}
impl Display for KEM {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
KEM::MlKem768 => "MlKem768",
KEM::XWing => "XWing",
KEM::X25519 => "x25519",
KEM::McEliece => "McEliece",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Ciphersuite {
hash_function: HashFunction,
signature_scheme: SignatureScheme,
kem: KEM,
hash_length: u8,
encapsulation_key_length: usize,
signing_key_length: usize,
verification_key_length: usize,
signature_length: usize,
}
impl Ciphersuite {
pub fn kem_key_len(&self) -> usize {
self.encapsulation_key_length
}
pub fn signature_len(&self) -> usize {
self.signature_length
}
pub fn signing_key_len(&self) -> usize {
self.signing_key_length
}
pub fn verification_key_len(&self) -> usize {
self.verification_key_length
}
pub fn hash_function(&self) -> HashFunction {
self.hash_function
}
pub fn kem(&self) -> KEM {
self.kem
}
pub fn signature_scheme(&self) -> SignatureScheme {
self.signature_scheme
}
pub fn hash_len(&self) -> usize {
self.hash_length as usize
}
pub fn resolve_ciphersuite(
kem: KEM,
hash_function: HashFunction,
signature_scheme: SignatureScheme,
// This should be None 99.9999% of the time
custom_hash_length: Option<u8>,
) -> Result<Self, KKTError> {
let hash_len = match custom_hash_length {
Some(l) => {
if l < 16 {
return Err(KKTError::InsecureHashLen);
} else {
l
}
}
None => DEFAULT_HASH_LEN as u8,
};
Ok(Self {
hash_function,
signature_scheme,
kem,
hash_length: hash_len,
encapsulation_key_length: match kem {
// 1184 bytes
KEM::MlKem768 => MlKem768PublicKey::len(),
// 1216 bytes = 1184 + 32
KEM::XWing => MlKem768PublicKey::len() + CURVE25519_KEY_LEN,
// 32 bytes
KEM::X25519 => CURVE25519_KEY_LEN,
// 524160 bytes
KEM::McEliece => classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES,
},
signing_key_length: match signature_scheme {
// 32 bytes
SignatureScheme::Ed25519 => ed25519::SECRET_KEY_LENGTH,
},
verification_key_length: match signature_scheme {
// 32 bytes
SignatureScheme::Ed25519 => ed25519::PUBLIC_KEY_LENGTH,
},
signature_length: match signature_scheme {
// 64 bytes
SignatureScheme::Ed25519 => ed25519::SIGNATURE_LENGTH,
},
})
}
pub fn encode(&self) -> [u8; CIPHERSUITE_ENCODING_LEN] {
// [kem, hash, hashlen, sig]
[
match self.kem {
KEM::XWing => 0,
KEM::MlKem768 => 1,
KEM::McEliece => 2,
KEM::X25519 => 255,
},
match self.hash_function {
HashFunction::Blake3 => 0,
HashFunction::SHAKE256 => 1,
HashFunction::SHAKE128 => 2,
HashFunction::SHA256 => 3,
},
match self.hash_length as usize {
DEFAULT_HASH_LEN => 0u8,
_ => self.hash_length,
},
match self.signature_scheme {
SignatureScheme::Ed25519 => 0,
},
]
}
pub fn decode(encoding: [u8; CIPHERSUITE_ENCODING_LEN]) -> Result<Self, KKTError> {
let kem = match encoding[0] {
0 => KEM::XWing,
1 => KEM::MlKem768,
2 => KEM::McEliece,
255 => KEM::X25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined KEM: {}", encoding[0]),
});
}
};
let hash_function = match encoding[1] {
0 => HashFunction::Blake3,
1 => HashFunction::SHAKE256,
2 => HashFunction::SHAKE128,
3 => HashFunction::SHA256,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Hash Function: {}", encoding[1]),
});
}
};
let custom_hash_length = match encoding[2] {
0 => None,
_ => Some(encoding[2]),
};
let signature_scheme = match encoding[3] {
0 => SignatureScheme::Ed25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Signature Scheme: {}", encoding[3]),
});
}
};
Self::resolve_ciphersuite(kem, hash_function, signature_scheme, custom_hash_length)
}
}
impl Display for Ciphersuite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&format!(
"{}_{}({})_{}",
self.kem, self.hash_function, self.hash_length, self.signature_scheme
)
.to_ascii_lowercase(),
)
}
}
pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Result<Algorithm, KKTError> {
match kem {
KEM::MlKem768 => Ok(Algorithm::MlKem768),
+6 -8
View File
@@ -6,7 +6,7 @@ use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_S
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::fmt::Display;
pub const KKT_CONTEXT_LEN: usize = 7;
pub const KKT_CONTEXT_LEN: usize = 3 + CIPHERSUITE_ENCODING_LEN;
// bitmask used: 0b1110_0000
#[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
@@ -202,13 +202,11 @@ impl KKTContext {
info: format!("Header - Invalid KKT Mode: {raw_kkt_mode}"),
})?;
let ciphersuite_bytes = header_bytes[2..6].try_into().map_err(|_| {
KKTError::CiphersuiteDecodingError {
info: format!(
"Incorrect Encoding Length: actual: 4 != expected: {CIPHERSUITE_ENCODING_LEN}",
),
}
})?;
// SAFETY: we're taking exactly `CIPHERSUITE_ENCODING_LEN` bytes
#[allow(clippy::unwrap_used)]
let ciphersuite_bytes = header_bytes[2..2 + CIPHERSUITE_ENCODING_LEN]
.try_into()
.unwrap();
Ok(KKTContext {
version: kkt_version,
+6 -9
View File
@@ -1,10 +1,7 @@
// Copyright 2025-2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
KKT_INITIAL_FRAME_AAD, ciphersuite::CURVE25519_KEY_LEN, context::KKTContext, error::KKTError,
frame::KKTFrame,
};
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;
@@ -36,7 +33,7 @@ impl KKTSessionSecret {
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..CURVE25519_KEY_LEN]);
pub_key.copy_from_slice(&public_key[0..x25519::PUBLIC_KEY_SIZE]);
// Todo: check validity of pk...
let pk = x25519::PublicKey::from(pub_key);
@@ -71,7 +68,7 @@ where
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() + CURVE25519_KEY_LEN);
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);
@@ -84,19 +81,19 @@ 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() < CURVE25519_KEY_LEN + TAG_LEN + NONCE_LEN {
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..CURVE25519_KEY_LEN],
&encrypted_frame_bytes[0..x25519::PUBLIC_KEY_SIZE],
)?;
let (kkt_frame, kkt_context) = decrypt_kkt_frame(
&shared_secret,
&encrypted_frame_bytes[CURVE25519_KEY_LEN..],
&encrypted_frame_bytes[x25519::PUBLIC_KEY_SIZE..],
KKT_INITIAL_FRAME_AAD,
)?;
Ok((shared_secret, kkt_frame, kkt_context))
+4 -4
View File
@@ -1,19 +1,19 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::KKTStatus;
use nym_kkt_ciphersuite::error::KKTCiphersuiteError;
use std::fmt::Debug;
use thiserror::Error;
use crate::context::KKTStatus;
#[derive(Error, Debug)]
pub enum KKTError {
#[error("Signature constructor error")]
SigConstructorError,
#[error("Signature verification error")]
SigVerifError,
#[error("Ciphersuite Decoding Error: {}", info)]
CiphersuiteDecodingError { info: String },
#[error(transparent)]
CiphersuiteDecodingError(#[from] KKTCiphersuiteError),
#[error("KEM mapping failure: {}", info)]
KEMMapping { info: &'static str },
+13 -20
View File
@@ -1,8 +1,9 @@
use crate::ciphersuite::HashFunction;
use std::collections::HashMap;
use classic_mceliece_rust::keypair_boxed;
use libcrux_sha3;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KEMKeyDigests};
use rand::{CryptoRng, RngCore};
pub fn generate_keypair_ed25519<R>(
@@ -72,26 +73,18 @@ pub fn hash_key_bytes(
hash_length: usize,
key_bytes: &[u8],
) -> Vec<u8> {
let mut hashed_key: Vec<u8> = vec![0u8; hash_length];
match hash_function {
HashFunction::Blake3 => {
let mut hasher = blake3::Hasher::new();
hasher.update(key_bytes);
hasher.finalize_xof().fill(&mut hashed_key);
hasher.reset();
}
HashFunction::SHAKE256 => {
libcrux_sha3::shake256_ema(&mut hashed_key, key_bytes);
}
HashFunction::SHAKE128 => {
libcrux_sha3::shake128_ema(&mut hashed_key, key_bytes);
}
HashFunction::SHA256 => {
libcrux_sha3::sha256_ema(&mut hashed_key, key_bytes);
}
}
hash_function.digest(key_bytes, hash_length)
}
hashed_key
/// attempt to produce digests of the provided key using all known [HashFunction] with a default
/// hash length where variable output is available
pub fn produce_key_digests(key_bytes: &[u8]) -> KEMKeyDigests {
use strum::IntoEnumIterator;
let mut digests = HashMap::new();
for hash in HashFunction::iter() {
digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN));
}
digests
}
/// This does NOT run in constant time.
+9
View File
@@ -3,6 +3,7 @@
use crate::{noise_protocol::NoiseError, replay::ReplayError};
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use nym_kkt::ciphersuite::{HashFunction, KEM};
use thiserror::Error;
#[derive(Error, Debug)]
@@ -93,4 +94,12 @@ pub enum LpError {
#[error("attempted to create an LP responder without providing a valid KEM key")]
ResponderWithMissingKEMKey,
#[error(
"there are no known digests for remote's KEM key with {kem} KEM and {hash_function} hash function"
)]
NoKnownKEMKeyDigests {
kem: KEM,
hash_function: HashFunction,
},
}
+20 -13
View File
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::ciphersuite::{HashFunction, KEM};
use std::collections::HashMap;
use std::sync::Arc;
/// Representation of a local Lewes Protocol peer
@@ -44,18 +46,21 @@ impl LpLocalPeer {
/// 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_digest = match &self.kem_psq {
None => Vec::new(),
Some(kem_keys) => nym_kkt::key_utils::hash_key_bytes(
&nym_kkt::ciphersuite::HashFunction::Blake3,
nym_kkt::ciphersuite::DEFAULT_HASH_LEN,
kem_keys.public_key().as_bytes(),
),
let expected_kem_key_digests = match &self.kem_psq {
None => HashMap::new(),
Some(kem_keys) => {
let hashes =
nym_kkt::key_utils::produce_key_digests(kem_keys.public_key().as_bytes());
let mut digests = HashMap::new();
digests.insert(KEM::X25519, hashes);
digests
}
};
LpRemotePeer {
ed25519_public: *self.ed25519.public_key(),
x25519_public: *self.x25519.public_key(),
expected_kem_key_digest,
expected_kem_key_digests,
}
}
@@ -81,8 +86,7 @@ pub struct LpRemotePeer {
pub(crate) x25519_public: x25519::PublicKey,
/// Expected digest of the remote's KEM key
// TODO: this might have to be replaced by a HashMap<HashFunction, Vec<u8>> instead
pub(crate) expected_kem_key_digest: Vec<u8>,
pub(crate) expected_kem_key_digests: HashMap<KEM, HashMap<HashFunction, Vec<u8>>>,
}
impl LpRemotePeer {
@@ -90,7 +94,7 @@ impl LpRemotePeer {
LpRemotePeer {
ed25519_public,
x25519_public,
expected_kem_key_digest: vec![],
expected_kem_key_digests: Default::default(),
}
}
@@ -103,8 +107,11 @@ impl LpRemotePeer {
}
#[must_use]
pub fn with_kem_key_digest(mut self, expected_kem_key_digest: Vec<u8>) -> Self {
self.expected_kem_key_digest = expected_kem_key_digest;
pub fn with_kem_key_digests(
mut self,
expected_kem_key_digests: HashMap<KEM, HashMap<HashFunction, Vec<u8>>>,
) -> Self {
self.expected_kem_key_digests = expected_kem_key_digests;
self
}
}
+20 -1
View File
@@ -20,6 +20,7 @@ use crate::{LpError, LpMessage, LpPacket};
use nym_crypto::asymmetric::x25519;
use nym_kkt::KKT_RESPONSE_AAD;
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
use nym_kkt::context::KKTContext;
use nym_kkt::encryption::{
KKTSessionSecret, decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
encrypt_kkt_frame,
@@ -640,6 +641,23 @@ impl LpSession {
}
}
/// Attempt to retrieve expected KEM key hash of the remote
/// for [KEM] key type and [HashFunction] specified by own [KKTContext]
fn expected_kem_key_hash(&self, context: KKTContext) -> Result<&Vec<u8>, LpError> {
let kem = context.ciphersuite().kem();
let hash_function = context.ciphersuite().hash_function();
let digests = self
.remote_peer
.expected_kem_key_digests
.get(&kem)
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
digests
.get(&hash_function)
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
}
/// Processes a KKT response from the responder.
///
/// Decrypts and validates the response and stores the authenticated KEM public key
@@ -674,12 +692,13 @@ impl LpSession {
let remote_encapsulation_key =
match decrypt_kkt_frame(&session_secret, encrypted_response_bytes, KKT_RESPONSE_AAD) {
Ok((response_frame, remote_context)) => {
let expected_kem_key_digest = self.expected_kem_key_hash(context)?;
match initiator_ingest_response(
&mut context,
&response_frame,
&remote_context,
&self.remote_peer.ed25519_public,
&self.remote_peer.expected_kem_key_digest,
expected_kem_key_digest,
) {
Ok(remote_encapsulation_key) => remote_encapsulation_key,
Err(e) => {
+1
View File
@@ -23,6 +23,7 @@ nym-crypto = { workspace = true }
nym-ip-packet-requests = { workspace = true }
nym-sphinx = { workspace = true }
nym-wireguard-types = { workspace = true }
nym-kkt-ciphersuite = { path = "../nym-kkt-ciphersuite" }
[dev-dependencies]
bincode.workspace = true
+4 -3
View File
@@ -1,6 +1,7 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use nym_authenticator_requests::AuthenticatorVersion;
@@ -15,6 +16,7 @@ mod serialisation;
pub use lp_messages::{
LpGatewayData, LpRegistrationRequest, LpRegistrationResponse, RegistrationMode,
};
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests};
pub use serialisation::BincodeError;
#[derive(Debug, Clone)]
@@ -35,11 +37,10 @@ pub struct GatewayData {
pub private_ipv6: Ipv6Addr,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug)]
pub struct LpData {
pub address: SocketAddr,
// TODO: modify it into a map once we know more about the PSQv2 structure
pub expected_kem_key_hash: Vec<u8>,
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
}
#[derive(Clone, Copy, Debug)]
+1
View File
@@ -31,6 +31,7 @@ nym-exit-policy = { workspace = true }
nym-noise-keys = { workspace = true }
nym-wireguard-types = { workspace = true }
nym-upgrade-mode-check = { workspace = true, features = ["openapi"] }
nym-kkt-ciphersuite = { path = "../../common/nym-kkt-ciphersuite" }
# feature-specific dependencies:
@@ -72,6 +72,28 @@ pub enum LPKEM {
McEliece,
}
impl From<LPKEM> for nym_kkt_ciphersuite::KEM {
fn from(lpkem: LPKEM) -> Self {
match lpkem {
LPKEM::MlKem768 => nym_kkt_ciphersuite::KEM::MlKem768,
LPKEM::XWing => nym_kkt_ciphersuite::KEM::XWing,
LPKEM::X25519 => nym_kkt_ciphersuite::KEM::X25519,
LPKEM::McEliece => nym_kkt_ciphersuite::KEM::McEliece,
}
}
}
impl From<nym_kkt_ciphersuite::KEM> for LPKEM {
fn from(kem: nym_kkt_ciphersuite::KEM) -> Self {
match kem {
nym_kkt_ciphersuite::KEM::MlKem768 => LPKEM::MlKem768,
nym_kkt_ciphersuite::KEM::XWing => LPKEM::XWing,
nym_kkt_ciphersuite::KEM::X25519 => LPKEM::X25519,
nym_kkt_ciphersuite::KEM::McEliece => LPKEM::McEliece,
}
}
}
#[derive(
Serialize,
Deserialize,
@@ -96,6 +118,28 @@ pub enum LPHashFunction {
Sha256,
}
impl From<LPHashFunction> for nym_kkt_ciphersuite::HashFunction {
fn from(lp_hash_fnction: LPHashFunction) -> Self {
match lp_hash_fnction {
LPHashFunction::Blake3 => nym_kkt_ciphersuite::HashFunction::Blake3,
LPHashFunction::Shake128 => nym_kkt_ciphersuite::HashFunction::SHAKE128,
LPHashFunction::Shake256 => nym_kkt_ciphersuite::HashFunction::SHAKE256,
LPHashFunction::Sha256 => nym_kkt_ciphersuite::HashFunction::SHA256,
}
}
}
impl From<nym_kkt_ciphersuite::HashFunction> for LPHashFunction {
fn from(kem: nym_kkt_ciphersuite::HashFunction) -> Self {
match kem {
nym_kkt_ciphersuite::HashFunction::Blake3 => LPHashFunction::Blake3,
nym_kkt_ciphersuite::HashFunction::SHAKE128 => LPHashFunction::Shake128,
nym_kkt_ciphersuite::HashFunction::SHAKE256 => LPHashFunction::Shake256,
nym_kkt_ciphersuite::HashFunction::SHA256 => LPHashFunction::Sha256,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+7 -20
View File
@@ -42,8 +42,7 @@ use nym_bin_common::bin_info;
use nym_credential_verification::UpgradeModeState;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender};
use nym_kkt::ciphersuite::{DEFAULT_HASH_LEN, HashFunction};
use nym_kkt::key_utils::hash_encapsulation_key;
use nym_kkt::key_utils::produce_key_digests;
use nym_mixnet_client::client::ActiveConnections;
use nym_mixnet_client::forwarder::MixForwardingSender;
use nym_network_requester::{
@@ -777,28 +776,16 @@ impl NymNode {
fn compute_kem_key_hashes(&self) -> (LPKEM, HashMap<LPHashFunction, Vec<u8>>) {
let kem = LPKEM::X25519;
let mut hashes = HashMap::new();
let kem_key_bytes = self.entry_gateway.psq_kem_key.public_key().as_bytes();
hashes.insert(
LPHashFunction::Blake3,
hash_encapsulation_key(&HashFunction::Blake3, DEFAULT_HASH_LEN, kem_key_bytes),
);
hashes.insert(
LPHashFunction::Shake128,
hash_encapsulation_key(&HashFunction::SHAKE128, DEFAULT_HASH_LEN, kem_key_bytes),
);
hashes.insert(
LPHashFunction::Shake256,
hash_encapsulation_key(&HashFunction::SHAKE256, DEFAULT_HASH_LEN, kem_key_bytes),
);
hashes.insert(
LPHashFunction::Sha256,
hash_encapsulation_key(&HashFunction::SHA256, DEFAULT_HASH_LEN, kem_key_bytes),
);
// convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests`
let digests = produce_key_digests(kem_key_bytes.as_ref())
.into_iter()
.map(|(f, d)| (f.into(), d))
.collect();
(kem, hashes)
(kem, digests)
}
pub(crate) async fn build_http_server(&self) -> Result<NymNodeHttpServer, NymNodeError> {
+2 -2
View File
@@ -194,10 +194,10 @@ impl RegistrationClient {
.map_err(|_| RegistrationClientError::X25519PubkeyConversionFailure)?;
let entry_peer = LpRemotePeer::new(self.config.entry.node.identity, entry_x25519_public)
.with_kem_key_digest(entry_lp_data.expected_kem_key_hash);
.with_kem_key_digests(entry_lp_data.expected_kem_key_hashes);
let exit_peer = LpRemotePeer::new(self.config.exit.node.identity, exit_x25519_public)
.with_kem_key_digest(exit_lp_data.expected_kem_key_hash);
.with_kem_key_digests(exit_lp_data.expected_kem_key_hashes);
// STEP 2: Establish outer session with entry gateway
// This creates the LP session that will be used to forward packets to exit.
+18 -4
View File
@@ -4181,18 +4181,19 @@ dependencies = [
[[package]]
name = "num_enum"
version = "0.7.3"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
dependencies = [
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.7.3"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
dependencies = [
"proc-macro-crate 3.3.0",
"proc-macro2",
@@ -4489,6 +4490,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "nym-kkt-ciphersuite"
version = "1.20.1"
dependencies = [
"num_enum",
"strum",
"strum_macros",
"thiserror 2.0.12",
]
[[package]]
name = "nym-mixnet-contract-common"
version = "1.20.1"
@@ -4535,6 +4546,8 @@ dependencies = [
"regex",
"schemars",
"serde",
"serde_json",
"tracing",
"url",
"utoipa",
]
@@ -4550,6 +4563,7 @@ dependencies = [
"nym-crypto",
"nym-exit-policy",
"nym-http-api-client",
"nym-kkt-ciphersuite",
"nym-noise-keys",
"nym-upgrade-mode-check",
"nym-wireguard-types",
+1
View File
@@ -28,6 +28,7 @@ url = { workspace = true }
# Nym crates
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-crypto = { path = "../../common/crypto" }
nym-kkt-ciphersuite = { path = "../../common/nym-kkt-ciphersuite" }
nym-http-api-client = { path = "../../common/http-api-client" }
nym-kcp = { path = "../../common/nym-kcp" }
nym-lp = { path = "../../common/nym-lp" }
+2 -2
View File
@@ -120,7 +120,7 @@ impl SpeedtestClient {
let client_ip = "0.0.0.0".parse()?;
let gw_peer = LpRemotePeer::new(self.gateway.identity, self.gateway.identity.to_x25519()?)
.with_kem_key_digest(self.gateway.kem_key_hash.clone());
.with_kem_key_digests(self.gateway.kem_key_hashes.clone());
let mut lp_client = LpRegistrationClient::<TcpStream>::new_with_default_config(
self.identity_keypair.clone(),
@@ -166,7 +166,7 @@ impl SpeedtestClient {
let client_ip = "0.0.0.0".parse()?;
let gw_peer = LpRemotePeer::new(self.gateway.identity, self.gateway.identity.to_x25519()?)
.with_kem_key_digest(self.gateway.kem_key_hash.clone());
.with_kem_key_digests(self.gateway.kem_key_hashes.clone());
let mut lp_client = LpRegistrationClient::new_with_default_config(
self.identity_keypair.clone(),
+4 -1
View File
@@ -6,14 +6,17 @@
#![allow(unused)]
use anyhow::{anyhow, bail, Context, Result};
use nym_api_requests::models::{LPHashFunction, LPKEM};
use nym_api_requests::nym_nodes::SkimmedNode;
use nym_crypto::asymmetric::ed25519;
use nym_http_api_client::UserAgent;
use nym_kkt_ciphersuite::{KEMKeyDigests, KEM};
use nym_sphinx_types::Node as SphinxNode;
use nym_topology::{NymRouteProvider, NymTopology, NymTopologyMetadata};
use nym_validator_client::nym_api::NymApiClientExt;
use rand::prelude::IteratorRandom;
use rand::{CryptoRng, Rng};
use std::collections::HashMap;
use std::net::SocketAddr;
use tracing::{debug, info};
use url::Url;
@@ -27,7 +30,7 @@ const LP_DATA_PORT: u16 = 51264;
#[derive(Debug, Clone)]
pub struct GatewayInfo {
pub identity: ed25519::PublicKey,
pub kem_key_hash: Vec<u8>,
pub kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
pub sphinx_key: nym_crypto::asymmetric::x25519::PublicKey,
/// Mix host (IP:port for Sphinx mixing)