crypto/mldsa/mldsa87.rs
1//! ML-DSA-87 post-quantum signatures (FIPS 204, security category 5).
2//!
3//! See [`MlDsa87SecretKey`] and [`MlDsa87PublicKey`] for the signing and
4//! verification APIs.
5
6use super::mldsa::{self, MlDsaError, MlDsaKeyMaterial, PARAMS_87};
7
8/// Size in bytes of an encoded ML-DSA-87 public key.
9pub const ML_DSA_87_PUBLIC_KEY_SIZE: usize = 2592;
10/// Size in bytes of an encoded ML-DSA-87 signature.
11pub const ML_DSA_87_SIGNATURE_SIZE: usize = 4627;
12/// Size in bytes of an ML-DSA-87 seed (private key).
13pub const ML_DSA_87_SEED_SIZE: usize = mldsa::SEED_SIZE;
14/// Maximum length in bytes of an ML-DSA-87 context string.
15pub const ML_DSA_87_CONTEXT_MAX_LEN: usize = mldsa::CONTEXT_MAX_LEN;
16
17const K: usize = 8;
18const L: usize = 7;
19
20/// An ML-DSA-87 public key.
21///
22/// Verification is stateless. Use [`MlDsa87PublicKey::verify`] for a message
23/// and optional context, or [`MlDsa87PublicKey::verify_external_mu`] for a
24/// precomputed 64-byte message representative.
25///
26/// ```
27/// # use crypto::mldsa::MlDsa87SecretKey;
28/// # let seed = [0u8; 32];
29/// let key = MlDsa87SecretKey::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 MlDsa87PublicKey {
35 bytes: [u8; ML_DSA_87_PUBLIC_KEY_SIZE],
36}
37
38impl MlDsa87PublicKey {
39 /// Creates a public key from its 2592-byte encoded form.
40 pub fn from_bytes(bytes: &[u8; ML_DSA_87_PUBLIC_KEY_SIZE]) -> Self {
41 Self {
42 bytes: *bytes,
43 }
44 }
45
46 /// Returns the 2592-byte encoded form of this key.
47 pub fn to_bytes(&self) -> [u8; ML_DSA_87_PUBLIC_KEY_SIZE] {
48 self.bytes
49 }
50
51 /// Verifies an ML-DSA-87 `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_87_SIGNATURE_SIZE],
60 ctx: &[u8],
61 ) -> Result<(), MlDsaError> {
62 mldsa::verify_message::<K, L, ML_DSA_87_PUBLIC_KEY_SIZE, ML_DSA_87_SIGNATURE_SIZE>(
63 &PARAMS_87,
64 &self.bytes,
65 message,
66 signature,
67 ctx,
68 )
69 }
70
71 /// Verifies an ML-DSA-87 `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_87_SIGNATURE_SIZE],
82 ) -> Result<(), MlDsaError> {
83 mldsa::verify_external_mu::<K, L, ML_DSA_87_PUBLIC_KEY_SIZE, ML_DSA_87_SIGNATURE_SIZE>(
84 &PARAMS_87,
85 &self.bytes,
86 mu,
87 signature,
88 )
89 }
90}
91
92impl From<&[u8; ML_DSA_87_PUBLIC_KEY_SIZE]> for MlDsa87PublicKey {
93 fn from(bytes: &[u8; ML_DSA_87_PUBLIC_KEY_SIZE]) -> Self {
94 Self::from_bytes(bytes)
95 }
96}
97
98impl TryFrom<&[u8]> for MlDsa87PublicKey {
99 type Error = MlDsaError;
100
101 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
102 let bytes: &[u8; ML_DSA_87_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 [`MlDsa87SecretKey`].
108///
109/// Useful for sizing caller-owned/arena storage on memory-constrained targets.
110pub const ML_DSA_87_SECRET_KEY_SIZE: usize = core::mem::size_of::<MlDsa87SecretKey>();
111
112/// An expanded ML-DSA-87 secret key.
113///
114/// This is the only way to sign. Key generation runs once, in
115/// [`MlDsa87SecretKey::new`] or [`MlDsa87SecretKey::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_87_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 MlDsa87SecretKey {
124 inner: MlDsaKeyMaterial<K, L, ML_DSA_87_PUBLIC_KEY_SIZE>,
125}
126
127impl MlDsa87SecretKey {
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_87_SEED_SIZE]) -> Self {
131 Self {
132 inner: MlDsaKeyMaterial::from_seed(&PARAMS_87, seed),
133 }
134 }
135
136 /// Generates a random secret key.
137 ///
138 /// The seed can be retrieved afterwards with [`MlDsa87SecretKey::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_87)?,
147 })
148 }
149
150 /// Returns the public key for this secret key.
151 pub fn public_key(&self) -> MlDsa87PublicKey {
152 MlDsa87PublicKey::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_87_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_87_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_87_SIGNATURE_SIZE], MlDsaError> {
183 let mut sig = [0u8; ML_DSA_87_SIGNATURE_SIZE];
184 self.inner.sign_derand_into(&PARAMS_87, 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_87_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_87_SIGNATURE_SIZE], MlDsaError> {
213 let mut sig = [0u8; ML_DSA_87_SIGNATURE_SIZE];
214 self.inner.sign_external_mu_derand_into(&PARAMS_87, mu, rnd, &mut sig)?;
215 Ok(sig)
216 }
217}