From 654715bf81c7cb859ab1a047037f733a083eca44 Mon Sep 17 00:00:00 2001 From: Mike Lodder Date: Wed, 12 Aug 2026 09:22:26 -0600 Subject: [PATCH] implement kem trait Signed-off-by: Mike Lodder --- .github/workflows/sntrup-kem.yml | 1 + Cargo.lock | 3 + sntrup-kem/Cargo.toml | 11 + sntrup-kem/README.md | 24 +++ sntrup-kem/examples/kem_traits.rs | 43 ++++ sntrup-kem/src/kem.rs | 321 ++++++++++++++++++++++++------ sntrup-kem/src/lib.rs | 5 +- sntrup-kem/src/ops.rs | 79 ++++++++ sntrup-kem/src/types.rs | 6 +- 9 files changed, 425 insertions(+), 68 deletions(-) create mode 100644 sntrup-kem/examples/kem_traits.rs create mode 100644 sntrup-kem/src/ops.rs diff --git a/.github/workflows/sntrup-kem.yml b/.github/workflows/sntrup-kem.yml index ad410d9..18cdfbd 100644 --- a/.github/workflows/sntrup-kem.yml +++ b/.github/workflows/sntrup-kem.yml @@ -53,6 +53,7 @@ jobs: - run: cargo test --no-default-features - run: cargo test - run: cargo test --all-features + - run: cargo test --features kem - run: cargo test --features serde,force-scalar cross: diff --git a/Cargo.lock b/Cargo.lock index bc0835d..74e532b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1353,8 +1353,11 @@ dependencies = [ "criterion", "getrandom", "hex", + "hybrid-array", + "kem", "rand", "rand_chacha", + "rand_core", "serde", "serde_json", "serdect", diff --git a/sntrup-kem/Cargo.toml b/sntrup-kem/Cargo.toml index bbd4bc2..fa897a1 100644 --- a/sntrup-kem/Cargo.toml +++ b/sntrup-kem/Cargo.toml @@ -11,6 +11,9 @@ categories = ["algorithms", "cryptography"] readme = "README.md" edition = "2024" +[package.metadata.docs.rs] +features = ["kem", "serde"] + [features] default = ["kgen", "ecap", "dcap"] kgen = [] @@ -19,6 +22,7 @@ dcap = [] alloc = [] force-scalar = [] std = [] +kem = ["dep:hybrid-array", "dep:kem", "dep:rand_core", "kgen", "ecap", "dcap"] serde = ["dep:serdect", "dep:serde"] js = ["getrandom/wasm_js"] @@ -28,6 +32,9 @@ rand = "0.10.0" rand_chacha = "0.10.0" subtle = "2" getrandom = { version = "0.4", optional = true } +hybrid-array = { version = "0.4.14", features = ["extra-sizes"], optional = true } +kem = { version = "0.3", optional = true } +rand_core = { version = "0.10", optional = true } serde = { version = "1", optional = true, default-features = false } serdect = { version = "0.4", optional = true } # sha2 0.11 dropped the `asm` feature; hardware SHA acceleration is now selected @@ -40,6 +47,10 @@ zeroize = { version = "1", features = ["derive"] } criterion = "0.7" serde_json = "1" +[[example]] +name = "kem_traits" +required-features = ["kem"] + [[bench]] name = "mod" harness = false diff --git a/sntrup-kem/README.md b/sntrup-kem/README.md index 50b3bd4..05d125d 100644 --- a/sntrup-kem/README.md +++ b/sntrup-kem/README.md @@ -48,6 +48,7 @@ The KEM API is split into three default features so downstream crates can pull i | `kgen` | **yes** | Key generation: `SntrupKem::generate_key`, `SntrupKem::generate_key_deterministic` | | `ecap` | **yes** | Encapsulation: `EncapsulationKey::encapsulate` | | `dcap` | **yes** | Decapsulation: `DecapsulationKey::decapsulate` | +| `kem` | no | Implements the [`kem`](https://docs.rs/kem) crate traits for every parameter set | | `force-scalar` | no | Disable SIMD (AVX2/NEON) and use pure-Rust scalar code | | `serde` | no | Enables `Serialize`/`Deserialize` for all key and ciphertext types (via `serdect` for constant-time hex encoding) | | `js` | no | Enables WebAssembly support for `wasm32-unknown-unknown` by configuring `getrandom` to use JavaScript's `crypto.getRandomValues()` | @@ -175,6 +176,29 @@ let ek2 = EncapsulationKey::::try_from(ek_bytes).unwrap(); assert_eq!(ek, ek2); ``` +### `kem` crate integration + +Enable the `kem` feature to use any parameter set through the generic +[`kem`](https://docs.rs/kem) traits: + +```rust +# #[cfg(feature = "kem")] { +use rand::SeedableRng; +use rand::rngs::{StdRng, SysRng}; +use sntrup_kem::kem::{Decapsulate, Encapsulate, Kem, Sntrup761Params}; + +let Ok(mut rng) = StdRng::try_from_rng(&mut SysRng) else { + return; +}; +let (dk, ek) = Sntrup761Params::generate_keypair_from_rng(&mut rng); +let (ct, sent) = ek.encapsulate_with_rng(&mut rng); +assert_eq!(dk.decapsulate(&ct), sent); +# } +``` + +The module re-exports the traits and parameter-set marker types. See +`examples/kem_traits.rs` for generic use and key import/export. + ## WebAssembly To compile for `wasm32-unknown-unknown`, enable the `js` feature so that `getrandom` uses JavaScript's `crypto.getRandomValues()` for randomness: diff --git a/sntrup-kem/examples/kem_traits.rs b/sntrup-kem/examples/kem_traits.rs new file mode 100644 index 0000000..955f18f --- /dev/null +++ b/sntrup-kem/examples/kem_traits.rs @@ -0,0 +1,43 @@ +//! Streamlined NTRU Prime through the [`kem`](https://docs.rs/kem) crate traits. + +use rand::SeedableRng; +use rand::rngs::{StdRng, SysRng}; +use rand_core::CryptoRng; +use sntrup_kem::kem::{ + Decapsulate, DecapsulationKey, Decapsulator, Encapsulate, EncapsulationKey, Generate, Kem, + KemSizes, KeyExport, Sntrup653Params, Sntrup761Params, Sntrup1277Params, TryKeyInit, +}; + +fn round_trip(mut rng: impl CryptoRng) -> (usize, usize) +where + K: KemSizes + + Kem, DecapsulationKey = DecapsulationKey>, +{ + let (dk, ek) = K::generate_keypair_from_rng(&mut rng); + let (ct, sent) = ek.encapsulate_with_rng(&mut rng); + let received = dk.decapsulate(&ct); + assert_eq!(sent, received); + (ct.len(), received.len()) +} + +fn main() { + let Ok(mut rng) = StdRng::try_from_rng(&mut SysRng) else { + return; + }; + + for (ct, ss) in [ + round_trip::(&mut rng), + round_trip::(&mut rng), + round_trip::(&mut rng), + ] { + assert!(ct > 0); + assert_eq!(ss, 32); + } + + let dk = DecapsulationKey::::generate_from_rng(&mut rng); + let exported = dk.encapsulation_key().to_bytes(); + let Ok(imported) = EncapsulationKey::::new(&exported) else { + return; + }; + assert_eq!(&imported, dk.encapsulation_key()); +} diff --git a/sntrup-kem/src/kem.rs b/sntrup-kem/src/kem.rs index da02d07..b6fa0c8 100644 --- a/sntrup-kem/src/kem.rs +++ b/sntrup-kem/src/kem.rs @@ -1,79 +1,272 @@ -//! Internal KEM operations for Streamlined NTRU Prime. +//! Implementations of the traits from the [`kem`] crate. //! -//! Top-level keygen/encaps/decaps functions that delegate to `utils` for -//! the core cryptographic operations. - -use crate::params::SntrupParameters; -use crate::{r3, utils, zx}; -use rand::CryptoRng; -use zeroize::Zeroize; - -/// Generate a Streamlined NTRU Prime key pair. -/// -/// Returns `(pk_bytes, sk_bytes)` as `Vec`. -#[cfg(feature = "kgen")] -pub(crate) fn keygen(params: &SntrupParameters, rng: &mut impl CryptoRng) -> (Vec, Vec) { - let p = params.p; - - // Generate g and its reciprocal in R3 - let mut g = vec![0i8; p]; - let mut gr = loop { - zx::random::random_small(&mut g, rng); - let (mask, mut gr) = r3::reciprocal(&g, p); - if mask == 0 { - break gr; - } - // Rejected reciprocal is still derived from the secret g — wipe it. - gr.zeroize(); - }; +//! These adapters let Streamlined NTRU Prime parameter sets be used in generic +//! code alongside other KEMs. The traits are re-exported here so callers do not +//! need a direct, version-matched dependency on the `kem` crate. +//! +//! # Example +//! +//! ``` +//! use rand::SeedableRng; +//! use rand::rngs::{StdRng, SysRng}; +//! use sntrup_kem::kem::{Decapsulate, Encapsulate, Kem, Sntrup761Params}; +//! +//! let Ok(mut rng) = StdRng::try_from_rng(&mut SysRng) else { +//! return; +//! }; +//! let (dk, ek) = Sntrup761Params::generate_keypair_from_rng(&mut rng); +//! let (ct, sent) = ek.encapsulate_with_rng(&mut rng); +//! let received = dk.decapsulate(&ct); +//! +//! assert_eq!(sent, received); +//! ``` - // Generate f with Hamming weight w - let mut f = vec![0i8; p]; - zx::random::random_tsmall(&mut f, p, params.w, rng); +use hybrid_array::{Array, ArraySize}; +/// The [`kem`] crate traits, re-exported for use with this module's key types. +pub use kem::{ + Ciphertext, Decapsulate, Decapsulator, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport, + KeySizeUser, SharedKey, TryDecapsulate, TryKeyInit, +}; +use rand_core::{CryptoRng, TryCryptoRng}; +use zeroize::Zeroizing; - // Generate random rho for implicit rejection (raw random bytes, per PQClean) - let mut rho = vec![0u8; params.small_encode_size]; - rng.fill_bytes(&mut rho); +/// The parameter-set marker types supported by these trait implementations. +pub use crate::{ + Sntrup653Params, Sntrup761Params, Sntrup857Params, Sntrup953Params, Sntrup1013Params, + Sntrup1277Params, +}; - let result = utils::derive_key(&f, &g, &gr, &rho, params); +fn array_from_slice(bytes: &[u8]) -> Array { + let mut array = Array::default(); + array.copy_from_slice(bytes); + array +} - // Zeroize secret intermediates - f.zeroize(); - g.zeroize(); - gr.zeroize(); - rho.zeroize(); +/// Compile-time sizes used by the [`kem`] trait implementations. +pub trait KemSizes: + crate::SntrupParams + Copy + Clone + core::fmt::Debug + Eq + Ord + Send + Sync + 'static +{ + /// Encapsulation key size. + type EncapsulationKeySize: ArraySize; + /// Decapsulation key size. + type DecapsulationKeySize: ArraySize; + /// Ciphertext size. + type CiphertextSize: ArraySize; + /// Shared key size. + type SharedKeySize: ArraySize; +} + +/// A Streamlined NTRU Prime encapsulation key for use with the [`kem`] traits. +#[derive(Clone)] +pub struct EncapsulationKey(crate::EncapsulationKey

); - result +/// A Streamlined NTRU Prime decapsulation key for use with the [`kem`] traits. +pub struct DecapsulationKey { + key: crate::DecapsulationKey

