mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Re-targeted onto SHIPPED consensus v1.35.30 (cloud go.mod pin: pulsar
v1.9.0, threshold v1.12.1), the surface cloud actually embeds.
Item7(a) - unbounded allocation DoS (MINIMAL, NON-BREAKING):
PulsarRoundSigner.Finalize built the signer bitmap via
pulsarlib.CanonicalSignerSet + singletonBitmap, both of which size a
[]byte at (PartyID/8+1) from an attacker-controlled uint32 PartyID with
no upper bound. A single Round2 partial naming PartyID near
math.MaxUint32 drives a ~512MiB allocation. Fix, entirely consensus-side
(pulsarlib's CanonicalSignerSet signature is left untouched so v1.9.0 need
not change and nothing has to propagate):
- Finalize pre-filters the WHOLE partials slice, rejecting any
PartyID >= s.ValidatorSetSize with the new typed ErrPartyIDOutOfRange
BEFORE CanonicalSignerSet or any allocation. The whole slice (not just
the threshold-chosen subset) is checked so a hostile PartyID cannot
hide behind the first-threshold cut.
- singletonBitmap now takes validatorSetSize and bound-checks before its
allocation (defense-in-depth for any future direct caller).
- PulsarRoundSigner.ValidatorSetSize already existed as a declared field
for exactly this bound; it is now load-bearing.
Severity is LOW for cloud: the embedding's ingestLeg (driver.go) already
rejects any leg unless idx==PartyID and the author is a registered voter,
so PartyID is bounded upstream — this is library hygiene closing the gap
for any caller without that upstream guard.
New tests (dos_regression_test.go): oversized PartyID rejected on the real
Round2 -> Finalize path; rejected even when it would be excluded from the
chosen subset; in-range happy path still succeeds with a bitmap bounded by
ValidatorSetSize.
Item7(b) - structural Verify is not the finalization authority: confirmed
the shipped v1.35.30 surface is already SOUND — QuasarCert.VerifyUnderPolicy
(cert_policy_verify.go) is the policy-driven cryptographic authority and is
the path pkg/wire quantum finality calls (policies.go). The structural
QuasarCert.Verify(validators) has exactly one non-test caller, engine.go's
processBlock self-consistency gate on a cert THIS process just generated —
not a trust decision on adversarial bytes. Hardened its doc comment to say
so and to point at VerifyUnderPolicy, and added a regression lock
(verify_authority_test.go): a forged-but-shaped cert passes structural
Verify but is rejected by VerifyUnderPolicy (reaching and failing the BLS
signature check under a (PQ-off,Hybrid) policy) and by VerifyWithRealKeys /
VerifyWithRealKeysPolaris.
No go.mod/go.sum change. Built and tested green against the clean v1.35.30
dependency set (threshold v1.12.1 / pulsar v1.9.0) — no age go.sum
landmine on this surface.
52 lines
2.6 KiB
Go
52 lines
2.6 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// errors.go -- typed errors for the Pulsar cert profile. These are the
|
|
// fallback / retry signals the Quasar orchestrator matches against to decide
|
|
// whether to fall back to Corona, retry with a fresh nonce, or slash.
|
|
|
|
package pulsar
|
|
|
|
import "errors"
|
|
|
|
var (
|
|
// ErrNonceCertPoolEmpty signals the background NonceCert pool is empty.
|
|
// The orchestrator MUST fall back to Corona (or retry once the pool
|
|
// refills); the hot path NEVER generates a nonce inline.
|
|
ErrNonceCertPoolEmpty = errors.New("quasar/pulsar: NonceCert pool empty; fall back to Corona")
|
|
|
|
// ErrProfileNotReady signals the secret-bearing FIPS 204 signature-assembly
|
|
// core is not registered (pulsar.ProductionBCCSigningReady() is false: the
|
|
// BCC/CEF ZK proofs are fail-closed). Finalize still returns a
|
|
// structurally-complete ConsensusCert with an EMPTY Signature; the
|
|
// orchestrator falls back to Corona.
|
|
ErrProfileNotReady = errors.New("quasar/pulsar: BCC signing core not registered; fall back to Corona")
|
|
|
|
// ErrSessionFieldUnset rejects a PulsarSession with an all-zero
|
|
// security-relevant root (unbound field). Signing under it would allow
|
|
// cross-round replay.
|
|
ErrSessionFieldUnset = errors.New("quasar/pulsar: session has an unset security-relevant field")
|
|
|
|
// ErrNonCanonicalNonce signals the supplied nonce id is not the canonical
|
|
// pool selection for this session. Every signer recomputes the canonical
|
|
// index and refuses a non-canonical nonce (anti-grind).
|
|
ErrNonCanonicalNonce = errors.New("quasar/pulsar: non-canonical nonce for session")
|
|
|
|
// ErrInsufficientShares signals fewer collected shares than the threshold.
|
|
ErrInsufficientShares = errors.New("quasar/pulsar: fewer shares than threshold")
|
|
|
|
// ErrNoNonceCert signals Round2/Finalize were called without a bound
|
|
// Round1 (no NonceCert).
|
|
ErrNoNonceCert = errors.New("quasar/pulsar: no bound NonceCert (call Round1 first)")
|
|
|
|
// ErrPartyIDOutOfRange signals a partial names a PartyID that cannot
|
|
// belong to the configured ValidatorSetSize. Finalize rejects such a
|
|
// partial BEFORE any PartyID-sized bitmap allocation: pulsarlib's
|
|
// CanonicalSignerSet / singletonBitmap size a []byte at (PartyID/8+1),
|
|
// so an unbounded attacker-controlled uint32 PartyID would drive an
|
|
// unbounded (~512MiB from one MaxUint32) allocation — a memory-
|
|
// exhaustion DoS. Bounding PartyID here closes it without touching the
|
|
// pulsarlib CanonicalSignerSet signature.
|
|
ErrPartyIDOutOfRange = errors.New("quasar/pulsar: partyID out of validator set range")
|
|
)
|