qs_crypto/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2//! Quantum-Sign cryptographic module boundary.
3//! Provides deterministic randomness interfaces and (eventually) signature/KEM glue.
4#![forbid(unsafe_code)]
5#![deny(missing_docs)]
6
7#[cfg(not(feature = "std"))]
8extern crate alloc;
9
10/// Utilities for canonical SPKI handling and key identifiers.
11pub mod public;
12#[cfg(feature = "std")]
13use std::{format, string::String, vec, vec::Vec};
14
15#[cfg(not(feature = "std"))]
16use alloc::{format, string::String, vec, vec::Vec};
17use core::{convert::TryFrom, fmt, mem, str::FromStr};
18use ml_dsa::{
19    EncodedSignature, EncodedSigningKey, EncodedVerifyingKey, KeyGen, MlDsa87,
20    Signature as MlSignature, SigningKey, VerifyingKey,
21};
22use pkcs8::spki::EncodePublicKey;
23pub use public::{
24    kid_from_spki_der, spki_der_canonical, spki_mldsa_paramset, spki_subject_key_bytes,
25};
26use qs_drbg::rand_adapter::DrbgRng;
27use qs_drbg::{Error as InnerError, HmacDrbg};
28use sha2::{Digest, Sha256};
29use zeroize::{Zeroize, ZeroizeOnDrop};
30
31/// FIPS 204 ML-DSA-87 canonical lengths (bytes).
32pub mod mldsa87 {
33    /// Public key length as mandated by FIPS 204 Table 2.
34    pub const PUBLIC_KEY_LEN: usize = 2592;
35    /// Secret key length as mandated by FIPS 204 Table 2.
36    pub const SECRET_KEY_LEN: usize = 4896;
37    /// Signature length as mandated by FIPS 204 Table 2.
38    pub const SIGNATURE_LEN: usize = 4627;
39}
40
41/// Length in bytes of an ML-DSA-87 signing key.
42pub const MLDSA87_SECRET_KEY_LEN: usize = mem::size_of::<EncodedSigningKey<MlDsa87>>();
43/// Length in bytes of an ML-DSA-87 verifying key.
44pub const MLDSA87_PUBLIC_KEY_LEN: usize = mem::size_of::<EncodedVerifyingKey<MlDsa87>>();
45/// Length in bytes of an ML-DSA-87 signature.
46pub const MLDSA87_SIGNATURE_LEN: usize = mem::size_of::<EncodedSignature<MlDsa87>>();
47
48const SIGNING_CONTEXT: &[u8] = b"quantum-sign.v1";
49/// Domain separator used when binding algorithm + policy into the signed transcript.
50const TRANSCRIPT_DOMAIN: &[u8] = b"quantum-sign:v1";
51/// Length of the transcript digest (SHA-256 output).
52pub const TRANSCRIPT_DIGEST_LEN: usize = 32;
53
54/// Supported artifact digest algorithms.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum DigestAlg {
57    /// SHA-256 with 32-byte output.
58    Sha256,
59    /// SHA-512 with 64-byte output.
60    Sha512,
61    /// SHAKE256 XOF truncated to 64 bytes.
62    Shake256_64,
63}
64
65impl DigestAlg {
66    /// String representation used in policy and intent metadata.
67    pub fn as_str(self) -> &'static str {
68        match self {
69            DigestAlg::Sha256 => "sha256",
70            DigestAlg::Sha512 => "sha512",
71            DigestAlg::Shake256_64 => "shake256-64",
72        }
73    }
74
75    /// Output length of the digest in bytes.
76    pub fn output_len(self) -> usize {
77        match self {
78            DigestAlg::Sha256 => 32,
79            DigestAlg::Sha512 | DigestAlg::Shake256_64 => 64,
80        }
81    }
82}
83
84impl FromStr for DigestAlg {
85    type Err = &'static str;
86
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        match s.to_ascii_lowercase().as_str() {
89            "sha256" => Ok(DigestAlg::Sha256),
90            "sha512" => Ok(DigestAlg::Sha512),
91            "shake256-64" | "shake256" => Ok(DigestAlg::Shake256_64),
92            _ => Err("unsupported digest algorithm"),
93        }
94    }
95}
96
97/// Errors surfaced by deterministic random bit generators inside the crypto module.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum DrbgError {
100    /// Generated output exceeded the maximum request length.
101    RequestTooLarge,
102    /// Generator must be reseeded with fresh entropy before more output is produced.
103    ReseedRequired,
104    /// Underlying entropy source failed.
105    EntropyUnavailable,
106    /// Entropy health check failed (weak or repeated seed material).
107    EntropyHealthFailed,
108}
109
110impl fmt::Display for DrbgError {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            DrbgError::RequestTooLarge => write!(f, "DRBG request exceeds per-call limit"),
114            DrbgError::ReseedRequired => write!(f, "DRBG reseed required"),
115            DrbgError::EntropyUnavailable => write!(f, "OS entropy unavailable"),
116            DrbgError::EntropyHealthFailed => write!(f, "entropy health check failed"),
117        }
118    }
119}
120
121#[cfg(feature = "std")]
122impl std::error::Error for DrbgError {}
123
124impl From<InnerError> for DrbgError {
125    fn from(value: InnerError) -> Self {
126        match value {
127            InnerError::RequestTooLarge => DrbgError::RequestTooLarge,
128            InnerError::ReseedRequired => DrbgError::ReseedRequired,
129            InnerError::EntropyUnavailable => DrbgError::EntropyUnavailable,
130            InnerError::EntropyHealthFailed => DrbgError::EntropyHealthFailed,
131        }
132    }
133}
134
135/// Compute the policy-bound transcript digest consumed by ML-DSA signatures.
136pub fn transcript_digest(
137    sign_alg: &str,
138    digest_alg: &str,
139    message_digest: &[u8],
140    policy_hash: Option<&[u8]>,
141) -> [u8; TRANSCRIPT_DIGEST_LEN] {
142    let mut hasher = Sha256::new();
143    hasher.update(TRANSCRIPT_DOMAIN);
144    hasher.update(b"|alg:");
145    hasher.update(sign_alg.as_bytes());
146    hasher.update(b"|hash:");
147    hasher.update(digest_alg.as_bytes());
148    if let Some(ph) = policy_hash {
149        hasher.update(b"|policy:");
150        hasher.update(ph);
151    }
152    hasher.update(b"|msg:");
153    hasher.update(message_digest);
154    let digest = hasher.finalize();
155    let mut out = [0u8; TRANSCRIPT_DIGEST_LEN];
156    out.copy_from_slice(&digest);
157    out
158}
159
160/// Trait implemented by deterministic random bit generators used by Quantum-Sign.
161pub trait DeterministicRng {
162    /// Fill `out` with pseudorandom bytes.
163    fn fill_bytes(&mut self, out: &mut [u8]) -> Result<(), DrbgError>;
164
165    /// Reseed the generator with new entropy and optional additional input.
166    fn reseed(&mut self, entropy: &[u8], additional_input: Option<&[u8]>) -> Result<(), DrbgError>;
167
168    /// Adjust the reseed interval (number of generate calls permitted before mandatory reseed).
169    fn set_reseed_interval(&mut self, interval: u64);
170
171    /// Adjust the byte budget that forces a reseed.
172    fn set_max_bytes_between_reseed(&mut self, bytes: u128);
173}
174
175/// HMAC-DRBG (SHA-512) wrapper implementing [`DeterministicRng`].
176#[derive(Zeroize, ZeroizeOnDrop)]
177pub struct HmacSha512Drbg {
178    inner: HmacDrbg,
179}
180
181impl HmacSha512Drbg {
182    /// Instantiate DRBG from caller-provided entropy/nonce/personalization strings.
183    pub fn new(
184        entropy: &[u8],
185        nonce: &[u8],
186        personalization: Option<&[u8]>,
187    ) -> Result<Self, DrbgError> {
188        let inner = HmacDrbg::new(entropy, nonce, personalization).map_err(DrbgError::from)?;
189        Ok(Self { inner })
190    }
191
192    /// Instantiate DRBG using the operating system CSPRNG for entropy and nonce.
193    pub fn from_os(personalization: Option<&[u8]>) -> Result<Self, DrbgError> {
194        let inner = HmacDrbg::from_os(personalization).map_err(DrbgError::from)?;
195        Ok(Self { inner })
196    }
197
198    /// Borrow the inner DRBG mutably (for adapters).
199    pub fn inner_mut(&mut self) -> &mut HmacDrbg {
200        &mut self.inner
201    }
202
203    /// Expose the internal state for testing; callers must not use this outside tests.
204    #[cfg(test)]
205    pub fn inner(&self) -> &HmacDrbg {
206        &self.inner
207    }
208}
209
210impl fmt::Debug for HmacSha512Drbg {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        f.debug_struct("HmacSha512Drbg").finish_non_exhaustive()
213    }
214}
215
216impl DeterministicRng for HmacSha512Drbg {
217    fn fill_bytes(&mut self, out: &mut [u8]) -> Result<(), DrbgError> {
218        self.inner.generate(out, None).map_err(DrbgError::from)
219    }
220
221    fn reseed(&mut self, entropy: &[u8], additional_input: Option<&[u8]>) -> Result<(), DrbgError> {
222        self.inner
223            .reseed(entropy, additional_input)
224            .map_err(DrbgError::from)
225    }
226
227    fn set_reseed_interval(&mut self, interval: u64) {
228        self.inner.set_reseed_interval(interval);
229    }
230
231    fn set_max_bytes_between_reseed(&mut self, bytes: u128) {
232        self.inner.set_max_bytes_between_reseed(bytes);
233    }
234}
235
236/// Utility for generating a fixed number of bytes using a fresh OS-seeded DRBG.
237pub fn random_bytes(len: usize) -> Result<Vec<u8>, DrbgError> {
238    let mut drbg = HmacSha512Drbg::from_os(None)?;
239    let mut buf = vec![0u8; len];
240    drbg.fill_bytes(&mut buf)?;
241    Ok(buf)
242}
243
244/// Simple keypair container.
245#[derive(Debug, Clone)]
246pub struct Keypair {
247    /// Secret signing key bytes.
248    pub secret: Vec<u8>,
249    /// Public verifying key bytes.
250    pub public: Vec<u8>,
251}
252
253impl Drop for Keypair {
254    fn drop(&mut self) {
255        self.zeroize();
256    }
257}
258
259impl Zeroize for Keypair {
260    fn zeroize(&mut self) {
261        self.secret.zeroize();
262        self.public.zeroize();
263    }
264}
265
266impl ZeroizeOnDrop for Keypair {}
267
268/// Cryptographic operation errors.
269#[derive(Debug)]
270pub enum CryptoError {
271    /// Deterministic RNG failure.
272    Drbg(DrbgError),
273    /// Provided key bytes were malformed.
274    InvalidKey,
275    /// Provided signature bytes were malformed.
276    InvalidSignature,
277    /// Signature generation failed.
278    SigningFailed,
279    /// The supplied digest length was incorrect.
280    BadDigestLen {
281        /// Expected digest length in bytes.
282        expected: usize,
283        /// Actual digest length supplied by the caller.
284        got: usize,
285    },
286    /// SPKI or public key parsing failed.
287    PublicKey(String),
288}
289
290impl fmt::Display for CryptoError {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        match self {
293            CryptoError::Drbg(err) => write!(f, "{err}"),
294            CryptoError::InvalidKey => write!(f, "invalid key material"),
295            CryptoError::InvalidSignature => write!(f, "invalid signature"),
296            CryptoError::SigningFailed => write!(f, "signature generation failed"),
297            CryptoError::BadDigestLen { expected, got } => {
298                write!(f, "bad digest length: expected {expected}, got {got}")
299            }
300            CryptoError::PublicKey(msg) => write!(f, "public key error: {msg}"),
301        }
302    }
303}
304
305impl From<DrbgError> for CryptoError {
306    fn from(err: DrbgError) -> Self {
307        CryptoError::Drbg(err)
308    }
309}
310
311#[cfg(feature = "std")]
312impl std::error::Error for CryptoError {}
313
314/// Generate an ML-DSA-87 keypair using the provided DRBG.
315pub fn keypair_mldsa87(drbg: &mut HmacSha512Drbg) -> Result<Keypair, CryptoError> {
316    let mut rng = DrbgRng::new(drbg.inner_mut());
317    let kp = MlDsa87::key_gen(&mut rng);
318    let sk = kp.signing_key().encode().to_vec();
319    let pk = kp.verifying_key().encode().to_vec();
320    Ok(Keypair {
321        secret: sk,
322        public: pk,
323    })
324}
325
326/// Produce an ML-DSA-87 signature over `message` (usually a digest) with explicit context.
327pub fn sign_mldsa87(
328    drbg: &mut HmacSha512Drbg,
329    secret_key: &[u8],
330    message_digest: &[u8],
331    digest_alg: DigestAlg,
332    policy_hash: Option<&[u8]>,
333) -> Result<Vec<u8>, CryptoError> {
334    if message_digest.len() != digest_alg.output_len() {
335        return Err(CryptoError::BadDigestLen {
336            expected: digest_alg.output_len(),
337            got: message_digest.len(),
338        });
339    }
340    let enc =
341        EncodedSigningKey::<MlDsa87>::try_from(secret_key).map_err(|_| CryptoError::InvalidKey)?;
342    let sk = SigningKey::<MlDsa87>::decode(&enc);
343    let transcript =
344        transcript_digest("mldsa-87", digest_alg.as_str(), message_digest, policy_hash);
345    drbg.inner_mut()
346        .generate(&mut [], Some(&transcript))
347        .map_err(DrbgError::from)?;
348    let mut rng = DrbgRng::new(drbg.inner_mut());
349    let sig = sk
350        .sign_randomized(transcript.as_slice(), SIGNING_CONTEXT, &mut rng)
351        .map_err(|_| CryptoError::SigningFailed)?;
352    Ok(sig.encode().to_vec())
353}
354
355/// Verify an ML-DSA-87 signature over `message`.
356pub fn verify_mldsa87(
357    public_key: &[u8],
358    message_digest: &[u8],
359    digest_alg: DigestAlg,
360    signature: &[u8],
361    policy_hash: Option<&[u8]>,
362) -> Result<(), CryptoError> {
363    if message_digest.len() != digest_alg.output_len() {
364        return Err(CryptoError::BadDigestLen {
365            expected: digest_alg.output_len(),
366            got: message_digest.len(),
367        });
368    }
369    if public_key.len() != mldsa87::PUBLIC_KEY_LEN {
370        return Err(CryptoError::InvalidSignature);
371    }
372    if signature.len() != mldsa87::SIGNATURE_LEN {
373        return Err(CryptoError::InvalidSignature);
374    }
375    let enc_vk = EncodedVerifyingKey::<MlDsa87>::try_from(public_key)
376        .map_err(|_| CryptoError::InvalidKey)?;
377    let vk = VerifyingKey::<MlDsa87>::decode(&enc_vk);
378    let enc_sig = EncodedSignature::<MlDsa87>::try_from(signature)
379        .map_err(|_| CryptoError::InvalidSignature)?;
380    let sig = MlSignature::<MlDsa87>::decode(&enc_sig).ok_or(CryptoError::InvalidSignature)?;
381    let transcript =
382        transcript_digest("mldsa-87", digest_alg.as_str(), message_digest, policy_hash);
383    if vk.verify_with_context(transcript.as_slice(), SIGNING_CONTEXT, &sig) {
384        Ok(())
385    } else {
386        Err(CryptoError::InvalidSignature)
387    }
388}
389
390/// Compute the canonical key identifier for an ML-DSA-87 verifying key.
391pub fn kid_from_public_key(public_key: &[u8]) -> Result<String, CryptoError> {
392    let spki = public_key_to_spki(public_key)?;
393    Ok(public::kid_from_spki_der(&spki))
394}
395
396/// Convert a raw ML-DSA-87 public key into canonical SPKI DER bytes.
397pub fn public_key_to_spki(public_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
398    let enc_vk = EncodedVerifyingKey::<MlDsa87>::try_from(public_key)
399        .map_err(|_| CryptoError::InvalidKey)?;
400    let vk = VerifyingKey::<MlDsa87>::decode(&enc_vk);
401    let spki = vk
402        .to_public_key_der()
403        .map_err(|_| CryptoError::InvalidKey)?;
404    Ok(spki.as_bytes().to_vec())
405}
406
407/// Verify an ML-DSA-87 signature against an SPKI-encoded public key.
408pub fn verify_mldsa87_spki(
409    spki_der: &[u8],
410    message_digest: &[u8],
411    digest_alg: DigestAlg,
412    signature: &[u8],
413    policy_hash: Option<&[u8]>,
414) -> Result<(), CryptoError> {
415    let public_key =
416        spki_subject_key_bytes(spki_der).map_err(|e| CryptoError::PublicKey(format!("{e}")))?;
417    if public_key.len() != mldsa87::PUBLIC_KEY_LEN {
418        return Err(CryptoError::InvalidSignature);
419    }
420    if signature.len() != mldsa87::SIGNATURE_LEN {
421        return Err(CryptoError::InvalidSignature);
422    }
423    verify_mldsa87(
424        &public_key,
425        message_digest,
426        digest_alg,
427        signature,
428        policy_hash,
429    )
430}
431/// Return true when the supplied signature algorithm identifier satisfies Level-5 policy.
432pub fn is_level5_sig_alg(alg: &str) -> bool {
433    matches!(
434        alg,
435        "mldsa-87"
436            | "slh-dsa-sha2-256s"
437            | "slh-dsa-sha2-256f"
438            | "slh-dsa-shake-256s"
439            | "slh-dsa-shake-256f"
440    )
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use sha2::{Digest, Sha512};
447
448    fn entropy(seed: u8) -> [u8; 48] {
449        let mut out = [0u8; 48];
450        for (i, byte) in out.iter_mut().enumerate() {
451            *byte = seed.wrapping_add(i as u8);
452        }
453        out
454    }
455
456    fn nonce(seed: u8) -> [u8; 16] {
457        let mut out = [0u8; 16];
458        for (i, byte) in out.iter_mut().enumerate() {
459            *byte = seed.wrapping_add((i * 5) as u8);
460        }
461        out
462    }
463
464    #[test]
465    fn keygen_and_sign_verify() {
466        let mut drbg = HmacSha512Drbg::new(&entropy(1), &nonce(2), Some(b"keygen")).unwrap();
467        let kp = keypair_mldsa87(&mut drbg).expect("keypair");
468
469        let mut signer_drbg = HmacSha512Drbg::new(&entropy(3), &nonce(4), Some(b"sign")).unwrap();
470        let mut hasher = Sha512::new();
471        hasher.update(b"deterministic digest");
472        let digest = hasher.finalize().to_vec();
473        let sig = sign_mldsa87(
474            &mut signer_drbg,
475            &kp.secret,
476            &digest,
477            DigestAlg::Sha512,
478            None,
479        )
480        .expect("sign");
481
482        verify_mldsa87(&kp.public, &digest, DigestAlg::Sha512, &sig, None).expect("verify");
483
484        let mut bad_hasher = Sha512::new();
485        bad_hasher.update(b"different");
486        let bad_digest = bad_hasher.finalize().to_vec();
487        assert!(verify_mldsa87(&kp.public, &bad_digest, DigestAlg::Sha512, &sig, None).is_err());
488    }
489
490    #[test]
491    fn reject_wrong_digest_length() {
492        let mut drbg = HmacSha512Drbg::new(&entropy(7), &nonce(8), Some(b"keygen")).unwrap();
493        let kp = keypair_mldsa87(&mut drbg).expect("keypair");
494        let mut signer_drbg = HmacSha512Drbg::new(&entropy(9), &nonce(10), Some(b"sign")).unwrap();
495        let short = vec![0u8; DigestAlg::Sha512.output_len() - 1];
496        assert!(matches!(
497            sign_mldsa87(
498                &mut signer_drbg,
499                &kp.secret,
500                &short,
501                DigestAlg::Sha512,
502                None
503            ),
504            Err(CryptoError::BadDigestLen { .. })
505        ));
506    }
507
508    #[test]
509    fn kid_from_public_key_has_expected_length() {
510        let mut drbg = HmacSha512Drbg::new(&entropy(11), &nonce(12), Some(b"keygen")).unwrap();
511        let kp = keypair_mldsa87(&mut drbg).expect("keypair");
512        let kid = kid_from_public_key(&kp.public).expect("kid from public");
513        assert_eq!(kid.len(), 16);
514    }
515
516    #[test]
517    fn verify_rejects_truncated_signature() {
518        let mut drbg = HmacSha512Drbg::new(&entropy(21), &nonce(22), Some(b"keygen")).unwrap();
519        let kp = keypair_mldsa87(&mut drbg).expect("keypair");
520        let mut signer_drbg = HmacSha512Drbg::new(&entropy(23), &nonce(24), Some(b"sign")).unwrap();
521        let digest = vec![0xAB; DigestAlg::Sha512.output_len()];
522        let sig = sign_mldsa87(
523            &mut signer_drbg,
524            &kp.secret,
525            &digest,
526            DigestAlg::Sha512,
527            None,
528        )
529        .expect("sign");
530        assert_eq!(sig.len(), mldsa87::SIGNATURE_LEN);
531
532        let mut truncated = sig.clone();
533        truncated.truncate(truncated.len() - 1);
534        assert!(verify_mldsa87(&kp.public, &digest, DigestAlg::Sha512, &sig, None).is_ok());
535        assert!(verify_mldsa87(&kp.public, &digest, DigestAlg::Sha512, &truncated, None).is_err());
536
537        // SPKI path
538        let spki = public_key_to_spki(&kp.public).expect("spki");
539        assert!(verify_mldsa87_spki(&spki, &digest, DigestAlg::Sha512, &truncated, None).is_err());
540    }
541}