Skip to main content

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}