mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
Hash walked the commitment map D by dense index 0..len(D)-1, but D is
keyed by party id. For any signer set that is not {0,1,...,k-1} that walk
read absent low indices and skipped the present high ones, so the parties
at the high indices could replace their round-1 commitment without moving
the digest that seeds u and the per-pair masks — a signer could equivocate
its commitment after the fact. Walk D by the agreed set T instead, which
is already written into the transcript, so every commitment is bound in a
fixed order.
Co-authored-by: Hanzo Dev <[email protected]>
196 lines
6.3 KiB
Go
196 lines
6.3 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/lattice/v7/ring"
|
|
"github.com/luxfi/lattice/v7/utils/structs"
|
|
)
|
|
|
|
func bindingRing(t *testing.T) *ring.Ring {
|
|
t.Helper()
|
|
r, err := ring.NewRing(256, []uint64{8380417})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// markedCommitment builds a commitment matrix carrying one distinguishing
|
|
// coefficient, so a change to it is a change to the bytes the transcript
|
|
// should cover.
|
|
func markedCommitment(r *ring.Ring, mark uint64) structs.Matrix[ring.Poly] {
|
|
m := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range m {
|
|
m[i] = make(structs.Vector[ring.Poly], 2)
|
|
for j := range m[i] {
|
|
m[i][j] = r.NewPoly()
|
|
}
|
|
}
|
|
m[0][0].Coeffs[0][0] = mark
|
|
return m
|
|
}
|
|
|
|
// TestHashBindsEveryCommitment is the binding property of the round-2
|
|
// transcript: the digest that seeds u and the per-pair masks must cover the
|
|
// round-1 commitment of every party in the agreed set.
|
|
//
|
|
// Hash walks D by dense index 0..len(D)-1 instead of by the agreed set T. For
|
|
// a signer set that is not {0, 1, ..., k-1} that walk reads absent entries and
|
|
// skips present ones, so the parties at the high indices can replace their
|
|
// commitments freely without moving the digest.
|
|
func TestHashBindsEveryCommitment(t *testing.T) {
|
|
r := bindingRing(t)
|
|
A := make(structs.Matrix[ring.Poly], 2)
|
|
for i := range A {
|
|
A[i] = structs.Vector[ring.Poly]{r.NewPoly(), r.NewPoly()}
|
|
}
|
|
b := structs.Vector[ring.Poly]{r.NewPoly(), r.NewPoly()}
|
|
|
|
for _, T := range [][]int{
|
|
{0, 1, 2},
|
|
{2, 3, 4},
|
|
{0, 2, 4},
|
|
{5, 6, 7},
|
|
} {
|
|
t.Run("", func(t *testing.T) {
|
|
base := map[int]structs.Matrix[ring.Poly]{}
|
|
for i, id := range T {
|
|
base[id] = markedCommitment(r, uint64(i+1))
|
|
}
|
|
baseline := Hash(nil, A, b, base, 1, T)
|
|
|
|
for _, id := range T {
|
|
altered := map[int]structs.Matrix[ring.Poly]{}
|
|
for k, v := range base {
|
|
altered[k] = v
|
|
}
|
|
altered[id] = markedCommitment(r, 0xDEADBEEF)
|
|
|
|
if bytes.Equal(baseline, Hash(nil, A, b, altered, 1, T)) {
|
|
t.Errorf("signer set %v: replacing the commitment of party %d leaves the transcript unchanged", T, id)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHashBindsTheSessionAndTheSignerSet covers the other two transcript
|
|
// inputs. Two sessions, or two signer sets, must never share a digest — the
|
|
// digest is what a replayed partial response would have to match.
|
|
func TestHashBindsTheSessionAndTheSignerSet(t *testing.T) {
|
|
r := bindingRing(t)
|
|
A := structs.Matrix[ring.Poly]{{*r.NewPoly().CopyNew()}}
|
|
b := structs.Vector[ring.Poly]{r.NewPoly()}
|
|
D := map[int]structs.Matrix[ring.Poly]{0: markedCommitment(r, 1), 1: markedCommitment(r, 2)}
|
|
|
|
baseline := Hash(nil, A, b, D, 1, []int{0, 1})
|
|
|
|
if bytes.Equal(baseline, Hash(nil, A, b, D, 2, []int{0, 1})) {
|
|
t.Error("the transcript does not bind the session id")
|
|
}
|
|
if bytes.Equal(baseline, Hash(nil, A, b, D, 1, []int{1, 0})) {
|
|
t.Error("the transcript does not bind the order of the signer set")
|
|
}
|
|
if bytes.Equal(baseline, Hash(nil, A, b, D, 1, []int{0, 1, 2})) {
|
|
t.Error("the transcript does not bind the size of the signer set")
|
|
}
|
|
}
|
|
|
|
// TestHashBindsThePublicKey — the transcript names the key the round is
|
|
// signing under. Two eras that share a digest would let a partial response
|
|
// from one aggregate under the other.
|
|
func TestHashBindsThePublicKey(t *testing.T) {
|
|
r := bindingRing(t)
|
|
D := map[int]structs.Matrix[ring.Poly]{0: markedCommitment(r, 1)}
|
|
T := []int{0}
|
|
|
|
mkA := func(mark uint64) structs.Matrix[ring.Poly] {
|
|
p := r.NewPoly()
|
|
p.Coeffs[0][0] = mark
|
|
return structs.Matrix[ring.Poly]{{p}}
|
|
}
|
|
mkB := func(mark uint64) structs.Vector[ring.Poly] {
|
|
p := r.NewPoly()
|
|
p.Coeffs[0][0] = mark
|
|
return structs.Vector[ring.Poly]{p}
|
|
}
|
|
|
|
baseline := Hash(nil, mkA(1), mkB(1), D, 1, T)
|
|
if bytes.Equal(baseline, Hash(nil, mkA(2), mkB(1), D, 1, T)) {
|
|
t.Error("the transcript does not bind the public matrix A")
|
|
}
|
|
if bytes.Equal(baseline, Hash(nil, mkA(1), mkB(2), D, 1, T)) {
|
|
t.Error("the transcript does not bind the rounded public key b")
|
|
}
|
|
}
|
|
|
|
// TestDeriveSessionIDIsInjectiveOverItsInputs — the session identifier is the
|
|
// deterministic half of the nonce key. Two distinct (sid, T) that mapped to
|
|
// one identifier would put two signatures on one nonce stream whenever the
|
|
// hedge salt is pinned, which is exactly what the KAT harnesses do.
|
|
func TestDeriveSessionIDIsInjectiveOverItsInputs(t *testing.T) {
|
|
seen := map[[SessionIDSize]byte]string{}
|
|
record := func(label string, id [SessionIDSize]byte) {
|
|
if prev, dup := seen[id]; dup {
|
|
t.Errorf("%s and %s derive the same session identifier", prev, label)
|
|
}
|
|
seen[id] = label
|
|
}
|
|
|
|
for _, c := range []struct {
|
|
label string
|
|
sid int
|
|
T []int
|
|
}{
|
|
{"sid=0 T={}", 0, nil},
|
|
{"sid=0 T={0}", 0, []int{0}},
|
|
{"sid=1 T={0}", 1, []int{0}},
|
|
{"sid=0 T={1}", 0, []int{1}},
|
|
{"sid=0 T={0,1}", 0, []int{0, 1}},
|
|
{"sid=0 T={1,0}", 0, []int{1, 0}},
|
|
{"sid=1 T={0,1}", 1, []int{0, 1}},
|
|
{"sid=-1 T={0}", -1, []int{0}},
|
|
} {
|
|
record(c.label, DeriveSessionID(nil, c.sid, c.T))
|
|
}
|
|
}
|
|
|
|
// TestGenerateMACBindsTheDirection — the tag party i sends to party j must not
|
|
// be the tag j sends to i, or a peer can reflect a tag back and pass the gate
|
|
// under the shared pairwise key.
|
|
func TestGenerateMACBindsTheDirection(t *testing.T) {
|
|
r := bindingRing(t)
|
|
D := markedCommitment(r, 7)
|
|
key := bytes.Repeat([]byte{0xA5}, 32)
|
|
T := []int{0, 1}
|
|
|
|
iToJ := GenerateMAC(nil, D, key, 0, 1, T, 1, false)
|
|
jVerifies := GenerateMAC(nil, D, key, 1, 1, T, 0, true)
|
|
if !bytes.Equal(iToJ, jVerifies) {
|
|
t.Fatal("positive control failed: the sender and the verifier disagree on the tag")
|
|
}
|
|
|
|
reflected := GenerateMAC(nil, D, key, 1, 1, T, 0, false)
|
|
if bytes.Equal(iToJ, reflected) {
|
|
t.Error("the tag i sends to j equals the tag j sends to i")
|
|
}
|
|
|
|
if bytes.Equal(iToJ, GenerateMAC(nil, D, key, 0, 2, T, 1, false)) {
|
|
t.Error("the tag does not bind the session id")
|
|
}
|
|
if bytes.Equal(iToJ, GenerateMAC(nil, D, key, 0, 1, []int{0, 1, 2}, 1, false)) {
|
|
t.Error("the tag does not bind the signer set")
|
|
}
|
|
if bytes.Equal(iToJ, GenerateMAC(nil, markedCommitment(r, 8), key, 0, 1, T, 1, false)) {
|
|
t.Error("the tag does not bind the commitment")
|
|
}
|
|
if bytes.Equal(iToJ, GenerateMAC(nil, D, bytes.Repeat([]byte{0x5A}, 32), 0, 1, T, 1, false)) {
|
|
t.Error("the tag does not bind the pairwise key")
|
|
}
|
|
}
|