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..42adcc2f 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
@@ -113,17 +113,28 @@ 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,
) -> (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]);
@@ -192,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,
@@ -209,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,
@@ -299,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,
@@ -311,14 +319,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 +455,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 +815,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..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);
@@ -101,6 +106,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 +119,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 +243,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 +253,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 +361,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 +393,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 +428,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 +460,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 +526,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 +1085,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 +1152,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 +1208,7 @@ impl Algorithm {
hazmat::Ciphertext::::from_slice(buf).map(|s| {
Ciphertext {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1181,7 +1217,7 @@ impl Algorithm {
hazmat::Ciphertext::::from_slice(buf).map(|s| {
Ciphertext {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1190,7 +1226,7 @@ impl Algorithm {
hazmat::Ciphertext::::from_slice(buf).map(|s| {
Ciphertext {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1199,7 +1235,7 @@ impl Algorithm {
hazmat::Ciphertext::::from_slice(buf).map(|s| {
Ciphertext {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1215,47 +1251,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 +1300,7 @@ impl Algorithm {
hazmat::SharedSecret::::from_slice(buf).map(|s| {
SharedSecret {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1273,7 +1309,7 @@ impl Algorithm {
hazmat::SharedSecret::::from_slice(buf).map(|s| {
SharedSecret {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1282,7 +1318,7 @@ impl Algorithm {
hazmat::SharedSecret::::from_slice(buf).map(|s| {
SharedSecret {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1291,7 +1327,7 @@ impl Algorithm {
hazmat::SharedSecret::::from_slice(buf).map(|s| {
SharedSecret {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1300,7 +1336,7 @@ impl Algorithm {
hazmat::SharedSecret::::from_slice(buf).map(|s| {
SharedSecret {
algorithm: *self,
- value: s.0,
+ value: s.into_vec(),
}
})
}
@@ -1364,7 +1400,7 @@ impl Algorithm {
},
DecryptionKey {
algorithm: *self,
- value: sk.0,
+ value: sk.into_vec(),
},
)
}
@@ -1436,19 +1472,25 @@ 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((
Ciphertext {
algorithm: *self,
- value: ct.0,
+ value: ct.into_vec(),
},
SharedSecret {
algorithm: *self,
- value: ss.0,
+ value: ss.into_vec(),
},
))
}
@@ -1516,16 +1558,19 @@ 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((
Ciphertext {
algorithm: *self,
- value: ct.0,
+ value: ct.into_vec(),
},
SharedSecret {
algorithm: *self,
- value: ss.0,
+ value: ss.into_vec(),
},
))
}
@@ -1594,13 +1639,16 @@ 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);
Ok((
SharedSecret {
algorithm: *self,
- value: ss.0,
+ value: ss.into_vec(),
},
mu,
))
@@ -1652,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