Key Management
The Chamber Key Management API is the operator-facing lifecycle surface over runtime and DynamoDB keys. Five HTTP verbs under /api/v1/chamber/keys/*, each independently gated:
| Method | Path | Operation | Scope | Enable flag |
|---|---|---|---|---|
GET | /api/v1/chamber/keys | List | chamber_keys_list | Always mounted when key_sources.dynamodb is configured |
POST | /api/v1/chamber/keys/generate | Generate | chamber_keys_generate | chamber.keys.generate.enabled |
POST | /api/v1/chamber/keys | Import | chamber_keys_import | chamber.keys.import.enabled |
PATCH | /api/v1/chamber/keys | Activate / deactivate | chamber_keys_patch | chamber.keys.lifecycle.enabled |
DELETE | /api/v1/chamber/keys | Deactivate or remove | chamber_keys_delete | chamber.keys.lifecycle.enabled |
All routes require an authenticated client token with the listed scope. All can return 503 Service Unavailable when the signer is sealed because the handler calls require_unsealed_signer before key work starts.
Configuration
Section titled “Configuration”chamber: keys: list: max_concurrent_requests: 16 generate: enabled: true max_items_per_request: 100 max_concurrent_requests: 16 request_timeout_seconds: 600 backup: enabled: false # recipients: ["age1..."] import: enabled: true max_items_per_request: 100 max_concurrent_requests: 16 request_timeout_seconds: 600 lifecycle: enabled: true max_items_per_request: 500 max_concurrent_requests: 16 request_timeout_seconds: 600Per-verb fields
Section titled “Per-verb fields”The generated Configuration Reference is the source of truth for defaults.
| Field | Type | Default | Notes |
|---|---|---|---|
list.max_concurrent_requests | NonZero usize | 16 | Tower concurrency cap. List is always mounted when key_sources.dynamodb is configured |
generate.enabled | bool | false | Boot validation refuses true without key_sources.dynamodb |
generate.max_items_per_request | NonZero usize | 100 | Maximum count per request |
generate.max_concurrent_requests | NonZero usize | 16 | Tower request-level cap. Per-request batch parallelism is std::thread::available_parallelism() and not operator-configurable |
generate.request_timeout_seconds | NonZero u64 | 600 | Per-request timeout |
generate.backup.enabled | bool | false | When true, requires non-empty recipients (boot-validated) |
generate.backup.recipients | Vec<String> | [] | age public keys (age1...) for the BIP-39 mnemonic backup |
import.enabled | bool | false | Memory-only imports work without a mutable backend; persistent imports require key_sources.dynamodb |
import.max_items_per_request | NonZero usize | 100 | Hard cap on combined keystores + raw_keys length |
import.max_concurrent_requests | NonZero usize | 16 | Tower request-level cap |
import.request_timeout_seconds | NonZero u64 | 600 | Per-request timeout (long because scrypt + BLS per key) |
lifecycle.enabled | bool | false | Memory-key deletes work without a mutable backend; PATCH on memory keys returns a per-key error |
lifecycle.max_items_per_request | NonZero usize | 500 | Higher than import because PATCH/DELETE is one UpdateItem per key with no crypto |
lifecycle.max_concurrent_requests | NonZero usize | 16 | Tower request-level cap |
lifecycle.request_timeout_seconds | NonZero u64 | 600 | Per-request timeout |
Operations
Section titled “Operations”The key-management routes are documented interactively in the API Reference. This page focuses on behavior that matters operationally.
Generate
Section titled “Generate”Derives BLS validator keys via BIP-39 mnemonic → EIP-2333 → bls::Keypair, builds and self-verifies the DepositData, optionally encrypts the mnemonic with age to one or more recipients, and persists each key as KeyStatus::Inactive.
Generated keys start inactive so that:
- Operators submit deposit data to the Ethereum deposit contract.
- The validator appears on the beacon chain after the activation queue.
- Only then does the operator activate signing.
Signing against an inactive key fails at the GVR/key-resolution boundary — the key is in DynamoDB but is not loaded into the live signer cache.
Mnemonic Backup
Section titled “Mnemonic Backup”When chamber.keys.generate.backup.enabled = true, the BIP-39 mnemonic for each generated key is encrypted with each recipient’s age public key and stored alongside the key in DynamoDB. To recover: decrypt with the corresponding age private key (kept offline), re-derive via EIP-2333, re-import. This is independent of KMS — your last-resort recovery path if the master key becomes unreconstructable.
Generation is all-or-nothing for derivation and deposit-data construction. Persistence failures are reported per key in the response.
Import
Section titled “Import”Accepts EIP-2335 keystores and raw hex BLS keys. Decrypts in parallel under a CPU semaphore, then either writes through to DynamoDB or inserts only into the in-memory DashMap.
Import supports two storage modes:
storage.persist | KeySource | Persists across restart? | Requires key_sources.dynamodb |
|---|---|---|---|
true (default) | DynamoDB | yes | yes |
false | Memory | no | no (works even on a filesystem-only signer) |
Memory mode is also the path the Key Manager API takes — different surface, identical outcome for KeySource::Memory keys.
Returns metadata for every loaded key. Always mounted when key_sources.dynamodb is configured.
The signer’s in-memory cache only contains active keys. Inactive keys are retrieved directly from DynamoDB when requested, so operators can audit generated-but-not-yet-activated keys without making them signable.
Patch applies per-key status transitions:
active→inactive— removes the key from the in-memory signing cache. The DynamoDB row stays; the encrypted blob is untouched. Subsequent re-activation round-trips through the same decryption pathinactive→active— decrypts the DynamoDB row with the master key and inserts the keypair into the signing cache- PATCH on a
KeySource::Memorykey returns a per-key error (memory keys have no DynamoDB row to flip) - PATCH on a missing pubkey returns
not_foundper-key, not 404 overall
Delete
Section titled “Delete”For each pubkey:
KeySource::Dynamodb→ marks the DynamoDB rowinactiveand removes the key from the in-memory signing cache. The encrypted row remains for later reactivation or audit.KeySource::Memory→ removes from the in-memoryDashMaponly (no DynamoDB row exists)KeySource::Filesystem→ returnsnot_activeper-key (filesystem keys are immutable from HTTP; remove the file and restart)
Auth and route layering
Section titled “Auth and route layering”Every chamber.keys.* route is gated by:
- The configured auth scope (see Access Control)
require_unsealed_signer— returns503if the signer is in any state other thanUnsealedorAwaitingRotation- Tower middleware stack:
LoadShedLayer→ConcurrencyLimitLayer(max_concurrent_requests)→TimeoutLayer(request_timeout_seconds)
The 503-sealed response is documented on every chamber.keys endpoint in the OpenAPI spec.
See also
Section titled “See also”- DynamoDB key source — the storage layer this API operates on
- Seal & Unseal — the master-key reconstruction the routes depend on
- Access Control — policies, scopes, tokens
- API Reference — interactive OpenAPI spec