, + encapsulation_key: EncapsulationKey

, } -/// Encapsulate with a public key. -/// -/// Returns `(ciphertext_bytes, shared_secret_bytes)`. -#[cfg(feature = "ecap")] -pub(crate) fn encaps( - pk: &[u8], - params: &SntrupParameters, - rng: &mut impl CryptoRng, -) -> (Vec, Vec) { - let p = params.p; +impl core::fmt::Debug for EncapsulationKey

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} - // Generate random r with Hamming weight w - let mut r = vec![0i8; p]; - zx::random::random_tsmall(&mut r, p, params.w, rng); +impl PartialEq for EncapsulationKey

{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} - let (ct, ss) = utils::create_cipher(&r, pk, params); +impl Eq for EncapsulationKey

{} - // Zeroize secret intermediate - r.zeroize(); +impl core::fmt::Debug for DecapsulationKey

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("DecapsulationKey") + .field("algorithm", &P::NAME) + .finish_non_exhaustive() + } +} - (ct, ss.to_vec()) +impl KeySizeUser for EncapsulationKey

{ + type KeySize = P::EncapsulationKeySize; } -/// Decapsulate with a secret key. -/// -/// Returns shared secret bytes. -#[cfg(feature = "dcap")] -pub(crate) fn decaps(sk: &[u8], ct: &[u8], params: &SntrupParameters) -> Vec { - let ss = utils::decapsulate_inner(ct, sk, params); - ss.to_vec() +impl TryKeyInit for EncapsulationKey

