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