niko_trust/internal/protocol/mutation_test.go
Niko Marmeladkov 20cc52c3a5 feat: network layer — PoW, checkpoint chain, gossip, light node, WS, delegation, rotation, BFT
- BLAKE3 keyed proof-of-work on object storage and auth challenges,
  with frozen vectors cross-checked against an independent Python
  reference implementing the single-block hash it needs.
- Sparse Merkle trie over object IDs: order-independent roots,
  inclusion and absence proofs (internal/smt).
- Signed checkpoint chain per relay: transport key amendment to INV-1,
  /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe
  epoch continuity (internal/checkpoint).
- Head gossip with TOFU pinning and equivocation detection; light node
  (cmd/lightnode) that stores no history: quorum of pinned relays,
  every served object proven against the agreed root, LRU disk cache.
- WebSocket streaming on relay and light node (coder/websocket):
  scoped channels mirroring REST, raw envelopes verified client-side;
  light node marks streamed objects unproven until checkpoint coverage.
- Protocol v1 additions: DelegationClaim tag 0x07 with deterministic
  chain resolution in verify.Graph, KeyRotationRequest/Confirm tags
  0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge;
  spec sections, frozen vectors appended byte-identically, Python
  reference extended.
- Optional permissioned BFT finality over gossip (internal/bft):
  prevote/precommit with quorum certificates verifiable offline.
- Quick wins: Policy.TrustedIssuers, per-type stored metrics,
  batch fetch, lexicographic lists with stable cursor pagination.
- Security review of the network layer (docs/SECURITY-REVIEW.md) with
  findings F-01..F-09; hub send/close race and unstable pagination
  fixed under review.

12 packages green, vet/gofmt clean, protocol fuzzing stable.
2026-08-25 20:38:40 +03:00

142 lines
4.6 KiB
Go

package protocol_test
import (
"testing"
"git.n1ko.dev/Niko/niko_trust/internal/protocol"
)
// Mutation tests: any single-byte change to the canonical bytes or to the
// signature must break verification, and every frozen vector must still
// verify under its own signature. This is the strongest form of the
// "the bytes that arrived are the bytes that are verified" property
// (docs/IMPLEMENTATION_NOTES.md property 2): if a verifier re-serialised the
// decoded object, a mutation that survived round-tripping would pass.
// verifyFor returns an error when the given vector no longer verifies.
func verifyFor(t *testing.T, name string, reqTce, reqSig, rotTce, rotSig, tceB, sig []byte) error {
t.Helper()
switch name {
case "identity/nikocraft", "identity/niko":
_, err := protocol.VerifyIdentity(tceB, sig)
return err
case "claim/boolean", "claim/all-value-types":
_, err := protocol.VerifyClaim(tceB, sig)
return err
case "revocation/boolean-claim":
_, err := protocol.VerifyRevocation(tceB, sig)
return err
case "approval_request/ban":
_, err := protocol.VerifyApprovalRequest(tceB, sig)
return err
case "approval_response/allow", "approval_response/deny":
_, err := protocol.VerifyApprovalResponse(reqTce, reqSig, tceB, sig)
return err
case "auth_assertion/ws":
_, err := protocol.VerifyAuthAssertion(tceB, sig, "trust.n1ko.dev")
return err
case "delegation/minimal", "delegation/multi-predicate":
_, err := protocol.VerifyDelegationClaim(tceB, sig)
return err
case "key_rotation/request":
_, err := protocol.VerifyKeyRotationRequest(tceB, sig)
return err
case "key_rotation/confirm":
_, err := protocol.VerifyKeyRotationConfirm(rotTce, rotSig, tceB, sig)
return err
}
return nil
}
func TestMutationSweep(t *testing.T) {
vf := loadVectors(t)
byName := map[string]*vectorEntry{}
for i := range vf.Vectors {
byName[vf.Vectors[i].Name] = &vf.Vectors[i]
}
req := byName["approval_request/ban"]
reqTce := mustHex(t, req.TCEHex)
reqSig := mustHex(t, req.SignatureHex)
rot := byName["key_rotation/request"]
rotTce := mustHex(t, rot.TCEHex)
rotSig := mustHex(t, rot.SignatureHex)
for _, v := range vf.Vectors {
t.Run(v.Name, func(t *testing.T) {
b := mustHex(t, v.TCEHex)
sig := mustHex(t, v.SignatureHex)
// The unchanged vector must verify.
if err := verifyFor(t, v.Name, reqTce, reqSig, rotTce, rotSig, b, sig); err != nil {
t.Fatalf("baseline does not verify: %v", err)
}
// Flip every bit of the TCE bytes, one at a time.
for i := 0; i < len(b); i++ {
for _, mask := range []byte{0x01, 0x80} {
mut := append([]byte{}, b...)
mut[i] ^= mask
if err := verifyFor(t, v.Name, reqTce, reqSig, rotTce, rotSig, mut, sig); err == nil {
t.Fatalf("verified TCE with byte %d flipped (mask 0x%02x)", i, mask)
}
}
}
// Flip every bit of the signature, one at a time.
for i := 0; i < len(sig); i++ {
mut := append([]byte{}, sig...)
mut[i] ^= 0x01
if err := verifyFor(t, v.Name, reqTce, reqSig, rotTce, rotSig, b, mut); err == nil {
t.Fatalf("verified with signature byte %d flipped", i)
}
}
})
}
}
func TestDenyAndAllowDifferInOneByte(t *testing.T) {
vf := loadVectors(t)
byName := map[string]*vectorEntry{}
for i := range vf.Vectors {
byName[vf.Vectors[i].Name] = &vf.Vectors[i]
}
allow := byName["approval_response/allow"]
deny := byName["approval_response/deny"]
req := byName["approval_request/ban"]
a := mustHex(t, allow.TCEHex)
d := mustHex(t, deny.TCEHex)
if len(a) != len(d) {
t.Fatalf("allow/deny differ in length")
}
diffs := 0
for i := range a {
if a[i] != d[i] {
diffs++
}
}
// The reference design makes deny differ from allow in exactly the
// decision byte, so a verifier that ignores the decision is detectable.
if diffs != 1 {
t.Fatalf("expected the two responses to differ in one byte, got %d", diffs)
}
reqTce := mustHex(t, req.TCEHex)
reqSig := mustHex(t, req.SignatureHex)
allowSig := mustHex(t, allow.SignatureHex)
denySig := mustHex(t, deny.SignatureHex)
if _, err := protocol.VerifyApprovalResponse(reqTce, reqSig, d, denySig); err != nil {
t.Fatalf("deny does not verify: %v", err)
}
if _, err := protocol.VerifyApprovalResponse(reqTce, reqSig, a, allowSig); err != nil {
t.Fatalf("allow does not verify: %v", err)
}
// A decision cannot be transplanted across the two documents.
if _, err := protocol.VerifyApprovalResponse(reqTce, reqSig, a, denySig); err == nil {
t.Fatal("deny signature verified over allow bytes")
}
if _, err := protocol.VerifyApprovalResponse(reqTce, reqSig, d, allowSig); err == nil {
t.Fatal("allow signature verified over deny bytes")
}
}