quantum_sign/crypto/
mod.rs

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