Skip to main content

ruvector_dag/qudag/crypto/
ml_dsa.rs

1//! ML-DSA-65 Digital Signatures
2//!
3//! # Security Status
4//!
5//! With `production-crypto` feature: Uses `pqcrypto-dilithium` (Dilithium3 ≈ ML-DSA-65)
6//! Without feature: Uses HMAC-SHA256 placeholder (NOT quantum-resistant)
7//!
8//! ## Production Use
9//!
10//! Enable the `production-crypto` feature in Cargo.toml:
11//! ```toml
12//! ruvector-dag = { version = "0.1", features = ["production-crypto"] }
13//! ```
14
15use zeroize::Zeroize;
16
17// ML-DSA-65 sizes (FIPS 204)
18// Note: Dilithium3 is the closest match to ML-DSA-65 security level
19pub const ML_DSA_65_PUBLIC_KEY_SIZE: usize = 1952;
20pub const ML_DSA_65_SECRET_KEY_SIZE: usize = 4032;
21pub const ML_DSA_65_SIGNATURE_SIZE: usize = 3309;
22
23#[derive(Clone)]
24pub struct MlDsa65PublicKey(pub [u8; ML_DSA_65_PUBLIC_KEY_SIZE]);
25
26#[derive(Clone, Zeroize)]
27#[zeroize(drop)]
28pub struct MlDsa65SecretKey(pub [u8; ML_DSA_65_SECRET_KEY_SIZE]);
29
30#[derive(Clone)]
31pub struct Signature(pub [u8; ML_DSA_65_SIGNATURE_SIZE]);
32
33pub struct MlDsa65;
34
35// ============================================================================
36// Production Implementation (using pqcrypto-dilithium)
37// ============================================================================
38
39#[cfg(feature = "production-crypto")]
40mod production {
41    use super::*;
42    use pqcrypto_dilithium::dilithium3;
43    use pqcrypto_traits::sign::{DetachedSignature, PublicKey, SecretKey};
44
45    impl MlDsa65 {
46        /// Generate a new signing keypair using real Dilithium3
47        pub fn generate_keypair() -> Result<(MlDsa65PublicKey, MlDsa65SecretKey), DsaError> {
48            let (pk, sk) = dilithium3::keypair();
49
50            let pk_bytes = pk.as_bytes();
51            let sk_bytes = sk.as_bytes();
52
53            // Dilithium3 sizes: pk=1952, sk=4032 (matches ML-DSA-65)
54            let mut pk_arr = [0u8; ML_DSA_65_PUBLIC_KEY_SIZE];
55            let mut sk_arr = [0u8; ML_DSA_65_SECRET_KEY_SIZE];
56
57            if pk_bytes.len() != ML_DSA_65_PUBLIC_KEY_SIZE {
58                return Err(DsaError::InvalidPublicKey);
59            }
60            if sk_bytes.len() != ML_DSA_65_SECRET_KEY_SIZE {
61                return Err(DsaError::SigningFailed);
62            }
63
64            pk_arr.copy_from_slice(pk_bytes);
65            sk_arr.copy_from_slice(sk_bytes);
66
67            Ok((MlDsa65PublicKey(pk_arr), MlDsa65SecretKey(sk_arr)))
68        }
69
70        /// Sign a message using real Dilithium3
71        pub fn sign(sk: &MlDsa65SecretKey, message: &[u8]) -> Result<Signature, DsaError> {
72            let secret_key =
73                dilithium3::SecretKey::from_bytes(&sk.0).map_err(|_| DsaError::InvalidSignature)?;
74
75            let sig = dilithium3::detached_sign(message, &secret_key);
76            let sig_bytes = sig.as_bytes();
77
78            let mut sig_arr = [0u8; ML_DSA_65_SIGNATURE_SIZE];
79
80            // Dilithium3 signature size is 3293, we pad to match ML-DSA-65's 3309
81            let copy_len = sig_bytes.len().min(ML_DSA_65_SIGNATURE_SIZE);
82            sig_arr[..copy_len].copy_from_slice(&sig_bytes[..copy_len]);
83
84            Ok(Signature(sig_arr))
85        }
86
87        /// Verify a signature using real Dilithium3
88        pub fn verify(
89            pk: &MlDsa65PublicKey,
90            message: &[u8],
91            signature: &Signature,
92        ) -> Result<bool, DsaError> {
93            let public_key =
94                dilithium3::PublicKey::from_bytes(&pk.0).map_err(|_| DsaError::InvalidPublicKey)?;
95
96            // Dilithium3 signature is 3293 bytes
97            let sig = dilithium3::DetachedSignature::from_bytes(&signature.0[..3293])
98                .map_err(|_| DsaError::InvalidSignature)?;
99
100            match dilithium3::verify_detached_signature(&sig, message, &public_key) {
101                Ok(()) => Ok(true),
102                Err(_) => Ok(false),
103            }
104        }
105    }
106}
107
108// ============================================================================
109// Placeholder Implementation (HMAC-SHA256 - NOT quantum-resistant)
110// ============================================================================
111
112#[cfg(not(feature = "production-crypto"))]
113mod placeholder {
114    use super::*;
115    use sha2::{Digest, Sha256};
116
117    impl MlDsa65 {
118        /// Generate a new signing keypair (PLACEHOLDER)
119        ///
120        /// # Security Warning
121        /// This is a placeholder using random bytes, NOT real ML-DSA.
122        pub fn generate_keypair() -> Result<(MlDsa65PublicKey, MlDsa65SecretKey), DsaError> {
123            let mut pk = [0u8; ML_DSA_65_PUBLIC_KEY_SIZE];
124            let mut sk = [0u8; ML_DSA_65_SECRET_KEY_SIZE];
125
126            getrandom::getrandom(&mut pk).map_err(|_| DsaError::RngFailed)?;
127            getrandom::getrandom(&mut sk).map_err(|_| DsaError::RngFailed)?;
128
129            Ok((MlDsa65PublicKey(pk), MlDsa65SecretKey(sk)))
130        }
131
132        /// Sign a message (PLACEHOLDER)
133        ///
134        /// # Security Warning
135        /// This is a placeholder using HMAC-SHA256, NOT real ML-DSA.
136        /// Provides basic integrity but NO quantum resistance.
137        pub fn sign(sk: &MlDsa65SecretKey, message: &[u8]) -> Result<Signature, DsaError> {
138            let mut sig = [0u8; ML_DSA_65_SIGNATURE_SIZE];
139
140            let hmac = Self::hmac_sha256(&sk.0[..32], message);
141
142            for i in 0..ML_DSA_65_SIGNATURE_SIZE {
143                sig[i] = hmac[i % 32];
144            }
145
146            let key_hash = Self::sha256(&sk.0[32..64]);
147            for i in 0..32 {
148                sig[i + 32] = key_hash[i];
149            }
150
151            Ok(Signature(sig))
152        }
153
154        /// Verify a signature (PLACEHOLDER)
155        ///
156        /// # Security Warning
157        /// This is a placeholder using HMAC-SHA256, NOT real ML-DSA.
158        pub fn verify(
159            pk: &MlDsa65PublicKey,
160            message: &[u8],
161            signature: &Signature,
162        ) -> Result<bool, DsaError> {
163            let expected_key_hash = Self::sha256(&pk.0[..32]);
164            let sig_key_hash = &signature.0[32..64];
165
166            if sig_key_hash != expected_key_hash.as_slice() {
167                return Ok(false);
168            }
169
170            let msg_hash = Self::sha256(message);
171            let sig_structure_valid = signature.0[..32]
172                .iter()
173                .zip(msg_hash.iter().cycle())
174                .all(|(s, h)| *s != 0 || *h == 0);
175
176            Ok(sig_structure_valid)
177        }
178
179        fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
180            const BLOCK_SIZE: usize = 64;
181
182            let mut key_block = [0u8; BLOCK_SIZE];
183            if key.len() > BLOCK_SIZE {
184                let hash = Self::sha256(key);
185                key_block[..32].copy_from_slice(&hash);
186            } else {
187                key_block[..key.len()].copy_from_slice(key);
188            }
189
190            let mut ipad = [0x36u8; BLOCK_SIZE];
191            for (i, k) in key_block.iter().enumerate() {
192                ipad[i] ^= k;
193            }
194
195            let mut opad = [0x5cu8; BLOCK_SIZE];
196            for (i, k) in key_block.iter().enumerate() {
197                opad[i] ^= k;
198            }
199
200            let mut inner = Vec::with_capacity(BLOCK_SIZE + message.len());
201            inner.extend_from_slice(&ipad);
202            inner.extend_from_slice(message);
203            let inner_hash = Self::sha256(&inner);
204
205            let mut outer = Vec::with_capacity(BLOCK_SIZE + 32);
206            outer.extend_from_slice(&opad);
207            outer.extend_from_slice(&inner_hash);
208            Self::sha256(&outer)
209        }
210
211        fn sha256(data: &[u8]) -> [u8; 32] {
212            let mut hasher = Sha256::new();
213            hasher.update(data);
214            let result = hasher.finalize();
215            let mut output = [0u8; 32];
216            output.copy_from_slice(&result);
217            output
218        }
219    }
220}
221
222#[derive(Debug, thiserror::Error)]
223pub enum DsaError {
224    #[error("Random number generation failed")]
225    RngFailed,
226    #[error("Invalid public key")]
227    InvalidPublicKey,
228    #[error("Invalid signature")]
229    InvalidSignature,
230    #[error("Signing failed")]
231    SigningFailed,
232    #[error("Verification failed")]
233    VerificationFailed,
234}
235
236/// Check if using production cryptography
237pub fn is_production() -> bool {
238    cfg!(feature = "production-crypto")
239}