mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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.