quantica/ml_dsa/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-DSA: Module-Lattice-Based Digital Signature Standard (FIPS 204).
10//!
11//! This crate implements the ML-DSA (formerly CRYSTALS-Dilithium) digital signature
12//! scheme as specified in FIPS 204. ML-DSA is a post-quantum lattice-based signature
13//! scheme built on the hardness of the Module Learning With Errors (M-LWE) and
14//! Module Short Integer Solution (M-SIS) problems.
15//!
16//! Three parameter sets are provided, corresponding to NIST security levels 2, 3, and 5:
17//!
18//! - [`MlDsa44Scheme`] -- ML-DSA-44 (security level 2, ~128-bit classical security)
19//! - [`MlDsa65Scheme`] -- ML-DSA-65 (security level 3, ~192-bit classical security)
20//! - [`MlDsa87Scheme`] -- ML-DSA-87 (security level 5, ~256-bit classical security)
21//!
22//! # Examples
23//!
24//! ```rust
25//! use quantica::ml_dsa::{MlDsa44Scheme, OsRng, MlDsa};
26//!
27//! let mut rng = OsRng;
28//! let (pk, sk) = MlDsa44Scheme::keygen(&mut rng).unwrap();
29//! let msg = b"Hello, post-quantum world!";
30//! let sig = MlDsa44Scheme::sign(&sk, msg, b"", &mut rng).unwrap();
31//! let valid = MlDsa44Scheme::verify(&pk, msg, b"", &sig).unwrap();
32//! assert!(valid);
33//! ```
34
35// API tiers: `params`/`rng` are facade; `decompose`/`encode`/`sample` are
36// hazmat expert bricks (no stability promise); everything else is internal.
37
38/// Decomposition, rounding, and hint functions for signatures.
39#[cfg(feature = "hazmat")]
40pub mod decompose;
41#[cfg(not(feature = "hazmat"))]
42pub(crate) mod decompose;
43/// Core ML-DSA algorithms — the un-typed raw-slice facade (hazmat tier:
44/// no typed-wrapper validation; used by the C FFI and KAT tooling).
45#[cfg(feature = "hazmat")]
46pub mod dsa;
47#[cfg(not(feature = "hazmat"))]
48pub(crate) mod dsa;
49/// Encoding and decoding of keys, signatures, and polynomials.
50#[cfg(feature = "hazmat")]
51pub mod encode;
52#[cfg(not(feature = "hazmat"))]
53pub(crate) mod encode;
54// NTT — Montgomery domain with lazy reduction; crate-internal (naive
55// standalone use is incorrect).
56pub(crate) mod ntt;
57/// ML-DSA parameter sets and constants (FIPS 204, Table 1).
58pub mod params;
59/// Cryptographic random number generation trait and OS-backed implementation.
60pub mod rng;
61/// Sampling algorithms for matrix, secret, and masking generation.
62#[cfg(feature = "hazmat")]
63pub mod sample;
64#[cfg(not(feature = "hazmat"))]
65pub(crate) mod sample;
66// Keccak/SHA-3/SHAKE wrappers over the shared tessera core.
67pub(crate) mod sha3;
68
69// First-order arithmetic masking (DPA / template countermeasure) — SCA
70// work-in-progress per the Tier roadmap; crate-internal.
71#[cfg(feature = "sca-protected")]
72pub(crate) mod masked;
73
74// Fisher-Yates shuffled NTT (SPA countermeasure) — SCA WIP; crate-internal.
75#[cfg(feature = "sca-protected")]
76pub(crate) mod shuffle;
77
78// Small polynomial representation (i16) for secret vectors — RAM
79// optimization WIP with a strict correctness precondition; crate-internal.
80#[cfg(feature = "small-secret")]
81pub(crate) mod smallpoly;
82
83// Compressed polynomial / challenge storage — RAM layout WIP; crate-internal.
84#[cfg(any(feature = "compressed-poly", feature = "compressed-challenge"))]
85pub(crate) mod compressed;
86
87// In-crate KATs for the `pub(crate)` internal interface (keygen/sign/verify_internal).
88#[cfg(test)]
89mod acvp_internal;
90
91use alloc::vec::Vec;
92use core::marker::PhantomData;
93
94pub use crate::prehash::PreHash;
95pub use params::{MlDsa44, MlDsa65, MlDsa87, Params};
96pub use rng::CryptoRng;
97#[cfg(feature = "std")]
98pub use rng::OsRng;
99
100use crate::secret::SecretBytes;
101
102// =====================================================================
103// Typed key / signature wrappers
104// =====================================================================
105
106/// ML-DSA **verifying key** (the public half of a key pair).
107///
108/// Type-tagged with the parameter set `P`. Public material — no
109/// zeroization is performed on drop.
110pub struct VerifyingKey<P: Params> {
111 bytes: Vec<u8>,
112 _marker: PhantomData<P>,
113}
114
115impl<P: Params> VerifyingKey<P> {
116 /// Wrap a raw byte slice. Length is validated against
117 /// [`Params::PK_LEN`].
118 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlDsaError> {
119 if bytes.len() != P::PK_LEN {
120 return Err(MlDsaError::InvalidPublicKey);
121 }
122 Ok(Self {
123 bytes: bytes.to_vec(),
124 _marker: PhantomData,
125 })
126 }
127
128 /// Borrow the encoded verifying key as a byte slice.
129 pub fn as_bytes(&self) -> &[u8] {
130 &self.bytes
131 }
132
133 /// Length in bytes (always [`Params::PK_LEN`]).
134 pub fn len(&self) -> usize {
135 self.bytes.len()
136 }
137}
138
139impl<P: Params> AsRef<[u8]> for VerifyingKey<P> {
140 fn as_ref(&self) -> &[u8] {
141 &self.bytes
142 }
143}
144
145impl<P: Params> core::ops::Deref for VerifyingKey<P> {
146 type Target = [u8];
147 fn deref(&self) -> &[u8] {
148 &self.bytes
149 }
150}
151
152impl<P: Params> Clone for VerifyingKey<P> {
153 fn clone(&self) -> Self {
154 Self {
155 bytes: self.bytes.clone(),
156 _marker: PhantomData,
157 }
158 }
159}
160
161/// ML-DSA **signing key** (the private half of a key pair).
162///
163/// Backed by [`SecretBytes`] — wipes its memory on [`Drop`] via
164/// `silentops::ct_zeroize`. Type-tagged with `P` to prevent
165/// cross-parameter-set use.
166pub struct SigningKey<P: Params> {
167 bytes: SecretBytes,
168 _marker: PhantomData<P>,
169}
170
171impl<P: Params> SigningKey<P> {
172 /// Wrap a raw byte slice. Length is validated against
173 /// [`Params::SK_LEN`].
174 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlDsaError> {
175 if bytes.len() != P::SK_LEN {
176 return Err(MlDsaError::InvalidSecretKey);
177 }
178 Ok(Self {
179 bytes: SecretBytes::from_slice(bytes),
180 _marker: PhantomData,
181 })
182 }
183
184 /// Borrow the encoded signing key as a byte slice.
185 pub fn as_bytes(&self) -> &[u8] {
186 self.bytes.as_bytes()
187 }
188
189 /// Length in bytes (always [`Params::SK_LEN`]).
190 pub fn len(&self) -> usize {
191 self.bytes.len()
192 }
193}
194
195impl<P: Params> AsRef<[u8]> for SigningKey<P> {
196 fn as_ref(&self) -> &[u8] {
197 self.bytes.as_bytes()
198 }
199}
200
201impl<P: Params> core::ops::Deref for SigningKey<P> {
202 type Target = [u8];
203 fn deref(&self) -> &[u8] {
204 self.bytes.as_bytes()
205 }
206}
207
208/// ML-DSA **signature**. Type-tagged with the parameter set `P`.
209///
210/// Signatures are public material (transmitted alongside the message)
211/// and are not zeroized.
212pub struct Signature<P: Params> {
213 bytes: Vec<u8>,
214 _marker: PhantomData<P>,
215}
216
217impl<P: Params> Signature<P> {
218 /// Wrap a raw byte slice. Length is validated against
219 /// [`Params::SIG_LEN`].
220 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlDsaError> {
221 if bytes.len() != P::SIG_LEN {
222 return Err(MlDsaError::InvalidSignature);
223 }
224 Ok(Self {
225 bytes: bytes.to_vec(),
226 _marker: PhantomData,
227 })
228 }
229
230 /// Borrow the encoded signature as a byte slice.
231 pub fn as_bytes(&self) -> &[u8] {
232 &self.bytes
233 }
234
235 /// Length in bytes (always [`Params::SIG_LEN`]).
236 pub fn len(&self) -> usize {
237 self.bytes.len()
238 }
239}
240
241impl<P: Params> AsRef<[u8]> for Signature<P> {
242 fn as_ref(&self) -> &[u8] {
243 &self.bytes
244 }
245}
246
247impl<P: Params> core::ops::Deref for Signature<P> {
248 type Target = [u8];
249 fn deref(&self) -> &[u8] {
250 &self.bytes
251 }
252}
253
254impl<P: Params> Clone for Signature<P> {
255 fn clone(&self) -> Self {
256 Self {
257 bytes: self.bytes.clone(),
258 _marker: PhantomData,
259 }
260 }
261}
262
263/// Error types for ML-DSA operations.
264///
265/// Each variant corresponds to a specific failure mode that can occur during
266/// key generation, signing, or verification.
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub enum MlDsaError {
269 /// Random number generation failed.
270 ///
271 /// Returned when the underlying RNG (e.g., `/dev/urandom`) cannot provide bytes.
272 RngFailure,
273 /// Invalid public key (wrong length or format).
274 ///
275 /// The provided public key does not have the expected byte length for the
276 /// chosen parameter set.
277 InvalidPublicKey,
278 /// Invalid secret key (wrong length or format).
279 ///
280 /// The provided secret key does not have the expected byte length for the
281 /// chosen parameter set.
282 InvalidSecretKey,
283 /// Invalid signature (wrong length or format).
284 ///
285 /// The provided signature does not have the expected byte length for the
286 /// chosen parameter set, or its internal encoding is malformed.
287 InvalidSignature,
288 /// Context string too long (> 255 bytes).
289 ///
290 /// FIPS 204 limits the optional context string to at most 255 bytes.
291 ContextTooLong,
292 /// Signature verification failed.
293 VerificationFailed,
294}
295
296impl core::fmt::Display for MlDsaError {
297 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
298 match self {
299 MlDsaError::RngFailure => write!(f, "RNG failure"),
300 MlDsaError::InvalidPublicKey => write!(f, "Invalid public key"),
301 MlDsaError::InvalidSecretKey => write!(f, "Invalid secret key"),
302 MlDsaError::InvalidSignature => write!(f, "Invalid signature"),
303 MlDsaError::ContextTooLong => write!(f, "Context string too long"),
304 MlDsaError::VerificationFailed => write!(f, "Signature verification failed"),
305 }
306 }
307}
308
309#[cfg(feature = "std")]
310impl std::error::Error for MlDsaError {}
311
312/// Generic ML-DSA interface parameterized by security level.
313///
314/// This struct provides the high-level API for ML-DSA key generation, signing,
315/// and verification. The type parameter `P` selects the parameter set
316/// ([`MlDsa44`], [`MlDsa65`], or [`MlDsa87`]).
317///
318/// All methods are stateless; [`MlDsa`] carries no runtime data and exists only
319/// to bind the parameter set at the type level.
320pub struct MlDsa<P: Params> {
321 _marker: PhantomData<P>,
322}
323
324impl<P: Params> MlDsa<P> {
325 /// Generate a new ML-DSA key pair.
326 ///
327 /// Implements Algorithm 1 of FIPS 204 (ML-DSA.KeyGen). Draws 32 random
328 /// bytes from `rng` and derives a public key / secret key pair.
329 ///
330 /// Returns `(pk, sk)` where `pk` has length [`Self::PK_LEN`] and `sk` has
331 /// length [`Self::SK_LEN`].
332 ///
333 /// # Errors
334 ///
335 /// Returns [`MlDsaError::RngFailure`] if the RNG cannot provide bytes.
336 ///
337 /// # Examples
338 ///
339 /// ```rust
340 /// use quantica::ml_dsa::{MlDsa, MlDsa44, OsRng};
341 ///
342 /// let mut rng = OsRng;
343 /// let (pk, sk) = MlDsa::<MlDsa44>::keygen(&mut rng).unwrap();
344 /// assert_eq!(pk.len(), MlDsa::<MlDsa44>::PK_LEN);
345 /// assert_eq!(sk.len(), MlDsa::<MlDsa44>::SK_LEN);
346 /// ```
347 pub fn keygen(rng: &mut dyn CryptoRng) -> Result<(VerifyingKey<P>, SigningKey<P>), MlDsaError> {
348 let (pk_v, sk_v) = dsa::keygen::<P>(rng)?;
349 Ok((
350 VerifyingKey {
351 bytes: pk_v,
352 _marker: PhantomData,
353 },
354 SigningKey {
355 bytes: SecretBytes::from_vec(sk_v),
356 _marker: PhantomData,
357 },
358 ))
359 }
360
361 /// Sign a message with an optional context string.
362 ///
363 /// Implements Algorithm 2 of FIPS 204 (ML-DSA.Sign). Uses **hedged signing**:
364 /// 32 random bytes are drawn from `rng` and mixed with the secret key material
365 /// to produce the per-signature nonce. This provides resilience against
366 /// fault attacks compared to purely deterministic signing.
367 ///
368 /// The signing algorithm uses a rejection sampling loop internally: candidate
369 /// signatures are generated until one passes all norm checks, so execution
370 /// time may vary.
371 ///
372 /// - `sk`: secret key (must be [`Self::SK_LEN`] bytes).
373 /// - `msg`: message to sign (arbitrary length).
374 /// - `ctx`: optional context string (at most 255 bytes).
375 /// - `rng`: source of randomness for hedged signing.
376 ///
377 /// Returns a signature of length [`Self::SIG_LEN`].
378 ///
379 /// # Errors
380 ///
381 /// - [`MlDsaError::InvalidSecretKey`] if `sk` has the wrong length.
382 /// - [`MlDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
383 /// - [`MlDsaError::RngFailure`] if the RNG cannot provide bytes.
384 ///
385 /// # Examples
386 ///
387 /// ```rust
388 /// use quantica::ml_dsa::{MlDsa, MlDsa44, OsRng};
389 ///
390 /// let mut rng = OsRng;
391 /// let (pk, sk) = MlDsa::<MlDsa44>::keygen(&mut rng).unwrap();
392 /// let sig = MlDsa::<MlDsa44>::sign(&sk, b"message", b"", &mut rng).unwrap();
393 /// assert_eq!(sig.len(), MlDsa::<MlDsa44>::SIG_LEN);
394 /// ```
395 pub fn sign(
396 sk: &SigningKey<P>,
397 msg: &[u8],
398 ctx: &[u8],
399 rng: &mut dyn CryptoRng,
400 ) -> Result<Signature<P>, MlDsaError> {
401 let sig_v = dsa::sign::<P>(sk.as_bytes(), msg, ctx, rng)?;
402 Ok(Signature {
403 bytes: sig_v,
404 _marker: PhantomData,
405 })
406 }
407
408 /// Sign with pre-hashing — HashML-DSA (Algorithm 4 of FIPS 204).
409 ///
410 /// The message is replaced by its digest under `ph` (e.g.
411 /// [`PreHash::Sha256`]); the library computes `ph(msg)` and embeds the
412 /// matching OID, producing `M' = 0x01 || len(ctx) || ctx || OID || ph(msg)`.
413 ///
414 /// # Arguments
415 ///
416 /// * `sk` - The secret key (must be [`Self::SK_LEN`] bytes).
417 /// * `msg` - The message to pre-hash and sign (arbitrary length).
418 /// * `ctx` - Optional context string, at most 255 bytes (`b""` for none).
419 /// * `ph` - The pre-hash function [`PreHash`] applied to `msg`.
420 /// * `rng` - Source of randomness for hedged signing.
421 ///
422 /// # Returns
423 ///
424 /// A [`Signature<P>`] of length [`Self::SIG_LEN`].
425 ///
426 /// # Errors
427 ///
428 /// * [`MlDsaError::InvalidSecretKey`] if `sk` has the wrong length.
429 /// * [`MlDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
430 /// * [`MlDsaError::RngFailure`] if the RNG cannot provide bytes.
431 pub fn sign_prehash(
432 sk: &SigningKey<P>,
433 msg: &[u8],
434 ctx: &[u8],
435 ph: PreHash,
436 rng: &mut dyn CryptoRng,
437 ) -> Result<Signature<P>, MlDsaError> {
438 let sig_v = dsa::sign_prehash::<P>(sk.as_bytes(), msg, ctx, ph, rng)?;
439 Ok(Signature {
440 bytes: sig_v,
441 _marker: PhantomData,
442 })
443 }
444
445 /// Verify a signature on a message with an optional context string.
446 ///
447 /// Implements Algorithm 3 of FIPS 204 (ML-DSA.Verify).
448 ///
449 /// - `pk`: public key (must be [`Self::PK_LEN`] bytes).
450 /// - `msg`: the signed message.
451 /// - `ctx`: the context string used during signing (at most 255 bytes).
452 /// - `sig`: the signature (must be [`Self::SIG_LEN`] bytes).
453 ///
454 /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if verification
455 /// fails (invalid signature content), or an `Err` for structural issues.
456 ///
457 /// # Errors
458 ///
459 /// - [`MlDsaError::InvalidPublicKey`] if `pk` has the wrong length.
460 /// - [`MlDsaError::InvalidSignature`] if `sig` has the wrong length.
461 /// - [`MlDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
462 ///
463 /// # Examples
464 ///
465 /// ```rust
466 /// use quantica::ml_dsa::{MlDsa, MlDsa44, OsRng};
467 ///
468 /// let mut rng = OsRng;
469 /// let (pk, sk) = MlDsa::<MlDsa44>::keygen(&mut rng).unwrap();
470 /// let sig = MlDsa::<MlDsa44>::sign(&sk, b"msg", b"", &mut rng).unwrap();
471 /// assert!(MlDsa::<MlDsa44>::verify(&pk, b"msg", b"", &sig).unwrap());
472 /// ```
473 pub fn verify(pk: &VerifyingKey<P>, msg: &[u8], ctx: &[u8], sig: &Signature<P>) -> Result<bool, MlDsaError> {
474 dsa::verify::<P>(pk.as_bytes(), msg, ctx, sig.as_bytes())
475 }
476
477 /// Verify a pre-hash HashML-DSA signature (Algorithm 5 of FIPS 204).
478 ///
479 /// `ph` must match the [`PreHash`] used at signing time.
480 ///
481 /// # Arguments
482 ///
483 /// * `pk` - The public key (must be [`Self::PK_LEN`] bytes).
484 /// * `msg` - The signed message.
485 /// * `ctx` - The context string used during signing, at most 255 bytes.
486 /// * `ph` - The pre-hash function [`PreHash`], matching the one used at signing.
487 /// * `sig` - The signature (must be [`Self::SIG_LEN`] bytes).
488 ///
489 /// # Returns
490 ///
491 /// `Ok(true)` if the signature is valid, `Ok(false)` if verification fails.
492 ///
493 /// # Errors
494 ///
495 /// * [`MlDsaError::InvalidPublicKey`] if `pk` has the wrong length.
496 /// * [`MlDsaError::InvalidSignature`] if `sig` has the wrong length.
497 /// * [`MlDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
498 pub fn verify_prehash(
499 pk: &VerifyingKey<P>,
500 msg: &[u8],
501 ctx: &[u8],
502 ph: PreHash,
503 sig: &Signature<P>,
504 ) -> Result<bool, MlDsaError> {
505 dsa::verify_prehash::<P>(pk.as_bytes(), msg, ctx, ph, sig.as_bytes())
506 }
507
508 /// Public key length in bytes for this parameter set.
509 pub const PK_LEN: usize = P::PK_LEN;
510
511 /// Secret key length in bytes for this parameter set.
512 pub const SK_LEN: usize = P::SK_LEN;
513
514 /// Signature length in bytes for this parameter set.
515 pub const SIG_LEN: usize = P::SIG_LEN;
516}
517
518/// Convenience alias for ML-DSA-44 (NIST security level 2).
519pub type MlDsa44Scheme = MlDsa<MlDsa44>;
520/// Convenience alias for ML-DSA-65 (NIST security level 3).
521pub type MlDsa65Scheme = MlDsa<MlDsa65>;
522/// Convenience alias for ML-DSA-87 (NIST security level 5).
523pub type MlDsa87Scheme = MlDsa<MlDsa87>;