Files
corona/primitives/hash_test.go
T
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

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")
}