If you've found a vulnerability, report it privately rather than in a public issue. That covers anything that could leak a secret, forge a signature, or otherwise be exploited. Include what's needed to reproduce it: scheme, parameter set, platform, and ideally a minimal case. Expect an acknowledgement within a week and a coordinated fix. When the cause lives in the vendored C reference code, the report is forwarded upstream (mlkem-native, mldsa-native, slhdsa-c, Monocypher) and the pin is bumped as part of the fix. Write to d3z.the.dev@gmail.com.
quantex is a binding layer. All cryptographic arithmetic runs in vendored C
reference implementations, pinned by commit and vendored verbatim (see
quantex/cbits/README.md for pins and local patches):
| Component | Source | Role |
|---|---|---|
| ML-KEM | mlkem-native | FIPS 203 |
| ML-DSA | mldsa-native | FIPS 204 |
| SLH-DSA | slhdsa-c | FIPS 205 |
| X25519 (inside X-Wing) | Monocypher | CFRG draft combiner is a local shim |
The library defends against:
- Scheme misuse: phantom types make mixing schemes a compile error.
- Malformed input: strict length validation at every decode boundary, plus the FIPS 203 §7.2 modulus check and §7.3 hash-consistency check on ML-KEM and X-Wing key import.
- Accidental secret exposure: no
Showfor secrets, byte-count-onlyShowfor public material, no implicit serialization, and zeroizing storage. - Non-constant-time secret comparison: equality goes through a branch-free accumulator.
It does not defend against a compromised process (an attacker who can read your heap reads your keys), microarchitectural side channels beyond what the upstream implementations provide, or fault attacks.
Constant-time behavior is inherited from upstream. mlkem-native, mldsa-native,
and slhdsa-c
avoid secret-dependent branches and table lookups by construction; Monocypher
documents constant-time X25519. The wrapper adds none of its own: the Haskell
layer moves opaque buffers, and the C shims (quantex_xwing.c,
quantex_memory.c) use fixed-size operations. Secret equality goes through
quantex_compare, a branch-free accumulator.
No claim is made about hardening against speculative execution or against power and EM side channels.
- Secret keys, seeds, signing randomness, and shared secrets live in
SecretBytes: C-heap buffers whose finalizer zeroizes (explicit_bzero,SecureZeroMemory, or a volatilememset) before freeing. - Buffers are also zeroized on allocation, before the caller fills them, so a partial fill can never surface recycled heap as key material.
- Zero-on-GC is best-effort. GHC gives no upper bound on when, or whether, a finalizer runs before process exit.
- Swap and core dumps are resisted best-effort. Every
SecretBytesbuffer is pinned on allocation (mlockorVirtualLock) and, on Linux, markedMADV_DONTDUMP; the finalizer unpins before freeing. This is silent on failure: underRLIMIT_MEMLOCKexhaustion, missing privilege, or an old kernel, the secret stays swappable with no error. Locking is page-granular, so each buffer is allocated page-aligned and page-rounded and owns its pages outright: unpinning one buffer can never unpin another that shares a page. No guarantee is made that a secret never reaches disk. - The X-Wing shim zeroizes every intermediate secret: the expanded seed, the ML-KEM and X25519 secrets, both shared halves, and the combiner transcript.
- Upstream stack temporaries follow upstream behavior. Monocypher wipes its internals; mlkem-native, mldsa-native, and slhdsa-c do not scrub theirs.
These functions copy secrets into ordinary, non-zeroized memory by design. Treat their results as radioactive:
secretKeyBytes,sharedSecretBytes, andseedBytes: the serialization boundary. Storing the 64-byte seed viaseedBytesis the recommended at-rest form for an ML-KEM secret key, but the revealed copy is still radioactive. Keep it under the same protection as the expanded key.Codecdeliberately covers only public-material types (public keys, ciphertexts, signatures); secret material has noCodecinstance, soencode/decodecannot cross this boundary implicitly.- Constructing a secret from a
ByteString(secretKey,seed) copies into zeroizing storage but cannot scrub the caller's source bytes. To skip that copy, usesecretKeyAllocateto fill a locked page directly, or the library's own entropy path (Quantex.Random.entropySecret).
To consume a secret without crossing that boundary, use withSharedSecret (KEM)
and withSecretKey (KEM and signatures): they lend the raw zeroizing buffer to a
callback, so a KDF or AEAD key derives without materializing an unzeroized copy.
The borrowed pointer is valid only inside the callback and must not escape it.
The IO convenience wrappers draw from the platform RNG through a minimal shim:
getrandom on Linux, arc4random_buf on BSD and macOS, BCryptGenRandom on
Windows, with a getentropy fallback. Failure throws; there is no userspace PRNG
fallback. The deterministic *From functions never touch system entropy.
- Known-answer tests: ACVP vectors for ML-KEM, ML-DSA, and SLH-DSA (a bundled
subset plus an extended suite over the full internal projections), and X-Wing
vectors from the spec repository. Provenance is in
quantex/test/vectors/README.md. - Prehash (HashML-DSA FIPS 204 §5.4, HashSLH-DSA FIPS 205 §10.2): the twelve
digest functions run through the
quantex_prehashshim over the slhdsa-c hash primitives, pinned by the FIPS 180-4 and 202"abc"known answers, which distinguishes same-length functions that the size checks cannot. Domain separation from pure signing (representative prefix byte 0 versus 1) is asserted both ways: a pure signature never verifies as prehash, and the reverse never verifies as pure. ADigestcarries no secret material, so it is an ordinaryByteString, not zeroizing storage. - Every advertised size is cross-checked three ways: against the vendored headers
(
static_assert), againstslh_param_tat runtime, and against the literal tables of FIPS 203/204/205 and the X-Wing draft in the test suite. - Before every release the test suite is also run under an AddressSanitizer instrumented build that exercises the full bound C surface. Leak detection is disabled there: the GHC RTS keeps allocations until process exit, which LeakSanitizer misreads as leaks. This run is a release gate, not a hosted CI job.