# CA/PKI Engine Implementation Plan ## Context Metacrypt needs its first concrete engine implementation: the CA (PKI) engine. This provides X.509 certificate issuance for Metacircular infrastructure. A single root CA issues scoped intermediate CAs ("issuers"), which in turn issue leaf certificates. An unauthenticated public API serves CA/issuer certificates to allow systems to bootstrap TLS trust. Certificate generation uses the `certgen` package from `git.wntrmute.dev/mc/goutils/certlib/certgen`. ## Implementation Order ### Step 1: Add goutils dependency **File: `go.mod`** - Add `git.wntrmute.dev/mc/goutils` with local replace directive (same pattern as mcias) - Run `go mod tidy` ### Step 2: Update engine framework **File: `internal/engine/engine.go`** 1. Add `CallerInfo` struct to carry auth context into engines: ```go type CallerInfo struct { Username string Roles []string IsAdmin bool } ``` 2. Add `CallerInfo *CallerInfo` field to `Request` struct 3. Add `config map[string]interface{}` parameter to `Engine.Initialize` interface method 4. Update `Registry.Mount` to accept and pass through `config map[string]interface{}` 5. Add `GetEngine(name string) (Engine, error)` method on `Registry` (needed for public PKI routes) 6. Add `GetMount(name string) (*Mount, error)` method on `Registry` (to verify mount type) 7. Export `Mount.Engine` field (or add accessor) so server can type-assert to `*ca.CAEngine` **File: `internal/engine/engine_test.go`** - Update `mockEngine.Initialize` signature for new `config` parameter - Update all `reg.Mount(...)` calls to pass `nil` config ### Step 3: Create CA engine types **File: `internal/engine/ca/types.go`** (new) - `CAConfig` - stored in barrier, holds org info, key algorithm, root expiry - `IssuerConfig` - per-issuer config (name, key algorithm, expiry, max_ttl, created_by/at) - `CertRecord` - issued cert metadata stored in barrier (serial, issuer, CN, SANs, cert PEM, issued_by/at; NO private key) ### Step 4: Create certificate profiles **File: `internal/engine/ca/profiles.go`** (new) Default `certgen.Profile` entries: - `server`: digital signature + key encipherment, server auth, 90 days - `client`: digital signature, client auth, 90 days - `peer`: digital signature + key encipherment, server auth + client auth, 90 days ### Step 5: Implement CA engine **File: `internal/engine/ca/ca.go`** (new) Core struct: ```go type CAEngine struct { mu sync.RWMutex barrier barrier.Barrier mountPath string config *CAConfig rootCert *x509.Certificate rootKey crypto.PrivateKey issuers map[string]*issuerState } ``` **Initialize**: Parse config, generate self-signed root CA via `certgen.GenerateSelfSigned`, store root cert+key and config in barrier. **Unseal**: Load config, root cert+key, and all issuers from barrier into memory. **Seal**: Zeroize all in-memory key material, nil out pointers. **HandleRequest** dispatch: | Operation | Auth Required | Description | |-----------|--------------|-------------| | `get-root` | none | Return root CA cert PEM | | `get-chain` | none | Return full chain PEM | | `get-issuer` | none | Return issuer cert PEM (path=name) | | `create-issuer` | admin | Generate intermediate CA signed by root | | `delete-issuer` | admin | Remove issuer and zeroize key | | `list-issuers` | any auth | List issuer names | | `issue` | user/admin | Issue leaf cert from named issuer | | `get-cert` | any auth | Get cert record by serial | | `list-certs` | any auth | List issued cert summaries | | `renew` | user/admin/system | Re-issue cert with same attributes, new validity | **create-issuer** flow: 1. Generate key pair, build CSR with CN="{name}", O=config.Organization 2. Sign with root via `profile.SignRequest(rootCert, csr, rootKey)` (profile: IsCA=true, PathLen=0) 3. Store issuer cert+key+config in barrier under `issuers/{name}/` **issue** flow: 1. Look up issuer by name from `req.Path` 2. Start from named profile defaults, apply user overrides (ttl, key_usages, ext_key_usages, key_algorithm) 3. Generate leaf key pair, build CSR, sign with issuer via `profile.SignRequest` 4. Store `CertRecord` in barrier (cert PEM + metadata, NO private key) 5. Return cert PEM, private key PEM, chain PEM, serial, metadata **renew** flow: 1. Load original `CertRecord`, parse cert to extract subject/SANs/usages 2. Generate new key, sign new cert with same issuer and attributes 3. Store new `CertRecord`, return new cert+key+chain Exported methods for public routes: - `GetRootCertPEM() ([]byte, error)` - `GetIssuerCertPEM(name string) ([]byte, error)` - `GetChainPEM(issuerName string) ([]byte, error)` **Barrier storage layout:** ``` engine/ca/{mount}/config.json engine/ca/{mount}/root/cert.pem engine/ca/{mount}/root/key.pem engine/ca/{mount}/issuers/{name}/cert.pem engine/ca/{mount}/issuers/{name}/key.pem engine/ca/{mount}/issuers/{name}/config.json engine/ca/{mount}/certs/{serial_hex}.json ``` ### Step 6: Update server routes **File: `internal/server/routes.go`** 1. **Replace `handleEngineMount` stub**: Parse `{name, type, config}` from JSON body, call `s.engines.Mount(ctx, name, type, config)` 2. **Replace `handleEngineRequest` stub**: Parse request body, populate `req.CallerInfo` from `TokenInfoFromContext`, call `s.engines.HandleRequest`, return response JSON 3. **Add public PKI routes** (no auth middleware): ```go mux.HandleFunc("GET /v1/pki/{mount}/ca", s.handlePKIRoot) mux.HandleFunc("GET /v1/pki/{mount}/ca/chain", s.handlePKIChain) mux.HandleFunc("GET /v1/pki/{mount}/issuer/{name}", s.handlePKIIssuer) ``` Each handler: get mount, verify type=ca, type-assert to `*ca.CAEngine`, call exported method, write PEM with `Content-Type: application/x-pem-file`. Return 503 if sealed. ### Step 7: Register CA factory **File: `cmd/metacrypt/server.go`** - Import `git.wntrmute.dev/mc/metacrypt/internal/engine/ca` - After creating `engineRegistry`, call `engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)` ### Step 8: Tests **File: `internal/engine/ca/ca_test.go`** (new) In-memory barrier implementation for testing (stores data in a `map[string][]byte`). Tests: - Initialize generates valid self-signed root CA - Unseal/Seal lifecycle preserves and zeroizes state - Create issuer produces intermediate signed by root - Issue certificate with profile defaults - Issue certificate with user overrides (ttl, key_usages) - Issued cert private key NOT stored in barrier - Renew produces new cert with same attributes - Get/List certs - Auth: create-issuer rejects non-admin CallerInfo - Auth: issue allows user role, rejects nil CallerInfo ## Verification ```bash go build ./... # Compiles cleanly go test ./... # All tests pass go vet ./... # No issues ``` ## Files Summary | File | Action | |------|--------| | `go.mod` | modify (add goutils) | | `internal/engine/engine.go` | modify (CallerInfo, config param, GetEngine/GetMount) | | `internal/engine/engine_test.go` | modify (update mock + call sites) | | `internal/engine/ca/types.go` | create | | `internal/engine/ca/profiles.go` | create | | `internal/engine/ca/ca.go` | create | | `internal/engine/ca/ca_test.go` | create | | `internal/server/routes.go` | modify (wire up engine handlers, add PKI routes) | | `cmd/metacrypt/server.go` | modify (register CA factory) |