Files
node/cmd/ceremony/security_regression_test.go
zeekayandHanzo Dev b1cc108c9f gofmt
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 01:51:51 -07:00

366 lines
12 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
// =============================================================================
// CRITICAL Regressions
// =============================================================================
// TestRegressionC01_ToxicWasteZeroed verifies that after contribute() returns,
// all secret scalars (tau, alpha, beta) and their big.Int intermediates are
// zeroed out.
// Leave them in memory and the secret scalars stay recoverable from the
// process for as long as it lives.
func TestRegressionC01_ToxicWasteZeroed(t *testing.T) {
// Verify zeroBI function exists and actually zeros big.Int limbs.
x := new(big.Int).SetUint64(0xDEADBEEFCAFEBABE)
if x.Sign() == 0 {
t.Fatal("precondition: x must be non-zero")
}
zeroBI(x)
// After zeroing: value must be 0
if x.Sign() != 0 {
t.Fatal("zeroBI must set big.Int to zero -- C-01 regression")
}
// Limbs must all be zero
for _, limb := range x.Bits() {
if limb != 0 {
t.Fatalf("zeroBI must zero all limbs, found non-zero limb -- C-01 regression")
}
}
// Verify with multi-word value (256-bit)
y := new(big.Int)
y.SetString("DEADBEEFCAFEBABE0123456789ABCDEF0011223344556677AABBCCDDEEFF0099", 16)
if len(y.Bits()) < 2 {
t.Fatal("precondition: y must have multiple limbs")
}
zeroBI(y)
if y.Sign() != 0 {
t.Fatal("zeroBI must zero multi-word big.Int -- C-01 regression")
}
for _, limb := range y.Bits() {
if limb != 0 {
t.Fatal("zeroBI must zero all limbs of multi-word big.Int -- C-01 regression")
}
}
}
// TestRegressionC02_CeremonyChainLinking verifies that tampering with any
// intermediate contribution's StateHash or PrevHash is detected by verifyCeremony.
// Without that check a contribution can be reordered or a state tampered with
// and nothing says so.
func TestRegressionC02_CeremonyChainLinking(t *testing.T) {
// Build a valid ceremony with 3 contributions
state, err := initCeremony("test-c02", 1<<4, 3)
if err != nil {
t.Fatalf("initCeremony: %v", err)
}
for _, p := range []string{"Alice", "Bob", "Carol"} {
contrib, err := contribute(state, p)
if err != nil {
t.Fatalf("contribute %s: %v", p, err)
}
state.Contributions = append(state.Contributions, contrib)
}
// Baseline: valid state must verify
if err := verifyCeremony(state); err != nil {
t.Fatalf("valid ceremony must verify: %v", err)
}
// Test 1: Tamper with contribution[1].StateHash
t.Run("tamper_StateHash", func(t *testing.T) {
clone := cloneState(t, state)
clone.Contributions[1].StateHash = "0000000000000000000000000000000000000000000000000000000000000000"
if err := verifyCeremony(clone); err == nil {
t.Fatal("tampered StateHash must be detected -- C-02 regression")
}
})
// Test 2: Tamper with contribution[1].PrevHash
t.Run("tamper_PrevHash", func(t *testing.T) {
clone := cloneState(t, state)
clone.Contributions[1].PrevHash = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
if err := verifyCeremony(clone); err == nil {
t.Fatal("tampered PrevHash must be detected -- C-02 regression")
}
})
// Test 3: Reorder contributions (swap 1 and 2)
t.Run("reorder_contributions", func(t *testing.T) {
clone := cloneState(t, state)
clone.Contributions[1], clone.Contributions[2] = clone.Contributions[2], clone.Contributions[1]
if err := verifyCeremony(clone); err == nil {
t.Fatal("reordered contributions must be detected -- C-02 regression")
}
})
// Test 4: First contribution with non-empty PrevHash
t.Run("first_nonempty_PrevHash", func(t *testing.T) {
clone := cloneState(t, state)
clone.Contributions[0].PrevHash = "abc123"
if err := verifyCeremony(clone); err == nil {
t.Fatal("first contribution with non-empty PrevHash must be detected -- C-02 regression")
}
})
// Test 5: Empty StateHash
t.Run("empty_StateHash", func(t *testing.T) {
clone := cloneState(t, state)
clone.Contributions[2].StateHash = ""
if err := verifyCeremony(clone); err == nil {
t.Fatal("empty StateHash must be detected -- C-02 regression")
}
})
}
// =============================================================================
// MEDIUM Regressions
// =============================================================================
// TestRegressionM01_CeremonyAlphaBetaConsistency verifies that verifyCeremony
// detects inconsistency between alphaG1 and the tau ratio.
// Without the cross-check, the alpha/beta arrays and tau can disagree unnoticed.
func TestRegressionM01_CeremonyAlphaBetaConsistency(t *testing.T) {
state, err := initCeremony("test-m01", 1<<4, 1)
if err != nil {
t.Fatalf("initCeremony: %v", err)
}
contrib, err := contribute(state, "Honest")
if err != nil {
t.Fatalf("contribute: %v", err)
}
state.Contributions = append(state.Contributions, contrib)
// Corrupt alphaG1[1] with a random point that breaks the tau ratio
t.Run("corrupt_alphaG1", func(t *testing.T) {
clone := cloneState(t, state)
// Set alphaG1[1] to the generator (breaks alpha*tau relationship)
_, _, g1, _ := bn254.Generators()
clone.AlphaG1[1] = G1Point{g1}
if err := verifyCeremony(clone); err == nil {
t.Fatal("inconsistent alphaG1 must reject -- M-01 regression")
}
})
// Corrupt betaG1[1] with a random point
t.Run("corrupt_betaG1", func(t *testing.T) {
clone := cloneState(t, state)
_, _, g1, _ := bn254.Generators()
clone.BetaG1[1] = G1Point{g1}
if err := verifyCeremony(clone); err == nil {
t.Fatal("inconsistent betaG1 must reject -- M-01 regression")
}
})
// Corrupt betaG2 (breaks betaG1/betaG2 cross-check)
t.Run("corrupt_betaG2", func(t *testing.T) {
clone := cloneState(t, state)
_, _, _, g2 := bn254.Generators()
clone.BetaG2 = G2Point{g2}
if err := verifyCeremony(clone); err == nil {
t.Fatal("inconsistent betaG2 must reject -- M-01 regression")
}
})
}
// TestRegressionM02_SameRatioArgOrder verifies that sameRatio checks the
// correct ratio direction (n1/d1 == n2/d2, not swapped args).
// Swap sameRatio's arguments and the check passes only for tau=1, the identity
// case, so every real ceremony verifies for the wrong reason.
func TestRegressionM02_SameRatioArgOrder(t *testing.T) {
// A real ceremony with random tau != 1 must verify.
// If sameRatio args were swapped, it would only pass for tau=1.
state, err := initCeremony("test-m02", 1<<4, 1)
if err != nil {
t.Fatalf("initCeremony: %v", err)
}
contrib, err := contribute(state, "Participant")
if err != nil {
t.Fatalf("contribute: %v", err)
}
state.Contributions = append(state.Contributions, contrib)
// With real random tau (astronomically unlikely to be 1), verification
// must pass. If args were swapped, this would fail.
if err := verifyCeremony(state); err != nil {
t.Fatalf("valid ceremony with random tau must verify (arg order correct) -- M-02 regression: %v", err)
}
// Also verify sameRatio directly with a known non-identity scalar
_, _, g1, g2 := bn254.Generators()
var scalar fr.Element
scalar.SetUint64(42) // tau = 42, definitely not 1
var bi big.Int
scalar.BigInt(&bi)
var tauG1 bn254.G1Affine
tauG1.ScalarMultiplication(&g1, &bi)
var tauG2 bn254.G2Affine
tauG2.ScalarMultiplication(&g2, &bi)
// Correct order: tauG1/G1 == tauG2/G2 (both ratios = tau = 42)
if !sameRatio(tauG1, g1, tauG2, g2) {
t.Fatal("sameRatio(tau*G1, G1, tau*G2, G2) must be true -- M-02 regression")
}
// Wrong ratio must fail
var wrongG1 bn254.G1Affine
var wrongScalar fr.Element
wrongScalar.SetUint64(7)
var wrongBI big.Int
wrongScalar.BigInt(&wrongBI)
wrongG1.ScalarMultiplication(&g1, &wrongBI)
if sameRatio(wrongG1, g1, tauG2, g2) {
t.Fatal("sameRatio with different ratios must be false -- M-02 regression")
}
}
// =============================================================================
// INFO Regressions
// =============================================================================
// TestRegressionI03_DefaultPowerIs20 verifies the ceremony CLI default power
// flag is 20.
// A default of 10 is too small for the circuits this ceremony is run for.
func TestRegressionI03_DefaultPowerIs20(t *testing.T) {
// The cmdInit function in main.go uses:
// power := fs.Int("power", 20, ...)
// We verify by checking the initCeremony output with 2^20.
numConstraints := 1 << 20
state, err := initCeremony("power-test", numConstraints, 1)
if err != nil {
t.Fatalf("initCeremony with 2^20: %v", err)
}
if state.PowersNeeded != numConstraints+1 {
t.Fatalf("expected %d powers, got %d -- I-03 regression", numConstraints+1, state.PowersNeeded)
}
}
// =============================================================================
// LOW Regressions
// =============================================================================
// TestRegressionL01_CeremonyHasTests drives the whole ceremony lifecycle —
// init, contribute, verify — so the package has one test that exercises the
// path end to end rather than only its pieces.
func TestRegressionL01_CeremonyHasTests(t *testing.T) {
state, err := initCeremony("meta-test", 1<<4, 1)
if err != nil {
t.Fatalf("initCeremony: %v", err)
}
contrib, err := contribute(state, "Tester")
if err != nil {
t.Fatalf("contribute: %v", err)
}
state.Contributions = append(state.Contributions, contrib)
if err := verifyCeremony(state); err != nil {
t.Fatalf("ceremony lifecycle must work: %v", err)
}
}
// TestRegressionL03_StateFileHasIntegrity verifies that writeState produces
// a file with an integrity hash and readState validates it.
// A state file with no integrity field corrupts silently.
func TestRegressionL03_StateFileHasIntegrity(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "integrity-test.json")
state, err := initCeremony("integrity", 1<<4, 1)
if err != nil {
t.Fatalf("initCeremony: %v", err)
}
if err := writeState(state, path); err != nil {
t.Fatalf("writeState: %v", err)
}
// Read raw JSON and verify envelope has "integrity" field
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
var envelope struct {
State jsontext.Value `json:"state"`
Integrity string `json:"integrity"`
}
if err := json.Unmarshal(raw, &envelope); err != nil {
t.Fatalf("unmarshal envelope: %v", err)
}
if envelope.Integrity == "" {
t.Fatal("state file must have non-empty integrity field -- L-03 regression")
}
// Verify the integrity hash is correct
h := sha256.Sum256(envelope.State)
expected := hex.EncodeToString(h[:])
if envelope.Integrity != expected {
t.Fatalf("integrity hash mismatch: got %s, want %s -- L-03 regression", envelope.Integrity, expected)
}
// Tamper and verify readState rejects it
envelope.Integrity = strings.Repeat("0", 64)
tampered, err := json.Marshal(envelope)
if err != nil {
t.Fatalf("marshal tampered: %v", err)
}
tamperedPath := filepath.Join(dir, "tampered.json")
if err := os.WriteFile(tamperedPath, tampered, 0600); err != nil {
t.Fatalf("write tampered: %v", err)
}
_, err = readState(tamperedPath)
if err == nil {
t.Fatal("readState must reject tampered integrity -- L-03 regression")
}
if !strings.Contains(err.Error(), "integrity") {
t.Errorf("error must mention 'integrity', got: %v", err)
}
}
// =============================================================================
// Helpers
// =============================================================================
// cloneState deep-copies a CeremonyState via JSON round-trip.
func cloneState(t *testing.T, orig *CeremonyState) *CeremonyState {
t.Helper()
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal state: %v", err)
}
var clone CeremonyState
if err := json.Unmarshal(data, &clone); err != nil {
t.Fatalf("unmarshal state: %v", err)
}
return &clone
}