← Week 2: ML-KEM and ML-DSA

Day 10: ML-KEM in Rust — Key Generation with aws-lc-rs

Phase 3 · July 17, 2026

← Week 2: ML-KEM and ML-DSA

Agenda (2–3 hours)

  • Read (30 min): aws-lc-rs kem module docs; aws-lc-rs README
  • Setup (30 min): Create pqc-demo project, configure aws-lc-rs
  • Build (90 min): ML-KEM key generation, serialization, round-trip test
← Week 2: ML-KEM and ML-DSA

aws-lc-rs vs ml-kem crate

Two good options for ML-KEM in Rust:

aws-lc-rs ml-kem
Backed by AWS-LC (BoringSSL fork) Pure Rust
PQC support ML-KEM, ML-DSA, SLH-DSA ML-KEM only
rustls integration Yes (rustls-aws-lc-rs) No
Production use Yes (used in s2n-tls) Learning/research
Build complexity Requires C build (cmake) Zero

Strategy: use ml-kem for conceptual exploration today/tomorrow, then switch to
aws-lc-rs for ML-DSA and the rustls integration later this week.

← Week 2: ML-KEM and ML-DSA

Project Setup

# pqc-demo/Cargo.toml
[dependencies]
ml-kem = "0.3"           # pure Rust ML-KEM (days 10-11)
aws-lc-rs = "1"          # production PQC (days 12-13 onward)
rustls = "0.23"
rustls-aws-lc-rs = "0.2"
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.26"
hex = "0.4"
rand_core = "0.6"
# aws-lc-rs needs cmake at build time
# On Ubuntu: sudo apt install cmake
# On macOS: brew install cmake
← Week 2: ML-KEM and ML-DSA

ML-KEM Key Generation (ml-kem crate)

use ml_kem::{MlKem768, KemCore};
use rand_core::OsRng;

fn main() {
    // Generate a key pair
    let (dk, ek) = MlKem768::generate(&mut OsRng);
    // dk = decapsulation key (private)
    // ek = encapsulation key (public)

    println!("Encapsulation key (public): {} bytes",
             ek.as_bytes().len());    // expect 1184
    println!("Decapsulation key (private): {} bytes",
             dk.as_bytes().len());   // expect 2400

    // Serialize
    let ek_bytes: [u8; 1184] = ek.as_bytes().into();
    let dk_bytes: [u8; 2400] = dk.as_bytes().into();

    println!("Public key (hex): {}", hex::encode(&ek_bytes[..32]));
}
← Week 2: ML-KEM and ML-DSA

Key Serialization and Deserialization

use ml_kem::{MlKem768, EncodedSizeUser, KemCore};
use ml_kem::kem::{EncapsKey, DecapsKey};

// Deserialize public key from bytes
let ek_restored = EncapsKey::<MlKem768>::from_bytes(&ek_bytes.into());

// Deserialize private key
let dk_restored = DecapsKey::<MlKem768>::from_bytes(&dk_bytes.into());

// Verify round-trip: encapsulate with original, decapsulate with restored
let (ct, ss1) = ek.encapsulate(&mut OsRng).unwrap();
let ss2 = dk_restored.decapsulate(&ct).unwrap();
assert_eq!(ss1.as_bytes(), ss2.as_bytes());
println!("Round-trip: OK");
← Week 2: ML-KEM and ML-DSA

Comparing Key Sizes Empirically

use ml_kem::{MlKem512, MlKem768, MlKem1024, KemCore};

fn print_sizes<K: KemCore>() where K::EncapsKey: EncodedSizeUser {
    let (dk, ek) = K::generate(&mut OsRng);
    let (ct, _) = ek.encapsulate(&mut OsRng).unwrap();
    println!("pk: {} bytes, sk: {} bytes, ct: {} bytes",
             ek.as_bytes().len(),
             dk.as_bytes().len(),
             ct.as_bytes().len());
}

print_sizes::<MlKem512>();   // 800, 1632, 768
print_sizes::<MlKem768>();   // 1184, 2400, 1088
print_sizes::<MlKem1024>();  // 1568, 3168, 1568
← Week 2: ML-KEM and ML-DSA

Challenge Assignment

Create pqc-demo/src/kem.rs with:

pub struct KemKeyPair {
    pub public_key_bytes: Vec<u8>,
    pub private_key_bytes: Vec<u8>,
    pub algorithm: &'static str,
}

pub fn generate_ml_kem_768() -> KemKeyPair;
pub fn print_size_comparison();  // prints ML-KEM-512/768/1024 vs X25519 sizes

Then write a test that:

  1. Generates a key pair
  2. Serializes public key to bytes
  3. Deserializes and re-encapsulates
  4. Verifies shared secrets match
  5. Verifies the key sizes match FIPS 203 Table 2
← Week 2: ML-KEM and ML-DSA

Resources

  • ml-kem crate: docs.rs/ml-kem
  • aws-lc-rs kem module: docs.rs/aws-lc-rs/latest/aws_lc_rs/kem
  • NIST FIPS 203 Table 2: parameter set sizes (ground truth)