- 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.
142 lines
4.6 KiB
Go
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")
|
|
}
|
|
}
|