Files
zooqueen b36d13ebd5 release(2026-06-pq-threshold-stack): add FULLY-PQ-AUDIT + STANDARD-ASSUMPTIONS
FULLY-PQ-AUDIT.md: per-production-path PQ-primitive ledger with file:line
evidence, complete classical-crypto reachability list, and the verdict.
Headline finding: QuasarCert.VerifyWithRealKeys required BLS-12-381 on the
finality path (contradicting the cert's pure-PQ contract) -- FOUND AND FIXED
under this audit (consensus: BLS optional + fail-closed >=1-leg guard,
regression-tested). Post-fix verdict: the production consensus/finality/
signature-verification envelope is FULLY POST-QUANTUM. All other classical
crypto (optional BLS fast-path, Corona DKG-ceremony Ed25519/X25519, Verkle
banderwagon point-decode, qzmq ML-KEM stub, gated EVM precompiles, superseded+
forbidden Groth16) is confirmed OFF the finality-cert verification path.

STANDARD-ASSUMPTIONS.md: the irreducible minimal cryptographic-assumption
ledger -- Module-LWE, Module-SIS, SLH-DSA SHAKE hash-family EU-CMA, FRI
proximity-gap soundness (staged), SHAKE/cSHAKE/KMAC as RO, honest-validator
BFT threshold. For each: dependent claims, why standard/accepted, parameter
set + NIST PQ category. The honest 'fully proven UNDER THESE assumptions'
bedrock; explicitly excludes pairing/DLOG/factoring/trusted-setup.
2026-06-21 15:25:24 -07:00

22 KiB

Fully-PQ Audit — Lux PQ Threshold Stack, release 2026-06-pq-threshold-stack

Question. Does every PRODUCTION security path in the release rest only on post-quantum assumptions? A single reachable pairing / discrete-log primitive on the PQ-finality verification path fails the test.

Scope. This audit walks each production security path, names the primitive, names its PQ assumption, and cites file:line evidence. It then enumerates every classical (non-PQ) primitive found in the stack and states its exact reachability. The audit is over the commits pinned in RELEASE-MANIFEST.toml. Evidence was obtained by reading the code, not the prose docs; where a doc disagrees with the code, the code wins and the drift is noted.

Headline. The sweep found one real classical-crypto dependency on a production finality-verification path — QuasarCert.VerifyWithRealKeys unconditionally required a valid BLS-12-381 aggregate, contradicting the cert's own "empty in pure-PQ" contract — and it has been fixed in this audit (BLS made one optional leg, with a fail-closed ≥1-verified-leg guard; regression-tested). Post-fix, the finality envelope is fully PQ. The finding and fix are documented in full in §2.1.

Method note. "Production security path" = the data flow a node executes to decide finality or to verify a consensus / identity / transaction signature. It is distinct from (a) setup ceremonies (DKG / resharing), (b) the transport/session layer, (c) EVM-state-witness verification, and (d) research-only / superseded code. The "fully PQ" verdict is a statement about the finality + signature verification envelope, not about every byte of classical crypto that exists anywhere in the tree.


1. Production security paths and their PQ assumptions

# Production path Primitive PQ assumption Evidence (file:line)
P1 Per-validator consensus signature (ML-DSA), full-node-verifiable quorum cert ML-DSA-44/65/87 (FIPS 204) Module-LWE + Module-SIS consensus/protocol/quasar/quorum_scheme.go:148 (defaultQuorumVerifiers[MLDSA65]=mldsaVerifier(mldsa.MLDSA65)) → pk.VerifySignatureCtx (:171) over crypto/mldsa
P2 Per-validator consensus signature (SLH-DSA), quorum cert SLH-DSA-192s/192f/256s (FIPS 205) Hash one-wayness; FORS + WOTS+ EU-CMA (SHAKE) quorum_scheme.go:152 (slhdsaVerifier(magnetar.ModeM192s)) → magnetar.VerifyCtx → circl/slhdsa.Verify (magnetar/ref/go/pkg/magnetar/verify.go:111)
P3 Weighted quorum certificate = finality evidence (full-node-verifiable mode) P1 ∧ P2 over a weighted-Merkle validator set Module-LWE/MSIS + hash one-wayness + Merkle/SHA-3 binding registry is ML-DSA + SLH-DSA only (quorum_scheme.go:140-153); WeightedQuorumCert.Verify rebuilds the message itself + per-signer FIPS verify + Merkle inclusion + weight floor (quorum_cert.go:246-290)
P4 QuasarCert cryptographic verify (engine/pq + QuantumPolicy wire) BLS (OPTIONAL) + Corona + ML-DSA legs Module-LWE (Corona/ML-DSA) — BLS optional, see §2.1 types.go:190 VerifyWithRealKeys; called by engine/pq/engine.go:131 + pkg/wire/policies.go:717. Fixed in §2.1: BLS now optional + fail-closed ≥1-leg.
P5 Threshold ML-DSA finality leg (Pulsar) Pulsar threshold ML-DSA (FIPS 204 byte-equal; BCC/CEF no-leak) Module-LWE + Module-SIS Pulsar pkg verify path carries no classical primitive (grep over pulsar/ref/go/pkg/ = empty); output is FIPS-204 ML-DSA bytes (PULSAR-C3, interop-tested CIRCL + pq-crystals)
P6 Threshold Module-LWE finality leg (Corona) Corona threshold (Ringtail/Raccoon, Module-LWE) Module-LWE Corona threshold signature output is Module-LWE; verify path carries no classical sig primitive (§2.3 covers the DKG-channel classical crypto, which is NOT this path)
P7 Hash-based finality leg (Magnetar certification) SLH-DSA (FIPS 205) certification over independent sigs Hash one-wayness; FORS + WOTS+ EU-CMA magnetar/.../standalone.go (PerValidatorKeypair/ValidatorSign/VerifyAggregateCert); production = N independent FIPS 205 sigs, no key sharing/reconstruction (call-graph proof §3)
P8 PQ STARK substrate (P3Q) — staged for finality compression FRI low-degree test over Goldilocks + Keccak Merkle FRI proximity-gap soundness; SHAKE/Keccak collision resistance p3q/Cargo.toml deps = sha3/hex/thiserror; Goldilocks P=0xFFFF_FFFF_0000_0001 (p3q-field/src/goldilocks.rs:15); unsafe_code=deny. Not yet on live finality path — see §4.
P9 EVM PQ-signature precompiles (tx verification) ML-DSA / SLH-DSA Module-LWE/MSIS / hash one-wayness dispatch to crypto/mldsa + crypto/slhdsa; classical precompiles profile-gated by contract.RefuseUnderStrictPQ (§2.5)
P10 Transport / session secrecy (network layer, NOT finality) ML-KEM-768 (FIPS 203), ZAP PQ handshake Module-LWE (ML-KEM) zap/quic/conn.go, zap/docs/SPEC-ZAP-PQ-v1.md; qzmq real backend qzmq/mlkem_integration.go::RealMLKEM768. §2.4: qzmq factory wires a stub.

The PQ-only registry (P3). The weighted quorum certificate's per-signer verifier registry contains exactly two families and no third:

defaultQuorumVerifiers = map[QuorumSchemeID]quorumVerifyFn{
    QuorumSchemeMLDSA44:    mldsaVerifier(mldsa.MLDSA44),
    QuorumSchemeMLDSA65:    mldsaVerifier(mldsa.MLDSA65),
    QuorumSchemeMLDSA87:    mldsaVerifier(mldsa.MLDSA87),
    QuorumSchemeSLHDSA192s: slhdsaVerifier(magnetar.ModeM192s),
    QuorumSchemeSLHDSA192f: slhdsaVerifier(magnetar.ModeM192f),
    QuorumSchemeSLHDSA256s: slhdsaVerifier(magnetar.ModeM256s),
}

(quorum_scheme.go:140-153) — no BLS/ECDSA/Ed25519/pairing verifier. A pure-PQ cert is the default constructor: BuildQuasarCertFromWeightedQuorum builds a cert "where the per-validator-signature surface IS the finality evidence … The other legs (BLS / Corona / Pulsar / Magnetar) are left empty" (quorum_quasarcert.go).


2. Classical crypto found, with exact reachability

2.1 BLS12-381 (pairing) — FINDING (CRITICAL, NOW FIXED): was mandatory on the VerifyWithRealKeys finality path

The defect (pre-fix). QuasarCert.VerifyWithRealKeys — the cryptographic cert verifier, wired into production at engine/pq/engine.go:131 and pkg/wire/policies.go:717 — unconditionally required a valid BLS-12-381 aggregate:

// (pre-fix) types.go:194
if len(c.BLS) == 0 { return false }        // BLS bytes mandatory
if blsAggPubKey == nil { return false }    // BLS key mandatory
if !bls.Verify(blsAggPubKey, blsSig, message) { return false }

Corona and ML-DSA legs were conditional (nil key → skip), but a BLS-empty cert was rejected, and VerifyWithRealKeysPolaris calls VerifyWithRealKeys first (types.go:283), so even the maximum-assurance Polaris path inherited the gate. The PQ engine added a second mandatory-BLS gate (engine/pq/engine.go:117, "no BLS aggregate verify key attached"). Net: no pure-PQ QuasarCert could be finalised through this surface; a quantum break of BLS-12-381 could forge any cert that passed it, regardless of the PQ legs.

This contradicted (a) the cert's own documented contract — BLS []byte // … empty in pure-PQ (types.go:69), HasClassicalFastPath (types.go:95-103), "a pure-PQ cert has no BLS bytes" (types.go:84-86); (b) METASTABLE_SIGNED_ QUERIES.md ("No classical primitive in the finality path. BLS is not needed."); and (c) the sibling VerkleWitness verifier (witness.go:227-252), which already profile-gates BLS out under strict-PQ. The build side (ComposePolaris, BuildQuasarCertFromWeightedQuorum) builds BLS-optional certs; the verify side rejected them. The two surfaces disagreed, and the verify side gates finality.

The fix (this audit). BLS is now one optional leg, verified iff the cert carries BLS bytes, exactly like Corona/ML-DSA — with a fail-closed guard so removing the mandatory gate cannot let an evidence-free cert pass:

// (post-fix) types.go VerifyWithRealKeys — leg-count semantics
verified := 0
if len(c.BLS) > 0 { /* require key; bls.Verify; */ verified++ }      // OPTIONAL
if rtGroupKey != nil { /* Corona verify; */ verified++ }
if len(c.MLDSARollup) > 0 { /* ML-DSA verify; */ verified++ }
return verified > 0   // fail-closed: never finalise on absence of evidence

The engine pre-check (engine/pq/engine.go:117) was likewise relaxed from "BLS key required" to "≥1 verify key required (BLS, Corona, or ML-DSA)". The QuantumPolicy SigQuasar wire format (policies.go:684-688, 523-550) keeps its by-design BLS+Corona dual shape — that is a specific hybrid policy, not the pure-PQ path; pure-PQ finality runs through the WeightedQuorumCert (P3) or a Pulsar/Polaris cert, not SigQuasar.

Regression tests (consensus/protocol/quasar/pure_pq_cert_test.go, both PASS):

  • TestVerifyWithRealKeys_PurePQ_NoBLS — a pure-PQ cert (BLS empty, one real ML-DSA-65 leg) verifies with a nil BLS key; a tampered leg is rejected.
  • TestVerifyWithRealKeys_FailClosed_NoLeg — an empty cert and a rollup-bytes-without-keys cert are both rejected (no fail-open).

Full protocol/quasar suite green (ok … 22.0s); engine/pq green; full consensus build clean.

Verdict. Pre-fix: FAIL (mandatory pairing on the VerifyWithRealKeys finality path). Post-fix: BLS is (a) optional ✓, (b) labeled classical ✓, (c) not required for PQ finality ✓ — a pure-PQ cert finalises on its lattice/hash leg alone. PASS (after fix).

2.2 TRAP — quasar/bls.go is NOT pairing crypto (it is KMAC256)

consensus/protocol/quasar/bls.go is named "BLS" but implements KMAC256 (SP 800-185, a PQ symmetric MAC) for DAG vertex ordering / event-horizon attestations. CertBundle.BLSAgg is a KMAC256 output (bls.go:50, computed kmac256(blsKey, c.Message, len, customQuasarEvent HorizonBLSMAC)); the banner states it "operates independently of block finality signatures" (bls.go:14-16). This is post-quantum. Auditors must not flag BLSAgg/bls.go as classical crypto — disambiguate field-name "BLS" (KMAC leg) from import crypto/bls (real pairing, §2.1). PASS (PQ).

2.3 Ed25519 + X25519 (Corona) — DKG / resharing ceremony channel auth. NOT the signature-verify path.

  • Where. corona/reshare/pairwise.go (X25519 KEX + Ed25519-signed ephemerals, lines 28-32, 57, 89, 116), corona/reshare/complaint.go (Ed25519 complaint sigs, 55, 161-175), corona/dkg2/complaint.go (Ed25519, 57, 164-178). corona/keyera/bootstrap_pedersen.go:560 only mentions ed25519 in a comment — no usage.
  • Reachability. Authenticates the point-to-point channels of the resharing / DKG ceremony (MitM defense on setup messages). NOT the Corona threshold-signature output. A finality verifier checking a Corona threshold signature (Module-LWE) never touches Ed25519/X25519. Confirmed: Corona's classical crypto is confined to reshare/ + dkg2/; the steady-state sign/verify path is Module-LWE only. (Pulsar pkg verify path is fully clean — no classical primitive at all.)
  • PQ option exists. The kernel documents "X25519 in the classical baseline, X25519 ‖ ML-KEM-768 hybrid in the post-quantum mode … Ed25519 / ML-DSA-65" wire-identity (corona/reshare/pairwise.go:12; corona/papers/lp-073-pulsar/sections/06-resharing.tex:124).
  • Residual. A quantum adversary breaking Ed25519/X25519 could MitM a resharing ceremony (not forge a finality cert, not forge a steady-state signature). Even in "PQ hybrid" mode the Ed25519 wire-identity signature on the ephemeral stays classical unless the ML-DSA-65 wire-identity option is selected.
  • Verdict. Not on the PQ-finality path. PASS for finality; FINDING (medium) for ceremony channel auth: default it to ML-DSA-65 wire-identity + X25519‖ML-KEM-768 hybrid to make the resharing ceremony fully PQ.

2.4 ML-KEM transport — real backend exists, but qzmq factory wires the stub

qzmq/mlkem_integration.go::RealMLKEM768 is a correct FIPS 203 wrapper over crypto/mlkem (lines 13-63), BUT qzmq/crypto.go::getKEM(MLKEM768) returns the stub MLKEM768Type (random bytes, marked (stub) at :282, encaps rand.Read(ss)); RealMLKEM768 is unreferenced outside its file/tests. Reachability: transport/session, NOT finality; canonical inter-node transport is ZAP (zap/quic/conn.go), which carries its own PQ handshake; qzmq is a secondary ZMQ transport. FINDING (medium): route qzmq getKEM MLKEM768/HybridX25519MLKEM768 to RealMLKEM768 before qzmq carries a confidential session. As shipped, a qzmq "ML-KEM" session derives a random (non-agreed) secret → a correctness wall (non-functional), not a secret downgrade to classical. Off the finality envelope.

2.5 Classical EVM precompiles (secp256k1, P-256, Ed25519, BN254 pairing) — profile-gated.

Classical signature/pairing precompiles ship for EVM compatibility; under the strict-PQ profile they are refused at the top of Run() by the single gate contract.RefuseUnderStrictPQ (decomplected one-function gate). The consensus-side analog is ChainSecurityProfile.AcceptsValidatorScheme (consensus/config/validator_scheme.go), which refuses classical NodeID schemes (0x90) on strict-PQ "regardless of the operator flag". Classical tx crypto is opt-in per profile, not on the strict-PQ finality path. PASS.

2.6 Groth16 / BN254 ML-DSA rollup — SUPERSEDED + FORBIDDEN under strict-PQ.

The legacy succinct backend wrapped N ML-DSA-65 verifications in Groth16 over BLS12-381 (QuasarCert.MLDSARollup // … STARK or Groth16, types.go:73). It is superseded — the default MLDSARollup realization is ProofBackendDirectWeightedQuorum (full-node-verifiable, no SNARK, quorum_cert.go:1014-1015); STARK compression is a "SEPARATE future backend (ProofBackendID 0x10/0x20)" (quorum_cert.go:31-32). It is forbidden under strict-PQ by coherent gates: ProofPolicyGroth16BN254Forbid (config/pq_mode.go:431), ProofBackendGroth16WrapForbid (:551), ProofFormatGroth16WrappedForbid (config/security_profile.go:141), config/profiles.go:247 ForbidClassicalSNARKs:true, and BridgeProfile.RefuseGroth16Wrap() — "the gate every Groth16 verification call site MUST consult" (config/bridge_profile.go:382-393, AllowsGroth16Wrap:false on strict). No groth16.Verify/bn254 call is implemented on the finality path; gnark-crypto is a transitive-only indirect dep (via geth), present in consensus/examples/02-ai-payment/go.sum (example app) and go.mod indirect, not invoked. PASS (superseded + forbidden). Doc-drift note: the MLDSARollup field comment and consensus LLM.md still describe the 3-tuple Groth16 framing; the code is the 5-leg PQ-default. Update to "(direct WQC default; STARK staged; Groth16 forbidden under strict-PQ)".

2.7 Banderwagon / Bandersnatch curve (Verkle witness) — point-decode only; hash-based proof; no in-repo production caller.

consensus/protocol/quasar/witness.go imports crypto/banderwagon (:14) and decodes a Verkle commitment as a curve point (commitment.SetBytes, :344) on the strict-PQ branch (verifyVerkleCommitment, reached at :234 under IsPulsarM()). But: (a) the proof check verifyIPAOpening (:510) is hash-based — despite its name it re-derives a 48-byte cSHAKE256/KMAC digest (LUX-VERKLE-IPA-PROOF-V1) and constant-time-compares; it is NOT a curve-based IPA soundness argument. The only classical-curve operation that executes is the banderwagon.Element.SetBytes() point decode / subgroup check. (b) VerifyStateTransition/NewVerkleWitness have no production (non-test) caller inside the consensus repo (production callers, if any, live in node/EVM repos). (c) This is an EVM-state-transition witness, not the consensus signature certificate (P3/P4). Verdict: off the finality-cert verification path; the residual is a curve point-decode (not a soundness check) on a state-witness with no in-repo production caller. FINDING (low): the strict-PQ Verkle commitment should use a hash-based vector commitment (e.g. a SHA-3 Merkle / Verkle-over-hash) so even the point-decode leaves the strict-PQ input path; track for the node/EVM Verkle integration.


3. Spot-confirmation of the per-primitive claims that gate the verdict

  • M2 — production reconstructs no shared secret (call-graph proof). assembleSignatureBytes (the Lagrange master reconstruction) has exactly one caller — magnetar/ref/go/pkg/magnetar/thbsse.go:952, inside Combine (THBS-SE research path). slhSignAtom has exactly one caller (thbsse_assemble.go:234). Neither is reachable from the production entry points (ValidatorSign/Sign/VerifyAggregateCert). Production holds each validator's own circl key and reconstructs nothing. Confirmed.
  • P3Q is PQ-only at the dependency level. Cargo.lock closure contains zero curve/pairing crates (bn254|bls12|ark-|halo2|kzg|secp256| curve25519-dalek|pairing|blstrs|group|ff all absent); vectors/forbidden_imports.txt + scripts/lint_forbidden.sh ban bn254/kzg/groth16/plonk/halo/poseidon; CI lint enforces absence. 123 tests pass. Confirmed.
  • Pulsar / Magnetar source carry no curve/pairing/secp/ed25519. grep over pulsar/ref/go/pkg/ and magnetar/ref/go/pkg/ = empty (the "Schnorr" hits in Pulsar are lattice Maurer/sigma PoKs over R_q, not EC-Schnorr; the "ed25519" hits are doc-comments for an opaque accountability identity key, no crypto/ed25519 import). Confirmed.

4. The one honest scope line: P3Q is staged, not live, for finality

Today's finality is full-node-verifiable (P3): a node downloads the per-validator records and checks each PQ signature itself. It is not yet STARK-compressed. The P3Q circuit that would verify N FIPS signatures inside a Keccak-AIR is staged, not built: the six p3q-zchain circuits are audit-gated stubs that return Err(not_yet_audit_gated(...)) and whose *Air structs carry no Air trait impl, so they cannot be passed to prove_air/verify_air (P3Q-C7, confirmed). This is disclosed, not overclaimed. Consequence: the live finality path uses P1/P2/P3 (direct PQ signature verification), which is fully PQ; P3Q's FRI assumption (P8) is on the roadmap finality-compression path, not today's verification.


5. Verdict

After the §2.1 fix, the production consensus / finality / signature- verification envelope is FULLY POST-QUANTUM. Both finality-cert verification surfaces — the full-node-verifiable WeightedQuorumCert (P3) and the general QuasarCert.VerifyWithRealKeys (P4) — now verify a pure-PQ cert with no BLS, against ML-DSA (FIPS 204, Module-LWE/MSIS) and SLH-DSA (FIPS 205, hash one-wayness) only. No BLS/pairing/ECDSA/Ed25519 verifier is required on any finality path; BLS is one optional fast-path leg, empty in pure-PQ.

The one fully-PQ defect found, and its disposition:

Defect Path Pre-fix Disposition
VerifyWithRealKeys required BLS-12-381 (pairing) engine/pq/engine.go:131, pkg/wire/policies.go:717 FAIL — mandatory pairing on finality FIXED in this audit: BLS optional + fail-closed ≥1-leg guard; regression-tested (pure_pq_cert_test.go, both PASS); full suite + build green

Every classical primitive found, with reachability (post-fix):

Classical primitive Where Reachability On PQ-finality path?
BLS12-381 (pairing) crypto/bls via quasar.go Signer; VerifyWithRealKeys Optional QuasarCert.BLS fast-path leg; empty in pure-PQ (after fix) No (optional, labeled, not required)
Ed25519 + X25519 corona/reshare/, corona/dkg2/ DKG/resharing ceremony channel auth (MitM defense) No (setup ceremony, not sig-verify)
Banderwagon (curve) quasar/witness.go Verkle Point-decode only (proof is hash-based); no in-repo production caller; EVM-state witness No (state witness, not the signature cert)
ML-KEM stub (qzmq) qzmq/crypto.go getKEM Transport/session (secondary; ZAP is canonical); non-functional stub No (transport, not finality)
secp256k1 / P-256 / Ed25519 / BN254 precompiles EVM Profile-gated; refused under strict-PQ by contract.RefuseUnderStrictPQ No (opt-in EVM compat, gated off)
Groth16 / BN254 rollup legacy MLDSARollup backend Superseded (default = direct WQC) + forbidden under strict-PQ No (superseded + profile-forbidden)

Findings to act on (none block the post-fix fully-PQ-finality verdict):

  1. (critical, FIXED) VerifyWithRealKeys BLS-mandatory. Fixed + regression- tested in this audit. See §2.1.
  2. (medium) qzmq ML-KEM stub. Route getKEM to RealMLKEM768. Off the finality path; non-functional stub, not a classical downgrade.
  3. (medium) Corona resharing ceremony channel auth defaults to classical. Default to ML-DSA-65 wire-identity + X25519‖ML-KEM-768 hybrid. Off the finality + steady-state signature-verify path.
  4. (low) Verkle commitment point-decode. Move the strict-PQ Verkle commitment to a hash-based vector commitment so even the point-decode leaves the strict-PQ input path. EVM-state witness, no in-repo production caller.
  5. (informational) Doc drift. types.go:73 MLDSARollup comment + consensus LLM.md still describe the 3-tuple Groth16 framing; the code is the 5-leg PQ-default WQC.

Bottom line. The sweep found exactly one "reachable pairing on the finality path" — VerifyWithRealKeys's mandatory BLS — and it was fixed under this audit with a fail-closed guard and regression tests. The remaining classical crypto is intentional (optional BLS fast-path; gated EVM compat) or peripheral (ceremony channel auth; EVM-state-witness point-decode; transport), none on the finality-cert verification path. Post-fix: the production security envelope is fully PQ.