ml_dsa_sign_verify/ml_dsa_sign_verify.rs
1// ML-DSA Sign / Verify Example
2//
3// Demonstrates the ML-DSA digital signature scheme (FIPS 204) using the
4// ML-DSA-65 parameter set (NIST Security Category 3, ~192-bit classical).
5//
6// ML-DSA is a lattice-based signature scheme (formerly CRYSTALS-Dilithium).
7// It uses hedged signing: each signature mixes in fresh randomness, so signing
8// the same message twice yields different (but equally valid) signatures.
9//
10// The flow is:
11// 1. Key generation -- produces a public key (pk) and a secret key (sk).
12// 2. Signing -- produces a signature over a message, with an
13// optional context string for domain separation.
14// 3. Verification -- checks the signature against the public key.
15// 4. Tamper test -- flips a bit in the signature and confirms that
16// verification correctly rejects it.
17//
18// Run:
19// cargo run --example sign_verify -p ml_dsa
20
21use quantica::ml_dsa::*;
22use std::time::Instant;
23
24/// Pretty-print a byte slice as a hex string.
25fn hex(bytes: &[u8]) -> String {
26 bytes.iter().map(|b| format!("{:02x}", b)).collect()
27}
28
29fn main() {
30 println!("=== ML-DSA (FIPS 204) Sign / Verify Example ===\n");
31
32 let mut rng = OsRng;
33
34 // ---------------------------------------------------------------
35 // Step 1: Key generation (ML-DSA-65)
36 // ---------------------------------------------------------------
37 // ML-DSA-65 targets NIST Security Category 3, suitable for most
38 // applications requiring post-quantum signature security.
39 let t0 = Instant::now();
40 let (pk, sk) = MlDsa65Scheme::keygen(&mut rng).expect("keygen failed");
41 let keygen_us = t0.elapsed().as_micros();
42
43 println!("--- Key Generation (ML-DSA-65) ---");
44 println!(" public key: {} bytes", pk.len());
45 println!(" secret key: {} bytes", sk.len());
46 println!(" time: {} us", keygen_us);
47 println!();
48
49 // ---------------------------------------------------------------
50 // Step 2: Sign a message with a context string
51 // ---------------------------------------------------------------
52 // The context string provides domain separation. Different contexts
53 // produce different signatures even for the same message and key.
54 // It must be at most 255 bytes. An empty context (b"") is valid.
55 let message = b"Post-quantum cryptography is here.";
56 let context = b"example-context-v1";
57
58 let t0 = Instant::now();
59 let sig = MlDsa65Scheme::sign(&sk, message, context, &mut rng).expect("sign failed");
60 let sign_us = t0.elapsed().as_micros();
61
62 println!("--- Signing ---");
63 println!(" message: {:?}", std::str::from_utf8(message).unwrap());
64 println!(" context: {:?}", std::str::from_utf8(context).unwrap());
65 println!(" signature: {} bytes", sig.len());
66 println!(" sig[0..32]: {}", hex(&sig[..32]));
67 println!(" time: {} us", sign_us);
68 println!();
69
70 // ---------------------------------------------------------------
71 // Step 3: Verify the signature
72 // ---------------------------------------------------------------
73 // The verifier needs the public key, the original message, the same
74 // context string, and the signature.
75 let t0 = Instant::now();
76 let valid = MlDsa65Scheme::verify(&pk, message, context, &sig).expect("verify call failed");
77 let verify_us = t0.elapsed().as_micros();
78
79 println!("--- Verification ---");
80 println!(" valid: {}", valid);
81 println!(" time: {} us", verify_us);
82 assert!(valid, "Signature should be valid!");
83 println!();
84
85 // ---------------------------------------------------------------
86 // Step 4: Tamper with the signature and verify it is rejected
87 // ---------------------------------------------------------------
88 // Flipping a single bit in the signature must cause verification to fail.
89 let mut tampered_bytes = sig.as_bytes().to_vec();
90 tampered_bytes[0] ^= 0x01;
91 let tampered_sig = quantica::ml_dsa::Signature::<MlDsa65>::from_bytes(&tampered_bytes).expect("from_bytes");
92
93 let tampered_valid = MlDsa65Scheme::verify(&pk, message, context, &tampered_sig).expect("verify call failed");
94
95 println!("--- Tamper Test ---");
96 println!(" flipped bit in sig[0]");
97 println!(" valid: {} (expected: false)", tampered_valid);
98 assert!(!tampered_valid, "Tampered signature should be rejected!");
99 println!();
100
101 // ---------------------------------------------------------------
102 // Summary of sizes across all parameter sets
103 // ---------------------------------------------------------------
104 println!("--- ML-DSA Parameter Set Sizes ---");
105 println!(
106 " ML-DSA-44: pk={:>4} sk={:>4} sig={:>4}",
107 MlDsa44Scheme::PK_LEN,
108 MlDsa44Scheme::SK_LEN,
109 MlDsa44Scheme::SIG_LEN,
110 );
111 println!(
112 " ML-DSA-65: pk={:>4} sk={:>4} sig={:>4}",
113 MlDsa65Scheme::PK_LEN,
114 MlDsa65Scheme::SK_LEN,
115 MlDsa65Scheme::SIG_LEN,
116 );
117 println!(
118 " ML-DSA-87: pk={:>4} sk={:>4} sig={:>4}",
119 MlDsa87Scheme::PK_LEN,
120 MlDsa87Scheme::SK_LEN,
121 MlDsa87Scheme::SIG_LEN,
122 );
123 println!();
124
125 println!("All checks passed.");
126}