Skip to main content

dcrypt_sign/dilithium/
mod.rs

1//! FIPS 204 Module-Lattice-Based Digital Signature Algorithm (ML-DSA).
2//!
3//! Key generation, signing, verification, sampling, arithmetic, and encoding
4//! are implemented in safe Rust in this crate. Public keys, expanded private
5//! keys, and signatures use Algorithms 22, 24, and 26 of final FIPS 204.
6//! Independent implementations are used only as verification-workspace oracles.
7//!
8//! Version 3 exposes only the final-standard `MlDsa44`, `MlDsa65`, and
9//! `MlDsa87` names. Pre-standard Dilithium names are deliberately absent so a
10//! caller cannot mistake legacy encodings or semantics for FIPS 204 objects.
11
12use crate::error::Error as SignError;
13#[cfg(not(feature = "std"))]
14use alloc::{format, string::ToString, vec::Vec};
15use core::{fmt, marker::PhantomData};
16use dcrypt_api::{Result as ApiResult, SecretVec, Signature as SignatureTrait, ZeroizingBytes};
17use dcrypt_internal::{
18    try_fill_bytes_zeroing_on_error, CryptoRng, RngCore, Zeroize, ZeroizeOnDrop, Zeroizing,
19};
20use dcrypt_params::pqc::ml_dsa::{MlDsa44Params, MlDsa65Params, MlDsa87Params, MlDsaSchemeParams};
21
22mod arithmetic;
23mod encoding;
24mod polyvec;
25mod sampling;
26mod sign;
27
28/// ML-DSA public key encoded with FIPS 204 Algorithm 22 (`pkEncode`).
29#[derive(Clone, Debug)]
30pub struct MlDsaPublicKey(pub(crate) Vec<u8>);
31
32/// ML-DSA expanded private key encoded with FIPS 204 Algorithm 24 (`skEncode`).
33///
34/// In particular, bytes `64..128` contain the complete 64-byte `tr = H(pk, 64)`
35/// value. Bare bytes do not carry a format version, so use paired import or
36/// external provenance/framing when distinguishing affected legacy objects.
37#[derive(Clone)]
38pub struct MlDsaSecretKey {
39    bytes: SecretVec,
40    public_key: Option<Vec<u8>>,
41}
42
43/// ML-DSA signature encoded with FIPS 204 Algorithm 26 (`sigEncode`).
44#[derive(Clone, Debug)]
45pub struct MlDsaSignature(pub(crate) Vec<u8>);
46
47impl Zeroize for MlDsaSecretKey {
48    fn zeroize(&mut self) {
49        self.bytes.zeroize();
50        self.public_key.zeroize();
51    }
52}
53
54impl Drop for MlDsaSecretKey {
55    fn drop(&mut self) {
56        self.zeroize();
57    }
58}
59
60impl ZeroizeOnDrop for MlDsaSecretKey {}
61
62impl fmt::Debug for MlDsaSecretKey {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.debug_struct("MlDsaSecretKey")
65            .field("bytes", &"[REDACTED]")
66            .finish()
67    }
68}
69
70impl AsRef<[u8]> for MlDsaPublicKey {
71    fn as_ref(&self) -> &[u8] {
72        &self.0
73    }
74}
75
76impl AsMut<[u8]> for MlDsaPublicKey {
77    fn as_mut(&mut self) -> &mut [u8] {
78        &mut self.0
79    }
80}
81
82impl AsRef<[u8]> for MlDsaSecretKey {
83    fn as_ref(&self) -> &[u8] {
84        &self.bytes
85    }
86}
87
88impl AsRef<[u8]> for MlDsaSignature {
89    fn as_ref(&self) -> &[u8] {
90        &self.0
91    }
92}
93
94impl AsMut<[u8]> for MlDsaSignature {
95    fn as_mut(&mut self) -> &mut [u8] {
96        &mut self.0
97    }
98}
99
100impl MlDsaSecretKey {
101    /// Decode and fully validate a final-FIPS-204 expanded private key.
102    ///
103    /// Validation recomputes `A*s1+s2`, `t1`, `t0`, the public key, and the
104    /// 64-byte `tr`; the returned key always retains its derived public key.
105    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignError> {
106        let public_key = match bytes.len() {
107            2560 => MlDsa44Params::validate_secret_key(bytes)?,
108            4032 => MlDsa65Params::validate_secret_key(bytes)?,
109            4896 => MlDsa87Params::validate_secret_key(bytes)?,
110            _ => {
111                return Err(SignError::Deserialization(format!(
112                    "invalid ML-DSA expanded private key size: {} bytes",
113                    bytes.len()
114                )))
115            }
116        };
117
118        Ok(Self {
119            bytes: SecretVec::from_slice(bytes),
120            public_key: Some(public_key),
121        })
122    }
123
124    /// Decode an expanded private key and validate it against its public key.
125    ///
126    /// Besides validating the expanded key itself, this requires the supplied
127    /// public key to equal the public key derived from all secret components.
128    pub fn from_bytes_with_public_key(
129        bytes: &[u8],
130        public_key: &MlDsaPublicKey,
131    ) -> Result<Self, SignError> {
132        match bytes.len() {
133            2560 => MlDsa44Params::validate_key_pair(bytes, public_key.as_ref())?,
134            4032 => MlDsa65Params::validate_key_pair(bytes, public_key.as_ref())?,
135            4896 => MlDsa87Params::validate_key_pair(bytes, public_key.as_ref())?,
136            _ => {
137                return Err(SignError::Deserialization(format!(
138                    "invalid ML-DSA expanded private key size: {} bytes",
139                    bytes.len()
140                )))
141            }
142        }
143
144        Ok(Self {
145            bytes: SecretVec::from_slice(bytes),
146            public_key: Some(public_key.as_ref().to_vec()),
147        })
148    }
149
150    /// Return the exact FIPS 204 expanded private-key encoding.
151    pub fn to_bytes(&self) -> &[u8] {
152        &self.bytes
153    }
154
155    /// Export the expanded private key into exact-size zeroizing storage.
156    pub fn to_bytes_zeroizing(&self) -> ZeroizingBytes {
157        self.bytes.to_bytes_zeroizing_boxed()
158    }
159
160    /// Return the public key retained at generation or paired import time.
161    pub fn public_key(&self) -> Result<MlDsaPublicKey, SignError> {
162        self.public_key
163            .as_ref()
164            .cloned()
165            .map(MlDsaPublicKey)
166            .ok_or_else(|| {
167                SignError::InvalidKey(
168                    "public-key derivation is unavailable for an unpaired imported ML-DSA expanded key; import with from_bytes_with_public_key"
169                        .to_string(),
170                )
171            })
172    }
173}
174
175impl MlDsaPublicKey {
176    /// Decode and validate a final-FIPS-204 public key.
177    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignError> {
178        match bytes.len() {
179            1312 => MlDsa44Params::validate_public_key(bytes)?,
180            1952 => MlDsa65Params::validate_public_key(bytes)?,
181            2592 => MlDsa87Params::validate_public_key(bytes)?,
182            _ => {
183                return Err(SignError::Deserialization(format!(
184                    "invalid ML-DSA public key size: {} bytes",
185                    bytes.len()
186                )))
187            }
188        }
189
190        Ok(Self(bytes.to_vec()))
191    }
192
193    /// Return the exact FIPS 204 public-key encoding.
194    pub fn to_bytes(&self) -> &[u8] {
195        &self.0
196    }
197}
198
199impl MlDsaSignature {
200    /// Decode a final-FIPS-204 signature and enforce canonical hint encoding.
201    ///
202    /// Duplicate or unsorted hint indices, non-monotonic hint boundaries, and
203    /// nonzero unused hint bytes are rejected here and again during verification.
204    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignError> {
205        match bytes.len() {
206            2420 => validate_hint_encoding(bytes, 32 + 4 * 576, 80, 4)?,
207            3309 => validate_hint_encoding(bytes, 48 + 5 * 640, 55, 6)?,
208            4627 => validate_hint_encoding(bytes, 64 + 7 * 640, 75, 8)?,
209            _ => {
210                return Err(SignError::InvalidSignatureSize {
211                    expected: 0,
212                    actual: bytes.len(),
213                })
214            }
215        }
216
217        Ok(Self(bytes.to_vec()))
218    }
219
220    /// Return the exact FIPS 204 signature encoding.
221    pub fn to_bytes(&self) -> &[u8] {
222        &self.0
223    }
224}
225
226fn validate_hint_encoding(
227    signature: &[u8],
228    hint_offset: usize,
229    omega: usize,
230    k: usize,
231) -> Result<(), SignError> {
232    let hint = signature
233        .get(hint_offset..)
234        .ok_or_else(|| SignError::Deserialization("truncated ML-DSA hint".to_string()))?;
235    if hint.len() != omega + k {
236        return Err(SignError::Deserialization(
237            "invalid ML-DSA hint length".to_string(),
238        ));
239    }
240
241    let (indices, boundaries) = hint.split_at(omega);
242    let mut start = 0usize;
243    for &boundary in boundaries {
244        let end = usize::from(boundary);
245        if end < start || end > omega {
246            return Err(SignError::Deserialization(
247                "non-monotonic ML-DSA hint boundaries".to_string(),
248            ));
249        }
250        if !indices[start..end].windows(2).all(|pair| pair[0] < pair[1]) {
251            return Err(SignError::Deserialization(
252                "duplicate or unsorted ML-DSA hint indices".to_string(),
253            ));
254        }
255        start = end;
256    }
257
258    if indices[start..].iter().any(|&byte| byte != 0) {
259        return Err(SignError::Deserialization(
260            "nonzero unused ML-DSA hint bytes".to_string(),
261        ));
262    }
263
264    Ok(())
265}
266
267/// Internal adapter implemented only for the three parameter sets standardized
268/// by FIPS 204. It is public solely because it appears in a public trait impl.
269#[doc(hidden)]
270pub trait MlDsaBackend: MlDsaSchemeParams + Sized {
271    fn validate_public_key(bytes: &[u8]) -> Result<(), SignError> {
272        encoding::unpack_public_key::<Self>(bytes).map(|_| ())
273    }
274
275    fn validate_secret_key(bytes: &[u8]) -> Result<Vec<u8>, SignError> {
276        sign::validate_secret_key_internal::<Self>(bytes)
277    }
278
279    fn validate_key_pair(secret_key: &[u8], public_key: &[u8]) -> Result<(), SignError> {
280        Self::validate_public_key(public_key)?;
281        sign::validate_key_pair_internal::<Self>(secret_key, public_key)
282    }
283
284    fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> Result<(Vec<u8>, SecretVec), SignError> {
285        sign::keypair_internal::<Self, R>(rng)
286    }
287
288    fn sign_internal(
289        formatted_message: &[u8],
290        secret_key: &[u8],
291        randomizer: &[u8; 32],
292        supplied_mu: Option<&[u8; 64]>,
293    ) -> Result<Vec<u8>, SignError> {
294        sign::sign_internal::<Self>(formatted_message, secret_key, randomizer, supplied_mu)
295    }
296
297    fn verify_internal(
298        formatted_message: &[u8],
299        signature: &[u8],
300        public_key: &[u8],
301        supplied_mu: Option<&[u8; 64]>,
302    ) -> Result<(), SignError> {
303        sign::verify_internal::<Self>(formatted_message, signature, public_key, supplied_mu)
304    }
305}
306
307impl MlDsaBackend for MlDsa44Params {}
308impl MlDsaBackend for MlDsa65Params {}
309impl MlDsaBackend for MlDsa87Params {}
310
311/// ML-DSA signature scheme parameterized by a final FIPS 204 parameter set.
312pub struct MlDsa<P: MlDsaSchemeParams + 'static> {
313    _params: PhantomData<P>,
314}
315
316fn format_pure_message(message: &[u8], context: &[u8]) -> Result<Vec<u8>, SignError> {
317    if context.len() > u8::MAX as usize {
318        return Err(SignError::InvalidParameter(format!(
319            "ML-DSA context is {} bytes; maximum is 255",
320            context.len()
321        )));
322    }
323    let mut formatted = Vec::with_capacity(2 + context.len() + message.len());
324    formatted.push(0);
325    formatted.push(context.len() as u8);
326    formatted.extend_from_slice(context);
327    formatted.extend_from_slice(message);
328    Ok(formatted)
329}
330
331impl<P> MlDsa<P>
332where
333    P: MlDsaBackend + Send + Sync + 'static,
334{
335    /// Generate a hedged FIPS 204 pure signature with an explicit caller RNG.
336    pub fn sign_with_rng<R: CryptoRng + RngCore>(
337        message: &[u8],
338        secret_key: &MlDsaSecretKey,
339        rng: &mut R,
340    ) -> ApiResult<MlDsaSignature> {
341        Self::sign_with_context_rng(message, &[], secret_key, rng)
342    }
343
344    /// Generate a hedged FIPS 204 pure signature with a context and caller RNG.
345    pub fn sign_with_context_rng<R: CryptoRng + RngCore>(
346        message: &[u8],
347        context: &[u8],
348        secret_key: &MlDsaSecretKey,
349        rng: &mut R,
350    ) -> ApiResult<MlDsaSignature> {
351        let formatted = format_pure_message(message, context).map_err(dcrypt_api::Error::from)?;
352        let mut randomizer = Zeroizing::new([0u8; 32]);
353        try_fill_bytes_zeroing_on_error(rng, &mut *randomizer)
354            .map_err(|error| dcrypt_api::Error::from(SignError::Rng(error.to_string())))?;
355        let result = P::sign_internal(&formatted, secret_key.as_ref(), &randomizer, None)
356            .map(MlDsaSignature)
357            .map_err(dcrypt_api::Error::from);
358        result
359    }
360
361    /// Generate the optional deterministic FIPS 204 pure signature.
362    pub fn sign_deterministic(
363        message: &[u8],
364        secret_key: &MlDsaSecretKey,
365    ) -> ApiResult<MlDsaSignature> {
366        Self::sign_deterministic_with_context(message, &[], secret_key)
367    }
368
369    /// Generate the optional deterministic FIPS 204 pure signature with context.
370    pub fn sign_deterministic_with_context(
371        message: &[u8],
372        context: &[u8],
373        secret_key: &MlDsaSecretKey,
374    ) -> ApiResult<MlDsaSignature> {
375        let formatted = format_pure_message(message, context).map_err(dcrypt_api::Error::from)?;
376        let signature = P::sign_internal(&formatted, secret_key.as_ref(), &[0u8; 32], None)
377            .map_err(dcrypt_api::Error::from)?;
378        Ok(MlDsaSignature(signature))
379    }
380
381    /// Verify a pure FIPS 204 signature with an explicit context.
382    pub fn verify_with_context(
383        message: &[u8],
384        context: &[u8],
385        signature: &MlDsaSignature,
386        public_key: &MlDsaPublicKey,
387    ) -> ApiResult<()> {
388        validate_hint_encoding_for_len(signature.as_ref()).map_err(dcrypt_api::Error::from)?;
389        let formatted = format_pure_message(message, context).map_err(dcrypt_api::Error::from)?;
390        P::verify_internal(&formatted, signature.as_ref(), public_key.as_ref(), None)
391            .map_err(dcrypt_api::Error::from)
392    }
393
394    /// ACVP/internal interface: sign an already formatted `M'` with exact `rnd`.
395    #[doc(hidden)]
396    pub fn sign_internal_with_randomizer(
397        formatted_message: &[u8],
398        secret_key: &MlDsaSecretKey,
399        randomizer: &[u8; 32],
400    ) -> ApiResult<MlDsaSignature> {
401        P::sign_internal(formatted_message, secret_key.as_ref(), randomizer, None)
402            .map(MlDsaSignature)
403            .map_err(dcrypt_api::Error::from)
404    }
405
406    /// ACVP/internal interface: sign an externally supplied 64-byte `mu`.
407    #[doc(hidden)]
408    pub fn sign_mu_with_randomizer(
409        mu: &[u8; 64],
410        secret_key: &MlDsaSecretKey,
411        randomizer: &[u8; 32],
412    ) -> ApiResult<MlDsaSignature> {
413        P::sign_internal(&[], secret_key.as_ref(), randomizer, Some(mu))
414            .map(MlDsaSignature)
415            .map_err(dcrypt_api::Error::from)
416    }
417
418    /// ACVP/internal interface: verify an already formatted `M'`.
419    #[doc(hidden)]
420    pub fn verify_internal_message(
421        formatted_message: &[u8],
422        signature: &MlDsaSignature,
423        public_key: &MlDsaPublicKey,
424    ) -> ApiResult<()> {
425        validate_hint_encoding_for_len(signature.as_ref()).map_err(dcrypt_api::Error::from)?;
426        P::verify_internal(
427            formatted_message,
428            signature.as_ref(),
429            public_key.as_ref(),
430            None,
431        )
432        .map_err(dcrypt_api::Error::from)
433    }
434
435    /// ACVP/internal interface: verify against an externally supplied `mu`.
436    #[doc(hidden)]
437    pub fn verify_mu(
438        mu: &[u8; 64],
439        signature: &MlDsaSignature,
440        public_key: &MlDsaPublicKey,
441    ) -> ApiResult<()> {
442        validate_hint_encoding_for_len(signature.as_ref()).map_err(dcrypt_api::Error::from)?;
443        P::verify_internal(&[], signature.as_ref(), public_key.as_ref(), Some(mu))
444            .map_err(dcrypt_api::Error::from)
445    }
446}
447
448impl<P> SignatureTrait for MlDsa<P>
449where
450    P: MlDsaBackend + Send + Sync + 'static,
451{
452    type PublicKey = MlDsaPublicKey;
453    type SecretKey = MlDsaSecretKey;
454    type SignatureData = MlDsaSignature;
455    type KeyPair = (Self::PublicKey, Self::SecretKey);
456
457    fn name() -> &'static str {
458        P::NAME
459    }
460
461    fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Self::KeyPair> {
462        let (public, secret) = P::keypair(rng).map_err(dcrypt_api::Error::from)?;
463        Ok((
464            MlDsaPublicKey(public.clone()),
465            MlDsaSecretKey {
466                bytes: secret,
467                public_key: Some(public),
468            },
469        ))
470    }
471
472    fn public_key(keypair: &Self::KeyPair) -> Self::PublicKey {
473        keypair.0.clone()
474    }
475
476    fn secret_key(keypair: &Self::KeyPair) -> Self::SecretKey {
477        keypair.1.clone()
478    }
479
480    fn sign(message: &[u8], secret_key: &Self::SecretKey) -> ApiResult<Self::SignatureData> {
481        Self::sign_deterministic(message, secret_key)
482    }
483
484    fn verify(
485        message: &[u8],
486        signature: &Self::SignatureData,
487        public_key: &Self::PublicKey,
488    ) -> ApiResult<()> {
489        Self::verify_with_context(message, &[], signature, public_key)
490    }
491}
492
493fn validate_hint_encoding_for_len(signature: &[u8]) -> Result<(), SignError> {
494    match signature.len() {
495        2420 => validate_hint_encoding(signature, 32 + 4 * 576, 80, 4),
496        3309 => validate_hint_encoding(signature, 48 + 5 * 640, 55, 6),
497        4627 => validate_hint_encoding(signature, 64 + 7 * 640, 75, 8),
498        actual => Err(SignError::InvalidSignatureSize {
499            expected: 0,
500            actual,
501        }),
502    }
503}
504
505/// ML-DSA-44 (FIPS 204 security category 2).
506pub type MlDsa44 = MlDsa<MlDsa44Params>;
507/// ML-DSA-65 (FIPS 204 security category 3).
508pub type MlDsa65 = MlDsa<MlDsa65Params>;
509/// ML-DSA-87 (FIPS 204 security category 5).
510pub type MlDsa87 = MlDsa<MlDsa87Params>;
511
512#[cfg(test)]
513mod tests;