Files
corona/reshare/fuzz_commit_complaint_test.go
T
Hanzo AI a2c1d1da31 corona: symmetric domain separation — PULSAR-* tags -> CORONA-*
Leftover from Corona's "Pulsar-R" lineage. Pulsar (M-LWE) and Corona
(R-LWE) are independent constructions with separate hardness assumptions,
so their cSHAKE personalisation strings must be distinct.

Changes (Go, non-luxcpp):
  - hash tags: PULSAR-HC-v1 -> CORONA-HC-v1, etc. (HU, TRANSCRIPT, PRF, MAC, PAIRWISE)
  - profile IDs: "Pulsar-SHA3" -> "Corona-SHA3", "Pulsar-BLAKE3" -> "Corona-BLAKE3"
  - context strings: pulsar.dkg2.A.v1 -> corona.dkg2.A.v1, etc.
  - env vars: PULSAR_RESHARE_KAT_PATH -> CORONA_RESHARE_KAT_PATH, etc.
  - struct names: pulsarSHA3 -> coronaSHA3
  - KAT derive roots: sign_e2e_pulsar -> sign_e2e_corona

What's preserved (different scope):
  - luxcpp/crypto/pulsar/* path references in comments (separate repo,
    out of scope; the C++ side will rename in its own commit)
  - Cross-runtime KAT files on disk (will regenerate next CI run)

All 11 packages test green: dkg, dkg2, hash, keyera, networking,
primitives, reshare, sign, threshold, utils, wire.
2026-05-13 14:19:13 -07:00

145 lines
4.3 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Reshare commit + complaint wire-format fuzz harnesses.
//
// FuzzReshareCommitDigest — CommitDigest over malformed Vector[Poly] bytes
// FuzzReshareComplaintMessage — canonical Complaint.Bytes() parser
//
// Property: every decoder path under fuzzMaxRawSize never escapes a
// panic, even on attacker-controlled length-prefix inputs
// (luxfi/lattice#2).
package reshare
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"github.com/luxfi/corona/hash"
"github.com/luxfi/corona/utils"
"github.com/luxfi/lattice/v7/ring"
"github.com/luxfi/lattice/v7/utils/structs"
)
const fuzzMaxRawSize = 1024
// fuzzCommitDigestRecover invokes CommitDigest against a 1-element
// commit vector reconstructed from raw bytes, with a recover boundary
// to neutralize the upstream-lattigo DoS path (luxfi/lattice#2).
func fuzzCommitDigestRecover(raw []byte) (err error) {
if len(raw) > fuzzMaxRawSize {
return fmt.Errorf("input exceeds fuzzMaxRawSize")
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("commit-digest panic recovered: %v", r)
}
}()
r, perr := ring.NewRing(1<<10, []uint64{0x7ffffd8001})
if perr != nil {
return perr
}
v := utils.InitializeVector(r, 4)
if _, derr := v.ReadFrom(bytes.NewReader(raw)); derr != nil {
return derr
}
_ = CommitDigest([]structs.Vector[ring.Poly]{v}, hash.Default())
return nil
}
// parseComplaintBytes is the inverse of Complaint.Bytes — a
// length-prefix-aware structural parser. It is intentionally
// strict: rejects truncated inputs, oversized evidence claims, and
// version-tag mismatches with a clean error rather than a panic.
func parseComplaintBytes(raw []byte) (*Complaint, error) {
const versionTag = "corona.reshare.complaint.v1"
if len(raw) < len(versionTag)+32+4+4+1+4 {
return nil, fmt.Errorf("truncated: %d bytes", len(raw))
}
if string(raw[:len(versionTag)]) != versionTag {
return nil, fmt.Errorf("bad version tag")
}
off := len(versionTag)
c := &Complaint{}
copy(c.TranscriptHash[:], raw[off:off+32])
off += 32
c.SenderID = int(binary.BigEndian.Uint32(raw[off : off+4]))
off += 4
c.ComplainerID = int(binary.BigEndian.Uint32(raw[off : off+4]))
off += 4
c.Reason = ComplaintReason(raw[off])
off++
evLen := binary.BigEndian.Uint32(raw[off : off+4])
off += 4
if int(evLen) > fuzzMaxRawSize {
return nil, fmt.Errorf("evidence length %d exceeds cap", evLen)
}
if off+int(evLen) > len(raw) {
return nil, fmt.Errorf("evidence truncated: claim %d, have %d",
evLen, len(raw)-off)
}
c.Evidence = append([]byte(nil), raw[off:off+int(evLen)]...)
return c, nil
}
// addSmallSeeds adds a small seed corpus.
func addSmallSeeds(f *testing.F) {
f.Add([]byte{})
f.Add([]byte{0x00})
f.Add(bytes.Repeat([]byte{0xff}, 32))
f.Add(append([]byte("corona.reshare.complaint.v1"), bytes.Repeat([]byte{0x00}, 41)...))
f.Add(append([]byte("corona.reshare.complaint.v1"),
append(bytes.Repeat([]byte{0x00}, 41),
[]byte{0xff, 0xff, 0xff, 0xff}...)...)) // huge evidence claim
}
// FuzzReshareCommitDigest fuzzes the commit-digest surface (lattigo
// Vector decode + transcript hash). Property: 0 panics under the
// fuzz max raw size cap.
func FuzzReshareCommitDigest(f *testing.F) {
addSmallSeeds(f)
f.Fuzz(func(t *testing.T, raw []byte) {
_ = fuzzCommitDigestRecover(raw)
})
}
// FuzzReshareComplaintMessage fuzzes the structural complaint parser.
// Property: the parser cleanly errors on malformed inputs and never
// panics, even on huge evidence-length claims.
func FuzzReshareComplaintMessage(f *testing.F) {
addSmallSeeds(f)
f.Fuzz(func(t *testing.T, raw []byte) {
_, _ = parseComplaintBytes(raw)
})
}
// TestFuzzCorpus_ReshareCommitDigestReplay replays the small-seed
// corpus deterministically.
func TestFuzzCorpus_ReshareCommitDigestReplay(t *testing.T) {
for _, raw := range [][]byte{
{},
{0x00},
bytes.Repeat([]byte{0xff}, 32),
} {
_ = fuzzCommitDigestRecover(raw)
}
}
// TestFuzzCorpus_ReshareComplaintReplay confirms the parser rejects
// malformed inputs cleanly.
func TestFuzzCorpus_ReshareComplaintReplay(t *testing.T) {
for _, raw := range [][]byte{
{},
{0x00},
append([]byte("corona.reshare.complaint.v1"), bytes.Repeat([]byte{0x00}, 41)...),
} {
_, _ = parseComplaintBytes(raw)
}
}