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

344 lines
10 KiB
Go

package verify_test
import (
"bytes"
"testing"
"time"
"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/transport"
"git.n1ko.dev/Niko/niko_trust/pkg/verify"
"git.n1ko.dev/Niko/niko_trust/pkg/address"
)
const base = uint64(1_700_000_000)
func env(t *testing.T, tceBytes, sig []byte) *transport.Envelope {
t.Helper()
return &transport.Envelope{TCE: tceBytes, Signature: sig}
}
func TestTrustedWithoutApproval(t *testing.T) {
issuer, _ := signer.Generate()
subject, _ := signer.Generate()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: subject.Public(),
Claims: map[string]tce.Value{"admin": tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: make([]byte, tce.NonceSize),
}
cb, _ := protocol.EncodeClaim(c)
g := verify.NewGraph()
if err := g.Add(env(t, cb, issuer.Sign(cb))); err != nil {
t.Fatal(err)
}
res := g.Evaluate(verify.Policy{
Subject: subject.Address(),
Predicate: "admin",
Now: base + 10,
})
if !res.Trusted {
t.Fatalf("expected trusted, got %q", res.Reason)
}
if res.Issuer.String() != issuer.Address().String() {
t.Fatal("issuer mismatch")
}
}
func TestRevocationWins(t *testing.T) {
issuer, _ := signer.Generate()
subject, _ := signer.Generate()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: subject.Public(),
Claims: map[string]tce.Value{"admin": tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: make([]byte, tce.NonceSize),
}
cb, _ := protocol.EncodeClaim(c)
id := tce.ComputeID(cb).String()
rv := &protocol.Revocation{
Issuer: issuer.Public(),
ClaimID: tce.ComputeID(cb),
Reason: "mistake",
CreatedAt: base,
Nonce: make([]byte, tce.NonceSize),
}
rvb, _ := protocol.EncodeRevocation(rv)
g := verify.NewGraph()
if err := g.Add(env(t, cb, issuer.Sign(cb))); err != nil {
t.Fatal(err)
}
if err := g.Add(env(t, rvb, issuer.Sign(rvb))); err != nil {
t.Fatal(err)
}
res := g.Evaluate(verify.Policy{Subject: subject.Address(), Predicate: "admin", Now: base + 10})
if res.Trusted || !res.Revoked {
t.Fatalf("expected revoked, got trusted=%v revoked=%v (%s)", res.Trusted, res.Revoked, res.Reason)
}
_ = id
}
func TestApprovalRequired(t *testing.T) {
issuer, _ := signer.Generate()
approver, _ := signer.Generate()
subject, _ := signer.Generate()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: subject.Public(),
Claims: map[string]tce.Value{"admin": tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: make([]byte, tce.NonceSize),
}
cb, _ := protocol.EncodeClaim(c)
// Request from the issuer to the approver.
req := &protocol.ApprovalRequest{
Sender: issuer.Public(),
Recipient: approver.Public(),
Action: "admin",
Message: "please approve admin",
CreatedAt: base,
ExpiresAt: base + 30,
Nonce: make([]byte, tce.NonceSize),
}
reqb, _ := protocol.EncodeApprovalRequest(req)
reqID := tce.ComputeID(reqb).String()
resp := &protocol.ApprovalResponse{
RequestHash: tce.ComputeID(reqb),
Responder: approver.Public(),
Decision: protocol.Allow,
CreatedAt: base + 5,
Nonce: make([]byte, tce.NonceSize),
}
respb, _ := protocol.EncodeApprovalResponse(resp)
approvers := []address.Address{approver.Address()}
g := verify.NewGraph()
g.Add(env(t, cb, issuer.Sign(cb)))
g.Add(env(t, reqb, issuer.Sign(reqb)))
g.Add(env(t, respb, approver.Sign(respb)))
// Without the response the claim is not trusted (approval required).
missing := verify.NewGraph()
missing.Add(env(t, cb, issuer.Sign(cb)))
missing.Add(env(t, reqb, issuer.Sign(reqb)))
r0 := missing.Evaluate(verify.Policy{Subject: subject.Address(), Predicate: "admin", Approvers: approvers, Now: base + 10})
if r0.Trusted {
t.Fatal("expected untrusted without approval response")
}
// With the response it is trusted and approved by the approver.
r1 := g.Evaluate(verify.Policy{Subject: subject.Address(), Predicate: "admin", Approvers: approvers, Now: base + 10})
if !r1.Trusted {
t.Fatalf("expected trusted with approval, got %q", r1.Reason)
}
if r1.ApprovedBy.String() != approver.Address().String() {
t.Fatalf("approved by %s, want %s", r1.ApprovedBy, approver.Address())
}
_ = reqID
_ = time.Now
}
func TestApprovalThreshold(t *testing.T) {
issuer, _ := signer.Generate()
a1, _ := signer.Generate()
a2, _ := signer.Generate()
subject, _ := signer.Generate()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: subject.Public(),
Claims: map[string]tce.Value{"admin": tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: make([]byte, tce.NonceSize),
}
cb, _ := protocol.EncodeClaim(c)
approvers := []address.Address{a1.Address(), a2.Address()}
// closure returning objects to add
pair := func(approver *signer.Signer) (*transport.Envelope, *transport.Envelope) {
req := &protocol.ApprovalRequest{
Sender: issuer.Public(),
Recipient: approver.Public(),
Action: "admin",
CreatedAt: base,
ExpiresAt: base + 30,
Nonce: make([]byte, tce.NonceSize),
}
reqb, _ := protocol.EncodeApprovalRequest(req)
resp := &protocol.ApprovalResponse{
RequestHash: tce.ComputeID(reqb),
Responder: approver.Public(),
Decision: protocol.Allow,
CreatedAt: base + 5,
Nonce: make([]byte, tce.NonceSize),
}
respb, _ := protocol.EncodeApprovalResponse(resp)
return env(t, reqb, issuer.Sign(reqb)), env(t, respb, approver.Sign(respb))
}
g := verify.NewGraph()
g.Add(env(t, cb, issuer.Sign(cb)))
r1, rp1 := pair(a1)
g.Add(r1)
g.Add(rp1)
// Only one of two approvers has answered; threshold 2 -> not trusted.
r0 := g.Evaluate(verify.Policy{Subject: subject.Address(), Predicate: "admin", Approvers: approvers, Threshold: 2, Now: base + 10})
if r0.Trusted {
t.Fatal("expected untrusted with only one approval at threshold 2")
}
// Both approve -> trusted, both listed.
r2, rp2 := pair(a2)
g.Add(r2)
g.Add(rp2)
r1b := g.Evaluate(verify.Policy{Subject: subject.Address(), Predicate: "admin", Approvers: approvers, Threshold: 2, Now: base + 10})
if !r1b.Trusted || len(r1b.ApprovedByAll) != 2 {
t.Fatalf("expected trusted with 2 approvals, got trusted=%v approvers=%d", r1b.Trusted, len(r1b.ApprovedByAll))
}
}
func TestApprovalWithdrawal(t *testing.T) {
issuer, _ := signer.Generate()
approver, _ := signer.Generate()
subject, _ := signer.Generate()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: subject.Public(),
Claims: map[string]tce.Value{"admin": tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: make([]byte, tce.NonceSize),
}
cb, _ := protocol.EncodeClaim(c)
req := &protocol.ApprovalRequest{
Sender: issuer.Public(),
Recipient: approver.Public(),
Action: "admin",
CreatedAt: base,
ExpiresAt: base + 30,
Nonce: make([]byte, tce.NonceSize),
}
reqb, _ := protocol.EncodeApprovalRequest(req)
resp := &protocol.ApprovalResponse{
RequestHash: tce.ComputeID(reqb),
Responder: approver.Public(),
Decision: protocol.Allow,
CreatedAt: base + 5,
Nonce: make([]byte, tce.NonceSize),
}
respb, _ := protocol.EncodeApprovalResponse(resp)
respID := tce.ComputeID(respb).String()
approvers := []address.Address{approver.Address()}
g := verify.NewGraph()
g.Add(env(t, cb, issuer.Sign(cb)))
g.Add(env(t, reqb, issuer.Sign(reqb)))
g.Add(env(t, respb, approver.Sign(respb)))
if r := g.Evaluate(verify.Policy{Subject: subject.Address(), Predicate: "admin", Approvers: approvers, Now: base + 10}); !r.Trusted {
t.Fatal("expected trusted before withdrawal")
}
// Approver revokes their own response object.
rv := &protocol.Revocation{
Issuer: approver.Public(),
ClaimID: tce.ComputeID(respb),
Reason: "changed my mind",
CreatedAt: base + 6,
Nonce: make([]byte, tce.NonceSize),
}
rvb, _ := protocol.EncodeRevocation(rv)
g.Add(env(t, rvb, approver.Sign(rvb)))
_ = respID
if r := g.Evaluate(verify.Policy{Subject: subject.Address(), Predicate: "admin", Approvers: approvers, Now: base + 10}); r.Trusted {
t.Fatal("expected untrusted after approval withdrawal")
}
}
func TestRejectsBadSignature(t *testing.T) {
issuer, _ := signer.Generate()
impostor, _ := signer.Generate()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: issuer.Public(),
Claims: map[string]tce.Value{"x": tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: make([]byte, tce.NonceSize),
}
cb, _ := protocol.EncodeClaim(c)
g := verify.NewGraph()
if err := g.Add(env(t, cb, impostor.Sign(cb))); err == nil {
t.Fatal("expected Add to reject bad signature")
}
}
func TestTrustedIssuersFilter(t *testing.T) {
good, _ := signer.Generate()
bad, _ := signer.Generate()
subject, _ := signer.Generate()
g := verify.NewGraph()
addClaim := func(issuer *signer.Signer, nonce byte) {
t.Helper()
c := &protocol.Claim{
Issuer: issuer.Public(),
Subject: subject.Public(),
Claims: map[string]tce.Value{"mod": tce.Bool(true)},
CreatedAt: base - 100,
Serial: 1,
Nonce: bytes.Repeat([]byte{nonce}, tce.NonceSize),
}
cb, _ := protocol.EncodeClaim(c)
if err := g.Add(env(t, cb, issuer.Sign(cb))); err != nil {
t.Fatal(err)
}
}
addClaim(bad, 0x11)
// With the filter, the untrusted claim does not satisfy the policy.
res := g.Evaluate(verify.Policy{
Subject: subject.Address(),
Predicate: "mod",
Now: base + 10,
TrustedIssuers: []address.Address{good.Address()},
})
if res.Trusted || res.Issuer.String() != "" && res.Issuer.IsZero() == false {
t.Fatalf("untrusted issuer accepted: %+v", res)
}
// The same claim satisfies once its issuer is trusted.
addClaim(good, 0x12)
res2 := g.Evaluate(verify.Policy{
Subject: subject.Address(),
Predicate: "mod",
Now: base + 10,
TrustedIssuers: []address.Address{good.Address()},
})
if !res2.Trusted || res2.Issuer.String() != good.Address().String() {
t.Fatalf("trusted issuer rejected: %q", res2.Reason)
}
}