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///
123/// Secrets are zeroized on drop when the `zeroize` feature is enabled.
124#[derive(Debug)]
125pub struct MlDsa87SecretKey {
126 inner: MlDsaKeyMaterial<K, L, ML_DSA_87_PUBLIC_KEY_SIZE>,
127}
128
129impl MlDsa87SecretKey {
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_87_SEED_SIZE]) -> Self {
133 Self {
134 inner: MlDsaKeyMaterial::from_seed(&PARAMS_87, seed),
135 }
136 }
137
138 /// Generates a random secret key.
139 ///
140 /// The seed can be retrieved afterwards with [`MlDsa87SecretKey::seed`]
141 /// so it can be persisted.
142 #[cfg(feature = "random")]
143 pub fn generate() -> Self {
144 Self {
145 inner: MlDsaKeyMaterial::from_random(&PARAMS_87),
146 }
147 }
148
149 /// Returns the public key for this secret key.
150 pub fn public_key(&self) -> MlDsa87PublicKey {
151 MlDsa87PublicKey::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_87_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_87_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_87_SIGNATURE_SIZE], MlDsaError> {
180 let mut sig = [0u8; ML_DSA_87_SIGNATURE_SIZE];
181 self.inner.sign_derand_into(&PARAMS_87, 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_87_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_87_SIGNATURE_SIZE] {
199 let mut sig = [0u8; ML_DSA_87_SIGNATURE_SIZE];
200 self.inner.sign_external_mu_derand_into(&PARAMS_87, mu, rnd, &mut sig);
201 sig
202 }
203}