saorsa_pqc/api/sig.rs
1// Rust 1.92+ raises unused_assignments on struct fields read via getter methods
2// when used with derive macros like Zeroize. This is a known false positive.
3#![allow(unused_assignments)]
4
5//! ML-DSA (Module-Lattice-Based Digital Signature Algorithm) API
6//!
7//! Provides a simple interface to FIPS 204 ML-DSA for quantum-resistant digital signatures
8//! without requiring users to manage RNG or internal details.
9//!
10//! # Examples
11//!
12//! ## Basic Signature and Verification
13//! ```rust,no_run
14//! use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaVariant};
15//!
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! // Create ML-DSA instance with 192-bit security
18//! let dsa = ml_dsa_65();
19//!
20//! // Generate a signing keypair
21//! let (public_key, secret_key) = dsa.generate_keypair()?;
22//!
23//! // Sign a message
24//! let message = b"Important document to sign";
25//! let signature = dsa.sign(&secret_key, message)?;
26//!
27//! // Verify the signature
28//! let is_valid = dsa.verify(&public_key, message, &signature)?;
29//! assert!(is_valid);
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## Document Signing with Context
35//! ```rust,no_run
36//! use saorsa_pqc::api::sig::{MlDsa, MlDsaVariant};
37//!
38//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
39//! let dsa = MlDsa::new(MlDsaVariant::MlDsa87); // Maximum security
40//! let (public_key, secret_key) = dsa.generate_keypair()?;
41//!
42//! // Sign with additional context for domain separation
43//! let document = b"Contract #12345";
44//! let context = b"legal-documents-v1";
45//! let signature = dsa.sign_with_context(&secret_key, document, context)?;
46//!
47//! // Verify with the same context
48//! let valid = dsa.verify_with_context(&public_key, document, &signature, context)?;
49//! assert!(valid);
50//! # Ok(())
51//! # }
52//! ```
53//!
54//! ## Security Levels
55//! - ML-DSA-44: NIST Level 2 (~128-bit classical, ~90-bit quantum)
56//! - ML-DSA-65: NIST Level 3 (~192-bit classical, ~128-bit quantum)
57//! - ML-DSA-87: NIST Level 5 (~256-bit classical, ~170-bit quantum)
58
59use super::errors::{PqcError, PqcResult};
60use rand_core::OsRng;
61use zeroize::{Zeroize, ZeroizeOnDrop};
62
63// Import FIPS implementations
64use fips204::traits::{KeyGen, SerDes, Signer, Verifier};
65use fips204::{ml_dsa_44, ml_dsa_65, ml_dsa_87};
66
67/// ML-DSA algorithm variants
68///
69/// Selects the security level and performance characteristics for ML-DSA operations.
70/// Higher security levels provide more protection but require larger keys and signatures.
71///
72/// # Examples
73/// ```rust,no_run
74/// use saorsa_pqc::api::sig::MlDsaVariant;
75///
76/// // Choose based on security requirements
77/// let standard = MlDsaVariant::MlDsa65; // Recommended for most uses
78/// let lightweight = MlDsaVariant::MlDsa44; // For constrained environments
79/// let maximum = MlDsaVariant::MlDsa87; // For highest security needs
80///
81/// println!("Public key size: {} bytes", standard.public_key_size());
82/// println!("Signature size: {} bytes", standard.signature_size());
83/// println!("Security: {}", standard.security_level());
84/// ```
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum MlDsaVariant {
87 /// ML-DSA-44: NIST Level 2 security (~128-bit classical)
88 /// - Public key: 1312 bytes
89 /// - Secret key: 2560 bytes
90 /// - Signature: 2420 bytes
91 MlDsa44,
92 /// ML-DSA-65: NIST Level 3 security (~192-bit classical) [RECOMMENDED]
93 /// - Public key: 1952 bytes
94 /// - Secret key: 4032 bytes
95 /// - Signature: 3309 bytes
96 MlDsa65,
97 /// ML-DSA-87: NIST Level 5 security (~256-bit classical)
98 /// - Public key: 2592 bytes
99 /// - Secret key: 4896 bytes
100 /// - Signature: 4627 bytes
101 MlDsa87,
102}
103
104// Manual implementation of Zeroize for MlDsaVariant (no-op since it contains no sensitive data)
105impl zeroize::Zeroize for MlDsaVariant {
106 fn zeroize(&mut self) {
107 // No sensitive data to zeroize in an enum variant selector
108 }
109}
110
111impl MlDsaVariant {
112 /// Get the public key size in bytes
113 #[must_use]
114 pub const fn public_key_size(&self) -> usize {
115 match self {
116 Self::MlDsa44 => 1312,
117 Self::MlDsa65 => 1952,
118 Self::MlDsa87 => 2592,
119 }
120 }
121
122 /// Get the secret key size in bytes
123 #[must_use]
124 pub const fn secret_key_size(&self) -> usize {
125 match self {
126 Self::MlDsa44 => 2560,
127 Self::MlDsa65 => 4032,
128 Self::MlDsa87 => 4896,
129 }
130 }
131
132 /// Get the signature size in bytes
133 #[must_use]
134 pub const fn signature_size(&self) -> usize {
135 match self {
136 Self::MlDsa44 => 2420,
137 Self::MlDsa65 => 3309,
138 Self::MlDsa87 => 4627,
139 }
140 }
141
142 /// Get the security level description
143 #[must_use]
144 pub const fn security_level(&self) -> &'static str {
145 match self {
146 Self::MlDsa44 => "NIST Level 2 (~128-bit)",
147 Self::MlDsa65 => "NIST Level 3 (~192-bit)",
148 Self::MlDsa87 => "NIST Level 5 (~256-bit)",
149 }
150 }
151
152 /// Maximum context length (255 bytes for all variants)
153 pub const MAX_CONTEXT_LENGTH: usize = 255;
154}
155
156/// ML-DSA public key
157///
158/// Contains the public verification key for ML-DSA signatures.
159/// This key can be freely shared and is used to verify signatures.
160///
161/// # Examples
162/// ```rust,no_run
163/// use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaPublicKey, MlDsaVariant};
164///
165/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
166/// let dsa = ml_dsa_65();
167/// let (public_key, _) = dsa.generate_keypair()?;
168///
169/// // Export for distribution
170/// let key_bytes = public_key.to_bytes();
171/// println!("Public key size: {} bytes", key_bytes.len());
172///
173/// // Import from bytes
174/// let imported = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, &key_bytes)?;
175/// assert_eq!(public_key.variant(), imported.variant());
176/// # Ok(())
177/// # }
178/// ```
179#[derive(Clone, Zeroize, ZeroizeOnDrop)]
180pub struct MlDsaPublicKey {
181 #[zeroize(skip)]
182 variant: MlDsaVariant,
183 bytes: Vec<u8>,
184}
185
186impl MlDsaPublicKey {
187 /// Get the variant of this key
188 #[must_use]
189 pub const fn variant(&self) -> MlDsaVariant {
190 self.variant
191 }
192
193 /// Export the public key as bytes
194 #[must_use]
195 pub fn to_bytes(&self) -> Vec<u8> {
196 self.bytes.clone()
197 }
198
199 /// Import a public key from bytes
200 ///
201 /// # Errors
202 ///
203 /// Returns an error if the byte slice has an incorrect length for the specified variant.
204 pub fn from_bytes(variant: MlDsaVariant, bytes: &[u8]) -> PqcResult<Self> {
205 if bytes.len() != variant.public_key_size() {
206 return Err(PqcError::InvalidKeySize {
207 expected: variant.public_key_size(),
208 got: bytes.len(),
209 });
210 }
211
212 // Validate by trying to deserialize
213 match variant {
214 MlDsaVariant::MlDsa44 => {
215 let _ = ml_dsa_44::PublicKey::try_from_bytes(bytes.try_into().map_err(|_| {
216 PqcError::InvalidKeySize {
217 expected: variant.public_key_size(),
218 got: bytes.len(),
219 }
220 })?)
221 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
222 }
223 MlDsaVariant::MlDsa65 => {
224 let _ = ml_dsa_65::PublicKey::try_from_bytes(bytes.try_into().map_err(|_| {
225 PqcError::InvalidKeySize {
226 expected: variant.public_key_size(),
227 got: bytes.len(),
228 }
229 })?)
230 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
231 }
232 MlDsaVariant::MlDsa87 => {
233 let _ = ml_dsa_87::PublicKey::try_from_bytes(bytes.try_into().map_err(|_| {
234 PqcError::InvalidKeySize {
235 expected: variant.public_key_size(),
236 got: bytes.len(),
237 }
238 })?)
239 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
240 }
241 }
242
243 Ok(Self {
244 variant,
245 bytes: bytes.to_vec(),
246 })
247 }
248}
249
250/// ML-DSA secret key
251///
252/// Contains the private signing key for ML-DSA signatures.
253/// This key must be kept secret and is automatically zeroized when dropped.
254///
255/// # Security Considerations
256/// - Never expose secret keys in logs or error messages
257/// - Store securely (encrypted at rest)
258/// - Use secure channels for transmission
259/// - Consider hardware security modules (HSMs) for production
260///
261/// # Examples
262/// ```rust,no_run
263/// use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaSecretKey, MlDsaVariant};
264///
265/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
266/// let dsa = ml_dsa_65();
267/// let (_, secret_key) = dsa.generate_keypair()?;
268///
269/// // Serialize for secure storage
270/// let key_bytes = secret_key.to_bytes();
271/// // Store key_bytes securely (encrypted)
272///
273/// // Later, restore from secure storage
274/// let restored = MlDsaSecretKey::from_bytes(MlDsaVariant::MlDsa65, &key_bytes)?;
275/// # Ok(())
276/// # }
277/// ```
278#[derive(Clone, Zeroize, ZeroizeOnDrop)]
279pub struct MlDsaSecretKey {
280 #[zeroize(skip)]
281 variant: MlDsaVariant,
282 bytes: Vec<u8>,
283}
284
285impl MlDsaSecretKey {
286 /// Get the variant of this key
287 #[must_use]
288 pub const fn variant(&self) -> MlDsaVariant {
289 self.variant
290 }
291
292 /// Export the secret key as bytes (handle with care!)
293 #[must_use]
294 pub fn to_bytes(&self) -> Vec<u8> {
295 self.bytes.clone()
296 }
297
298 /// Import a secret key from bytes
299 ///
300 /// # Errors
301 ///
302 /// Returns an error if the byte slice has an incorrect length for the specified variant.
303 pub fn from_bytes(variant: MlDsaVariant, bytes: &[u8]) -> PqcResult<Self> {
304 if bytes.len() != variant.secret_key_size() {
305 return Err(PqcError::InvalidKeySize {
306 expected: variant.secret_key_size(),
307 got: bytes.len(),
308 });
309 }
310
311 // Validate by trying to deserialize
312 match variant {
313 MlDsaVariant::MlDsa44 => {
314 let _ = ml_dsa_44::PrivateKey::try_from_bytes(bytes.try_into().map_err(|_| {
315 PqcError::InvalidKeySize {
316 expected: variant.secret_key_size(),
317 got: bytes.len(),
318 }
319 })?)
320 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
321 }
322 MlDsaVariant::MlDsa65 => {
323 let _ = ml_dsa_65::PrivateKey::try_from_bytes(bytes.try_into().map_err(|_| {
324 PqcError::InvalidKeySize {
325 expected: variant.secret_key_size(),
326 got: bytes.len(),
327 }
328 })?)
329 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
330 }
331 MlDsaVariant::MlDsa87 => {
332 let _ = ml_dsa_87::PrivateKey::try_from_bytes(bytes.try_into().map_err(|_| {
333 PqcError::InvalidKeySize {
334 expected: variant.secret_key_size(),
335 got: bytes.len(),
336 }
337 })?)
338 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
339 }
340 }
341
342 Ok(Self {
343 variant,
344 bytes: bytes.to_vec(),
345 })
346 }
347}
348
349/// ML-DSA signature
350///
351/// A quantum-resistant digital signature produced by ML-DSA.
352/// Signatures are non-deterministic (include randomness) for enhanced security.
353///
354/// # Size Requirements
355/// - ML-DSA-44: 2420 bytes
356/// - ML-DSA-65: 3309 bytes
357/// - ML-DSA-87: 4627 bytes
358///
359/// # Examples
360/// ```rust,no_run
361/// use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaSignature, MlDsaVariant};
362///
363/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
364/// let dsa = ml_dsa_65();
365/// let (public_key, secret_key) = dsa.generate_keypair()?;
366///
367/// // Create signature
368/// let message = b"Document to sign";
369/// let signature = dsa.sign(&secret_key, message)?;
370///
371/// // Serialize for transmission
372/// let sig_bytes = signature.to_bytes();
373/// assert_eq!(sig_bytes.len(), 3309); // ML-DSA-65 signature size
374///
375/// // Deserialize received signature
376/// let received_sig = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig_bytes)?;
377///
378/// // Verify
379/// assert!(dsa.verify(&public_key, message, &received_sig)?);
380/// # Ok(())
381/// # }
382/// ```
383#[derive(Clone, Zeroize, ZeroizeOnDrop)]
384pub struct MlDsaSignature {
385 #[zeroize(skip)]
386 variant: MlDsaVariant,
387 bytes: Vec<u8>,
388}
389
390impl MlDsaSignature {
391 /// Get the variant of this signature
392 #[must_use]
393 pub const fn variant(&self) -> MlDsaVariant {
394 self.variant
395 }
396
397 /// Export the signature as bytes
398 #[must_use]
399 pub fn to_bytes(&self) -> Vec<u8> {
400 self.bytes.clone()
401 }
402
403 /// Import a signature from bytes
404 ///
405 /// # Errors
406 ///
407 /// Returns an error if the byte slice has an incorrect length for the specified variant.
408 pub fn from_bytes(variant: MlDsaVariant, bytes: &[u8]) -> PqcResult<Self> {
409 if bytes.len() != variant.signature_size() {
410 return Err(PqcError::InvalidSignatureSize {
411 expected: variant.signature_size(),
412 got: bytes.len(),
413 });
414 }
415
416 Ok(Self {
417 variant,
418 bytes: bytes.to_vec(),
419 })
420 }
421}
422
423/// ML-DSA main API
424///
425/// The main interface for ML-DSA digital signature operations.
426/// This struct provides methods for key generation, signing, and verification
427/// according to NIST FIPS 204 standard.
428///
429/// # Examples
430///
431/// ## Basic Usage
432/// ```rust,no_run
433/// use saorsa_pqc::api::sig::{MlDsa, MlDsaVariant};
434///
435/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
436/// // Create instance with chosen security level
437/// let dsa = MlDsa::new(MlDsaVariant::MlDsa65);
438///
439/// // Generate keys
440/// let (public_key, secret_key) = dsa.generate_keypair()?;
441///
442/// // Sign and verify
443/// let message = b"Important message";
444/// let signature = dsa.sign(&secret_key, message)?;
445/// assert!(dsa.verify(&public_key, message, &signature)?);
446/// # Ok(())
447/// # }
448/// ```
449///
450/// ## With Context for Domain Separation
451/// ```rust,no_run
452/// use saorsa_pqc::api::sig::{MlDsa, MlDsaVariant};
453///
454/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
455/// let dsa = MlDsa::new(MlDsaVariant::MlDsa87);
456/// let (public_key, secret_key) = dsa.generate_keypair()?;
457///
458/// // Use context to prevent cross-protocol attacks
459/// let message = b"Transaction #42";
460/// let context = b"blockchain-v2";
461///
462/// let signature = dsa.sign_with_context(&secret_key, message, context)?;
463/// let valid = dsa.verify_with_context(&public_key, message, &signature, context)?;
464/// assert!(valid);
465///
466/// // Different context will fail verification
467/// let wrong_context = b"blockchain-v1";
468/// let invalid = dsa.verify_with_context(&public_key, message, &signature, wrong_context)?;
469/// assert!(!invalid);
470/// # Ok(())
471/// # }
472/// ```
473pub struct MlDsa {
474 variant: MlDsaVariant,
475}
476
477impl MlDsa {
478 /// Create a new ML-DSA instance with the specified variant
479 ///
480 /// # Arguments
481 /// * `variant` - The ML-DSA parameter set to use (44, 65, or 87)
482 ///
483 /// # Example
484 /// ```rust,no_run
485 /// use saorsa_pqc::api::sig::{MlDsa, MlDsaVariant};
486 ///
487 /// let dsa_44 = MlDsa::new(MlDsaVariant::MlDsa44); // NIST Level 2
488 /// let dsa_65 = MlDsa::new(MlDsaVariant::MlDsa65); // NIST Level 3
489 /// let dsa_87 = MlDsa::new(MlDsaVariant::MlDsa87); // NIST Level 5
490 /// ```
491 #[must_use]
492 pub const fn new(variant: MlDsaVariant) -> Self {
493 Self { variant }
494 }
495
496 /// Generate a new key pair
497 ///
498 /// Creates a new ML-DSA key pair using the system's secure random number generator.
499 ///
500 /// # Returns
501 /// A tuple containing:
502 /// - `MlDsaPublicKey`: The public key for signature verification
503 /// - `MlDsaSecretKey`: The secret key for signing
504 ///
505 /// # Errors
506 /// Returns an error if key generation fails (extremely rare with proper RNG).
507 ///
508 /// # Example
509 /// ```rust,no_run
510 /// use saorsa_pqc::api::sig::ml_dsa_65;
511 ///
512 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
513 /// let dsa = ml_dsa_65();
514 /// let (public_key, secret_key) = dsa.generate_keypair()?;
515 ///
516 /// println!("Public key: {} bytes", public_key.to_bytes().len());
517 /// println!("Secret key: {} bytes", secret_key.to_bytes().len());
518 /// # Ok(())
519 /// # }
520 /// ```
521 #[allow(clippy::large_stack_frames)]
522 pub fn generate_keypair(&self) -> PqcResult<(MlDsaPublicKey, MlDsaSecretKey)> {
523 match self.variant {
524 MlDsaVariant::MlDsa44 => {
525 let (pk, sk) = ml_dsa_44::try_keygen_with_rng(&mut OsRng)
526 .map_err(|e| PqcError::KeyGenerationFailed(e.to_string()))?;
527 Ok((
528 MlDsaPublicKey {
529 variant: self.variant,
530 bytes: pk.into_bytes().to_vec(),
531 },
532 MlDsaSecretKey {
533 variant: self.variant,
534 bytes: sk.into_bytes().to_vec(),
535 },
536 ))
537 }
538 MlDsaVariant::MlDsa65 => {
539 let (pk, sk) = ml_dsa_65::try_keygen_with_rng(&mut OsRng)
540 .map_err(|e| PqcError::KeyGenerationFailed(e.to_string()))?;
541 Ok((
542 MlDsaPublicKey {
543 variant: self.variant,
544 bytes: pk.into_bytes().to_vec(),
545 },
546 MlDsaSecretKey {
547 variant: self.variant,
548 bytes: sk.into_bytes().to_vec(),
549 },
550 ))
551 }
552 MlDsaVariant::MlDsa87 => {
553 let (pk, sk) = ml_dsa_87::try_keygen_with_rng(&mut OsRng)
554 .map_err(|e| PqcError::KeyGenerationFailed(e.to_string()))?;
555 Ok((
556 MlDsaPublicKey {
557 variant: self.variant,
558 bytes: pk.into_bytes().to_vec(),
559 },
560 MlDsaSecretKey {
561 variant: self.variant,
562 bytes: sk.into_bytes().to_vec(),
563 },
564 ))
565 }
566 }
567 }
568
569 /// Generate a deterministic key pair from a 32-byte seed
570 ///
571 /// Creates an ML-DSA key pair deterministically from the provided seed value.
572 /// The same seed will always produce the same key pair, as specified by FIPS 204.
573 ///
574 /// # Security
575 ///
576 /// `xi` **must** be a 32-byte value with full entropy (e.g. output of a CSPRNG
577 /// or a key-derivation function such as HKDF/BLAKE3). A predictable or
578 /// low-entropy seed produces a predictable key pair. Treat the seed with the
579 /// same confidentiality as a private key.
580 ///
581 /// Prefer [`generate_keypair`](Self::generate_keypair) for use-cases that do
582 /// not require reproducibility.
583 ///
584 /// # Arguments
585 /// * `xi` - A 32-byte seed value with full entropy
586 ///
587 /// # Returns
588 /// A tuple containing:
589 /// - `MlDsaPublicKey`: The public key for signature verification
590 /// - `MlDsaSecretKey`: The secret key for signing
591 ///
592 /// # Example
593 /// ```rust,no_run
594 /// use saorsa_pqc::api::sig::ml_dsa_65;
595 ///
596 /// // In production, use a CSPRNG or KDF — not a fixed value.
597 /// let dsa = ml_dsa_65();
598 /// let seed = [42u8; 32];
599 /// let (pk1, sk1) = dsa.generate_keypair_from_seed(&seed);
600 /// let (pk2, sk2) = dsa.generate_keypair_from_seed(&seed);
601 /// assert_eq!(pk1.to_bytes(), pk2.to_bytes());
602 /// ```
603 #[must_use]
604 #[allow(clippy::large_stack_frames)]
605 pub fn generate_keypair_from_seed(&self, xi: &[u8; 32]) -> (MlDsaPublicKey, MlDsaSecretKey) {
606 match self.variant {
607 MlDsaVariant::MlDsa44 => {
608 let (pk, sk) = ml_dsa_44::KG::keygen_from_seed(xi);
609 (
610 MlDsaPublicKey {
611 variant: self.variant,
612 bytes: pk.into_bytes().to_vec(),
613 },
614 MlDsaSecretKey {
615 variant: self.variant,
616 bytes: sk.into_bytes().to_vec(),
617 },
618 )
619 }
620 MlDsaVariant::MlDsa65 => {
621 let (pk, sk) = ml_dsa_65::KG::keygen_from_seed(xi);
622 (
623 MlDsaPublicKey {
624 variant: self.variant,
625 bytes: pk.into_bytes().to_vec(),
626 },
627 MlDsaSecretKey {
628 variant: self.variant,
629 bytes: sk.into_bytes().to_vec(),
630 },
631 )
632 }
633 MlDsaVariant::MlDsa87 => {
634 let (pk, sk) = ml_dsa_87::KG::keygen_from_seed(xi);
635 (
636 MlDsaPublicKey {
637 variant: self.variant,
638 bytes: pk.into_bytes().to_vec(),
639 },
640 MlDsaSecretKey {
641 variant: self.variant,
642 bytes: sk.into_bytes().to_vec(),
643 },
644 )
645 }
646 }
647 }
648
649 /// Sign a message
650 ///
651 /// Creates a digital signature for the given message using the secret key.
652 /// The signature includes randomness for enhanced security against side-channel attacks.
653 ///
654 /// # Arguments
655 /// * `secret_key` - The secret signing key
656 /// * `message` - The message to sign (can be any length)
657 ///
658 /// # Returns
659 /// A signature that can be verified with the corresponding public key
660 ///
661 /// # Errors
662 /// - `InvalidInput`: If the secret key variant doesn't match
663 /// - `SigningFailed`: If the signing operation fails
664 ///
665 /// # Example
666 /// ```rust,no_run
667 /// use saorsa_pqc::api::sig::ml_dsa_65;
668 ///
669 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
670 /// let dsa = ml_dsa_65();
671 /// let (public_key, secret_key) = dsa.generate_keypair()?;
672 ///
673 /// // Sign any size message
674 /// let message = b"This message can be any length";
675 /// let signature = dsa.sign(&secret_key, message)?;
676 ///
677 /// // Verify the signature
678 /// assert!(dsa.verify(&public_key, message, &signature)?);
679 /// # Ok(())
680 /// # }
681 /// ```
682 pub fn sign(&self, secret_key: &MlDsaSecretKey, message: &[u8]) -> PqcResult<MlDsaSignature> {
683 self.sign_with_context(secret_key, message, b"")
684 }
685
686 /// Sign a message with context
687 ///
688 /// Creates a signature with an additional context string for domain separation.
689 /// This prevents signatures from being valid across different protocols or applications.
690 ///
691 /// # Arguments
692 /// * `secret_key` - The secret signing key
693 /// * `message` - The message to sign
694 /// * `context` - Domain separation context (max 255 bytes)
695 ///
696 /// # Security Note
697 /// Using context strings is recommended when the same keys are used in multiple
698 /// protocols to prevent cross-protocol signature attacks.
699 ///
700 /// # Errors
701 /// - `InvalidInput`: If the secret key variant doesn't match
702 /// - `ContextTooLong`: If context exceeds 255 bytes
703 /// - `SigningFailed`: If the signing operation fails
704 ///
705 /// # Example
706 /// ```rust,no_run
707 /// use saorsa_pqc::api::sig::ml_dsa_65;
708 ///
709 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
710 /// let dsa = ml_dsa_65();
711 /// let (public_key, secret_key) = dsa.generate_keypair()?;
712 ///
713 /// // Sign with application-specific context
714 /// let invoice = b"Invoice #2024-001";
715 /// let context = b"accounting-system-v3";
716 /// let signature = dsa.sign_with_context(&secret_key, invoice, context)?;
717 ///
718 /// // Must verify with same context
719 /// assert!(dsa.verify_with_context(&public_key, invoice, &signature, context)?);
720 /// # Ok(())
721 /// # }
722 /// ```
723 pub fn sign_with_context(
724 &self,
725 secret_key: &MlDsaSecretKey,
726 message: &[u8],
727 context: &[u8],
728 ) -> PqcResult<MlDsaSignature> {
729 if secret_key.variant != self.variant {
730 return Err(PqcError::InvalidInput(format!(
731 "Key variant {:?} doesn't match DSA variant {:?}",
732 secret_key.variant, self.variant
733 )));
734 }
735
736 if context.len() > MlDsaVariant::MAX_CONTEXT_LENGTH {
737 return Err(PqcError::ContextTooLong {
738 max: MlDsaVariant::MAX_CONTEXT_LENGTH,
739 got: context.len(),
740 });
741 }
742
743 match self.variant {
744 MlDsaVariant::MlDsa44 => {
745 let sk = ml_dsa_44::PrivateKey::try_from_bytes(
746 secret_key.bytes.as_slice().try_into().map_err(|_| {
747 PqcError::InvalidKeySize {
748 expected: self.variant.secret_key_size(),
749 got: secret_key.bytes.len(),
750 }
751 })?,
752 )
753 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
754
755 let sig = sk
756 .try_sign_with_rng(&mut OsRng, message, context)
757 .map_err(|e| PqcError::SigningFailed(e.to_string()))?;
758
759 Ok(MlDsaSignature {
760 variant: self.variant,
761 bytes: sig.to_vec(),
762 })
763 }
764 MlDsaVariant::MlDsa65 => {
765 let sk = ml_dsa_65::PrivateKey::try_from_bytes(
766 secret_key.bytes.as_slice().try_into().map_err(|_| {
767 PqcError::InvalidKeySize {
768 expected: self.variant.secret_key_size(),
769 got: secret_key.bytes.len(),
770 }
771 })?,
772 )
773 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
774
775 let sig = sk
776 .try_sign_with_rng(&mut OsRng, message, context)
777 .map_err(|e| PqcError::SigningFailed(e.to_string()))?;
778
779 Ok(MlDsaSignature {
780 variant: self.variant,
781 bytes: sig.to_vec(),
782 })
783 }
784 MlDsaVariant::MlDsa87 => {
785 let sk = ml_dsa_87::PrivateKey::try_from_bytes(
786 secret_key.bytes.as_slice().try_into().map_err(|_| {
787 PqcError::InvalidKeySize {
788 expected: self.variant.secret_key_size(),
789 got: secret_key.bytes.len(),
790 }
791 })?,
792 )
793 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
794
795 let sig = sk
796 .try_sign_with_rng(&mut OsRng, message, context)
797 .map_err(|e| PqcError::SigningFailed(e.to_string()))?;
798
799 Ok(MlDsaSignature {
800 variant: self.variant,
801 bytes: sig.to_vec(),
802 })
803 }
804 }
805 }
806
807 /// Verify a signature
808 ///
809 /// Verifies that a signature was created by the holder of the secret key
810 /// corresponding to the provided public key.
811 ///
812 /// # Arguments
813 /// * `public_key` - The public verification key
814 /// * `message` - The original message that was signed
815 /// * `signature` - The signature to verify
816 ///
817 /// # Returns
818 /// - `Ok(true)` if the signature is valid
819 /// - `Ok(false)` if the signature is invalid
820 /// - `Err(_)` if verification cannot be performed (wrong key type, etc.)
821 ///
822 /// # Errors
823 ///
824 /// Returns an error if the signature verification process fails due to
825 /// incompatible key types or internal verification errors.
826 ///
827 /// # Example
828 /// ```rust,no_run
829 /// use saorsa_pqc::api::sig::ml_dsa_65;
830 ///
831 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
832 /// let dsa = ml_dsa_65();
833 /// let (public_key, secret_key) = dsa.generate_keypair()?;
834 ///
835 /// let message = b"Authenticate this";
836 /// let signature = dsa.sign(&secret_key, message)?;
837 ///
838 /// // Valid signature
839 /// assert!(dsa.verify(&public_key, message, &signature)?);
840 ///
841 /// // Modified message fails
842 /// let wrong_message = b"Authenticate that";
843 /// assert!(!dsa.verify(&public_key, wrong_message, &signature)?);
844 /// # Ok(())
845 /// # }
846 /// ```
847 pub fn verify(
848 &self,
849 public_key: &MlDsaPublicKey,
850 message: &[u8],
851 signature: &MlDsaSignature,
852 ) -> PqcResult<bool> {
853 self.verify_with_context(public_key, message, signature, b"")
854 }
855
856 /// Verify a signature with context
857 ///
858 /// Verifies a signature that was created with a context string.
859 /// The same context must be provided for successful verification.
860 ///
861 /// # Arguments
862 /// * `public_key` - The public verification key
863 /// * `message` - The original message
864 /// * `signature` - The signature to verify
865 /// * `context` - The context string used during signing
866 ///
867 /// # Returns
868 /// - `Ok(true)` if the signature is valid with the given context
869 /// - `Ok(false)` if the signature is invalid or context doesn't match
870 /// - `Err(_)` if verification cannot be performed
871 ///
872 /// # Errors
873 ///
874 /// Returns an error if the signature verification process fails due to
875 /// incompatible key types, invalid context, or internal verification errors.
876 ///
877 /// # Example
878 /// ```rust,no_run
879 /// use saorsa_pqc::api::sig::ml_dsa_65;
880 ///
881 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
882 /// let dsa = ml_dsa_65();
883 /// let (public_key, secret_key) = dsa.generate_keypair()?;
884 ///
885 /// let message = b"Protocol message";
886 /// let context = b"protocol-v1";
887 /// let signature = dsa.sign_with_context(&secret_key, message, context)?;
888 ///
889 /// // Correct context succeeds
890 /// assert!(dsa.verify_with_context(&public_key, message, &signature, context)?);
891 ///
892 /// // Wrong context fails
893 /// let wrong_context = b"protocol-v2";
894 /// assert!(!dsa.verify_with_context(&public_key, message, &signature, wrong_context)?);
895 /// # Ok(())
896 /// # }
897 /// ```
898 pub fn verify_with_context(
899 &self,
900 public_key: &MlDsaPublicKey,
901 message: &[u8],
902 signature: &MlDsaSignature,
903 context: &[u8],
904 ) -> PqcResult<bool> {
905 if public_key.variant != self.variant {
906 return Err(PqcError::InvalidInput(format!(
907 "Key variant {:?} doesn't match DSA variant {:?}",
908 public_key.variant, self.variant
909 )));
910 }
911
912 if signature.variant != self.variant {
913 return Err(PqcError::InvalidInput(format!(
914 "Signature variant {:?} doesn't match DSA variant {:?}",
915 signature.variant, self.variant
916 )));
917 }
918
919 if context.len() > MlDsaVariant::MAX_CONTEXT_LENGTH {
920 return Err(PqcError::ContextTooLong {
921 max: MlDsaVariant::MAX_CONTEXT_LENGTH,
922 got: context.len(),
923 });
924 }
925
926 match self.variant {
927 MlDsaVariant::MlDsa44 => {
928 let pk = ml_dsa_44::PublicKey::try_from_bytes(
929 public_key.bytes.as_slice().try_into().map_err(|_| {
930 PqcError::InvalidKeySize {
931 expected: self.variant.public_key_size(),
932 got: public_key.bytes.len(),
933 }
934 })?,
935 )
936 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
937
938 let sig_array: [u8; 2420] =
939 signature.bytes.as_slice().try_into().map_err(|_| {
940 PqcError::InvalidSignatureSize {
941 expected: self.variant.signature_size(),
942 got: signature.bytes.len(),
943 }
944 })?;
945
946 Ok(pk.verify(message, &sig_array, context))
947 }
948 MlDsaVariant::MlDsa65 => {
949 let pk = ml_dsa_65::PublicKey::try_from_bytes(
950 public_key.bytes.as_slice().try_into().map_err(|_| {
951 PqcError::InvalidKeySize {
952 expected: self.variant.public_key_size(),
953 got: public_key.bytes.len(),
954 }
955 })?,
956 )
957 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
958
959 let sig_array: [u8; 3309] =
960 signature.bytes.as_slice().try_into().map_err(|_| {
961 PqcError::InvalidSignatureSize {
962 expected: self.variant.signature_size(),
963 got: signature.bytes.len(),
964 }
965 })?;
966
967 Ok(pk.verify(message, &sig_array, context))
968 }
969 MlDsaVariant::MlDsa87 => {
970 let pk = ml_dsa_87::PublicKey::try_from_bytes(
971 public_key.bytes.as_slice().try_into().map_err(|_| {
972 PqcError::InvalidKeySize {
973 expected: self.variant.public_key_size(),
974 got: public_key.bytes.len(),
975 }
976 })?,
977 )
978 .map_err(|e| PqcError::SerializationError(e.to_string()))?;
979
980 let sig_array: [u8; 4627] =
981 signature.bytes.as_slice().try_into().map_err(|_| {
982 PqcError::InvalidSignatureSize {
983 expected: self.variant.signature_size(),
984 got: signature.bytes.len(),
985 }
986 })?;
987
988 Ok(pk.verify(message, &sig_array, context))
989 }
990 }
991 }
992}
993
994/// Convenience function to create ML-DSA-65 (recommended default)
995///
996/// ML-DSA-65 provides NIST Level 3 security (~192-bit classical security),
997/// which is suitable for most applications and offers a good balance
998/// between security and performance.
999///
1000/// # Why ML-DSA-65?
1001/// - Quantum resistance equivalent to 128-bit quantum security
1002/// - Moderate key and signature sizes
1003/// - Good performance on modern hardware
1004/// - Recommended by NIST for general use
1005///
1006/// # Example
1007/// ```rust,no_run
1008/// use saorsa_pqc::api::sig::ml_dsa_65;
1009///
1010/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1011/// // Quick setup with recommended parameters
1012/// let dsa = ml_dsa_65();
1013///
1014/// // Use just like any MlDsa instance
1015/// let (public_key, secret_key) = dsa.generate_keypair()?;
1016/// let message = b"Sign this";
1017/// let signature = dsa.sign(&secret_key, message)?;
1018/// assert!(dsa.verify(&public_key, message, &signature)?);
1019/// # Ok(())
1020/// # }
1021/// ```
1022///
1023/// For other security levels, use:
1024/// - `MlDsa::new(MlDsaVariant::MlDsa44)` for NIST Level 2 (128-bit classical)
1025/// - `MlDsa::new(MlDsaVariant::MlDsa87)` for NIST Level 5 (256-bit classical)
1026#[must_use]
1027pub const fn ml_dsa_65() -> MlDsa {
1028 MlDsa::new(MlDsaVariant::MlDsa65)
1029}
1030
1031/// Convenience function to create ML-DSA-44 (lightweight option)
1032///
1033/// ML-DSA-44 provides NIST Level 2 security (~128-bit classical security),
1034/// suitable for applications with strict size or performance constraints.
1035///
1036/// # Example
1037/// ```rust,no_run
1038/// use saorsa_pqc::api::sig::ml_dsa_44;
1039///
1040/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1041/// let dsa = ml_dsa_44();
1042/// let (public_key, secret_key) = dsa.generate_keypair()?;
1043/// # Ok(())
1044/// # }
1045/// ```
1046#[must_use]
1047pub const fn ml_dsa_44() -> MlDsa {
1048 MlDsa::new(MlDsaVariant::MlDsa44)
1049}
1050
1051/// Convenience function to create ML-DSA-87 (maximum security)
1052///
1053/// ML-DSA-87 provides NIST Level 5 security (~256-bit classical security),
1054/// suitable for applications requiring the highest level of security.
1055///
1056/// # Example
1057/// ```rust,no_run
1058/// use saorsa_pqc::api::sig::ml_dsa_87;
1059///
1060/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1061/// let dsa = ml_dsa_87();
1062/// let (public_key, secret_key) = dsa.generate_keypair()?;
1063/// # Ok(())
1064/// # }
1065/// ```
1066#[must_use]
1067pub const fn ml_dsa_87() -> MlDsa {
1068 MlDsa::new(MlDsaVariant::MlDsa87)
1069}
1070
1071#[cfg(test)]
1072#[allow(clippy::indexing_slicing)]
1073#[allow(clippy::unwrap_used, clippy::expect_used)]
1074mod tests {
1075 use super::*;
1076
1077 #[test]
1078 fn test_ml_dsa_65_sign_verify() {
1079 let dsa = ml_dsa_65();
1080 let (pk, sk) = dsa.generate_keypair().unwrap();
1081
1082 let message = b"Test message";
1083 let sig = dsa.sign(&sk, message).unwrap();
1084
1085 assert!(dsa.verify(&pk, message, &sig).unwrap());
1086
1087 // Wrong message should fail
1088 assert!(!dsa.verify(&pk, b"Wrong message", &sig).unwrap());
1089 }
1090
1091 #[test]
1092 fn test_all_variants() {
1093 for variant in [
1094 MlDsaVariant::MlDsa44,
1095 MlDsaVariant::MlDsa65,
1096 MlDsaVariant::MlDsa87,
1097 ] {
1098 let dsa = MlDsa::new(variant);
1099 let (pk, sk) = dsa.generate_keypair().unwrap();
1100
1101 let message = b"Test message for all variants";
1102 let sig = dsa.sign(&sk, message).unwrap();
1103
1104 assert!(dsa.verify(&pk, message, &sig).unwrap());
1105 }
1106 }
1107
1108 #[test]
1109 fn test_with_context() {
1110 let dsa = ml_dsa_65();
1111 let (pk, sk) = dsa.generate_keypair().unwrap();
1112
1113 let message = b"Test message";
1114 let context = b"test context";
1115 let sig = dsa.sign_with_context(&sk, message, context).unwrap();
1116
1117 // Correct context verifies
1118 assert!(dsa
1119 .verify_with_context(&pk, message, &sig, context)
1120 .unwrap());
1121
1122 // Wrong context fails
1123 assert!(!dsa
1124 .verify_with_context(&pk, message, &sig, b"wrong context")
1125 .unwrap());
1126 }
1127
1128 #[test]
1129 fn test_serialization() {
1130 let dsa = ml_dsa_65();
1131 let (pk, sk) = dsa.generate_keypair().unwrap();
1132
1133 // Serialize and deserialize keys
1134 let pk_bytes = pk.to_bytes();
1135 let sk_bytes = sk.to_bytes();
1136
1137 let pk2 = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, &pk_bytes).unwrap();
1138 let sk2 = MlDsaSecretKey::from_bytes(MlDsaVariant::MlDsa65, &sk_bytes).unwrap();
1139
1140 // Use deserialized keys
1141 let message = b"Test";
1142 let sig = dsa.sign(&sk2, message).unwrap();
1143 assert!(dsa.verify(&pk2, message, &sig).unwrap());
1144 }
1145
1146 #[test]
1147 fn test_seeded_keygen_deterministic() {
1148 let seed = [42u8; 32];
1149 for variant in [
1150 MlDsaVariant::MlDsa44,
1151 MlDsaVariant::MlDsa65,
1152 MlDsaVariant::MlDsa87,
1153 ] {
1154 let dsa = MlDsa::new(variant);
1155 let (pk1, sk1) = dsa.generate_keypair_from_seed(&seed);
1156 let (pk2, sk2) = dsa.generate_keypair_from_seed(&seed);
1157
1158 assert_eq!(
1159 pk1.to_bytes(),
1160 pk2.to_bytes(),
1161 "Public keys must match for {:?}",
1162 variant
1163 );
1164 assert_eq!(
1165 sk1.to_bytes(),
1166 sk2.to_bytes(),
1167 "Secret keys must match for {:?}",
1168 variant
1169 );
1170
1171 // Signing with seeded key should verify
1172 let message = b"Deterministic test";
1173 let sig = dsa.sign(&sk1, message).unwrap();
1174 assert!(dsa.verify(&pk1, message, &sig).unwrap());
1175
1176 // Serialization roundtrip for seeded keys
1177 let pk_restored = MlDsaPublicKey::from_bytes(variant, &pk1.to_bytes()).unwrap();
1178 let sk_restored = MlDsaSecretKey::from_bytes(variant, &sk1.to_bytes()).unwrap();
1179 let sig2 = dsa.sign(&sk_restored, message).unwrap();
1180 assert!(dsa.verify(&pk_restored, message, &sig2).unwrap());
1181 }
1182 }
1183
1184 #[test]
1185 fn test_seeded_keygen_different_seeds() {
1186 let dsa = ml_dsa_65();
1187 let (pk1, _) = dsa.generate_keypair_from_seed(&[1u8; 32]);
1188 let (pk2, _) = dsa.generate_keypair_from_seed(&[2u8; 32]);
1189 assert_ne!(
1190 pk1.to_bytes(),
1191 pk2.to_bytes(),
1192 "Different seeds must produce different keys"
1193 );
1194 }
1195
1196 #[test]
1197 fn test_context_too_long() {
1198 let dsa = ml_dsa_65();
1199 let (_, sk) = dsa.generate_keypair().unwrap();
1200
1201 let message = b"Test";
1202 let long_context = vec![0u8; 256]; // Too long
1203
1204 let result = dsa.sign_with_context(&sk, message, &long_context);
1205 assert!(matches!(result, Err(PqcError::ContextTooLong { .. })));
1206 }
1207}