1use std::path::Path;
6
7use anyhow::{Context, Result, anyhow, bail, ensure};
8use ml_dsa::common::getrandom::SysRng;
9use ml_dsa::pkcs8::der::asn1::OctetStringRef;
10use ml_dsa::pkcs8::der::oid::db::fips204;
11use ml_dsa::pkcs8::der::zeroize::Zeroizing;
12use ml_dsa::pkcs8::der::{AnyRef, Decode, SliceReader, Tag, Tagged};
13use ml_dsa::pkcs8::spki::AssociatedAlgorithmIdentifier;
14use ml_dsa::pkcs8::{
15 DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding, ObjectIdentifier,
16 PrivateKeyInfoRef, SecretDocument, SubjectPublicKeyInfoRef,
17};
18use ml_dsa::{Keypair, MlDsaParams, Seed, Signature, SigningKey, VerifyingKey};
19use strum::{Display, EnumString};
20
21pub trait MldsaPkcs8Params:
23 MlDsaParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>
24{
25}
26
27impl<P> MldsaPkcs8Params for P where
28 P: MlDsaParams + AssociatedAlgorithmIdentifier<Params = AnyRef<'static>>
29{
30}
31
32#[derive(Copy, Clone, PartialEq, Eq, Debug, EnumString, Display)]
34#[strum(ascii_case_insensitive)]
35pub enum MldsaParameterSet {
36 #[strum(serialize = "ML-DSA-44")]
37 Mldsa44,
38 #[strum(serialize = "ML-DSA-65")]
39 Mldsa65,
40 #[strum(serialize = "ML-DSA-87")]
41 Mldsa87,
42}
43
44impl MldsaParameterSet {
45 pub fn from_oid(oid: ObjectIdentifier) -> Option<Self> {
47 match oid {
48 fips204::ID_ML_DSA_44 => Some(Self::Mldsa44),
49 fips204::ID_ML_DSA_65 => Some(Self::Mldsa65),
50 fips204::ID_ML_DSA_87 => Some(Self::Mldsa87),
51 _ => None,
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, EnumString, Display)]
59#[strum(ascii_case_insensitive)]
60pub enum MldsaKeyFormat {
61 #[default]
62 #[strum(to_string = "pem", serialize = "pkcs8-pem")]
63 Pem,
64 #[strum(to_string = "der", serialize = "pkcs8-der")]
65 Der,
66}
67
68impl MldsaKeyFormat {
69 pub fn ext(&self) -> &'static str {
71 match self {
72 Self::Pem => "pem",
73 Self::Der => "der",
74 }
75 }
76
77 pub fn pub_ext(&self) -> String {
79 format!("pub.{}", self.ext())
80 }
81}
82
83fn decode_key(input: &[u8]) -> Result<SecretDocument> {
85 if input.trim_ascii_start().starts_with(b"-----BEGIN") {
86 let pem = std::str::from_utf8(input).context("Failed to read PEM contents as UTF-8")?;
87 Ok(SecretDocument::from_pem(pem)?.1)
88 } else {
89 Ok(SecretDocument::try_from(input)?)
90 }
91}
92
93pub fn read_mldsa_key_file(path: impl AsRef<Path>) -> Result<SecretDocument> {
95 let path = path.as_ref();
96 let data = Zeroizing::new(
97 std::fs::read(path).with_context(|| format!("Failed to read file: {path:?}"))?,
98 );
99 decode_key(&data)
100}
101
102fn signing_key_from_seed_and_expanded<P: MldsaPkcs8Params>(
104 private_key: &[u8],
105) -> Result<SigningKey<P>> {
106 let (seed, expanded) = AnyRef::from_der(private_key)?.sequence(|r: &mut SliceReader<'_>| {
107 Ok::<_, ml_dsa::pkcs8::der::Error>((
108 <&OctetStringRef>::decode(r)?.as_bytes(),
109 <&OctetStringRef>::decode(r)?.as_bytes(),
110 ))
111 })?;
112 let seed = Zeroizing::new(
113 Seed::try_from(seed).map_err(|_| anyhow!("Bad ML-DSA seed length {}", seed.len()))?,
114 );
115 let key = SigningKey::<P>::from_seed(&seed);
116 #[allow(deprecated)]
120 let derived = Zeroizing::new(key.expanded_key().to_expanded());
121 ensure!(
122 derived.as_slice() == expanded,
123 "ML-DSA expanded private key does not match the seed"
124 );
125 Ok(key)
126}
127
128fn signing_key_from_pkcs8<P: MldsaPkcs8Params>(
131 info: PrivateKeyInfoRef<'_>,
132) -> Result<SigningKey<P>> {
133 info.algorithm
134 .assert_algorithm_oid(P::ALGORITHM_IDENTIFIER.oid)?;
135 let private_key = info.private_key.as_bytes();
136 let key = match AnyRef::from_der(private_key)?.tag() {
137 Tag::Sequence => signing_key_from_seed_and_expanded::<P>(private_key)?,
138 Tag::OctetString => bail!("Expanded-only ML-DSA private keys are not supported"),
139 _ => SigningKey::<P>::try_from(info.clone())?,
140 };
141 if let Some(public_key) = &info.public_key {
142 ensure!(
143 public_key.as_bytes() == Some(key.verifying_key().encode().as_slice()),
144 "ML-DSA public key does not match the private key"
145 );
146 }
147 Ok(key)
148}
149
150pub fn mldsa_private_key_from_bytes<P: MldsaPkcs8Params>(input: &[u8]) -> Result<SigningKey<P>> {
152 let doc = decode_key(input)?;
153 signing_key_from_pkcs8(PrivateKeyInfoRef::from_der(doc.as_bytes())?)
154}
155
156pub fn load_mldsa_private_key<P: MldsaPkcs8Params>(
158 path: impl AsRef<Path>,
159) -> Result<SigningKey<P>> {
160 let doc = read_mldsa_key_file(path)?;
161 signing_key_from_pkcs8(PrivateKeyInfoRef::from_der(doc.as_bytes())?)
162}
163
164pub fn save_mldsa_private_key<P: MldsaPkcs8Params>(
166 key: &SigningKey<P>,
167 path: impl AsRef<Path>,
168 format: MldsaKeyFormat,
169) -> Result<()> {
170 let path = path.as_ref();
171 match format {
172 MldsaKeyFormat::Pem => std::fs::write(path, key.to_pkcs8_pem(LineEnding::default())?),
173 MldsaKeyFormat::Der => std::fs::write(path, key.to_pkcs8_der()?.as_bytes()),
174 }
175 .with_context(|| format!("Failed to write file: {path:?}"))
176}
177
178pub fn mldsa_public_key_from_bytes<P: MldsaPkcs8Params>(input: &[u8]) -> Result<VerifyingKey<P>> {
180 Ok(VerifyingKey::from_public_key_der(
181 decode_key(input)?.as_bytes(),
182 )?)
183}
184
185pub fn load_mldsa_public_key<P: MldsaPkcs8Params>(
187 path: impl AsRef<Path>,
188) -> Result<VerifyingKey<P>> {
189 Ok(VerifyingKey::from_public_key_der(
190 read_mldsa_key_file(path)?.as_bytes(),
191 )?)
192}
193
194pub fn mldsa_key_from_der<P: MldsaPkcs8Params>(
198 der: &[u8],
199) -> Result<(VerifyingKey<P>, Option<SigningKey<P>>)> {
200 if let Ok(spki) = SubjectPublicKeyInfoRef::from_der(der) {
203 return Ok((VerifyingKey::try_from(spki)?, None));
204 }
205 let info = PrivateKeyInfoRef::from_der(der)
206 .context("Not an ML-DSA public (SubjectPublicKeyInfo) or private (PKCS#8) key")?;
207 let key = signing_key_from_pkcs8(info)?;
208 Ok((key.verifying_key(), Some(key)))
209}
210
211pub fn load_mldsa_key<P: MldsaPkcs8Params>(
214 path: impl AsRef<Path>,
215) -> Result<(VerifyingKey<P>, Option<SigningKey<P>>)> {
216 mldsa_key_from_der(read_mldsa_key_file(path)?.as_bytes())
217}
218
219pub fn save_mldsa_public_key<P: MldsaPkcs8Params>(
221 key: &VerifyingKey<P>,
222 path: impl AsRef<Path>,
223 format: MldsaKeyFormat,
224) -> Result<()> {
225 let path = path.as_ref();
226 match format {
227 MldsaKeyFormat::Pem => std::fs::write(path, key.to_public_key_pem(LineEnding::default())?),
228 MldsaKeyFormat::Der => std::fs::write(path, key.to_public_key_der()?.as_bytes()),
229 }
230 .with_context(|| format!("Failed to write file: {path:?}"))
231}
232
233pub fn mldsa_parameter_set_from_der(der: &[u8]) -> Result<MldsaParameterSet> {
235 let oid = match SubjectPublicKeyInfoRef::from_der(der) {
236 Ok(spki) => spki.algorithm.oid,
237 Err(_) => PrivateKeyInfoRef::from_der(der)?.algorithm.oid,
238 };
239 MldsaParameterSet::from_oid(oid).ok_or_else(|| anyhow!("Not an ML-DSA key: {oid}"))
240}
241
242pub fn mldsa_sign<P: MlDsaParams>(
244 key: &SigningKey<P>,
245 context: &[u8],
246 msg: &[u8],
247) -> Result<Signature<P>> {
248 key.expanded_key()
249 .sign_randomized(msg, context, &mut SysRng)
250 .map_err(|_| anyhow!("ML-DSA signature generation failed"))
251}
252
253pub fn mldsa_verify<P: MlDsaParams>(
255 key: &VerifyingKey<P>,
256 context: &[u8],
257 signature: &[u8],
258 msg: &[u8],
259) -> Result<()> {
260 let signature = Signature::<P>::try_from(signature)
261 .map_err(|_| anyhow!("Malformed ML-DSA signature of length {}", signature.len()))?;
262 ensure!(
263 key.verify_with_context(msg, context, &signature),
264 "ML-DSA signature did not pass verification"
265 );
266 Ok(())
267}
268
269#[cfg(test)]
270mod test {
271 use super::*;
272 use crate::util::testdata;
273 use ml_dsa::pkcs8::der::Encode;
274 use ml_dsa::pkcs8::der::asn1::BitStringRef;
275 use ml_dsa::{Generate, MlDsa65, MlDsa87};
276
277 #[test]
278 fn key_encoding() -> Result<()> {
279 let sk = SigningKey::<MlDsa87>::try_generate()?;
280 let pk = sk.verifying_key();
281 let pem = sk.to_pkcs8_pem(LineEnding::default())?;
282 assert_eq!(mldsa_private_key_from_bytes::<MlDsa87>(pem.as_bytes())?, sk);
283 let der = sk.to_pkcs8_der()?;
284 assert_eq!(mldsa_private_key_from_bytes::<MlDsa87>(der.as_bytes())?, sk);
285 let pem = pk.to_public_key_pem(LineEnding::default())?;
286 assert_eq!(mldsa_public_key_from_bytes::<MlDsa87>(pem.as_bytes())?, pk);
287 let der = pk.to_public_key_der()?;
288 assert_eq!(mldsa_public_key_from_bytes::<MlDsa87>(der.as_bytes())?, pk);
289
290 assert!(mldsa_private_key_from_bytes::<MlDsa87>(der.as_bytes()).is_err());
292 let der = sk.to_pkcs8_der()?;
293 assert!(mldsa_public_key_from_bytes::<MlDsa87>(der.as_bytes()).is_err());
294 assert!(mldsa_private_key_from_bytes::<MlDsa65>(der.as_bytes()).is_err());
296 Ok(())
297 }
298
299 fn to_pkcs8_v2(sk: &SigningKey<MlDsa87>, pk: &VerifyingKey<MlDsa87>) -> Result<Vec<u8>> {
301 let v1 = sk.to_pkcs8_der()?;
302 let mut info = PrivateKeyInfoRef::from_der(v1.as_bytes())?;
303 let pk = pk.encode();
304 info.public_key = Some(BitStringRef::new(0, &pk)?);
305 Ok(info.to_der()?)
306 }
307
308 #[test]
309 fn pkcs8_v2() -> Result<()> {
310 let sk = SigningKey::<MlDsa87>::try_generate()?;
311 let v2 = to_pkcs8_v2(&sk, &sk.verifying_key())?;
312 assert_eq!(mldsa_private_key_from_bytes::<MlDsa87>(&v2)?, sk);
313
314 let other = SigningKey::<MlDsa87>::try_generate()?;
316 let v2 = to_pkcs8_v2(&sk, &other.verifying_key())?;
317 assert!(mldsa_private_key_from_bytes::<MlDsa87>(&v2).is_err());
318 let err = mldsa_key_from_der::<MlDsa87>(&v2).unwrap_err();
320 assert!(err.to_string().contains("does not match the private key"));
321 Ok(())
322 }
323
324 #[test]
325 fn sign_verify() -> Result<()> {
326 let sk = SigningKey::<MlDsa87>::try_generate()?;
327 let pk = sk.verifying_key();
328 let sig = mldsa_sign(&sk, b"context", b"message")?.encode();
329 mldsa_verify(&pk, b"context", &sig, b"message")?;
330 assert!(mldsa_verify(&pk, b"other", &sig, b"message").is_err());
331 assert!(mldsa_verify(&pk, b"context", &sig, b"other").is_err());
332 assert!(mldsa_verify(&pk, b"context", &sig[1..], b"message").is_err());
333
334 mldsa_sign(&sk, &[0x5a; 255], b"message")?;
336 assert!(mldsa_sign(&sk, &[0x5a; 256], b"message").is_err());
337 Ok(())
338 }
339
340 #[test]
341 fn parameter_set() {
342 let oid = MlDsa87::ALGORITHM_IDENTIFIER.oid;
343 assert_eq!(
344 MldsaParameterSet::from_oid(oid),
345 Some(MldsaParameterSet::Mldsa87)
346 );
347 assert_eq!(
348 "ml-dsa-87".parse::<MldsaParameterSet>(),
349 Ok(MldsaParameterSet::Mldsa87)
350 );
351 }
352
353 const KAT_MESSAGE: &[u8] = b"The quick brown fox jumps over the lazy dog";
355 const KAT_CONTEXT: &[u8] = b"OpenTitan";
356
357 #[test]
358 fn known_answer() -> Result<()> {
359 let public = std::fs::read(testdata("mldsa/mldsa87.pub.pem"))?;
360 let signature = std::fs::read(testdata("mldsa/mldsa87.sig"))?;
361 let pk = mldsa_public_key_from_bytes::<MlDsa87>(&public)?;
362 mldsa_verify(&pk, KAT_CONTEXT, &signature, KAT_MESSAGE)
363 }
364
365 #[test]
366 fn openssl_interop() -> Result<()> {
367 let seed_priv = std::fs::read(testdata("mldsa/openssl_mldsa87.seed-priv.pem"))?;
369 let seed_only = std::fs::read_to_string(testdata("mldsa/openssl_mldsa87.seed-only.pem"))?;
370 let priv_only = std::fs::read(testdata("mldsa/openssl_mldsa87.priv-only.pem"))?;
371 let public = std::fs::read(testdata("mldsa/openssl_mldsa87.pub.pem"))?;
372
373 let pk = mldsa_public_key_from_bytes::<MlDsa87>(&public)?;
374 let both = mldsa_private_key_from_bytes::<MlDsa87>(&seed_priv)?;
375 let seed = mldsa_private_key_from_bytes::<MlDsa87>(seed_only.as_bytes())?;
376 assert_eq!(both, seed);
377 assert_eq!(seed.verifying_key(), pk);
378 assert_eq!(
379 *seed.to_pkcs8_pem(LineEnding::LF)?,
380 seed_only.replace("\r\n", "\n")
381 );
382 let err = mldsa_private_key_from_bytes::<MlDsa87>(&priv_only).unwrap_err();
383 assert!(err.to_string().contains("Expanded-only"));
384 Ok(())
385 }
386}