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.
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:560only 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, insideCombine(THBS-SE research path).slhSignAtomhas 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.lockclosure contains zero curve/pairing crates (bn254|bls12|ark-|halo2|kzg|secp256| curve25519-dalek|pairing|blstrs|group|ffall absent);vectors/forbidden_imports.txt+scripts/lint_forbidden.shban 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/andmagnetar/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, nocrypto/ed25519import). 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):
- (critical, FIXED)
VerifyWithRealKeysBLS-mandatory. Fixed + regression- tested in this audit. See §2.1. - (medium) qzmq ML-KEM stub. Route
getKEMtoRealMLKEM768. Off the finality path; non-functional stub, not a classical downgrade. - (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.
- (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.
- (informational) Doc drift.
types.go:73MLDSARollupcomment + consensusLLM.mdstill 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.