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}