← Week 2: ML-KEM and ML-DSA

Day 13: ML-DSA Sign and Verify with aws-lc-rs

Phase 3 · July 20, 2026

← Week 2: ML-KEM and ML-DSA

Agenda (2–3 hours)

  • Read (20 min): aws-lc-rs signature module docs; ML_DSA_65 API
  • Build (120 min): ML-DSA key generation, sign, verify in Rust
  • Verify (30 min): Sign a certificate-like structure and verify it
← Week 2: ML-KEM and ML-DSA

ML-DSA with aws-lc-rs

use aws_lc_rs::signature::{
    KeyPair, Signature, UnparsedPublicKey,
    ML_DSA_65, ML_DSA_65_PUBLIC_KEY,
};

// Generate key pair
let key_pair = KeyPair::generate(&ML_DSA_65)?;

// Extract public key bytes
let pub_key_bytes = key_pair.public_key().as_ref().to_vec();
println!("Public key: {} bytes", pub_key_bytes.len()); // expect 1952

// Sign a message
let message = b"Hello, post-quantum world";
let signature = key_pair.sign(message)?;
println!("Signature: {} bytes", signature.as_ref().len()); // expect 3293
← Week 2: ML-KEM and ML-DSA

Verification

// Verify using the public key bytes (simulating a verifier with no private key)
let public_key = UnparsedPublicKey::new(&ML_DSA_65_PUBLIC_KEY, &pub_key_bytes);
public_key.verify(message, signature.as_ref())?;
println!("Signature valid ✓");

// Tampered message should fail
let result = public_key.verify(b"tampered message", signature.as_ref());
assert!(result.is_err(), "tampered message should fail verification");
println!("Tampered message rejected ✓");

// Tampered signature should fail
let mut bad_sig = signature.as_ref().to_vec();
bad_sig[100] ^= 0xFF;
let result = public_key.verify(message, &bad_sig);
assert!(result.is_err(), "tampered signature should fail verification");
println!("Tampered signature rejected ✓");
← Week 2: ML-KEM and ML-DSA

Deterministic Signing Verification

ML-DSA is deterministic: same message + same key → same signature.

let sig1 = key_pair.sign(message)?;
let sig2 = key_pair.sign(message)?;
assert_eq!(sig1.as_ref(), sig2.as_ref());
println!("Deterministic: same message gives same signature ✓");

Compare to ECDSA where each call to sign produces a different signature (due to random k).

This property is useful for reproducibility and avoids the catastrophic failure mode
of ECDSA with weak randomness.

← Week 2: ML-KEM and ML-DSA

Simulating Certificate-Like Signing

use serde_json::json;

// Simulate signing a "certificate" structure (simplified)
let cert_data = json!({
    "subject": "device-001",
    "public_key": hex::encode(&[1u8; 1184]),  // placeholder ML-KEM public key
    "not_before": "2026-07-20T00:00:00Z",
    "not_after":  "2027-07-20T00:00:00Z",
    "serial": "deadbeef01020304",
}).to_string();

// CA signs the cert data
let ca_key = KeyPair::generate(&ML_DSA_65)?;
let signature = ca_key.sign(cert_data.as_bytes())?;

// Verifier checks
let ca_pub = UnparsedPublicKey::new(&ML_DSA_65_PUBLIC_KEY, ca_key.public_key().as_ref());
ca_pub.verify(cert_data.as_bytes(), signature.as_ref())?;
println!("Simulated cert signature valid ✓");
← Week 2: ML-KEM and ML-DSA

Benchmark: ML-DSA-65 vs ECDSA P-256

fn bench_sign(iterations: u32) {
    // ML-DSA-65
    let kp = KeyPair::generate(&ML_DSA_65).unwrap();
    let start = Instant::now();
    for _ in 0..iterations {
        kp.sign(b"benchmark message").unwrap();
    }
    println!("ML-DSA-65 sign: {:.1} µs/op",
             start.elapsed().as_micros() as f64 / iterations as f64);

    // ECDSA P-256 (aws-lc-rs)
    let ec_kp = EcdsaKeyPair::generate(&ECDSA_P256_SHA256_ASN1_SIGNING).unwrap();
    let start = Instant::now();
    for _ in 0..iterations { ... }
    println!("ECDSA P-256 sign: {:.1} µs/op", ...);
}
← Week 2: ML-KEM and ML-DSA

Challenge Assignment

Implement pqc-demo/src/dsa.rs:

pub fn ml_dsa_65_demo() -> anyhow::Result<()>;
    // Generate key pair, sign "hello world", verify, tamper + verify fails

pub fn benchmark_sign_verify(iterations: u32);
    // Benchmark ML-DSA-65 and ECDSA P-256, print comparison table

pub fn simulate_ca_issuance(num_certs: u32) -> anyhow::Result<Duration>;
    // Simulate signing num_certs "certificate" JSON blobs, return total time
    // Print: "can issue N certs/second"

Then answer: given the signing throughput, what is the maximum certificate issuance
rate your CA could sustain with ML-DSA-65? Is this sufficient for your team's scale?

← Week 2: ML-KEM and ML-DSA

Resources

  • aws-lc-rs signature module: docs.rs/aws-lc-rs — ML_DSA_65, KeyPair, UnparsedPublicKey
  • aws-lc-rs ecdsa module: for ECDSA comparison benchmark
  • aws-lc-rs examples: github.com/aws/aws-lc-rs/tree/main/aws-lc-rs/examples