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.
274 lines
8.5 KiB
Go
274 lines
8.5 KiB
Go
// Package main is the Corona sign+verify KAT oracle.
|
|
//
|
|
// Given a fixed master seed, it emits a deterministic JSON file of
|
|
// known-answer test vectors covering the full LP-073 Corona threshold
|
|
// signature pipeline (Gen + SignRound1 + SignRound2{Preprocess,} +
|
|
// SignFinalize + Verify) for the canonical (t, n) configurations
|
|
// 2-of-3, 3-of-5, 5-of-7, 7-of-11. The C++ port at
|
|
// luxcpp/crypto/corona/cpp/sign/ replays these entries byte-equal.
|
|
//
|
|
// Corona's sign/sign.go is byte-identical to corona/sign/sign.go (the
|
|
// only diff is the import path), so the JSON shape mirrors the existing
|
|
// corona sign_verify_e2e KAT.
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./cmd/sign_oracle --out <dir>
|
|
//
|
|
// Determinism is required. Two runs with the same MasterSeed produce
|
|
// byte-equal JSON files.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/luxfi/corona/primitives"
|
|
"github.com/luxfi/corona/sign"
|
|
"github.com/luxfi/corona/utils"
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
"github.com/luxfi/lattice/v7/utils/sampling"
|
|
"github.com/luxfi/lattice/v7/utils/structs"
|
|
"github.com/zeebo/blake3"
|
|
)
|
|
|
|
// MasterSeed is the deterministic root of all KAT generation. Changing
|
|
// it invalidates every downstream port's expected outputs, so it stays
|
|
// fixed across sessions.
|
|
const MasterSeed uint64 = 0xDEADBEEFCAFEBABE
|
|
|
|
// derive expands MasterSeed into a per-KAT 32-byte sub-seed via BLAKE3
|
|
// with a domain-separation tag. This keeps each KAT independent of the
|
|
// others (so adding a future KAT does not perturb existing files).
|
|
func derive(tag string) []byte {
|
|
h := blake3.New()
|
|
var seedBytes [8]byte
|
|
binary.BigEndian.PutUint64(seedBytes[:], MasterSeed)
|
|
_, _ = h.Write(seedBytes[:])
|
|
_, _ = h.Write([]byte(tag))
|
|
return h.Sum(nil)[:32]
|
|
}
|
|
|
|
// expand returns n bytes of BLAKE3 stream from key (deterministic).
|
|
func expand(key []byte, n int) []byte {
|
|
out := make([]byte, n)
|
|
xof := blake3.New()
|
|
_, _ = xof.Write(key)
|
|
_, _ = xof.Digest().Read(out)
|
|
return out
|
|
}
|
|
|
|
// uint64SliceToHex emits a fixed-width 16-hex-char-per-uint64 BE encoding.
|
|
func uint64SliceToHex(c []uint64) string {
|
|
out := make([]byte, 0, len(c)*16)
|
|
for _, v := range c {
|
|
var b [8]byte
|
|
binary.BigEndian.PutUint64(b[:], v)
|
|
out = append(out, []byte(hex.EncodeToString(b[:]))...)
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func writeJSON(path string, v any) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
b, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, b, 0o644)
|
|
}
|
|
|
|
func hashMatrix(m structs.Matrix[ring.Poly]) string {
|
|
var buf bytes.Buffer
|
|
_, _ = m.WriteTo(&buf)
|
|
h := sha256.Sum256(buf.Bytes())
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
func hashVector(v structs.Vector[ring.Poly]) string {
|
|
var buf bytes.Buffer
|
|
_, _ = v.WriteTo(&buf)
|
|
h := sha256.Sum256(buf.Bytes())
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
func hashVectors(m map[int]structs.Vector[ring.Poly], n int) []string {
|
|
out := make([]string, n)
|
|
for i := 0; i < n; i++ {
|
|
out[i] = hashVector(m[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
type signEntry struct {
|
|
T int `json:"t"`
|
|
N int `json:"n"`
|
|
SeedHex string `json:"seed_hex"`
|
|
MsgHex string `json:"msg_hex"`
|
|
Msg string `json:"msg"`
|
|
AHashHex string `json:"a_hash_hex"`
|
|
BTildeHashHex string `json:"btilde_hash_hex"`
|
|
SkSharesHex []string `json:"sk_shares_hash_hex"`
|
|
PartialSigsHex []string `json:"partial_sigs_hash_hex"`
|
|
CHex string `json:"c_hex"`
|
|
ZHex string `json:"z_hex"`
|
|
DeltaHex string `json:"delta_hex"`
|
|
Verify bool `json:"verify"`
|
|
}
|
|
|
|
func emitSignVerify(outDir string) error {
|
|
root := derive("sign_e2e_corona")
|
|
cfgs := []struct{ t, n int }{
|
|
{2, 3}, {3, 5}, {5, 7}, {7, 11},
|
|
}
|
|
// One canonical message per (t,n). The 4-entry KAT requirement keeps
|
|
// the JSON small while still pinning every Sign+Verify byte path.
|
|
msgPerCfg := []string{"alpha", "beta", "gamma", "delta"}
|
|
|
|
out := struct {
|
|
Description string `json:"description"`
|
|
Entries []signEntry `json:"entries"`
|
|
}{
|
|
Description: "Full Corona Sign+Verify round-trip (LP-073 Q-witness). " +
|
|
"For each (t,n,msg,seed): Gen → SignRound1 (all parties) → " +
|
|
"SignRound2Preprocess+SignRound2 (all parties) → SignFinalize → " +
|
|
"Verify. Corona's sign module is byte-identical to the original construction at " +
|
|
"the Go source level (only the import path differs). The current " +
|
|
"KAT signs with K=Threshold=n; the t field documents the " +
|
|
"threshold-aware variant for downstream use. SHA-256 hashes are " +
|
|
"used for large fields (A, BTilde, sk shares, partial sigs) to " +
|
|
"keep file size finite while still binding the C++ port " +
|
|
"byte-for-byte.",
|
|
}
|
|
|
|
var q uint64 = sign.Q
|
|
for ci, cfg := range cfgs {
|
|
seed := expand(append(root, []byte(fmt.Sprintf("cfg-%d", ci))...), 32)
|
|
msg := msgPerCfg[ci]
|
|
|
|
// Reset global stateful randomness to ensure determinism across
|
|
// calls. The `utils.PrecomputedRandomness` is process-wide.
|
|
utils.PrecomputedRandomness = nil
|
|
utils.RandomnessIndex = 0
|
|
|
|
// Force K = n, Threshold = n so the optimized t=k Shamir path runs.
|
|
sign.K = cfg.n
|
|
sign.Threshold = cfg.n
|
|
|
|
r, err := ring.NewRing(1<<sign.LogN, []uint64{sign.Q})
|
|
if err != nil {
|
|
return fmt.Errorf("ring.NewRing(Q): %w", err)
|
|
}
|
|
rXi, _ := ring.NewRing(1<<sign.LogN, []uint64{sign.QXi})
|
|
rNu, _ := ring.NewRing(1<<sign.LogN, []uint64{sign.QNu})
|
|
|
|
prng, _ := sampling.NewKeyedPRNG(seed)
|
|
uniformSampler := ring.NewUniformSampler(prng, r)
|
|
|
|
T := make([]int, cfg.n)
|
|
for i := range T {
|
|
T[i] = i
|
|
}
|
|
lagrange := primitives.ComputeLagrangeCoefficients(r, T, big.NewInt(int64(q)))
|
|
|
|
A, skShares, seeds, macKeys, b := sign.Gen(r, rXi, uniformSampler, seed, lagrange)
|
|
|
|
parties := make([]*sign.Party, cfg.n)
|
|
for i := 0; i < cfg.n; i++ {
|
|
prngI, _ := sampling.NewKeyedPRNG(seed)
|
|
usI := ring.NewUniformSampler(prngI, r)
|
|
parties[i] = sign.NewParty(i, r, rXi, rNu, usI)
|
|
parties[i].SkShare = skShares[i]
|
|
parties[i].Seed = seeds
|
|
parties[i].MACKeys = macKeys[i]
|
|
// Pin the hedged-nonce salt to a deterministic, per-party
|
|
// source so the KAT signature bytes reproduce across runs
|
|
// and the C++ runner. Production keeps crypto/rand.
|
|
parties[i].Rand = sign.DeterministicNonceSource(seed, i)
|
|
lambda := r.NewPoly()
|
|
lambda.Copy(lagrange[i])
|
|
r.NTT(lambda, lambda)
|
|
r.MForm(lambda, lambda)
|
|
parties[i].Lambda = lambda
|
|
}
|
|
|
|
sid := 1
|
|
// GenerateRandomSeed pulls the next KeySize bytes from the same
|
|
// PrecomputedRandomness stream that sign.Gen seeded; this makes
|
|
// the prfKey deterministic given `seed` (the C++ runner's
|
|
// recover_prf_key reproduces the same bytes).
|
|
prfKey := primitives.GenerateRandomSeed()
|
|
|
|
D := make(map[int]structs.Matrix[ring.Poly])
|
|
MACs := make(map[int]map[int][]byte)
|
|
for _, pid := range T {
|
|
d, macs, err := parties[pid].SignRound1(A, sid, prfKey, T)
|
|
if err != nil {
|
|
return fmt.Errorf("sign-e2e: SignRound1 party %d: %w", pid, err)
|
|
}
|
|
D[pid], MACs[pid] = d, macs
|
|
}
|
|
|
|
z := make(map[int]structs.Vector[ring.Poly])
|
|
for _, pid := range T {
|
|
ok, DSum, hash := parties[pid].SignRound2Preprocess(A, b, D, MACs, sid, T)
|
|
if !ok {
|
|
return fmt.Errorf("sign-e2e: MAC verify failed t=%d n=%d msg=%q",
|
|
cfg.t, cfg.n, msg)
|
|
}
|
|
z[pid] = parties[pid].SignRound2(A, b, DSum, sid, msg, T, prfKey, hash)
|
|
}
|
|
|
|
final := parties[0]
|
|
c, zSum, delta := final.SignFinalize(z, A, b)
|
|
|
|
ok := sign.Verify(r, rXi, rNu, zSum, A, msg, b, c, delta)
|
|
if !ok {
|
|
return fmt.Errorf("sign-e2e: Verify returned false for cfg=(%d,%d) msg=%q",
|
|
cfg.t, cfg.n, msg)
|
|
}
|
|
|
|
out.Entries = append(out.Entries, signEntry{
|
|
T: cfg.t,
|
|
N: cfg.n,
|
|
SeedHex: hex.EncodeToString(seed),
|
|
MsgHex: hex.EncodeToString([]byte(msg)),
|
|
Msg: msg,
|
|
AHashHex: hashMatrix(A),
|
|
BTildeHashHex: hashVector(b),
|
|
SkSharesHex: hashVectors(skShares, cfg.n),
|
|
PartialSigsHex: hashVectors(z, cfg.n),
|
|
CHex: uint64SliceToHex(c.Coeffs[0]),
|
|
ZHex: hashVector(zSum),
|
|
DeltaHex: hashVector(delta),
|
|
Verify: ok,
|
|
})
|
|
}
|
|
|
|
return writeJSON(filepath.Join(outDir, "sign_kat.json"), out)
|
|
}
|
|
|
|
func main() {
|
|
out := flag.String("out", "", "output directory")
|
|
flag.Parse()
|
|
if *out == "" {
|
|
fmt.Fprintln(os.Stderr, "usage: sign_oracle --out <dir>")
|
|
os.Exit(2)
|
|
}
|
|
if err := emitSignVerify(*out); err != nil {
|
|
fmt.Fprintf(os.Stderr, "emit: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("wrote %s/sign_kat.json (4 entries)\n", *out)
|
|
}
|