# SSH CA Engine Implementation Plan

## Overview

The SSH CA engine signs SSH host and user certificates using Go's
`golang.org/x/crypto/ssh` package. It follows the same architecture as the CA
engine: a single CA key pair signs certificates directly (no intermediate
hierarchy, since SSH certificates are flat).

## Engine Type

`sshca` — registered constant already exists in `internal/engine/engine.go`.

## Mount Configuration

Passed as `config` at mount time:

| Field           | Default          | Description                              |
|-----------------|------------------|------------------------------------------|
| `key_algorithm` | `"ed25519"`      | CA key type: `ed25519`, `ecdsa-p256`, `ecdsa-p384` |
| `max_ttl`       | `"87600h"`       | Maximum certificate validity             |
| `default_ttl`   | `"24h"`          | Default certificate validity             |

RSA is intentionally excluded — Ed25519 and ECDSA are preferred for SSH CAs.
This avoids the need for a `key_size` parameter and simplifies key generation.

## Barrier Storage Layout

```
engine/sshca/{mount}/config.json             Engine configuration
engine/sshca/{mount}/ca/key.pem              CA private key (PEM, PKCS8)
engine/sshca/{mount}/ca/pubkey.pub           CA public key (SSH authorized_keys format)
engine/sshca/{mount}/profiles/{name}.json    Signing profiles
engine/sshca/{mount}/certs/{serial}.json     Signed cert records
engine/sshca/{mount}/krl_version.json        KRL version counter
```

## In-Memory State

```go
type SSHCAEngine struct {
    barrier    barrier.Barrier
    config     *SSHCAConfig
    caKey      crypto.PrivateKey      // CA signing key
    caSigner   ssh.Signer             // ssh.Signer wrapping caKey
    mountPath  string
    krlVersion uint64                 // monotonically increasing
    mu         sync.RWMutex
}
```

Key material (`caKey`, `caSigner`) is zeroized on `Seal()`.

## Lifecycle

### Initialize

1. Parse and validate config: ensure `key_algorithm` is one of `ed25519`,
   `ecdsa-p256`, `ecdsa-p384`. Parse `max_ttl` and `default_ttl` as
   `time.Duration`.
2. Store config in barrier as `{mountPath}config.json`.
3. Generate CA key pair:
   - `ed25519`: `ed25519.GenerateKey(rand.Reader)`
   - `ecdsa-p256`: `ecdsa.GenerateKey(elliptic.P256(), rand.Reader)`
   - `ecdsa-p384`: `ecdsa.GenerateKey(elliptic.P384(), rand.Reader)`
4. Marshal private key to PEM using `x509.MarshalPKCS8PrivateKey` →
   `pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})`.
5. Store private key PEM in barrier at `{mountPath}ca/key.pem`.
6. Generate SSH public key via `ssh.NewPublicKey(pubKey)`, marshal with
   `ssh.MarshalAuthorizedKey`. Store at `{mountPath}ca/pubkey.pub`.
7. Load key into memory: `ssh.NewSignerFromKey(caKey)` → `caSigner`.
8. Initialize `krlVersion` to 0, store in barrier.

### Unseal

1. Load config JSON from barrier, unmarshal into `*SSHCAConfig`.
2. Load `{mountPath}ca/key.pem` from barrier, decode PEM, parse with
   `x509.ParsePKCS8PrivateKey` → `caKey`.
3. Create `caSigner` via `ssh.NewSignerFromKey(caKey)`.
4. Load `krl_version.json` from barrier → `krlVersion`.

### Seal

1. Zeroize `caKey` using the shared `zeroizeKey` helper (see Implementation
   References below).
2. Nil out `caSigner`, `config`.

## Operations

