Skip to content

Security: d3z-the-dev/quantex

Security

SECURITY.md

Security

Reporting a vulnerability

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.

Threat model

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 Show for secrets, byte-count-only Show for 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 properties

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.

Zeroization

  • Secret keys, seeds, signing randomness, and shared secrets live in SecretBytes: C-heap buffers whose finalizer zeroizes (explicit_bzero, SecureZeroMemory, or a volatile memset) 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 SecretBytes buffer is pinned on allocation (mlock or VirtualLock) and, on Linux, marked MADV_DONTDUMP; the finalizer unpins before freeing. This is silent on failure: under RLIMIT_MEMLOCK exhaustion, 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.

Deliberate boundaries

These functions copy secrets into ordinary, non-zeroized memory by design. Treat their results as radioactive:

  • secretKeyBytes, sharedSecretBytes, and seedBytes: the serialization boundary. Storing the 64-byte seed via seedBytes is 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. Codec deliberately covers only public-material types (public keys, ciphertexts, signatures); secret material has no Codec instance, so encode/decode cannot 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, use secretKeyAllocate to 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.

Randomness

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.

Verification

  • 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_prehash shim 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. A Digest carries no secret material, so it is an ordinary ByteString, not zeroizing storage.
  • Every advertised size is cross-checked three ways: against the vendored headers (static_assert), against slh_param_t at 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.

There aren't any published security advisories