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.
78 lines
3.0 KiB
Go
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")
|
|
}
|
|
}
|