- Fix #61: handleRotateKey and handleDeleteUser now zeroize stored privBytes instead of calling Bytes() (which returns a copy). New state populates privBytes; old references nil'd for GC. - Add audit logging subsystem (internal/audit) with structured event recording for cryptographic operations. - Add audit log engine spec (engines/auditlog.md). - Add ValidateName checks across all engines for path traversal (#48). - Update AUDIT.md: all High findings resolved (0 open). - Add REMEDIATION.md with detailed remediation tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
6.1 KiB
Go
227 lines
6.1 KiB
Go
package seal
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
|
|
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
|
"git.wntrmute.dev/kyle/metacrypt/internal/db"
|
|
)
|
|
|
|
func setupSeal(t *testing.T) (*Manager, func()) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
database, err := db.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.Migrate(database); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
b := barrier.NewAESGCMBarrier(database)
|
|
mgr := NewManager(database, b, nil, slog.Default())
|
|
return mgr, func() { _ = database.Close() }
|
|
}
|
|
|
|
func TestSealInitializeAndUnseal(t *testing.T) {
|
|
mgr, cleanup := setupSeal(t)
|
|
defer cleanup()
|
|
|
|
if err := mgr.CheckInitialized(); err != nil {
|
|
t.Fatalf("CheckInitialized: %v", err)
|
|
}
|
|
if mgr.State() != StateUninitialized {
|
|
t.Fatalf("state: got %v, want Uninitialized", mgr.State())
|
|
}
|
|
|
|
password := []byte("test-password-123")
|
|
// Use fast params for testing.
|
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
|
|
|
if err := mgr.Initialize(context.Background(), password, params); err != nil {
|
|
t.Fatalf("Initialize: %v", err)
|
|
}
|
|
if mgr.State() != StateUnsealed {
|
|
t.Fatalf("state after init: got %v, want Unsealed", mgr.State())
|
|
}
|
|
|
|
// Seal.
|
|
if err := mgr.Seal(); err != nil {
|
|
t.Fatalf("Seal: %v", err)
|
|
}
|
|
if mgr.State() != StateSealed {
|
|
t.Fatalf("state after seal: got %v, want Sealed", mgr.State())
|
|
}
|
|
|
|
// Unseal with correct password.
|
|
if err := mgr.Unseal(password); err != nil {
|
|
t.Fatalf("Unseal: %v", err)
|
|
}
|
|
if mgr.State() != StateUnsealed {
|
|
t.Fatalf("state after unseal: got %v, want Unsealed", mgr.State())
|
|
}
|
|
}
|
|
|
|
func TestSealWrongPassword(t *testing.T) {
|
|
mgr, cleanup := setupSeal(t)
|
|
defer cleanup()
|
|
_ = mgr.CheckInitialized()
|
|
|
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
|
_ = mgr.Initialize(context.Background(), []byte("correct"), params)
|
|
_ = mgr.Seal()
|
|
|
|
err := mgr.Unseal([]byte("wrong"))
|
|
if !errors.Is(err, ErrInvalidPassword) {
|
|
t.Fatalf("expected ErrInvalidPassword, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSealDoubleInitialize(t *testing.T) {
|
|
mgr, cleanup := setupSeal(t)
|
|
defer cleanup()
|
|
_ = mgr.CheckInitialized()
|
|
|
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
|
_ = mgr.Initialize(context.Background(), []byte("password"), params)
|
|
|
|
err := mgr.Initialize(context.Background(), []byte("password"), params)
|
|
if !errors.Is(err, ErrAlreadyInitialized) {
|
|
t.Fatalf("expected ErrAlreadyInitialized, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSealCheckInitializedPersists(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "test.db")
|
|
|
|
// First: initialize.
|
|
database, _ := db.Open(dbPath)
|
|
_ = db.Migrate(database)
|
|
b := barrier.NewAESGCMBarrier(database)
|
|
mgr := NewManager(database, b, nil, slog.Default())
|
|
_ = mgr.CheckInitialized()
|
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
|
_ = mgr.Initialize(context.Background(), []byte("password"), params)
|
|
_ = database.Close()
|
|
|
|
// Second: reopen and check.
|
|
database2, _ := db.Open(dbPath)
|
|
defer func() { _ = database2.Close() }()
|
|
b2 := barrier.NewAESGCMBarrier(database2)
|
|
mgr2 := NewManager(database2, b2, nil, slog.Default())
|
|
_ = mgr2.CheckInitialized()
|
|
if mgr2.State() != StateSealed {
|
|
t.Fatalf("state after reopen: got %v, want Sealed", mgr2.State())
|
|
}
|
|
}
|
|
|
|
func TestSealRotateMEK(t *testing.T) {
|
|
mgr, cleanup := setupSeal(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
_ = mgr.CheckInitialized()
|
|
|
|
password := []byte("test-password")
|
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
|
_ = mgr.Initialize(ctx, password, params)
|
|
|
|
// Create a DEK and write data through the barrier.
|
|
b := mgr.Barrier()
|
|
_ = b.CreateKey(ctx, "system")
|
|
_ = b.CreateKey(ctx, "engine/ca/prod")
|
|
_ = b.Put(ctx, "policy/rule1", []byte("policy-data"))
|
|
_ = b.Put(ctx, "engine/ca/prod/cert", []byte("cert-data"))
|
|
|
|
// Rotate MEK.
|
|
if err := mgr.RotateMEK(ctx, password); err != nil {
|
|
t.Fatalf("RotateMEK: %v", err)
|
|
}
|
|
|
|
// Data should still be readable.
|
|
got, err := b.Get(ctx, "policy/rule1")
|
|
if err != nil {
|
|
t.Fatalf("Get after MEK rotation: %v", err)
|
|
}
|
|
if string(got) != "policy-data" {
|
|
t.Fatalf("data: got %q", got)
|
|
}
|
|
|
|
got2, err := b.Get(ctx, "engine/ca/prod/cert")
|
|
if err != nil {
|
|
t.Fatalf("Get engine data after MEK rotation: %v", err)
|
|
}
|
|
if string(got2) != "cert-data" {
|
|
t.Fatalf("data: got %q", got2)
|
|
}
|
|
|
|
// Seal and unseal with the same password should work
|
|
// (the new MEK is now encrypted with the KWK).
|
|
if err := mgr.Seal(); err != nil {
|
|
t.Fatalf("Seal: %v", err)
|
|
}
|
|
if err := mgr.Unseal(password); err != nil {
|
|
t.Fatalf("Unseal after MEK rotation: %v", err)
|
|
}
|
|
|
|
got3, err := b.Get(ctx, "engine/ca/prod/cert")
|
|
if err != nil {
|
|
t.Fatalf("Get after seal/unseal: %v", err)
|
|
}
|
|
if string(got3) != "cert-data" {
|
|
t.Fatalf("data after seal/unseal: got %q", got3)
|
|
}
|
|
}
|
|
|
|
func TestSealRotateMEKWrongPassword(t *testing.T) {
|
|
mgr, cleanup := setupSeal(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
_ = mgr.CheckInitialized()
|
|
|
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
|
_ = mgr.Initialize(ctx, []byte("correct"), params)
|
|
|
|
err := mgr.RotateMEK(ctx, []byte("wrong"))
|
|
if !errors.Is(err, ErrInvalidPassword) {
|
|
t.Fatalf("expected ErrInvalidPassword, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSealRotateMEKWhenSealed(t *testing.T) {
|
|
mgr, cleanup := setupSeal(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
_ = mgr.CheckInitialized()
|
|
|
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
|
_ = mgr.Initialize(ctx, []byte("password"), params)
|
|
_ = mgr.Seal()
|
|
|
|
err := mgr.RotateMEK(ctx, []byte("password"))
|
|
if !errors.Is(err, ErrSealed) {
|
|
t.Fatalf("expected ErrSealed, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSealStateString(t *testing.T) {
|
|
tests := []struct {
|
|
want string
|
|
state ServiceState
|
|
}{
|
|
{want: "uninitialized", state: StateUninitialized},
|
|
{want: "sealed", state: StateSealed},
|
|
{want: "initializing", state: StateInitializing},
|
|
{want: "unsealed", state: StateUnsealed},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := tt.state.String(); got != tt.want {
|
|
t.Errorf("State(%d).String() = %q, want %q", tt.state, got, tt.want)
|
|
}
|
|
}
|
|
}
|