Skip to main content

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>;