{ + fn new(key: &Key) -> Result { + crate::EncapsulationKey::

::try_from(key.as_slice()) + .map(Self) + .map_err(|_| InvalidKey) + } +} + +impl KeyExport for EncapsulationKey

{ + fn to_bytes(&self) -> Key { + array_from_slice(self.0.as_ref()) + } +} + +impl KeySizeUser for DecapsulationKey

{ + type KeySize = P::DecapsulationKeySize; +} + +impl TryKeyInit for DecapsulationKey

{ + fn new(key: &Key) -> Result { + let key = crate::DecapsulationKey::

::try_from(key.as_slice()).map_err(|_| InvalidKey)?; + let encapsulation_key = EncapsulationKey(key.encapsulation_key()); + Ok(Self { + key, + encapsulation_key, + }) + } +} + +impl KeyExport for DecapsulationKey

{ + fn to_bytes(&self) -> Key { + array_from_slice(self.key.as_ref()) + } +} + +impl Generate for DecapsulationKey

{ + fn try_generate_from_rng(rng: &mut R) -> Result { + let mut seed = Zeroizing::new([0u8; 32]); + rng.try_fill_bytes(seed.as_mut())?; + let (encapsulation_key, key) = crate::SntrupKem::

::generate_key_deterministic(&seed); + Ok(Self { + key, + encapsulation_key: EncapsulationKey(encapsulation_key), + }) + } +} + +impl

