Implement CA/PKI engine with two-tier X.509 certificate issuance

Add the first concrete engine implementation: a CA (PKI) engine that generates
a self-signed root CA at mount time, issues scoped intermediate CAs ("issuers"),
and signs leaf certificates using configurable profiles (server, client, peer).

Engine framework updates:
- Add CallerInfo struct for auth context in engine requests
- Add config parameter to Engine.Initialize for mount-time configuration
- Export Mount.Engine field; add GetEngine/GetMount on Registry

CA engine (internal/engine/ca/):
- Two-tier PKI: root CA → issuers → leaf certificates
- 10 operations: get-root, get-chain, get-issuer, create/delete/list issuers,
  issue, get-cert, list-certs, renew
- Certificate profiles with user-overridable TTL, key usages, and key algorithm
- Private keys never stored in barrier; zeroized from memory on seal
- Supports ECDSA, RSA, and Ed25519 key types via goutils/certlib/certgen

Server routes:
- Wire up engine mount/request handlers (replace Phase 1 stubs)
- Add public PKI routes (/v1/pki/{mount}/ca, /ca/chain, /issuer/{name})
  for unauthenticated TLS trust bootstrapping

Also includes: ARCHITECTURE.md, deploy config updates, operational tooling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 21:57:52 -07:00
parent 4ddd32b117
commit 8f77050a84
26 changed files with 2980 additions and 129 deletions

