Skip to main content

crypto/mldsa/
mldsa65.rs

1//! ML-DSA-65 post-quantum signatures (FIPS 204, security category 3).
2//!
3//! See [`MlDsa65SecretKey`] and [`MlDsa65PublicKey`] for the signing and
4//! verification APIs.
5
6use super::mldsa::{self, MlDsaError, MlDsaKeyMaterial, PARAMS_65};
7
8/// Size in bytes of an encoded ML-DSA-65 public key.
9pub const ML_DSA_65_PUBLIC_KEY_SIZE: usize = 1952;
10/// Size in bytes of an encoded ML-DSA-65 signature.
11pub const ML_DSA_65_SIGNATURE_SIZE: usize = 3309;
12/// Size in bytes of an ML-DSA-65 seed (private key).
13pub const ML_DSA_65_SEED_SIZE: usize = mldsa::SEED_SIZE;
14/// Maximum length in bytes of an ML-DSA-65 context string.
15pub const ML_DSA_65_CONTEXT_MAX_LEN: usize = mldsa::CONTEXT_MAX_LEN;
16
17const K: usize = 6;
18const L: usize = 5;
19
20/// An ML-DSA-65 public key.
21///
22/// Verification is stateless. Use [`MlDsa65PublicKey::verify`] for a message
23/// and optional context, or [`MlDsa65PublicKey::verify_external_mu`] for a
24/// precomputed 64-byte message representative.
25///
26/// ```
27/// # use crypto::mldsa::MlDsa65SecretKey;
28/// # let seed = [0u8; 32];
29/// let key = MlDsa65SecretKey::new(&seed);
30/// let signature = key.sign_derand(b"message", b"", &[0u8; 32]).unwrap();
31/// assert!(key.public_key().verify(b"message", &signature, b"").is_ok());
32/// ```
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct MlDsa65PublicKey {
35    bytes: [u8; ML_DSA_65_PUBLIC_KEY_SIZE],
36}
37
38impl MlDsa65PublicKey {
39    /// Creates a public key from its 1952-byte encoded form.
40    pub fn from_bytes(bytes: &[u8; ML_DSA_65_PUBLIC_KEY_SIZE]) -> Self {
41        Self {
42            bytes: *bytes,
43        }
44    }
45
46    /// Returns the 1952-byte encoded form of this key.
47    pub fn to_bytes(&self) -> [u8; ML_DSA_65_PUBLIC_KEY_SIZE] {
48        self.bytes
49    }
50
51    /// Verifies an ML-DSA-65 `signature` over `message` with the optional
52    /// context `ctx`.
53    ///
54    /// Returns [`MlDsaError::InvalidSignature`] if the signature is invalid and
55    /// [`MlDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
56    pub fn verify(
57        &self,
58        message: &[u8],
59        signature: &[u8; ML_DSA_65_SIGNATURE_SIZE],
60        ctx: &[u8],
61    ) -> Result<(), MlDsaError> {
62        mldsa::verify_message::<K, L, ML_DSA_65_PUBLIC_KEY_SIZE, ML_DSA_65_SIGNATURE_SIZE>(
63            &PARAMS_65,
64            &self.bytes,
65            message,
66            signature,
67            ctx,
68        )
69    }
70
71    /// Verifies an ML-DSA-65 `signature` over a precomputed 64-byte message
72    /// representative `mu` (FIPS 204 "external mu" verification).
73    ///
74    /// `mu` must be the output of the FIPS 204 message-representative
75    /// computation; this function performs no domain separation or hashing.
76    ///
77    /// Returns [`MlDsaError::InvalidSignature`] if the signature is invalid.
78    pub fn verify_external_mu(
79        &self,
80        mu: &[u8; 64],
81        signature: &[u8; ML_DSA_65_SIGNATURE_SIZE],
82    ) -> Result<(), MlDsaError> {
83        mldsa::verify_external_mu::<K, L, ML_DSA_65_PUBLIC_KEY_SIZE, ML_DSA_65_SIGNATURE_SIZE>(
84            &PARAMS_65,
85            &self.bytes,
86            mu,
87            signature,
88        )
89    }
90}
91
92impl From<&[u8; ML_DSA_65_PUBLIC_KEY_SIZE]> for MlDsa65PublicKey {
93    fn from(bytes: &[u8; ML_DSA_65_PUBLIC_KEY_SIZE]) -> Self {
94        Self::from_bytes(bytes)
95    }
96}
97
98impl TryFrom<&[u8]> for MlDsa65PublicKey {
99    type Error = MlDsaError;
100
101    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
102        let bytes: &[u8; ML_DSA_65_PUBLIC_KEY_SIZE] = bytes.try_into().map_err(|_| MlDsaError::InvalidPublicKey)?;
103        Ok(Self::from_bytes(bytes))
104    }
105}
106
107/// Size in bytes of an initialized [`MlDsa65SecretKey`].
108///
109/// Useful for sizing caller-owned/arena storage on memory-constrained targets.
110pub const ML_DSA_65_SECRET_KEY_SIZE: usize = core::mem::size_of::<MlDsa65SecretKey>();
111
112/// An expanded ML-DSA-65 secret key.
113///
114/// This is the only way to sign. Key generation runs once, in
115/// [`MlDsa65SecretKey::new`] or [`MlDsa65SecretKey::generate`], and the
116/// resulting matrix `A` and secret vectors in the NTT domain are cached, so
117/// signing does not repeat the expensive key generation.
118///
119/// The key is a plain fixed-size value (about [`ML_DSA_65_SECRET_KEY_SIZE`]
120/// bytes) that never allocates, which makes it usable on `no_std` and embedded
121/// targets.
122#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
123pub struct MlDsa65SecretKey {
124    inner: MlDsaKeyMaterial<K, L, ML_DSA_65_PUBLIC_KEY_SIZE>,
125}
126
127impl MlDsa65SecretKey {
128    /// Expands `seed` into a secret key, running the full FIPS 204 key
129    /// generation and caching the NTT-domain matrix and secret vectors.
130    pub fn new(seed: &[u8; ML_DSA_65_SEED_SIZE]) -> Self {
131        Self {
132            inner: MlDsaKeyMaterial::from_seed(&PARAMS_65, seed),
133        }
134    }
135
136    /// Generates a random secret key.
137    ///
138    /// The seed can be retrieved afterwards with [`MlDsa65SecretKey::seed`]
139    /// so it can be persisted.
140    ///
141    /// Returns [`MlDsaError::Random`] when the operating system's random
142    /// number generator is unavailable or fails.
143    #[cfg(feature = "random")]
144    pub fn generate() -> Result<Self, MlDsaError> {
145        Ok(Self {
146            inner: MlDsaKeyMaterial::from_random(&PARAMS_65)?,
147        })
148    }
149
150    /// Returns the public key for this secret key.
151    pub fn public_key(&self) -> MlDsa65PublicKey {
152        MlDsa65PublicKey::from_bytes(self.inner.public_key())
153    }
154
155    /// Returns the 32-byte seed this key was initialized from.
156    pub fn seed(&self) -> &[u8; ML_DSA_65_SEED_SIZE] {
157        self.inner.seed()
158    }
159
160    /// Signs `message` with a fresh random nonce.
161    ///
162    /// `ctx` is the optional FIPS 204 context string and must be at most 255
163    /// bytes; it returns [`MlDsaError::ContextTooLong`] otherwise. It returns
164    /// [`MlDsaError::Random`] when the operating system's random number
165    /// generator is unavailable or fails.
166    #[cfg(feature = "random")]
167    pub fn sign(&self, message: &[u8], ctx: &[u8]) -> Result<[u8; ML_DSA_65_SIGNATURE_SIZE], MlDsaError> {
168        let rnd: [u8; 32] = crate::random::bytes()?;
169        self.sign_derand(message, ctx, &rnd)
170    }
171
172    /// Signs `message` deterministically for a fixed 32-byte `rnd`.
173    ///
174    /// Passing `rnd = [0u8; 32]` gives the deterministic FIPS 204 variant;
175    /// any other value gives the hedged/randomized variant. `ctx` must be at
176    /// most 255 bytes, returning [`MlDsaError::ContextTooLong`] otherwise.
177    pub fn sign_derand(
178        &self,
179        message: &[u8],
180        ctx: &[u8],
181        rnd: &[u8; 32],
182    ) -> Result<[u8; ML_DSA_65_SIGNATURE_SIZE], MlDsaError> {
183        let mut sig = [0u8; ML_DSA_65_SIGNATURE_SIZE];
184        self.inner.sign_derand_into(&PARAMS_65, message, ctx, rnd, &mut sig)?;
185        Ok(sig)
186    }
187
188    /// Signs a precomputed 64-byte message representative `mu` (FIPS 204
189    /// "external mu" signing) with a fresh random nonce.
190    ///
191    /// `mu` must be the output of the FIPS 204 message-representative
192    /// computation; this function performs no domain separation or hashing.
193    ///
194    /// Returns [`MlDsaError::Random`] when the operating system's random
195    /// number generator is unavailable or fails.
196    #[cfg(feature = "random")]
197    pub fn sign_external_mu(&self, mu: &[u8; 64]) -> Result<[u8; ML_DSA_65_SIGNATURE_SIZE], MlDsaError> {
198        let rnd: [u8; 32] = crate::random::bytes()?;
199        self.sign_external_mu_derand(mu, &rnd)
200    }
201
202    /// Signs a precomputed 64-byte message representative `mu` (FIPS 204
203    /// "external mu" signing) deterministically for a fixed `rnd`.
204    ///
205    /// Returns [`MlDsaError::RetryLimitExceeded`] in the (statistically
206    /// impossible) event that the signing rejection loop exceeds the FIPS 204
207    /// limit of 814 iterations.
208    pub fn sign_external_mu_derand(
209        &self,
210        mu: &[u8; 64],
211        rnd: &[u8; 32],
212    ) -> Result<[u8; ML_DSA_65_SIGNATURE_SIZE], MlDsaError> {
213        let mut sig = [0u8; ML_DSA_65_SIGNATURE_SIZE];
214        self.inner.sign_external_mu_derand_into(&PARAMS_65, mu, rnd, &mut sig)?;
215        Ok(sig)
216    }
217}