Skip to content

Latest commit

 

History

History
445 lines (361 loc) · 26.7 KB

security.md

File metadata and controls

445 lines (361 loc) · 26.7 KB

Security Model

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:

  1. Quantum-capable adversaries can break classical public-key schemes (RSA, ECC, and similar)
  2. Adversaries are computationally strong (classical and quantum where relevant)
  3. Side-channel attacks are real and must be mitigated in implementation
  4. Memory safety is critical for cryptographic implementations

Threat Model

Adversary Capabilities

  • 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

Security Goals

  • 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

Algorithm Selection

Security Tiers

lib-Q provides three security tiers to balance quantum resistance with performance:

Tier 1: Ultra-Secure (Pure Post-Quantum)

  • 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."

Tier 2: Balanced (Post-Quantum)

  • 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

Tier 3: Performance (Post-Quantum + Optimized)

  • 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

Zero-Knowledge Proofs (ZKPs)

All tiers support post-quantum zero-knowledge proofs:

zk-STARKs (Primary)

  • 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> (ZkpField in lib-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 SessionKeyDerivationAir commitments). 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 example prove_secret_value_nist / HashPreimageNistAir and the NIST preimage APIs in lib-q-zkp::api).
  • WASM: lib-q-zkp and lib-q-lattice-zkp are exercised in CI for wasm32-unknown-unknown and 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.

Other ZKP work in-tree

  • lib-q-lattice-zkp: Research crate for module-lattice commitments and sigma-style proofs on lib-q-ring; not an AIR/STARK pipeline. See zkp-implementation.md.

Privacy-oriented protocols (registry metadata only)

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.
Constant-time scope (Phase 7 paths)

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 (aggregated verify_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.

Replay model
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.

ZKP Features

  • Proof generation / verification: ZkpProver / ZkpVerifier and lower-level StarkProver / StarkVerifier over 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.

Post-Quantum Hash Functions

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):

  1. SHAKE256 (Primary)

    • Variable output length
    • Used for hash-based signatures (SLH-DSA)
    • 256-bit security level
    • NIST standardized
  2. SHAKE128 (General Purpose)

    • Variable output length
    • Used for general hashing needs
    • 128-bit security level
    • NIST standardized
  3. 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.

NIST PQC Standardization

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:

Standardized Algorithms

  • 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)

Selected; standard not yet published

  • 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.

Code-based KEMs (workspace implementations)

  • 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.

Emerging Post-Quantum Algorithms

  • 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, feature saturnin-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.

Forbidden Classical Algorithms

The following classical algorithms are explicitly forbidden in lib-Q:

Forbidden KEMs

  • RSA: Broken by Shor's algorithm
  • ECC: Broken by Shor's algorithm
  • DH: Broken by Shor's algorithm
  • ECDH: Broken by Shor's algorithm

Forbidden Signatures

  • 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

Forbidden identity-based / pairing-based public-key schemes

  • 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 of 2^40, 2^64 and 2^96 respectively, needing circuit widths of 1.50 · 2^211, 1.50 · 2^163 and 1.50 · 2^99 qubits. Once a depth limit binds, cost scales linearly in 2^k rather than as 2^{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' own 2^112 quantum claim, which remains the number this repo publishes.

Forbidden Hash Functions

  • 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.

Forbidden Symmetric Ciphers

  • 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-random deterministic RNG (0.0.4+): test/KAT expansion uses KT128 (libQ-DET-RNG-v1 domain), not ChaCha20. Optional deterministic-saturnin uses Saturnin CTR for cipher-aligned test streams.

Implementation Security Guidelines

Constant-Time Operations

All cryptographic operations must be constant-time to prevent timing attacks:

Constant-Time Requirements

  • 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

Implementation Examples

// 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)
}

WebAssembly and constant-time discipline

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.

Memory Safety

Rust's ownership model provides memory safety, but additional measures are required:

Secure Memory Management

  • Automatic zeroing: Use zeroize crate 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

Implementation Examples

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 }
    }
}

Input Validation

Comprehensive input validation prevents many security vulnerabilities:

Validation Requirements

  • 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

Implementation Examples

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(())
}

Random Number Generation

Cryptographically secure random number generation is essential:

RNG Requirements

  • 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

Implementation Examples

// 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)
}

Security Testing

Static Analysis

  • 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

Standing triage: rust/hard-coded-cryptographic-value

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.

Dynamic Analysis

  • Fuzzing: Automated input testing for edge cases
  • Memory testing: AddressSanitizer and MemorySanitizer
  • Timing analysis: Constant-time verification
  • Side-channel testing: Power and timing analysis

Formal Verification

  • 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

Security Audits

Internal Audits

  • Code review: Security-focused code review process
  • Architecture review: Security architecture validation
  • Implementation review: Implementation security validation
  • Integration review: Integration security validation

External Audits

  • Third-party audits: Independent security audits
  • Academic review: Academic community review
  • Industry review: Industry expert review
  • Community review: Open source community review

Audit Requirements

  • 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

Incident Response

Security Incident Process

  1. Detection: Automated and manual detection of security issues
  2. Assessment: Rapid assessment of impact and severity
  3. Containment: Immediate containment of security issues
  4. Investigation: Thorough investigation of root causes
  5. Remediation: Comprehensive remediation of security issues
  6. Communication: Transparent communication with users
  7. Prevention: Implementation of preventive measures

Disclosure Policy

  • 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 Contact

  • Security email: github@enkom.dev
  • PGP key: Published for secure communication (when available)
  • Responsible disclosure: Recognition for responsible disclosure