Skip to content

Security: N0laa/qsmm

Security

SECURITY.md

QSMM Security Audit

Deep security audit completed 2026-03-31. Scope: all 5 modules (shatter, void, shield, proof, cloak), cryptographic analysis, side-channel analysis, protocol-level attacks.

Threat Model

QSMM is a library consumed by higher-level systems (e.g., darkfs). The threat model assumes:

  1. Adversary has full source code and understands all algorithms.
  2. Adversary does NOT have the user key (SecretKey).
  3. Storage medium is untrusted: adversary can read and write any block.
  4. No active monitoring: adversary cannot observe timing or memory during operation.
  5. Quantum adversary (for shield module): adversary has access to a fault-tolerant quantum computer.

Module Security Summary

Module Purpose Crypto Primitives Status
shield Post-quantum key wrapping ML-KEM (FIPS 203), ML-DSA (FIPS 204), XChaCha20-Poly1305 Secure
shatter Metadata mesh via secret sharing Shamir's SSS over GF(256), HMAC-SHA256 locator Secure
void Entropy masking for deniability ChaCha20 keyed stream, HMAC-SHA256 ownership tags See findings
proof ZK integrity proofs Halo2 zk-SNARK, MiMC-style hash See findings
cloak FHE metadata operations TFHE-rs Not audited (heavy dependency)

Findings

# Finding Severity Module Status
QA-1 MiMC hash uses 10 rounds (recommended ~91 for 255-bit field) MEDIUM proof/hasher.rs FIXED — increased to 91 rounds
QA-2 Multi-snapshot XOR: data region cancels when plaintext unchanged, revealing 1 bit per block MEDIUM void/namespace.rs FIXED — per-seal mask derivation
QA-3 Multi-snapshot XOR: zero/non-zero boundary in data region reveals approximate plaintext length within tier MEDIUM void/namespace.rs FIXED — per-seal mask derivation
QA-4 open_block allocation pattern differs between match/non-match (Vec with data vs empty Vec) LOW void/namespace.rs OPEN
QA-5 Shard collision resolution loop has no upper bound (safe in practice due to HMAC-SHA256 uniformity) LOW shatter/locator.rs Acceptable

Previously Fixed

# Finding Severity Status
VOID-2 XOR tag cancellation in multi-snapshot HIGH FIXED — random nonce per seal
VOID-3 Sealed block length leaked plaintext size HIGH FIXED — tier padding
PROOF-1 Commutative hash allowed sibling reordering HIGH FIXED — MiMC with asymmetric init (2left + 3right + 7)
PROOF-2 Unconstrained final mixing in proof circuit HIGH FIXED — mix_selector gate constrains hash = state + left^2 + right
CT-1 Hand-rolled constant_time_eq MEDIUM FIXED — replaced with subtle::ConstantTimeEq

Detailed Findings

QA-1: MiMC Insufficient Rounds (MEDIUM)

Module: proof/hasher.rs

The MiMC hash uses 10 rounds of (state + rc)^7. For a ~255-bit field (Pallas) with exponent 7, the standard recommendation is:

rounds >= ceil(log_7(field_size)) = ceil(255 / log2(7)) = 91 rounds

With 10 rounds, the algebraic degree is 7^10 = 2^28.07. While this still makes brute-force collision search infeasible (birthday bound is O(2^127.5) regardless of degree), the low algebraic degree leaves a narrower margin against algebraic attacks such as Grobner basis or interpolation.

Practical impact: Low. The hash is only used inside Halo2 circuits where the prover is assumed honest for integrity proofs. An adversary would need to find an actual collision (O(2^113) minimum) to exploit this. The code already notes "For production, replace with Poseidon."

Recommended fix: Increase to 91+ rounds, or replace with Poseidon hash (SNARK-friendly, well-analyzed).

QA-2/QA-3: Multi-Snapshot XOR Data Leak (MEDIUM)

Module: void/namespace.rs

The void mask is deterministic per (mask_seed, block_id). When the same block is sealed twice:

