# 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) |
