Files
zooqueen fd3e1e88f4 quasar: harden weighted quorum cert envelope binding (5 red-team findings)
The crypto core (TupleHash256, FIPS 204/205 verify, weighted Merkle,
leaf binding, distinct-signer ordering, weight overflow guards) was
sound. The ENVELOPE binding was not. Fixes, smallest correct change,
no new fail-open paths:

F1 (CRITICAL) sub-quorum finality forgery — QuorumThreshold was
   unbound: Verify checked Sum(weight) >= attacker-chosen
   c.QuorumThreshold with no floor and no signature binding. Two-part
   fix:
   (a) bind quorum_threshold into the SIGNED message. Added
       QuorumThreshold to QuorumMessageEnvelope and into the
       QuorumConsensusMessage re-frame parts (quorum_message.go) — the
       quorum-cert-specific layer where qc_type / value_hash already
       live. NOT added to the shared ComputeRoundDigest (that round
       digest is consumed by non-quorum consensus rounds; threshold is
       not one of its axes). QuorumMessageForCert now fills the
       threshold from the cert, so honest signatures over the real
       quorum cannot be re-framed under a lowered one.
   (b) MANDATORY verifier floor: QuorumVerifierConfig.MinThreshold.
       Verify FAILS CLOSED if MinThreshold is unset (zero) and rejects
       any cert whose QuorumThreshold < MinThreshold. A cert may not
       assert its own security parameter.

F2 (HIGH) decode DoS — UnmarshalWeightedQuorumCert reserved capacity
   for an attacker-controlled uint32 SignerCount (0xFFFFFFFF ~= 446 GB).
   Cap SignerCount against the remaining buffer (wqcMinRecordBytes = 66)
   before the make, mirroring the existing step_count cap. O(1)
   rejection, no allocation.

F3 (HIGH) opaque-message trust / role-replay — Verify took a
   caller-supplied message. It now takes the chain envelope and rebuilds
   the position binding ITSELF via QuorumMessageForCert; the raw-message
   form is demoted to the internal verifyWithMessage helper. Folds in
   the LOW finding: Verify asserts envelope.ProofBackend ==
   cert.ProofBackendID() before building the message. QCType
   prepare->commit role-replay and every position axis are now caught at
   the rebuilt-message signature check. VerifyWeightedQuorumLeg updated
   to take the envelope.

F4 (MED) weighted-Merkle proof malleability — weightedProofShape gives
   identical shapes for different LeafCount (e.g. (0,3) and (0,4) both
   "RR"), so a record had multiple valid (LeafIndex,LeafCount) encodings
   -> byte-distinct certs, breaking dedup/equivocation. Bind each
   signer's (LeafIndex,LeafCount) into computeSignerCommitment; any
   malleation now changes the commitment and is rejected.

F5 (MED) mixed-scheme FIPS-context asymmetry — magnetar.ValidatorSign
   signs the empty FIPS-205 context while ML-DSA signs cfg.Context;
   Verify applied one context to all, so mixed certs only worked with
   ctx=nil. Added contextForScheme: ML-DSA -> cfg.Context, SLH-DSA ->
   empty context (the round-digest message carries the domain binding).

Tests: new quorum_redteam_test.go proves each PoC is now REJECTED
((a) forged below-floor threshold, (b) threshold-swap message binding,
(c) 0xFFFFFFFF SignerCount fast-reject, (d) position/QCType/value_hash
tamper, (e) non-canonical (LeafIndex,LeafCount)). Existing tests
updated for the envelope-based Verify signature + mandatory
MinThreshold. config: ProofFormatDirectWeightedQuorumV1 (0x30) and
VerifierDirectWeightedQuorumPQ (0x0030) wire constants for the cert.

Verify: go build ./protocol/quasar/... ok; go test
'Quorum|WeightedMerkle|QuasarCert|RoundDigest|Redteam' 154 PASS / 0
FAIL; gofmt clean; go vet clean; framework fail-open gate ok.
2026-06-21 14:08:05 -07:00
..