Files

317 lines
11 KiB
Go

package registry
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"strings"
"sync"
"testing"
"time"
"github.com/klarkxy/nekonest-cloud/relay/internal/authsnapshot"
"github.com/klarkxy/nekonest-cloud/relay/internal/controlplane"
)
type snapshotSource struct{}
func (snapshotSource) AuthorizationSnapshot(context.Context, string, int64) (controlplane.SnapshotResponse, error) {
return controlplane.SnapshotResponse{}, context.Canceled
}
func (snapshotSource) AuthorizationDelta(context.Context, string, int64) (controlplane.DeltaResponse, error) {
return controlplane.DeltaResponse{}, context.Canceled
}
type signer struct {
public ed25519.PublicKey
private ed25519.PrivateKey
keyring authsnapshot.Keyring
}
func newSigner(t *testing.T, now time.Time) signer {
t.Helper()
public, private, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
return signer{
public: public,
private: private,
keyring: authsnapshot.Keyring{"key": {
PublicKey: public, NotBefore: now.Add(-time.Hour), RetainUntil: now.Add(time.Hour),
}},
}
}
func (signer signer) response(t *testing.T, now time.Time, tenantID, deviceID string) controlplane.SnapshotResponse {
t.Helper()
key := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
payload := authsnapshot.Payload{
SnapshotVersion: 1,
TenantID: tenantID,
TenantStatus: "active",
HomeRegion: "test",
RelayNodeID: "node_test",
PlacementGeneration: 1,
AuthorizationRevision: 1,
Devices: []authsnapshot.Device{{
DeviceID: deviceID,
CredentialHash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
IdentityFingerprint: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
Ed25519Public: key,
X25519Public: key,
Name: "Host",
OS: "linux",
}},
IssuedAt: now.Add(-time.Second).Format(time.RFC3339Nano),
ExpiresAt: now.Add(4 * time.Minute).Format(time.RFC3339Nano),
}
bytes, err := authsnapshot.SigningBytes(payload)
if err != nil {
t.Fatal(err)
}
jwk, _ := json.Marshal(map[string]string{
"kty": "OKP", "crv": "Ed25519", "kid": "key",
"x": base64.RawURLEncoding.EncodeToString(signer.public),
})
return controlplane.SnapshotResponse{
Snapshot: authsnapshot.Signed{
Algorithm: "Ed25519", KID: "key", Payload: payload,
Signature: base64.RawURLEncoding.EncodeToString(ed25519.Sign(signer.private, bytes)),
},
PublicKeyJWK: jwk,
}
}
func (signer signer) resign(t *testing.T, response controlplane.SnapshotResponse) controlplane.SnapshotResponse {
t.Helper()
bytes, err := authsnapshot.SigningBytes(response.Snapshot.Payload)
if err != nil {
t.Fatal(err)
}
response.Snapshot.Signature = base64.RawURLEncoding.EncodeToString(ed25519.Sign(signer.private, bytes))
return response
}
func TestDefaultAuthorizationPollingFitsRevocationObjective(t *testing.T) {
now := time.Now().UTC()
signer := newSigner(t, now)
registryValue, err := New(Config{
DataRoot: t.TempDir(), NodeID: "node_test",
AllowedOrigins: []string{"https://pwa.example"},
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
Keyring: signer.keyring, ControlPlane: snapshotSource{},
})
if err != nil {
t.Fatal(err)
}
defer registryValue.Close()
if registryValue.config.DeltaInterval != 5*time.Second || registryValue.config.RequestTimeout != 3*time.Second {
t.Fatalf("delta=%s timeout=%s", registryValue.config.DeltaInterval, registryValue.config.RequestTimeout)
}
if worstHealthy := registryValue.config.DeltaInterval + 2*registryValue.config.RequestTimeout; worstHealthy >= 15*time.Second {
t.Fatalf("healthy revocation path exceeds objective: %s", worstHealthy)
}
}
func TestConcurrentAcceptReturnsSingleTenantEngine(t *testing.T) {
now := time.Now().UTC()
signer := newSigner(t, now)
registryValue, err := New(Config{
DataRoot: t.TempDir(), NodeID: "node_test",
AllowedOrigins: []string{"https://pwa.example"},
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
Keyring: signer.keyring, ControlPlane: snapshotSource{},
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
})
if err != nil {
t.Fatal(err)
}
defer registryValue.Close()
response := signer.response(t, now,
"tenant_0123456789abcdef0123456789abcdef",
"host_0123456789abcdef0123456789abcdef",
)
results := make([]*Tenant, 8)
errorsFound := make([]error, 8)
var wait sync.WaitGroup
for index := range results {
wait.Add(1)
go func(index int) {
defer wait.Done()
results[index], errorsFound[index] = registryValue.Accept(context.Background(), response)
}(index)
}
wait.Wait()
for index := range results {
if errorsFound[index] != nil || results[index] != results[0] {
t.Fatalf("result[%d]=%p err=%v; first=%p", index, results[index], errorsFound[index], results[0])
}
}
}
func TestDeviceIdentityCannotBeOwnedByTwoTenants(t *testing.T) {
now := time.Now().UTC()
signer := newSigner(t, now)
registryValue, err := New(Config{
DataRoot: t.TempDir(), NodeID: "node_test",
AllowedOrigins: []string{"https://pwa.example"},
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
Keyring: signer.keyring, ControlPlane: snapshotSource{},
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
})
if err != nil {
t.Fatal(err)
}
defer registryValue.Close()
deviceID := "host_0123456789abcdef0123456789abcdef"
first := signer.response(t, now, "tenant_0123456789abcdef0123456789abcdef", deviceID)
second := signer.response(t, now, "tenant_fedcba9876543210fedcba9876543210", deviceID)
if _, err := registryValue.Accept(context.Background(), first); err != nil {
t.Fatal(err)
}
if _, err := registryValue.Accept(context.Background(), second); err == nil {
t.Fatal("cross-tenant device identity was accepted")
}
}
func TestOlderAuthorizationRevisionCannotRestoreRevokedDevice(t *testing.T) {
now := time.Now().UTC()
signer := newSigner(t, now)
registryValue, err := New(Config{
DataRoot: t.TempDir(), NodeID: "node_test",
AllowedOrigins: []string{"https://pwa.example"},
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
Keyring: signer.keyring, ControlPlane: snapshotSource{},
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
})
if err != nil {
t.Fatal(err)
}
defer registryValue.Close()
tenantID := "tenant_0123456789abcdef0123456789abcdef"
deviceID := "host_0123456789abcdef0123456789abcdef"
revisionOne := signer.response(t, now, tenantID, deviceID)
if _, err := registryValue.Accept(context.Background(), revisionOne); err != nil {
t.Fatal(err)
}
revisionTwo := revisionOne
revisionTwo.Snapshot.Payload.AuthorizationRevision = 2
revisionTwo.Snapshot.Payload.Devices = nil
revisionTwo = signer.resign(t, revisionTwo)
if _, err := registryValue.Accept(context.Background(), revisionTwo); err != nil {
t.Fatal(err)
}
if _, ok := registryValue.ByDeviceID(deviceID); ok {
t.Fatal("revoked device remained routable after newer revision")
}
if _, err := registryValue.Accept(context.Background(), revisionOne); err == nil {
t.Fatal("older signed authorization revision restored a revoked device")
}
if _, ok := registryValue.ByDeviceID(deviceID); ok {
t.Fatal("stale revision restored revoked device routing")
}
}
func TestExpiredTenantReleasesActiveCapacity(t *testing.T) {
now := time.Now().UTC()
signer := newSigner(t, now)
registryValue, err := New(Config{
DataRoot: t.TempDir(), NodeID: "node_test",
AllowedOrigins: []string{"https://pwa.example"},
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
Keyring: signer.keyring, ControlPlane: snapshotSource{},
RefreshInterval: time.Hour, DeltaInterval: time.Hour, MaxTenants: 1,
})
if err != nil {
t.Fatal(err)
}
defer registryValue.Close()
firstTenantID := "tenant_0123456789abcdef0123456789abcdef"
first, err := registryValue.Accept(context.Background(), signer.response(
t, now, firstTenantID, "host_0123456789abcdef0123456789abcdef",
))
if err != nil {
t.Fatal(err)
}
registryValue.mu.RLock()
firstSlot := registryValue.slots[firstTenantID]
registryValue.mu.RUnlock()
registryValue.expire(firstSlot, first)
if _, err := registryValue.Accept(context.Background(), signer.response(
t, now, "tenant_fedcba9876543210fedcba9876543210", "host_fedcba9876543210fedcba9876543210",
)); err != nil {
t.Fatalf("capacity was not released after expiry: %v", err)
}
}
func TestQuiesceFencesExactGeneration(t *testing.T) {
now := time.Now().UTC()
signer := newSigner(t, now)
registryValue, err := New(Config{
DataRoot: t.TempDir(), NodeID: "node_test",
AllowedOrigins: []string{"https://pwa.example"},
RouteSecret: []byte("0123456789abcdef0123456789abcdef"),
Keyring: signer.keyring, ControlPlane: snapshotSource{},
RefreshInterval: time.Hour, DeltaInterval: time.Hour,
})
if err != nil {
t.Fatal(err)
}
defer registryValue.Close()
tenantID := "tenant_0123456789abcdef0123456789abcdef"
if _, err := registryValue.Accept(context.Background(), signer.response(
t, now, tenantID, "host_0123456789abcdef0123456789abcdef",
)); err != nil {
t.Fatal(err)
}
if _, err := registryValue.Quiesce(tenantID, 2); err == nil {
t.Fatal("wrong generation was quiesced")
}
paths, err := registryValue.Quiesce(tenantID, 1)
if err != nil {
t.Fatal(err)
}
if paths.Database == "" {
t.Fatal("quiesce did not return the fenced tenant paths")
}
if _, ok := registryValue.ByDeviceID("host_0123456789abcdef0123456789abcdef"); ok {
t.Fatal("quiesced tenant remained routable")
}
}
func TestRouteHintIsOpaqueAuthenticatedAndCrossNodeDecodable(t *testing.T) {
now := time.Now().UTC()
signer := newSigner(t, now)
secret := []byte("0123456789abcdef0123456789abcdef")
registryValue, err := New(Config{
DataRoot: t.TempDir(), NodeID: "node_test",
AllowedOrigins: []string{"https://pwa.example"}, RouteSecret: secret,
Keyring: signer.keyring, ControlPlane: snapshotSource{},
})
if err != nil {
t.Fatal(err)
}
defer registryValue.Close()
tenantID := "tenant_0123456789abcdef0123456789abcdef"
hint := registryValue.routeHint(tenantID, 19)
if hint == "" || strings.Contains(hint, tenantID) {
t.Fatalf("route hint leaked tenant identity: %q", hint)
}
decodedTenant, generation, err := registryValue.DecodeRouteHint(hint)
if err != nil || decodedTenant != tenantID || generation != 19 {
t.Fatalf("decoded tenant=%q generation=%d err=%v", decodedTenant, generation, err)
}
tamperedBytes, err := base64.RawURLEncoding.DecodeString(hint)
if err != nil {
t.Fatal(err)
}
tamperedBytes[len(tamperedBytes)/2] ^= 0x01
tampered := base64.RawURLEncoding.EncodeToString(tamperedBytes)
if _, _, err := registryValue.DecodeRouteHint(tampered); err == nil {
t.Fatal("tampered route hint was accepted")
}
}