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.
453 lines
12 KiB
Go
453 lines
12 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package primitives
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"github.com/luxfi/corona/hash"
|
|
"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"
|
|
)
|
|
|
|
func TestPRNGKey(t *testing.T) {
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create a test secret key share
|
|
prng, _ := sampling.NewPRNG()
|
|
sampler := ring.NewUniformSampler(prng, r)
|
|
skShare := make(structs.Vector[ring.Poly], 3)
|
|
for i := range skShare {
|
|
skShare[i] = sampler.ReadNew()
|
|
}
|
|
|
|
key := PRNGKey(nil, skShare)
|
|
|
|
if len(key) != 32 {
|
|
t.Errorf("PRNGKey() returned %d bytes, want 32", len(key))
|
|
}
|
|
|
|
// Verify deterministic
|
|
key2 := PRNGKey(nil, skShare)
|
|
for i := range key {
|
|
if key[i] != key2[i] {
|
|
t.Error("PRNGKey() is not deterministic")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateMAC(t *testing.T) {
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create test inputs
|
|
prng, _ := sampling.NewPRNG()
|
|
sampler := ring.NewUniformSampler(prng, r)
|
|
|
|
TildeD := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range TildeD {
|
|
TildeD[i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range TildeD[i] {
|
|
TildeD[i][j] = sampler.ReadNew()
|
|
}
|
|
}
|
|
|
|
MACKey := []byte("test-mac-key-32-bytes-long------")
|
|
partyID := 1
|
|
sid := 1
|
|
T := []int{1, 2, 3}
|
|
otherParty := 2
|
|
|
|
// Test generation mode
|
|
mac := GenerateMAC(nil, TildeD, MACKey, partyID, sid, T, otherParty, false)
|
|
if len(mac) != 32 {
|
|
t.Errorf("GenerateMAC() returned %d bytes, want 32", len(mac))
|
|
}
|
|
|
|
// Test verification mode
|
|
macVerify := GenerateMAC(nil, TildeD, MACKey, partyID, sid, T, otherParty, true)
|
|
if len(macVerify) != 32 {
|
|
t.Errorf("GenerateMAC() in verify mode returned %d bytes, want 32", len(macVerify))
|
|
}
|
|
}
|
|
|
|
func TestGaussianHash(t *testing.T) {
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
hashIn := []byte("test-hash-32-bytes-long---------")
|
|
mu := "test-message"
|
|
sigmaU := 1.0
|
|
boundU := 6.0
|
|
length := 5
|
|
|
|
result := GaussianHash(nil, r, hashIn, mu, sigmaU, boundU, length)
|
|
|
|
if len(result) != length {
|
|
t.Errorf("GaussianHash() returned %d elements, want %d", len(result), length)
|
|
}
|
|
|
|
// Verify deterministic
|
|
result2 := GaussianHash(nil, r, hashIn, mu, sigmaU, boundU, length)
|
|
for i := range result {
|
|
if !r.Equal(result[i], result2[i]) {
|
|
t.Error("GaussianHash() is not deterministic")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPRF(t *testing.T) {
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sd_ij := []byte("seed-data")
|
|
PRFKey := []byte("prf-key-32-bytes-long-----------")
|
|
mu := "message"
|
|
hashIn := []byte("hash-data")
|
|
n := 5
|
|
|
|
result := PRF(nil, r, sd_ij, PRFKey, mu, hashIn, n)
|
|
|
|
if len(result) != n {
|
|
t.Errorf("PRF() returned %d elements, want %d", len(result), n)
|
|
}
|
|
|
|
// Verify deterministic
|
|
result2 := PRF(nil, r, sd_ij, PRFKey, mu, hashIn, n)
|
|
for i := range result {
|
|
if !r.Equal(result[i], result2[i]) {
|
|
t.Error("PRF() is not deterministic")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHash(t *testing.T) {
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prng, _ := sampling.NewPRNG()
|
|
sampler := ring.NewUniformSampler(prng, r)
|
|
|
|
// Create test inputs
|
|
A := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range A {
|
|
A[i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range A[i] {
|
|
A[i][j] = sampler.ReadNew()
|
|
}
|
|
}
|
|
|
|
b := make(structs.Vector[ring.Poly], 2)
|
|
for i := range b {
|
|
b[i] = sampler.ReadNew()
|
|
}
|
|
|
|
D := make(map[int]structs.Matrix[ring.Poly])
|
|
for k := 0; k < 2; k++ {
|
|
D[k] = make(structs.Matrix[ring.Poly], 2)
|
|
for i := range D[k] {
|
|
D[k][i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range D[k][i] {
|
|
D[k][i][j] = sampler.ReadNew()
|
|
}
|
|
}
|
|
}
|
|
|
|
sid := 1
|
|
T := []int{1, 2}
|
|
|
|
result := Hash(nil, A, b, D, sid, T)
|
|
|
|
if len(result) != 32 {
|
|
t.Errorf("Hash() returned %d bytes, want 32", len(result))
|
|
}
|
|
|
|
// Verify deterministic
|
|
result2 := Hash(nil, A, b, D, sid, T)
|
|
for i := range result {
|
|
if result[i] != result2[i] {
|
|
t.Error("Hash() is not deterministic")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLowNormHash(t *testing.T) {
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prng, _ := sampling.NewPRNG()
|
|
sampler := ring.NewUniformSampler(prng, r)
|
|
|
|
// Create test inputs
|
|
A := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range A {
|
|
A[i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range A[i] {
|
|
A[i][j] = sampler.ReadNew()
|
|
}
|
|
}
|
|
|
|
b := make(structs.Vector[ring.Poly], 2)
|
|
for i := range b {
|
|
b[i] = sampler.ReadNew()
|
|
}
|
|
|
|
h := make(structs.Vector[ring.Poly], 2)
|
|
for i := range h {
|
|
h[i] = sampler.ReadNew()
|
|
}
|
|
|
|
mu := "message"
|
|
kappa := 10
|
|
|
|
result := LowNormHash(nil, r, A, b, h, mu, kappa)
|
|
|
|
if result.N() == 0 {
|
|
t.Error("LowNormHash() returned invalid polynomial")
|
|
}
|
|
|
|
// Verify deterministic
|
|
result2 := LowNormHash(nil, r, A, b, h, mu, kappa)
|
|
if !r.Equal(result, result2) {
|
|
t.Error("LowNormHash() is not deterministic")
|
|
}
|
|
}
|
|
|
|
func TestGenerateRandomSeed(t *testing.T) {
|
|
// Initialize precomputed randomness for the test
|
|
testKey := []byte("test-key-for-randomness-generation")
|
|
utils.PrecomputeRandomness(1024, testKey) // Precompute enough randomness for the test
|
|
|
|
seed := GenerateRandomSeed()
|
|
|
|
if len(seed) != 32 {
|
|
t.Errorf("GenerateRandomSeed() returned %d bytes, want 32", len(seed))
|
|
}
|
|
|
|
// Verify randomness (two calls should produce different results)
|
|
seed2 := GenerateRandomSeed()
|
|
same := true
|
|
for i := range seed {
|
|
if seed[i] != seed2[i] {
|
|
same = false
|
|
break
|
|
}
|
|
}
|
|
if same {
|
|
t.Error("GenerateRandomSeed() appears to be deterministic")
|
|
}
|
|
}
|
|
|
|
// TestCoronaSHA3VsBLAKE3_DistinctOutput is the F22 fix surfaced as a test:
|
|
// the same byte-identical inputs must produce different bytes under
|
|
// Corona-SHA3 and Corona-BLAKE3 across every Sign-path primitive. If two
|
|
// suites collide here, customization tags or framing are broken.
|
|
func TestCoronaSHA3VsBLAKE3_DistinctOutput(t *testing.T) {
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sha3 := hash.NewCoronaSHA3()
|
|
bl3 := hash.NewCoronaBLAKE3()
|
|
|
|
prng, _ := sampling.NewPRNG()
|
|
sampler := ring.NewUniformSampler(prng, r)
|
|
|
|
t.Run("PRNGKey", func(t *testing.T) {
|
|
skShare := make(structs.Vector[ring.Poly], 3)
|
|
for i := range skShare {
|
|
skShare[i] = sampler.ReadNew()
|
|
}
|
|
a := PRNGKey(sha3, skShare)
|
|
b := PRNGKey(bl3, skShare)
|
|
if bytes.Equal(a, b) {
|
|
t.Fatal("PRNGKey: SHA3 and BLAKE3 produced identical bytes for same input")
|
|
}
|
|
})
|
|
|
|
t.Run("DeriveSessionID", func(t *testing.T) {
|
|
T := []int{0, 1, 2}
|
|
a := DeriveSessionID(sha3, 42, T)
|
|
// Distinct suite → distinct SessionID (F22 cross-profile sep).
|
|
if a == DeriveSessionID(bl3, 42, T) {
|
|
t.Fatal("DeriveSessionID: SHA3 and BLAKE3 produced identical SessionID")
|
|
}
|
|
// Distinct sid → distinct SessionID.
|
|
if a == DeriveSessionID(sha3, 43, T) {
|
|
t.Fatal("DeriveSessionID: sid not bound")
|
|
}
|
|
// Distinct signer set → distinct SessionID.
|
|
if a == DeriveSessionID(sha3, 42, []int{0, 1, 3}) {
|
|
t.Fatal("DeriveSessionID: signer set T not bound")
|
|
}
|
|
// Deterministic: same inputs → same SessionID.
|
|
if a != DeriveSessionID(sha3, 42, T) {
|
|
t.Fatal("DeriveSessionID: not deterministic")
|
|
}
|
|
})
|
|
|
|
t.Run("PRNGKeyForRound", func(t *testing.T) {
|
|
skShare := make(structs.Vector[ring.Poly], 3)
|
|
for i := range skShare {
|
|
skShare[i] = sampler.ReadNew()
|
|
}
|
|
sid := DeriveSessionID(sha3, 42, []int{0, 1, 2})
|
|
sid2 := DeriveSessionID(sha3, 43, []int{0, 1, 2})
|
|
var salt, salt2 [SessionIDSize]byte
|
|
salt[0], salt2[0] = 0x01, 0x02
|
|
|
|
a := PRNGKeyForRound(sha3, skShare, sid, salt)
|
|
b := PRNGKeyForRound(bl3, skShare, sid, salt)
|
|
if bytes.Equal(a, b) {
|
|
t.Fatal("PRNGKeyForRound: SHA3 and BLAKE3 produced identical bytes for same input")
|
|
}
|
|
// SessionID binding: distinct SessionID, same salt → distinct key.
|
|
if bytes.Equal(a, PRNGKeyForRound(sha3, skShare, sid2, salt)) {
|
|
t.Fatal("PRNGKeyForRound: SessionID not bound into key")
|
|
}
|
|
// Hedge binding: same SessionID, distinct salt → distinct key.
|
|
// This is the property that closes the sid-reuse leak: even if an
|
|
// external sid collision makes SessionID repeat, fresh salt makes
|
|
// the nonce key (hence R) differ.
|
|
if bytes.Equal(a, PRNGKeyForRound(sha3, skShare, sid, salt2)) {
|
|
t.Fatal("PRNGKeyForRound: hedge salt not bound into key")
|
|
}
|
|
// Deterministic in (sessionID, salt).
|
|
if !bytes.Equal(a, PRNGKeyForRound(sha3, skShare, sid, salt)) {
|
|
t.Fatal("PRNGKeyForRound: not deterministic in (sessionID, salt)")
|
|
}
|
|
})
|
|
|
|
t.Run("GenerateMAC", func(t *testing.T) {
|
|
TildeD := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range TildeD {
|
|
TildeD[i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range TildeD[i] {
|
|
TildeD[i][j] = sampler.ReadNew()
|
|
}
|
|
}
|
|
key := []byte("00000000000000000000000000000000")
|
|
a := GenerateMAC(sha3, TildeD, key, 1, 7, []int{1, 2, 3}, 2, false)
|
|
b := GenerateMAC(bl3, TildeD, key, 1, 7, []int{1, 2, 3}, 2, false)
|
|
if bytes.Equal(a, b) {
|
|
t.Fatal("GenerateMAC: SHA3 and BLAKE3 produced identical bytes for same input")
|
|
}
|
|
})
|
|
|
|
t.Run("Hash", func(t *testing.T) {
|
|
A := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range A {
|
|
A[i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range A[i] {
|
|
A[i][j] = sampler.ReadNew()
|
|
}
|
|
}
|
|
bv := make(structs.Vector[ring.Poly], 2)
|
|
for i := range bv {
|
|
bv[i] = sampler.ReadNew()
|
|
}
|
|
D := make(map[int]structs.Matrix[ring.Poly])
|
|
D[0] = A
|
|
a := Hash(sha3, A, bv, D, 1, []int{1, 2})
|
|
b := Hash(bl3, A, bv, D, 1, []int{1, 2})
|
|
if bytes.Equal(a, b) {
|
|
t.Fatal("Hash: SHA3 and BLAKE3 produced identical bytes for same input")
|
|
}
|
|
})
|
|
|
|
t.Run("LowNormHash", func(t *testing.T) {
|
|
A := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range A {
|
|
A[i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range A[i] {
|
|
A[i][j] = sampler.ReadNew()
|
|
}
|
|
}
|
|
bv := make(structs.Vector[ring.Poly], 2)
|
|
hv := make(structs.Vector[ring.Poly], 2)
|
|
for i := range bv {
|
|
bv[i] = sampler.ReadNew()
|
|
hv[i] = sampler.ReadNew()
|
|
}
|
|
a := LowNormHash(sha3, r, A, bv, hv, "msg", 5)
|
|
b := LowNormHash(bl3, r, A, bv, hv, "msg", 5)
|
|
if r.Equal(a, b) {
|
|
t.Fatal("LowNormHash: SHA3 and BLAKE3 sampled identical polynomial for same input")
|
|
}
|
|
})
|
|
|
|
t.Run("PRF", func(t *testing.T) {
|
|
key := []byte("00000000000000000000000000000000")
|
|
seed := []byte("seed-data")
|
|
h := []byte("hash-data")
|
|
a := PRF(sha3, r, seed, key, "msg", h, 5)
|
|
b := PRF(bl3, r, seed, key, "msg", h, 5)
|
|
// Compare coefficient by coefficient — any difference suffices.
|
|
differ := false
|
|
for i := range a {
|
|
if !r.Equal(a[i], b[i]) {
|
|
differ = true
|
|
break
|
|
}
|
|
}
|
|
if !differ {
|
|
t.Fatal("PRF: SHA3 and BLAKE3 produced identical vectors for same input")
|
|
}
|
|
})
|
|
|
|
t.Run("GaussianHash", func(t *testing.T) {
|
|
hashIn := []byte("test-hash-32-bytes-long---------")
|
|
a := GaussianHash(sha3, r, hashIn, "mu", 1.0, 6.0, 5)
|
|
b := GaussianHash(bl3, r, hashIn, "mu", 1.0, 6.0, 5)
|
|
differ := false
|
|
for i := range a {
|
|
if !r.Equal(a[i], b[i]) {
|
|
differ = true
|
|
break
|
|
}
|
|
}
|
|
if !differ {
|
|
t.Fatal("GaussianHash: SHA3 and BLAKE3 produced identical vectors for same input")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestKATsRegenerated documents the cross-suite KAT state.
|
|
//
|
|
// The legacy BLAKE3 KATs in cmd/corona_oracle_v2/ historically reflected
|
|
// raw blake3.New() framing in primitives/hash.go. After the suite
|
|
// refactor, primitives now uses coronaBLAKE3.PRF / coronaBLAKE3.Hu /
|
|
// coronaBLAKE3.MAC which prepend customization tags and length-prefix —
|
|
// so the BLAKE3 oracle output_hex no longer byte-matches pre-refactor
|
|
// transcripts. New Corona-SHA3 KATs are not yet emitted.
|
|
//
|
|
// This is documented in pulsar/CHANGELOG.md as follow-up work. The test
|
|
// here is a guard rail: it fails if anyone hand-edits the legacy BLAKE3
|
|
// JSON in tree before regeneration, so the C++ port team gets a loud
|
|
// signal.
|
|
func TestKATsRegenerated(t *testing.T) {
|
|
t.Skip("legacy BLAKE3 KATs and new Corona-SHA3 KATs land as follow-up; see pulsar/CHANGELOG.md")
|
|
}
|