Files
consensus/protocol/quasar/verify_authority_test.go
zeekay b4c8129831 fix(quasar/pulsar): bound PartyID before bitmap alloc; lock structural-Verify invariant
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.
2026-07-08 07:32:00 -07:00

78 lines
3.0 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// verify_authority_test.go -- Item7(b) regression lock (shipped v1.35.30
// surface): the structural QuasarCert.Verify is NOT the finalization
// authority; QuasarCert.VerifyUnderPolicy (the policy-driven cryptographic
// verifier that pkg/wire's quantum finality actually calls) is. This proves
// it by constructing a cert that is correctly SHAPED (every leg the
// structural gate demands is non-empty) but entirely FORGED (garbage bytes),
// showing Verify accepts it while VerifyUnderPolicy — and VerifyWithRealKeys
// — reject it.
package quasar
import (
"testing"
"github.com/luxfi/consensus/config"
"github.com/luxfi/crypto/bls"
)
// TestQuasarCertVerify_StructuralOnly_IsNotFinalizationAuthority locks the
// invariant documented on QuasarCert.Verify.
func TestQuasarCertVerify_StructuralOnly_IsNotFinalizationAuthority(t *testing.T) {
// Forged-but-shaped: BLS + Corona + MLDSARollup all present (garbage),
// which is exactly what the structural gate checks.
forged := &QuasarCert{
BLS: []byte("not-a-real-bls-signature-at-all"),
Corona: []byte{0x01, 0x02, 0x03},
MLDSARollup: []byte{0x04, 0x05, 0x06},
}
// (1) The structural gate passes it.
if !forged.Verify(nil) {
t.Fatal("expected structural Verify() to pass a forged-but-shaped cert")
}
// (2) The production authority rejects it. Policy (PQ-off, Hybrid)
// requires exactly the BLS leg; a real (fresh) BLS key is supplied,
// so VerifyUnderPolicy reaches the BLS signature check and fails
// there — the forged BLS bytes verify under no key.
sk, err := bls.NewSecretKey()
if err != nil {
t.Fatalf("bls.NewSecretKey: %v", err)
}
pk := sk.PublicKey()
policy := config.CertPolicy{Mode: config.CertModeOff, Variant: config.CertVariantHybrid}
if got := policy.RequiredLegs(); len(got) != 1 || got[0] != config.LegBLS {
t.Fatalf("precondition: (PQ-off,Hybrid) RequiredLegs = %v, want [LegBLS]", got)
}
keys := CertKeys{BLS: pk}
if forged.VerifyUnderPolicy([]byte("some finality message"), policy, keys) {
t.Fatal("VerifyUnderPolicy (the production authority) must reject a cert that only passes the structural gate")
}
// (3) The lower-level real-key verifier rejects it too.
if forged.VerifyWithRealKeys([]byte("some finality message"), pk, nil, nil) {
t.Fatal("VerifyWithRealKeys must reject a cert that only passes the structural gate")
}
if forged.VerifyWithRealKeysPolaris([]byte("some finality message"), pk, nil, nil, nil, nil) {
t.Fatal("VerifyWithRealKeysPolaris must reject a cert that only passes the structural gate")
}
}
// TestQuasarCertVerify_NilAndEmptyCert confirms the structural gate itself
// fails closed on degenerate inputs.
func TestQuasarCertVerify_NilAndEmptyCert(t *testing.T) {
var nilCert *QuasarCert
if nilCert.Verify(nil) {
t.Fatal("nil *QuasarCert must not verify")
}
if (&QuasarCert{}).Verify(nil) {
t.Fatal("empty QuasarCert (no legs) must not verify")
}
}