Fix ECDH zeroization, add audit logging, and remediate high findings

- 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>
This commit is contained in:
2026-03-17 14:04:39 -07:00
parent b33d1f99a0
commit 5c5d7e184e
24 changed files with 1699 additions and 72 deletions

View File

@@ -10,6 +10,7 @@ import (
"sync"
"time"
"git.wntrmute.dev/kyle/metacrypt/internal/audit"
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
)
@@ -54,6 +55,7 @@ type Manager struct {
lockoutUntil time.Time
db *sql.DB
barrier *barrier.AESGCMBarrier
audit *audit.Logger
logger *slog.Logger
mek []byte
state ServiceState
@@ -62,10 +64,11 @@ type Manager struct {
}
// NewManager creates a new seal manager.
func NewManager(db *sql.DB, b *barrier.AESGCMBarrier, logger *slog.Logger) *Manager {
func NewManager(db *sql.DB, b *barrier.AESGCMBarrier, auditLog *audit.Logger, logger *slog.Logger) *Manager {
return &Manager{
db: db,
barrier: b,
audit: auditLog,
logger: logger,
state: StateUninitialized,
}
@@ -223,6 +226,10 @@ func (m *Manager) Unseal(password []byte) error {
mek, err := crypto.Decrypt(kwk, encryptedMEK, nil)
if err != nil {
m.logger.Debug("unseal failed: invalid password")
m.audit.Log(context.Background(), audit.Event{
Caller: "operator", Operation: "unseal", Outcome: "denied",
Error: "invalid password",
})
return ErrInvalidPassword
}
@@ -235,6 +242,9 @@ func (m *Manager) Unseal(password []byte) error {
m.mek = mek
m.state = StateUnsealed
m.unsealAttempts = 0
m.audit.Log(context.Background(), audit.Event{
Caller: "operator", Operation: "unseal", Outcome: "success",
})
m.logger.Debug("unseal succeeded, barrier unsealed")
return nil
}
@@ -340,6 +350,9 @@ func (m *Manager) Seal() error {
}
_ = m.barrier.Seal()
m.state = StateSealed
m.audit.Log(context.Background(), audit.Event{
Caller: "operator", Operation: "seal", Outcome: "success",
})
m.logger.Debug("service sealed")
return nil
}

View File

@@ -23,7 +23,7 @@ func setupSeal(t *testing.T) (*Manager, func()) {
t.Fatalf("migrate: %v", err)
}
b := barrier.NewAESGCMBarrier(database)
mgr := NewManager(database, b, slog.Default())
mgr := NewManager(database, b, nil, slog.Default())
return mgr, func() { _ = database.Close() }
}
@@ -103,7 +103,7 @@ func TestSealCheckInitializedPersists(t *testing.T) {
database, _ := db.Open(dbPath)
_ = db.Migrate(database)
b := barrier.NewAESGCMBarrier(database)
mgr := NewManager(database, b, slog.Default())
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)
@@ -113,7 +113,7 @@ func TestSealCheckInitializedPersists(t *testing.T) {
database2, _ := db.Open(dbPath)
defer func() { _ = database2.Close() }()
b2 := barrier.NewAESGCMBarrier(database2)
mgr2 := NewManager(database2, b2, slog.Default())
mgr2 := NewManager(database2, b2, nil, slog.Default())
_ = mgr2.CheckInitialized()
if mgr2.State() != StateSealed {
t.Fatalf("state after reopen: got %v, want Sealed", mgr2.State())