niko_trust/pkg/protocol/accessors_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

180 lines
5.3 KiB
Go

package protocol_test
import (
"testing"
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
)
// TestAccessors covers the trivial Value/object accessors that the golden and
// rule tests do not call: Decision.String, and the Signature() copies returned
// by every decoder.
func TestAccessors(t *testing.T) {
if protocol.Allow.String() != "allow" || protocol.Deny.String() != "deny" {
t.Error("Decision.String")
}
if protocol.Decision(99).String() != "invalid" {
t.Error("invalid Decision.String")
}
if !protocol.Allow.Valid() || !protocol.Deny.Valid() || protocol.Decision(99).Valid() {
t.Error("Decision.Valid")
}
vf := loadVectors(t)
for _, v := range vf.Vectors {
b := mustHex(t, v.TCEHex)
sig := mustHex(t, v.SignatureHex)
check := func(o interface {
TCE() []byte
Signature() []byte
}) {
t.Helper()
if len(o.TCE()) == 0 {
t.Errorf("%s: TCE() empty", v.Name)
}
// Signature is only populated after VerifyX; before that it is
// nil, which is the intended behaviour. Calling it must not panic
// and, once set, must return an independent copy.
got := o.Signature()
if len(got) > 0 {
cp := append([]byte(nil), got...)
cp[0] ^= 0xff
if string(o.Signature()) == string(cp) {
t.Errorf("%s: Signature() not a stable copy", v.Name)
}
}
_ = sig
}
switch v.Name {
case "identity/nikocraft", "identity/niko":
o, err := protocol.DecodeIdentity(b)
if err != nil {
t.Fatal(err)
}
check(o)
_ = o
case "claim/boolean", "claim/all-value-types":
o, err := protocol.DecodeClaim(b)
if err != nil {
t.Fatal(err)
}
check(o)
case "revocation/boolean-claim":
o, err := protocol.DecodeRevocation(b)
if err != nil {
t.Fatal(err)
}
check(o)
case "approval_request/ban":
o, err := protocol.DecodeApprovalRequest(b)
if err != nil {
t.Fatal(err)
}
check(o)
case "approval_response/allow", "approval_response/deny":
o, err := protocol.DecodeApprovalResponse(b)
if err != nil {
t.Fatal(err)
}
check(o)
case "auth_assertion/ws":
o, err := protocol.DecodeAuthAssertion(b)
if err != nil {
t.Fatal(err)
}
check(o)
}
}
}
// TestValidateCurrentEdges exercises the clock-skew boundaries of section 13.1.
func TestValidateCurrentEdges(t *testing.T) {
const skew = protocol.MaxClockSkew
cases := []struct {
created, expires, now uint64
wantErr bool
}{
{1000, 2000, 1500, false},
{1000, 0, 1500, false}, // no expiry
{1000, 2000, 1000 - skew - 1, true}, // not yet valid (beyond skew)
{1000, 2000, 1000 - skew, false}, // exactly at skew: ok
{1000, 2000, 2000 + skew, false}, // still current within skew
{1000, 2000, 2000 + skew + 1, true}, // expired beyond skew
}
for i, c := range cases {
err := protocol.ValidateCurrent(c.created, c.expires, c.now)
if (err != nil) != c.wantErr {
t.Errorf("case %d: ValidateCurrent = %v, wantErr=%v", i, err, c.wantErr)
}
}
}
// TestClaimStatusAtEdges checks the three-way lifecycle reported by the
// protocol layer (active / expired; never "not found").
func TestClaimStatusAtEdges(t *testing.T) {
const skew = protocol.MaxClockSkew
mk := func(expires uint64) *protocol.Claim {
return &protocol.Claim{ExpiresAt: expires}
}
if protocol.ClaimStatusAt(nil, 0) != protocol.StatusActive {
t.Error("nil claim is active (caller decides absence)")
}
if protocol.ClaimStatusAt(mk(2000), 1500) != protocol.StatusActive {
t.Error("within window active")
}
if protocol.ClaimStatusAt(mk(2000), 2000+skew) != protocol.StatusActive {
t.Error("within skew active")
}
if protocol.ClaimStatusAt(mk(2000), 2000+skew+1) != protocol.StatusExpired {
t.Error("beyond skew expired")
}
if protocol.ClaimStatusAt(mk(0), 9999) != protocol.StatusActive {
t.Error("no expiry never expires")
}
}
// TestVerifyRevocationOfEdges exercises the issuer/claim binding errors.
func TestVerifyRevocationOfEdges(t *testing.T) {
// A revocation that does not bind to its claim must be rejected outright,
// even before any store lookup.
vf := loadVectors(t)
rev := byName(t, vf, "revocation/boolean-claim")
claim := byName(t, vf, "claim/boolean")
revObj, err := protocol.DecodeRevocation(mustHex(t, rev.TCEHex))
if err != nil {
t.Fatal(err)
}
claimObj, err := protocol.DecodeClaim(mustHex(t, claim.TCEHex))
if err != nil {
t.Fatal(err)
}
if err := protocol.VerifyRevocationOf(revObj, claimObj); err != nil {
t.Fatalf("valid binding rejected: %v", err)
}
if err := protocol.VerifyRevocationOf(nil, claimObj); err != protocol.ErrNil {
t.Errorf("nil rev: %v", err)
}
if err := protocol.VerifyRevocationOf(revObj, nil); err != protocol.ErrNil {
t.Errorf("nil claim: %v", err)
}
// A revocation whose embedded claim_id does not match the claim it
// targets must fail the binding, even with a valid issuer.
badRev := *revObj
var wrong tce.ID
wrong[0] = 0xff
badRev.ClaimID = wrong
if err := protocol.VerifyRevocationOf(&badRev, claimObj); err != protocol.ErrWrongClaim {
t.Errorf("wrong claim_id must fail binding, got %v", err)
}
// An issuer that is not the claim's issuer must also fail.
badIssuer := *revObj
var other [32]byte
other[0] = 0xab
badIssuer.Issuer = other[:]
if err := protocol.VerifyRevocationOf(&badIssuer, claimObj); err != protocol.ErrWrongIssuer {
t.Errorf("wrong issuer must fail binding, got %v", err)
}
}