Skip to main content

qv_core/crypto/
engine.rs

1use ml_dsa::{MlDsa87, EncodedVerifyingKey, KeyGen};
2use ml_dsa::signature::{Signer, Verifier};
3use rand::rngs::OsRng;
4use sha3::{Sha3_256, Digest};
5use zeroize::Zeroizing;
6use crate::error::{QVError, QVResult};
7
8/// ML-DSA-87 signature size (FIPS 204, security level 5).
9pub const SIG_LEN: usize = 4627;
10/// ML-DSA-87 verifying key size.
11pub const VK_LEN: usize = 2592;
12/// Signing key seed size (32 bytes — preferred serialization).
13pub const SEED_LEN: usize = 32;
14
15// ─── Key wrappers ─────────────────────────────────────────────────────────────
16
17/// Signing (private) key — wraps the 32-byte seed + expanded state.
18pub struct QVSigningKey {
19    inner: ml_dsa::SigningKey<MlDsa87>,
20}
21
22/// Verifying (public) key for ML-DSA-87.
23pub struct QVVerifyingKey {
24    inner: ml_dsa::VerifyingKey<MlDsa87>,
25}
26
27impl QVSigningKey {
28    /// Restore from a 32-byte seed.
29    pub fn from_bytes(seed: &[u8]) -> QVResult<Self> {
30        if seed.len() != SEED_LEN {
31            return Err(QVError::KeyGenFailed);
32        }
33        let arr: ml_dsa::B32 = seed.try_into().map_err(|_| QVError::KeyGenFailed)?;
34        Ok(QVSigningKey { inner: MlDsa87::from_seed(&arr) })
35    }
36
37    /// Serialize as 32-byte seed (zeroized on drop).
38    pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
39        Zeroizing::new(self.inner.to_seed().to_vec())
40    }
41}
42
43impl QVVerifyingKey {
44    /// Deserialize from raw bytes (2592 bytes for ML-DSA-87).
45    pub fn from_bytes(bytes: &[u8]) -> QVResult<Self> {
46        let arr = EncodedVerifyingKey::<MlDsa87>::try_from(bytes)
47            .map_err(|_| QVError::KeyGenFailed)?;
48        Ok(QVVerifyingKey { inner: ml_dsa::VerifyingKey::decode(&arr) })
49    }
50
51    /// Serialize to raw bytes.
52    pub fn to_bytes(&self) -> Vec<u8> {
53        self.inner.encode().to_vec()
54    }
55}
56
57// ─── Operations ───────────────────────────────────────────────────────────────
58
59/// Generate a fresh ML-DSA-87 keypair using OS CSPRNG.
60///
61/// We generate a random 32-byte seed with `rand 0.8` (OS-backed) and pass it
62/// to `MlDsa87::from_seed` — this sidesteps the `rand_core` v0.6 vs v0.9
63/// version mismatch between our `rand` dep and ml-dsa's internal rand_core.
64pub fn generate_keypair() -> QVResult<(QVSigningKey, QVVerifyingKey)> {
65    use rand::RngCore;
66    let mut seed_bytes = [0u8; 32];
67    OsRng.fill_bytes(&mut seed_bytes);
68    let seed: ml_dsa::B32 = seed_bytes.into();
69    let sk_inner = MlDsa87::from_seed(&seed);
70    let vk_inner = ml_dsa::signature::Keypair::verifying_key(&sk_inner);
71    Ok((QVSigningKey { inner: sk_inner }, QVVerifyingKey { inner: vk_inner }))
72}
73
74/// Sign a message with ML-DSA-87 (deterministic). Returns 4627-byte signature.
75pub fn sign(sk: &QVSigningKey, message: &[u8]) -> QVResult<Vec<u8>> {
76    let sig: ml_dsa::Signature<MlDsa87> = sk.inner.try_sign(message)
77        .map_err(|_| QVError::SignatureInvalid)?;
78    Ok(sig.encode().to_vec())
79}
80
81/// Verify an ML-DSA-87 signature over `message`.
82pub fn verify(vk: &QVVerifyingKey, message: &[u8], signature_bytes: &[u8]) -> QVResult<()> {
83    let sig = ml_dsa::Signature::<MlDsa87>::try_from(signature_bytes)
84        .map_err(|_| QVError::SignatureInvalid)?;
85    vk.inner.verify(message, &sig).map_err(|_| QVError::SignatureInvalid)
86}
87
88/// SHA3-256 digest.
89pub fn sha3_256(data: &[u8]) -> [u8; 32] {
90    let mut h = Sha3_256::new();
91    h.update(data);
92    h.finalize().into()
93}