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