1023
internal/engine/ca/ca.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,649 @@
package ca
import (
"context"
"crypto/x509"
"encoding/pem"
"strings"
"sync"
"testing"
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
)
// memBarrier is an in-memory barrier for testing.
type memBarrier struct {
mu sync.RWMutex
data map[string][]byte
}
func newMemBarrier() *memBarrier {
return &memBarrier{data: make(map[string][]byte)}
}
func (m *memBarrier) Unseal(_ []byte) error { return nil }
func (m *memBarrier) Seal() error { return nil }
func (m *memBarrier) IsSealed() bool { return false }
func (m *memBarrier) Get(_ context.Context, path string) ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
v, ok := m.data[path]
if !ok {
return nil, barrier.ErrNotFound
}
cp := make([]byte, len(v))
copy(cp, v)
return cp, nil
}
func (m *memBarrier) Put(_ context.Context, path string, value []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
cp := make([]byte, len(value))
copy(cp, value)
m.data[path] = cp
return nil
}
func (m *memBarrier) Delete(_ context.Context, path string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data, path)
return nil
}
func (m *memBarrier) List(_ context.Context, prefix string) ([]string, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var paths []string
for k := range m.data {
if strings.HasPrefix(k, prefix) {
paths = append(paths, strings.TrimPrefix(k, prefix))
}
}
return paths, nil
}
func adminCaller() *engine.CallerInfo {
return &engine.CallerInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true}
}
func userCaller() *engine.CallerInfo {
return &engine.CallerInfo{Username: "user", Roles: []string{"user"}, IsAdmin: false}
}
func setupEngine(t *testing.T) (*CAEngine, *memBarrier) {
t.Helper()
b := newMemBarrier()
eng := NewCAEngine().(*CAEngine)
ctx := context.Background()
config := map[string]interface{}{
"organization": "TestOrg",
"key_algorithm": "ecdsa",
"key_size": float64(256),
"root_expiry": "87600h",
}
if err := eng.Initialize(ctx, b, "engine/ca/test/", config); err != nil {
t.Fatalf("Initialize: %v", err)
}
return eng, b
}
func TestInitializeGeneratesRootCA(t *testing.T) {
eng, _ := setupEngine(t)
if eng.rootCert == nil {
t.Fatal("root cert is nil")
}
if eng.rootKey == nil {
t.Fatal("root key is nil")
}
if !eng.rootCert.IsCA {
t.Error("root cert is not a CA")
}
if eng.rootCert.Subject.CommonName != "TestOrg Root CA" {
t.Errorf("root CN: got %q, want %q", eng.rootCert.Subject.CommonName, "TestOrg Root CA")
}
if eng.rootCert.MaxPathLen != 1 {
t.Errorf("root MaxPathLen: got %d, want 1", eng.rootCert.MaxPathLen)
}
}
func TestUnsealSealLifecycle(t *testing.T) {
eng, b := setupEngine(t)
mountPath := "engine/ca/test/"
// Seal and verify state is cleared.
if err := eng.Seal(); err != nil {
t.Fatalf("Seal: %v", err)
}
if eng.rootCert != nil {
t.Error("rootCert should be nil after seal")
}
if eng.rootKey != nil {
t.Error("rootKey should be nil after seal")
}
// Unseal and verify state is restored.
ctx := context.Background()
if err := eng.Unseal(ctx, b, mountPath); err != nil {
t.Fatalf("Unseal: %v", err)
}
if eng.rootCert == nil {
t.Error("rootCert should be non-nil after unseal")
}
if eng.rootKey == nil {
t.Error("rootKey should be non-nil after unseal")
}
if !eng.rootCert.IsCA {
t.Error("root cert should be CA after unseal")
}
}
func TestCreateIssuer(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
req := &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{
"name": "infra",
},
}
resp, err := eng.HandleRequest(ctx, req)
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
if resp.Data["name"] != "infra" {
t.Errorf("issuer name: got %v, want %q", resp.Data["name"], "infra")
}
// Verify the issuer cert is an intermediate CA signed by root.
certPEM := resp.Data["cert_pem"].(string)
block, _ := pem.Decode([]byte(certPEM))
if block == nil {
t.Fatal("failed to decode issuer cert PEM")
}
issuerCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse issuer cert: %v", err)
}
if !issuerCert.IsCA {
t.Error("issuer cert should be a CA")
}
// MaxPathLen 0 with MaxPathLenZero=false parses as -1 in Go's x509.
// Either 0 or -1 is acceptable for a path-length-constrained intermediate.
if issuerCert.MaxPathLen > 0 {
t.Errorf("issuer MaxPathLen: got %d, want 0 or -1", issuerCert.MaxPathLen)
}
if issuerCert.Subject.CommonName != "infra" {
t.Errorf("issuer CN: got %q, want %q", issuerCert.Subject.CommonName, "infra")
}
// Verify issuer is in memory.
if _, ok := eng.issuers["infra"]; !ok {
t.Error("issuer not found in memory")
}
}
func TestCreateIssuerRejectsNonAdmin(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
req := &engine.Request{
Operation: "create-issuer",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"name": "infra",
},
}
_, err := eng.HandleRequest(ctx, req)
if err == nil {
t.Fatal("expected error for non-admin create-issuer")
}
if err != ErrForbidden {
t.Errorf("expected ErrForbidden, got: %v", err)
}
}
func TestCreateIssuerRejectsNilCallerInfo(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
req := &engine.Request{
Operation: "create-issuer",
Data: map[string]interface{}{
"name": "infra",
},
}
_, err := eng.HandleRequest(ctx, req)
if err != ErrUnauthorized {
t.Errorf("expected ErrUnauthorized, got: %v", err)
}
}
func TestIssueCertificate(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
// Create an issuer first.
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Issue a certificate.
resp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
Path: "infra",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "web.example.com",
"profile": "server",
"dns_names": []interface{}{"web.example.com", "www.example.com"},
},
})
if err != nil {
t.Fatalf("issue: %v", err)
}
if resp.Data["cn"] != "web.example.com" {
t.Errorf("cn: got %v", resp.Data["cn"])
}
if resp.Data["serial"] == nil || resp.Data["serial"] == "" {
t.Error("serial should not be empty")
}
if resp.Data["cert_pem"] == nil {
t.Error("cert_pem should not be nil")
}
if resp.Data["key_pem"] == nil {
t.Error("key_pem should not be nil")
}
if resp.Data["chain_pem"] == nil {
t.Error("chain_pem should not be nil")
}
// Verify the leaf cert.
certPEM := resp.Data["cert_pem"].(string)
block, _ := pem.Decode([]byte(certPEM))
leafCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse leaf cert: %v", err)
}
if leafCert.IsCA {
t.Error("leaf cert should not be a CA")
}
if leafCert.Subject.CommonName != "web.example.com" {
t.Errorf("leaf CN: got %q", leafCert.Subject.CommonName)
}
if len(leafCert.DNSNames) != 2 {
t.Errorf("leaf DNSNames: got %v", leafCert.DNSNames)
}
}
func TestIssueCertificateWithOverrides(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Issue with custom TTL and key usages.
resp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "peer.example.com",
"profile": "peer",
"ttl": "720h",
"key_usages": []interface{}{"digital signature"},
"ext_key_usages": []interface{}{"client auth"},
},
})
if err != nil {
t.Fatalf("issue with overrides: %v", err)
}
certPEM := resp.Data["cert_pem"].(string)
block, _ := pem.Decode([]byte(certPEM))
leafCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse leaf: %v", err)
}
// Verify client auth EKU.
hasClientAuth := false
for _, eku := range leafCert.ExtKeyUsage {
if eku == x509.ExtKeyUsageClientAuth {
hasClientAuth = true
}
}
if !hasClientAuth {
t.Error("expected client auth EKU")
}
}
func TestIssueRejectsNilCallerInfo(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "test.example.com",
},
})
if err != ErrUnauthorized {
t.Errorf("expected ErrUnauthorized, got: %v", err)
}
}
func TestPrivateKeyNotStoredInBarrier(t *testing.T) {
eng, b := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
resp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "test.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("issue: %v", err)
}
serial := resp.Data["serial"].(string)
// Check that the cert record does not contain a private key.
recordData, err := b.Get(ctx, "engine/ca/test/certs/"+serial+".json")
if err != nil {
t.Fatalf("get cert record: %v", err)
}
if strings.Contains(string(recordData), "PRIVATE KEY") {
t.Error("cert record should not contain private key")
}
}
func TestRenewCertificate(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Issue original cert.
issueResp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "renew.example.com",
"profile": "server",
"dns_names": []interface{}{"renew.example.com"},
},
})
if err != nil {
t.Fatalf("issue: %v", err)
}
origSerial := issueResp.Data["serial"].(string)
// Renew.
renewResp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "renew",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"serial": origSerial,
},
})
if err != nil {
t.Fatalf("renew: %v", err)
}
newSerial := renewResp.Data["serial"].(string)
if newSerial == origSerial {
t.Error("renewed cert should have different serial")
}
if renewResp.Data["cn"] != "renew.example.com" {
t.Errorf("renewed CN: got %v", renewResp.Data["cn"])
}
if renewResp.Data["cert_pem"] == nil {
t.Error("renewed cert_pem should not be nil")
}
if renewResp.Data["key_pem"] == nil {
t.Error("renewed key_pem should not be nil")
}
}
func TestGetAndListCerts(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Issue two certs.
for _, cn := range []string{"a.example.com", "b.example.com"} {
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": cn,
"profile": "server",
},
})
if err != nil {
t.Fatalf("issue %s: %v", cn, err)
}
}
// List certs.
listResp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "list-certs",
CallerInfo: userCaller(),
})
if err != nil {
t.Fatalf("list-certs: %v", err)
}
certs, ok := listResp.Data["certs"].([]map[string]interface{})
if !ok {
t.Fatalf("certs type: %T", listResp.Data["certs"])
}
if len(certs) != 2 {
t.Errorf("expected 2 certs, got %d", len(certs))
}
// Get a specific cert.
serial := certs[0]["serial"].(string)
getResp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "get-cert",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"serial": serial,
},
})
if err != nil {
t.Fatalf("get-cert: %v", err)
}
if getResp.Data["serial"] != serial {
t.Errorf("get-cert serial: got %v, want %v", getResp.Data["serial"], serial)
}
}
func TestUnsealRestoresIssuers(t *testing.T) {
eng, b := setupEngine(t)
ctx := context.Background()
mountPath := "engine/ca/test/"
// Create issuer.
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Seal.
eng.Seal()
// Unseal.
if err := eng.Unseal(ctx, b, mountPath); err != nil {
t.Fatalf("Unseal: %v", err)
}
// Verify issuer was restored.
if _, ok := eng.issuers["infra"]; !ok {
t.Error("issuer 'infra' not restored after unseal")
}
// Verify we can issue from the restored issuer.
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "after-unseal.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("issue after unseal: %v", err)
}
}
func TestDeleteIssuer(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "delete-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("delete-issuer: %v", err)
}
if _, ok := eng.issuers["infra"]; ok {
t.Error("issuer should be deleted")
}
}
func TestPublicMethods(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
// Test GetRootCertPEM.
rootPEM, err := eng.GetRootCertPEM()
if err != nil {
t.Fatalf("GetRootCertPEM: %v", err)
}
block, _ := pem.Decode(rootPEM)
if block == nil {
t.Fatal("failed to decode root PEM")
}
// Create issuer for chain/issuer tests.
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Test GetIssuerCertPEM.
issuerPEM, err := eng.GetIssuerCertPEM("infra")
if err != nil {
t.Fatalf("GetIssuerCertPEM: %v", err)
}
block, _ = pem.Decode(issuerPEM)
if block == nil {
t.Fatal("failed to decode issuer PEM")
}
// Test GetChainPEM.
chainPEM, err := eng.GetChainPEM("infra")
if err != nil {
t.Fatalf("GetChainPEM: %v", err)
}
// Chain should contain two certificates.
certCount := strings.Count(string(chainPEM), "BEGIN CERTIFICATE")
if certCount != 2 {
t.Errorf("chain should contain 2 certs, got %d", certCount)
}
// Test nonexistent issuer.
_, err = eng.GetIssuerCertPEM("nonexistent")
if err != ErrIssuerNotFound {
t.Errorf("expected ErrIssuerNotFound, got: %v", err)
}
}

