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