niko_trust/pkg/verify/delegation_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

248 lines
6.8 KiB
Go

package verify_test
import (
"bytes"
"testing"
"git.n1ko.dev/Niko/niko_trust/pkg/address"
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
"git.n1ko.dev/Niko/niko_trust/pkg/verify"
)
// delegationFixture builds a graph plus signers for chain scenarios.
type delegationFixture struct {
root *signer.Signer
mid *signer.Signer
issuer *signer.Signer
subj *signer.Signer
g *verify.Graph
}
func newDelegationFixture(t *testing.T) *delegationFixture {
t.Helper()
f := &delegationFixture{
root: mustSigner(t),
mid: mustSigner(t),
issuer: mustSigner(t),
subj: mustSigner(t),
g: verify.NewGraph(),
}
return f
}
func mustSigner(t *testing.T) *signer.Signer {
t.Helper()
s, err := signer.Generate()
if err != nil {
t.Fatal(err)
}
return s
}
func (f *delegationFixture) addDelegation(t *testing.T, granter, grantee *signer.Signer,
predicate string, maxDepth uint64, createdAt uint64, expiresAt uint64, serial uint64, nonce byte) {
t.Helper()
d := &protocol.DelegationClaim{
Granter: granter.Public(),
Grantee: grantee.Public(),
Predicates: map[string]tce.Value{predicate: tce.Bool(true)},
MaxDepth: maxDepth,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
Serial: serial,
Nonce: bytes.Repeat([]byte{nonce}, tce.NonceSize),
}
b, err := protocol.EncodeDelegationClaim(d)
if err != nil {
t.Fatal(err)
}
if err := f.g.Add(env(t, b, granter.Sign(b))); err != nil {
t.Fatal(err)
}
}
func (f *delegationFixture) revokeDelegation(t *testing.T, granter *signer.Signer,
cp *protocol.DelegationClaim, nonce byte) *protocol.DelegationClaim {
t.Helper()
id := tce.ComputeID(mustTCEOf(t, cp))
rev := &protocol.Revocation{
Issuer: granter.Public(),
ClaimID: id,
CreatedAt: base + 50,
Nonce: bytes.Repeat([]byte{nonce}, tce.NonceSize),
}
b, err := protocol.EncodeRevocation(rev)
if err != nil {
t.Fatal(err)
}
if err := f.g.Add(env(t, b, granter.Sign(b))); err != nil {
t.Fatal(err)
}
return cp
}
func mustTCEOf(t *testing.T, o interface{ TCE() []byte }) []byte {
t.Helper()
b := o.TCE()
if b == nil {
t.Fatal("object has no retained canonical bytes; was it added through Add?")
}
return b
}
func (f *delegationFixture) addClaim(t *testing.T, issuer *signer.Signer, predicate string) {
t.Helper()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: f.subj.Public(),
Claims: map[string]tce.Value{predicate: tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: bytes.Repeat([]byte{0x21}, tce.NonceSize),
}
cb, err := protocol.EncodeClaim(c)
if err != nil {
t.Fatal(err)
}
if err := f.g.Add(env(t, cb, issuer.Sign(cb))); err != nil {
t.Fatal(err)
}
}
func (f *delegationFixture) evaluate(predicate string, now uint64) verify.Result {
return f.g.Evaluate(verify.Policy{
Subject: f.subj.Address(),
Predicate: predicate,
TrustedIssuers: []address.Address{f.root.Identity().Address()},
Now: now,
})
}
func TestDelegationChainDepthTwo(t *testing.T) {
f := newDelegationFixture(t)
// root grants mid (depth budget 2), mid grants issuer (budget 1).
f.addDelegation(t, f.root, f.mid, "mod", 2, base-200, 0, 1, 0x31)
f.addDelegation(t, f.mid, f.issuer, "mod", 1, base-150, 0, 1, 0x32)
f.addClaim(t, f.issuer, "mod")
res := f.evaluate("mod", base+10)
if !res.Trusted {
t.Fatalf("chain rejected: %q", res.Reason)
}
if res.Issuer.String() != f.issuer.Address().String() {
t.Fatalf("issuer %s", res.Issuer)
}
if len(res.Chain) != 3 ||
res.Chain[0].String() != f.issuer.Address().String() ||
res.Chain[1].String() != f.mid.Address().String() ||
res.Chain[2].String() != f.root.Address().String() {
t.Fatalf("chain = %v", res.Chain)
}
}
func TestDelegationPredicateMismatch(t *testing.T) {
f := newDelegationFixture(t)
f.addDelegation(t, f.root, f.issuer, "other.predicate", 0, base-200, 0, 1, 0x33)
f.addClaim(t, f.issuer, "mod")
if res := f.evaluate("mod", base+10); res.Trusted {
t.Fatal("claim accepted through a grant covering a different predicate")
}
}
func TestDelegationExpiredLinkFails(t *testing.T) {
f := newDelegationFixture(t)
// The only link expired before the evaluation instant.
f.addDelegation(t, f.root, f.issuer, "mod", 0, base-400, base-300, 1, 0x34)
f.addClaim(t, f.issuer, "mod")
if res := f.evaluate("mod", base+10); res.Trusted {
t.Fatal("expired grant accepted")
}
}
func TestDelegationRevokedLinkFails(t *testing.T) {
f := newDelegationFixture(t)
f.addDelegation(t, f.root, f.issuer, "mod", 0, base-200, 0, 1, 0x35)
// Rebuild the same grant deterministically to obtain its ID for the
// revocation, then revoke it.
d := &protocol.DelegationClaim{
Granter: f.root.Public(),
Grantee: f.issuer.Public(),
Predicates: map[string]tce.Value{"mod": tce.Bool(true)},
MaxDepth: 0,
CreatedAt: base - 200,
Serial: 1,
Nonce: bytes.Repeat([]byte{0x35}, tce.NonceSize),
}
id := computeIDOf(t, d)
rev := &protocol.Revocation{
Issuer: f.root.Public(),
ClaimID: id,
Reason: "grant withdrawn",
CreatedAt: base - 50,
Nonce: bytes.Repeat([]byte{0x36}, tce.NonceSize),
}
rb, err := protocol.EncodeRevocation(rev)
if err != nil {
t.Fatal(err)
}
if err := f.g.Add(env(t, rb, f.root.Sign(rb))); err != nil {
t.Fatal(err)
}
f.addClaim(t, f.issuer, "mod")
if res := f.evaluate("mod", base+10); res.Trusted {
t.Fatal("revoked grant accepted")
}
}
func TestDelegationMaxDepthBudgetRespected(t *testing.T) {
f := newDelegationFixture(t)
// The middle link forbids any re-delegation below it.
f.addDelegation(t, f.root, f.mid, "mod", 2, base-200, 0, 1, 0x37)
f.addDelegation(t, f.mid, f.issuer, "mod", 0, base-150, 0, 1, 0x38)
f.addClaim(t, f.issuer, "mod")
if res := f.evaluate("mod", base+10); res.Trusted {
t.Fatal("two-hop chain accepted although the last link has budget 0")
}
}
func TestDelegationCycleTerminates(t *testing.T) {
f := newDelegationFixture(t)
// a ↔ b mutual grants: bounded by MaxDepth, never loops forever.
a, _ := signer.Generate()
b, _ := signer.Generate()
f.addDelegation(t, a, b, "mod", 9, base-200, 0, 1, 0x39)
f.addDelegation(t, b, a, "mod", 9, base-200, 0, 1, 0x3A)
f.addClaim(t, a, "mod") // claim issuer is not the trusted root
if res := f.evaluate("mod", base+10); res.Trusted {
t.Fatal("cycle satisfied the policy without reaching the trusted root")
}
}
func TestDirectIssuerStillTrustedWithFilterSet(t *testing.T) {
f := newDelegationFixture(t)
f.addClaim(t, f.root, "mod")
if res := f.evaluate("mod", base+10); !res.Trusted || len(res.Chain) != 0 {
t.Fatalf("direct issuance broken: %+v", res)
}
}
func computeIDOf(t *testing.T, d *protocol.DelegationClaim) tce.ID {
t.Helper()
b, err := protocol.EncodeDelegationClaim(d)
if err != nil {
t.Fatal(err)
}
return tce.ComputeID(b)
}