Skip to main content

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}