View File

@@ -0,0 +1,41 @@
package ca
import "git.wntrmute.dev/kyle/goutils/certlib/certgen"
// Default certificate profiles.
var defaultProfiles = map[string]certgen.Profile{
"server": {
KeyUse: []string{"digital signature", "key encipherment"},
ExtKeyUsages: []string{"server auth"},
Expiry: "2160h", // 90 days
},
"client": {
KeyUse: []string{"digital signature"},
ExtKeyUsages: []string{"client auth"},
Expiry: "2160h", // 90 days
},
"peer": {
KeyUse: []string{"digital signature", "key encipherment"},
ExtKeyUsages: []string{"server auth", "client auth"},
Expiry: "2160h", // 90 days
},
}
// GetProfile returns a copy of the named default profile.
func GetProfile(name string) (certgen.Profile, bool) {
p, ok := defaultProfiles[name]
if !ok {
return certgen.Profile{}, false
}
// Return a copy so callers can modify.
cp := certgen.Profile{
IsCA: p.IsCA,
PathLen: p.PathLen,
Expiry: p.Expiry,
KeyUse: make([]string, len(p.KeyUse)),
ExtKeyUsages: make([]string, len(p.ExtKeyUsages)),
}
copy(cp.KeyUse, p.KeyUse)
copy(cp.ExtKeyUsages, p.ExtKeyUsages)
return cp, true
}

