Skip to main content

quantica/ml_kem/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4// The key / signature / ciphertext types are fixed-size byte containers;
5// `len()` returns a compile-time constant (`Params::*_LEN`), so `is_empty()`
6// (always `false`) carries no meaning. Documented allow per CLAUDE.md §6.
7#![allow(clippy::len_without_is_empty)]
8
9//! # ML-KEM — Module-Lattice-Based Key-Encapsulation Mechanism
10//!
11//! A pure-Rust implementation of **FIPS 203** (ML-KEM) providing three
12//! parameter sets: [`MlKem512`], [`MlKem768`], and [`MlKem1024`].
13//!
14//! Uses only the Rust standard library. No external dependencies.
15//!
16//! ## Quick start
17//!
18//! ```no_run
19//! use quantica::ml_kem::*;
20//!
21//! let mut rng = OsRng;
22//!
23//! // Key generation
24//! let (ek, dk) = MlKem::<MlKem768>::keygen(&mut rng).unwrap();
25//!
26//! // Encapsulation (sender)
27//! let (shared_secret_s, ciphertext) = MlKem::<MlKem768>::encaps(&ek, &mut rng).unwrap();
28//!
29//! // Decapsulation (receiver)
30//! let shared_secret_r = MlKem::<MlKem768>::decaps(&dk, &ciphertext, &mut rng).unwrap();
31//!
32//! assert_eq!(shared_secret_s, shared_secret_r);
33//! ```
34//!
35//! ## Side-channel countermeasures
36//!
37//! This implementation includes multiple layers of protection against
38//! physical side-channel attacks:
39//!
40//! - **Constant-time**: no secret-dependent branches or memory accesses
41//! - **Zeroization**: all secret intermediates erased via volatile writes
42//! - **First-order masking**: secret polynomials split into additive shares (DPA/template)
43//! - **Double decaps**: fault detection on FO comparison (DFA)
44//! - **dk integrity**: H(ek) verification at decaps time (DFA)
45//! - **NTT shuffling**: randomized butterfly order (SPA)
46//!
47//! ## Module overview
48//!
49//! Three API tiers (see the crate-level "API tiers" section): the guaranteed
50//! facade, the `hazmat`-gated expert bricks, and crate-internal modules.
51//!
52//! | Module       | Tier | Description |
53//! |--------------|------|-------------|
54//! | [`params`]   | facade | Parameter sets and the [`Params`] trait |
55//! | [`rng`]      | facade | Cryptographic RNG trait and OS-backed implementation |
56//! | `kem`        | hazmat | Deterministic FIPS 203 internals (CAVP/KAT tooling) |
57//! | `encode`     | hazmat | Encoding, decoding, compression, decompression |
58//! | `sample`     | hazmat | Polynomial sampling (NTT domain and CBD) |
59//! | `kpke`       | internal | Unauthenticated K-PKE component (FO-transform input) |
60//! | `ntt`        | internal | NTT and polynomial arithmetic (Montgomery domain) |
61//! | `sha3`       | internal | SHA-3 / SHAKE wrappers (FIPS 202) |
62//! | `masked`     | internal | First-order arithmetic masking for polynomials |
63//! | `shuffle`    | internal | Fisher-Yates shuffle for NTT butterfly randomization |
64
65/// Byte encoding/decoding and compression/decompression (Algorithms 3-6).
66#[cfg(feature = "hazmat")]
67pub mod encode;
68#[cfg(not(feature = "hazmat"))]
69pub(crate) mod encode;
70/// ML-KEM key encapsulation: keygen, encaps, decaps (Algorithms 16-21).
71///
72/// Under `hazmat` this exposes the deterministic `*_internal` FIPS 203
73/// functions (caller-supplied `d`/`z`/`m`) used for CAVP/KAT tooling —
74/// they skip the facade's input validation and fault checks by design.
75#[cfg(feature = "hazmat")]
76pub mod kem;
77#[cfg(not(feature = "hazmat"))]
78pub(crate) mod kem;
79// K-PKE is the UNAUTHENTICATED (IND-CPA) component scheme; using it directly
80// bypasses the FO transform and breaks the KEM's security. Crate-internal.
81pub(crate) mod kpke;
82// First-order arithmetic masking — SCA countermeasure work-in-progress
83// (Tier roadmap); crate-internal until the API stabilises.
84pub(crate) mod masked;
85// NTT and modular polynomial arithmetic. Works in the Montgomery domain with
86// lazy reduction bounds; crate-internal (naive standalone use is incorrect).
87pub(crate) mod ntt;
88/// ML-KEM parameter sets and the [`Params`] trait.
89pub mod params;
90/// Cryptographic random number generation trait and OS-backed implementation.
91pub mod rng;
92/// Polynomial sampling algorithms: `sample_ntt` and `sample_poly_cbd`.
93#[cfg(feature = "hazmat")]
94pub mod sample;
95#[cfg(not(feature = "hazmat"))]
96pub(crate) mod sample;
97// SHA-3 / SHAKE wrappers over the shared tessera Keccak core.
98pub(crate) mod sha3;
99// Fisher-Yates NTT butterfly shuffling — SCA countermeasure WIP; internal.
100pub(crate) mod shuffle;
101
102pub use params::{MlKem512, MlKem768, MlKem1024, Params};
103pub use rng::CryptoRng;
104#[cfg(feature = "std")]
105pub use rng::OsRng;
106
107use crate::secret::{SecretArray, SecretBytes};
108use alloc::vec::Vec;
109use core::marker::PhantomData;
110
111// =====================================================================
112// Typed key / ciphertext / shared-secret wrappers
113// =====================================================================
114
115/// ML-KEM **encapsulation key** (the public half of a key pair).
116///
117/// Type-tagged with the parameter set `P` so the type system can
118/// catch mismatches between security levels at compile time. The
119/// underlying bytes are a plain `Vec<u8>` because the encapsulation
120/// key is public material — no zeroization is performed on drop.
121pub struct EncapsulationKey<P: Params> {
122    bytes: Vec<u8>,
123    _marker: PhantomData<P>,
124}
125
126impl<P: Params> EncapsulationKey<P> {
127    /// Wrap a raw byte vector. Length is validated against
128    /// [`Params::EK_LEN`] for the parameter set `P`.
129    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlKemError> {
130        if bytes.len() != P::EK_LEN {
131            return Err(MlKemError::InvalidEncapsulationKey);
132        }
133        Ok(Self {
134            bytes: bytes.to_vec(),
135            _marker: PhantomData,
136        })
137    }
138
139    /// Borrow the encoded encapsulation key as a byte slice.
140    pub fn as_bytes(&self) -> &[u8] {
141        &self.bytes
142    }
143
144    /// Length in bytes (always [`Params::EK_LEN`]).
145    pub fn len(&self) -> usize {
146        self.bytes.len()
147    }
148}
149
150impl<P: Params> AsRef<[u8]> for EncapsulationKey<P> {
151    fn as_ref(&self) -> &[u8] {
152        &self.bytes
153    }
154}
155
156impl<P: Params> core::ops::Deref for EncapsulationKey<P> {
157    type Target = [u8];
158    fn deref(&self) -> &[u8] {
159        &self.bytes
160    }
161}
162
163impl<P: Params> Clone for EncapsulationKey<P> {
164    fn clone(&self) -> Self {
165        Self {
166            bytes: self.bytes.clone(),
167            _marker: PhantomData,
168        }
169    }
170}
171
172/// ML-KEM **decapsulation key** (the private half of a key pair).
173///
174/// Backed by a [`SecretBytes`] container that wipes its memory on
175/// [`Drop`] using `silentops::ct_zeroize`. Type-tagged with `P` to
176/// prevent accidental cross-parameter-set use.
177pub struct DecapsulationKey<P: Params> {
178    bytes: SecretBytes,
179    _marker: PhantomData<P>,
180}
181
182impl<P: Params> DecapsulationKey<P> {
183    /// Wrap a raw byte slice into a zeroizing decapsulation key.
184    /// Length is validated against [`Params::DK_LEN`].
185    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlKemError> {
186        if bytes.len() != P::DK_LEN {
187            return Err(MlKemError::InvalidDecapsulationKey);
188        }
189        Ok(Self {
190            bytes: SecretBytes::from_slice(bytes),
191            _marker: PhantomData,
192        })
193    }
194
195    /// Borrow the encoded decapsulation key as a byte slice.
196    pub fn as_bytes(&self) -> &[u8] {
197        self.bytes.as_bytes()
198    }
199
200    /// Length in bytes (always [`Params::DK_LEN`]).
201    pub fn len(&self) -> usize {
202        self.bytes.len()
203    }
204}
205
206impl<P: Params> AsRef<[u8]> for DecapsulationKey<P> {
207    fn as_ref(&self) -> &[u8] {
208        self.bytes.as_bytes()
209    }
210}
211
212impl<P: Params> core::ops::Deref for DecapsulationKey<P> {
213    type Target = [u8];
214    fn deref(&self) -> &[u8] {
215        self.bytes.as_bytes()
216    }
217}
218
219/// ML-KEM **ciphertext** wrapping the encapsulated shared secret.
220///
221/// Type-tagged with `P`. Ciphertexts are not secret (they travel on
222/// the wire), so no zeroization is performed.
223pub struct Ciphertext<P: Params> {
224    bytes: Vec<u8>,
225    _marker: PhantomData<P>,
226}
227
228impl<P: Params> Ciphertext<P> {
229    /// Wrap a raw byte slice. Length is validated against
230    /// [`Params::CT_LEN`].
231    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlKemError> {
232        if bytes.len() != P::CT_LEN {
233            return Err(MlKemError::InvalidCiphertext);
234        }
235        Ok(Self {
236            bytes: bytes.to_vec(),
237            _marker: PhantomData,
238        })
239    }
240
241    /// Borrow the encoded ciphertext as a byte slice.
242    pub fn as_bytes(&self) -> &[u8] {
243        &self.bytes
244    }
245
246    /// Length in bytes (always [`Params::CT_LEN`]).
247    pub fn len(&self) -> usize {
248        self.bytes.len()
249    }
250}
251
252impl<P: Params> AsRef<[u8]> for Ciphertext<P> {
253    fn as_ref(&self) -> &[u8] {
254        &self.bytes
255    }
256}
257
258impl<P: Params> core::ops::Deref for Ciphertext<P> {
259    type Target = [u8];
260    fn deref(&self) -> &[u8] {
261        &self.bytes
262    }
263}
264
265impl<P: Params> Clone for Ciphertext<P> {
266    fn clone(&self) -> Self {
267        Self {
268            bytes: self.bytes.clone(),
269            _marker: PhantomData,
270        }
271    }
272}
273
274/// 32-byte ML-KEM shared secret.
275///
276/// Backed by a [`SecretArray<32>`] which wipes itself on [`Drop`].
277/// Equality is constant-time via `silentops::ct_eq`.
278pub type SharedSecret = SecretArray<32>;
279
280/// Errors returned by ML-KEM operations.
281///
282/// All error variants are designed to avoid leaking secret information;
283/// timing is independent of the specific failure path.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum MlKemError {
286    /// The cryptographic random number generator failed to produce bytes.
287    ///
288    /// This typically indicates a system-level failure (e.g., `/dev/urandom`
289    /// is unavailable).
290    RngFailure,
291    /// The provided encapsulation (public) key has an invalid length or
292    /// fails the modulus check required by FIPS 203 Section 7.2.
293    InvalidEncapsulationKey,
294    /// The provided decapsulation (private) key has an invalid length or
295    /// fails the `H(ek)` integrity check embedded in the key.
296    ///
297    /// This may indicate storage corruption or fault injection.
298    InvalidDecapsulationKey,
299    /// The provided ciphertext has an invalid length for the parameter set.
300    InvalidCiphertext,
301}
302
303impl core::fmt::Display for MlKemError {
304    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
305        match self {
306            Self::RngFailure => write!(f, "Random bit generation failed"),
307            Self::InvalidEncapsulationKey => write!(f, "Invalid encapsulation key"),
308            Self::InvalidDecapsulationKey => write!(f, "Invalid decapsulation key"),
309            Self::InvalidCiphertext => write!(f, "Invalid ciphertext"),
310        }
311    }
312}
313
314#[cfg(feature = "std")]
315impl std::error::Error for MlKemError {}
316
317/// Main ML-KEM interface, generic over a [`Params`] parameter set.
318///
319/// This is the primary API for ML-KEM key encapsulation. It wraps the
320/// lower-level functions in `kem` (hazmat tier) and provides a convenient, type-safe
321/// interface parameterized by security level.
322///
323/// # Type parameters
324///
325/// * `P` - One of [`MlKem512`], [`MlKem768`], or [`MlKem1024`], selecting
326///   the security category (1, 3, or 5 respectively).
327///
328/// # Example
329///
330/// ```no_run
331/// use quantica::ml_kem::*;
332///
333/// let mut rng = OsRng;
334/// let (ek, dk) = MlKem::<MlKem768>::keygen(&mut rng).unwrap();
335/// let (ss, ct) = MlKem::<MlKem768>::encaps(&ek, &mut rng).unwrap();
336/// let ss2 = MlKem::<MlKem768>::decaps(&dk, &ct, &mut rng).unwrap();
337/// assert_eq!(ss, ss2);
338/// ```
339pub struct MlKem<P: Params>(core::marker::PhantomData<P>);
340
341impl<P: Params> MlKem<P> {
342    /// Generate an ML-KEM key pair.
343    ///
344    /// Produces an encapsulation key (public) and a decapsulation key (private)
345    /// using randomness from the provided RNG. Implements Algorithm 19 of FIPS 203.
346    ///
347    /// The decapsulation key includes an integrity hash `H(ek)` that is verified
348    /// during decapsulation to detect storage corruption (DFA protection).
349    ///
350    /// # Arguments
351    ///
352    /// * `rng` - A cryptographic random number generator implementing [`CryptoRng`].
353    ///
354    /// # Returns
355    ///
356    /// A tuple `(encapsulation_key, decapsulation_key)` of typed
357    /// wrappers. The decapsulation key auto-zeroizes on `Drop`.
358    ///
359    /// # Errors
360    ///
361    /// Returns [`MlKemError::RngFailure`] if the RNG fails to produce bytes.
362    ///
363    /// # Example
364    ///
365    /// ```no_run
366    /// use quantica::ml_kem::*;
367    /// let mut rng = OsRng;
368    /// let (ek, dk) = MlKem::<MlKem768>::keygen(&mut rng).unwrap();
369    /// assert_eq!(ek.len(), MlKem768::EK_LEN);
370    /// assert_eq!(dk.len(), MlKem768::DK_LEN);
371    /// ```
372    pub fn keygen(rng: &mut impl CryptoRng) -> Result<(EncapsulationKey<P>, DecapsulationKey<P>), MlKemError> {
373        let (ek_v, dk_v) = kem::keygen::<P>(rng)?;
374        Ok((
375            EncapsulationKey {
376                bytes: ek_v,
377                _marker: PhantomData,
378            },
379            DecapsulationKey {
380                bytes: SecretBytes::from_vec(dk_v),
381                _marker: PhantomData,
382            },
383        ))
384    }
385
386    /// Encapsulate a shared secret against an encapsulation key.
387    ///
388    /// Given a public encapsulation key, generates a fresh 32-byte shared
389    /// secret and the corresponding ciphertext. Implements Algorithm 20 of
390    /// FIPS 203 with input validation (length and modulus checks).
391    ///
392    /// # Arguments
393    ///
394    /// * `ek` - The encapsulation (public) key, exactly [`Params::EK_LEN`] bytes.
395    /// * `rng` - A cryptographic random number generator implementing [`CryptoRng`].
396    ///
397    /// # Returns
398    ///
399    /// A tuple `(shared_secret, ciphertext)` where the shared secret is 32 bytes.
400    ///
401    /// # Errors
402    ///
403    /// * [`MlKemError::InvalidEncapsulationKey`] if `ek` has wrong length or fails
404    ///   the modulus check.
405    /// * [`MlKemError::RngFailure`] if the RNG fails.
406    ///
407    /// # Example
408    ///
409    /// ```no_run
410    /// use quantica::ml_kem::*;
411    /// let mut rng = OsRng;
412    /// let (ek, _dk) = MlKem::<MlKem768>::keygen(&mut rng).unwrap();
413    /// let (shared_secret, ciphertext) = MlKem::<MlKem768>::encaps(&ek, &mut rng).unwrap();
414    /// ```
415    pub fn encaps(
416        ek: &EncapsulationKey<P>,
417        rng: &mut impl CryptoRng,
418    ) -> Result<(SharedSecret, Ciphertext<P>), MlKemError> {
419        let (ss, ct) = kem::encaps::<P>(ek.as_bytes(), rng)?;
420        Ok((
421            SharedSecret::new(ss),
422            Ciphertext {
423                bytes: ct,
424                _marker: PhantomData,
425            },
426        ))
427    }
428
429    /// Decapsulate a ciphertext with full DFA protection.
430    ///
431    /// Recovers the 32-byte shared secret from a ciphertext using the
432    /// decapsulation (private) key. Implements Algorithm 21 of FIPS 203
433    /// with two additional DFA countermeasures:
434    ///
435    /// 1. **dk integrity check** -- verifies `H(ek)` stored in `dk` to detect
436    ///    fault injection on key material in memory.
437    /// 2. **Double computation** -- runs the internal decapsulation twice and
438    ///    compares results. A single-fault attack can only corrupt one execution,
439    ///    so divergent results indicate fault injection.
440    ///
441    /// Recommended for embedded and high-security contexts where physical
442    /// fault attacks are in the threat model.
443    ///
444    /// # Arguments
445    ///
446    /// * `dk` - The decapsulation (private) key, exactly [`Params::DK_LEN`] bytes.
447    /// * `ct` - The ciphertext, exactly [`Params::CT_LEN`] bytes.
448    /// * `rng` - A cryptographic random number generator (reserved for future use).
449    ///
450    /// # Returns
451    ///
452    /// The 32-byte shared secret.
453    ///
454    /// # Errors
455    ///
456    /// * [`MlKemError::InvalidDecapsulationKey`] if `dk` has wrong length or
457    ///   fails the integrity check.
458    /// * [`MlKemError::InvalidCiphertext`] if `ct` has wrong length.
459    ///
460    /// # Example
461    ///
462    /// ```no_run
463    /// use quantica::ml_kem::*;
464    /// let mut rng = OsRng;
465    /// let (ek, dk) = MlKem::<MlKem768>::keygen(&mut rng).unwrap();
466    /// let (ss, ct) = MlKem::<MlKem768>::encaps(&ek, &mut rng).unwrap();
467    /// let ss2 = MlKem::<MlKem768>::decaps(&dk, &ct, &mut rng).unwrap();
468    /// assert_eq!(ss, ss2);
469    /// ```
470    pub fn decaps(
471        dk: &DecapsulationKey<P>,
472        ct: &Ciphertext<P>,
473        rng: &mut impl CryptoRng,
474    ) -> Result<SharedSecret, MlKemError> {
475        kem::decaps::<P>(dk.as_bytes(), ct.as_bytes(), rng).map(SharedSecret::new)
476    }
477
478    /// Decapsulate a ciphertext without double computation (faster variant).
479    ///
480    /// Same as [`MlKem::decaps`] but omits the double-computation DFA
481    /// countermeasure, making it roughly twice as fast. The `H(ek)` integrity
482    /// check on the decapsulation key is still performed.
483    ///
484    /// Use this for software-only contexts where physical fault injection
485    /// is not part of the threat model.
486    ///
487    /// # Arguments
488    ///
489    /// * `dk` - The decapsulation (private) key, exactly [`Params::DK_LEN`] bytes.
490    /// * `ct` - The ciphertext, exactly [`Params::CT_LEN`] bytes.
491    ///
492    /// # Returns
493    ///
494    /// The 32-byte shared secret.
495    ///
496    /// # Errors
497    ///
498    /// * [`MlKemError::InvalidDecapsulationKey`] if `dk` has wrong length or
499    ///   fails the integrity check.
500    /// * [`MlKemError::InvalidCiphertext`] if `ct` has wrong length.
501    pub fn decaps_fast(dk: &DecapsulationKey<P>, ct: &Ciphertext<P>) -> Result<SharedSecret, MlKemError> {
502        kem::decaps_single::<P>(dk.as_bytes(), ct.as_bytes()).map(SharedSecret::new)
503    }
504}
505
506// --- Deterministic internal functions (hazmat: CAVP / KAT tooling) ---
507//
508// These implement the FIPS 203 `*_internal` algorithms with caller-supplied
509// randomness and skip the facade's input validation and fault countermeasures
510// BY DESIGN (that is what the CAVP vectors require). They are part of the
511// `hazmat` tier: offered for test-vector tooling, with no stability or
512// misuse-resistance promise. Not compiled into the public surface otherwise.
513#[cfg(feature = "hazmat")]
514impl<P: Params> MlKem<P> {
515    /// Deterministic key generation for testing and CAVP validation.
516    ///
517    /// Implements Algorithm 16 of FIPS 203 directly, using caller-supplied
518    /// seeds `d` and `z` instead of drawing them from an RNG.
519    ///
520    /// # Arguments
521    ///
522    /// * `d` - 32-byte seed for K-PKE key generation.
523    /// * `z` - 32-byte implicit rejection value stored in the decapsulation key.
524    ///
525    /// # Returns
526    ///
527    /// A tuple `(encapsulation_key, decapsulation_key)` as byte vectors.
528    pub fn keygen_internal(d: &[u8; 32], z: &[u8; 32]) -> (Vec<u8>, Vec<u8>) {
529        kem::keygen_internal::<P>(d, z)
530    }
531
532    /// Deterministic encapsulation for testing and CAVP validation.
533    ///
534    /// Implements Algorithm 17 of FIPS 203 directly, using a caller-supplied
535    /// message `m` instead of drawing it from an RNG. No input validation
536    /// is performed on `ek`.
537    ///
538    /// # Arguments
539    ///
540    /// * `ek` - The encapsulation (public) key.
541    /// * `m` - 32-byte random message seed.
542    ///
543    /// # Returns
544    ///
545    /// A tuple `(shared_secret, ciphertext)`.
546    pub fn encaps_internal(ek: &[u8], m: &[u8; 32]) -> ([u8; 32], Vec<u8>) {
547        kem::encaps_internal::<P>(ek, m)
548    }
549
550    /// Deterministic decapsulation for testing and CAVP validation.
551    ///
552    /// Implements Algorithm 18 of FIPS 203 directly, with no input
553    /// validation or DFA countermeasures. All comparisons are still
554    /// constant-time.
555    ///
556    /// # Arguments
557    ///
558    /// * `dk` - The decapsulation (private) key.
559    /// * `ct` - The ciphertext.
560    ///
561    /// # Returns
562    ///
563    /// The 32-byte shared secret.
564    pub fn decaps_internal(dk: &[u8], ct: &[u8]) -> [u8; 32] {
565        kem::decaps_internal::<P>(dk, ct)
566    }
567}