Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions frodo-kem/CONFORMANCE.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 4 additions & 2 deletions frodo-kem/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"

Expand Down
47 changes: 36 additions & 11 deletions frodo-kem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -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 2<sup>8</sup>). Prefer standard
FrodoKEM unless that usage restriction is enforced by the application.

When in doubt use the FrodoKEM algorithm variants.

Expand All @@ -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.

Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions frodo-kem/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
82 changes: 80 additions & 2 deletions frodo-kem/src/hazmat/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -88,6 +89,11 @@ from_slice_impl!(Ciphertext);
serde_impl!(Ciphertext);

impl<P: Params> Ciphertext<P> {
/// Consume this ciphertext and return its serialized bytes.
pub(crate) fn into_vec(self) -> Vec<u8> {
self.0
}

/// Convert a slice of bytes into a ciphertext
pub fn from_slice(bytes: &[u8]) -> FrodoResult<Self> {
if bytes.len() != P::CIPHERTEXT_LENGTH {
Expand Down Expand Up @@ -150,6 +156,11 @@ impl<'a, P: Params> From<&'a Ciphertext<P>> 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<Self> {
Expand Down Expand Up @@ -255,6 +266,11 @@ impl<P: Params> EncryptionKey<P> {
pub struct EncryptionKeyRef<'a, P: Params>(pub(crate) &'a [u8], pub(crate) PhantomData<P>);

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<Self> {
if bytes.len() != P::PUBLIC_KEY_LENGTH {
Expand Down Expand Up @@ -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<P: Params>(pub(crate) Vec<u8>, pub(crate) PhantomData<P>);

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

impl<P: Params> CtEq for DecryptionKey<P> {
fn ct_eq(&self, other: &Self) -> ctutils::Choice {
self.0.ct_eq(&other.0)
}
}

impl<P: Params> Eq for DecryptionKey<P> {}

impl<P: Params> PartialEq for DecryptionKey<P> {
fn eq(&self, other: &Self) -> bool {
bool::from(self.ct_eq(other))
}
}

impl<P: Params> AsRef<[u8]> for DecryptionKey<P> {
fn as_ref(&self) -> &[u8] {
&self.0
Expand All @@ -307,11 +343,22 @@ impl<P: Params> Zeroize for DecryptionKey<P> {

impl<P: Params> ZeroizeOnDrop for DecryptionKey<P> {}

impl<P: Params> Drop for DecryptionKey<P> {
fn drop(&mut self) {
self.zeroize();
}
}

from_slice_impl!(DecryptionKey);

serde_impl!(DecryptionKey);

impl<P: Params> DecryptionKey<P> {
/// Consume this key and return its serialized bytes.
pub(crate) fn into_vec(mut self) -> Vec<u8> {
core::mem::take(&mut self.0)
}

/// Convert a slice of bytes into a secret key
pub fn from_slice(bytes: &[u8]) -> FrodoResult<Self> {
if bytes.len() != P::SECRET_KEY_LENGTH {
Expand Down Expand Up @@ -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<P: Params>(pub(crate) Vec<u8>, pub(crate) PhantomData<P>);

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

impl<P: Params> CtEq for SharedSecret<P> {
fn ct_eq(&self, other: &Self) -> ctutils::Choice {
self.0.ct_eq(&other.0)
}
}

impl<P: Params> Eq for SharedSecret<P> {}

impl<P: Params> PartialEq for SharedSecret<P> {
fn eq(&self, other: &Self) -> bool {
bool::from(self.ct_eq(other))
}
}

impl<P: Params> AsRef<[u8]> for SharedSecret<P> {
fn as_ref(&self) -> &[u8] {
&self.0
Expand All @@ -453,11 +520,22 @@ impl<P: Params> Zeroize for SharedSecret<P> {

impl<P: Params> ZeroizeOnDrop for SharedSecret<P> {}

impl<P: Params> Drop for SharedSecret<P> {
fn drop(&mut self) {
self.zeroize();
}
}

from_slice_impl!(SharedSecret);

serde_impl!(SharedSecret);

impl<P: Params> SharedSecret<P> {
/// Consume this shared secret and return its bytes.
pub(crate) fn into_vec(mut self) -> Vec<u8> {
core::mem::take(&mut self.0)
}

/// Convert a slice of bytes into a shared secret
pub fn from_slice(bytes: &[u8]) -> FrodoResult<Self> {
if bytes.len() != P::SHARED_SECRET_LENGTH {
Expand Down
Loading