Harvest now, decrypt later
Long-lived secrets — health, finance, state records — are already worth recording. Key exchange is the part that must change first.
Calix PQC is a from-the-standard implementation of the two post-quantum algorithms NIST standardised in 2024 — ML-KEM-768 for key exchange and ML-DSA-65 for signatures — checked byte for byte against NIST's own test vectors.
RSA and elliptic-curve cryptography fall to Shor's algorithm on a large enough quantum computer. Traffic recorded now can be stored and decrypted later. Moving to NIST's post-quantum standards is the only fix, and it has to start before the machine exists.
Long-lived secrets — health, finance, state records — are already worth recording. Key exchange is the part that must change first.
NIST published FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) in August 2024. These are the algorithms governments and vendors are now migrating to.
A subtly wrong implementation is worse than none. Calix PQC is judged against NIST's vectors, independent implementations and seeded faults — see the validation report.
Both are implemented in Rust without borrowing a post-quantum crate, so every line that touches a secret can be audited — and both ship behind the same small C interface.
Lets two parties agree on a 32-byte secret over a public channel. Formerly CRYSTALS-Kyber.
| Security category | NIST 3 (≈ AES-192) |
|---|---|
| Encapsulation key | 1,184 bytes |
| Decapsulation key | 2,400 bytes |
| Ciphertext | 1,088 bytes |
| Shared secret | 32 bytes |
| Key generation · encrypt · decrypt | ≈ 0.5 · 0.8 · 0.6 ms (ARM64) |
| Hardening | implicit rejection, FIPS 203 §7.2/§7.3 key checks |
Non-repudiable signatures that stay unforgeable against quantum attackers. Formerly CRYSTALS-Dilithium.
| Security category | NIST 3 |
|---|---|
| Public key | 1,952 bytes |
| Secret key | 4,032 bytes · or a 32-byte seed |
| Signature | 3,309 bytes |
| Keygen · sign 1 KB · verify 1 KB | 92 · 193 · 68 µs (Apple M3 Pro) |
| Mode | pure ML-DSA, hedged signing, context ≤ 255 B |
Encrypt on iOS, decrypt on Android, in Go or in a browser: the byte layout is fixed. ML-KEM derives the key; AES-256-GCM protects the data and authenticates the header.
Overhead per message: 1,121 bytes. Blobs written by earlier versions (0x04 Kyber round 3, 0x05 ML-KEM + ChaCha20-Poly1305) can still be read; new data is always 0x06.
Passing test vectors proves the maths. The rest of the work is what an attacker, or an auditor, would actually probe.
Hardware division takes value-dependent time — the KyberSlash class of leaks. The compiled WebAssembly release and an unoptimised debug build are disassembled and searched for division instructions on every build (tests/ct/no-div.sh).
Eight deliberate ML-DSA bugs (MakeHint boundary, SampleInBall sign, ExpandA order, ExpandMask counter, non-canonical hints, Decompose constants and more) were injected one at a time. Every one turned the suite red.
ML-DSA keys, signatures, verdicts and the reaction to a single flipped bit are compared byte for byte against the separate RustCrypto ml-dsa crate (300 cases per run, 10,000 on demand); ML-KEM is checked against the independent ml-kem crate.
Encapsulation keys with out-of-range coefficients are rejected (780 official malformed keys), decapsulation keys failing the H(ek) check are refused, tampered ciphertexts fall through implicit rejection, crafted length fields cannot over-read, and the blob header is authenticated.
Nonces and hedged-signature randomness come from an HMAC_DRBG (SP 800-90A, SHA-512) seeded from the OS, validated on 480 CAVP cases, that reseeds after a process fork and refuses requests past the reseed interval.
The lattice code, Keccak and NTT are in-house; the only dependencies are AES-GCM, HMAC/SHA-2, getrandom, subtle and zeroize. Ephemeral keys are wiped from memory. About 3,600 lines of Rust including tests.
One core, one C header, ready-made bindings for the stacks product teams actually use.
// Go — encrypt to a post-quantum public key, then sign kp, _ := ppqc.GenerateKeyPair() // ML-KEM-768 blob, _ := ppqc.Encrypt(kp.PublicKey, []byte("hello")) plain, _ := ppqc.Decrypt(kp.SecretKey, blob) sk, _ := ppqc.MLDSAKeyPairFromSeed(seed) // ML-DSA-65, 32-byte seed sig, _ := ppqc.MLDSASign(sk.SecretKey, msg, []byte("ctx")) ok := ppqc.MLDSAVerify(sk.PublicKey, msg, []byte("ctx"), sig)