mirror of
https://github.com/luxfi/pulsar.git
synced 2026-07-27 06:55:04 +00:00
pulsar: origin-auth safe-by-default — fail-closed on nil verifier (RED MEDIUM nit #1)
AggregateBCC/AggregateBCCWithBlame previously aggregated WITHOUT origin
authentication when auth==nil (SetIdentity opt-in), so a production caller that
forgot to wire the verifier silently reverted to the exclude-honest-victim
footgun. Now:
- auth == nil => ErrOriginAuthRequired (FAIL-CLOSED), matching the nonce
ledger's no-fail-open posture.
- the unauthenticated path is reachable ONLY via the EXPLICIT opt-out sentinel
UnauthenticatedAggregation (trusted-channel / test). The sentinel panics if
ever invoked as a real verifier (fail loud, never silent).
- DistributedBCCSigner.FinalizeWithBlame forwards d.idVerify, so a signer with
no SetIdentity refuses FAIL-CLOSED too; opt out with
SetIdentity(nil, UnauthenticatedAggregation).
Test (origin_auth_gate_test.go): the DEFAULT (nil verifier) free-fn AND signer
paths refuse with ErrOriginAuthRequired; the explicit opt-out and a real verifier
both aggregate to a FIPS-valid signature; the sentinel is not a usable verifier.
Trusted in-memory ceremonies (runBCCCeremony, talus, sub-quorum, sound-proof,
no-reconstruct behavioural) opt out explicitly. GATE B / RED MEDIUM flip /
capstone / no-reconstruct all stay green.
This commit is contained in:
@@ -322,12 +322,14 @@ func TestRED_PoC_MEDIUM_CannotFrameOrExcludeHonestVictim(t *testing.T) {
|
||||
t.Fatalf("RED MEDIUM (impersonation): honest victim blamed off a forged victim-authored partial")
|
||||
}
|
||||
|
||||
// (3) CONTRAST — the verifier is load-bearing. With NO identity verifier the
|
||||
// same front-run EXCLUDES the victim (the forged malformed partial occupies
|
||||
// the slot; the real one is dropped as a duplicate) -> t-1 valid ->
|
||||
// ErrInsufficientSigners. No blame is emitted either way (blame is gated on
|
||||
// auth), but exclusion-resistance REQUIRES the verifier.
|
||||
_, _, blamesNoAuth, errNoAuth := AggregateBCCWithBlame(f.params, f.setup, agg.evalPoints, quorum, 0, nil,
|
||||
// (3) CONTRAST — the verifier is load-bearing. On the EXPLICIT unauthenticated
|
||||
// opt-out (UnauthenticatedAggregation) the same front-run EXCLUDES the
|
||||
// victim (the forged malformed partial occupies the slot; the real one is
|
||||
// dropped as a duplicate) -> t-1 valid -> ErrInsufficientSigners. No blame
|
||||
// is emitted either way (blame is gated on auth), but exclusion-resistance
|
||||
// REQUIRES a real verifier. (A bare nil verifier is refused FAIL-CLOSED —
|
||||
// see TestGATE_OriginAuth_*; here we deliberately drive the opt-out path.)
|
||||
_, _, blamesNoAuth, errNoAuth := AggregateBCCWithBlame(f.params, f.setup, agg.evalPoints, quorum, 0, UnauthenticatedAggregation,
|
||||
nil, msg, agg.c, &agg.cHat, agg.w1, sid, partials[victim].NonceID, threshold, input)
|
||||
if !errors.Is(errNoAuth, ErrInsufficientSigners) {
|
||||
t.Fatalf("contrast: expected the un-authenticated path to drop to t-1 (front-run exclusion), got %v", errNoAuth)
|
||||
|
||||
@@ -494,8 +494,14 @@ type IdentitySigner interface {
|
||||
// partial to its true producer; verifier authenticates OTHER nodes' partials
|
||||
// when this node aggregates, so a forged partial stamped with a victim's PartyID
|
||||
// is dropped (never blamed/excluded). Both must be set for authenticated blame;
|
||||
// production wires the validator's identity key + the validator-set verifier
|
||||
// here. Returns the signer for chaining.
|
||||
// production wires the validator's identity key + the validator-set verifier here.
|
||||
//
|
||||
// SAFE BY DEFAULT (RED MEDIUM). A signer that NEVER calls SetIdentity has
|
||||
// idVerify == nil, and FinalizeWithBlame then refuses to aggregate FAIL-CLOSED
|
||||
// (ErrOriginAuthRequired) — a forgotten verifier can no longer silently revert to
|
||||
// the unauthenticated exclude-honest-victim footgun. A trusted-channel ceremony
|
||||
// that deliberately wants the unauthenticated path opts OUT explicitly with
|
||||
// SetIdentity(nil, UnauthenticatedAggregation). Returns the signer for chaining.
|
||||
func (d *DistributedBCCSigner) SetIdentity(signer IdentitySigner, verifier AbortSignatureVerifier) *DistributedBCCSigner {
|
||||
d.idSigner = signer
|
||||
d.idVerify = verifier
|
||||
@@ -698,6 +704,10 @@ func (d *DistributedBCCSigner) Finalize(r1 SignRound1, partials []Partial) (Aggr
|
||||
// empty for the caller's identity layer (TranscriptForComplaint gives the
|
||||
// to-be-signed bytes). A VALID-sigma WRONG-z is NOT attributed here (BDLOP
|
||||
// residual — share_commit.go); it remains a liveness fault, never a forgery.
|
||||
//
|
||||
// ORIGIN-AUTH SAFE-BY-DEFAULT (RED MEDIUM): it forwards this signer's idVerify,
|
||||
// so a signer with no SetIdentity (idVerify == nil) is refused FAIL-CLOSED with
|
||||
// ErrOriginAuthRequired (see AggregateBCCWithBlame / SetIdentity).
|
||||
func (d *DistributedBCCSigner) FinalizeWithBlame(r1 SignRound1, partials []Partial) (Aggregate, ConsensusCert, []AbortEvidence, error) {
|
||||
if !d.IsAggregator() {
|
||||
return Aggregate{}, ConsensusCert{}, nil, ErrAlgNotAggregator
|
||||
@@ -793,6 +803,38 @@ func authenticatePartial(p Partial, expectedAuthor NodeID, epoch uint64, sid, no
|
||||
return VerifySignedProtocolMessage(&m, v)
|
||||
}
|
||||
|
||||
// ErrOriginAuthRequired is returned by AggregateBCC / AggregateBCCWithBlame when
|
||||
// no origin-authentication verifier is wired (auth == nil). It is the safe-by-
|
||||
// default refusal (RED MEDIUM): a caller that simply FORGOT to wire the verifier
|
||||
// must NOT silently fall back to unauthenticated aggregation (the exclude-honest-
|
||||
// victim footgun) — it fails CLOSED, matching the nonce ledger's no-fail-open
|
||||
// posture. A caller that genuinely wants the unauthenticated path opts OUT
|
||||
// explicitly with UnauthenticatedAggregation.
|
||||
var ErrOriginAuthRequired = errors.New("pulsar: aggregation requires an origin-authentication verifier — pass an AbortSignatureVerifier (DistributedBCCSigner.SetIdentity), or UnauthenticatedAggregation to explicitly opt out (trusted-channel / test only)")
|
||||
|
||||
// unauthenticatedAggregation is the concrete type of the UnauthenticatedAggregation
|
||||
// opt-out sentinel. It is NEVER invoked as a verifier: AggregateBCCWithBlame
|
||||
// branches on identity equality (auth == UnauthenticatedAggregation) and takes
|
||||
// the no-origin-auth path WITHOUT calling any method on it. Its method therefore
|
||||
// PANICS — reaching it means the sentinel was mistaken for a real verifier, and a
|
||||
// security primitive must fail LOUD rather than silently accept or drop partials.
|
||||
// The type is unexported, so the ONLY way to obtain it is the exported var below.
|
||||
type unauthenticatedAggregation struct{}
|
||||
|
||||
func (unauthenticatedAggregation) VerifyAbortSignature(NodeID, []byte, []byte) bool {
|
||||
panic("pulsar: UnauthenticatedAggregation is an opt-out sentinel, not a usable verifier")
|
||||
}
|
||||
|
||||
// UnauthenticatedAggregation is the EXPLICIT opt-OUT for the AggregateBCC /
|
||||
// AggregateBCCWithBlame origin-authentication gate. Pass it as auth to
|
||||
// ACKNOWLEDGE that partials are aggregated WITHOUT origin authentication — no
|
||||
// blame is produced and a forged partial stamped with a victim's slot cannot be
|
||||
// distinguished from the victim's own. It exists ONLY for trusted-channel test
|
||||
// ceremonies and the structural no-reconstruct proofs; PRODUCTION wires a real
|
||||
// AbortSignatureVerifier (via DistributedBCCSigner.SetIdentity). A bare nil auth
|
||||
// is refused FAIL-CLOSED (ErrOriginAuthRequired) — opting out must be deliberate.
|
||||
var UnauthenticatedAggregation AbortSignatureVerifier = unauthenticatedAggregation{}
|
||||
|
||||
// AggregateBCC is the free-function aggregation surface: ANY process
|
||||
// holding the public setup, the challenge c, the public target w1, and the
|
||||
// collected z-partials can produce the FIPS 204 signature WITHOUT any
|
||||
@@ -818,16 +860,30 @@ func AggregateBCC(params *Params, setup *AlgSetup, evalPoints []uint32, quorum [
|
||||
// fewer than t DISTINCT signers. Malformed ZShares are rejected without
|
||||
// panicking (unpackPolyVecChecked). It holds NO share, sk, or seed.
|
||||
//
|
||||
// ORIGIN AUTHENTICATION (RED MEDIUM). quorum maps PartyID → expected validator
|
||||
// NodeID; auth is the identity-layer verifier; epoch binds the round. When auth
|
||||
// is non-nil, a partial is ACCEPTED only if it carries a valid identity
|
||||
// signature whose author IS quorum[PartyID] — a forged partial stamped with a
|
||||
// victim's slot is dropped, so an attacker can neither blame nor front-run-
|
||||
// exclude an honest victim. Blame is emitted ONLY for authenticated partials;
|
||||
// it is NEVER produced off a raw unauthenticated PartyID (with auth == nil no
|
||||
// blame is produced at all — attribution fails closed). The networked transport
|
||||
// that authenticates DELIVERY remains a flagged residual.
|
||||
// ORIGIN AUTHENTICATION (RED MEDIUM), SAFE BY DEFAULT. quorum maps PartyID →
|
||||
// expected validator NodeID; auth is the identity-layer verifier; epoch binds the
|
||||
// round. auth == nil is REFUSED FAIL-CLOSED (ErrOriginAuthRequired): a caller
|
||||
// that forgot to wire the verifier must NOT silently revert to unauthenticated
|
||||
// aggregation. With a REAL verifier a partial is ACCEPTED only if it carries a
|
||||
// valid identity signature whose author IS quorum[PartyID] — a forged partial
|
||||
// stamped with a victim's slot is dropped, so an attacker can neither blame nor
|
||||
// front-run-exclude an honest victim, and blame is emitted ONLY for authenticated
|
||||
// partials. The unauthenticated path (no origin check, no blame) is reachable
|
||||
// ONLY by the EXPLICIT opt-out auth == UnauthenticatedAggregation (trusted-channel
|
||||
// / test). The networked transport that authenticates DELIVERY remains a flagged
|
||||
// residual.
|
||||
func AggregateBCCWithBlame(params *Params, setup *AlgSetup, evalPoints []uint32, quorum []NodeID, epoch uint64, auth AbortSignatureVerifier, ctx, msg []byte, c poly, cHat *poly, w1 polyVec, sid, nonceID [32]byte, threshold int, partials []Partial) (Aggregate, ConsensusCert, []PartialBlame, error) {
|
||||
// ORIGIN-AUTH SAFE-BY-DEFAULT (RED MEDIUM). A nil verifier is a caller that
|
||||
// FORGOT to wire origin authentication — refuse FAIL-CLOSED rather than
|
||||
// silently revert to the unauthenticated exclude-honest-victim footgun (the
|
||||
// same no-fail-open posture the nonce ledger has). A caller that genuinely
|
||||
// wants the unauthenticated path (trusted-channel test ceremony / structural
|
||||
// no-reconstruct proof) opts OUT explicitly with UnauthenticatedAggregation.
|
||||
if auth == nil {
|
||||
return Aggregate{}, ConsensusCert{}, nil, ErrOriginAuthRequired
|
||||
}
|
||||
authenticated := auth != UnauthenticatedAggregation
|
||||
|
||||
gamma2, beta, omega, ok := bccParams(params.Mode)
|
||||
if !ok {
|
||||
return Aggregate{}, ConsensusCert{}, nil, ErrBCCParamSet
|
||||
@@ -844,9 +900,10 @@ func AggregateBCCWithBlame(params *Params, setup *AlgSetup, evalPoints []uint32,
|
||||
// unauthenticated / wrong-slot / forged partial HERE, with NO blame against
|
||||
// the slot's honest owner, BEFORE the first-per-PartyID and duplicate logic
|
||||
// (so a forgery cannot occupy the victim's slot or evict the victim's real
|
||||
// partial). With auth == nil the identity layer is not wired: the round still
|
||||
// aggregates sigma-checked partials but produces NO blame (see below).
|
||||
if auth != nil {
|
||||
// partial). On the explicit UnauthenticatedAggregation opt-out the identity
|
||||
// layer is intentionally not wired: the round still aggregates sigma-checked
|
||||
// partials but produces NO blame (see below).
|
||||
if authenticated {
|
||||
authed := make([]Partial, 0, len(partials))
|
||||
for i := range partials {
|
||||
p := partials[i]
|
||||
@@ -870,11 +927,11 @@ func AggregateBCCWithBlame(params *Params, setup *AlgSetup, evalPoints []uint32,
|
||||
seen := make(map[uint32]bool, len(partials))
|
||||
var blames []PartialBlame
|
||||
blame := func(party uint32, reason BlameReason) {
|
||||
// Never emit blame off an unauthenticated PartyID (RED MEDIUM): with no
|
||||
// identity verifier wired, attribution is not sound, so produce none.
|
||||
// With auth != nil only authenticated partials reach here, so the PartyID
|
||||
// is the genuine slot owner and the blame is sound.
|
||||
if auth == nil {
|
||||
// Never emit blame off an unauthenticated PartyID (RED MEDIUM): on the
|
||||
// UnauthenticatedAggregation opt-out attribution is not sound, so produce
|
||||
// none. When authenticated, only origin-checked partials reach here, so the
|
||||
// PartyID is the genuine slot owner and the blame is sound.
|
||||
if !authenticated {
|
||||
return
|
||||
}
|
||||
blames = append(blames, PartialBlame{PartyID: party, Reason: reason, SessionID: sid, NonceID: nonceID})
|
||||
|
||||
@@ -158,6 +158,12 @@ func runBCCCeremony(t *testing.T, f *bccFixture, q int, sid [32]byte, ctx, msg [
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// This helper exercises the no-reconstruct FIPS-validity math over a trusted
|
||||
// in-memory bus; origin authentication is orthogonal and is exercised by the
|
||||
// blame/capstone suite (bccRound wires a REAL identity set). Opt OUT of the
|
||||
// origin-auth gate EXPLICITLY so aggregation is allowed (a bare nil verifier
|
||||
// is now refused FAIL-CLOSED — ErrOriginAuthRequired).
|
||||
nd.SetIdentity(nil, UnauthenticatedAggregation)
|
||||
nodes[i] = nd
|
||||
}
|
||||
|
||||
@@ -350,6 +356,10 @@ func TestDistributedBCC_SubQuorumCannotSign(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("signer %d: %v", i, err)
|
||||
}
|
||||
// Trusted in-memory bus: opt OUT of origin-auth explicitly (a bare nil
|
||||
// verifier is now refused FAIL-CLOSED). The threshold bound under test is
|
||||
// orthogonal to origin authentication.
|
||||
nd.SetIdentity(nil, UnauthenticatedAggregation)
|
||||
nodes[i] = nd
|
||||
if err := nd.SetNonceShare(nonceID, deal.YShares[quorum[i]]); err != nil {
|
||||
t.Fatalf("set nonce share %d: %v", i, err)
|
||||
@@ -408,6 +418,10 @@ func TestDistributedBCC_SoundPartialZProofRejectsForgery(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("signer %d: %v", i, err)
|
||||
}
|
||||
// Trusted in-memory bus: opt OUT of origin-auth explicitly (a bare nil
|
||||
// verifier is refused FAIL-CLOSED). This test probes the SOUND sigma proof
|
||||
// catching a forged z, orthogonal to origin authentication.
|
||||
nd.SetIdentity(nil, UnauthenticatedAggregation)
|
||||
nodes[i] = nd
|
||||
_ = nd.SetNonceShare(nonceID, deal.YShares[quorum[i]])
|
||||
r1, _ := nd.Round1(sid, nonceID, deal.Cert)
|
||||
|
||||
@@ -244,6 +244,10 @@ func TestCommittee_NoReconstruct_Invariant_CombinerSeesNoSecret(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("signer %d: %v", i, err)
|
||||
}
|
||||
// Trusted in-memory bus: opt OUT of origin-auth explicitly (a bare nil
|
||||
// verifier is refused FAIL-CLOSED). The no-reconstruct / threshold bound
|
||||
// under test is orthogonal to origin authentication.
|
||||
nd.SetIdentity(nil, UnauthenticatedAggregation)
|
||||
nodes[i] = nd
|
||||
if err := nd.SetNonceShare(nonceID, deal.YShares[quorum[i]]); err != nil {
|
||||
t.Fatalf("set nonce share %d: %v", i, err)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pulsar
|
||||
|
||||
// origin_auth_gate_test.go — GATE (RED MEDIUM, post-merge nit #1): origin
|
||||
// authentication is SAFE BY DEFAULT. The aggregation surface no longer silently
|
||||
// aggregates unauthenticated partials when the verifier is missing — a caller
|
||||
// that FORGOT to wire it is refused FAIL-CLOSED (ErrOriginAuthRequired), so it
|
||||
// cannot revert to the exclude-honest-victim footgun. The unauthenticated path
|
||||
// is reachable ONLY by the EXPLICIT UnauthenticatedAggregation opt-out (test /
|
||||
// trusted-channel). This is the same no-fail-open posture the nonce ledger has.
|
||||
//
|
||||
// Two faces, one gate:
|
||||
// - the free function AggregateBCC / AggregateBCCWithBlame, and
|
||||
// - the DistributedBCCSigner.Finalize / FinalizeWithBlame entrypoint.
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// GATE (free fn) — nil verifier REFUSES; explicit opt-out and a real verifier
|
||||
// both aggregate to a FIPS-valid signature. Driven on real (bccRound-signed)
|
||||
// partials over a boundary-clear nonce, so the success directions are
|
||||
// deterministic.
|
||||
func TestGATE_OriginAuth_FreeFn_DefaultRefuses(t *testing.T) {
|
||||
const n, threshold = 5, 3
|
||||
f := newBCCFixture(t, ModeP65, n, threshold)
|
||||
var sid [32]byte
|
||||
copy(sid[:], []byte("origin-auth-freefn"))
|
||||
msg := []byte("safe-by-default origin authentication")
|
||||
partials, agg, _, quorum, evalPoints := bccRound(t, f, msg, sid)
|
||||
nonceID := partials[0].NonceID
|
||||
|
||||
// (1) DEFAULT (nil verifier) is REFUSED FAIL-CLOSED — both the blame surface
|
||||
// and the back-compat wrapper.
|
||||
if _, _, _, err := AggregateBCCWithBlame(f.params, f.setup, evalPoints, quorum, 0, nil,
|
||||
nil, msg, agg.c, &agg.cHat, agg.w1, sid, nonceID, threshold, partials); !errors.Is(err, ErrOriginAuthRequired) {
|
||||
t.Fatalf("GATE FAILED: nil-verifier AggregateBCCWithBlame returned %v, want ErrOriginAuthRequired (must fail closed)", err)
|
||||
}
|
||||
if _, _, err := AggregateBCC(f.params, f.setup, evalPoints, quorum, 0, nil,
|
||||
nil, msg, agg.c, &agg.cHat, agg.w1, sid, nonceID, threshold, partials); !errors.Is(err, ErrOriginAuthRequired) {
|
||||
t.Fatalf("GATE FAILED: nil-verifier AggregateBCC returned %v, want ErrOriginAuthRequired", err)
|
||||
}
|
||||
|
||||
// (2) EXPLICIT opt-out (UnauthenticatedAggregation) aggregates and yields a
|
||||
// FIPS-valid signature — the deliberate trusted-channel path.
|
||||
_, certOptOut, _, err := AggregateBCCWithBlame(f.params, f.setup, evalPoints, quorum, 0, UnauthenticatedAggregation,
|
||||
nil, msg, agg.c, &agg.cHat, agg.w1, sid, nonceID, threshold, partials)
|
||||
if err != nil {
|
||||
t.Fatalf("explicit UnauthenticatedAggregation opt-out must aggregate, got %v", err)
|
||||
}
|
||||
if !fipsVerify(t, f.setup, msg, &certOptOut.Signature) {
|
||||
t.Fatalf("opt-out aggregate failed unmodified FIPS 204 verify")
|
||||
}
|
||||
|
||||
// (3) A REAL verifier authenticates the (bccRound-signed) partials and yields a
|
||||
// FIPS-valid signature too — the production path.
|
||||
_, certAuth, _, err := AggregateBCCWithBlame(f.params, f.setup, evalPoints, quorum, 0, f.idset,
|
||||
nil, msg, agg.c, &agg.cHat, agg.w1, sid, nonceID, threshold, partials)
|
||||
if err != nil {
|
||||
t.Fatalf("authenticated aggregation must succeed on signed partials, got %v", err)
|
||||
}
|
||||
if !fipsVerify(t, f.setup, msg, &certAuth.Signature) {
|
||||
t.Fatalf("authenticated aggregate failed unmodified FIPS 204 verify")
|
||||
}
|
||||
t.Logf("GATE PASS (origin-auth safe-by-default, free fn): nil verifier => ErrOriginAuthRequired (fail-closed); UnauthenticatedAggregation opt-out AND a real verifier both aggregate to a FIPS-valid signature")
|
||||
}
|
||||
|
||||
// GATE (signer) — a DistributedBCCSigner that never called SetIdentity (idVerify
|
||||
// == nil, the forgot-to-wire path) refuses Finalize / FinalizeWithBlame
|
||||
// FAIL-CLOSED; the explicit opt-out SetIdentity(nil, UnauthenticatedAggregation)
|
||||
// is the only lever that lifts the refusal.
|
||||
func TestGATE_OriginAuth_Signer_DefaultRefuses(t *testing.T) {
|
||||
const n, threshold = 5, 3
|
||||
f := newBCCFixture(t, ModeP65, n, threshold)
|
||||
quorum, evalPoints, qshares := f.quorum(threshold)
|
||||
var sid [32]byte
|
||||
copy(sid[:], []byte("origin-auth-signer"))
|
||||
msg := []byte("signer fails closed without identity")
|
||||
var nonceID [32]byte
|
||||
nonceID[0] = 0x4d
|
||||
deal, err := DealNonceMPCDebug(f.setup, quorum, evalPoints, threshold, nonceID, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("nonce deal: %v", err)
|
||||
}
|
||||
|
||||
// Drive a full-threshold ceremony with NO SetIdentity (the forgot-to-wire path).
|
||||
nodes := make([]*DistributedBCCSigner, threshold)
|
||||
partials := make([]Partial, 0, threshold)
|
||||
var aggR1 SignRound1
|
||||
for i := 0; i < threshold; i++ {
|
||||
nd, e := NewDistributedBCCSigner(f.params, f.setup, qshares[i], quorum, evalPoints, sid, nil, msg, rand.Reader)
|
||||
if e != nil {
|
||||
t.Fatalf("signer %d: %v", i, e)
|
||||
}
|
||||
nodes[i] = nd
|
||||
if e := nd.SetNonceShare(nonceID, deal.YShares[quorum[i]]); e != nil {
|
||||
t.Fatalf("set nonce share %d: %v", i, e)
|
||||
}
|
||||
r1, e := nd.Round1(sid, nonceID, deal.Cert)
|
||||
if e != nil {
|
||||
t.Fatalf("round1 %d: %v", i, e)
|
||||
}
|
||||
if nd.IsAggregator() {
|
||||
aggR1 = r1
|
||||
}
|
||||
p, e := nd.Round2(r1, PartialInput{})
|
||||
if e != nil {
|
||||
t.Fatalf("round2 %d: %v", i, e)
|
||||
}
|
||||
partials = append(partials, p)
|
||||
}
|
||||
var agg *DistributedBCCSigner
|
||||
for _, nd := range nodes {
|
||||
if nd.IsAggregator() {
|
||||
agg = nd
|
||||
}
|
||||
}
|
||||
if agg == nil {
|
||||
t.Fatal("no aggregator")
|
||||
}
|
||||
|
||||
// DEFAULT (no SetIdentity): both entrypoints refuse FAIL-CLOSED.
|
||||
if _, _, _, ferr := agg.FinalizeWithBlame(aggR1, partials); !errors.Is(ferr, ErrOriginAuthRequired) {
|
||||
t.Fatalf("GATE FAILED: default signer FinalizeWithBlame returned %v, want ErrOriginAuthRequired", ferr)
|
||||
}
|
||||
if _, _, ferr := agg.Finalize(aggR1, partials); !errors.Is(ferr, ErrOriginAuthRequired) {
|
||||
t.Fatalf("GATE FAILED: default signer Finalize returned %v, want ErrOriginAuthRequired", ferr)
|
||||
}
|
||||
|
||||
// EXPLICIT opt-out lifts the refusal (the auth gate no longer fires). The FIPS
|
||||
// hint outcome is orthogonal here; assert only that the origin-auth refusal is
|
||||
// gone — the free-fn gate above proves the opt-out actually produces a valid sig.
|
||||
agg.SetIdentity(nil, UnauthenticatedAggregation)
|
||||
if _, _, _, ferr := agg.FinalizeWithBlame(aggR1, partials); errors.Is(ferr, ErrOriginAuthRequired) {
|
||||
t.Fatalf("GATE FAILED: explicit UnauthenticatedAggregation opt-out still refused with ErrOriginAuthRequired")
|
||||
}
|
||||
t.Logf("GATE PASS (origin-auth safe-by-default, signer): a signer with no SetIdentity refuses Finalize/FinalizeWithBlame FAIL-CLOSED (ErrOriginAuthRequired); SetIdentity(nil, UnauthenticatedAggregation) is the explicit lever that lifts it")
|
||||
}
|
||||
|
||||
// Sentinel hygiene — the UnauthenticatedAggregation opt-out must NEVER be used as
|
||||
// a real verifier (it has no key material); if mistaken for one it must fail LOUD
|
||||
// (panic), never silently accept/drop. The gate branches on identity before any
|
||||
// method call, so this path is unreachable in correct flow — assert it stays loud.
|
||||
func TestGATE_OriginAuth_SentinelIsNotAVerifier(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatalf("GATE FAILED: UnauthenticatedAggregation.VerifyAbortSignature did not panic — a sentinel must not silently act as a verifier")
|
||||
}
|
||||
}()
|
||||
_ = UnauthenticatedAggregation.VerifyAbortSignature(NodeID{}, nil, nil)
|
||||
}
|
||||
@@ -431,6 +431,10 @@ func runTalusMPCCeremony(t *testing.T, f *bccFixture, q int, sid [32]byte, ctx,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Trusted in-memory bus: opt OUT of origin-auth explicitly (a bare nil
|
||||
// verifier is refused FAIL-CLOSED). TALUS leak-free CSCP is orthogonal
|
||||
// to origin authentication.
|
||||
nd.SetIdentity(nil, UnauthenticatedAggregation)
|
||||
if err := nd.SetNonceShare(nonceID, yShares[quorum[i]]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -566,6 +570,9 @@ func TestTalus_MPC_SubQuorumCannotSign(t *testing.T) {
|
||||
var aggR1 SignRound1
|
||||
for i := 0; i < threshold; i++ {
|
||||
nd, _ := NewDistributedBCCSigner(f.params, f.setup, qshares[i], quorum, evalPoints, sid, nil, msg, rand.Reader)
|
||||
// Trusted in-memory bus: opt OUT of origin-auth explicitly (a bare nil
|
||||
// verifier is refused FAIL-CLOSED); the threshold bound is orthogonal.
|
||||
nd.SetIdentity(nil, UnauthenticatedAggregation)
|
||||
_ = nd.SetNonceShare(nonceID, yShares[quorum[i]])
|
||||
r1, _ := nd.Round1(sid, nonceID, *cert)
|
||||
if nd.IsAggregator() {
|
||||
|
||||
Reference in New Issue
Block a user