Implement CA/PKI engine with two-tier X.509 certificate issuance

Add the first concrete engine implementation: a CA (PKI) engine that generates
a self-signed root CA at mount time, issues scoped intermediate CAs ("issuers"),
and signs leaf certificates using configurable profiles (server, client, peer).

Engine framework updates:
- Add CallerInfo struct for auth context in engine requests
- Add config parameter to Engine.Initialize for mount-time configuration
- Export Mount.Engine field; add GetEngine/GetMount on Registry

CA engine (internal/engine/ca/):
- Two-tier PKI: root CA → issuers → leaf certificates
- 10 operations: get-root, get-chain, get-issuer, create/delete/list issuers,
  issue, get-cert, list-certs, renew
- Certificate profiles with user-overridable TTL, key usages, and key algorithm
- Private keys never stored in barrier; zeroized from memory on seal
- Supports ECDSA, RSA, and Ed25519 key types via goutils/certlib/certgen

Server routes:
- Wire up engine mount/request handlers (replace Phase 1 stubs)
- Add public PKI routes (/v1/pki/{mount}/ca, /ca/chain, /issuer/{name})
  for unauthenticated TLS trust bootstrapping

Also includes: ARCHITECTURE.md, deploy config updates, operational tooling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 21:57:52 -07:00
parent 4ddd32b117
commit 8f77050a84
26 changed files with 2980 additions and 129 deletions

699
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,699 @@
# 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
server/ HTTP server, routes, middleware
web/
templates/ Go HTML templates (layout, init, unseal, login, dashboard)
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 |
| Zeroization | Explicit overwrite | MEK, KWK, 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 Entries │ Each entry encrypted individually
│ ├── Policy rules │ with MEK via AES-256-GCM
│ ├── Engine configs │
│ └── Engine DEKs │ Per-engine data encryption keys
└──────────────────────┘
```
### Ciphertext Format
All encrypted values use a versioned binary format:
```
[version: 1 byte][nonce: 12 bytes][ciphertext + GCM tag]
```
The version byte (currently `0x01`) enables future algorithm migration,
including post-quantum hybrid schemes.
---
## 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)
│ ┌──────────────────┐
└─────────────►│ Sealed │ MEK 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 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
- **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 <token>` 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 // e.g. "read", "write", "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.
---
## 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/kyle/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` | 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 | 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
### 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 |
### 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 |
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?issuer=` | 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.
### Policy (Admin Only)
| Method | Path | Description |
|--------|-----------------------|---------------------|
| GET | `/v1/policy/rules` | List all rules |
| POST | `/v1/policy/rules` | Create a rule |
| GET | `/v1/policy/rule?id=` | Get rule by ID |
| DELETE | `/v1/policy/rule?id=` | Delete rule by ID |
### 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
---
## 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 |
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.
---
## 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
tls_cert = "/path/cert.pem" # required
tls_key = "/path/key.pem" # required
[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.2
- Cipher suites: `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384`,
`TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`
- 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.2; only AEAD cipher suites |
| 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
- **gRPC API** — In addition to the JSON REST API (config field already reserved)
- **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.