- 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
248 lines
6.8 KiB
Go
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)
|
|
}
|