View File

@@ -0,0 +1,37 @@
package ca
import "time"
// CAConfig is the CA engine configuration stored in the barrier.
type CAConfig struct {
Organization string `json:"organization"`
Country string `json:"country,omitempty"`
KeyAlgorithm string `json:"key_algorithm"` // "ecdsa", "rsa", "ed25519"
KeySize int `json:"key_size"` // e.g. 384 for ECDSA, 4096 for RSA
RootExpiry string `json:"root_expiry"` // e.g. "87600h" (10 years)
}
// IssuerConfig is per-issuer configuration stored in the barrier.
type IssuerConfig struct {
Name string `json:"name"`
KeyAlgorithm string `json:"key_algorithm"`
KeySize int `json:"key_size"`
Expiry string `json:"expiry"` // issuer cert expiry, e.g. "43800h" (5 years)
MaxTTL string `json:"max_ttl"` // max leaf cert TTL, e.g. "8760h" (1 year)
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
// CertRecord is metadata for an issued certificate, stored in the barrier.
// The private key is NOT stored.
type CertRecord struct {
Serial string `json:"serial"`
Issuer string `json:"issuer"`
CN string `json:"cn"`
SANs []string `json:"sans,omitempty"`
Profile string `json:"profile"`
CertPEM string `json:"cert_pem"`
IssuedBy string `json:"issued_by"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
}

View File

@@ -27,11 +27,19 @@ var (
ErrUnknownType = errors.New("engine: unknown engine type")
)
// CallerInfo carries authentication context into engines.
type CallerInfo struct {
Username string
Roles []string
IsAdmin bool
}
// Request is a request to an engine.
type Request struct {
Operation string
Path string
Data map[string]interface{}
Operation string
Path string
Data map[string]interface{}
CallerInfo *CallerInfo
}
// Response is a response from an engine.
@@ -44,7 +52,7 @@ type Engine interface {
// Type returns the engine type.
Type() EngineType
// Initialize sets up the engine for first use.
Initialize(ctx context.Context, b barrier.Barrier, mountPath string) error
Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error
// Unseal opens the engine using state from the barrier.
Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error
// Seal closes the engine and zeroizes key material.
@@ -58,10 +66,10 @@ type Factory func() Engine
// Mount represents a mounted engine instance.
type Mount struct {
Name string `json:"name"`
Type EngineType `json:"type"`
MountPath string `json:"mount_path"`
engine Engine
Name string `json:"name"`
Type EngineType `json:"type"`
MountPath string `json:"mount_path"`
Engine Engine `json:"-"`
}
// Registry manages mounted engine instances.
@@ -89,7 +97,7 @@ func (r *Registry) RegisterFactory(t EngineType, f Factory) {
}
// Mount creates and initializes a new engine mount.
func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType) error {
func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType, config map[string]interface{}) error {
r.mu.Lock()
defer r.mu.Unlock()
@@ -105,7 +113,7 @@ func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType
eng := factory()
mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name)
if err := eng.Initialize(ctx, r.barrier, mountPath); err != nil {
if err := eng.Initialize(ctx, r.barrier, mountPath, config); err != nil {
return fmt.Errorf("engine: initialize %q: %w", name, err)
}
@@ -113,11 +121,35 @@ func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType
Name: name,
Type: engineType,
MountPath: mountPath,
engine: eng,
Engine: eng,
}
return nil
}
// GetEngine returns the engine for the given mount name.
func (r *Registry) GetEngine(name string) (Engine, error) {
r.mu.RLock()
defer r.mu.RUnlock()
mount, exists := r.mounts[name]
if !exists {
return nil, ErrMountNotFound
}
return mount.Engine, nil
}
// GetMount returns the mount for the given name.
func (r *Registry) GetMount(name string) (*Mount, error) {
r.mu.RLock()
defer r.mu.RUnlock()
mount, exists := r.mounts[name]
if !exists {
return nil, ErrMountNotFound
}
return mount, nil
}
// Unmount removes and seals an engine mount.
func (r *Registry) Unmount(name string) error {
r.mu.Lock()
@@ -128,7 +160,7 @@ func (r *Registry) Unmount(name string) error {
return ErrMountNotFound
}
if err := mount.engine.Seal(); err != nil {
if err := mount.Engine.Seal(); err != nil {
return fmt.Errorf("engine: seal %q: %w", name, err)
}
@@ -162,7 +194,7 @@ func (r *Registry) HandleRequest(ctx context.Context, mountName string, req *Req
return nil, ErrMountNotFound
}
return mount.engine.HandleRequest(ctx, req)
return mount.Engine.HandleRequest(ctx, req)
}
// SealAll seals all mounted engines.
@@ -171,7 +203,7 @@ func (r *Registry) SealAll() error {
defer r.mu.Unlock()
for name, mount := range r.mounts {
if err := mount.engine.Seal(); err != nil {
if err := mount.Engine.Seal(); err != nil {
return fmt.Errorf("engine: seal %q: %w", name, err)
}
}

View File

@@ -14,22 +14,28 @@ type mockEngine struct {
unsealed bool
}
func (m *mockEngine) Type() EngineType { return m.engineType }
func (m *mockEngine) Initialize(_ context.Context, _ barrier.Barrier, _ string) error { m.initialized = true; return nil }
func (m *mockEngine) Unseal(_ context.Context, _ barrier.Barrier, _ string) error { m.unsealed = true; return nil }
func (m *mockEngine) Seal() error { m.unsealed = false; return nil }
func (m *mockEngine) Type() EngineType { return m.engineType }
func (m *mockEngine) Initialize(_ context.Context, _ barrier.Barrier, _ string, _ map[string]interface{}) error {
m.initialized = true
return nil
}
func (m *mockEngine) Unseal(_ context.Context, _ barrier.Barrier, _ string) error {
m.unsealed = true
return nil
}
func (m *mockEngine) Seal() error { m.unsealed = false; return nil }
func (m *mockEngine) HandleRequest(_ context.Context, _ *Request) (*Response, error) {
return &Response{Data: map[string]interface{}{"ok": true}}, nil
}
type mockBarrier struct{}
func (m *mockBarrier) Unseal(_ []byte) error { return nil }
func (m *mockBarrier) Seal() error { return nil }
func (m *mockBarrier) IsSealed() bool { return false }
func (m *mockBarrier) Get(_ context.Context, _ string) ([]byte, error) { return nil, barrier.ErrNotFound }
func (m *mockBarrier) Put(_ context.Context, _ string, _ []byte) error { return nil }
func (m *mockBarrier) Delete(_ context.Context, _ string) error { return nil }
func (m *mockBarrier) Unseal(_ []byte) error { return nil }
func (m *mockBarrier) Seal() error { return nil }
func (m *mockBarrier) IsSealed() bool { return false }
func (m *mockBarrier) Get(_ context.Context, _ string) ([]byte, error) { return nil, barrier.ErrNotFound }
func (m *mockBarrier) Put(_ context.Context, _ string, _ []byte) error { return nil }
func (m *mockBarrier) Delete(_ context.Context, _ string) error { return nil }
func (m *mockBarrier) List(_ context.Context, _ string) ([]string, error) { return nil, nil }
func TestRegistryMountUnmount(t *testing.T) {
@@ -39,7 +45,7 @@ func TestRegistryMountUnmount(t *testing.T) {
})
ctx := context.Background()
if err := reg.Mount(ctx, "default", EngineTypeTransit); err != nil {
if err := reg.Mount(ctx, "default", EngineTypeTransit, nil); err != nil {
t.Fatalf("Mount: %v", err)
}
@@ -52,7 +58,7 @@ func TestRegistryMountUnmount(t *testing.T) {
}
// Duplicate mount should fail.
if err := reg.Mount(ctx, "default", EngineTypeTransit); err != ErrMountExists {
if err := reg.Mount(ctx, "default", EngineTypeTransit, nil); err != ErrMountExists {
t.Fatalf("expected ErrMountExists, got: %v", err)
}
@@ -75,7 +81,7 @@ func TestRegistryUnmountNotFound(t *testing.T) {
func TestRegistryUnknownType(t *testing.T) {
reg := NewRegistry(&mockBarrier{})
err := reg.Mount(context.Background(), "test", EngineTypeTransit)
err := reg.Mount(context.Background(), "test", EngineTypeTransit, nil)
if err == nil {
t.Fatal("expected error for unknown engine type")
}
@@ -88,7 +94,7 @@ func TestRegistryHandleRequest(t *testing.T) {
})
ctx := context.Background()
reg.Mount(ctx, "test", EngineTypeTransit)
reg.Mount(ctx, "test", EngineTypeTransit, nil)
resp, err := reg.HandleRequest(ctx, "test", &Request{Operation: "encrypt"})
if err != nil {
@@ -111,8 +117,8 @@ func TestRegistrySealAll(t *testing.T) {
})
ctx := context.Background()
reg.Mount(ctx, "eng1", EngineTypeTransit)
reg.Mount(ctx, "eng2", EngineTypeTransit)
reg.Mount(ctx, "eng1", EngineTypeTransit, nil)
reg.Mount(ctx, "eng2", EngineTypeTransit, nil)
if err := reg.SealAll(); err != nil {
t.Fatalf("SealAll: %v", err)