- 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
188 lines
5.1 KiB
Go
188 lines
5.1 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"
|
|
)
|
|
|
|
// rotationFixture stores both halves of a rotation link.
|
|
type rotationFixture struct {
|
|
oldKey *signer.Signer
|
|
newKey *signer.Signer
|
|
subj *signer.Signer
|
|
g *verify.Graph
|
|
}
|
|
|
|
func newRotationFixture(t *testing.T) *rotationFixture {
|
|
t.Helper()
|
|
return &rotationFixture{
|
|
oldKey: mustSigner(t),
|
|
newKey: mustSigner(t),
|
|
subj: mustSigner(t),
|
|
g: verify.NewGraph(),
|
|
}
|
|
}
|
|
|
|
func (f *rotationFixture) addRotation(t *testing.T, old, new *signer.Signer,
|
|
createdAt uint64, lifetime int64, confAt uint64, nonce byte) {
|
|
|
|
t.Helper()
|
|
req := &protocol.KeyRotationRequest{
|
|
Successor: new.Public(),
|
|
Predecessor: old.Public(),
|
|
CreatedAt: createdAt,
|
|
ExpiresAt: createdAt + uint64(lifetime),
|
|
}
|
|
rb, err := protocol.EncodeKeyRotationRequest(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rsig := new.Sign(rb)
|
|
if err := f.g.Add(env(t, rb, rsig)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
conf := &protocol.KeyRotationConfirm{
|
|
RotationHash: tce.ComputeID(rb),
|
|
CreatedAt: confAt,
|
|
Nonce: bytes.Repeat([]byte{nonce}, tce.NonceSize),
|
|
}
|
|
cb, err := protocol.EncodeKeyRotationConfirm(conf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.g.Add(env(t, cb, old.Sign(cb))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (f *rotationFixture) addClaimBy(t *testing.T, key *signer.Signer) {
|
|
c := &protocol.Claim{
|
|
Issuer: key.Public(),
|
|
Subject: f.subj.Public(),
|
|
Claims: map[string]tce.Value{"mod": tce.Bool(true)},
|
|
CreatedAt: base - 100,
|
|
Serial: 1,
|
|
Nonce: bytes.Repeat([]byte{0x41}, tce.NonceSize),
|
|
}
|
|
cb, _ := protocol.EncodeClaim(c)
|
|
_ = f.g.Add(env(t, cb, key.Sign(cb)))
|
|
}
|
|
|
|
func (f *rotationFixture) evaluate(now uint64, maxAge uint64) verify.Result {
|
|
rootAddr := f.oldKey.Identity().Address()
|
|
return f.g.Evaluate(verify.Policy{
|
|
Subject: f.subj.Address(),
|
|
Predicate: "mod",
|
|
TrustedIssuers: []address.Address{rootAddr},
|
|
RotationMaxAge: maxAge,
|
|
Now: now,
|
|
})
|
|
}
|
|
|
|
func TestRotationSingleHop(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
f.addRotation(t, f.oldKey, f.newKey, base-30, 60, base-20, 0x51)
|
|
f.addClaimBy(t, f.newKey)
|
|
|
|
res := f.evaluate(base+10, 0)
|
|
if !res.Trusted {
|
|
t.Fatalf("rotated issuer rejected: %q", res.Reason)
|
|
}
|
|
if len(res.RotationChain) != 2 ||
|
|
res.RotationChain[0].String() != f.newKey.Identity().Address().String() ||
|
|
res.RotationChain[1].String() != f.oldKey.Identity().Address().String() {
|
|
t.Fatalf("rotation chain = %v", res.RotationChain)
|
|
}
|
|
}
|
|
|
|
func TestRotationTwoHops(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
mid := mustSigner(t)
|
|
|
|
// root(old) → mid → new
|
|
f.addRotation(t, f.oldKey, mid, base-60, 60, base-50, 0x52)
|
|
f.addRotation(t, mid, f.newKey, base-40, 60, base-30, 0x53)
|
|
f.addClaimBy(t, f.newKey)
|
|
|
|
res := f.evaluate(base+10, 0)
|
|
if !res.Trusted || len(res.RotationChain) != 3 {
|
|
t.Fatalf("two-hop rotation failed: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestRotationWithoutConfirmFails(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
req := &protocol.KeyRotationRequest{
|
|
Successor: f.newKey.Public(),
|
|
Predecessor: f.oldKey.Public(),
|
|
CreatedAt: base - 30,
|
|
ExpiresAt: base + 30,
|
|
}
|
|
rb, _ := protocol.EncodeKeyRotationRequest(req)
|
|
_ = f.g.Add(env(t, rb, f.newKey.Sign(rb)))
|
|
f.addClaimBy(t, f.newKey)
|
|
|
|
if res := f.evaluate(base+10, 0); res.Trusted {
|
|
t.Fatal("unconfirmed succession accepted")
|
|
}
|
|
}
|
|
|
|
func TestRotationWrongConfirmerFails(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
impostor := mustSigner(t)
|
|
|
|
req := &protocol.KeyRotationRequest{
|
|
Successor: f.newKey.Public(),
|
|
Predecessor: f.oldKey.Public(),
|
|
CreatedAt: base - 30,
|
|
ExpiresAt: base + 30,
|
|
}
|
|
rb, _ := protocol.EncodeKeyRotationRequest(req)
|
|
_ = f.g.Add(env(t, rb, f.newKey.Sign(rb)))
|
|
|
|
conf := &protocol.KeyRotationConfirm{
|
|
RotationHash: tce.ComputeID(rb),
|
|
CreatedAt: base - 10,
|
|
Nonce: bytes.Repeat([]byte{0x54}, tce.NonceSize),
|
|
}
|
|
cb, _ := protocol.EncodeKeyRotationConfirm(conf)
|
|
_ = f.g.Add(env(t, cb, impostor.Sign(cb))) // not the predecessor
|
|
|
|
f.addClaimBy(t, f.newKey)
|
|
if res := f.evaluate(base+10, 0); res.Trusted {
|
|
t.Fatal("consent from a stranger accepted")
|
|
}
|
|
}
|
|
|
|
func TestRotationStaleLinkFailsWithMaxAge(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
f.addRotation(t, f.oldKey, f.newKey, base-500, 60, base-490, 0x55)
|
|
f.addClaimBy(t, f.newKey)
|
|
|
|
// Fresh enough without a limit.
|
|
if res := f.evaluate(base+10, 0); !res.Trusted {
|
|
t.Fatalf("no-limit policy rejected fresh history: %q", res.Reason)
|
|
}
|
|
// Stale once freshness is demanded: confirm is ~500s old, limit 300s.
|
|
if res := f.evaluate(base+10, 300); res.Trusted {
|
|
t.Fatal("stale rotation accepted under RotationMaxAge")
|
|
}
|
|
}
|
|
|
|
func TestRotationWindowEnforced(t *testing.T) {
|
|
f := newRotationFixture(t)
|
|
// Confirm dated after the request expired by more than the skew.
|
|
f.addRotation(t, f.oldKey, f.newKey, base-100, 10, base+200, 0x56)
|
|
f.addClaimBy(t, f.newKey)
|
|
|
|
if res := f.evaluate(base+250, 0); res.Trusted {
|
|
t.Fatal("confirm outside the request window accepted")
|
|
}
|
|
}
|