lib-Q targets post-quantum asymmetric cryptography and SHA-3–family hashing for its stated APIs, with Saturnin and SHAKE-based symmetric options as the design center (see SECURITY.md). Our threat model assumes:
- Quantum-capable adversaries can break classical public-key schemes (RSA, ECC, and similar)
- Adversaries are computationally strong (classical and quantum where relevant)
- Side-channel attacks are real and must be mitigated in implementation
- Memory safety is critical for cryptographic implementations
- Quantum computers: Can break RSA, ECC, and other classical algorithms
- Classical computers: Unlimited computational power
- Side-channel attacks: Timing, power analysis, cache attacks
- Memory attacks: Buffer overflows, use-after-free, memory leaks
- Implementation attacks: Protocol-level vulnerabilities
- Confidentiality: Messages remain secret even against quantum adversaries
- Integrity: Messages cannot be tampered with undetected
- Authenticity: Message origin can be verified
- Forward secrecy: Compromised keys don't affect past communications
- Post-quantum security: All security properties hold against quantum attacks
lib-Q provides three security tiers to balance quantum resistance with performance:
- KEMs: ML-KEM, CB-KEM, HQC
- Signatures: ML-DSA, FN-DSA, SLH-DSA
- Symmetric: SHAKE256-based constructions, Saturnin
- HPKE: Pure post-quantum HPKE with Saturnin AEAD
- Hash: SHAKE256, SHAKE128, cSHAKE256
- Use Case: Maximum security, performance secondary
- Note (CB-KEM): CB-KEM's conservative public-key-pseudorandomness assumption is eroded by a provable quasipolynomial distinguisher (ePrint 2026/1630) — not a message/key break, but see the Code-based KEMs caveat below before treating it as strictly "ultra-secure."
- KEMs: ML-KEM, CB-KEM, HQC
- Signatures: ML-DSA, FN-DSA, SLH-DSA
- Symmetric: Saturnin, SHAKE-based AEAD options
- HPKE: PQ-only HPKE (ML-KEM suite + Saturnin / SHAKE256 / optional duplex-sponge AEAD)
- Hash: SHAKE256, SHAKE128, cSHAKE256
- Use Case: Strong security with good performance
- KEMs: ML-KEM, CB-KEM, HQC
- Signatures: ML-DSA, FN-DSA
- Symmetric: Saturnin, SHAKE-based AEAD options (optimized modes where applicable)
- HPKE: PQ-only HPKE with the same primitive set; tiering is primarily about parameter sets and AEAD choice, not a classical “hybrid” KEM
- Hash: SHAKE256, SHAKE128, cSHAKE256
- Use Case: Maximum performance, strong security
All tiers support post-quantum zero-knowledge proofs:
- Scalable: Proof size grows roughly logarithmically with trace size for the parameters chosen (FRI blowup, query count).
- Transparent: No trusted setup required.
- Post-quantum secure (default pipeline): Soundness is argued from collision resistance of the transcript layer (default SHAKE256 challenger and Merkle hashing in
lib-q-zkp::stark::default_config) together with FRI and query repetition—not from a classical discrete-log or pairing assumption. - Trace field: The default native field for STARK traces is
Complex<Mersenne31>(ZkpFieldinlib-q-zkp). It is chosen for FFT/FRI efficiency; security margins must be set via FRI parameters and the hash-backed transcript, not by treating the field as a 256-bit secrecy container. - Poseidon inside some AIRs: For constraint efficiency, several shipped paths use Poseidon-128 inside the AIR (for example default secret-preimage proofs, Merkle inclusion, and
SessionKeyDerivationAircommitments). Transcript binding for those proofs still uses the STARK challenger configuration (SHAKE256 by default). If your policy requires NIST-only hash commitments inside the proved statement, use the documented NIST variants (for exampleprove_secret_value_nist/HashPreimageNistAirand the NIST preimage APIs inlib-q-zkp::api). - WASM:
lib-q-zkpandlib-q-lattice-zkpare exercised in CI forwasm32-unknown-unknownand wasm-bindgen smoke tests; see ZKP Implementation. - Use cases: Verifiable computation, privacy-preserving proofs of statements encoded as AIRs, and protocol-adjacent demos—always match the proof type to your assurance target.
lib-Q implements zk-STARKs via its own NIST-adapted stack (lib-q-stark and satellite crates) and an optional full Plonky3-derived stack (lib-q-plonky); see ZKP Implementation for layout, features, and CI.
lib-q-lattice-zkp: Research crate for module-lattice commitments and sigma-style proofs onlib-q-ring; not an AIR/STARK pipeline. See zkp-implementation.md.
These components use NIST PQC primitives (ML-KEM, ML-DSA-field-compatible rings, Saturnin, SHAKE256) but are not drop-in replacements for the lib-q-core KEM/signature providers. They are labeled under AlgorithmCategory::PrivacyProtocol in lib-q-types for policy and documentation.
| Component | Crate / module | Security notes |
|---|---|---|
| Blind issuance + anonymous tokens | lib-q-lattice-zkp (blind, token) |
CRS Ajtai model; see BLIND_ISSUANCE.md. Issuer attestation binds blinded commitments; not Chaum blind RSA. SpendingProof carries the token serial; double-spends are rejected at the application layer by serial-set membership (verifier-tracked registry, not in-protocol). |
| Nullifier + batch amortisation | lib-q-lattice-zkp (sigma/uniqueness, sigma/amortise) |
Deterministic nullifiers for registries; opening proofs bind nullifiers into Fiat–Shamir contexts. Uniqueness amortisation labels enable single-batch verification across multiple commitments under one realm. |
| Hierarchical Merkle + opening | lib-q-lattice-zkp (sigma/hierarchical) — prove_level_membership / verify_level_membership |
Leaf payload is revealed (clearance level, role tag, parent digest). The verifier learns the path position. Full position-and-attribute-hiding PVTN ZK is future work; this construction is a Merkle path certificate composed with an Ajtai opening tied to the leaf. |
| Federation ring openings | lib-q-ring-sig |
Opening proofs over a shared CRS; linear-scan verification is not issuer-hiding toward the verifier (see crate DESIGN). Default DualRing-LB path uses an aggregated CCS 2021–style verify (verify_dual_ring_opening). |
| Credential presentation | lib-q-ring-sig (credential) |
Default path verifies with verify_dualring_lb (full-ring aggregate). A legacy API remains that uses verify_federation_opening_scan (linear cost; see module docs). The verifier does not receive the signer index on the default path. |
The following paths are wired for statistical timing / TVLA-style harnesses via
lib-q-sca-test privacy_workloads
(scaffold only; not a certification claim):
BlindIssuance::verify— blind issuance bundle verification.verify_federation_opening— federation opening at a known signer index.verify_dualring_lb— DualRing-LB (aggregatedverify_dual_ring_opening; uniform cost across ring slots).BlindSignature::verify_blind_signature— pilot blind-signature bundle verify.verify_private_membership— private-membership pilot verifier.registry_nullifier,witness_nullifier,federation_digest— public transcript / registry digests (SHAKE256).
Prover-side rejection-sampling paths (BlindIssuance::request/issuer_sign,
sign_federation_message, prove_opening, prove_dual_ring_opening, sign_dualring_lb, amortise) are intentionally excluded:
their timing is data-dependent by construction (loop count of the abort), and they
must be invoked only on attacker-independent secrets.
| Asset | Replay defence | Where enforced |
|---|---|---|
Anonymous token (AnonymousToken::spend) |
Application-layer registry of spent serials | Verifier maintains a per-realm serial → seen set; SpendingProof::verify rejects mismatched serials. |
- Proof generation / verification:
ZkpProver/ZkpVerifierand lower-levelStarkProver/StarkVerifierover configured AIRs (see ZKP Implementation). - Privacy-preserving statements: Depends on what is public in the AIR (public inputs, Merkle roots, metadata); not every API hides all witness bits—read the AIR and proof-type docs.
- Scalable verification: Verifier cost is dominated by Merkle openings and FRI checks at the chosen security parameters.
For general-purpose hashing and XOFs in KEM, signature, and AEAD-facing APIs, the design center is the SHA-3 family (quantum-resistant in the usual hash security model):
-
SHAKE256 (Primary)
- Variable output length
- Used for hash-based signatures (SLH-DSA)
- 256-bit security level
- NIST standardized
-
SHAKE128 (General Purpose)
- Variable output length
- Used for general hashing needs
- 128-bit security level
- NIST standardized
-
cSHAKE256 (Customizable)
- Customizable hash function
- Domain separation capabilities
- Used for specific applications requiring customization
- NIST standardized
STARK / ZKP note: lib-q-zkp also uses Poseidon-128 inside specific AIR and Merkle gadgets where documented. That is an in-circuit primitive for arithmetization efficiency; it does not replace SHAKE256 as the default Fiat-Shamir / Merkle transcript hash in the recommended STARK configuration. Integrators with a strict "NIST-only inside the proved commitment" requirement should select the NIST preimage and NIST-hash AIR paths described in ZKP Implementation.
Our post-quantum asymmetric primitives are drawn from NIST's Post-Quantum Cryptography process; each one's exact status (published standard, selected-but-unpublished, or unselected submission) is called out below, and the emerging-algorithm family further down in this document is outside that process entirely:
- ML-KEM: NIST PQC Standard (FIPS 203)
- ML-DSA: NIST PQC Standard (FIPS 204)
- SLH-DSA: NIST PQC Standard (FIPS 205; see lib-q-slh-dsa/README.md — only the simple instances are standardized, the robust variants are not provided here)
- FN-DSA: Selected by NIST for standardization (the FALCON algorithm). FIPS 206 has not been published — there is no finalized standard, and no public draft, to be "compliant" with. See lib-q-fn-dsa/README.md.
- CB-KEM (Classic McEliece–family parameter sets) and HQC are implemented as post-quantum KEMs in this repository; track each crate’s README and NIST publications for the exact standardization status of the parameter sets you enable.
- Structural-cryptanalysis caveat (CB-KEM). IACR ePrint 2026/1630 (2026) gives a provable classical quasipolynomial-time distinguisher of Goppa–McEliece public keys from random, with concrete estimates (2^114–2^124 binary operations) below generic decoding for every Classic McEliece parameter set — and, for
mceliece348864, below that set's nominal ~2^128 target. It is a public-key distinguisher, not a message- or key-recovery attack, and is "not yet practical"; the message-recovery extension is heuristic and far from practical. No code/wire/parameter change follows from it. Full, source-verified write-up and the tracking obligation (CM-1): lib-q-cb-kem/SECURITY.md.
- Structural-cryptanalysis caveat (CB-KEM). IACR ePrint 2026/1630 (2026) gives a provable classical quasipolynomial-time distinguisher of Goppa–McEliece public keys from random, with concrete estimates (2^114–2^124 binary operations) below generic decoding for every Classic McEliece parameter set — and, for
- Saturnin: Lightweight symmetric algorithm suite with 256-bit block cipher
- Designed for IoT and constrained devices
- Provides authenticated encryption and hashing modes
- Superior post-quantum security compared to classical alternatives
- Saturnin-SIV (
lib-q-aead, featuresaturnin-siv): a 256-bit, misuse-resistant (SIV-style) AEAD built from KMAC256-derived keys + Saturnin-CTR. It is the only AEAD in this repo combining a 256-bit key and tag with nonce-misuse resistance — "256-bit" being the key and tag width, not a claimed security level; the designers claim 224-bit classical and 112-bit quantum (LWC spec v1.1 §1.2), never 256, as this file itself says eight lines below — Romulus-M (below) is misuse-resistant but only at 128-bit strength, and the other 256-bit Saturnin modes (CTR-Cascade/Short) are nonce-respecting only. Prefer it for file encryption or any scenario where nonce reuse across re-encryptions is plausible and 256-bit margin is required.
- Romulus (
lib-q-romulus, NIST Lightweight Cryptography finalist, SKINNY-based AEAD): 128-bit key/nonce/tag, unlike the 256-bit key/tag used by Saturnin, Rocca-S, and duplex-sponge elsewhere in this document — under Grover's algorithm that is a ~2^64 post-quantum margin. The 256-bit modes are materially stronger, but do NOT quote ~2^128 for them either: Saturnin's designers claim 224-bit, not 256 (LWC spec section 1.2), and its hash-level claim is 2^112 classical collision. Two modes: Romulus-N (nonce-respecting; nonce reuse breaks confidentiality) and Romulus-M (nonce-misuse-resistant, SIV-style — reusing a nonce does not enable forgery, though it remains 128-bit). See lib-q-romulus/SECURITY.md and lib-q-aead README § Key commitment.
The following classical algorithms are explicitly forbidden in lib-Q:
- RSA: Broken by Shor's algorithm
- ECC: Broken by Shor's algorithm
- DH: Broken by Shor's algorithm
- ECDH: Broken by Shor's algorithm
- RSA-PSS: Broken by Shor's algorithm
- ECDSA: Broken by Shor's algorithm
- Ed25519: Broken by Shor's algorithm
- Ed448: Broken by Shor's algorithm
- Pairing-based / bilinear-group encryption and signatures (e.g. BLS, and identity-based encryption or identity-based matchmaking encryption built on the Bilinear Diffie–Hellman assumption): rest on the discrete-log/pairing hardness that Shor's algorithm breaks — same class as ECC/ECDH above. lib-Q implements none and adds none.
- Radar dispositions that land in this class are recorded under docs/radar/; see ePrint 2023/1435 IB-ME for a worked example of why a pairing-based IB-ME scheme is out of scope for a post-quantum workspace.
On the "Grover halves it" convention used throughout this section. Naive halving is the standard, conservative way to state a post-quantum margin and this document keeps it. It is worth knowing that the one published concrete Grover circuit for Saturnin-256 is materially worse for the attacker than halving suggests: Jang, Baksi, Kim, Song, Seo and Chattopadhyay, IACR ePrint 2020/1485, Table 16 (p.22), put unlimited-depth key search at G-cost 1.13 · 2^147 — not
2^128— and Table 20(a) (p.26) puts it at 1.63 · 2^246, 1.63 · 2^222 and 1.63 · 2^190 under NIST MAXDEPTH limits of2^40,2^64and2^96respectively, needing circuit widths of1.50 · 2^211,1.50 · 2^163and1.50 · 2^99qubits. Once a depth limit binds, cost scales linearly in2^krather than as2^{k/2}. Cite these as an upper bound on one particular, visibly unoptimised circuit — never as a lower bound on Saturnin's security, and never as a replacement for the designers' own2^112quantum claim, which remains the number this repo publishes.
-
MD5: Completely broken (collision and preimage attacks)
-
SHA-1: Practically broken (collision attacks)
-
SHA-256 / SHA-512 (SHA-2 family): Grover's algorithm halves the effective security level — SHA-256 is reduced to 128-bit security against quantum adversaries, which falls below the required margin for new designs. lib-Q uses the SHA-3 / Keccak family exclusively for hash-based constructions.
Note: SHA-2 may still appear inside third-party crates as part of TLS record MAC or HKDF where no lib-Q-controlled path is involved. Those usages are not sanctioned for security-critical lib-Q operations.
-
AES-128: Grover's algorithm reduces security to approximately 64 bits — not acceptable for post-quantum use.
-
ChaCha20 / Poly1305 (standalone): 256-bit key variants survive Grover with ~128-bit quantum security, but these primitives are not part of the lib-Q algorithm set, which centers on Saturnin and SHA-3 family constructions. Do not substitute them for lib-Q primitives.
Note: AES-256 may appear inside reviewed implementations where a standard or interoperability layer requires it (for example NIST AES-CTR-DRBG KAT paths in
lib-q-random). That is not an invitation to add AES-GCM or ChaCha20-Poly1305 as general-purpose user-facing substitutes for the Saturnin / SHAKE-centered AEAD story without maintainer review.lib-q-randomdeterministic RNG (0.0.4+): test/KAT expansion uses KT128 (libQ-DET-RNG-v1domain), not ChaCha20. Optionaldeterministic-saturninuses Saturnin CTR for cipher-aligned test streams.
All cryptographic operations must be constant-time to prevent timing attacks:
- No branching on secret data: All branches must be independent of secret values
- Constant-time comparison: Use dedicated comparison functions
- Constant-time selection: Use bitwise operations for conditional selection
- Cache-timing resistance: Avoid memory access patterns that depend on secrets
// Constant-time comparison
pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
// Constant-time selection
pub fn constant_time_select(condition: bool, a: u32, b: u32) -> u32 {
let mask = if condition { 0xffffffff } else { 0 };
(a & mask) | (b & !mask)
}The same source-level rules (no secret-dependent branches, subtle-based selection where applicable, table-lookup discipline) apply when compiling for wasm32-unknown-unknown. LLVM may choose different instruction sequences and memory layouts than for x86_64 or AArch64; constant-time intent in Rust is not a formal proof on any target, and WASM adds a distinct JIT and sandbox model. Treat WASM deployments like any other high-risk environment: pin toolchains, run regression tests on the actual target, and plan dedicated side-channel review if your threat model assumes browser-grade adversaries.
Rust's ownership model provides memory safety, but additional measures are required:
- Automatic zeroing: Use
zeroizecrate for sensitive data - No heap allocation: Stack-only operations for constrained environments
- Memory barriers: Prevent compiler optimizations that could leak secrets
- Secure deallocation: Ensure sensitive data is properly cleared
use zeroize::Zeroize;
#[derive(Zeroize)]
#[zeroize(drop)]
pub struct SecretKey {
key: [u8; 32],
}
impl SecretKey {
pub fn new() -> Self {
let mut key = [0u8; 32];
getrandom::getrandom(&mut key).expect("Failed to generate random key");
Self { key }
}
}Comprehensive input validation prevents many security vulnerabilities:
- Key sizes: Verify all key sizes match expected values
- Algorithm parameters: Validate security levels and algorithm choices
- Buffer bounds: Check all buffer accesses are within bounds
- Format validation: Verify input data formats are correct
pub fn validate_key_size(key: &[u8], expected_size: usize) -> Result<(), Error> {
if key.len() != expected_size {
return Err(Error::InvalidKeySize {
expected: expected_size,
actual: key.len(),
});
}
Ok(())
}
pub fn validate_security_level(level: u32) -> Result<(), Error> {
if !is_supported_security_level(level) {
return Err(Error::InvalidSecurityLevel {
level,
supported: SECURITY_LEVELS,
});
}
Ok(())
}Cryptographically secure random number generation is essential:
- Cryptographic quality: Use cryptographically secure RNG
- Entropy sources: Multiple entropy sources for robustness
- Platform integration: Use platform-specific secure RNG when available
- WASM compatibility: Support for browser and Node.js environments
// Platform-specific RNG
#[cfg(target_arch = "wasm32")]
pub fn secure_random_bytes(buffer: &mut [u8]) -> Result<(), Error> {
use wasm_bindgen::JsCast;
use web_sys::window;
let window = window().ok_or(Error::NoWindow)?;
let crypto = window.crypto().ok_or(Error::NoCrypto)?;
let array = js_sys::Uint8Array::new_with_length(buffer.len() as u32);
crypto.get_random_values_with_u8_array(&array);
array.copy_to(buffer);
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn secure_random_bytes(buffer: &mut [u8]) -> Result<(), Error> {
getrandom::getrandom(buffer).map_err(|_| Error::RandomGenerationFailed)
}- Rust Analyzer: Type safety and memory safety checks
- Clippy: Additional linting and security checks
- Cargo Audit: Dependency vulnerability scanning
- CodeQL: Advanced static analysis for security vulnerabilities
This rule is the dominant source of code-scanning alerts in this repo (66 dismissed as of 2026-08-07) and it will keep firing. Before dismissing another one, read this — and then still check the individual alert, because the whole point is that the next one might be real.
Why it recurs. The rule flags literal keys, nonces and salts. Rust convention puts unit tests
in a #[cfg(test)] mod tests block inside the production source file, and cryptographic tests
necessarily use fixed inputs — a KAT with a random nonce is not a KAT. CodeQL analyses the file as
a whole and does not exclude #[cfg(test)], so every new test fixture becomes a new critical
alert.
Why it cannot be configured away. Code scanning here is GitHub default setup, which accepts
no config file, no paths-ignore and no query filters. Switching to advanced setup would not help
either: the offending lines are in the same files as production code, so path exclusion cannot
separate them, and suppressing the rule outright would discard genuine findings.
How to triage one. Establish which side of the #[cfg(test)] boundary the flagged line falls
on — find the nearest preceding #[cfg(test)] and confirm the line is inside that module, by
reading it, not by a line-number heuristic. If it is test-only, dismiss as "used in tests" with
a comment naming the file, the line, the line the cfg(test) opens on, and why the value must be
fixed. GitHub caps that comment at 280 characters. If the value is reachable from a non-test
build, it is a real finding: do not dismiss it, file it.
A hard-coded value in a *_with_seed / *_with_m_salt style API is expected — those entry points
exist precisely so tests can drive the algorithm deterministically. Confirm the production path
(e.g. encapsulate()) draws from the RNG, and say so in the dismissal.
- Fuzzing: Automated input testing for edge cases
- Memory testing: AddressSanitizer and MemorySanitizer
- Timing analysis: Constant-time verification
- Side-channel testing: Power and timing analysis
- Cryptographic proofs: Formal verification of algorithm correctness
- Implementation proofs: Formal verification of constant-time properties
- Protocol proofs: Formal verification of security protocols
- Memory safety proofs: Formal verification of memory safety
- Code review: Security-focused code review process
- Architecture review: Security architecture validation
- Implementation review: Implementation security validation
- Integration review: Integration security validation
- Third-party audits: Independent security audits
- Academic review: Academic community review
- Industry review: Industry expert review
- Community review: Open source community review
- Zero classical crypto: Verify no classical algorithms are used
- Constant-time verification: Verify all operations are constant-time
- Memory safety: Verify memory safety properties
- Input validation: Verify comprehensive input validation
- Random number generation: Verify secure RNG usage
- Detection: Automated and manual detection of security issues
- Assessment: Rapid assessment of impact and severity
- Containment: Immediate containment of security issues
- Investigation: Thorough investigation of root causes
- Remediation: Comprehensive remediation of security issues
- Communication: Transparent communication with users
- Prevention: Implementation of preventive measures
- Responsible disclosure: Coordinated disclosure with researchers
- Transparent communication: Open communication about security issues
- Timely updates: Prompt updates for security issues
- User notification: Clear notification of affected users
- Security email: github@enkom.dev
- PGP key: Published for secure communication (when available)
- Responsible disclosure: Recognition for responsible disclosure