| Operation         | Auth Required | Description                                         |
|-------------------|---------------|-----------------------------------------------------|
| `get-ca-pubkey`   | None          | Return CA public key in SSH authorized_keys format   |
| `sign-host`       | User+Policy   | Sign an SSH host certificate                         |
| `sign-user`       | User+Policy   | Sign an SSH user certificate                         |
| `create-profile`  | Admin         | Create a signing profile                             |
| `update-profile`  | Admin         | Update a signing profile                             |
| `get-profile`     | User/Admin    | Get signing profile details                          |
| `list-profiles`   | User/Admin    | List signing profiles                                |
| `delete-profile`  | Admin         | Delete a signing profile                             |
| `get-cert`        | User/Admin    | Get cert record by serial                            |
| `list-certs`      | User/Admin    | List issued cert summaries                           |
| `revoke-cert`     | Admin         | Revoke a certificate (soft flag)                     |
| `delete-cert`     | Admin         | Delete a certificate record                          |

### HandleRequest dispatch

Follow the CA engine's pattern (`internal/engine/ca/ca.go:284-317`):

```go
func (e *SSHCAEngine) HandleRequest(ctx context.Context, req *engine.Request) (*engine.Response, error) {
    switch req.Operation {
    case "get-ca-pubkey":
        return e.handleGetCAPublicKey(ctx)
    case "sign-host":
        return e.handleSignHost(ctx, req)
    case "sign-user":
        return e.handleSignUser(ctx, req)
    case "create-profile":
        return e.handleCreateProfile(ctx, req)
    case "update-profile":
        return e.handleUpdateProfile(ctx, req)
    case "get-profile":
        return e.handleGetProfile(ctx, req)
    case "list-profiles":
        return e.handleListProfiles(ctx, req)
    case "delete-profile":
        return e.handleDeleteProfile(ctx, req)
    case "get-cert":
        return e.handleGetCert(ctx, req)
    case "list-certs":
        return e.handleListCerts(ctx, req)
    case "revoke-cert":
        return e.handleRevokeCert(ctx, req)
    case "delete-cert":
        return e.handleDeleteCert(ctx, req)
    default:
        return nil, fmt.Errorf("sshca: unknown operation: %s", req.Operation)
    }
}
```

### sign-host

Request data:

| Field         | Required | Description                                    |
|---------------|----------|------------------------------------------------|
| `public_key`  | Yes      | SSH public key to sign (authorized_keys format) |
| `hostnames`   | Yes      | Valid principals (hostnames)                    |
| `ttl`         | No       | Validity duration (default: `default_ttl`)     |
| `extensions`  | No       | Map of extensions to include                   |

Flow:
1. Authenticate caller (`req.CallerInfo.IsUser()`); admins bypass policy checks.
2. Parse the supplied SSH public key with `ssh.ParsePublicKey(ssh.ParseAuthorizedKey(...))`.
3. Parse TTL: if provided parse as `time.Duration`, cap at `config.MaxTTL`.
   If not provided, use `config.DefaultTTL`.
4. Policy check: for each hostname, check policy on
   `sshca/{mount}/id/{hostname}`, action `sign`. Use `req.CheckPolicy`.
   Fail early before generating a serial or building the cert.
5. Generate a 64-bit serial: `var buf [8]byte; rand.Read(buf[:]); serial := binary.BigEndian.Uint64(buf[:])`.
6. Build `ssh.Certificate`:
   ```go
   cert := &ssh.Certificate{
       Key:             parsedPubKey,
       Serial:          serial,
       CertType:        ssh.HostCert,
       KeyId:           fmt.Sprintf("host:%s:%d", hostnames[0], serial),
       ValidPrincipals: hostnames,
       ValidAfter:      uint64(time.Now().Unix()),
       ValidBefore:     uint64(time.Now().Add(ttl).Unix()),
       Permissions:     ssh.Permissions{Extensions: extensions},
   }
   ```
7. Sign: `cert.SignCert(rand.Reader, e.caSigner)`.
8. Store `CertRecord` in barrier at `{mountPath}certs/{serial}.json`.
9. Return: `{"certificate": ssh.MarshalAuthorizedKey(cert), "serial": serial}`.

### sign-user

Request data:

