# krypteia-quantica — Post-Quantum Cryptography for the krypteia workspace [](https://crates.io/crates/krypteia-quantica) [](https://docs.rs/krypteia-quantica) [](https://krypteia-rs.dev) [](https://www.apache.org/licenses/LICENSE-2.0) Pure-Rust implementations of the three NIST post-quantum standards, sharing a side-channel countermeasure toolkit (`silentops`, a companion crate of the same workspace) used by the classical side as well. Specifications (FIPS 203 / 204 / 205 PDFs) are vendored alongside the crate in the [krypteia](https://github.com/cslashm/krypteia) repository. ## Design rules The crate inherits the `krypteia` workspace design rules: 1. **Pure Rust, zero external crates** — only `core` (and `alloc`); `std` is optional behind a feature flag. 2. **Embedded-friendly** — small RAM footprint, fits secure elements, STM32 (Cortex-M0/M4/M33), RISC-V parts (ESP32-C3, …). 3. **Side-channel hardened** against SPA, DPA, DFA, template attacks, timing attacks. CT primitives come from `silentops`, with architecture-specific assembly backends. 4. **Validated** against the official NIST ACVP test vectors. 5. **C FFI-exposable** through the `quantica_ffi` companion crate. ## Algorithms | Standard | Algorithm | Type | Status | |----------|-----------|--------------------------------------------|------------------------------| | FIPS 203 | **ML-KEM** (ex-CRYSTALS-Kyber) | Key Encapsulation Mechanism | Implemented, ACVP + Wycheproof validated | | FIPS 204 | **ML-DSA** (ex-CRYSTALS-Dilithium) | Digital Signature | Implemented, ACVP + Wycheproof validated | | FIPS 205 | **SLH-DSA** (ex-SPHINCS+) | Stateless Hash-Based Signature | Implemented, ACVP validated (Wycheproof has no SLH-DSA corpus yet) | ### ML-KEM (FIPS 203) Module-lattice-based Key Encapsulation Mechanism. Derived from CRYSTALS-Kyber. A `keygen / encaps / decaps` KEM producing a 32-byte shared secret; decapsulation uses the Fujisaki–Okamoto transform with implicit rejection so a malformed ciphertext yields a deterministic secret indistinguishable from a legitimate one. ### ML-DSA (FIPS 204) Module-lattice-based Digital Signature Algorithm. Derived from CRYSTALS-Dilithium. A Fiat–Shamir-with-aborts signature scheme; signing is a hedged rejection loop that mixes fresh randomness with the secret key. Verification is deterministic and does not touch any secret. ### SLH-DSA (FIPS 205) Stateless Hash-Based Digital Signature Algorithm. Derived from SPHINCS+. Security relies on the second-preimage resistance of SHAKE / SHA-2 only — no algebraic assumption. Signatures are large (7–50 KiB depending on parameter set) but the underlying primitive is conservative and quantum-safe. ## Cargo features ```toml [dependencies] quantica = { path = "../quantica" } # default = std + 3 algos + sca-protected ``` | Feature | Default | Effect | |-----------------|:-------:|------------------------------------------------------------------------------| | `std` | ✅ | Pulls in the Rust standard library. Enables `OsRng` and `std::error::Error` impls. | | `ml-kem` | ✅ | Compiles the FIPS 203 module (`quantica::ml_kem`). | | `ml-dsa` | ✅ | Compiles the FIPS 204 module (`quantica::ml_dsa`). | | `slh-dsa` | ✅ | Compiles the FIPS 205 module (`quantica::slh_dsa`). | | `sca-protected` | ✅ | Activates the masking + shuffled-NTT defences in ML-KEM and ML-DSA. | | `hazmat` | ☐ | Exposes the expert building-block tier (see *API tiers* below). | **Advanced — CT backend, RAM/stack tuning, and extra SCA/fault countermeasures** (all off by default; the RAM/stack knobs mainly target ML-DSA on constrained devices): | Feature | Default | Effect | |----------------------------|:-------:|------------------------------------------------------------------------------| | `asm-x86_64` | ☐ | Routes the `silentops` CT primitives through their inline-asm x86_64 backend so LLVM cannot lower a bit-twiddle into a secret-dependent `CMOV` (cache-timing leak). Recommended for x86_64 production/verification builds; inert on other targets. | | `low-mem` | ☐ | Recomputes the ML-DSA public matrix `A` on the fly from `rho` via SHAKE128 instead of a 57 KB stack array (~57 KB less peak stack, ~2× slower Sign). | | `low-stack` | ☐ | Moves the ML-DSA rejection-loop temporaries from stack to heap; with `low-mem`, ML-DSA-87 Sign peak stack drops ~174 KB → ~52 KB. Requires `alloc`. | | `compressed-poly` | ☐ | Stores intermediate polynomials at 3 bytes/coeff (768 vs 1024 B/poly) — 25% RAM saving in the ML-DSA rejection loop, negligible CPU. | | `indexed-sk` | ☐ | Decodes ML-DSA `s1`/`s2`/`t0` one polynomial at a time instead of all 23 KB at once (lower `sk_decode` stack). Best combined with `low-stack`. | | `small-secret` | ☐ | Stores ML-DSA `s1`/`s2` in i16 (reusing the ML-KEM NTT) instead of i32 — halves secret-poly storage. Requires `ml-kem`; **incompatible with `sca-protected`**. | | `union-buffer` | ☐ | Reuses one 1 KB polynomial workspace for all ML-DSA rejection-loop temporaries (Bos–Renes–Sprenkels, AFRICACRYPT 2022). **Incompatible with `sca-protected`**. | | `compressed-challenge` | ☐ | Stores the ML-DSA challenge `c` in 68 bytes + schoolbook multiply instead of NTT (~2 KB saved). **Incompatible with `sca-protected`**. | | `sca-masked-y` | ☐ | ML-DSA: samples the masking vector `y` as two arithmetic shares from SHAKE256, never materializing an unmasked `y` (DPA on `s1`, ePrint 2025/276). Implies `sca-protected`. | | `sca-ct-rejection` | ☐ | ML-DSA: computes every rejection-loop intermediate each iteration and takes one branch-free accept/reject at the end (timing leak on the early abort, ePrint 2025/582). Implies `sca-protected`. | | `sca-fors-redundancy` | ☐ | SLH-DSA: signs FORS twice and CT-compares both signatures + derived public keys, aborting on mismatch (single-fault grafting forgery, Genêt TCHES 2023/3). Additive. | | `sca-fors-dummy-siblings` | ☐ | SLH-DSA: streams all `2^A` FORS leaves in fixed order so no `idx`-dependent PRF address leaks (template oracle, Kannwischer COSADE 2018). Output byte-identical to FIPS 205. Additive. | | `sca-fors-indices-check` | ☐ | SLH-DSA / fault: re-derives the FORS indices from the digest and CT-compares to those consumed while signing, returning `FaultDetected` on mismatch (Castelnovi 2018 zero-index). Additive. | The `sca-protected`-incompatible RAM knobs above are enforced at compile time (a `compile_error!` fires if both are enabled). Disabling `std` makes the crate `no_std` (still requires `alloc`). In that mode the OS-backed `OsRng` disappears — the caller must provide their own `CryptoRng` impl wrapping a hardware RNG. ## API tiers The public surface is split in three deliberate tiers (since v0.2): 1. **Facade — the guaranteed API.** The typed entry points `MlKem
`, `MlDsa
`, `SlhDsa
`, their key/signature/ciphertext wrappers, the
`params`/`rng` modules, `prehash` and `secret`. Semver applies here;
applications should use only this tier.
2. **`hazmat` — expert bricks, feature-gated, no promise.** The FIPS-level
building blocks (byte encodings, samplers, Decompose/hints, the SLH-DSA
address/tweakable-hash surface, the raw-slice algorithm facades, the
deterministic `*_internal` CAVP entries) become public with the `hazmat`
feature, for KAT/test-vector tooling and research. **No stability,
misuse-resistance, or side-channel guarantee** — this tier may change in
*any* release, and misusing some bricks (e.g. the unauthenticated K-PKE
component) breaks the scheme's security.
3. **Internal.** Everything else (`ntt`, `kpke`, WOTS+/XMSS/FORS, the
masking/shuffling countermeasures) is `pub(crate)` — not reachable and
not a compatibility surface.
## Quick start
### ML-KEM (FIPS 203) — Key Encapsulation
```rust
use quantica::ml_kem::*;
let mut rng = OsRng;
// Key generation. ek is a public EncapsulationKey `, `Ciphertext ` | `DecapsulationKey `, `SharedSecret` |
| `ml_dsa` | `VerifyingKey `, `Signature ` | `SigningKey ` |
| `slh_dsa` | `VerifyingKey `, `Signature ` | `SigningKey ` |
All wrappers implement `from_bytes(&[u8])` (length-validated against the
parameter set), `as_bytes() -> &[u8]`, `Deref `, `MlDsa `, `SlhDsa `
are monomorphized at compile time via const generics, so a single
code path serves all security levels.
* **Internal byte-slice API stays raw** — `keygen_internal`,
`encaps_internal`, `sign_internal`, `verify_internal` accept and
return raw `&[u8]` / `Vec ::keygen` etc. wrap into the
typed key types.
* **Arithmetic widths** — i16 for ML-KEM (q = 3329 fits in 12 bits),
i32 for ML-DSA (q = 8 380 417 needs 23 bits), no NTT at all for
SLH-DSA.
* **NTT differences** — ML-KEM uses BitRev_7 with a partial NTT
(down to length-2, base-case multiply); ML-DSA uses BitRev_8 with a
full NTT (down to length-1, simple pointwise multiply).
* **SLH-DSA architecture** — WOTS+ → XMSS → Hypertree → FORS → SLH-DSA.
Purely hash-based, no algebraic structures.
## Side-channel countermeasures (summary)
### Always-on
These defences are active in every build, regardless of feature flags:
| Countermeasure | Algorithm | Threat addressed | How |
|-----------------------------|---------------------------|------------------------------------|----------------------------------------------------------------------|
| Constant-time arithmetic | ML-KEM, ML-DSA, SLH-DSA | Timing / cache-timing / basic SPA | Branchless `mod_q`, `ct_eq`, `ct_select` from `silentops` |
| Zeroize-on-Drop wrappers | ML-KEM, ML-DSA, SLH-DSA | Cold boot, memory dumps, UAF | `SecretBytes` / `SecretArray` → `silentops::ct_zeroize` on Drop |
| Volatile zeroization | ML-KEM, ML-DSA, SLH-DSA | Cold boot, memory dumps | `core::ptr::write_volatile` + `compiler_fence` on intermediates |
| **Double Decaps** | ML-KEM | DFA on FO comparison | Decaps runs twice; results compared; mismatch ⇒ random output |
| **dk integrity check** | ML-KEM | DFA on stored key material | `H(ek)` is embedded in `dk` and re-checked at every Decaps |
| **Hedged signing** | ML-DSA, SLH-DSA | Fault-induced nonce reuse | 32 bytes of fresh entropy mixed into the per-signature derivation |
### Feature-gated (`sca-protected`, on by default)
| Countermeasure | Algorithm | Threat addressed | Module |
|-----------------------------|--------------|------------------------------------|---------------------------------------|
| First-order additive masking| ML-KEM | First-order DPA, template attacks | `ml_kem::masked` |
| NTT butterfly shuffling | ML-KEM | SPA, trace alignment for DPA | `ml_kem::shuffle` |
| First-order additive masking| **ML-DSA** | First-order DPA, template attacks | `ml_dsa::masked` |
| Shuffled NTT (secret poly) | **ML-DSA** | SPA, trace alignment for DPA | `ml_dsa::shuffle` |
| Mask refresh between rounds | **ML-DSA** | Higher-order share correlation | `MaskedPoly::refresh()` between rejection iterations |
The masking layer is mathematically transparent — the masked path produces
**bit-identical** keys, ciphertexts, and signatures to the unmasked path,
which is why the NIST ACVP vectors keep matching with `sca-protected`
enabled. Internally:
* **ML-KEM**: secret polynomials `s`, `e`, etc. are split into two
additive shares mod `q = 3329` immediately after CBD sampling. NTTs run
on each share independently (linearity of the NTT), pointwise
multiplications by public matrices distribute over the shares.
* **ML-DSA**: in `dsa::sign_internal`, the secret-key vectors `s1`, `s2`,
`t0` are NTT-transformed via `shuffle::ntt_shuffled` then split into
`MaskedPoly` arrays. Each per-rejection-iteration multiplication
`ĉ · ŝx` runs through `masked_pointwise_mul_public`, followed by
`MaskedPoly::refresh()` to prevent inter-iteration share correlation.
Mask randomness is drawn from a SHAKE256-seeded deterministic `ScaRng`
(seed = `K ‖ rnd ‖ tr ‖ M'`), so `sign_internal` keeps a deterministic
signature and the ACVP fixed-`rnd` vectors still match.
* **SLH-DSA**: hash-based, no algebraic structure to mask — first-order
masking does not buy anything here. The always-on defences (CT
arithmetic, zeroization, hedged signing) are the relevant layer.
#### Approximate cost (single-threaded, release mode)
| Operation | Plain | `sca-protected` | Slowdown |
|------------------------|------------|-------------------|------------|
| ML-KEM-768 Decaps | ~0.03 ms | ~0.07 ms (double) | ~2.3× |
| ML-DSA-65 Sign | ~2.2 ms | ~7.1 ms | ~3.2× |
Numbers vary widely with hardware. Run the `quantica_bench` companion
crate for measurements on your machine.
### Timing leakage verification (dudect)
The shared `silentops::verify` module implements the dudect methodology
of Reparaz, Balasch and Verbauwhede (2017). A pre-built harness exercises
the most sensitive paths:
```bash
cargo run --release -p silentops --features std --example ct_verify_pqc
```
Currently checks:
* **ML-KEM-768 Decaps** — valid vs random ciphertext (implicit-rejection timing)
* **ML-KEM Barrett reduce** — small vs large input
* **ML-DSA-44 Sign** — message A vs message B (message-independent timing)
* **ML-DSA-44 Verify** — valid vs invalid signature
A t-statistic with `|t| < 4.5` after ~10⁶ samples is considered passing
(`p < 10⁻⁵`). Note that ML-DSA Sign uses rejection sampling, so its
timing inherently varies — a `FAIL` there is not necessarily a
vulnerability if the variation is independent of the secret key.
### Known residual surface
The following attack surfaces are *not* currently defended against and
are documented here so the reader knows what they are deploying. They
are tracked in the side-channel annex and in the tier-4 hardening
roadmap.
* **Masked Keccak / SHAKE** — the hash primitive feeding the PRF in
ML-KEM / ML-DSA / SLH-DSA is not masked; a DPA attacker with
trace access can mount Kannwischer-style attacks on `SK.seed`. A
3-share SHAKE variant is planned (see tier-4 item `T4-K`).
* **Grafting-tree fault attacks on SLH-DSA** — SLH-DSA signing does
not yet include a post-sign redundancy check; a single-fault
attacker (physical or Rowhammer-class) can coerce a forgery.
Redundancy is planned (tier-4 `T4-H` / `T4-J` / `T4-L`).
* **Heap allocations on the secret path** — secret-key buffers come
from `alloc` rather than caller-provided fixed buffers. A future
refactor will thread `&mut [u8]` end-to-end for bare-metal
stack-only operation.
* **Higher-order DPA across rejection iterations** — ML-DSA shares
`s1`, `s2`, `t0` are first-order-masked but not refreshed between
rejection iterations; a higher-order adversary combining two
iterations' leakage remains in scope. Scheduled as tier-4 `T4-C`.
* **Pointer-level CMOV by the compiler** — the Rust bit-hack CT
primitives are defended by the `silentops` asm backend on x86_64
and ARM; on targets without an asm backend (e.g. WebAssembly), the
CT guarantee is best-effort source-level only.
### Per-algorithm deep dives
The summary above lists which countermeasures are active; the full
per-algorithm SCA analyses — threat matrices, attack references, code
pointers, residual risks — live under
`quantica/doc/sca/countermeasures/` in the repository. The Sphinx
documentation pack (`./gendoc.sh quantica`) inlines them as a
navigable cross-linked tree below.
```{toctree}
:maxdepth: 2
../quantica/sca/index
```
## Performance
Run the workspace bench tool:
```bash
cargo run --release -p quantica_bench
```
Representative single-threaded numbers (no SIMD, no NEON, sca-protected on):
| Algorithm | KeyGen | Sign / Encaps | Verify / Decaps |
|------------------|----------|---------------|-----------------|
| ML-KEM-768 | ~0.03 ms | ~0.04 ms | ~0.07 ms |
| ML-DSA-65 | ~0.10 ms | ~7.1 ms | ~0.12 ms |
| SLH-DSA-SHAKE-128f | ~2 ms | ~40 ms | ~2 ms |
Notes:
* ML-KEM uses full Montgomery NTT arithmetic (shifts instead of divisions).
* ML-DSA Sign times vary because of rejection sampling.
* SLH-DSA is dominated by SHAKE evaluations; release mode is essential
(debug mode is ~100× slower).
## Building
### Desktop / server (default)
```bash
# Build everything (opt-level=2, CT-safe, all algos + sca-protected on)
cargo build --release -p quantica
# Build with no SCA countermeasures (faster, dudect baseline)
cargo build --release -p quantica \
--no-default-features --features std,ml-kem,ml-dsa,slh-dsa
# Run all tests (ACVP vectors, secret-module, masked/shuffle round-trips)
cargo test --release -p quantica
# Generate the rustdoc API reference
cargo doc -p quantica --no-deps --open
```
### `no_std` / bare-metal cross-compile
```bash
# Install the targets we care about
rustup target add thumbv7em-none-eabihf # Cortex-M4/M7
rustup target add thumbv6m-none-eabi # Cortex-M0/M0+
rustup target add thumbv8m.main-none-eabihf # Cortex-M33 (TrustZone)
rustup target add riscv32imc-unknown-none-elf # ESP32-C3, SiFive
# Cross-compile no_std + all 3 algos + sca-protected
cargo build -p quantica \
--no-default-features \
--features ml-kem,ml-dsa,slh-dsa,sca-protected \
--target thumbv7em-none-eabihf
```
In `no_std` mode the crate still depends on `alloc` (keys, ciphertexts and
signatures are `Vec , typed wrappers
│ │ ├── params.rs MlKem512, MlKem768, MlKem1024
│ │ ├── sha3.rs Thin wrappers: H, G, J, PRF, Xof
│ │ ├── ntt.rs NTT mod 3329 (full Montgomery, i16)
│ │ ├── encode.rs ByteEncode/Decode, Compress/Decompress
│ │ ├── sample.rs SampleNTT, SamplePolyCBD
│ │ ├── kpke.rs K-PKE (KeyGen, Encrypt, Decrypt)
│ │ ├── kem.rs ML-KEM + double-decaps + dk integrity (DFA)
│ │ ├── rng.rs CryptoRng trait + OsRng (std-only)
│ │ ├── masked.rs First-order additive masking (DPA)
│ │ └── shuffle.rs Fisher-Yates shuffled NTT (SPA)
│ ├── ml_dsa/ FIPS 204 ML-DSA (feature `ml-dsa`)
│ │ ├── mod.rs Public API: MlDsa , typed wrappers
│ │ ├── params.rs MlDsa44, MlDsa65, MlDsa87
│ │ ├── sha3.rs Thin wrappers: SHAKE128/256, sha3_256/512
│ │ ├── ntt.rs NTT mod 8 380 417 (Montgomery, i32)
│ │ ├── encode.rs BitPack, pk/sk/sig encode/decode
│ │ ├── sample.rs SampleInBall, RejNTTPoly, ExpandA/S/Mask
│ │ ├── decompose.rs Power2Round, Decompose, HighBits, Hints
│ │ ├── dsa.rs KeyGen, Sign (rejection loop, masked), Verify
│ │ ├── rng.rs CryptoRng trait + OsRng (std-only)
│ │ ├── masked.rs First-order additive masking (DPA)
│ │ └── shuffle.rs Fisher-Yates shuffled NTT (SPA)
│ └── slh_dsa/ FIPS 205 SLH-DSA (feature `slh-dsa`)
│ ├── mod.rs Public API: SlhDsa , typed wrappers
│ ├── params.rs 6 SHAKE parameter sets
│ ├── sha3.rs Shake256 streaming wrapper
│ ├── address.rs 32-byte ADRS structure
│ ├── hash.rs H_msg, PRF, PRF_msg, T_l, H, F
│ ├── wots.rs WOTS+ one-time signatures
│ ├── xmss.rs XMSS Merkle trees
│ ├── hypertree.rs Hypertree of XMSS trees
│ ├── fors.rs FORS forest
│ ├── slh.rs SLH-DSA top-level
│ └── rng.rs CryptoRng trait + OsRng (std-only)
├── examples/
│ ├── ml_kem_roundtrip.rs
│ ├── ml_dsa_sign_verify.rs
│ └── slh_dsa_sign_verify.rs
└── tests/
├── ml_kem_kat.rs
├── ml_dsa_kat.rs
├── slh_dsa_kat.rs
└── vectors/ NIST ACVP-Server JSON / .rsp vectors
```
## Known limitations
### Side-channel protection
* **`Vec