Files
corona/threshold/verify_batch_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

159 lines
4.2 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package threshold
import (
"errors"
"fmt"
"testing"
)
// freshSig produces one (groupKey, message, sig) triple by running a
// full 2-of-3 ceremony. The verifier in this package is pure, so the
// ceremony is the only realistic way to obtain a Corona signature
// (Sign1/Sign2/Combine are the only public entry points).
func freshSig(t testing.TB, message string) (*GroupKey, *Signature) {
t.Helper()
shares, groupKey, err := GenerateKeysTrustedDealer(2, 3, nil)
if err != nil {
t.Fatalf("GenerateKeysTrustedDealer: %v", err)
}
signers := make([]*Signer, 3)
for i, share := range shares {
signers[i] = NewSigner(share)
}
const sessionID = 1
prfKey := []byte("verify_batch-prf-key-32-bytes!!!")
signerIDs := []int{0, 1, 2}
round1 := make(map[int]*Round1Data)
for _, s := range signers {
d, err := s.Round1(sessionID, prfKey, signerIDs)
if err != nil {
t.Fatalf("Round1: %v", err)
}
round1[d.PartyID] = d
}
round2 := make(map[int]*Round2Data)
for _, s := range signers {
d, err := s.Round2(sessionID, message, prfKey, signerIDs, round1)
if err != nil {
t.Fatalf("Round2: %v", err)
}
round2[d.PartyID] = d
}
sig, err := signers[0].Finalize(round2)
if err != nil {
t.Fatalf("Finalize: %v", err)
}
return groupKey, sig
}
// TestVerifyBatch_AllValid asserts a batch of N valid signatures all
// report true and VerifyBatchAll returns true.
func TestVerifyBatch_AllValid(t *testing.T) {
const n = 4
gks := make([]*GroupKey, n)
msgs := make([]string, n)
sigs := make([]*Signature, n)
for i := 0; i < n; i++ {
msg := fmt.Sprintf("corona-batch-msg-%d", i)
gk, sig := freshSig(t, msg)
gks[i] = gk
msgs[i] = msg
sigs[i] = sig
}
results, err := VerifyBatch(gks, msgs, sigs)
if err != nil {
t.Fatalf("VerifyBatch err=%v", err)
}
if len(results) != n {
t.Fatalf("results len=%d, want %d", len(results), n)
}
for i, ok := range results {
if !ok {
t.Errorf("result[%d] = false, want true", i)
}
}
all, err := VerifyBatchAll(gks, msgs, sigs)
if err != nil || !all {
t.Errorf("VerifyBatchAll ok=%v err=%v, want (true, nil)", all, err)
}
}
// TestVerifyBatch_OneCorrupt asserts a single message-substitution
// corruption is localised: results[i] = false for the swapped entry,
// true for all the rest, and VerifyBatchAll returns false.
func TestVerifyBatch_OneCorrupt(t *testing.T) {
const n = 3
const badIdx = 1
gks := make([]*GroupKey, n)
msgs := make([]string, n)
sigs := make([]*Signature, n)
for i := 0; i < n; i++ {
msg := fmt.Sprintf("corona-batch-msg-%d", i)
gk, sig := freshSig(t, msg)
gks[i] = gk
msgs[i] = msg
sigs[i] = sig
}
// Verify under the WRONG message for the bad index — the signature
// was made over msgs[badIdx]; we ask the verifier to check against
// a different string, which the FIPS 204 verifier rejects.
msgs[badIdx] = "corona-batch-wrong-message"
results, err := VerifyBatch(gks, msgs, sigs)
if err != nil {
t.Fatalf("VerifyBatch err=%v", err)
}
for i, ok := range results {
if i == badIdx {
if ok {
t.Errorf("result[%d] = true, want false (corrupted entry)", i)
}
} else if !ok {
t.Errorf("result[%d] = false, want true", i)
}
}
all, err := VerifyBatchAll(gks, msgs, sigs)
if err != nil {
t.Fatalf("VerifyBatchAll err=%v", err)
}
if all {
t.Error("VerifyBatchAll = true, want false")
}
}
// TestVerifyBatch_StructuralMismatch asserts length-mismatched slices
// surface as ErrBatchSizeMismatch with a nil results slice.
func TestVerifyBatch_StructuralMismatch(t *testing.T) {
gks := []*GroupKey{nil, nil}
msgs := []string{"a", "b", "c"}
sigs := []*Signature{nil, nil}
results, err := VerifyBatch(gks, msgs, sigs)
if !errors.Is(err, ErrBatchSizeMismatch) {
t.Errorf("err = %v, want ErrBatchSizeMismatch", err)
}
if results != nil {
t.Errorf("results = %v, want nil", results)
}
}
// TestVerifyBatch_Empty asserts an empty batch is (nil, nil).
func TestVerifyBatch_Empty(t *testing.T) {
results, err := VerifyBatch(nil, nil, nil)
if err != nil {
t.Errorf("err = %v, want nil", err)
}
if results != nil {
t.Errorf("results = %v, want nil", results)
}
}