Files
dkg/transcript/transcript_test.go
zeekay 4a19f2bf25 feat(transcript,channel): SP800-185 transcript + authenticated share-only channels
transcript/: canonical byte-stable MPC transcript. SP 800-185
cSHAKE256/KMAC256 + encoders ported verbatim from the KAT-pinned pulsar
reference. Transcript is append-only with TupleHash256 framing so the
chain-committed TranscriptHash can never suffer boundary confusion.

channel/: authenticated message layer (ML-KEM-768 sealed + ML-DSA-65
signed). TWO structural security fixes over pulsar v0.1:
  - SHARE-ONLY ENVELOPES: Seal carries ONLY the per-recipient Shamir
    share, never the full contribution c_i. The v0.1 leak (every opener
    learned a full contribution => the master secret) is closed by
    construction; TestEnvelope_ShareOnly pins sealed size = share+tag.
  - COMMIT-THEN-REVEAL (component 8): commit.go binds each party's
    contribution before it sees others', killing last-mover key bias.

sign.go authenticates broadcasts/complaints under mandatory context
separation; DirectoryVerifier adapts the identity directory for blame.
Tests: round-trip, share-only regression, tamper/context/dealer
rejection, deterministic encap, context-separated sign/verify,
commit binding+hiding. circl pinned v1.6.3 (consumer-aligned).
2026-06-27 19:39:45 -07:00

120 lines
4.0 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package transcript
import (
"bytes"
"testing"
)
// TestEncoders pins the SP 800-185 §2.3 encoders to the standard's worked
// values (left_encode(0)=0100, right_encode(0)=0001), and checks encode_string
// framing length.
func TestEncoders(t *testing.T) {
if got := leftEncode(0); !bytes.Equal(got, []byte{0x01, 0x00}) {
t.Fatalf("left_encode(0) = %x, want 0100", got)
}
if got := rightEncode(0); !bytes.Equal(got, []byte{0x00, 0x01}) {
t.Fatalf("right_encode(0) = %x, want 0001", got)
}
// left_encode(8*1) for a 1-byte string: byte-length 1 → bit length 8.
if got := leftEncode(8); !bytes.Equal(got, []byte{0x01, 0x08}) {
t.Fatalf("left_encode(8) = %x, want 0108", got)
}
// bytepad pads to a multiple of w.
if got := bytepad([]byte{0xAA}, 8); len(got)%8 != 0 {
t.Fatalf("bytepad length %d not multiple of 8", len(got))
}
}
// TestTranscriptDeterministic checks two transcripts with identical appends
// produce identical hashes, and that order / label / data changes all diverge.
func TestTranscriptDeterministic(t *testing.T) {
build := func() *Transcript {
return New().
Append("round", []byte("r1")).
AppendU32("n", 5).
AppendHash("commit", [32]byte{1, 2, 3})
}
if build().Hash() != build().Hash() {
t.Fatal("identical transcripts must hash equally")
}
base := build().Hash()
// Different data.
diff1 := New().Append("round", []byte("r2")).AppendU32("n", 5).AppendHash("commit", [32]byte{1, 2, 3})
if diff1.Hash() == base {
t.Fatal("different data must diverge")
}
// Different order.
diff2 := New().AppendU32("n", 5).Append("round", []byte("r1")).AppendHash("commit", [32]byte{1, 2, 3})
if diff2.Hash() == base {
t.Fatal("different order must diverge")
}
// Different label.
diff3 := New().Append("ROUND", []byte("r1")).AppendU32("n", 5).AppendHash("commit", [32]byte{1, 2, 3})
if diff3.Hash() == base {
t.Fatal("different label must diverge")
}
}
// TestFramingNoBoundaryConfusion is the load-bearing property: a naive
// concatenation transcript collides on Append("a","bc") vs Append("ab","c");
// the TupleHash framing here must NOT.
func TestFramingNoBoundaryConfusion(t *testing.T) {
h1 := New().Append("a", []byte("bc")).Hash()
h2 := New().Append("ab", []byte("c")).Hash()
if h1 == h2 {
t.Fatal("boundary confusion: framing failed to disambiguate label/data split")
}
// Splitting one append into two must also diverge from one combined.
h3 := New().Append("x", []byte("12")).Hash()
h4 := New().Append("x", []byte("1")).Append("", []byte("2")).Hash()
if h3 == h4 {
t.Fatal("entry-count framing failed to disambiguate")
}
}
// TestDomainSeparation checks that the same payload under two customization
// tags produces different digests.
func TestDomainSeparation(t *testing.T) {
payload := []byte("same-bytes")
a := CommitDigest("TAG-A", payload)
b := CommitDigest("TAG-B", payload)
if a == b {
t.Fatal("customization tag must separate domains")
}
// FuncName separation: a plain SHAKE (empty N) must differ from our domain.
plain := CShake256(payload, 32, "", "TAG-A")
domain := CShake256(payload, 32, FuncName, "TAG-A")
if bytes.Equal(plain, domain) {
t.Fatal("function-name N must separate the luxfi/dkg domain from plain cSHAKE")
}
}
// TestKMAC256_KeyMatters checks KMAC256 depends on the key (a MAC, not a hash).
func TestKMAC256_KeyMatters(t *testing.T) {
msg := []byte("authenticate me")
a := KMAC256([]byte("key-1"), msg, 32, "T")
b := KMAC256([]byte("key-2"), msg, 32, "T")
if bytes.Equal(a, b) {
t.Fatal("KMAC256 must depend on the key")
}
if len(a) != 32 {
t.Fatalf("KMAC256 outLen = %d, want 32", len(a))
}
}
// TestFork checks a forked transcript diverges independently from its parent.
func TestFork(t *testing.T) {
base := New().Append("prefix", []byte("p"))
f := base.Fork()
base.Append("a", []byte("1"))
f.Append("b", []byte("2"))
if base.Hash() == f.Hash() {
t.Fatal("forked transcripts must evolve independently")
}
}