Add MEK rotation, per-engine DEKs, and v2 ciphertext format (audit #6, #22)

Implement a two-level key hierarchy: the MEK now wraps per-engine DEKs
stored in a new barrier_keys table, rather than encrypting all barrier
entries directly. A v2 ciphertext format (0x02) embeds the key ID so the
barrier can resolve which DEK to use on decryption. v1 ciphertext remains
supported for backward compatibility.

Key changes:
- crypto: EncryptV2/DecryptV2/ExtractKeyID for v2 ciphertext with key IDs
- barrier: key registry (CreateKey, RotateKey, ListKeys, MigrateToV2, ReWrapKeys)
- seal: RotateMEK re-wraps DEKs without re-encrypting data
- engine: Mount auto-creates per-engine DEK
- REST + gRPC: barrier/keys, barrier/rotate-mek, barrier/rotate-key, barrier/migrate
- proto: BarrierService (v1 + v2) with ListKeys, RotateMEK, RotateKey, Migrate
- db: migration v2 adds barrier_keys table

Also includes: security audit report, CSRF protection, engine design specs
(sshca, transit, user), path-bound AAD migration tool, policy engine
enhancements, and ARCHITECTURE.md updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 18:27:44 -07:00
parent ac4577f778
commit 64d921827e
44 changed files with 5184 additions and 90 deletions

View File

@@ -241,18 +241,84 @@ func TestEngineRequestPolicyAllowsWithRule(t *testing.T) {
}
}
// TestOperationAction verifies the read/write classification of operations.
func TestOperationAction(t *testing.T) {
readOps := []string{"list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer"}
for _, op := range readOps {
if got := operationAction(op); got != "read" {
t.Errorf("operationAction(%q) = %q, want %q", op, got, "read")
// TestEngineRequestAdminOnlyBlocksNonAdmin verifies that admin-only operations
// via the generic endpoint are rejected for non-admin users.
func TestEngineRequestAdminOnlyBlocksNonAdmin(t *testing.T) {
srv, sealMgr, _ := setupTestServer(t)
unsealServer(t, sealMgr, nil)
for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} {
body := makeEngineRequest("test-mount", op)
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
w := httptest.NewRecorder()
srv.handleEngineRequest(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("operation %q: expected 403 for non-admin, got %d", op, w.Code)
}
}
writeOps := []string{"issue", "renew", "create-issuer", "delete-issuer", "sign-csr", "revoke"}
for _, op := range writeOps {
if got := operationAction(op); got != "write" {
t.Errorf("operationAction(%q) = %q, want %q", op, got, "write")
}
// TestEngineRequestAdminOnlyAllowsAdmin verifies that admin-only operations
// via the generic endpoint are allowed for admin users.
func TestEngineRequestAdminOnlyAllowsAdmin(t *testing.T) {
srv, sealMgr, _ := setupTestServer(t)
unsealServer(t, sealMgr, nil)
for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} {
body := makeEngineRequest("test-mount", op)
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
req = withTokenInfo(req, &auth.TokenInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true})
w := httptest.NewRecorder()
srv.handleEngineRequest(w, req)
// Admin passes the admin check; will get 404 (mount not found) not 403.
if w.Code == http.StatusForbidden {
t.Errorf("operation %q: admin should not be forbidden, got 403", op)
}
}
}
// TestOperationAction verifies the action classification of operations.
func TestOperationAction(t *testing.T) {
tests := map[string]string{
// Read operations.
"list-issuers": policy.ActionRead,
"list-certs": policy.ActionRead,
"get-cert": policy.ActionRead,
"get-root": policy.ActionRead,
"get-chain": policy.ActionRead,
"get-issuer": policy.ActionRead,
"list-keys": policy.ActionRead,
"get-key": policy.ActionRead,
"get-public-key": policy.ActionRead,
"list-users": policy.ActionRead,
"get-profile": policy.ActionRead,
"list-profiles": policy.ActionRead,
// Granular crypto operations (including batch variants).
"encrypt": policy.ActionEncrypt,
"batch-encrypt": policy.ActionEncrypt,
"decrypt": policy.ActionDecrypt,
"batch-decrypt": policy.ActionDecrypt,
"sign": policy.ActionSign,
"sign-host": policy.ActionSign,
"sign-user": policy.ActionSign,
"verify": policy.ActionVerify,
"hmac": policy.ActionHMAC,
// Write operations.
"issue": policy.ActionWrite,
"renew": policy.ActionWrite,
"create-issuer": policy.ActionWrite,
"delete-issuer": policy.ActionWrite,
"sign-csr": policy.ActionWrite,
"revoke": policy.ActionWrite,
}
for op, want := range tests {
if got := operationAction(op); got != want {
t.Errorf("operationAction(%q) = %q, want %q", op, got, want)
}
}
}