Files
Antje Worring dc15c54a47 harden: hedged 256-bit nonce key, DRY CT comparator, honest CT/proof docs
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.
2026-06-21 08:21:58 -07:00

177 lines
6.2 KiB
Go

package sign
import (
"fmt"
"log"
"github.com/luxfi/corona/primitives"
"math/big"
"time"
"github.com/luxfi/lattice/v7/ring"
"github.com/luxfi/lattice/v7/utils/sampling"
"github.com/luxfi/lattice/v7/utils/structs"
"github.com/montanaflynn/stats"
)
var K int
var Threshold int
// main function orchestrates the threshold signature protocol
func LocalRun(x int) {
var totalGenDuration, totalFinalizeDuration, totalVerifyDuration time.Duration
// Create maps to collect durations across all runs
totalSignRound1Durations := make(map[int]float64)
totalSignRound2PreprocessDurations := make(map[int]float64)
totalSignRound2Durations := make(map[int]float64)
for run := 0; run < x; run++ {
log.Println("RUN:", run)
var genDuration, finalizeDuration, verifyDuration time.Duration
randomKey := make([]byte, KeySize)
r, _ := ring.NewRing(1<<LogN, []uint64{Q})
r_xi, _ := ring.NewRing(1<<LogN, []uint64{QXi})
r_nu, _ := ring.NewRing(1<<LogN, []uint64{QNu})
prng, _ := sampling.NewKeyedPRNG(randomKey)
uniformSampler := ring.NewUniformSampler(prng, r)
trustedDealerKey := randomKey
parties := make([]*Party, K)
for i := range parties {
prng, _ := sampling.NewKeyedPRNG(randomKey)
uniformSampler := ring.NewUniformSampler(prng, r)
parties[i] = NewParty(i, r, r_xi, r_nu, uniformSampler)
}
// GEN: Generate secret shares, seeds, and MAC keys
T := make([]int, K) // Active parties
for i := 0; i < K; i++ {
T[i] = i
}
lagrangeCoeffs := primitives.ComputeLagrangeCoefficients(r, T, big.NewInt(int64(Q)))
log.Println("Gen")
start := time.Now()
A, skShares, seeds, MACKeys, b := Gen(r, r_xi, uniformSampler, trustedDealerKey, lagrangeCoeffs)
genDuration = time.Since(start)
log.Println("Gen Duration:", genDuration)
for partyID := 0; partyID < K; partyID++ {
parties[partyID].SkShare = skShares[partyID]
parties[partyID].Seed = seeds
parties[partyID].MACKeys = MACKeys[partyID]
}
// Create maps to collect durations for this run
signRound1Durations := make(map[int]time.Duration)
signRound2PreprocessDurations := make(map[int]time.Duration)
signRound2Durations := make(map[int]time.Duration)
// SIGNATURE ROUND 1
mu := "Message"
sid := 1
PRFKey := primitives.GenerateRandomSeed()
log.Println("Generating lagrange coefficients...")
D := make(map[int]structs.Matrix[ring.Poly])
MACs := make(map[int]map[int][]byte)
for _, partyID := range T {
r.NTT(lagrangeCoeffs[partyID], lagrangeCoeffs[partyID])
r.MForm(lagrangeCoeffs[partyID], lagrangeCoeffs[partyID])
parties[partyID].Lambda = lagrangeCoeffs[partyID]
parties[partyID].Seed = seeds
log.Println("Sign Round 1, party", partyID)
start = time.Now()
var r1err error
D[partyID], MACs[partyID], r1err = parties[partyID].SignRound1(A, sid, []byte(PRFKey), T)
if r1err != nil {
log.Fatalf("SignRound1 failed for party %d: %v", partyID, r1err)
}
signRound1Durations[partyID] = time.Since(start)
}
// SIGNATURE ROUND 2
z := make(map[int]structs.Vector[ring.Poly])
for _, partyID := range T {
log.Println("Sign Round 2 preprocess, party", partyID)
start = time.Now()
valid, DSum, hash := parties[partyID].SignRound2Preprocess(A, b, D, MACs, sid, T)
if !valid {
log.Fatalf("MAC verification failed for party %d", partyID)
}
signRound2PreprocessDurations[partyID] = time.Since(start)
log.Println("Sign round 2 party", partyID)
start = time.Now()
z[partyID] = parties[partyID].SignRound2(A, b, DSum, sid, mu, T, []byte(PRFKey), hash)
signRound2Durations[partyID] = time.Since(start)
}
// SIGNATURE FINALIZE
log.Println("finalizing...")
finalParty := parties[0]
start = time.Now()
_, sig, Delta := finalParty.SignFinalize(z, A, b)
finalizeDuration = time.Since(start)
// Verify the signature
start = time.Now()
valid := Verify(r, r_xi, r_nu, sig, A, mu, b, finalParty.C, Delta)
verifyDuration = time.Since(start)
fmt.Printf("Signature Verification Result: %v\n", valid)
// Accumulate durations
totalGenDuration += genDuration
totalFinalizeDuration += finalizeDuration
totalVerifyDuration += verifyDuration
// Accumulate phase durations
for partyID, duration := range signRound1Durations {
totalSignRound1Durations[partyID] += float64(duration.Nanoseconds()) / 1e6
}
for partyID, duration := range signRound2PreprocessDurations {
totalSignRound2PreprocessDurations[partyID] += float64(duration.Nanoseconds()) / 1e6
}
for partyID, duration := range signRound2Durations {
totalSignRound2Durations[partyID] += float64(duration.Nanoseconds()) / 1e6
}
}
// Print averaged durations
fmt.Println("Averaged durations over", x, "runs:")
fmt.Printf("Gen duration: %.3f ms\n", float64(totalGenDuration.Nanoseconds())/1e6/float64(x))
fmt.Printf("Finalize duration: %.3f ms\n", float64(totalFinalizeDuration.Nanoseconds())/1e6/float64(x))
fmt.Printf("Verify duration: %.3f ms\n", float64(totalVerifyDuration.Nanoseconds())/1e6/float64(x))
// Calculate and print averaged statistics for each phase
printAveragedStats("Signature Round 1", totalSignRound1Durations, x)
printAveragedStats("Signature Round 2 Preprocess", totalSignRound2PreprocessDurations, x)
printAveragedStats("Signature Round 2", totalSignRound2Durations, x)
// Calculate and print total signing and offline durations
totalSigningDurations := make(map[int]float64)
for partyID := range totalSignRound1Durations {
totalSigningDurations[partyID] = totalSignRound1Durations[partyID] + totalSignRound2PreprocessDurations[partyID] + totalSignRound2Durations[partyID]
}
printAveragedStats("Total Signing", totalSigningDurations, x)
}
// printAveragedStats prints the mean, median, and standard deviation for a map of durations averaged over x runs
func printAveragedStats(phaseName string, totalDurations map[int]float64, x int) {
var values []float64
for _, totalDuration := range totalDurations {
values = append(values, totalDuration/float64(x))
}
mean, _ := stats.Mean(values)
median, _ := stats.Median(values)
stddev, _ := stats.StandardDeviation(values)
fmt.Printf("%s averaged duration stats over %d runs:\n", phaseName, x)
fmt.Printf(" Mean: %.3f ms\n", mean)
fmt.Printf(" Median: %.3f ms\n", median)
fmt.Printf(" Standard Deviation: %.3f ms\n", stddev)
}