Decapsulator for DecapsulationKey

+where + P: KemSizes + Kem>, +{ + type Kem = P; + + fn encapsulation_key(&self) -> &EncapsulationKey

{ + &self.encapsulation_key + } +} + +impl

Decapsulate for DecapsulationKey

+where + P: KemSizes + Kem>, +{ + fn decapsulate(&self, ct: &Ciphertext

) -> SharedKey

{ + let Ok(ct) = crate::Ciphertext::

::try_from(ct.as_slice()) else { + return SharedKey::

::default(); + }; + array_from_slice(self.key.decapsulate(&ct).as_ref()) + } +} + +impl

Encapsulate for EncapsulationKey

+where + P: KemSizes + Kem, +{ + type Kem = P; + + fn encapsulate_with_rng(&self, mut rng: &mut R) -> (Ciphertext

, SharedKey

) + where + R: CryptoRng + ?Sized, + { + let (ct, ss) = self.0.encapsulate(&mut rng); + (array_from_slice(ct.as_ref()), array_from_slice(ss.as_ref())) + } +} + +macro_rules! impl_kem { + ($($params:ident, $ek:ident, $dk:ident, $ct:ident, $ss:ident;)+) => { + $( + impl KemSizes for $params { + type EncapsulationKeySize = hybrid_array::sizes::$ek; + type DecapsulationKeySize = hybrid_array::sizes::$dk; + type CiphertextSize = hybrid_array::sizes::$ct; + type SharedKeySize = hybrid_array::sizes::$ss; + } + + impl Kem for $params { + type DecapsulationKey = DecapsulationKey; + type EncapsulationKey = EncapsulationKey; + type SharedKeySize = hybrid_array::sizes::$ss; + type CiphertextSize = hybrid_array::sizes::$ct; + } + )+ + }; +} + +impl_kem! { + Sntrup653Params, U994, U1518, U897, U32; + Sntrup761Params, U1158, U1763, U1039, U32; + Sntrup857Params, U1322, U1999, U1184, U32; + Sntrup953Params, U1505, U2254, U1349, U32; + Sntrup1013Params, U1623, U2417, U1455, U32; + Sntrup1277Params, U2067, U3059, U1847, U32; +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::SeedableRng; + + fn traits_round_trip(seed: u8) + where + K: KemSizes + + Kem, DecapsulationKey = DecapsulationKey>, + { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([seed; 32]); + let (dk, ek) = K::generate_keypair_from_rng(&mut rng); + + assert!(!format!("{dk:?}").contains(&hex::encode(dk.key.as_ref()))); + + let (ct, sent) = ek.encapsulate_with_rng(&mut rng); + assert_eq!(dk.decapsulate(&ct), sent); + + let imported_ek = EncapsulationKey::::new(&ek.to_bytes()); + assert!(imported_ek.is_ok()); + if let Ok(imported_ek) = imported_ek { + assert_eq!(imported_ek, ek); + } + assert_eq!(dk.encapsulation_key(), &ek); + + let imported_dk = DecapsulationKey::::new(&dk.to_bytes()); + assert!(imported_dk.is_ok()); + if let Ok(imported_dk) = imported_dk { + assert_eq!(imported_dk.decapsulate(&ct), sent); + assert_eq!(imported_dk.encapsulation_key(), &ek); + } + } + + macro_rules! kem_trait_tests { + ($($name:ident, $params:ident, $seed:expr;)+) => { + $( + #[test] + fn $name() { + traits_round_trip::<$params>($seed); + } + )+ + }; + } + + kem_trait_tests! { + round_trip_653, Sntrup653Params, 0x30; + round_trip_761, Sntrup761Params, 0x31; + round_trip_857, Sntrup857Params, 0x32; + round_trip_953, Sntrup953Params, 0x33; + round_trip_1013, Sntrup1013Params, 0x34; + round_trip_1277, Sntrup1277Params, 0x35; + } + + #[test] + fn corrupted_ciphertext_yields_a_different_key() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0x41; 32]); + let (dk, ek) = Sntrup761Params::generate_keypair_from_rng(&mut rng); + let (mut ct, sent) = ek.encapsulate_with_rng(&mut rng); + + let last = ct.len() - 1; + ct[last] ^= 0xFF; + assert_ne!(dk.decapsulate(&ct), sent); + } } diff --git a/sntrup-kem/src/lib.rs b/sntrup-kem/src/lib.rs index 14bb092..7508b1d 100644 --- a/sntrup-kem/src/lib.rs +++ b/sntrup-kem/src/lib.rs @@ -45,6 +45,7 @@ //! - `kgen`: Key generation (default) //! - `ecap`: Encapsulation (default) //! - `dcap`: Decapsulation (default) +//! - `kem`: Implementations of the [`kem`](https://docs.rs/kem) crate traits //! - `serde`: Serde serialization support via `serdect` // The `kgen`/`ecap`/`dcap` features select which KEM operations are compiled. @@ -60,7 +61,9 @@ mod ct; mod error; -mod kem; +#[cfg(feature = "kem")] +pub mod kem; +mod ops; mod params; mod r3; mod rq; diff --git a/sntrup-kem/src/ops.rs b/sntrup-kem/src/ops.rs new file mode 100644 index 0000000..da02d07 --- /dev/null +++ b/sntrup-kem/src/ops.rs @@ -0,0 +1,79 @@ +//! Internal KEM operations for Streamlined NTRU Prime. +//! +//! Top-level keygen/encaps/decaps functions that delegate to `utils` for +//! the core cryptographic operations. + +use crate::params::SntrupParameters; +use crate::{r3, utils, zx}; +use rand::CryptoRng; +use zeroize::Zeroize; + +/// Generate a Streamlined NTRU Prime key pair. +/// +/// Returns `(pk_bytes, sk_bytes)` as `Vec`. +#[cfg(feature = "kgen")] +pub(crate) fn keygen(params: &SntrupParameters, rng: &mut impl CryptoRng) -> (Vec, Vec) { + let p = params.p; + + // Generate g and its reciprocal in R3 + let mut g = vec![0i8; p]; + let mut gr = loop { + zx::random::random_small(&mut g, rng); + let (mask, mut gr) = r3::reciprocal(&g, p); + if mask == 0 { + break gr; + } + // Rejected reciprocal is still derived from the secret g — wipe it. + gr.zeroize(); + }; + + // Generate f with Hamming weight w + let mut f = vec![0i8; p]; + zx::random::random_tsmall(&mut f, p, params.w, rng); + + // Generate random rho for implicit rejection (raw random bytes, per PQClean) + let mut rho = vec![0u8; params.small_encode_size]; + rng.fill_bytes(&mut rho); + + let result = utils::derive_key(&f, &g, &gr, &rho, params); + + // Zeroize secret intermediates + f.zeroize(); + g.zeroize(); + gr.zeroize(); + rho.zeroize(); + + result +} + +/// Encapsulate with a public key. +/// +/// Returns `(ciphertext_bytes, shared_secret_bytes)`. +#[cfg(feature = "ecap")] +pub(crate) fn encaps( + pk: &[u8], + params: &SntrupParameters, + rng: &mut impl CryptoRng, +) -> (Vec, Vec) { + let p = params.p; + + // Generate random r with Hamming weight w + let mut r = vec![0i8; p]; + zx::random::random_tsmall(&mut r, p, params.w, rng); + + let (ct, ss) = utils::create_cipher(&r, pk, params); + + // Zeroize secret intermediate + r.zeroize(); + + (ct, ss.to_vec()) +} + +/// Decapsulate with a secret key. +/// +/// Returns shared secret bytes. +#[cfg(feature = "dcap")] +pub(crate) fn decaps(sk: &[u8], ct: &[u8], params: &SntrupParameters) -> Vec { + let ss = utils::decapsulate_inner(ct, sk, params); + ss.to_vec() +} diff --git a/sntrup-kem/src/types.rs b/sntrup-kem/src/types.rs index 62fe0bf..742a4b6 100644 --- a/sntrup-kem/src/types.rs +++ b/sntrup-kem/src/types.rs @@ -288,7 +288,7 @@ impl SntrupKem

{ pub fn generate_key( rng: &mut impl rand::CryptoRng, ) -> (EncapsulationKey

, DecapsulationKey

) { - let (pk, sk) = crate::kem::keygen(P::params(), rng); + let (pk, sk) = crate::ops::keygen(P::params(), rng); ( EncapsulationKey::from_vec(pk), DecapsulationKey::from_vec(sk), @@ -312,7 +312,7 @@ impl SntrupKem

{ impl EncapsulationKey

{ /// Encapsulate: produce a ciphertext and shared secret. pub fn encapsulate(&self, rng: &mut impl rand::CryptoRng) -> (Ciphertext

, SharedSecret

) { - let (ct, ss) = crate::kem::encaps(&self.bytes, P::params(), rng); + let (ct, ss) = crate::ops::encaps(&self.bytes, P::params(), rng); (Ciphertext::from_vec(ct), SharedSecret::from_vec(ss)) } } @@ -325,7 +325,7 @@ impl DecapsulationKey

{ /// On failure, returns a pseudorandom key derived from rho, /// indistinguishable from a valid key to an attacker. pub fn decapsulate(&self, ct: &Ciphertext

) -> SharedSecret

{ - let ss = crate::kem::decaps(&self.bytes, &ct.bytes, P::params()); + let ss = crate::ops::decaps(&self.bytes, &ct.bytes, P::params()); SharedSecret::from_vec(ss) } }