cdx_core/security/
ml_dsa.rs1use crate::error::invalid_manifest;
14use crate::{DocumentId, Result};
15
16use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo};
17use super::signer::{Signer, Verifier};
18
19#[cfg(feature = "ml-dsa")]
28pub struct MlDsaSigner {
29 signing_key: ml_dsa::SigningKey<ml_dsa::MlDsa65>,
30 seed: [u8; 32],
31 signer_info: SignerInfo,
32}
33
34#[cfg(feature = "ml-dsa")]
35impl MlDsaSigner {
36 pub fn from_bytes(seed_bytes: &[u8], signer_info: SignerInfo) -> Result<Self> {
49 use ml_dsa::KeyGen;
50
51 let seed: [u8; 32] = seed_bytes.try_into().map_err(|_| {
52 invalid_manifest(format!(
53 "Invalid ML-DSA-65 seed length: expected 32, got {}",
54 seed_bytes.len()
55 ))
56 })?;
57
58 let kp = ml_dsa::MlDsa65::from_seed(&seed.into());
59
60 Ok(Self {
61 signing_key: kp.signing_key().clone(),
62 seed,
63 signer_info,
64 })
65 }
66
67 #[allow(clippy::missing_panics_doc)] pub fn generate(signer_info: SignerInfo) -> Result<(Self, Vec<u8>)> {
76 use ml_dsa::KeyGen;
77
78 let kp = ml_dsa::MlDsa65::key_gen(&mut rand_core::UnwrapErr(getrandom::SysRng));
79 let seed: [u8; 32] = kp.to_seed().into();
80 let public_key_bytes = kp.verifying_key().encode().to_vec();
81
82 Ok((
83 Self {
84 signing_key: kp.signing_key().clone(),
85 seed,
86 signer_info,
87 },
88 public_key_bytes,
89 ))
90 }
91
92 #[must_use]
94 pub fn public_key_bytes(&self) -> Vec<u8> {
95 self.signing_key.verifying_key().encode().to_vec()
96 }
97
98 #[must_use]
104 pub fn secret_key_bytes(&self) -> Vec<u8> {
105 self.seed.to_vec()
106 }
107}
108
109#[cfg(feature = "ml-dsa")]
110impl Signer for MlDsaSigner {
111 fn algorithm(&self) -> SignatureAlgorithm {
112 SignatureAlgorithm::MlDsa65
113 }
114
115 fn signer_info(&self) -> SignerInfo {
116 self.signer_info.clone()
117 }
118
119 fn sign(&self, document_id: &DocumentId) -> Result<Signature> {
120 use base64::Engine;
121 use ml_dsa::signature::Signer as MlDsaSignerTrait;
122
123 if document_id.is_pending() {
124 return Err(crate::Error::InvalidManifest {
125 reason: "Cannot sign a pending document ID".to_string(),
126 });
127 }
128
129 let signature = self.signing_key.sign(document_id.digest());
131
132 let value = base64::engine::general_purpose::STANDARD.encode(signature.encode());
134
135 let sig_id = format!(
137 "sig-{}",
138 &crate::Hasher::hash(crate::HashAlgorithm::Sha256, value.as_bytes()).hex_digest()[..8]
139 );
140
141 Ok(Signature::new(
142 sig_id,
143 SignatureAlgorithm::MlDsa65,
144 self.signer_info.clone(),
145 value,
146 ))
147 }
148}
149
150#[cfg(feature = "ml-dsa")]
152pub struct MlDsaVerifier {
153 verifying_key: ml_dsa::VerifyingKey<ml_dsa::MlDsa65>,
154}
155
156#[cfg(feature = "ml-dsa")]
157impl MlDsaVerifier {
158 pub fn from_bytes(public_key_bytes: &[u8]) -> Result<Self> {
168 let verifying_key =
169 ml_dsa::VerifyingKey::decode(public_key_bytes.try_into().map_err(|_| {
170 invalid_manifest(format!(
171 "Invalid ML-DSA-65 public key length: got {}",
172 public_key_bytes.len()
173 ))
174 })?);
175
176 Ok(Self { verifying_key })
177 }
178}
179
180#[cfg(feature = "ml-dsa")]
181impl Verifier for MlDsaVerifier {
182 fn verify(
183 &self,
184 document_id: &DocumentId,
185 signature: &Signature,
186 ) -> Result<SignatureVerification> {
187 use base64::Engine;
188 use ml_dsa::signature::Verifier as MlDsaVerifierTrait;
189
190 if signature.algorithm != SignatureAlgorithm::MlDsa65 {
191 return Ok(SignatureVerification::invalid(
192 &signature.id,
193 format!(
194 "Algorithm mismatch: expected ML-DSA-65, got {}",
195 signature.algorithm
196 ),
197 ));
198 }
199
200 let sig_bytes = base64::engine::general_purpose::STANDARD
202 .decode(&signature.value)
203 .map_err(|e| invalid_manifest(format!("Failed to decode signature: {e}")))?;
204
205 let ml_sig =
207 ml_dsa::Signature::<ml_dsa::MlDsa65>::try_from(sig_bytes.as_slice()).map_err(|_| {
208 invalid_manifest(format!(
209 "Invalid ML-DSA-65 signature length: got {}",
210 sig_bytes.len()
211 ))
212 })?;
213
214 match self.verifying_key.verify(document_id.digest(), &ml_sig) {
216 Ok(()) => Ok(SignatureVerification::valid(&signature.id)),
217 Err(e) => Ok(SignatureVerification::invalid(
218 &signature.id,
219 format!("ML-DSA-65 signature verification failed: {e}"),
220 )),
221 }
222 }
223}
224
225#[cfg(all(test, feature = "ml-dsa"))]
226mod tests {
227 use super::*;
228 use crate::security::test_helpers;
229
230 fn generate_keypair() -> (MlDsaSigner, MlDsaVerifier) {
231 let signer_info = SignerInfo::new("Test ML-DSA Signer");
232 let (signer, public_key_bytes) = MlDsaSigner::generate(signer_info).unwrap();
233 let verifier = MlDsaVerifier::from_bytes(&public_key_bytes).unwrap();
234 (signer, verifier)
235 }
236
237 #[test]
238 fn test_generate_and_sign() {
239 let signer_info = SignerInfo::new("Test ML-DSA Signer");
240 let (signer, public_key_bytes) = MlDsaSigner::generate(signer_info).unwrap();
241
242 assert!(!public_key_bytes.is_empty());
243
244 test_helpers::assert_sign_produces_valid_signature(&signer, SignatureAlgorithm::MlDsa65);
245 }
246
247 #[test]
248 fn test_sign_and_verify() {
249 let (signer, verifier) = generate_keypair();
250 test_helpers::assert_sign_verify_roundtrip(&signer, &verifier);
251 }
252
253 #[test]
254 fn test_verify_wrong_document() {
255 let (signer, verifier) = generate_keypair();
256 test_helpers::assert_verify_wrong_document_fails(&signer, &verifier);
257 }
258
259 #[test]
260 fn test_cannot_sign_pending_id() {
261 let (signer, _) = generate_keypair();
262 test_helpers::assert_cannot_sign_pending_id(&signer);
263 }
264
265 #[test]
266 fn test_algorithm_mismatch() {
267 let (signer, verifier) = generate_keypair();
268 test_helpers::assert_algorithm_mismatch_rejected(
269 &signer,
270 &verifier,
271 SignatureAlgorithm::ES256,
272 );
273 }
274
275 #[test]
276 fn test_key_round_trip() {
277 let signer_info = SignerInfo::new("Test Signer");
278 let (original_signer, _) = MlDsaSigner::generate(signer_info.clone()).unwrap();
279
280 let secret_bytes = original_signer.secret_key_bytes();
281 let public_bytes = original_signer.public_key_bytes();
282
283 let restored_signer = MlDsaSigner::from_bytes(&secret_bytes, signer_info).unwrap();
284
285 let doc_id = crate::Hasher::hash(crate::HashAlgorithm::Sha256, b"test document");
286 let sig1 = original_signer.sign(&doc_id).unwrap();
287 let sig2 = restored_signer.sign(&doc_id).unwrap();
288
289 let verifier = MlDsaVerifier::from_bytes(&public_bytes).unwrap();
290 assert!(verifier.verify(&doc_id, &sig1).unwrap().is_valid());
291 assert!(verifier.verify(&doc_id, &sig2).unwrap().is_valid());
292 }
293}