Skip to main content

crypto/mldsa/
mldsa44.rs

1//! ML-DSA-44 post-quantum signatures (FIPS 204, security category 2).
2//!
3//! See [`MlDsa44SecretKey`] and [`MlDsa44PublicKey`] for the signing and
4//! verification APIs.
5
6use super::mldsa::{self, MlDsaError, MlDsaKeyMaterial, PARAMS_44};
7
8/// Size in bytes of an encoded ML-DSA-44 public key.
9pub const ML_DSA_44_PUBLIC_KEY_SIZE: usize = 1312;
10/// Size in bytes of an encoded ML-DSA-44 signature.
11pub const ML_DSA_44_SIGNATURE_SIZE: usize = 2420;
12/// Size in bytes of an ML-DSA-44 seed (private key).
13pub const ML_DSA_44_SEED_SIZE: usize = mldsa::SEED_SIZE;
14/// Maximum length in bytes of an ML-DSA-44 context string.
15pub const ML_DSA_44_CONTEXT_MAX_LEN: usize = mldsa::CONTEXT_MAX_LEN;
16
17const K: usize = 4;
18const L: usize = 4;
19
20/// An ML-DSA-44 public key.
21///
22/// Verification is stateless. Use [`MlDsa44PublicKey::verify`] for a message
23/// and optional context, or [`MlDsa44PublicKey::verify_external_mu`] for a
24/// precomputed 64-byte message representative.
25///
26/// ```
27/// # use crypto::mldsa::MlDsa44SecretKey;
28/// # let seed = [0u8; 32];
29/// let key = MlDsa44SecretKey::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 MlDsa44PublicKey {
35    bytes: [u8; ML_DSA_44_PUBLIC_KEY_SIZE],
36}
37
38impl MlDsa44PublicKey {
39    /// Creates a public key from its 1312-byte encoded form.
40    pub fn from_bytes(bytes: &[u8; ML_DSA_44_PUBLIC_KEY_SIZE]) -> Self {
41        Self {
42            bytes: *bytes,
43        }
44    }
45
46    /// Returns the 1312-byte encoded form of this key.
47    pub fn to_bytes(&self) -> [u8; ML_DSA_44_PUBLIC_KEY_SIZE] {
48        self.bytes
49    }
50
51    /// Verifies an ML-DSA-44 `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_44_SIGNATURE_SIZE],
60        ctx: &[u8],
61    ) -> Result<(), MlDsaError> {
62        mldsa::verify_message::<K, L, ML_DSA_44_PUBLIC_KEY_SIZE, ML_DSA_44_SIGNATURE_SIZE>(
63            &PARAMS_44,
64            &self.bytes,
65            message,
66            signature,
67            ctx,
68        )
69    }
70
71    /// Verifies an ML-DSA-44 `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_44_SIGNATURE_SIZE],
82    ) -> Result<(), MlDsaError> {
83        mldsa::verify_external_mu::<K, L, ML_DSA_44_PUBLIC_KEY_SIZE, ML_DSA_44_SIGNATURE_SIZE>(
84            &PARAMS_44,
85            &self.bytes,
86            mu,
87            signature,
88        )
89    }
90}
91
92impl From<&[u8; ML_DSA_44_PUBLIC_KEY_SIZE]> for MlDsa44PublicKey {
93    fn from(bytes: &[u8; ML_DSA_44_PUBLIC_KEY_SIZE]) -> Self {
94        Self::from_bytes(bytes)
95    }
96}
97
98impl TryFrom<&[u8]> for MlDsa44PublicKey {
99    type Error = MlDsaError;
100
101    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
102        let bytes: &[u8; ML_DSA_44_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 [`MlDsa44SecretKey`].
108///
109/// Useful for sizing caller-owned/arena storage on memory-constrained targets.
110pub const ML_DSA_44_SECRET_KEY_SIZE: usize = core::mem::size_of::<MlDsa44SecretKey>();
111
112/// An expanded ML-DSA-44 secret key.
113///
114/// This is the only way to sign. Key generation runs once, in
115/// [`MlDsa44SecretKey::new`] or [`MlDsa44SecretKey::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_44_SECRET_KEY_SIZE`]
120/// bytes) that never allocates, which makes it usable on `no_std` and embedded
121/// targets.
122///
123/// Secrets are zeroized on drop when the `zeroize` feature is enabled.
124#[derive(Debug)]
125pub struct MlDsa44SecretKey {
126    inner: MlDsaKeyMaterial<K, L, ML_DSA_44_PUBLIC_KEY_SIZE>,
127}
128
129impl MlDsa44SecretKey {
130    /// Expands `seed` into a secret key, running the full FIPS 204 key
131    /// generation and caching the NTT-domain matrix and secret vectors.
132    pub fn new(seed: &[u8; ML_DSA_44_SEED_SIZE]) -> Self {
133        Self {
134            inner: MlDsaKeyMaterial::from_seed(&PARAMS_44, seed),
135        }
136    }
137
138    /// Generates a random secret key.
139    ///
140    /// The seed can be retrieved afterwards with [`MlDsa44SecretKey::seed`]
141    /// so it can be persisted.
142    #[cfg(feature = "random")]
143    pub fn generate() -> Self {
144        Self {
145            inner: MlDsaKeyMaterial::from_random(&PARAMS_44),
146        }
147    }
148
149    /// Returns the public key for this secret key.
150    pub fn public_key(&self) -> MlDsa44PublicKey {
151        MlDsa44PublicKey::from_bytes(self.inner.public_key())
152    }
153
154    /// Returns the 32-byte seed this key was initialized from.
155    pub fn seed(&self) -> &[u8; ML_DSA_44_SEED_SIZE] {
156        self.inner.seed()
157    }
158
159    /// Signs `message` with a fresh random nonce.
160    ///
161    /// `ctx` is the optional FIPS 204 context string and must be at most 255
162    /// bytes; it returns [`MlDsaError::ContextTooLong`] otherwise.
163    #[cfg(feature = "random")]
164    pub fn sign(&self, message: &[u8], ctx: &[u8]) -> Result<[u8; ML_DSA_44_SIGNATURE_SIZE], MlDsaError> {
165        let rnd: [u8; 32] = crate::random::random_bytes();
166        self.sign_derand(message, ctx, &rnd)
167    }
168
169    /// Signs `message` deterministically for a fixed 32-byte `rnd`.
170    ///
171    /// Passing `rnd = [0u8; 32]` gives the deterministic FIPS 204 variant;
172    /// any other value gives the hedged/randomized variant. `ctx` must be at
173    /// most 255 bytes, returning [`MlDsaError::ContextTooLong`] otherwise.
174    pub fn sign_derand(
175        &self,
176        message: &[u8],
177        ctx: &[u8],
178        rnd: &[u8; 32],
179    ) -> Result<[u8; ML_DSA_44_SIGNATURE_SIZE], MlDsaError> {
180        let mut sig = [0u8; ML_DSA_44_SIGNATURE_SIZE];
181        self.inner.sign_derand_into(&PARAMS_44, message, ctx, rnd, &mut sig)?;
182        Ok(sig)
183    }
184
185    /// Signs a precomputed 64-byte message representative `mu` (FIPS 204
186    /// "external mu" signing) with a fresh random nonce.
187    ///
188    /// `mu` must be the output of the FIPS 204 message-representative
189    /// computation; this function performs no domain separation or hashing.
190    #[cfg(feature = "random")]
191    pub fn sign_external_mu(&self, mu: &[u8; 64]) -> [u8; ML_DSA_44_SIGNATURE_SIZE] {
192        let rnd: [u8; 32] = crate::random::random_bytes();
193        self.sign_external_mu_derand(mu, &rnd)
194    }
195
196    /// Signs a precomputed 64-byte message representative `mu` (FIPS 204
197    /// "external mu" signing) deterministically for a fixed `rnd`.
198    pub fn sign_external_mu_derand(&self, mu: &[u8; 64], rnd: &[u8; 32]) -> [u8; ML_DSA_44_SIGNATURE_SIZE] {
199        let mut sig = [0u8; ML_DSA_44_SIGNATURE_SIZE];
200        self.inner.sign_external_mu_derand_into(&PARAMS_44, mu, rnd, &mut sig);
201        sig
202    }
203}