| Field            | Required | Description                                    |
|------------------|----------|------------------------------------------------|
| `public_key`     | Yes      | SSH public key to sign (authorized_keys format) |
| `principals`     | Yes      | Valid usernames/principals                      |
| `ttl`            | No       | Validity duration (default: `default_ttl`)     |
| `profile`        | No       | Signing profile name (see below)               |
| `extensions`     | No       | Map of extensions (e.g. `permit-pty`)          |

Critical options are **not accepted directly** in the sign request. They can
only be applied via a signing profile. This prevents unprivileged users from
setting security-sensitive options like `force-command` or `source-address`.

Flow:
1. Authenticate caller (`IsUser()`); admins bypass.
2. Parse the supplied SSH public key.
3. If `profile` is specified, load the signing profile from barrier and check
   policy (`sshca/{mount}/profile/{profile_name}`, action `read`). Merge the
   profile's critical options and extensions into the certificate. Any
   extensions in the request are merged with profile extensions; conflicts
   are resolved in favor of the profile. If the profile specifies
   `allowed_principals`, verify all requested principals are in the list.
4. If the profile specifies `max_ttl`, enforce it (cap the requested TTL).
5. Policy check: `sshca/{mount}/id/{principal}` for each principal, action `sign`.
   Default rule: a user can only sign certs for their own username as principal,
   unless a policy grants access to other principals. Implement by checking
   `req.CallerInfo.Username == principal` as the default-allow case.
   Fail early before generating a serial or building the cert.
6. Generate a 64-bit serial using `crypto/rand`.
7. Build `ssh.Certificate` with `CertType: ssh.UserCert`, principals, validity.
8. Set `Permissions.CriticalOptions` from profile (if any) and
   `Permissions.Extensions` from merged extensions. Default extensions when
   none specified: `{"permit-pty": ""}`.
9. Sign with `caSigner`.
10. Store `CertRecord` in barrier (includes profile name if used).
11. Return signed certificate in OpenSSH format + serial.

### Signing Profiles

A signing profile is a named, admin-defined template that controls what goes
into a signed user certificate. Profiles are the only way to set critical
options, and access to each profile is policy-gated.

#### Profile Configuration

```go
type SigningProfile struct {
    Name              string            `json:"name"`
    CriticalOptions   map[string]string `json:"critical_options"`             // e.g. {"force-command": "/usr/bin/rsync", "source-address": "10.0.0.0/8"}
    Extensions        map[string]string `json:"extensions"`                   // merged with request extensions
    MaxTTL            string            `json:"max_ttl,omitempty"`            // overrides engine max_ttl if shorter
    AllowedPrincipals []string          `json:"allowed_principals,omitempty"` // if set, restricts principals
}
```

#### Storage

```
engine/sshca/{mount}/profiles/{name}.json
```

#### Policy Gating

Access to a profile is controlled via policy on resource
`sshca/{mount}/profile/{profile_name}`, action `read`. A user must have
policy access to both the profile and the requested principals to sign
a certificate using that profile.

Example use cases:
- **`restricted-sftp`**: `force-command: "internal-sftp"`, `source-address: "10.0.0.0/8"` — grants users SFTP-only access from internal networks.
- **`deploy`**: `force-command: "/usr/local/bin/deploy"`, `source-address: "10.0.1.0/24"` — CI/CD deploy key with restricted command.
- **`unrestricted`**: empty critical options — for trusted users who need full shell access (admin-only policy).

## CertRecord

```go
type CertRecord struct {
    Serial     uint64    `json:"serial"`
    CertType   string    `json:"cert_type"`    // "host" or "user"
    Principals []string  `json:"principals"`
    CertData   string    `json:"cert_data"`    // OpenSSH authorized_keys format
    KeyID      string    `json:"key_id"`       // certificate KeyId field
    Profile    string    `json:"profile,omitempty"` // signing profile used (if any)
    IssuedBy   string    `json:"issued_by"`
    IssuedAt   time.Time `json:"issued_at"`
    ExpiresAt  time.Time `json:"expires_at"`
    Revoked    bool      `json:"revoked,omitempty"`
    RevokedAt  time.Time `json:"revoked_at,omitempty"`
    RevokedBy  string    `json:"revoked_by,omitempty"`
}
```

