niko_trust/pkg/protocol/mutation_test.go
Niko Marmeladkov 3bf13fa488 Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
  external Go projects can import the verified core; invariant tests
  updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
  sits behind a reverse proxy (off by default, header never trusted
  otherwise)
- examples/service + examples/approve: complete passwordless login round
  trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
2026-08-26 12:49:54 +03:00

142 lines
4.6 KiB
Go

package protocol_test
import (
"testing"
"git.n1ko.dev/Niko/niko_trust/pkg/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")
}
}