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.
167 lines
4.4 KiB
Go
167 lines
4.4 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package threshold
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"testing"
|
|
|
|
cgpu "github.com/luxfi/corona/gpu"
|
|
"github.com/luxfi/corona/sign"
|
|
)
|
|
|
|
// deterministicReader is a SHA-256 keystream over a 32-byte seed,
|
|
// suitable as an io.Reader for GenerateKeys. Two calls with the same
|
|
// seed produce the same bytes, which fixes the trustedDealerKey across
|
|
// the CPU and GPU legs of this test.
|
|
type deterministicReader struct {
|
|
seed [32]byte
|
|
block [32]byte
|
|
offset int
|
|
ctr uint64
|
|
}
|
|
|
|
func newDeterministicReader(seed [32]byte) *deterministicReader {
|
|
r := &deterministicReader{seed: seed}
|
|
r.refill()
|
|
return r
|
|
}
|
|
|
|
func (r *deterministicReader) refill() {
|
|
var ctrBuf [8]byte
|
|
binary.BigEndian.PutUint64(ctrBuf[:], r.ctr)
|
|
h := sha256.New()
|
|
h.Write(r.seed[:])
|
|
h.Write(ctrBuf[:])
|
|
sum := h.Sum(nil)
|
|
copy(r.block[:], sum)
|
|
r.offset = 0
|
|
r.ctr++
|
|
}
|
|
|
|
func (r *deterministicReader) Read(p []byte) (int, error) {
|
|
n := 0
|
|
for n < len(p) {
|
|
if r.offset >= len(r.block) {
|
|
r.refill()
|
|
}
|
|
m := copy(p[n:], r.block[r.offset:])
|
|
r.offset += m
|
|
n += m
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// TestThresholdSign_CPU_vs_GPU_ByteIdentical — running the full
|
|
// 2-round Pulsar signing protocol with GPU NTT dispatch off and then
|
|
// on (same deterministic dealer randomness, same message) MUST yield
|
|
// byte-identical signature components. This is the core correctness
|
|
// gate: a single off-by-one in the GPU NTT and the aggregator rejects
|
|
// the share. The contract is defended by lattice/gpu (Montgomery
|
|
// context byte-equal to ring.SubRing.NTT) and here we enforce it
|
|
// end-to-end through corona's signing flow.
|
|
func TestThresholdSign_CPU_vs_GPU_ByteIdentical(t *testing.T) {
|
|
// Same dealer randomness on both legs.
|
|
var seed [32]byte
|
|
copy(seed[:], "byte-equal-gpu-vs-cpu-pulsar-rt!")
|
|
|
|
const k = 3
|
|
const thr = 2
|
|
sessionID := 7
|
|
prfKey := []byte("byte-equal-prf-32-bytes-long-aaaa")
|
|
message := "byte-equal-message-cpu-vs-gpu"
|
|
signerIDs := []int{0, 1, 2}
|
|
|
|
runLeg := func(gpuOn bool) ([]byte, []byte, []byte) {
|
|
t.Helper()
|
|
if gpuOn {
|
|
// Force-engage the GPU path even at N=256, where the
|
|
// production threshold would leave dispatch off. The
|
|
// goal here is correctness (byte-equal across paths),
|
|
// not throughput.
|
|
if err := cgpu.UseAcceleratorForce(); err != nil {
|
|
t.Fatalf("UseAcceleratorForce: %v", err)
|
|
}
|
|
} else {
|
|
cgpu.DisableAccelerator()
|
|
}
|
|
shares, groupKey, err := GenerateKeys(thr, k, newDeterministicReader(seed))
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeys (gpu=%v): %v", gpuOn, err)
|
|
}
|
|
|
|
signers := make([]*Signer, k)
|
|
for i, sh := range shares {
|
|
signers[i] = NewSigner(sh)
|
|
// Pin the hedged-nonce salt to a deterministic, per-party
|
|
// source keyed by the shared dealer seed, so both the CPU and
|
|
// GPU legs draw identical salts and the resulting signature
|
|
// bytes are byte-identical across paths.
|
|
signers[i].SetNonceRand(sign.DeterministicNonceSource(seed[:], i))
|
|
}
|
|
|
|
round1 := make(map[int]*Round1Data)
|
|
for _, s := range signers {
|
|
d, err := s.Round1(sessionID, prfKey, signerIDs)
|
|
if err != nil {
|
|
t.Fatalf("Round1 (gpu=%v): %v", gpuOn, 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 (gpu=%v): %v", gpuOn, err)
|
|
}
|
|
round2[d.PartyID] = d
|
|
}
|
|
|
|
sig, err := signers[0].Finalize(round2)
|
|
if err != nil {
|
|
t.Fatalf("Finalize (gpu=%v): %v", gpuOn, err)
|
|
}
|
|
if !Verify(groupKey, message, sig) {
|
|
t.Fatalf("self-verify failed (gpu=%v)", gpuOn)
|
|
}
|
|
|
|
var cBuf, zBuf, dBuf bytes.Buffer
|
|
if _, err := sig.C.WriteTo(&cBuf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := sig.Z.WriteTo(&zBuf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := sig.Delta.WriteTo(&dBuf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cBuf.Bytes(), zBuf.Bytes(), dBuf.Bytes()
|
|
}
|
|
|
|
t.Cleanup(cgpu.DisableAccelerator)
|
|
|
|
cpuC, cpuZ, cpuD := runLeg(false)
|
|
gpuC, gpuZ, gpuD := runLeg(true)
|
|
|
|
if !bytes.Equal(cpuC, gpuC) {
|
|
t.Fatalf("sig.C differs: cpu=%x gpu=%x", head(cpuC), head(gpuC))
|
|
}
|
|
if !bytes.Equal(cpuZ, gpuZ) {
|
|
t.Fatalf("sig.Z differs: cpu=%x gpu=%x", head(cpuZ), head(gpuZ))
|
|
}
|
|
if !bytes.Equal(cpuD, gpuD) {
|
|
t.Fatalf("sig.Delta differs: cpu=%x gpu=%x", head(cpuD), head(gpuD))
|
|
}
|
|
}
|
|
|
|
func head(b []byte) []byte {
|
|
if len(b) > 32 {
|
|
return b[:32]
|
|
}
|
|
return b
|
|
}
|