

Overview
After building tools that check whether servers use post-quantum cryptography, I wanted to understand the algorithms themselves at the level of every polynomial and every byte. lattice-crypto-from-scratch implements ML-KEM (FIPS 203, key encapsulation), ML-DSA (FIPS 204, lattice signatures) and SLH-DSA (FIPS 205, hash-based signatures) straight from the standards, in Rust, without a single crypto crate.
Even the hashes are written here: Keccak-f[1600] with SHA3-224/256/384/512 and SHAKE128/256, and all six SHA-2 variants for the pre-hash modes. The library has zero dependencies; serde_json is used only by the tests to read NIST's vector files.
Technologies
- - Rust (no dependencies)
- - FIPS 203 — ML-KEM-512 / 768 / 1024
- - FIPS 204 — ML-DSA-44 / 65 / 87 + HashML-DSA
- - FIPS 205 — SLH-DSA (SHA-2 and SHAKE)
- - Keccak / SHA-3 / SHAKE, SHA-2 family
- - NIST ACVP test vectors
- - LMS / HSS (RFC 8554, SP 800-208), in progress

The full test run: every NIST ACVP vector file for ML-KEM, ML-DSA and SLH-DSA passes

Year
2026
Type
Rust Library
Role
Solo Developer
Vectors
2,103 / 2,103 pass
Highlights
Passes every official NIST ACVP vector: ML-KEM key generation and encapsulation/decapsulation including implicit rejection (240), ML-DSA key generation, signing and verification including pre-hash with 12 hash functions and external mu (615), and SLH-DSA key generation, signing and verification (1,248).
Every function names the FIPS algorithm number it implements, so the code can be read side by side with the standard.
A mutation check confirmed the vectors are doing their job: deliberately planted bugs were each caught by a failing vector.

ML-KEM / ML-DSA
q = 3329 / 8380417
Unit tests
42
Technical Implementation
ML-KEM: the number-theoretic transform over q = 3329, SampleNTT from SHAKE128, centred binomial sampling, ByteEncode and Compress, K-PKE, and the Fujisaki–Okamoto transform with implicit rejection. Decapsulation's final choice between the real key and the rejection key is branch-free, and both FIPS input checks on keys are enforced.
ML-DSA: the NTT over q = 8380417, rejection samplers, SampleInBall, Power2Round, Decompose and the hint functions, bit packing for keys and signatures, and the signing rejection loop, in both deterministic and hedged modes, plus HashML-DSA with twelve pre-hash functions.
SLH-DSA: WOTS+, XMSS and FORS trees inside the hypertree, for both the SHA-2 and SHAKE parameter sets. The slowest part of the test suite by far is signing large hash-based signatures, which made the size and speed trade-off of stateless hash-based signatures very concrete.
Testing: beyond the vectors, unit tests check the NTTs against schoolbook polynomial multiplication, encode/decode round trips, the rounding and hint identities, and every hash against Python's hashlib. Randomness comes from the OS (BCryptGenRandom on Windows, /dev/urandom elsewhere).

Purpose
Learning, not production
Limits & What's Next
Correct against the vectors is not the same as safe to deploy, and the README says so. Nobody has audited the compiled code for secret-dependent timing beyond the constant-time select in decapsulation, secrets are not zeroized after use, and the arithmetic is written to be read (no Montgomery or Barrett reduction, no SIMD).
Next: stateful hash-based signatures (LMS / HSS, the scheme most relevant to secure boot, which ties into my post-quantum secure boot research), a dudect-style timing harness to measure leaks, Montgomery arithmetic, and benchmarks against a production library.
