← Blog

crypto-pq 0.1.0: ML-KEM in no_std Rust

The first release of the lab's post-quantum library implements FIPS 203 for all three parameter sets, with no allocator, no unsafe code, and every gate we could find to run before shipping.

Update, 2 October 2026. This release was an experimental prototype and is withdrawn. It was never reviewed by a cryptographer, and it relied on third-party crates for SHA-3, constant-time comparison and memory wiping, which the lab no longer allows. The crates on crates.io now carry a withdrawal notice. crypto-pq is being redesigned from scratch, with no external dependencies and only standardized algorithms. The rest of this post describes the prototype as it was released.

crypto-pq is the library the lab was formed around: post-quantum cryptography with one contract across languages, starting with a Rust reference implementation of the NIST standards. Version 0.1.0 is on crates.io and GitHub. It ships one algorithm, ML-KEM, and this post is about what that took and what “ships” means here.

What is in the box

ML-KEM is the key-encapsulation mechanism standardized in FIPS 203 in August 2024, the first of the post-quantum standards to reach final status. All three parameter sets are implemented:

Parameter setEncapsulation keyDecapsulation keyCiphertextShared secret
ML-KEM-512800 bytes1 632 bytes768 bytes32 bytes
ML-KEM-7681 184 bytes2 400 bytes1 088 bytes32 bytes
ML-KEM-10241 568 bytes3 168 bytes1 568 bytes32 bytes

The paths follow one rule, crypto_pq::<category>::<algorithm>_<parameter set>, so the code reads the same way the standard does:

use crypto_pq::kem::ml_kem_768::{generate_keypair, Ciphertext, EncapsulationKey};

// Key generation includes the pairwise-consistency check of FIPS 203 §7.1.
let (dk, ek) = generate_keypair(&mut rng)?;

// The other side receives the encapsulation key as bytes and validates it.
let ek = EncapsulationKey::from_bytes(ek.as_bytes())?;
let (shared_secret_sender, ct) = ek.encapsulate(&mut rng);

let shared_secret_receiver = dk.decapsulate(&ct);
assert_eq!(shared_secret_sender.as_bytes(), shared_secret_receiver.as_bytes());

Keys can also be derived deterministically from the 64-byte seed the standard defines (d‖z), which is what you want for storage and for test vectors.

The rules

Everything in the workspace follows the same five rules, and they are the reason the first release is one algorithm rather than five.

  1. Only post-quantum public-key algorithms live here. The symmetric primitives they need internally (SHA-3, SHAKE) come from audited external crates.
  2. An algorithm is labeled production only when its standard is final and its crate passes every gate in CI. Until a third-party audit exists, the label stays not yet audited, and that is the label on this release.
  3. Draft standards stay behind unstable-* features that pin the draft revision.
  4. Standard parameter sets only. No custom variants, no reduced parameters.
  5. no_std, no allocator, #![forbid(unsafe_code)], secret-independent control flow and memory access, zeroization of secret material.

What “passes every gate” means for ML-KEM

The tests are the part of the release we care most about, so here is the list.

  • NIST ACVP vectors for key generation, encapsulation, and decapsulation, all three parameter sets, pinned to a commit of the official repository and checked by SHA-256 in CI so they cannot drift silently.
  • Negative vectors from C2SP’s CCTV project: encapsulation keys with coefficients outside the modulus, and ciphertexts that differ from a valid one only in ways that must be caught by the implicit-rejection path. Both are the cases implementations historically get wrong.
  • Ten thousand random ML-KEM-768 round trips cross-checked against Go’s crypto/mlkem, compared as accumulated hashes, so two independent implementations agree byte for byte on keys, ciphertexts, and secrets.
  • Exhaustive tests of every modular reduction: the Barrett and division-by-q helpers are checked for every input in their domain (2²⁶ values for the Barrett step), not sampled. A reduction that is wrong for one input in sixty-seven million is the kind of bug that survives random testing for years.
  • A no_std build for thumbv7em-none-eabihf and a build on the minimum supported Rust version (1.85), both in CI.

One thing the vectors taught us: a few of the widely circulated ML-KEM test files still encode the draft standard (they omit the domain-separation byte that FIPS 203 added to key generation), and an implementation that “passes” them is passing the wrong thing. We will write that up separately.

What is next

ML-DSA (FIPS 204) is next in the Rust workspace, followed by the first implementation in a second language against the same contract. The categories the paths reserve, sig, sig_stateful, aead, hash, xof, mac, kdf, rng, are the map; only kem is territory so far.

If you use the crate, the fastest way to help is to read it. The design decisions are recorded with the code so they can be checked rather than taken on trust, and the security policy says how to report what you find.