Serial is stored as `uint64` (not string) since SSH certificate serials are
uint64 natively. Barrier path uses the decimal string representation:
`fmt.Sprintf("%d", serial)`.

## Key Revocation List (KRL)

SSH servers cannot query Metacrypt in real time to check whether a certificate
has been revoked. Instead, the SSH CA engine generates a KRL that SSH servers
fetch periodically and reference via `RevokedKeys` in `sshd_config`.

### KRL Generation — Custom Implementation

**Important**: `golang.org/x/crypto/ssh` does **not** provide KRL generation
helpers. It can parse KRLs but not build them. The engine must implement KRL
serialization directly per the OpenSSH KRL format specification
(`PROTOCOL.krl` in the OpenSSH source).

The KRL format is a binary structure:

```
MAGIC   = "OPENSSH_KRL\x00" (12 bytes)
VERSION = uint32 (format version, always 1)
KRL_VERSION = uint64 (monotonically increasing per rebuild)
GENERATED_DATE = uint64 (Unix timestamp)
FLAGS   = uint64 (0)
RESERVED = string (empty)
COMMENT  = string (empty)
SECTIONS... (one or more typed sections)
```

For serial-based revocation (the simplest and most compact representation):

```
Section type: KRL_SECTION_CERTIFICATES (0x01)
  CA key blob: ssh.MarshalAuthorizedKey(caSigner.PublicKey())
  Subsection type: KRL_SECTION_CERT_SERIAL_LIST (0x20)
    Revoked serials: sorted list of uint64 serials
```

Implement as a `buildKRL` function:

```go
func (e *SSHCAEngine) buildKRL(revokedSerials []uint64) []byte {
    // 1. Sort serials.
    // 2. Write MAGIC header.
    // 3. Write KRL_VERSION (e.krlVersion), GENERATED_DATE (now), FLAGS (0).
    // 4. Write RESERVED (empty string), COMMENT (empty string).
    // 5. Write section header: type=0x01 (KRL_SECTION_CERTIFICATES).
    // 6. Write CA public key blob.
    // 7. Write subsection: type=0x20 (KRL_SECTION_CERT_SERIAL_LIST),
    //    followed by each serial as uint64 big-endian.
    // 8. Return assembled bytes.
}
```

Use `encoding/binary` with `binary.BigEndian` for all integer encoding.
SSH strings are length-prefixed: `uint32(len) + bytes`.

The KRL version counter is persisted in barrier at `{mountPath}krl_version.json`
and incremented on each rebuild. On unseal, the counter is loaded from barrier.

