mirror of
https://github.com/luxfi/corona.git
synced 2026-07-27 02:50:34 +00:00
Closes the CRIT-1 residual (D1-2) sid-entropy gap and the CT/doc hygiene
gaps to bring Corona to the no-leak/no-gap bar. EasyCrypt proofs untouched.
PRIORITY 1 — anti-nonce-reuse / no-leak durability:
- The Round-1 nonce-PRF key no longer keys on a bare 64-bit sid. It now
derives from a 256-bit domain-separated SessionID (primitives.DeriveSessionID:
TranscriptHash("corona.sign.session-id.v1" || be64(sid) || T)) AND a fresh
per-signature 256-bit hedge salt drawn inside the kernel from party.Rand
(default crypto/rand). PRNGKeyForRound = PRF(skShare, "CoronaNonceV3" ||
sessionID || salt). Hedging restores threshold-Raccoon's fresh-per-signature
nonce posture: reuse durability no longer rests on the external consensus
layer never reissuing an sid — even an sid collision yields distinct R with
prob 2^-256. A deterministic 256-bit SessionID alone is necessary but NOT
sufficient (it repeats when sid repeats); the salt is what closes the leak.
- SignRound1 is fail-closed: rejects an all-zero derived SessionID or all-zero
salt with ErrDegenerateSession, and surfaces a short-read error. The
consensus slot-uniqueness invariant is documented as a HARD precondition at
SignRound1 and Signer.Round1 (no longer a buried comment).
- KAT/oracle determinism preserved via one seam: sign.DeterministicNonceSource
(KeyedPRNG over seed || "corona.sign.nonce-salt.v1" || partyIndex), set on
Party.Rand / Signer.SetNonceRand only by reproducibility harnesses.
- SignRound1 / Signer.Round1 now return an error; all call sites updated.
- KAT REGEN: only sign_verify_e2e.json changes (nonce-key bytes moved);
transcript_hash.json / MAC / legacy PRNGKey vectors are byte-stable, proving
the consensus-agreed transcript path was left untouched. Regenerated via
`bash scripts/regen-kats.sh`; `--verify` confirms byte-determinism (10 files).
- Regression: sign/nonce_reuse_test.go proves same-(skShare,sid) yields distinct
D (fresh R), pinned-nonce reproduces byte-identically, and the degenerate-
session guard fires. TestE2EKATReplayDeterminism rewritten to assert both the
hedged-differs and pinned-reproduces properties (was a defanged no-op).
PRIORITY 2 — constant-time hygiene:
- reshare/commit.go already used a constant-time comparator; the real gap was
the verbatim duplication of constTimePolyEqual+uint64SliceToBytes across dkg2
and reshare. Consolidated to one canonical utils.ConstantTimePolyEqual; both
delegate (no dkg2<->reshare dependency). Orphaned imports removed.
- FullRankCheck and the reshare commit path are now covered in the CT review
with their public-operand justification.
PRIORITY 3 — doc accuracy:
- CONSTANT-TIME-REVIEW.md rewritten Corona-specific and file:line-accurate:
drops the stale Pulsar/lens/warp/secp256k1 content; audits the real call
sites (hedged nonce key, masking PRF, lattigo samplers as the residual TCB
axiom, utils.ConstantTimePolyEqual, CheckL2Norm/Verify/FullRankCheck big.Int
variable-time on PUBLIC operands, activation/commit-digest array equality,
keyera/reshare zeroization).
- PROOF-CLAIMS.md §1: threshold.Combine / sign.LocalSign (nonexistent) -> the
real sign.Party.SignFinalize exposed as threshold.Signer.Finalize.
- threshold/threshold.go package doc: Ring-LWE -> Module-LWE.
157 lines
3.9 KiB
Go
157 lines
3.9 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package threshold
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestGenerateKeys(t *testing.T) {
|
|
shares, groupKey, err := GenerateKeys(2, 3, nil)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeys failed: %v", err)
|
|
}
|
|
|
|
if len(shares) != 3 {
|
|
t.Errorf("expected 3 shares, got %d", len(shares))
|
|
}
|
|
if groupKey == nil {
|
|
t.Fatal("groupKey is nil")
|
|
}
|
|
if groupKey.A == nil {
|
|
t.Error("groupKey.A is nil")
|
|
}
|
|
if groupKey.BTilde == nil {
|
|
t.Error("groupKey.BTilde is nil")
|
|
}
|
|
|
|
for i, share := range shares {
|
|
if share.Index != i {
|
|
t.Errorf("share %d has index %d", i, share.Index)
|
|
}
|
|
if share.GroupKey != groupKey {
|
|
t.Errorf("share %d has wrong groupKey", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestThresholdSigningFlow(t *testing.T) {
|
|
// Generate 2-of-3 threshold keys
|
|
shares, groupKey, err := GenerateKeys(2, 3, nil)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeys failed: %v", err)
|
|
}
|
|
|
|
// Create signers for all parties
|
|
signers := make([]*Signer, 3)
|
|
for i, share := range shares {
|
|
signers[i] = NewSigner(share)
|
|
}
|
|
|
|
// Signing parameters
|
|
sessionID := 1
|
|
prfKey := []byte("test-prf-key-32-bytes-long!!!!!!")
|
|
signerIDs := []int{0, 1, 2}
|
|
message := "test block hash for consensus"
|
|
|
|
// Round 1: All parties compute D + MACs
|
|
round1Data := make(map[int]*Round1Data)
|
|
for _, signer := range signers {
|
|
data, err := signer.Round1(sessionID, prfKey, signerIDs)
|
|
if err != nil {
|
|
t.Fatalf("Round1: %v", err)
|
|
}
|
|
round1Data[data.PartyID] = data
|
|
t.Logf("Party %d: Round1 complete, D size: %d x %d", data.PartyID, len(data.D), len(data.D[0]))
|
|
}
|
|
|
|
// Round 2: All parties compute z shares
|
|
round2Data := make(map[int]*Round2Data)
|
|
for _, signer := range signers {
|
|
data, err := signer.Round2(sessionID, message, prfKey, signerIDs, round1Data)
|
|
if err != nil {
|
|
t.Fatalf("Party %d Round2 failed: %v", signer.share.Index, err)
|
|
}
|
|
round2Data[data.PartyID] = data
|
|
t.Logf("Party %d: Round2 complete, z size: %d", data.PartyID, len(data.Z))
|
|
}
|
|
|
|
// Finalize: Any party can aggregate
|
|
sig, err := signers[0].Finalize(round2Data)
|
|
if err != nil {
|
|
t.Fatalf("Finalize failed: %v", err)
|
|
}
|
|
t.Logf("Signature: C degree=%d, Z size=%d, Delta size=%d", sig.C.N(), len(sig.Z), len(sig.Delta))
|
|
|
|
// Verify
|
|
valid := Verify(groupKey, message, sig)
|
|
if !valid {
|
|
t.Error("signature verification failed")
|
|
}
|
|
t.Log("✓ Signature verified successfully")
|
|
}
|
|
|
|
func TestThresholdWrongMessage(t *testing.T) {
|
|
shares, groupKey, err := GenerateKeys(2, 3, nil)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeys failed: %v", err)
|
|
}
|
|
|
|
signers := make([]*Signer, 3)
|
|
for i, share := range shares {
|
|
signers[i] = NewSigner(share)
|
|
}
|
|
|
|
sessionID := 1
|
|
prfKey := []byte("test-prf-key-32-bytes-long!!!!!!")
|
|
signerIDs := []int{0, 1, 2}
|
|
message := "original message"
|
|
|
|
// Round 1
|
|
round1Data := make(map[int]*Round1Data)
|
|
for _, signer := range signers {
|
|
data, err := signer.Round1(sessionID, prfKey, signerIDs)
|
|
if err != nil {
|
|
t.Fatalf("Round1: %v", err)
|
|
}
|
|
round1Data[data.PartyID] = data
|
|
}
|
|
|
|
// Round 2
|
|
round2Data := make(map[int]*Round2Data)
|
|
for _, signer := range signers {
|
|
data, _ := signer.Round2(sessionID, message, prfKey, signerIDs, round1Data)
|
|
round2Data[data.PartyID] = data
|
|
}
|
|
|
|
// Finalize
|
|
sig, _ := signers[0].Finalize(round2Data)
|
|
|
|
// Verify with wrong message should fail
|
|
valid := Verify(groupKey, "wrong message", sig)
|
|
if valid {
|
|
t.Error("verification should fail for wrong message")
|
|
}
|
|
}
|
|
|
|
func TestInvalidThreshold(t *testing.T) {
|
|
// Threshold >= total
|
|
_, _, err := GenerateKeys(3, 3, nil)
|
|
if err != ErrInvalidThreshold {
|
|
t.Errorf("expected ErrInvalidThreshold, got %v", err)
|
|
}
|
|
|
|
// Threshold = 0
|
|
_, _, err = GenerateKeys(0, 3, nil)
|
|
if err != ErrInvalidThreshold {
|
|
t.Errorf("expected ErrInvalidThreshold, got %v", err)
|
|
}
|
|
|
|
// Too few parties
|
|
_, _, err = GenerateKeys(1, 1, nil)
|
|
if err != ErrInvalidPartyCount {
|
|
t.Errorf("expected ErrInvalidPartyCount, got %v", err)
|
|
}
|
|
}
|