- 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
180 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|