# Metacrypt Architecture & Specification Metacrypt is a cryptographic service for the Metacircular platform. It provides cryptographic resources via a modular "engines" architecture, backed by an encrypted storage barrier inspired by HashiCorp Vault. Authentication is delegated to the Metacircular Identity and Access Service (MCIAS). ## Table of Contents 1. [System Overview](#system-overview) 2. [Key Hierarchy & Cryptographic Design](#key-hierarchy--cryptographic-design) 3. [Seal/Unseal Lifecycle](#sealunseal-lifecycle) 4. [Encrypted Storage Barrier](#encrypted-storage-barrier) 5. [Authentication & Authorization](#authentication--authorization) 6. [Engine Architecture](#engine-architecture) 7. [API Surface](#api-surface) 8. [Web Interface](#web-interface) 9. [Database Schema](#database-schema) 10. [Configuration](#configuration) 11. [Deployment](#deployment) 12. [Security Model](#security-model) 13. [Future Work](#future-work) --- ## System Overview ``` ┌─────────────────────────────────────────┐ │ Clients (CLI / Web UI) │ └──────────────────┬──────────────────────┘ │ HTTPS (TLS 1.2+) ┌──────────────────▼──────────────────────┐ │ HTTP API & Web Routes (server/) │ │ ├── Seal/Unseal endpoints │ │ ├── Auth endpoints (MCIAS delegation) │ │ ├── Engine request routing │ │ └── Policy management │ ├─────────────────────────────────────────┤ │ Middleware │ │ ├── Structured logging │ │ ├── Token authentication │ │ ├── Policy authorization │ │ └── Unseal-state gating │ ├─────────────────────────────────────────┤ │ Service Layer │ │ ├── Seal Manager (seal/) │ │ ├── Authenticator (auth/) │ │ ├── Policy Engine (policy/) │ │ └── Engine Registry (engine/) │ ├─────────────────────────────────────────┤ │ Encrypted Storage Barrier (barrier/) │ │ └── AES-256-GCM per-entry encryption │ ├─────────────────────────────────────────┤ │ Database Layer (db/) │ │ └── SQLite (WAL mode, foreign keys) │ └─────────────────────────────────────────┘ ``` ### Package Layout ``` cmd/metacrypt/ CLI entry point (server, init, status, snapshot) internal/ config/ TOML configuration loading & validation crypto/ Low-level cryptographic primitives db/ SQLite setup & schema migrations seal/ Seal/unseal state machine barrier/ Encrypted key-value storage abstraction auth/ MCIAS token authentication & caching policy/ Priority-based ACL engine engine/ Pluggable engine registry & interface ca/ CA (PKI) engine — X.509 certificate issuance acme/ ACME protocol handler (RFC 8555); EAB, accounts, orders server/ REST API HTTP server, routes, middleware grpcserver/ gRPC server, interceptors, per-service handlers webserver/ Web UI HTTP server, routes, HTMX handlers proto/metacrypt/ v1/ Original gRPC proto definitions (generic Execute RPC) v2/ Typed gRPC proto definitions (per-operation RPCs, Timestamp fields) gen/metacrypt/v1/ Generated Go gRPC/protobuf code (v1) gen/metacrypt/v2/ Generated Go gRPC/protobuf code (v2) web/ templates/ Go HTML templates (layout, init, unseal, login, dashboard, PKI) static/ CSS, HTMX deploy/ Docker Compose, example configs ``` --- ## Key Hierarchy & Cryptographic Design ### Primitives | Purpose | Algorithm | Parameters | |--------------------|--------------------|---------------------------------| | Key derivation | Argon2id | 3 iterations, 128 MiB, 4 threads (configurable) | | Symmetric encryption | AES-256-GCM | 256-bit keys, 12-byte random nonce | | Key size | 256 bits | All symmetric keys | | Salt size | 256 bits | Argon2id salt | | CSPRNG | `crypto/rand` | Keys, salts, nonces | | Constant-time comparison | `crypto/subtle` | Password & token comparison | | DEK wrapping | AES-256-GCM | MEK wraps per-engine DEKs | | Zeroization | Explicit overwrite | MEK, KWK, DEKs, passwords in memory | ### Key Hierarchy ``` User Password (not stored) │ ▼ ┌─────────────────────────────────────┐ │ Argon2id(password, salt, params) │ salt + params stored in seal_config └──────────────────┬──────────────────┘ ▼ Key Wrap Key (KWK) 256-bit, ephemeral (derived on unseal) │ ▼ ┌────────────────┐ │ AES-256-GCM │ encrypted_mek stored in seal_config │ Decrypt │ └───────┬────────┘ ▼ Master Encryption Key (MEK) 256-bit, held in memory only when unsealed │ ▼ ┌──────────────────────────┐ │ barrier_keys table │ MEK wraps per-engine DEKs │ ├── "system" DEK │ policy rules, mount metadata │ ├── "engine/ca/prod" DEK │ per-engine data encryption │ ├── "engine/ca/dev" DEK │ │ └── ... │ └────────────┬─────────────┘ ▼ ┌──────────────────────────┐ │ Barrier Entries │ Each entry encrypted with its │ ├── Policy rules │ engine's DEK via AES-256-GCM │ ├── Engine configs │ (v2 ciphertext format) │ └── Engine secrets │ └──────────────────────────┘ ``` Each engine mount gets its own Data Encryption Key (DEK) stored in the `barrier_keys` table, wrapped by the MEK. Non-engine data (policy rules, mount metadata) uses a `"system"` DEK. This limits blast radius: compromise of a single DEK only exposes one engine's data. ### Ciphertext Format Two versioned binary formats are supported: **v1** (legacy, `0x01`): ``` [version: 1 byte][nonce: 12 bytes][ciphertext + GCM tag] ``` **v2** (current, `0x02`): ``` [version: 1 byte][key_id_len: 1 byte][key_id: N bytes][nonce: 12 bytes][ciphertext + GCM tag] ``` The v2 format embeds a key identifier in the ciphertext, allowing the barrier to determine which DEK to use for decryption without external metadata. Key IDs are short path-like strings: - `"system"` — system DEK (policy rules, mount metadata) - `"engine/{type}/{mount}"` — per-engine DEK (e.g. `"engine/ca/prod"`) v1 ciphertext is still accepted for backward compatibility: it is decrypted with the MEK directly (no key ID lookup). The `migrate-barrier` command converts all v1 entries to v2 format with per-engine DEKs. ### Key Rotation **MEK rotation** (`POST /v1/barrier/rotate-mek`): Generates a new MEK, re-wraps all DEKs in `barrier_keys` with the new MEK, and updates `seal_config`. Requires the unseal password for verification. This is O(number of engines) — no data re-encryption is needed. **DEK rotation** (`POST /v1/barrier/rotate-key`): Generates a new DEK for a specific key ID, re-encrypts all barrier entries under that key's prefix with the new DEK, and updates `barrier_keys`. This is O(entries per engine). **Migration** (`POST /v1/barrier/migrate`): Converts all v1 (MEK-encrypted) barrier entries to v2 format with per-engine DEKs. Creates DEKs on demand for each engine mount. Idempotent — entries already in v2 format are skipped. The `barrier_keys` table schema: ```sql CREATE TABLE barrier_keys ( key_id TEXT PRIMARY KEY, version INTEGER NOT NULL DEFAULT 1, encrypted_dek BLOB NOT NULL, created_at DATETIME DEFAULT (datetime('now')), rotated_at DATETIME DEFAULT (datetime('now')) ); ``` --- ## Seal/Unseal Lifecycle Metacrypt operates as a state machine with four states: ``` ┌────────────────┐ │ Uninitialized │ No seal_config row in DB └───────┬────────┘ │ Initialize(password) │ • generate salt │ • derive KWK = Argon2id(password, salt) │ • generate random MEK │ • store Encrypt(KWK, MEK) + salt + params ▼ Seal() ┌──────────────────┐ ◄──────────────│ Unsealed │ MEK in memory; barrier open │ └──────────────────┘ │ ▲ │ │ Unseal(password) │ │ • load salt + params + encrypted_mek │ │ • derive KWK = Argon2id(password, salt) │ │ • MEK = Decrypt(KWK, encrypted_mek) │ │ • barrier.Unseal(MEK) │ │ • load & decrypt DEKs from barrier_keys │ ┌──────────────────┐ └─────────────►│ Sealed │ MEK + DEKs zeroized; barrier locked └──────────────────┘ ``` ### Rate Limiting Unseal attempts are rate-limited to mitigate online brute-force: - **5 attempts** within a 1-minute sliding window - **60-second lockout** after exceeding the limit - Counter resets after 1 minute of inactivity ### Sealing Calling `Seal()` immediately: 1. Zeroizes all DEKs and the MEK from memory 2. Seals the storage barrier (all reads/writes return `ErrSealed`) 3. Seals all mounted engines 4. Flushes the authentication token cache --- ## Encrypted Storage Barrier The barrier provides an encrypted key-value store over the `barrier_entries` table. Every value is independently encrypted with the MEK using AES-256-GCM. ### Interface ```go type Barrier interface { Unseal(mek []byte) error Seal() error IsSealed() bool Get(ctx context.Context, path string) ([]byte, error) Put(ctx context.Context, path string, value []byte) error Delete(ctx context.Context, path string) error List(ctx context.Context, prefix string) ([]string, error) } ``` ### Path Namespace Conventions | Prefix | Owner | Contents | |--------------------------------|----------------|---------------------------| | `policy/rules/{id}` | Policy engine | JSON-encoded ACL rules | | `engine/{type}/{mount}/` | Engine | Config, keys, engine data | | `engine/ca/{mount}/root/` | CA engine | Root CA cert + key | | `engine/ca/{mount}/issuers/` | CA engine | Issuer certs, keys, config | | `engine/ca/{mount}/certs/` | CA engine | Issued cert records (no private keys) | ### Properties - **Encryption at rest**: All values encrypted with MEK before database write - **Path-bound integrity**: The entry path is included as GCM additional authenticated data (AAD), preventing an attacker with database access from swapping encrypted blobs between paths - **Fresh nonce per write**: Every `Put` generates a new random nonce - **Atomic upsert**: Uses `INSERT ... ON CONFLICT UPDATE` for Put - **Glob listing**: `List(prefix)` returns relative paths matching the prefix - **Thread-safe**: All operations guarded by `sync.RWMutex` - **Fail-closed**: Returns `ErrSealed` for any operation when the barrier is sealed --- ## Authentication & Authorization ### Authentication (MCIAS Delegation) Metacrypt does not manage user accounts. All authentication is delegated to MCIAS: 1. Client sends `POST /v1/auth/login` with `{username, password, totp_code}` 2. Metacrypt forwards credentials to the MCIAS client library 3. On success, MCIAS returns a bearer token and expiration 4. Token is returned to the client (also set as `metacrypt_token` cookie for web UI) 5. Subsequent requests include `Authorization: Bearer ` or the cookie **Token validation** calls MCIAS `ValidateToken()`, with results cached for 30 seconds (keyed by SHA-256 hash of the token) to reduce MCIAS load. **Admin detection**: Users with the `admin` role in MCIAS are granted admin privileges in Metacrypt. ### Authorization (Policy Engine) The policy engine evaluates access control rules stored in the barrier. **Rule structure**: ```go type Rule struct { ID string // unique identifier Priority int // lower number = higher priority Effect Effect // "allow" or "deny" Usernames []string // match specific users (optional) Roles []string // match roles (optional) Resources []string // glob patterns, e.g. "engine/transit/*" (optional) Actions []string // "any", "read", "write", "encrypt", "decrypt", "sign", "verify", "hmac", "admin" (optional) } ``` **Evaluation algorithm**: 1. If the requester has the `admin` role, **allow** immediately (bypass) 2. Collect all rules where username, role, resource, and action match 3. Sort matching rules by priority (ascending; lower number = higher priority) 4. Return the effect of the highest-priority matching rule 5. **Default deny** if no rules match Matching is case-insensitive for usernames and roles. Resources use glob patterns. Empty fields in a rule match everything. The special action `any` matches all actions except `admin` — admin must always be granted explicitly. Rules are validated on creation: effects must be `allow` or `deny`, and actions must be from the recognized set. --- ## Engine Architecture Engines are pluggable cryptographic service providers. The CA (PKI) engine is implemented; remaining engine types are planned. ### Engine Types | Type | Status | Purpose | |-----------|---------------|----------------------------------------------| | `ca` | Implemented | X.509 Certificate Authority (PKI) | | `sshca` | Planned | SSH Certificate Authority | | `transit` | Planned | Encrypt/decrypt data in transit (envelope encryption) | | `user` | Planned | User-to-user end-to-end encryption | ### Engine Interface ```go type CallerInfo struct { Username string Roles []string IsAdmin bool } type Request struct { Operation string Path string Data map[string]interface{} CallerInfo *CallerInfo } type Engine interface { Type() EngineType Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error Seal() error HandleRequest(ctx context.Context, req *Request) (*Response, error) } ``` `CallerInfo` carries authentication context into engine operations, allowing engines to enforce their own auth requirements (e.g. admin-only operations). `Initialize` accepts a `config` map for engine-specific configuration passed at mount time. ### Mount Registry Engines are instantiated through a factory pattern and tracked in a central registry: ```go registry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine) registry.Mount(ctx, "pki", engine.EngineTypeCA, map[string]interface{}{ "organization": "Metacircular", "key_algorithm": "ecdsa", "key_size": 384, }) // Creates engine at barrier path: engine/ca/pki/ ``` Each mount gets its own namespace in the barrier for isolated data storage (config, keys, operational data). Mounting an engine calls `Initialize()`, which performs first-time setup (e.g. generating a root CA). On subsequent unseals, `Unseal()` loads existing state from the barrier. The registry provides `GetEngine(name)` and `GetMount(name)` methods for direct engine access, used by the public PKI routes to serve certificates without authentication. ### Request Routing ``` POST /v1/engine/request {mount: "pki", operation: "issue", path: "infra", data: {...}} → Registry.HandleRequest("pki", req) → engine.HandleRequest(ctx, req) → Response{Data: {...}} ``` ### CA (PKI) Engine The CA engine (`internal/engine/ca/`) provides X.509 certificate issuance for Metacircular infrastructure. It implements a two-tier PKI: a single root CA issues scoped intermediate CAs ("issuers"), which in turn issue leaf certificates. Certificate generation uses the `certgen` package from `git.wntrmute.dev/mc/goutils/certlib/certgen`. #### Lifecycle - **Initialize**: Generates a self-signed root CA, stores root cert+key and config in the barrier. - **Unseal**: Loads config, root cert+key, and all issuers from the barrier into memory. - **Seal**: Zeroizes all in-memory private key material (root key, all issuer keys), nils out pointers. #### Operations | Operation | Auth Required | Description | |-----------------|---------------|------------------------------------------| | `get-root` | None | Return root CA cert PEM | | `get-chain` | None | Return full chain PEM (issuer + root) | | `get-issuer` | None | Return issuer cert PEM | | `create-issuer` | Admin | Generate intermediate CA signed by root | | `delete-issuer` | Admin | Remove issuer and zeroize its key | | `list-issuers` | Any auth | List issuer names | | `issue` | Admin | Issue leaf cert from named issuer | | `get-cert` | User/Admin | Get cert record by serial | | `list-certs` | User/Admin | List issued cert summaries | | `renew` | Admin | Re-issue cert with same attributes | #### Certificate Profiles Three default profiles control leaf certificate key usage and validity: | Profile | Key Usage | Ext Key Usage | Default TTL | |----------|----------------------------------------|----------------------------|-------------| | `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 | Users can override TTL, key usages, ext key usages, and key algorithm at issuance time. #### Issuance Flow 1. Look up issuer by name 2. Start from named profile defaults, apply user overrides 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 #### Barrier Storage Layout ``` engine/ca/{mount}/config.json CA config (org, key algo, root expiry) engine/ca/{mount}/root/cert.pem Root CA certificate engine/ca/{mount}/root/key.pem Root CA private key engine/ca/{mount}/issuers/{name}/cert.pem Issuer certificate engine/ca/{mount}/issuers/{name}/key.pem Issuer private key engine/ca/{mount}/issuers/{name}/config.json Issuer config engine/ca/{mount}/certs/{serial_hex}.json Issued cert record (no private key) ``` #### CA Configuration Passed as `config` at mount time: | Field | Default | Description | |-----------------|------------------|----------------------------------| | `organization` | `"Metacircular"` | Root/issuer certificate O field | | `country` | `""` | Root/issuer certificate C field | | `key_algorithm` | `"ecdsa"` | Key type: ecdsa, rsa, ed25519 | | `key_size` | `384` | Key size (e.g. 256/384 for ECDSA, 2048/4096 for RSA) | | `root_expiry` | `"87600h"` | Root CA validity (10 years) | --- ## API Surface Metacrypt exposes two API surfaces: a JSON REST API and a gRPC API. Both are kept in sync — every operation available via REST has a corresponding gRPC RPC. ### Seal/Unseal (Unauthenticated) | Method | Path | Description | Precondition | |--------|---------------|--------------------------------|------------------| | GET | `/v1/status` | Service state + version info | None | | POST | `/v1/init` | First-time seal initialization | `uninitialized` | | POST | `/v1/unseal` | Unseal with password | `sealed` | ### Seal Control (Admin Only) | Method | Path | Description | |--------|-------------|-------------------------| | POST | `/v1/seal` | Seal service & engines | ### Barrier Key Management (Admin Only) | Method | Path | Description | |--------|----------------------------|------------------------------------------| | GET | `/v1/barrier/keys` | List DEKs with version + rotation times | | POST | `/v1/barrier/rotate-mek` | Rotate MEK (re-wraps all DEKs) | | POST | `/v1/barrier/rotate-key` | Rotate a specific DEK (re-encrypts data) | | POST | `/v1/barrier/migrate` | Migrate v1 entries to v2 with per-engine DEKs | ### Authentication | Method | Path | Description | Auth Required | |--------|--------------------|--------------------------|---------------| | POST | `/v1/auth/login` | MCIAS login → token | No | | POST | `/v1/auth/logout` | Invalidate token | Yes | | GET | `/v1/auth/tokeninfo` | Current user info | Yes | ### Engines (Authenticated) | Method | Path | Description | Auth | |--------|-----------------------|---------------------------|-----------| | GET | `/v1/engine/mounts` | List mounted engines | User | | POST | `/v1/engine/mount` | Create new engine mount | Admin | | POST | `/v1/engine/unmount` | Remove engine mount | Admin | | POST | `/v1/engine/request` | Route request to engine | User | ### Policy (Admin Only) | Method | Path | Description | |--------|-----------------------|------------------------------------| | GET | `/v1/policy/rules` | List all policy rules | | POST | `/v1/policy/rules` | Create a new policy rule | | GET | `/v1/policy/rule?id=` | Get a policy rule by ID | | PUT | `/v1/policy/rule?id=` | Update a policy rule by ID | | DELETE | `/v1/policy/rule?id=` | Delete a policy rule by ID | The mount endpoint accepts `{name, type, config}` where `config` is an engine-type-specific configuration object. The request endpoint accepts `{mount, operation, path, data}` and populates `CallerInfo` from the authenticated user's token. ### Public PKI (Unauthenticated, Unsealed Required) | Method | Path | Description | |--------|-------------------------------------|-------------------------------| | GET | `/v1/pki/{mount}/ca` | Root CA certificate (PEM) | | GET | `/v1/pki/{mount}/ca/chain` | Full chain: issuer + root (PEM) | | GET | `/v1/pki/{mount}/issuer/{name}` | Issuer certificate (PEM) | These routes serve certificates with `Content-Type: application/x-pem-file`, allowing systems to bootstrap TLS trust without authentication. The mount must be of type `ca`; returns 404 otherwise. ### CA Certificate Management (Authenticated) | Method | Path | Description | Auth | |--------|---------------------------------------|------------------------------------|---------| | GET | `/v1/ca/{mount}/cert/{serial}` | Get certificate record by serial | User | | POST | `/v1/ca/{mount}/cert/{serial}/revoke` | Revoke a certificate | Admin | | DELETE | `/v1/ca/{mount}/cert/{serial}` | Delete a certificate record | Admin | ### ACME (RFC 8555) ACME protocol endpoints are mounted per CA engine instance and require no authentication (per the ACME spec). External Account Binding (EAB) is supported. | Method | Path | Description | Auth | |--------|-----------------------------------|--------------------------------------|---------| | GET | `/acme/{mount}/directory` | ACME directory object | None | | HEAD/GET | `/acme/{mount}/new-nonce` | Obtain a fresh replay nonce | None | | POST | `/acme/{mount}/new-account` | Register or retrieve an account | None | | POST | `/acme/{mount}/new-order` | Create a new certificate order | None | | POST | `/acme/{mount}/authz/{id}` | Fetch/respond to an authorization | None | | POST | `/acme/{mount}/challenge/{type}/{id}` | Respond to a challenge | None | | POST | `/acme/{mount}/finalize/{id}` | Finalize an order (submit CSR) | None | | POST | `/acme/{mount}/cert/{id}` | Download issued certificate | None | | POST | `/acme/{mount}/revoke-cert` | Revoke a certificate via ACME | None | ACME management endpoints require MCIAS authentication: | Method | Path | Description | Auth | |--------|-------------------------------|--------------------------------------|---------| | POST | `/v1/acme/{mount}/eab` | Create EAB credentials for a user | User | | PUT | `/v1/acme/{mount}/config` | Set default issuer for ACME mount | Admin | | GET | `/v1/acme/{mount}/accounts` | List ACME accounts | Admin | | GET | `/v1/acme/{mount}/orders` | List ACME orders | Admin | ### Error Responses All API errors return JSON: ```json {"error": "description of what went wrong"} ``` HTTP status codes: - `401` — missing or invalid token - `403` — insufficient privileges - `412` — service not initialized - `503` — service is sealed ### gRPC API Metacrypt also exposes a gRPC API defined in `proto/metacrypt/`. Two API versions exist: #### v1 (legacy proto definitions) The v1 API uses a generic `Execute` RPC for all engine operations. The v1 proto definitions are retained for reference; the active server implementation uses v2. ```protobuf rpc Execute(ExecuteRequest) returns (ExecuteResponse); message ExecuteRequest { string mount = 1; string operation = 2; string path = 3; google.protobuf.Struct data = 4; // JSON-like map } message ExecuteResponse { google.protobuf.Struct data = 1; // JSON-like map } ``` Timestamps are represented as RFC3339 strings within the `Struct` payload. The `EngineService` also provides `Mount`, `Unmount`, and `ListMounts` RPCs for engine lifecycle management. #### v2 (implemented) The v2 API (`proto/metacrypt/v2/`) replaces the generic `Execute` RPC with strongly-typed, per-operation RPCs and uses `google.protobuf.Timestamp` for all time fields. The gRPC server is fully implemented against v2. Key changes from v1: - **`CAService`**: 14 typed RPCs — `ImportRoot`, `GetRoot`, `CreateIssuer`, `DeleteIssuer`, `ListIssuers`, `GetIssuer`, `GetChain`, `IssueCert`, `GetCert`, `ListCerts`, `RenewCert`, `SignCSR`, `RevokeCert`, `DeleteCert`. - **`EngineService`**: Retains `Mount`, `Unmount`, `ListMounts`; drops the generic `Execute` RPC. `MountRequest.config` is `map` instead of `google.protobuf.Struct`. - **Timestamps**: All `issued_at` / `expires_at` fields use `google.protobuf.Timestamp` instead of RFC3339 strings. - **Message types**: `CertRecord` (full certificate data) and `CertSummary` (lightweight, for list responses) replace the generic struct maps. - **`BarrierService`**: `ListKeys`, `RotateMEK`, `RotateKey`, `Migrate` — admin-only key management RPCs for MEK/DEK rotation and v1→v2 migration. - **`ACMEService`**: `CreateEAB`, `SetConfig`, `ListAccounts`, `ListOrders`. - **`AuthService`**: String timestamps replaced by `google.protobuf.Timestamp`. The v2 proto definitions pass `buf lint` with no warnings. Generated Go code lives in `gen/metacrypt/v2/`. #### gRPC Interceptors (v2) The gRPC server (`internal/grpcserver/`) uses three interceptor maps to gate access: | Interceptor map | Effect | |-----------------------|-----------------------------------------------------| | `sealRequiredMethods` | Returns `UNAVAILABLE` if the barrier is sealed | | `authRequiredMethods` | Validates MCIAS bearer token; populates caller info | | `adminRequiredMethods`| Requires `IsAdmin == true` on the caller | All CA write operations, engine lifecycle RPCs, policy mutations, and ACME management RPCs are gated on unseal state, authentication, and (where appropriate) admin privilege. --- ## Web Interface Metacrypt includes an HTMX-powered web UI for basic operations: | Route | Purpose | |-------------------------------|-------------------------------------------------------| | `/` | Redirects based on service state | | `/init` | Password setup form (first-time only) | | `/unseal` | Password entry to unseal | | `/login` | MCIAS login form (username, password, TOTP) | | `/dashboard` | Engine mounts, service state, admin controls | | `/dashboard/mount-ca` | Mount a new CA engine (POST, admin) | | `/pki` | PKI overview: list issuers, download CA/issuer PEMs | | `/pki/import-root` | Import an existing root CA (POST) | | `/pki/create-issuer` | Create a new intermediate issuer (POST) | | `/pki/issue` | Issue a leaf certificate (POST) | | `/pki/download/{token}` | Download issued cert bundle as .tar.gz | | `/pki/issuer/{name}` | Issuer detail: certificates issued by that issuer | | `/pki/cert/{serial}` | Certificate detail page | | `/pki/cert/{serial}/download` | Download certificate files | | `/pki/cert/{serial}/revoke` | Revoke a certificate (POST) | | `/pki/cert/{serial}/delete` | Delete a certificate record (POST) | | `/pki/{issuer}` | Issuer detail (alternate path) | The dashboard shows mounted engines, the service state, and (for admins) a seal button. Templates use Go's `html/template` with a shared layout. HTMX provides form submission without full page reloads. The PKI pages communicate with the backend via the internal gRPC client (`internal/webserver/client.go`), which uses the v2 typed gRPC stubs (`CAService`, `EngineService`, `SystemService`, etc.). The issuer detail page supports filtering certificates by common name (case-insensitive substring match) and sorting by common name (default) or expiry date. --- ## Database Schema SQLite with WAL mode, foreign keys enabled, 5-second busy timeout. ### Tables **`seal_config`** — Single row storing the encrypted master key material. | Column | Type | Description | |-----------------|----------|--------------------------------------| | id | INTEGER | Always 1 (enforced primary key) | | encrypted_mek | BLOB | MEK encrypted with KWK | | kdf_salt | BLOB | 32-byte Argon2id salt | | argon2_time | INTEGER | Argon2id time cost | | argon2_memory | INTEGER | Argon2id memory cost (KiB) | | argon2_threads | INTEGER | Argon2id parallelism | | initialized_at | DATETIME | Timestamp of initialization | **`barrier_entries`** — Encrypted key-value store. | Column | Type | Description | |-------------|----------|-----------------------------------------| | path | TEXT | Primary key; hierarchical path | | value | BLOB | AES-256-GCM encrypted value | | created_at | DATETIME | Row creation time | | updated_at | DATETIME | Last modification time | **`schema_migrations`** — Tracks applied schema versions. | Column | Type | Description | |-------------|----------|--------------------------| | version | INTEGER | Migration version number | | applied_at | DATETIME | When applied | Migrations are idempotent and run sequentially at startup. --- ## Configuration TOML configuration with environment variable overrides (`METACRYPT_*`). ```toml [server] listen_addr = ":8443" # required grpc_addr = ":8444" # optional; gRPC server disabled if unset tls_cert = "/path/cert.pem" # required tls_key = "/path/key.pem" # required [web] listen_addr = "127.0.0.1:8080" # optional; web UI server address vault_grpc = "127.0.0.1:9443" # gRPC address of the vault server vault_ca_cert = "/path/ca.pem" # optional; CA cert to verify vault TLS [database] path = "/path/metacrypt.db" # required [mcias] server_url = "https://mcias.metacircular.net:8443" # required ca_cert = "/path/ca.pem" # optional, for custom CA [seal] argon2_time = 3 # default: 3 argon2_memory = 131072 # default: 128 MiB (in KiB) argon2_threads = 4 # default: 4 [log] level = "info" # default: "info" ``` Required fields are validated at startup; the server refuses to start if any are missing. --- ## Deployment ### Docker Multi-stage build: 1. **Builder stage**: Go compilation with symbols stripped (`-s -w`) 2. **Runtime stage**: Alpine 3.21, non-root `metacrypt` user ``` VOLUME /data # config, certs, database EXPOSE 8443 ENTRYPOINT ["metacrypt", "server", "--config", "/data/metacrypt.toml"] ``` ### TLS Configuration - Minimum TLS version: 1.3 (cipher suites managed by Go's TLS 1.3 implementation) - Timeouts: read 30s, write 30s, idle 120s ### CLI Commands | Command | Purpose | |------------|-------------------------------------------| | `server` | Start the HTTPS server | | `init` | Interactive first-time seal setup | | `status` | Query a running server's state | | `snapshot` | Create a consistent database backup (VACUUM INTO) | ### Graceful Shutdown The server handles `SIGINT` and `SIGTERM` signals, sealing all engines and closing connections before exit. --- ## Security Model ### Threat Mitigations | Threat | Mitigation | |-----------------------------|-------------------------------------------------------------| | Database theft | All barrier values encrypted with MEK (AES-256-GCM) | | Brute-force unseal | Argon2id (128 MiB memory-hard), rate limiting (5/min + lockout) | | MEK exposure | Held in memory only when unsealed; zeroized on seal | | Token theft | 30-second cache TTL; MCIAS-backed validation | | Privilege escalation | Default-deny policy; admin bypass only for MCIAS admin role | | Nonce reuse | Fresh random nonce per encryption operation | | Timing attacks | Constant-time comparison for passwords and tokens | | Unauthorized access at rest | Database file permissions 0600; non-root container user | | TLS downgrade | Minimum TLS 1.3 | | CA key compromise | CA/issuer keys encrypted in barrier; zeroized on seal; two-tier PKI limits blast radius | | Leaf key leakage via storage | Issued cert private keys never persisted; only returned to requester | ### Security Invariants 1. The MEK never leaves process memory and is never logged or serialized in plaintext. 2. The seal password is never stored; only its Argon2id-derived output is used transiently. 3. All barrier writes produce fresh ciphertexts (random nonce per encryption). 4. The service is fail-closed: a sealed barrier rejects all operations. 5. Admin privileges are determined solely by MCIAS role membership; Metacrypt has no local user database. 6. Issued certificate private keys are returned to the caller but **never stored** in the barrier. Only cert metadata is persisted. 7. CA and issuer private keys are encrypted at rest in the barrier and zeroized from memory on seal (explicit overwrite of ECDSA `D`, RSA `D` and primes, Ed25519 key bytes). --- ## Future Work ### Remaining Engine Implementations - **SSH CA Engine** — Sign SSH host and user certificates - **Transit Engine** — Encrypt/decrypt payloads on behalf of applications (envelope encryption); key rotation - **User Engine** — Key exchange and encryption between Metacircular users ### CA Engine Enhancements - **CRL management** — Certificate revocation lists - **OCSP responder** — Online certificate status checking - **Certificate templates** — Admin-defined custom profiles beyond server/client/peer ### Planned Capabilities - **Post-quantum readiness** — Hybrid key exchange (ML-KEM + ECDH); the versioned ciphertext format and engine interface are designed for algorithm agility - **Key rotation** — MEK and per-engine DEK rotation without re-sealing - **Audit logging** — Tamper-evident log of all cryptographic operations - **Engine persistence** — Auto-remounting engines from barrier state on unseal ### Public Key Algorithms The CA engine supports ECDSA (P-256, P-384, P-521), RSA, and Ed25519 for certificate key pairs, configurable per-CA and overridable per-issuer or per-issuance. The system is designed for algorithm agility to support future post-quantum algorithms.