The KRL is rebuilt (not stored in barrier — it's a derived artifact) on:
- `revoke-cert` — collects all revoked serials, rebuilds.
- `delete-cert` — if the cert was revoked, rebuilds from remaining revoked certs.
- Engine unseal — rebuilds from all revoked certs.

### Distribution

KRL distribution is a pull model. SSH servers fetch the current KRL via an
unauthenticated endpoint (analogous to the public CA key endpoint):

| Method | Path                               | Description                    |
|--------|-------------------------------------|--------------------------------|
| GET    | `/v1/sshca/{mount}/krl`            | Current KRL (binary)           |

The response includes:
- `Content-Type: application/octet-stream`
- `ETag` header: `fmt.Sprintf("%d", e.krlVersion)`, enabling conditional fetches.
- `Cache-Control: max-age=60` to encourage periodic refresh.

SSH servers should be configured to fetch the KRL on a cron schedule (e.g.
every 1–5 minutes) and write it to a local file referenced by `sshd_config`:

```
RevokedKeys /etc/ssh/metacrypt_krl
```

## gRPC Service (proto/metacrypt/v2/sshca.proto)

```protobuf
service SSHCAService {
    rpc GetCAPublicKey(GetCAPublicKeyRequest)   returns (GetCAPublicKeyResponse);
    rpc SignHost(SignHostRequest)                returns (SignHostResponse);
    rpc SignUser(SignUserRequest)                returns (SignUserResponse);
    rpc CreateProfile(CreateProfileRequest)     returns (CreateProfileResponse);
    rpc UpdateProfile(UpdateProfileRequest)     returns (UpdateProfileResponse);
    rpc GetProfile(GetProfileRequest)           returns (GetProfileResponse);
    rpc ListProfiles(ListProfilesRequest)       returns (ListProfilesResponse);
    rpc DeleteProfile(DeleteProfileRequest)     returns (DeleteProfileResponse);
    rpc GetCert(SSHGetCertRequest)              returns (SSHGetCertResponse);
    rpc ListCerts(SSHListCertsRequest)          returns (SSHListCertsResponse);
    rpc RevokeCert(SSHRevokeCertRequest)        returns (SSHRevokeCertResponse);
    rpc DeleteCert(SSHDeleteCertRequest)        returns (SSHDeleteCertResponse);
    rpc GetKRL(GetKRLRequest)                   returns (GetKRLResponse);
}
```

## REST Endpoints

Public (unseal required, no auth):

| Method | Path                               | Description                    |
|--------|-------------------------------------|--------------------------------|
| GET    | `/v1/sshca/{mount}/ca`             | CA public key (SSH format)     |
| GET    | `/v1/sshca/{mount}/krl`            | Current KRL (binary)           |

Typed endpoints (auth required):

| Method | Path                                       | Description          |
|--------|--------------------------------------------|----------------------|
| POST   | `/v1/sshca/{mount}/sign-host`              | Sign host cert       |
| POST   | `/v1/sshca/{mount}/sign-user`              | Sign user cert       |
| POST   | `/v1/sshca/{mount}/profiles`               | Create profile       |
| GET    | `/v1/sshca/{mount}/profiles`               | List profiles        |
| GET    | `/v1/sshca/{mount}/profiles/{name}`        | Get profile          |
| PUT    | `/v1/sshca/{mount}/profiles/{name}`        | Update profile       |
| DELETE | `/v1/sshca/{mount}/profiles/{name}`        | Delete profile       |
| GET    | `/v1/sshca/{mount}/certs`                  | List cert records    |
| GET    | `/v1/sshca/{mount}/cert/{serial}`          | Get cert record      |
| POST   | `/v1/sshca/{mount}/cert/{serial}/revoke`   | Revoke cert          |
| DELETE | `/v1/sshca/{mount}/cert/{serial}`          | Delete cert record   |

### REST Route Registration

Add to `internal/server/routes.go` in `registerRoutes`, following the CA
engine's pattern with `chi.URLParam`:

```go
// SSH CA public routes (no auth, unseal required).
r.Get("/v1/sshca/{mount}/ca", s.requireUnseal(s.handleSSHCAPublicKey))
r.Get("/v1/sshca/{mount}/krl", s.requireUnseal(s.handleSSHCAKRL))

// SSH CA typed routes (auth required).
r.Post("/v1/sshca/{mount}/sign-host", s.requireAuth(s.handleSSHCASignHost))
r.Post("/v1/sshca/{mount}/sign-user", s.requireAuth(s.handleSSHCASignUser))
r.Post("/v1/sshca/{mount}/profiles", s.requireAdmin(s.handleSSHCACreateProfile))
r.Get("/v1/sshca/{mount}/profiles", s.requireAuth(s.handleSSHCAListProfiles))
r.Get("/v1/sshca/{mount}/profiles/{name}", s.requireAuth(s.handleSSHCAGetProfile))
r.Put("/v1/sshca/{mount}/profiles/{name}", s.requireAdmin(s.handleSSHCAUpdateProfile))
r.Delete("/v1/sshca/{mount}/profiles/{name}", s.requireAdmin(s.handleSSHCADeleteProfile))
r.Get("/v1/sshca/{mount}/certs", s.requireAuth(s.handleSSHCAListCerts))
r.Get("/v1/sshca/{mount}/cert/{serial}", s.requireAuth(s.handleSSHCAGetCert))
r.Post("/v1/sshca/{mount}/cert/{serial}/revoke", s.requireAdmin(s.handleSSHCARevokeCert))
r.Delete("/v1/sshca/{mount}/cert/{serial}", s.requireAdmin(s.handleSSHCADeleteCert))
```

Each handler extracts `chi.URLParam(r, "mount")`, builds an `engine.Request`
with the appropriate operation name and data, and calls
`s.engines.HandleRequest(...)`. Follow the `handleGetCert`/`handleRevokeCert`
pattern in the existing code.

All operations are also accessible via the generic `POST /v1/engine/request`.

### gRPC Interceptor Maps

Add to `sealRequiredMethods`, `authRequiredMethods`, and `adminRequiredMethods`
in `internal/grpcserver/server.go`:

```go
// sealRequiredMethods:
"/metacrypt.v2.SSHCAService/GetCAPublicKey": true,
"/metacrypt.v2.SSHCAService/SignHost":       true,
"/metacrypt.v2.SSHCAService/SignUser":       true,
"/metacrypt.v2.SSHCAService/CreateProfile":  true,
"/metacrypt.v2.SSHCAService/UpdateProfile":  true,
"/metacrypt.v2.SSHCAService/GetProfile":     true,
"/metacrypt.v2.SSHCAService/ListProfiles":   true,
"/metacrypt.v2.SSHCAService/DeleteProfile":  true,
"/metacrypt.v2.SSHCAService/GetCert":        true,
"/metacrypt.v2.SSHCAService/ListCerts":      true,
"/metacrypt.v2.SSHCAService/RevokeCert":     true,
"/metacrypt.v2.SSHCAService/DeleteCert":     true,
"/metacrypt.v2.SSHCAService/GetKRL":         true,

// authRequiredMethods (all except GetCAPublicKey and GetKRL):
"/metacrypt.v2.SSHCAService/SignHost":       true,
"/metacrypt.v2.SSHCAService/SignUser":       true,
"/metacrypt.v2.SSHCAService/CreateProfile":  true,
"/metacrypt.v2.SSHCAService/UpdateProfile":  true,
"/metacrypt.v2.SSHCAService/GetProfile":     true,
"/metacrypt.v2.SSHCAService/ListProfiles":   true,
"/metacrypt.v2.SSHCAService/DeleteProfile":  true,
"/metacrypt.v2.SSHCAService/GetCert":        true,
"/metacrypt.v2.SSHCAService/ListCerts":      true,
"/metacrypt.v2.SSHCAService/RevokeCert":     true,
"/metacrypt.v2.SSHCAService/DeleteCert":     true,

// adminRequiredMethods:
"/metacrypt.v2.SSHCAService/CreateProfile":  true,
"/metacrypt.v2.SSHCAService/UpdateProfile":  true,
"/metacrypt.v2.SSHCAService/DeleteProfile":  true,
"/metacrypt.v2.SSHCAService/RevokeCert":     true,
"/metacrypt.v2.SSHCAService/DeleteCert":     true,
```

Also add SSH CA operations to `adminOnlyOperations` in `routes.go` (keys are
`engineType:operation` to avoid cross-engine name collisions):

```go
// SSH CA engine.
"sshca:create-profile": true,
"sshca:update-profile": true,
"sshca:delete-profile": true,
"sshca:revoke-cert":    true,
"sshca:delete-cert":    true,
```

## Web UI

Add to `/dashboard` the ability to mount an SSH CA engine.

Add an `/sshca` page (or section on the existing PKI page) displaying:
- CA public key (for `TrustedUserCAKeys` / `@cert-authority` lines)
- Sign host/user certificate form
- Certificate list with detail view

## Implementation Steps

1. **Move `zeroizeKey` to shared location**: Copy the `zeroizeKey` function
   from `internal/engine/ca/ca.go` (lines 1481–1498) to a new file
   `internal/engine/helpers.go` in the `engine` package. Export it as
   `engine.ZeroizeKey`. Update the CA engine to call `engine.ZeroizeKey`
   instead of its local copy. This avoids a circular import (sshca cannot
   import ca).

2. **`internal/engine/sshca/`** — Implement `SSHCAEngine`:
   - `types.go` — `SSHCAConfig`, `CertRecord`, `SigningProfile` structs.
   - `sshca.go` — `NewSSHCAEngine` factory, lifecycle methods (`Type`,
     `Initialize`, `Unseal`, `Seal`), `HandleRequest` dispatch.
   - `sign.go` — `handleSignHost`, `handleSignUser`.
   - `profiles.go` — Profile CRUD handlers.
   - `certs.go` — `handleGetCert`, `handleListCerts`, `handleRevokeCert`,
     `handleDeleteCert`.
   - `krl.go` — `buildKRL`, `rebuildKRL`, `handleGetKRL`,
     `collectRevokedSerials`.

3. **Register factory** in `cmd/metacrypt/server.go` (line 76):
   ```go
   engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
   ```

4. **Proto definitions** — `proto/metacrypt/v2/sshca.proto`, run `make proto`.

5. **gRPC handlers** — `internal/grpcserver/sshca.go`. Follow
   `internal/grpcserver/ca.go` pattern: `sshcaServer` struct wrapping
   `GRPCServer`, helper function for error mapping, typed RPC methods.
   Register with `pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s})`
   in `server.go`.

6. **REST routes** — Add to `internal/server/routes.go` per the route
   registration section above.

7. **Tests** — `internal/engine/sshca/sshca_test.go`: unit tests with
   in-memory barrier following the CA test pattern. Test:
   - Initialize + unseal lifecycle
   - sign-host: valid signing, TTL enforcement, serial uniqueness
   - sign-user: own-principal default, profile merging, profile TTL cap
   - Profile CRUD
   - Certificate list/get/revoke/delete
   - KRL rebuild correctness (revoked serials present, unrevoked absent)
   - Seal zeroizes key material

## Dependencies

- `golang.org/x/crypto/ssh` (already in `go.mod` via transitive deps)
- `encoding/binary` (stdlib, for KRL serialization)

## Security Considerations

- CA private key encrypted at rest in barrier, zeroized on seal.
- Signed certificates do not contain private keys.
- Serial numbers are always generated server-side using `crypto/rand` (64-bit);
  user-provided serials are not accepted.
- `max_ttl` is enforced server-side; the engine rejects TTL values exceeding it.
- User cert signing defaults to allowing only the caller's own username as
  principal, preventing privilege escalation.
- Critical options (`force-command`, `source-address`, etc.) are only settable
  via admin-defined signing profiles, never directly in the sign request. This
  prevents unprivileged users from bypassing `sshd_config` restrictions.
- Profile access is policy-gated: a user must have policy access to
  `sshca/{mount}/profile/{name}` to use a profile.
- RSA keys are excluded to reduce attack surface and simplify the implementation.

## Implementation References

These existing code patterns should be followed exactly:

| Pattern | Reference File | Lines |
|---------|---------------|-------|
| HandleRequest switch dispatch | `internal/engine/ca/ca.go` | 284–317 |
| zeroizeKey helper | `internal/engine/ca/ca.go` | 1481–1498 |
| CertRecord storage (JSON in barrier) | `internal/engine/ca/ca.go` | cert storage pattern |
| REST route registration with chi | `internal/server/routes.go` | 38–50 |
| gRPC handler structure | `internal/grpcserver/ca.go` | full file |
| gRPC interceptor maps | `internal/grpcserver/server.go` | 107–192 |
| Engine factory registration | `cmd/metacrypt/server.go` | 76 |
| adminOnlyOperations map | `internal/server/routes.go` | 259–279 |
