Files
warp/signature_scheme_fuzz_test.go
zeekay 443aa53f81 feat(warp)!: typed finality-evidence model (Beam/Pulsar/Corona/P3Q) + Corona relabel
Decomplect the PQ finality lanes into a typed, impossible-to-confuse model where
KIND chooses the verifier and SUITE only chooses parameterization (fail-closed on
mismatch). Kills the prior conflation where the corona-backed lattice lane was
mislabeled "Pulse/Pulsar-SHA3".

Four strict evidence kinds — each its own verifier + suite + key registry:
  beam-bls                BLS12-381 aggregate (fast classical)
  pulsar-threshold-mldsa  threshold ML-DSA, verified as a STANDARD FIPS-204
                          ML-DSA signature under a group public key; the TALUS
                          dealerless machinery is entirely offline (VerifyPulsar
                          imports only mldsa65 — enforced by an import-hygiene test)
  corona-ringtail         Ringtail/Module-LWE lattice threshold (the corona kernel)
  p3q-mldsa-rollup        succinct rollup of independent ML-DSA-65 sigs; strict
                          ONLY when its proof system is PQ (classical SNARK gated)
Plus the raw mldsa-cert-set as a NON-strict availability/audit artifact.

Subject-agnostic verifiers take (subject, evidence, keyEra): one dispatch serves
the warp cross-chain digest D and the consensus finality subject
  M = keccak256("QUASAR_FINALITY_V1" || chainID || height || round || blockID ||
      stateRoot || signerSetID || keyEraID || generation || pChainHeight || policyID)
folding generation + pChainHeight + policyID so a signature binds its era, its
validator snapshot, and its tier (tier-downgrade replay fails closed).

Policy tiers (BLS_FAST / HYBRID_PQ_CHECKPOINT / STRICT_QUASAR / RECOVERY) select
which lanes are required — orthogonal to verification and to keys. PulsarKeyEra
and CoronaKeyEra are symmetric but DISTINCT registry types; P3Q/cert-set resolve a
per-validator SignerSetAuthority — no resolver can alias another's key material.
Offline signers (pulsard, coronad, P3Q prover) are interfaces only, never imported
by the verify path.

Wire unchanged: D, the Message c14n, messageDST, the ZAP magic and kind bytes are
byte-identical (forge Go<->Solidity equality holds; golden envelope SHA unchanged).
The corona lane DST is renamed pulseDST->coronaDST (no deployed corona sigs;
envelope KAT regenerated).

22 tests green: non-confusable dispatch, suite/kind fail-closed, Pulsar import
hygiene, subject-identity across lanes, policyID downgrade, key-era distinctness,
P3Q-classical-root strict gate.
2026-06-27 13:47:13 -07:00

155 lines
4.1 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// signature_scheme_fuzz_test.go fuzzes the ZAP envelope decoder against
// the PQ-native posture. Properties:
//
// A. ParseEnvelope MUST NOT panic on any byte string.
// B. HasPQEvidence ⇔ len(MLDSACertSet) > 0.
// C. HasPQEvidence == false ⇒ strict-PQ gate returns ErrClassicalAuthForbidden.
// D. HasPQEvidence == true ⇒ strict-PQ gate accepts (presence dispatch).
package warp
import (
"bytes"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/pq"
)
func makeSchemeFuzzSeed(t testing.TB, hasPulse, hasCert bool) []byte {
t.Helper()
message := &Message{
NetworkID: 1,
SourceChainID: ids.ID{0xA1, 0xA2, 0xA3, 0xA4},
SourceNebulaRoot: [32]byte{0xDE, 0xAD, 0xBE, 0xEF},
SourceKeyEraID: 7,
SourceGeneration: 11,
HashSuiteID: MessageHashProfileTag,
Payload: []byte("scheme-fuzz"),
}
signers := NewBitSet()
signers.Add(0)
signers.Add(2)
var sigBytes [bls.SignatureLen]byte
copy(sigBytes[:], bytes.Repeat([]byte{0xAB}, bls.SignatureLen))
var pulse []byte
if hasPulse {
pulse = bytes.Repeat([]byte{0x42}, 64)
}
var cert []byte
if hasCert {
cert = bytes.Repeat([]byte{0xC3}, 192)
}
env, err := NewEnvelope(message, NewBitSetSignature(signers, sigBytes), pulse, cert)
if err != nil {
t.Fatalf("new envelope: %v", err)
}
wire, err := env.Bytes()
if err != nil {
t.Fatalf("env bytes: %v", err)
}
return wire
}
func FuzzSignatureSchemeLegParser(f *testing.F) {
f.Add(makeSchemeFuzzSeed(f, false, false))
f.Add(makeSchemeFuzzSeed(f, true, false))
f.Add(makeSchemeFuzzSeed(f, false, true))
f.Add(makeSchemeFuzzSeed(f, true, true))
f.Add([]byte{})
f.Add(append(wireMagic[:], kindEnvelope))
f.Add([]byte{0x05})
f.Fuzz(func(t *testing.T, raw []byte) {
// Property A: never panics.
env, err := ParseEnvelope(raw)
if err != nil {
return
}
// Property B: predicate matches the PQ lanes. PQ evidence = the Pulse
// (Corona threshold, primary) OR the ML-DSA cert-set (fallback).
hasField := len(env.CoronaSig) > 0 || len(env.MLDSACertSet) > 0
hasPredicate := env.HasPQEvidence()
if hasField != hasPredicate {
t.Fatalf("HasPQEvidence drift: field=%t predicate=%t", hasField, hasPredicate)
}
gateErr := pq.ValidateMode(pq.ModeStrictPQ, env, nil)
if !hasPredicate {
// Property C.
if gateErr == nil {
t.Fatalf("strict-PQ accepted classical-only envelope (raw=%x)", raw)
}
if !errorMatches(gateErr, pq.ErrClassicalAuthForbidden) {
t.Fatalf("classical-only refused with wrong error: %v", gateErr)
}
} else {
// Property D.
if gateErr != nil {
t.Fatalf("strict-PQ refused PQ-evidenced envelope: %v", gateErr)
}
}
// Canonical round-trip stability.
wire, err := env.Bytes()
if err != nil {
t.Fatalf("re-encode failed: %v", err)
}
if !bytes.Equal(raw, wire) {
t.Fatalf("wire not stable across round-trip:\n in =%x\n out=%x", raw, wire)
}
})
}
// FuzzCorruptedMLDSACertSet flips a byte of an otherwise-valid envelope.
// The decoder MUST refuse or produce a self-consistent envelope, never
// read into adjacent memory.
func FuzzCorruptedMLDSACertSet(f *testing.F) {
f.Add(makeSchemeFuzzSeed(f, false, true), uint16(0))
f.Add(makeSchemeFuzzSeed(f, true, true), uint16(64))
f.Add(makeSchemeFuzzSeed(f, false, true), uint16(0xFFFF))
f.Fuzz(func(t *testing.T, raw []byte, mutateAt uint16) {
if len(raw) < 10 {
return
}
corrupted := make([]byte, len(raw))
copy(corrupted, raw)
pos := int(mutateAt) % len(corrupted)
corrupted[pos] ^= 0xFF
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("ParseEnvelope panicked: %v", r)
}
}()
env, err := ParseEnvelope(corrupted)
if err == nil {
_ = env.Verify()
}
}()
})
}
// errorMatches walks err's unwrap chain looking for target.
func errorMatches(err error, target error) bool {
for err != nil {
if err == target {
return true
}
u, ok := err.(interface{ Unwrap() error })
if !ok {
return false
}
err = u.Unwrap()
}
return false
}