From 47d369ff6e4b76a1334eedce31cb4ffe34f22993 Mon Sep 17 00:00:00 2001 From: Mike Lodder Date: Fri, 24 Jul 2026 13:54:37 -0600 Subject: [PATCH 1/2] add kem crate traits and various updates Signed-off-by: Mike Lodder --- Cargo.lock | 3 +- frodo-kem/Cargo.toml | 4 +- frodo-kem/src/hazmat/models.rs | 82 ++++++++- frodo-kem/src/hazmat/traits.rs | 62 ++++++- frodo-kem/src/kem.rs | 319 +++++++++++++++++++++++++++++++++ frodo-kem/src/lib.rs | 105 +++++++---- 6 files changed, 527 insertions(+), 48 deletions(-) create mode 100644 frodo-kem/src/kem.rs diff --git a/Cargo.lock b/Cargo.lock index 9e6db6b5..29c31d62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,9 +463,11 @@ dependencies = [ "aes", "chacha20", "criterion", + "ctutils", "getrandom", "hex", "hybrid-array", + "kem", "openssl-sys", "postcard", "rand_core", @@ -478,7 +480,6 @@ dependencies = [ "serdect", "sha3", "shake", - "subtle", "thiserror", "toml", "zeroize", diff --git a/frodo-kem/Cargo.toml b/frodo-kem/Cargo.toml index 3fec49cd..e07b279e 100644 --- a/frodo-kem/Cargo.toml +++ b/frodo-kem/Cargo.toml @@ -58,14 +58,16 @@ std = [] [dependencies] aes = { version = "0.9", optional = true } +ctutils = "0.4" hex = { version = "0.4", optional = true } +hybrid-array = { version = "0.4.13", features = ["extra-sizes"] } +kem = "0.3" openssl-sys = { version = "0.9.104", optional = true } rand_core = { version = "0.10", features = [] } serde = { version = "1.0", features = ["derive"], optional = true } serdect = "0.4" sha3 = "0.12" shake = "0.1" -subtle = "2.6" thiserror = "2.0" zeroize = "1" diff --git a/frodo-kem/src/hazmat/models.rs b/frodo-kem/src/hazmat/models.rs index 4405d377..a2a83012 100644 --- a/frodo-kem/src/hazmat/models.rs +++ b/frodo-kem/src/hazmat/models.rs @@ -4,6 +4,7 @@ use super::{Expanded, Kem, Params, Sample}; use crate::{Error, FrodoResult}; use alloc::{boxed::Box, vec::Vec}; use core::marker::PhantomData; +use ctutils::CtEq; use zeroize::{Zeroize, ZeroizeOnDrop}; macro_rules! from_slice_impl { @@ -88,6 +89,11 @@ from_slice_impl!(Ciphertext); serde_impl!(Ciphertext); impl Ciphertext

{ + /// Consume this ciphertext and return its serialized bytes. + pub(crate) fn into_vec(self) -> Vec { + self.0 + } + /// Convert a slice of bytes into a ciphertext pub fn from_slice(bytes: &[u8]) -> FrodoResult { if bytes.len() != P::CIPHERTEXT_LENGTH { @@ -150,6 +156,11 @@ impl<'a, P: Params> From<&'a Ciphertext

> for CiphertextRef<'a, P> { } impl<'a, P: Params> CiphertextRef<'a, P> { + /// Create a ciphertext reference from bytes whose length was validated by its type. + pub(crate) fn from_validated_slice(bytes: &'a [u8]) -> Self { + Self(bytes, PhantomData) + } + /// Create a ciphertext reference #[allow(dead_code)] pub fn from_slice(bytes: &'a [u8]) -> FrodoResult { @@ -255,6 +266,11 @@ impl EncryptionKey

{ pub struct EncryptionKeyRef<'a, P: Params>(pub(crate) &'a [u8], pub(crate) PhantomData

); impl<'a, P: Params> EncryptionKeyRef<'a, P> { + /// Create a public-key reference from bytes whose length was already validated. + pub(crate) fn from_validated_slice(bytes: &'a [u8]) -> Self { + Self(bytes, PhantomData) + } + /// Create a public key reference pub fn from_slice(bytes: &'a [u8]) -> FrodoResult { if bytes.len() != P::PUBLIC_KEY_LENGTH { @@ -284,9 +300,29 @@ impl<'a, P: Params> EncryptionKeyRef<'a, P> { } /// A FrodoKEM secret key -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone)] pub struct DecryptionKey(pub(crate) Vec, pub(crate) PhantomData

); +impl core::fmt::Debug for DecryptionKey

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("DecryptionKey").finish_non_exhaustive() + } +} + +impl CtEq for DecryptionKey

{ + fn ct_eq(&self, other: &Self) -> ctutils::Choice { + self.0.ct_eq(&other.0) + } +} + +impl Eq for DecryptionKey

{} + +impl PartialEq for DecryptionKey

{ + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + impl AsRef<[u8]> for DecryptionKey

{ fn as_ref(&self) -> &[u8] { &self.0 @@ -307,11 +343,22 @@ impl Zeroize for DecryptionKey

{ impl ZeroizeOnDrop for DecryptionKey

{} +impl Drop for DecryptionKey

{ + fn drop(&mut self) { + self.zeroize(); + } +} + from_slice_impl!(DecryptionKey); serde_impl!(DecryptionKey); impl DecryptionKey

{ + /// Consume this key and return its serialized bytes. + pub(crate) fn into_vec(mut self) -> Vec { + core::mem::take(&mut self.0) + } + /// Convert a slice of bytes into a secret key pub fn from_slice(bytes: &[u8]) -> FrodoResult { if bytes.len() != P::SECRET_KEY_LENGTH { @@ -430,9 +477,29 @@ impl<'a, P: Params> DecryptionKeyRef<'a, P> { } /// A FrodoKEM shared secret -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone)] pub struct SharedSecret(pub(crate) Vec, pub(crate) PhantomData

); +impl core::fmt::Debug for SharedSecret

{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SharedSecret").finish_non_exhaustive() + } +} + +impl CtEq for SharedSecret

{ + fn ct_eq(&self, other: &Self) -> ctutils::Choice { + self.0.ct_eq(&other.0) + } +} + +impl Eq for SharedSecret

{} + +impl PartialEq for SharedSecret

{ + fn eq(&self, other: &Self) -> bool { + bool::from(self.ct_eq(other)) + } +} + impl AsRef<[u8]> for SharedSecret

{ fn as_ref(&self) -> &[u8] { &self.0 @@ -453,11 +520,22 @@ impl Zeroize for SharedSecret

{ impl ZeroizeOnDrop for SharedSecret

{} +impl Drop for SharedSecret

{ + fn drop(&mut self) { + self.zeroize(); + } +} + from_slice_impl!(SharedSecret); serde_impl!(SharedSecret); impl SharedSecret

{ + /// Consume this shared secret and return its bytes. + pub(crate) fn into_vec(mut self) -> Vec { + core::mem::take(&mut self.0) + } + /// Convert a slice of bytes into a shared secret pub fn from_slice(bytes: &[u8]) -> FrodoResult { if bytes.len() != P::SHARED_SECRET_LENGTH { diff --git a/frodo-kem/src/hazmat/traits.rs b/frodo-kem/src/hazmat/traits.rs index e755a627..008e1874 100644 --- a/frodo-kem/src/hazmat/traits.rs +++ b/frodo-kem/src/hazmat/traits.rs @@ -5,9 +5,9 @@ use crate::hazmat::{ SharedSecret, }; use alloc::{string::String, vec::Vec}; +use ctutils::{Choice, CtEq, CtSelect}; use rand_core::CryptoRng; use sha3::digest::{ExtendableOutput, ExtendableOutputReset, Update}; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; use zeroize::Zeroize; /// Trait for implementing the FrodoKEM sampling algorithm @@ -120,10 +120,22 @@ pub trait Kem: Params + Expanded + Sample { &self, rng: &mut R, ) -> (EncryptionKey, DecryptionKey) { - let mut sk = DecryptionKey::default(); - let mut pk = EncryptionKey::default(); let mut randomness = vec![0u8; Self::KEY_SEED_SIZE]; rng.fill_bytes(&mut randomness); + self.generate_keypair_from_seed(&mut randomness) + } + + /// Generate a keypair from a seed. + /// + /// The seed is used as the randomness part of the keypair + /// and will be zeroized after the keypair is generated. + fn generate_keypair_from_seed( + &self, + randomness: &mut [u8], + ) -> (EncryptionKey, DecryptionKey) { + debug_assert_eq!(randomness.len(), Self::KEY_SEED_SIZE); + let mut sk = DecryptionKey::default(); + let mut pk = EncryptionKey::default(); sk.random_s_mut() .copy_from_slice(&randomness[..Self::SHARED_SECRET_LENGTH]); @@ -311,14 +323,45 @@ pub trait Kem: Params + Expanded + Sample { &self, secret_key: S, ciphertext: C, + ) -> (SharedSecret, Vec) { + self.decapsulate_inner(secret_key, ciphertext, true) + } + + /// Decapsulate a ciphertext when only the shared secret is needed. + /// + /// This avoids allocating and copying the recovered message. + fn decapsulate_shared_secret< + 'a, + 'b, + S: Into>, + C: Into>, + >( + &self, + secret_key: S, + ciphertext: C, + ) -> SharedSecret { + self.decapsulate_inner(secret_key, ciphertext, false).0 + } + + /// Internal decapsulation implementation with optional message recovery. + #[doc(hidden)] + fn decapsulate_inner< + 'a, + 'b, + S: Into>, + C: Into>, + >( + &self, + secret_key: S, + ciphertext: C, + recover_message: bool, ) -> (SharedSecret, Vec) { let secret_key = secret_key.into(); let ciphertext = ciphertext.into(); let mut ss = SharedSecret::default(); let mut matrix_s = vec![0u16; Self::N_X_N_BAR]; - let pk = EncryptionKeyRef::::from_slice(secret_key.public_key()) - .expect("Invalid public key"); + let pk = EncryptionKeyRef::::from_validated_slice(secret_key.public_key()); for (i, b) in matrix_s.iter_mut().enumerate() { let bb = [ @@ -416,7 +459,12 @@ pub trait Kem: Params + Expanded + Sample { shake.update(&fin_k); shake.finalize_xof_into(&mut ss.0); - let mu_prime = g2_in[Self::BYTES_PK_HASH..Self::BYTES_PK_HASH + Self::BYTES_MU].to_vec(); + fin_k.zeroize(); + let mu_prime = if recover_message { + g2_in[Self::BYTES_PK_HASH..Self::BYTES_PK_HASH + Self::BYTES_MU].to_vec() + } else { + Vec::new() + }; matrix_s.zeroize(); matrix_w.zeroize(); @@ -771,7 +819,7 @@ pub trait Kem: Params + Expanded + Sample { /// Constant time select for a u16 array fn ct_select(&self, choice: Choice, a: &[u8], b: &[u8], out: &mut [u8]) { for i in 0..a.len() { - out[i] = u8::conditional_select(&b[i], &a[i], choice); + out[i] = b[i].ct_select(&a[i], choice); } } } diff --git a/frodo-kem/src/kem.rs b/frodo-kem/src/kem.rs new file mode 100644 index 00000000..1e4dfee8 --- /dev/null +++ b/frodo-kem/src/kem.rs @@ -0,0 +1,319 @@ +//! Implementations of the traits from the [`kem`] crate. + +use crate::hazmat; +use alloc::vec; +use hybrid_array::{Array, ArraySize}; +use kem::{ + Ciphertext, Decapsulate, Decapsulator, Generate, InvalidKey, Key, KeyExport, KeySizeUser, + SharedKey, TryKeyInit, +}; +use rand_core::{CryptoRng, TryCryptoRng}; +use zeroize::Zeroizing; + +fn array_from_slice(bytes: &[u8]) -> Array { + let mut array = Array::default(); + array.copy_from_slice(bytes); + array +} + +#[cfg(feature = "efrodo640aes")] +pub use hazmat::EphemeralFrodoKem640Aes; +#[cfg(feature = "efrodo640shake")] +pub use hazmat::EphemeralFrodoKem640Shake; +#[cfg(feature = "efrodo976aes")] +pub use hazmat::EphemeralFrodoKem976Aes; +#[cfg(feature = "efrodo976shake")] +pub use hazmat::EphemeralFrodoKem976Shake; +#[cfg(feature = "efrodo1344aes")] +pub use hazmat::EphemeralFrodoKem1344Aes; +#[cfg(feature = "efrodo1344shake")] +pub use hazmat::EphemeralFrodoKem1344Shake; +#[cfg(feature = "frodo640aes")] +pub use hazmat::FrodoKem640Aes; +#[cfg(feature = "frodo640shake")] +pub use hazmat::FrodoKem640Shake; +#[cfg(feature = "frodo976aes")] +pub use hazmat::FrodoKem976Aes; +#[cfg(feature = "frodo976shake")] +pub use hazmat::FrodoKem976Shake; +#[cfg(feature = "frodo1344aes")] +pub use hazmat::FrodoKem1344Aes; +#[cfg(feature = "frodo1344shake")] +pub use hazmat::FrodoKem1344Shake; + +/// A typed FrodoKEM encapsulation (public) key. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EncapsulationKey(hazmat::EncryptionKey); + +/// A typed FrodoKEM decapsulation (secret) key. +pub struct DecapsulationKey { + key: hazmat::DecryptionKey, + encapsulation_key: EncapsulationKey, +} + +impl core::fmt::Debug for DecapsulationKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("DecapsulationKey") + .field("algorithm", &core::any::type_name::()) + .finish_non_exhaustive() + } +} + +/// Compile-time sizes used by the [`kem`] trait implementations. +#[doc(hidden)] +pub trait KemSizes: + hazmat::Kem + 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; +} + +impl KeySizeUser for EncapsulationKey { + type KeySize = K::EncapsulationKeySize; +} + +impl TryKeyInit for EncapsulationKey { + fn new(key: &Key) -> Result { + hazmat::EncryptionKey::from_slice(key) + .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 = K::DecapsulationKeySize; +} + +impl TryKeyInit for DecapsulationKey { + fn new(key: &Key) -> Result { + let key = hazmat::DecryptionKey::from_slice(key).map_err(|_| InvalidKey)?; + let encapsulation_key = EncapsulationKey((&key).into()); + 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(vec![0; K::KEY_SEED_SIZE]); + rng.try_fill_bytes(&mut seed)?; + let (ek, dk) = K::default().generate_keypair_from_seed(&mut seed); + Ok(Self { + key: dk, + encapsulation_key: EncapsulationKey(ek), + }) + } +} + +impl Decapsulator for DecapsulationKey +where + K: KemSizes + kem::Kem>, +{ + type Kem = K; + + fn encapsulation_key(&self) -> &EncapsulationKey { + &self.encapsulation_key + } +} + +impl Decapsulate for DecapsulationKey +where + K: KemSizes + kem::Kem>, +{ + fn decapsulate(&self, ct: &Ciphertext) -> SharedKey { + let ct = hazmat::CiphertextRef::from_validated_slice(ct); + let shared_key = K::default().decapsulate_shared_secret(&self.key, ct); + array_from_slice(shared_key.as_ref()) + } +} + +impl kem::Encapsulate for EncapsulationKey +where + K: KemSizes + kem::Kem, +{ + type Kem = K; + + fn encapsulate_with_rng(&self, rng: &mut R) -> (Ciphertext, SharedKey) + where + R: CryptoRng + ?Sized, + { + let (ct, shared_key) = K::default().encapsulate_with_rng(&self.0, rng); + ( + array_from_slice(ct.as_ref()), + array_from_slice(shared_key.as_ref()), + ) + } +} + +macro_rules! impl_kem { + ($feature:literal, $kem:ty, $ek:ty, $dk:ty, $ct:ty, $ss:ty) => { + #[cfg(feature = $feature)] + impl KemSizes for $kem { + type EncapsulationKeySize = $ek; + type DecapsulationKeySize = $dk; + type CiphertextSize = $ct; + type SharedKeySize = $ss; + } + + #[cfg(feature = $feature)] + impl kem::Kem for $kem { + type DecapsulationKey = DecapsulationKey; + type EncapsulationKey = EncapsulationKey; + type SharedKeySize = $ss; + type CiphertextSize = $ct; + } + }; +} + +impl_kem!( + "frodo640aes", + FrodoKem640Aes, + hybrid_array::sizes::U9616, + hybrid_array::sizes::U19888, + hybrid_array::sizes::U9752, + hybrid_array::sizes::U16 +); +impl_kem!( + "frodo640shake", + FrodoKem640Shake, + hybrid_array::sizes::U9616, + hybrid_array::sizes::U19888, + hybrid_array::sizes::U9752, + hybrid_array::sizes::U16 +); +impl_kem!( + "frodo976aes", + FrodoKem976Aes, + hybrid_array::sizes::U15632, + hybrid_array::sizes::U31296, + hybrid_array::sizes::U15792, + hybrid_array::sizes::U24 +); +impl_kem!( + "frodo976shake", + FrodoKem976Shake, + hybrid_array::sizes::U15632, + hybrid_array::sizes::U31296, + hybrid_array::sizes::U15792, + hybrid_array::sizes::U24 +); +impl_kem!( + "frodo1344aes", + FrodoKem1344Aes, + hybrid_array::sizes::U21520, + hybrid_array::sizes::U43088, + hybrid_array::sizes::U21696, + hybrid_array::sizes::U32 +); +impl_kem!( + "frodo1344shake", + FrodoKem1344Shake, + hybrid_array::sizes::U21520, + hybrid_array::sizes::U43088, + hybrid_array::sizes::U21696, + hybrid_array::sizes::U32 +); +impl_kem!( + "efrodo640aes", + EphemeralFrodoKem640Aes, + hybrid_array::sizes::U9616, + hybrid_array::sizes::U19888, + hybrid_array::sizes::U9720, + hybrid_array::sizes::U16 +); +impl_kem!( + "efrodo640shake", + EphemeralFrodoKem640Shake, + hybrid_array::sizes::U9616, + hybrid_array::sizes::U19888, + hybrid_array::sizes::U9720, + hybrid_array::sizes::U16 +); +impl_kem!( + "efrodo976aes", + EphemeralFrodoKem976Aes, + hybrid_array::sizes::U15632, + hybrid_array::sizes::U31296, + hybrid_array::sizes::U15744, + hybrid_array::sizes::U24 +); +impl_kem!( + "efrodo976shake", + EphemeralFrodoKem976Shake, + hybrid_array::sizes::U15632, + hybrid_array::sizes::U31296, + hybrid_array::sizes::U15744, + hybrid_array::sizes::U24 +); +impl_kem!( + "efrodo1344aes", + EphemeralFrodoKem1344Aes, + hybrid_array::sizes::U21520, + hybrid_array::sizes::U43088, + hybrid_array::sizes::U21632, + hybrid_array::sizes::U32 +); +impl_kem!( + "efrodo1344shake", + EphemeralFrodoKem1344Shake, + hybrid_array::sizes::U21520, + hybrid_array::sizes::U43088, + hybrid_array::sizes::U21632, + hybrid_array::sizes::U32 +); + +#[cfg(all(test, feature = "frodo640shake"))] +mod tests { + use super::*; + use kem::{Encapsulate, Kem, KeyExport, TryKeyInit}; + use rand_core::SeedableRng; + + #[test] + fn kem_traits_round_trip() { + type K = FrodoKem640Shake; + + let mut rng = chacha20::ChaCha8Rng::from_seed([42; 32]); + let (dk, ek) = K::generate_keypair_from_rng(&mut rng); + let debug = format!("{dk:?}"); + assert!(!debug.contains(&hex::encode(dk.key.as_ref()))); + let dk = DecapsulationKey::::new(&dk.to_bytes()).expect("decapsulation key imports"); + let ek = EncapsulationKey::::new(&ek.to_bytes()).expect("encapsulation key imports"); + let (ct, sent) = ek.encapsulate_with_rng(&mut rng); + let received = dk.decapsulate(&ct); + + assert_eq!(sent, received); + } + + #[cfg(feature = "efrodo640shake")] + #[test] + fn ephemeral_kem_traits_round_trip() { + type K = EphemeralFrodoKem640Shake; + + let mut rng = chacha20::ChaCha8Rng::from_seed([24; 32]); + 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); + } +} diff --git a/frodo-kem/src/lib.rs b/frodo-kem/src/lib.rs index c9b5cb5c..c26d2810 100644 --- a/frodo-kem/src/lib.rs +++ b/frodo-kem/src/lib.rs @@ -101,6 +101,9 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; +pub mod kem; +pub use kem::{DecapsulationKey as KemDecapsulationKey, EncapsulationKey as KemEncapsulationKey}; + #[cfg(feature = "hazmat")] pub mod hazmat; #[cfg(not(feature = "hazmat"))] @@ -111,9 +114,9 @@ pub use error::*; use alloc::vec::Vec; use core::marker::PhantomData; +use ctutils::{Choice, CtEq}; use hazmat::*; use rand_core::CryptoRng; -use subtle::{Choice, ConstantTimeEq}; use zeroize::{Zeroize, ZeroizeOnDrop}; #[cfg(feature = "serde")] @@ -235,7 +238,7 @@ macro_rules! serde_impl { macro_rules! ct_eq_imp { ($name:ident) => { - impl ConstantTimeEq for $name { + impl CtEq for $name { fn ct_eq(&self, other: &Self) -> Choice { self.algorithm.ct_eq(&other.algorithm) & self.value.ct_eq(&other.value) } @@ -245,7 +248,7 @@ macro_rules! ct_eq_imp { impl PartialEq for $name { fn eq(&self, other: &Self) -> bool { - self.ct_eq(other).unwrap_u8() == 1 + bool::from(self.ct_eq(other)) } } }; @@ -353,12 +356,20 @@ impl EncryptionKey { } /// A `FrodoKEM` secret key -#[derive(Debug, Clone, Default)] +#[derive(Clone, Default)] pub struct DecryptionKey { pub(crate) algorithm: Algorithm, pub(crate) value: Vec, } +impl core::fmt::Debug for DecryptionKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("DecryptionKey") + .field("algorithm", &self.algorithm) + .finish_non_exhaustive() + } +} + impl AsRef<[u8]> for DecryptionKey { fn as_ref(&self) -> &[u8] { self.value.as_ref() @@ -377,6 +388,12 @@ impl Zeroize for DecryptionKey { impl ZeroizeOnDrop for DecryptionKey {} +impl Drop for DecryptionKey { + fn drop(&mut self) { + self.zeroize(); + } +} + impl DecryptionKey { /// Get the algorithm #[must_use] @@ -406,12 +423,20 @@ impl DecryptionKey { } /// A `FrodoKEM` shared secret -#[derive(Debug, Clone, Default)] +#[derive(Clone, Default)] pub struct SharedSecret { pub(crate) algorithm: Algorithm, pub(crate) value: Vec, } +impl core::fmt::Debug for SharedSecret { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SharedSecret") + .field("algorithm", &self.algorithm) + .finish_non_exhaustive() + } +} + impl AsRef<[u8]> for SharedSecret { fn as_ref(&self) -> &[u8] { self.value.as_ref() @@ -430,6 +455,12 @@ impl Zeroize for SharedSecret { impl ZeroizeOnDrop for SharedSecret {} +impl Drop for SharedSecret { + fn drop(&mut self) { + self.zeroize(); + } +} + impl SharedSecret { /// Get the algorithm #[must_use] @@ -490,7 +521,7 @@ pub enum Algorithm { EphemeralFrodoKem1344Shake, } -impl ConstantTimeEq for Algorithm { +impl CtEq for Algorithm { fn ct_eq(&self, other: &Self) -> Choice { match (self, other) { #[cfg(feature = "efrodo640aes")] @@ -1049,7 +1080,7 @@ impl Algorithm { fn inner_decryption_key_from_bytes(&self, buf: &[u8]) -> FrodoResult { hazmat::DecryptionKey::

::from_slice(buf).map(|s| DecryptionKey { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } @@ -1116,55 +1147,55 @@ impl Algorithm { Self::FrodoKem640Aes => { hazmat::Ciphertext::::from_slice(buf).map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo976aes")] Self::FrodoKem976Aes => { hazmat::Ciphertext::::from_slice(buf).map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo1344aes")] Self::FrodoKem1344Aes => { hazmat::Ciphertext::::from_slice(buf).map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo640shake")] Self::FrodoKem640Shake => { hazmat::Ciphertext::::from_slice(buf).map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo976shake")] Self::FrodoKem976Shake => { hazmat::Ciphertext::::from_slice(buf).map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo1344shake")] Self::FrodoKem1344Shake => hazmat::Ciphertext::::from_slice(buf) .map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }), #[cfg(feature = "efrodo640aes")] Self::EphemeralFrodoKem640Aes => { hazmat::Ciphertext::::from_slice(buf).map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "efrodo976aes")] Self::EphemeralFrodoKem976Aes => { hazmat::Ciphertext::::from_slice(buf).map(|s| Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "efrodo1344aes")] @@ -1172,7 +1203,7 @@ impl Algorithm { hazmat::Ciphertext::::from_slice(buf).map(|s| { Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1181,7 +1212,7 @@ impl Algorithm { hazmat::Ciphertext::::from_slice(buf).map(|s| { Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1190,7 +1221,7 @@ impl Algorithm { hazmat::Ciphertext::::from_slice(buf).map(|s| { Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1199,7 +1230,7 @@ impl Algorithm { hazmat::Ciphertext::::from_slice(buf).map(|s| { Ciphertext { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1215,47 +1246,47 @@ impl Algorithm { Self::FrodoKem640Aes => { hazmat::SharedSecret::::from_slice(buf).map(|s| SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo976aes")] Self::FrodoKem976Aes => { hazmat::SharedSecret::::from_slice(buf).map(|s| SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo1344aes")] Self::FrodoKem1344Aes => { hazmat::SharedSecret::::from_slice(buf).map(|s| SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), }) } #[cfg(feature = "frodo640shake")] Self::FrodoKem640Shake => hazmat::SharedSecret::::from_slice(buf) .map(|s| SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), }), #[cfg(feature = "frodo976shake")] Self::FrodoKem976Shake => hazmat::SharedSecret::::from_slice(buf) .map(|s| SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), }), #[cfg(feature = "frodo1344shake")] Self::FrodoKem1344Shake => hazmat::SharedSecret::::from_slice(buf) .map(|s| SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), }), #[cfg(feature = "efrodo640aes")] Self::EphemeralFrodoKem640Aes => { hazmat::SharedSecret::::from_slice(buf).map(|s| { SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1264,7 +1295,7 @@ impl Algorithm { hazmat::SharedSecret::::from_slice(buf).map(|s| { SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1273,7 +1304,7 @@ impl Algorithm { hazmat::SharedSecret::::from_slice(buf).map(|s| { SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1282,7 +1313,7 @@ impl Algorithm { hazmat::SharedSecret::::from_slice(buf).map(|s| { SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1291,7 +1322,7 @@ impl Algorithm { hazmat::SharedSecret::::from_slice(buf).map(|s| { SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1300,7 +1331,7 @@ impl Algorithm { hazmat::SharedSecret::::from_slice(buf).map(|s| { SharedSecret { algorithm: *self, - value: s.0, + value: s.into_vec(), } }) } @@ -1364,7 +1395,7 @@ impl Algorithm { }, DecryptionKey { algorithm: *self, - value: sk.0, + value: sk.into_vec(), }, ) } @@ -1444,11 +1475,11 @@ impl Algorithm { Ok(( Ciphertext { algorithm: *self, - value: ct.0, + value: ct.into_vec(), }, SharedSecret { algorithm: *self, - value: ss.0, + value: ss.into_vec(), }, )) } @@ -1521,11 +1552,11 @@ impl Algorithm { Ok(( Ciphertext { algorithm: *self, - value: ct.0, + value: ct.into_vec(), }, SharedSecret { algorithm: *self, - value: ss.0, + value: ss.into_vec(), }, )) } @@ -1600,7 +1631,7 @@ impl Algorithm { Ok(( SharedSecret { algorithm: *self, - value: ss.0, + value: ss.into_vec(), }, mu, )) From d39b79d660fdc68b1f6d8288c050b06b70304854 Mon Sep 17 00:00:00 2001 From: Mike Lodder Date: Fri, 24 Jul 2026 15:41:14 -0600 Subject: [PATCH 2/2] updates for iso Signed-off-by: Mike Lodder --- frodo-kem/CONFORMANCE.md | 85 +++++++++++++++++++++++++++++ frodo-kem/Cargo.toml | 2 +- frodo-kem/README.md | 47 ++++++++++++---- frodo-kem/src/error.rs | 6 ++ frodo-kem/src/hazmat/traits.rs | 20 +++---- frodo-kem/src/lib.rs | 71 +++++++++++++++++++++++- frodo-kem/tests/efrodoKAT/README.md | 10 +++- frodo-kem/tests/frodo.rs | 4 ++ frodo-kem/tests/frodoKAT/README.md | 7 ++- 9 files changed, 221 insertions(+), 31 deletions(-) create mode 100644 frodo-kem/CONFORMANCE.md diff --git a/frodo-kem/CONFORMANCE.md b/frodo-kem/CONFORMANCE.md new file mode 100644 index 00000000..8b0e1c53 --- /dev/null +++ b/frodo-kem/CONFORMANCE.md @@ -0,0 +1,85 @@ +# ISO/IEC 18033-2 FrodoKEM conformance review + +This document records the implementation review against Clause 14 of +ISO/IEC 18033-2:2006/Amd 2:2026. It is an engineering conformance review, not +an ISO certification or an independent security audit. + +## Conformance criteria + +Clause 14.2 requires a conforming implementation to identify `Frodo.KeyGen`, +`Frodo.Encaps`, or `Frodo.Decaps`, identify a parameter set listed by the +standard, and compute the corresponding mathematical function exactly. + +This crate maps those functions as follows: + +| ISO function | Rust implementation | +| --- | --- | +| `Frodo.KeyGen` | `hazmat::Kem::generate_keypair_from_seed` | +| `Frodo.Encaps` | `hazmat::Kem::encapsulate` | +| `Frodo.Decaps` | `hazmat::Kem::decapsulate` | + +The randomized wrappers obtain the exact amount of randomness required by the +selected parameter set before calling these deterministic functions. + +## Parameter sets + +The implementation covers the AES128 and SHAKE128 matrix-generation options +for each standard and ephemeral parameter set: + +- FrodoKEM-640, FrodoKEM-976, and FrodoKEM-1344 +- eFrodoKEM-640, eFrodoKEM-976, and eFrodoKEM-1344 + +The implementation uses the specified dimensions, modulus, encoding width, +CDF tables, SHAKE variant, key sizes, ciphertext sizes, shared-secret sizes, +salt sizes, and `seedSE` sizes. Standard FrodoKEM uses a salt and a `seedSE` +twice the security parameter. Ephemeral FrodoKEM uses no salt and a `seedSE` +equal to the security parameter. + +## Algorithm review + +- Key generation parses randomness as `s || seedSE || z`, derives `seedA`, + samples `S` and `E`, computes and packs `B`, hashes the public key, and emits + the specified public and secret key encodings. +- Encapsulation hashes `pk || mu || salt` as specified, uses the `0x96` domain + separator, samples all three error matrices, computes and packs `B'` and + `C`, appends the salt, and derives the shared secret from the complete + ciphertext and `k`. +- Decapsulation unpacks the ciphertext, recovers `mu'`, recomputes the + ciphertext, compares both matrix components in constant time, and selects + between `k'` and the fallback secret `s` without a secret-dependent branch. +- CDF sampling scans the complete table for every coefficient without a + secret-dependent branch. +- Dynamic API boundaries reject keys and ciphertexts tagged for another + parameter set and reject invalid message, salt, key, and ciphertext lengths. +- Secret keys, shared secrets, intermediate sampling material, recovered + messages, and fallback key material are zeroized where owned by the + implementation. + +## Known-answer tests + +The repository contains all 1,200 KAT cases from the FrodoKEM team's official +reference implementation at commit +`7a4e7219d06305e16aef734213001cd8fefbcc14`: + +- 600 standard FrodoKEM cases; +- 600 ephemeral FrodoKEM cases; +- 100 cases for every AES and SHAKE parameter set. + +Each case checks the generated public key, secret key, ciphertext, encapsulated +shared secret, and decapsulated shared secret. The test harness also requires +exactly 100 records in every vector file, preventing silently truncated vector +sets from passing. + +Additional tests cover parameter sizes, modified-ciphertext implicit +rejection, deterministic fallback output, and serialization. + +## Review limitations + +- No claim of ISO certification or third-party validation is made. +- Constant-time properties are established by source review and use of + constant-time primitives; they have not been independently verified on + every compiler, target, or microarchitecture. +- Resistance to physical side channels such as power or electromagnetic + analysis is outside the scope of this software review. +- Applications selecting eFrodoKEM must enforce its per-public-key ciphertext + limit. The crate cannot enforce that protocol-level lifecycle requirement. diff --git a/frodo-kem/Cargo.toml b/frodo-kem/Cargo.toml index e07b279e..5cc68417 100644 --- a/frodo-kem/Cargo.toml +++ b/frodo-kem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frodo-kem" -version = "0.1.0" +version = "0.2.0" description = "Pure Rust implementation of FrodoKEM and eFrodoKEM" authors = ["The RustCrypto Team"] documentation = "https://docs.rs/frodo-kem" diff --git a/frodo-kem/README.md b/frodo-kem/README.md index d9c41049..512af985 100644 --- a/frodo-kem/README.md +++ b/frodo-kem/README.md @@ -6,18 +6,40 @@ ![Apache2/MIT licensed][license-image] ![MSRV][msrv-image] -A pure rust implementation of -- [FrodoKEM Learning with Errors Key Encapsulation](https://frodokem.org/files/FrodoKEM-specification-20210604.pdf). -- [ISO Standard](https://frodokem.org/files/FrodoKEM-standard_proposal-20230314.pdf) -- [ISO Standard Annex](https://frodokem.org/files/FrodoKEM-annex-20230418.pdf) +A pure Rust implementation of FrodoKEM and eFrodoKEM as specified in +[ISO/IEC 18033-2:2006/Amd 2:2026][iso-standard]. -It's submission was included in NIST's PQ Round 3 competition, and is now being standardized at ISO. +FrodoKEM was an alternate candidate in round 3 of the NIST Post-Quantum +Cryptography Standardization Project and is now standardized by ISO. + +## ISO conformance + +This crate implements the `Frodo.KeyGen`, `Frodo.Encaps`, and `Frodo.Decaps` +mathematical functions from Clause 14 of ISO/IEC 18033-2:2006/Amd 2:2026 for +all twelve parameter sets listed below. + +Algorithmic conformance is checked against all 1,200 known-answer tests from +the FrodoKEM team's [official reference implementation][reference-kats]: +100 cases for each standard and ephemeral AES and SHAKE parameter set. The +tests verify deterministic key generation, encapsulation, and decapsulation +outputs. Additional tests exercise implicit rejection of modified ciphertexts, +parameter sizes, and serialization. + +Based on a clause-by-clause implementation review and the complete official +KAT suite, this crate conforms to the FrodoKEM algorithms and parameter sets +specified by Clause 14 of ISO/IEC 18033-2:2006/Amd 2:2026. + +This conformance assessment has not been independently verified by a third +party. The crate has not received ISO certification, an accredited +conformance assessment, or an independent security audit. See the detailed +[conformance review](CONFORMANCE.md) for the evidence and limitations behind +the claim. ## ⚠️ Security Warning -This crate has been tested against the test vectors provided by the FrodoKEM team -and been rigorously tested for correctness, performance, and security. It has -also been tested against opensafequatum's [liboqs](https://github.com/open-quantum-safe/liboqs) library to compatibility and correctness. +This crate has been tested against the test vectors provided by the FrodoKEM +team and for interoperability with Open Quantum Safe's +[liboqs](https://github.com/open-quantum-safe/liboqs). The implementation contained in this crate has never been independently audited! @@ -40,8 +62,9 @@ This crate provides the following FrodoKEM algorithms: - [x] eFrodoKEM-976-SHAKE ✅ - [x] eFrodoKEM-1344-SHAKE ✅ -eFrodoKEM is a variant of FrodoKEM that is meant to be used one-time only. Using more than once -is considered a security risk. +eFrodoKEM is intended only for applications that guarantee a small number of +ciphertexts per public key (for example, at most 28). Prefer standard +FrodoKEM unless that usage restriction is enforced by the application. When in doubt use the FrodoKEM algorithm variants. @@ -55,7 +78,7 @@ To speed up AES, there are a few options available: - `frodo-kem = { version = "0.3", features = ["openssl"] }` uses the `openssl` crate for AES. By default, the `aes` feature auto-detects the best AES implementation for your platform -for x86 and x86_64, +for x86 and x86\_64, but not on ARMv8 where it defaults to the software implementation as of this writing. To enable the ARMv8 AES instructions, the `aes_armv8` feature is enabled in the `.cargo/config` file in this crate. @@ -112,3 +135,5 @@ conditions. [docs-link]: https://docs.rs/frodo-kem/ [license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg [msrv-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg +[iso-standard]: https://www.iso.org/standard/86890.html +[reference-kats]: https://github.com/microsoft/PQCrypto-LWEKE/tree/7a4e7219d06305e16aef734213001cd8fefbcc14 diff --git a/frodo-kem/src/error.rs b/frodo-kem/src/error.rs index 7f364e40..271133a5 100644 --- a/frodo-kem/src/error.rs +++ b/frodo-kem/src/error.rs @@ -18,6 +18,12 @@ pub enum Error { /// The message length is invalid #[error("Invalid message length: {0}")] InvalidMessageLength(usize), + /// The salt length is invalid + #[error("Invalid salt length: {0}")] + InvalidSaltLength(usize), + /// A key or ciphertext belongs to a different algorithm + #[error("Algorithm mismatch")] + AlgorithmMismatch, /// Parsing string to algorithm #[error("Unsupported algorithm")] UnsupportedAlgorithm, diff --git a/frodo-kem/src/hazmat/traits.rs b/frodo-kem/src/hazmat/traits.rs index 008e1874..42adcc2f 100644 --- a/frodo-kem/src/hazmat/traits.rs +++ b/frodo-kem/src/hazmat/traits.rs @@ -113,9 +113,8 @@ pub trait Kem: Params + Expanded + Sample { /// Generate a keypair /// - /// See Algorithm 12 in [spec](https://frodokem.org/files/FrodoKEM-specification-20210604.pdf). - /// Algorithm 8.1 in [iso](https://frodokem.org/files/FrodoKEM-standard_proposal-20230314.pdf). - /// Algorithm 1 in [annex](https://frodokem.org/files/FrodoKEM-annex-20230418.pdf) + /// Implements `Frodo.KeyGen` from Clause 14 of + /// [ISO/IEC 18033-2:2006/Amd 2:2026](https://www.iso.org/standard/86890.html). fn generate_keypair( &self, rng: &mut R, @@ -204,9 +203,8 @@ pub trait Kem: Params + Expanded + Sample { /// Encapsulate a random message into a ciphertext. /// - /// See Algorithm 13 in the [spec](https://frodokem.org/files/FrodoKEM-specification-20210604.pdf). - /// Algorithm 8.2 in [iso](https://frodokem.org/files/FrodoKEM-standard_proposal-20230314.pdf). - /// Algorithm 2 in [annex](https://frodokem.org/files/FrodoKEM-annex-20230418.pdf) + /// Implements randomized `Frodo.Encaps` from Clause 14 of + /// [ISO/IEC 18033-2:2006/Amd 2:2026](https://www.iso.org/standard/86890.html). fn encapsulate_with_rng<'a, P: Into>, R: CryptoRng + ?Sized>( &self, public_key: P, @@ -221,9 +219,8 @@ pub trait Kem: Params + Expanded + Sample { /// Encapsulate a message into a ciphertext. /// - /// See Algorithm 13 in the [spec](https://frodokem.org/files/FrodoKEM-specification-20210604.pdf). - /// Algorithm 8.2 in [iso](https://frodokem.org/files/FrodoKEM-standard_proposal-20230314.pdf). - /// Algorithm 2 in [annex](https://frodokem.org/files/FrodoKEM-annex-20230418.pdf) + /// Implements deterministic `Frodo.Encaps` from Clause 14 of + /// [ISO/IEC 18033-2:2006/Amd 2:2026](https://www.iso.org/standard/86890.html). fn encapsulate<'a, P: Into>>( &self, public_key: P, @@ -311,9 +308,8 @@ pub trait Kem: Params + Expanded + Sample { /// Decapsulate the ciphertext into a shared secret. /// - /// See Algorithm 14 in the [spec](https://frodokem.org/files/FrodoKEM-specification-20210604.pdf). - /// Algorithm 8.3 in [iso](https://frodokem.org/files/FrodoKEM-standard_proposal-20230314.pdf). - /// Algorithm 3 in [annex](https://frodokem.org/files/FrodoKEM-annex-20230418.pdf) + /// Implements `Frodo.Decaps` from Clause 14 of + /// [ISO/IEC 18033-2:2006/Amd 2:2026](https://www.iso.org/standard/86890.html). fn decapsulate< 'a, 'b, diff --git a/frodo-kem/src/lib.rs b/frodo-kem/src/lib.rs index c26d2810..64e7618d 100644 --- a/frodo-kem/src/lib.rs +++ b/frodo-kem/src/lib.rs @@ -43,7 +43,8 @@ //! //! ## ☢️️ WARNING: HAZARDOUS ☢️ //! It is considered unsafe to use Ephemeral algorithms more than once. -//! For more information see [ISO Standard Annex](https://frodokem.org/files/FrodoKEM-annex-20230418.pdf). +//! For more information, see Clause 14 of +//! [ISO/IEC 18033-2:2006/Amd 2:2026](https://www.iso.org/standard/86890.html). //! //! ``` //! use frodo_kem::Algorithm; @@ -60,9 +61,13 @@ //! let (ct, enc_ss) = alg.encapsulate(&ek, &aes_256_key, &salt).unwrap(); //! let (dec_ss, dec_msg) = alg.decapsulate(&dk, &ct).unwrap(); //! -//! // Ephemeral method, no salt required +//! assert_eq!(enc_ss, dec_ss); +//! assert_eq!(&aes_256_key[..], dec_msg.as_slice()); +//! +//! // Ephemeral method: use a dedicated ephemeral keypair and no salt. //! let alg = Algorithm::EphemeralFrodoKem1344Shake; -//! let (ct, enc_ss) = alg.encapsulate(&ek, &aes_256_key, &[]).unwrap(); +//! let (ek, dk) = alg.generate_keypair(&mut rng); +//! let (ct, enc_ss) = alg.encapsulate(&ek, &aes_256_key, []).unwrap(); //! let (dec_ss, dec_msg) = alg.decapsulate(&dk, &ct).unwrap(); //! //! assert_eq!(enc_ss, dec_ss); @@ -1467,9 +1472,15 @@ impl Algorithm { msg: &[u8], salt: &[u8], ) -> FrodoResult<(Ciphertext, SharedSecret)> { + if encryption_key.algorithm != *self { + return Err(Error::AlgorithmMismatch); + } if K::BYTES_MU != msg.len() { return Err(Error::InvalidMessageLength(msg.len())); } + if K::BYTES_SALT != salt.len() { + return Err(Error::InvalidSaltLength(salt.len())); + } let pk = EncryptionKeyRef::from_slice(encryption_key.value.as_slice())?; let (ct, ss) = K::default().encapsulate(pk, msg, salt); Ok(( @@ -1547,6 +1558,9 @@ impl Algorithm { encryption_key: &EncryptionKey, rng: &mut R, ) -> FrodoResult<(Ciphertext, SharedSecret)> { + if encryption_key.algorithm != *self { + return Err(Error::AlgorithmMismatch); + } let pk = EncryptionKeyRef::from_slice(encryption_key.value.as_slice())?; let (ct, ss) = K::default().encapsulate_with_rng(pk, rng); Ok(( @@ -1625,6 +1639,9 @@ impl Algorithm { secret_key: &DecryptionKey, ciphertext: &Ciphertext, ) -> FrodoResult<(SharedSecret, Vec)> { + if secret_key.algorithm != *self || ciphertext.algorithm != *self { + return Err(Error::AlgorithmMismatch); + } let sk = DecryptionKeyRef::from_slice(secret_key.value.as_slice())?; let ct = CiphertextRef::from_slice(ciphertext.value.as_slice())?; let (ss, mu) = K::default().decapsulate(sk, ct); @@ -1683,6 +1700,54 @@ mod tests { use rand_core::{Rng, SeedableRng}; use rstest::*; + #[test] + fn conformance_boundaries_and_implicit_rejection() { + let algorithm = Algorithm::FrodoKem640Shake; + let other = Algorithm::EphemeralFrodoKem640Shake; + let mut rng = chacha20::ChaCha8Rng::from_seed([7; 32]); + let (encryption_key, decryption_key) = algorithm.generate_keypair(&mut rng); + + assert!(algorithm.encapsulate(&encryption_key, [], []).is_err()); + assert!( + algorithm + .encapsulate( + &encryption_key, + vec![0; algorithm.params().message_length], + [], + ) + .is_err() + ); + assert!( + other + .encapsulate_with_rng(&encryption_key, &mut rng) + .is_err() + ); + + let (ciphertext, shared_secret) = algorithm + .encapsulate_with_rng(&encryption_key, &mut rng) + .unwrap(); + let mut modified_ciphertext = ciphertext.clone(); + modified_ciphertext.value[0] ^= 1; + let rejected_secret = algorithm + .decapsulate(&decryption_key, &modified_ciphertext) + .unwrap() + .0; + let repeated_secret = algorithm + .decapsulate(&decryption_key, &modified_ciphertext) + .unwrap() + .0; + assert_ne!(rejected_secret, shared_secret); + assert_eq!(rejected_secret, repeated_secret); + + let wrong_ciphertext = + Ciphertext::from_bytes(other, vec![0; other.params().ciphertext_length]).unwrap(); + assert!( + algorithm + .decapsulate(&decryption_key, &wrong_ciphertext) + .is_err() + ); + } + #[rstest] #[case::aes640(Algorithm::FrodoKem640Aes)] #[case::aes976(Algorithm::FrodoKem976Aes)] diff --git a/frodo-kem/tests/efrodoKAT/README.md b/frodo-kem/tests/efrodoKAT/README.md index e32d2830..5db74321 100644 --- a/frodo-kem/tests/efrodoKAT/README.md +++ b/frodo-kem/tests/efrodoKAT/README.md @@ -1,2 +1,8 @@ -Test vectors taken from -[PQCrypto-LWEKE](https://github.com/microsoft/PQCrypto-LWEKE/tree/master/eFrodoKEM/KAT)` \ No newline at end of file +The 600 ephemeral FrodoKEM known-answer tests are taken from the FrodoKEM +team's [official reference implementation][reference] at commit +`7a4e7219d06305e16aef734213001cd8fefbcc14`. + +The upstream vector headers retain the historical `FrodoKEM` name. Only those +headers are relabelled here as `eFrodoKEM`; the vector payloads are unchanged. + +[reference]: https://github.com/microsoft/PQCrypto-LWEKE/tree/7a4e7219d06305e16aef734213001cd8fefbcc14/eFrodoKEM/KAT diff --git a/frodo-kem/tests/frodo.rs b/frodo-kem/tests/frodo.rs index f0380429..13daaeaa 100644 --- a/frodo-kem/tests/frodo.rs +++ b/frodo-kem/tests/frodo.rs @@ -25,8 +25,10 @@ fn test_vector(#[case] path: &str) { let path = PathBuf::from(path); let rsp_reader = RspReader::new(path); let mut rng = AesCtrDrbg::default(); + let mut count = 0; for rsp_data in rsp_reader { + count += 1; println!("{} Test {}", rsp_data.scheme, rsp_data.count + 1); rng.reseed(&rsp_data.seed); let (pk, sk) = rsp_data.scheme.generate_keypair(&mut rng); @@ -40,6 +42,8 @@ fn test_vector(#[case] path: &str) { let (dss, _) = rsp_data.scheme.decapsulate(&sk, &ct).unwrap(); assert_eq!(dss, rsp_data.ss); } + + assert_eq!(count, 100, "official KAT file must contain all 100 cases"); } /// Run all tests serially diff --git a/frodo-kem/tests/frodoKAT/README.md b/frodo-kem/tests/frodoKAT/README.md index cc8b08b7..cff38c37 100644 --- a/frodo-kem/tests/frodoKAT/README.md +++ b/frodo-kem/tests/frodoKAT/README.md @@ -1,2 +1,5 @@ -Test vectors taken from -[PQCrypto-LWEKE](https://github.com/microsoft/PQCrypto-LWEKE/tree/master/FrodoKEM/KAT)` \ No newline at end of file +The 600 standard FrodoKEM known-answer tests are taken from the FrodoKEM +team's [official reference implementation][reference] at commit +`7a4e7219d06305e16aef734213001cd8fefbcc14`. + +[reference]: https://github.com/microsoft/PQCrypto-LWEKE/tree/7a4e7219d06305e16aef734213001cd8fefbcc14/FrodoKEM/KAT