Files
corona/threshold/minority_soundness_test.go
zeekay 30abcdf1b1 threshold: make trusted-dealer keygen explicit, drop dead ReconstructSecret, pin sub-quorum soundness
Part of the Lux threshold-crypto security rip (killing trusted-dealer /
full-key-reconstruction footguns).

- Rename threshold.GenerateKeys -> threshold.GenerateKeysTrustedDealer.
  The function samples the full secret and sets sign.K / sign.Threshold in
  one process: it IS the trusted dealer. The new name mirrors
  keyera.BootstrapTrustedDealer so the trust model is explicit and
  greppable. Doc comment now states it is for test/KAT/CT/CLI/oracle use
  only, and that production chain keygen uses keyera.Bootstrap (dealerless
  Pedersen DKG). Stays exported; every caller updated (cli, ct/dudect,
  same-package tests) plus stale comment refs in cmd, keyera, wire,
  reshare.

- Delete utils.ReconstructSecret. It Lagrange-interpolates the full secret
  from shares and had zero callers anywhere in the repo: a
  reconstruction-shaped footgun left dead. CompareSecrets and the section
  retained.

- Include threshold/minority_soundness_test.go: an adversarial negative
  pinning that a strict sub-quorum cannot assemble a signature that
  verifies under the group key (uncancelled PRF mask fails the L2-norm
  gate). A sub-quorum forgery would be a catastrophic finality break.
2026-06-26 21:13:45 -07:00

103 lines
3.4 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package threshold
import (
"crypto/rand"
"testing"
)
// TestThresholdSubQuorumCannotForge pins the permissionless threshold-
// soundness invariant: a strict sub-quorum of the committee (a minority
// that omits at least one signer) CANNOT assemble a signature that
// verifies under the group public key.
//
// Why this holds at runtime: each party's Round-2 partial carries an
// additive PRF mask term (mask / maskPrime in sign.SignRound2). Those
// masks only telescope to zero when the partials are summed over the
// FULL signing set T. Drop any signer and the aggregate z retains an
// uncancelled, high-norm mask, so the L2-norm gate in sign.Verify
// (CheckL2Norm against Bsquare) rejects it. This is the runtime
// counterpart to the (t,n)-Shamir secrecy argument: fewer than the full
// quorum can neither interpolate s nor emit a low-norm signature.
//
// If a sub-quorum signature EVER verified, that would be a catastrophic
// forgery break (a minority producing valid finality certs), so this
// test is an adversarial negative, not a smoke test.
func TestThresholdSubQuorumCannotForge(t *testing.T) {
const tt, n = 3, 5
shares, gk, err := GenerateKeysTrustedDealer(tt, n, rand.Reader)
if err != nil {
t.Fatalf("GenerateKeysTrustedDealer(%d,%d): %v", tt, n, err)
}
signers := make([]*Signer, n)
for i, share := range shares {
signers[i] = NewSigner(share)
}
signerIDs := make([]int, n)
for i := range signerIDs {
signerIDs[i] = i
}
const sid = 1
prfKey := make([]byte, 32)
if _, err := rand.Read(prfKey); err != nil {
t.Fatal(err)
}
message := "corona minority-soundness test"
// Round 1 — every committee member participates so masks are set up
// over the full set T (the honest protocol).
r1 := make(map[int]*Round1Data, n)
for _, s := range signers {
d, err := s.Round1(sid, prfKey, signerIDs)
if err != nil {
t.Fatalf("Round1 party %d: %v", s.share.Index, err)
}
r1[d.PartyID] = d
}
// Round 2 — every committee member emits its z partial.
r2 := make(map[int]*Round2Data, n)
for _, s := range signers {
d, err := s.Round2(sid, message, prfKey, signerIDs, r1)
if err != nil {
t.Fatalf("Round2 party %d: %v", s.share.Index, err)
}
r2[d.PartyID] = d
}
// Positive control: the FULL quorum produces a verifying signature.
fullSig, err := signers[0].Finalize(r2)
if err != nil {
t.Fatalf("Finalize (full quorum): %v", err)
}
if !Verify(gk, message, fullSig) {
t.Fatal("full-quorum honest signature was rejected — test setup is wrong")
}
// Adversarial negative: a minority of size t-1 = 2 (parties {0,1})
// attempts to finalize using only its own partials. The aggregate z
// is missing n-(t-1)=3 mask-cancelling terms.
minority := map[int]*Round2Data{
0: r2[0],
1: r2[1],
}
if len(minority) >= n {
t.Fatalf("minority set not a strict subset: |minority|=%d n=%d", len(minority), n)
}
forged, err := signers[0].Finalize(minority)
if err != nil {
// A hard error here is also an acceptable rejection of the minority
// attempt; the invariant (no valid sub-quorum signature) still holds.
return
}
if Verify(gk, message, forged) {
t.Fatalf("CRITICAL: a %d-of-%d sub-quorum produced a signature that "+
"VERIFIED under the group key — threshold soundness is broken "+
"(minority forgery)", len(minority), n)
}
}