post-quantum kem · pure rust · fips 203 · mit/apache

ml-kem-512, ml-kem-768, ml-kem-1024 from the spec.

an implementation of the nist post-quantum kem (formerly kyber), in pure rust. all three parameter sets. no unsafe, no c bindings, only sha3 underneath. passes all 180 official nist acvp test vectors and is byte-for-byte cross-checked against the audited rustcrypto reference on 3000 random seeds. roughly 700 lines of code, written to be read top to bottom.

$ cargo add mlkem-rs
    Adding mlkem-rs v0.8.10 to dependencies
a handshake

alice and bob, future-proofed.

use mlkem::MlKem768;
use rand::thread_rng;

let mut rng = thread_rng();

// bob: long-term keypair, hand the public key to alice.
let (bob_pk, bob_sk) = MlKem768::keygen(&mut rng);

// alice: encapsulate against bob's public key.
let (ct, alice_ss) = MlKem768::encapsulate(&bob_pk, &mut rng);

// bob: decapsulate to recover the same shared secret.
let bob_ss = MlKem768::decapsulate(&bob_sk, &ct);

assert_eq!(alice_ss.as_bytes(), bob_ss.as_bytes());
// 32 shared bytes, safe against a quantum adversary.
parameter sets

three security levels, one api.

variant         k   pk        sk        ct        nist category
ml-kem-512      2   800 B     1632 B    768 B     1 (aes-128 equivalent)
ml-kem-768      3   1184 B    2400 B    1088 B    3 (aes-192 equivalent)
ml-kem-1024     4   1568 B    3168 B    1568 B    5 (aes-256 equivalent)
how it is tested

the floor is high.

180 nist acvp vectors

every published ml-kem test case from nist's algorithm validation program runs in tests/nist_kats.rs. 75 keygen, 75 encapsulation, 30 decapsulation, spread evenly across the three parameter sets. every byte of every output matches the reference.

3000-seed cross-check

against the audited rustcrypto ml-kem crate, byte-for-byte on pk, sk, ciphertext and the recovered shared secret. fixed chacha rng so any future regression is reproducible without saving a corpus.

24000 stress iterations

tests/stress.rs per-cargo-test: 5000 honest round-trips, 2000 random-tamper implicit-reject checks, 1000 garbage-input decap calls. per parameter set. about one second total.

cargo-fuzz harness

four targets in fuzz/: decap-no-panic, encap-no-panic, tampered-ct-implicit-reject, round-trip. nightly only. the stable-rust stress test mirrors the same property surface for ci.

no_std, no unsafe

builds on cortex-m4 (thumbv7em-none-eabihf) and wasm32 in ci. zero unsafe blocks. only crypto dep is sha3. since 0.5.0 the algebraic core is stack-only.

serde, on every type

optional serde feature implements Serialize + Deserialize on all twelve newtypes (pk, sk, ct, ss across three levels) via a custom byte-array impl that fits bincode, postcard, ciborium without any plumbing.

release timeline

sixteen versions to date.

0.2.0   parametrized: all three security levels via Params trait + macro
0.3.0   180 official NIST ACVP test vectors, all green
0.4.0   no_std + alloc-only, builds for cortex-m4 and wasm32
0.5.0   stack-allocated PolyVec / MatrixNtt, zero-alloc hot path
0.6.0   cargo-fuzz harness + 24000-iteration stable stress test
0.7.0   Kem trait, write code generic over the parameter set
0.8.0   optional serde feature on every newtype
0.8.1   docs.rs builds with all-features
0.8.2   examples/handshake.rs
0.8.3   TryFrom<&[u8]> + LengthError public type
0.8.4   pin MSRV to 1.70
0.8.5   examples/serde_save_restore.rs
0.8.6   crate-level rustdoc + missing_debug_implementations lint
0.8.7   cargo-deny config + Debug on the marker types
0.8.8   clippy::pedantic clean, with explicit allow categories
0.8.9   README final pass + badge row
0.8.10  drop removed doc_auto_cfg attr (rust 1.92)
why bother

there is already an audited crate. why write another?

i wasn't trying to replace anything. there is an audited rustcrypto crate, and that's what you should put in your tls handshake or your messaging app. but if i'd just imported it, i would not understand a single byte of what was happening. ml-kem hides a polynomial ring over z_3329, an incomplete number-theoretic transform, rejection sampling against a public seed, a centered-binomial distribution, and a fujisaki-okamoto transform with implicit reject. that's six concepts deep. the only way to know any of it is to write it yourself and watch the test vectors land green.

so the bargain is honest: this crate is for reading, learning, and writing tooling around. for production cryptography, use the audited one. and if you find a bug here, the cross-check against that audited crate would have caught it on every push since 0.2.0, so the bug is genuinely interesting.

install

one line.

$ cargo add mlkem-rs

that gives you the std default feature on. for embedded / wasm: cargo add mlkem-rs --no-default-features. for serde: cargo add mlkem-rs --features serde.

not audited. use rustcrypto's ml-kem for production. use this one to read 700 lines of post-quantum code top to bottom.