Files
kms/sdk/go/compliance_test.go
zeekay c0cd9dd532 infra: standardize ZAP duplex port to 9999
ZAP binary protocol duplex RPC standardizes on :9999 across every
service. Lux validator staking/RPC ports (9630/9631 mainnet, 9650/9651
devnet) are unchanged — those are luxd network protocol ports, not ZAP.

One ZAP port everywhere (was a mix of 9651/9652/9653/9633/9900).
2026-04-25 15:35:01 -07:00

801 lines
21 KiB
Go

// Copyright (C) 2020-2026, Hanzo AI Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package kms
import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
// mockComplianceServer creates a test HTTP server that simulates compliance MPC node endpoints.
func mockComplianceServer(t *testing.T) (*httptest.Server, *mockComplianceState) {
t.Helper()
state := &mockComplianceState{
retained: make(map[string]RetainedRecord),
breakGlass: make(map[string]*BreakGlassToken),
auditLog: make([]AuditEntry, 0),
secrets: make(map[string][]byte),
auditValid: true,
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state.mu.Lock()
defer state.mu.Unlock()
p := r.URL.Path
m := r.Method
// Route most-specific paths first to avoid prefix collisions.
switch {
// --- Audit: verify, export (before generic /audit) ---
case m == http.MethodGet && pathEndsWith(p, "/zk/compliance/audit/verify"):
resp := struct {
Valid bool `json:"valid"`
}{
Valid: state.auditValid,
}
json.NewEncoder(w).Encode(resp)
case m == http.MethodGet && pathContains(p, "/zk/compliance/audit/export"):
format := r.URL.Query().Get("format")
switch format {
case "json":
json.NewEncoder(w).Encode(state.auditLog)
case "csv":
w.Write([]byte("ts,actor,action,key,reason,ip\n"))
for _, e := range state.auditLog {
w.Write([]byte(e.Timestamp.Format(time.RFC3339) + "," + e.ActorID + "," + e.Action + "," + e.SecretKey + "," + e.Reason + "," + e.SourceIP + "\n"))
}
default:
http.Error(w, "unsupported format", http.StatusBadRequest)
}
case m == http.MethodGet && pathContains(p, "/zk/compliance/audit") && r.URL.Query().Get("since") != "":
resp := struct {
Entries []AuditEntry `json:"entries"`
}{
Entries: state.auditLog,
}
json.NewEncoder(w).Encode(resp)
// --- Break-glass: GET/DELETE with token (has extra path segments) before bare break-glass ---
case m == http.MethodGet && pathContains(p, "/zk/compliance/break-glass/") && !pathEndsWith(p, "/zk/compliance/break-glass/"):
// GET /v1/orgs/{org}/zk/compliance/break-glass/{token}/{key}
found := false
for _, tok := range state.breakGlass {
if time.Now().After(tok.ExpiresAt) {
http.Error(w, "break-glass token expired", http.StatusForbidden)
return
}
found = true
resp := struct {
Value []byte `json:"value"`
}{
Value: []byte("emergency-decrypted-value"),
}
json.NewEncoder(w).Encode(resp)
return
}
if !found {
http.Error(w, "invalid break-glass token", http.StatusForbidden)
return
}
case m == http.MethodDelete && pathContains(p, "/zk/compliance/break-glass/"):
// DELETE /v1/orgs/{org}/zk/compliance/break-glass/{token}
state.breakGlass = make(map[string]*BreakGlassToken)
state.auditLog = append(state.auditLog, AuditEntry{
Timestamp: time.Now(),
ActorID: "test-actor",
Action: "break-glass-revoke",
})
w.WriteHeader(http.StatusOK)
// --- Break-glass: bare endpoint (no token in path) ---
case m == http.MethodPost && pathEndsWith(p, "/zk/compliance/break-glass"):
var req breakGlassRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
token := &BreakGlassToken{
Token: "bg-token-" + time.Now().Format("150405.000"),
ExpiresAt: time.Now().Add(time.Duration(req.DurationMs) * time.Millisecond),
SecretKeys: req.SecretKeys,
}
state.breakGlass[token.Token] = token
state.auditLog = append(state.auditLog, AuditEntry{
Timestamp: time.Now(),
ActorID: "test-actor",
Action: "break-glass-request",
Reason: req.Reason,
})
w.WriteHeader(http.StatusOK)
case m == http.MethodGet && pathEndsWith(p, "/zk/compliance/break-glass"):
var latest *BreakGlassToken
for _, tok := range state.breakGlass {
if latest == nil || tok.ExpiresAt.After(latest.ExpiresAt) {
latest = tok
}
}
if latest == nil {
http.Error(w, "no break-glass tokens", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(latest)
// --- Retained ---
case m == http.MethodPost && pathEndsWith(p, "/zk/compliance/retained"):
var req retainRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
now := time.Now()
for _, k := range req.SecretKeys {
state.retained[k] = RetainedRecord{
Key: k,
RetainedAt: now,
ExpiresAt: now.Add(7 * 365 * 24 * time.Hour),
}
}
w.WriteHeader(http.StatusOK)
case m == http.MethodGet && pathEndsWith(p, "/zk/compliance/retained"):
records := make([]RetainedRecord, 0, len(state.retained))
for _, rec := range state.retained {
records = append(records, rec)
}
resp := struct {
Records []RetainedRecord `json:"records"`
}{
Records: records,
}
json.NewEncoder(w).Encode(resp)
// --- Regulator export ---
case m == http.MethodPost && pathEndsWith(p, "/zk/compliance/export"):
var req regulatorExportRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
state.lastExportKeys = req.SecretKeys
w.WriteHeader(http.StatusOK)
case m == http.MethodGet && pathEndsWith(p, "/zk/compliance/export"):
pkg := RegulatorPackage{
EncryptedSecrets: []byte("encrypted-blob"),
EscrowMaterial: []byte("escrow-wrapped-material"),
AuditTrail: []byte("chain-hashed-audit"),
ExportTimestamp: time.Now(),
}
json.NewEncoder(w).Encode(pkg)
// --- Enable/Disable compliance (most general /zk/compliance — must be LAST) ---
case m == http.MethodPost && pathEndsWith(p, "/zk/compliance"):
var req enableComplianceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
state.complianceEnabled = true
state.complianceMode = req.Mode
state.escrowPubKey = req.EscrowPubKey
w.WriteHeader(http.StatusOK)
case m == http.MethodDelete && pathEndsWith(p, "/zk/compliance"):
if state.hasActiveRetention() {
http.Error(w, "active retention prevents disabling compliance", http.StatusConflict)
return
}
state.complianceEnabled = false
w.WriteHeader(http.StatusOK)
// --- Delete secret (check retention) ---
case m == http.MethodDelete && pathContains(p, "/zk/secrets/"):
for _, rec := range state.retained {
if time.Now().Before(rec.ExpiresAt) {
http.Error(w, "secret is under retention", http.StatusForbidden)
return
}
}
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "not found: "+p, http.StatusNotFound)
}
}))
return srv, state
}
type mockComplianceState struct {
mu sync.Mutex
complianceEnabled bool
complianceMode ComplianceMode
escrowPubKey []byte
retained map[string]RetainedRecord
breakGlass map[string]*BreakGlassToken
auditLog []AuditEntry
secrets map[string][]byte
auditValid bool
lastExportKeys []string
}
func (s *mockComplianceState) hasActiveRetention() bool {
for _, rec := range s.retained {
if time.Now().Before(rec.ExpiresAt) {
return true
}
}
return false
}
// pathEndsWith checks if the URL path ends with the given suffix.
func pathEndsWith(path, suffix string) bool {
return len(path) >= len(suffix) && path[len(path)-len(suffix):] == suffix
}
// pathContains checks if the path contains the substring.
func pathContains(path, sub string) bool {
for i := 0; i <= len(path)-len(sub); i++ {
if path[i:i+len(sub)] == sub {
return true
}
}
return false
}
func newTestComplianceClient(t *testing.T, srvURL string) *Client {
t.Helper()
c, err := NewClient(Config{
Nodes: []string{srvURL},
OrgSlug: "compliance-org",
Threshold: 1,
})
if err != nil {
t.Fatal(err)
}
return c
}
func TestEnableCompliance_CreatesEscrowShard(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
escrowKey := []byte("regulator-hpke-public-key-32bytes")
err := c.EnableCompliance(ComplianceConfig{
Mode: ComplianceHIPAA,
EscrowPubKey: escrowKey,
RetentionYears: 7,
WORMAuditLog: true,
BreakGlass: true,
RegulatorAccess: RegulatorWithOrgCooperation,
})
if err != nil {
t.Fatalf("EnableCompliance: %v", err)
}
state.mu.Lock()
defer state.mu.Unlock()
if !state.complianceEnabled {
t.Error("expected compliance to be enabled")
}
if state.complianceMode != ComplianceHIPAA {
t.Errorf("expected HIPAA mode, got %d", state.complianceMode)
}
if string(state.escrowPubKey) != string(escrowKey) {
t.Error("expected escrow public key to be stored")
}
}
func TestEnableCompliance_ValidationErrors(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
tests := []struct {
name string
cfg ComplianceConfig
}{
{
name: "mode none",
cfg: ComplianceConfig{Mode: ComplianceNone, EscrowPubKey: []byte("key"), RetentionYears: 1},
},
{
name: "no escrow key",
cfg: ComplianceConfig{Mode: ComplianceSEC, RetentionYears: 1},
},
{
name: "zero retention",
cfg: ComplianceConfig{Mode: ComplianceSEC, EscrowPubKey: []byte("key"), RetentionYears: 0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := c.EnableCompliance(tt.cfg); err == nil {
t.Fatal("expected error, got nil")
}
})
}
}
func TestBreakGlass_CreatesTimeLimitedToken(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
token, err := c.RequestBreakGlass("patient emergency", []string{"db-password", "api-key"}, 15*time.Minute)
if err != nil {
t.Fatalf("RequestBreakGlass: %v", err)
}
if token.Token == "" {
t.Error("expected non-empty token")
}
if len(token.SecretKeys) != 2 {
t.Errorf("expected 2 secret keys, got %d", len(token.SecretKeys))
}
if token.ExpiresAt.Before(time.Now()) {
t.Error("expected token to expire in the future")
}
}
func TestBreakGlass_ExpiredTokenRejected(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Insert an already-expired token into mock state.
state.mu.Lock()
state.breakGlass["expired-token"] = &BreakGlassToken{
Token: "expired-token",
ExpiresAt: time.Now().Add(-1 * time.Hour),
SecretKeys: []string{"secret-1"},
}
state.mu.Unlock()
_, err := c.GetWithBreakGlass("expired-token", "secret-1")
if err == nil {
t.Fatal("expected error for expired break-glass token, got nil")
}
}
func TestBreakGlass_GetWithValidToken(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Insert a valid token into mock state.
state.mu.Lock()
state.breakGlass["valid-token"] = &BreakGlassToken{
Token: "valid-token",
ExpiresAt: time.Now().Add(1 * time.Hour),
SecretKeys: []string{"secret-1"},
}
state.mu.Unlock()
value, err := c.GetWithBreakGlass("valid-token", "secret-1")
if err != nil {
t.Fatalf("GetWithBreakGlass: %v", err)
}
if string(value) != "emergency-decrypted-value" {
t.Errorf("unexpected value: %q", value)
}
}
func TestBreakGlass_RevokeToken(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Insert a valid token.
state.mu.Lock()
state.breakGlass["revoke-me"] = &BreakGlassToken{
Token: "revoke-me",
ExpiresAt: time.Now().Add(1 * time.Hour),
SecretKeys: []string{"secret-1"},
}
state.mu.Unlock()
if err := c.RevokeBreakGlass("revoke-me"); err != nil {
t.Fatalf("RevokeBreakGlass: %v", err)
}
state.mu.Lock()
defer state.mu.Unlock()
if len(state.breakGlass) != 0 {
t.Error("expected break-glass tokens to be cleared after revoke")
}
}
func TestBreakGlass_ValidationErrors(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
if _, err := c.RequestBreakGlass("", []string{"key"}, time.Minute); err == nil {
t.Error("expected error for empty reason")
}
if _, err := c.RequestBreakGlass("reason", nil, time.Minute); err == nil {
t.Error("expected error for nil secret keys")
}
if _, err := c.RequestBreakGlass("reason", []string{"key"}, -time.Minute); err == nil {
t.Error("expected error for negative duration")
}
}
func TestAuditLog_ReturnsEntriesInOrder(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Pre-populate audit entries with ordered timestamps.
now := time.Now()
state.mu.Lock()
state.auditLog = []AuditEntry{
{Timestamp: now.Add(-2 * time.Hour), ActorID: "alice", Action: "set", SecretKey: "key-1"},
{Timestamp: now.Add(-1 * time.Hour), ActorID: "bob", Action: "get", SecretKey: "key-2"},
{Timestamp: now, ActorID: "carol", Action: "delete", SecretKey: "key-3"},
}
state.mu.Unlock()
entries, err := c.AuditLog(now.Add(-3*time.Hour), now.Add(time.Hour))
if err != nil {
t.Fatalf("AuditLog: %v", err)
}
if len(entries) != 3 {
t.Fatalf("expected 3 entries, got %d", len(entries))
}
// Verify order is preserved.
if entries[0].ActorID != "alice" || entries[1].ActorID != "bob" || entries[2].ActorID != "carol" {
t.Error("audit entries not in expected order")
}
// Verify timestamps are ordered.
for i := 1; i < len(entries); i++ {
if entries[i].Timestamp.Before(entries[i-1].Timestamp) {
t.Errorf("entry %d timestamp before entry %d", i, i-1)
}
}
}
func TestVerifyAuditLog_DetectsTampering(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Verify valid log.
valid, err := c.VerifyAuditLog()
if err != nil {
t.Fatalf("VerifyAuditLog: %v", err)
}
if !valid {
t.Error("expected audit log to be valid")
}
// Simulate tampering.
state.mu.Lock()
state.auditValid = false
state.mu.Unlock()
valid, err = c.VerifyAuditLog()
if err != nil {
t.Fatalf("VerifyAuditLog after tamper: %v", err)
}
if valid {
t.Error("expected audit log to be invalid after tampering")
}
}
func TestExportAuditLog_Formats(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
state.mu.Lock()
state.auditLog = []AuditEntry{
{Timestamp: time.Now(), ActorID: "alice", Action: "set", SecretKey: "key-1"},
}
state.mu.Unlock()
// JSON format.
data, err := c.ExportAuditLog("json")
if err != nil {
t.Fatalf("ExportAuditLog json: %v", err)
}
if len(data) == 0 {
t.Error("expected non-empty JSON export")
}
// CSV format.
data, err = c.ExportAuditLog("csv")
if err != nil {
t.Fatalf("ExportAuditLog csv: %v", err)
}
if len(data) == 0 {
t.Error("expected non-empty CSV export")
}
// Invalid format.
_, err = c.ExportAuditLog("xml")
if err == nil {
t.Error("expected error for unsupported format")
}
}
func TestMarkRetained_PreventsDeletetion(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Mark secrets as retained.
err := c.MarkRetained([]string{"db-password", "tls-cert"})
if err != nil {
t.Fatalf("MarkRetained: %v", err)
}
state.mu.Lock()
if len(state.retained) != 2 {
t.Errorf("expected 2 retained records, got %d", len(state.retained))
}
state.mu.Unlock()
// Verify ListRetained returns them.
records, err := c.ListRetained()
if err != nil {
t.Fatalf("ListRetained: %v", err)
}
if len(records) != 2 {
t.Fatalf("expected 2 retained records, got %d", len(records))
}
// Verify retention timestamps are set.
for _, rec := range records {
if rec.RetainedAt.IsZero() {
t.Error("expected non-zero RetainedAt")
}
if rec.ExpiresAt.IsZero() {
t.Error("expected non-zero ExpiresAt")
}
if rec.ExpiresAt.Before(rec.RetainedAt) {
t.Error("ExpiresAt should be after RetainedAt")
}
}
}
func TestRetentionExpired_AllowsDeletion(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Insert an already-expired retention record.
state.mu.Lock()
state.retained["old-secret"] = RetainedRecord{
Key: "old-secret",
RetainedAt: time.Now().Add(-10 * 365 * 24 * time.Hour),
ExpiresAt: time.Now().Add(-1 * time.Hour), // expired
}
state.mu.Unlock()
// Attempt to disable compliance — should succeed because retention is expired.
err := c.DisableCompliance()
if err != nil {
t.Fatalf("DisableCompliance should succeed when retention is expired: %v", err)
}
}
func TestRetentionActive_BlocksDisableCompliance(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
// Insert an active retention record.
state.mu.Lock()
state.retained["active-secret"] = RetainedRecord{
Key: "active-secret",
RetainedAt: time.Now(),
ExpiresAt: time.Now().Add(7 * 365 * 24 * time.Hour),
}
state.mu.Unlock()
// Attempt to disable compliance — should fail because retention is active.
err := c.DisableCompliance()
if err == nil {
t.Fatal("DisableCompliance should fail when retention is active")
}
}
func TestRegulatorExport_ProducesValidPackage(t *testing.T) {
srv, state := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
pkg, err := c.RegulatorExport([]string{"secret-1", "secret-2"})
if err != nil {
t.Fatalf("RegulatorExport: %v", err)
}
if len(pkg.EncryptedSecrets) == 0 {
t.Error("expected non-empty EncryptedSecrets")
}
if len(pkg.EscrowMaterial) == 0 {
t.Error("expected non-empty EscrowMaterial")
}
if len(pkg.AuditTrail) == 0 {
t.Error("expected non-empty AuditTrail")
}
if pkg.ExportTimestamp.IsZero() {
t.Error("expected non-zero ExportTimestamp")
}
// Verify the server received the right keys.
state.mu.Lock()
defer state.mu.Unlock()
if len(state.lastExportKeys) != 2 {
t.Errorf("expected 2 export keys, got %d", len(state.lastExportKeys))
}
}
func TestRegulatorExport_ValidationErrors(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
_, err := c.RegulatorExport(nil)
if err == nil {
t.Error("expected error for nil secret keys")
}
_, err = c.RegulatorExport([]string{})
if err == nil {
t.Error("expected error for empty secret keys")
}
}
func TestComplianceMode_AllModes(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
escrowKey := []byte("regulator-key")
modes := []struct {
name string
mode ComplianceMode
}{
{"HIPAA", ComplianceHIPAA},
{"SEC", ComplianceSEC},
{"FINRA", ComplianceFINRA},
{"SOX", ComplianceSOX},
{"GDPR", ComplianceGDPR},
}
for _, tt := range modes {
t.Run(tt.name, func(t *testing.T) {
err := c.EnableCompliance(ComplianceConfig{
Mode: tt.mode,
EscrowPubKey: escrowKey,
RetentionYears: 5,
WORMAuditLog: true,
})
if err != nil {
t.Fatalf("EnableCompliance(%s): %v", tt.name, err)
}
})
}
}
func TestMarkRetained_ValidationErrors(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
if err := c.MarkRetained(nil); err == nil {
t.Error("expected error for nil secret keys")
}
if err := c.MarkRetained([]string{}); err == nil {
t.Error("expected error for empty secret keys")
}
}
func TestGetWithBreakGlass_ValidationErrors(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
if _, err := c.GetWithBreakGlass("", "key"); err == nil {
t.Error("expected error for empty token")
}
if _, err := c.GetWithBreakGlass("tok", ""); err == nil {
t.Error("expected error for empty key")
}
}
func TestRevokeBreakGlass_ValidationErrors(t *testing.T) {
srv, _ := mockComplianceServer(t)
defer srv.Close()
c := newTestComplianceClient(t, srv.URL)
if err := c.RevokeBreakGlass(""); err == nil {
t.Error("expected error for empty token")
}
}
func TestConfigWithCompliance(t *testing.T) {
cfg := Config{
Nodes: []string{"https://node1:9999"},
OrgSlug: "test-org",
Threshold: 1,
Compliance: &ComplianceConfig{
Mode: ComplianceHIPAA,
EscrowPubKey: []byte("key"),
RetentionYears: 7,
WORMAuditLog: true,
BreakGlass: true,
},
}
c, err := NewClient(cfg)
if err != nil {
t.Fatalf("NewClient with compliance config: %v", err)
}
if c == nil {
t.Fatal("expected non-nil client")
}
}
func TestConfigWithoutCompliance(t *testing.T) {
cfg := Config{
Nodes: []string{"https://node1:9999"},
OrgSlug: "test-org",
Threshold: 1,
}
c, err := NewClient(cfg)
if err != nil {
t.Fatalf("NewClient without compliance config: %v", err)
}
if c == nil {
t.Fatal("expected non-nil client")
}
}