diff --git a/Cargo.lock b/Cargo.lock index 2705d107e0..f4042c9595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 7cf7fbe6d2..e195477930 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [ diff --git a/common/nym-kkt-ciphersuite/Cargo.toml b/common/nym-kkt-ciphersuite/Cargo.toml new file mode 100644 index 0000000000..079325f625 --- /dev/null +++ b/common/nym-kkt-ciphersuite/Cargo.toml @@ -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 diff --git a/common/nym-kkt-ciphersuite/src/error.rs b/common/nym-kkt-ciphersuite/src/error.rs new file mode 100644 index 0000000000..f644888d1e --- /dev/null +++ b/common/nym-kkt-ciphersuite/src/error.rs @@ -0,0 +1,21 @@ +// Copyright 2026 - Nym Technologies SA +// 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 }, +} diff --git a/common/nym-kkt-ciphersuite/src/lib.rs b/common/nym-kkt-ciphersuite/src/lib.rs new file mode 100644 index 0000000000..c34d40f0bc --- /dev/null +++ b/common/nym-kkt-ciphersuite/src/lib.rs @@ -0,0 +1,337 @@ +// Copyright 2026 - Nym Technologies SA +// 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>; + +#[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>(&self, data: M, output_length: usize) -> Vec { + 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 { + // 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> for HashLength { + type Error = KKTCiphersuiteError; + + fn try_from(value: Option) -> Result { + 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, + ) -> Result { + 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 { + 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(), + ) + } +} diff --git a/common/nym-kkt/Cargo.toml b/common/nym-kkt/Cargo.toml index 14b3f2f9fc..26c6159c49 100644 --- a/common/nym-kkt/Cargo.toml +++ b/common/nym-kkt/Cargo.toml @@ -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" } diff --git a/common/nym-kkt/src/ciphersuite.rs b/common/nym-kkt/src/ciphersuite.rs index a822afdf7d..c87915881c 100644 --- a/common/nym-kkt/src/ciphersuite.rs +++ b/common/nym-kkt/src/ciphersuite.rs @@ -1,37 +1,10 @@ // Copyright 2025 - Nym Technologies SA // 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, - ) -> Result { - 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 { - 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 { match kem { KEM::MlKem768 => Ok(Algorithm::MlKem768), diff --git a/common/nym-kkt/src/context.rs b/common/nym-kkt/src/context.rs index 8ca1b351c7..8034c52af7 100644 --- a/common/nym-kkt/src/context.rs +++ b/common/nym-kkt/src/context.rs @@ -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, diff --git a/common/nym-kkt/src/encryption.rs b/common/nym-kkt/src/encryption.rs index 4176d45515..75257fc79c 100644 --- a/common/nym-kkt/src/encryption.rs +++ b/common/nym-kkt/src/encryption.rs @@ -1,10 +1,7 @@ // Copyright 2025-2026 - Nym Technologies SA // 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 { 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)) diff --git a/common/nym-kkt/src/error.rs b/common/nym-kkt/src/error.rs index 61744154a1..1defc1b1e0 100644 --- a/common/nym-kkt/src/error.rs +++ b/common/nym-kkt/src/error.rs @@ -1,19 +1,19 @@ // Copyright 2025 - Nym Technologies SA // 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 }, diff --git a/common/nym-kkt/src/key_utils.rs b/common/nym-kkt/src/key_utils.rs index 2845e7a907..e612cbe3bf 100644 --- a/common/nym-kkt/src/key_utils.rs +++ b/common/nym-kkt/src/key_utils.rs @@ -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( @@ -72,26 +73,18 @@ pub fn hash_key_bytes( hash_length: usize, key_bytes: &[u8], ) -> Vec { - let mut hashed_key: Vec = 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. diff --git a/common/nym-lp/src/error.rs b/common/nym-lp/src/error.rs index 290289fd0a..caedcc84e4 100644 --- a/common/nym-lp/src/error.rs +++ b/common/nym-lp/src/error.rs @@ -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, + }, } diff --git a/common/nym-lp/src/peer.rs b/common/nym-lp/src/peer.rs index 02a6372ea6..ee81fd5351 100644 --- a/common/nym-lp/src/peer.rs +++ b/common/nym-lp/src/peer.rs @@ -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> instead - pub(crate) expected_kem_key_digest: Vec, + pub(crate) expected_kem_key_digests: HashMap>>, } 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) -> Self { - self.expected_kem_key_digest = expected_kem_key_digest; + pub fn with_kem_key_digests( + mut self, + expected_kem_key_digests: HashMap>>, + ) -> Self { + self.expected_kem_key_digests = expected_kem_key_digests; self } } diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index 58683f2176..7e7a9e1b41 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -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, 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) => { diff --git a/common/registration/Cargo.toml b/common/registration/Cargo.toml index 530584e0e5..2320182701 100644 --- a/common/registration/Cargo.toml +++ b/common/registration/Cargo.toml @@ -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 diff --git a/common/registration/src/lib.rs b/common/registration/src/lib.rs index 37ad465765..04fff700c0 100644 --- a/common/registration/src/lib.rs +++ b/common/registration/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // 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, + pub expected_kem_key_hashes: HashMap, } #[derive(Clone, Copy, Debug)] diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 14a8403379..ec7b6822c8 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -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: diff --git a/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs b/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs index 4e668e5d75..7f41cb9a42 100644 --- a/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs @@ -72,6 +72,28 @@ pub enum LPKEM { McEliece, } +impl From 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 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 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 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::*; diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index a46ae3e7da..0167b0a253 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -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>) { 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 { diff --git a/nym-registration-client/src/lib.rs b/nym-registration-client/src/lib.rs index 796d56de74..fc2489909c 100644 --- a/nym-registration-client/src/lib.rs +++ b/nym-registration-client/src/lib.rs @@ -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. diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 047a50110b..bc4614b5ef 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -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", diff --git a/tools/nym-lp-client/Cargo.toml b/tools/nym-lp-client/Cargo.toml index 188b123a9a..1c6b18fc43 100644 --- a/tools/nym-lp-client/Cargo.toml +++ b/tools/nym-lp-client/Cargo.toml @@ -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" } diff --git a/tools/nym-lp-client/src/client.rs b/tools/nym-lp-client/src/client.rs index fe78da54b8..744bc14dcb 100644 --- a/tools/nym-lp-client/src/client.rs +++ b/tools/nym-lp-client/src/client.rs @@ -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::::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(), diff --git a/tools/nym-lp-client/src/topology.rs b/tools/nym-lp-client/src/topology.rs index cbc5a35699..ebf3d8997f 100644 --- a/tools/nym-lp-client/src/topology.rs +++ b/tools/nym-lp-client/src/topology.rs @@ -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, + pub kem_key_hashes: HashMap, pub sphinx_key: nym_crypto::asymmetric::x25519::PublicKey, /// Mix host (IP:port for Sphinx mixing)