seal1 = [nonce1 | tag1 | padded_data] XOR mask_stream(seed, block_id)
seal2 = [nonce2 | tag2 | padded_data] XOR mask_stream(seed, block_id)

XOR = [nonce1 XOR nonce2 | tag1 XOR tag2 | 0...0]

The data+padding region cancels to zero because:

  • Same padded_data (same plaintext + same padding)
  • Same mask_stream (deterministic per block_id)

Information leaked:

  1. Whether the plaintext changed (data region all zeros = unchanged)
  2. Approximate plaintext length (zero/non-zero boundary within tier when data changes)

Mitigations already in place:

  • VOID-2 fix: nonce+tag region is non-zero (random nonces prevent tag cancellation)
  • Tier padding: exact length is hidden within tier boundaries

Recommended fix: Mix the per-seal random nonce into the mask derivation:

let mask = ChaCha20(HKDF(seed, nonce), block_id)

This makes the mask unique per seal, eliminating XOR cancellation entirely.

Cryptographic Analysis

HKDF Domain Separation

All HKDF info strings are unique and non-overlapping:

Domain String Module Purpose
qsmm-void-seed void/entropy.rs Mask seed derivation
qsmm-void-tag-key void/namespace.rs Ownership tag key
qsmm-shield-wrap-key shield/wrap.rs Key wrapping
qsmm-shard shatter/locator.rs Shard placement
qsmm-shard-collision shatter/locator.rs Collision resolution

Verdict: No domain collision. Keys derived for different purposes are cryptographically independent.

Shamir's Secret Sharing

  • Uses sharks crate over GF(256)
  • Threshold validated: k > 0, k <= n, n <= 255
  • Reconstruction requires exactly k shares; fewer shares reveal zero information (information-theoretic security)
  • Wrong threshold (wrong k) produces garbage, not the secret

Post-Quantum Key Wrapping (shield)

  • ML-KEM (FIPS 203) at three security levels (512/768/1024)
  • ML-DSA (FIPS 204) at three security levels (44/65/87)
  • Hybrid wrapping: ML-KEM shared secret -> HKDF -> XChaCha20-Poly1305
  • KEM ciphertext tampering produces random shared secret -> AEAD fails
  • Each wrap uses fresh KEM randomness (IND-CCA2)

Void Masking

  • ChaCha20(HKDF(user_key, hw_entropy, "qsmm-void-seed"), block_id) per block
  • XOR is self-inverse: mask = unmask
  • Stream is computationally indistinguishable from random (ChaCha20 is a PRF)
  • Known-plaintext attack: recovering one block's mask doesn't help with other blocks (different nonces)
  • Ownership tags use HMAC-SHA256 with nonce mixed in (prevents tag cancellation)
  • Tag comparison uses subtle::ConstantTimeEq

Memory Safety

  • #![deny(unsafe_code)] enforced crate-wide
  • All secret keys use Zeroize + ZeroizeOnDrop
  • Debug formatting redacts key material: SecretKey([REDACTED])
  • No panics reachable from untrusted input (all assert!() guard internal invariants)

Test Coverage

  • Property-based tests: proptest for SSS roundtrip, namespace isolation, masking determinism
  • Adversarial pentest suite: collision search, timing analysis, cross-namespace attacks
  • Deep audit tests: MiMC analysis, multi-snapshot XOR, avalanche, extreme inputs
  • Circuit soundness tests: MockProver for valid/invalid/tampered proofs

Attacks That Failed

  • MiMC collision search (4M pairs): no collisions found
  • Namespace cross-opening (50 namespaces): perfect isolation
  • Shard position clustering: uniformly distributed (chi-squared < 27.9)
  • Modular bias in shard offsets: < 10% deviation at 3-block worst case
  • KEM ciphertext tampering: AEAD correctly rejects
  • Cross-level KEM unwrap: correctly rejected
  • Wrap linkability: KEM ciphertexts are fully independent
  • bytes_to_fields encoding: verified injective for test inputs
  • Avalanche effect: ~50% bit flip on adjacent inputs (good diffusion)

There aren’t any published security advisories