Implement a two-level key hierarchy: the MEK now wraps per-engine DEKs stored in a new barrier_keys table, rather than encrypting all barrier entries directly. A v2 ciphertext format (0x02) embeds the key ID so the barrier can resolve which DEK to use on decryption. v1 ciphertext remains supported for backward compatibility. Key changes: - crypto: EncryptV2/DecryptV2/ExtractKeyID for v2 ciphertext with key IDs - barrier: key registry (CreateKey, RotateKey, ListKeys, MigrateToV2, ReWrapKeys) - seal: RotateMEK re-wraps DEKs without re-encrypting data - engine: Mount auto-creates per-engine DEK - REST + gRPC: barrier/keys, barrier/rotate-mek, barrier/rotate-key, barrier/migrate - proto: BarrierService (v1 + v2) with ListKeys, RotateMEK, RotateKey, Migrate - db: migration v2 adds barrier_keys table Also includes: security audit report, CSRF protection, engine design specs (sshca, transit, user), path-bound AAD migration tool, policy engine enhancements, and ARCHITECTURE.md updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
114
ARCHITECTURE.md
114
ARCHITECTURE.md
@@ -100,7 +100,8 @@ deploy/ Docker Compose, example configs
|
|||||||
| Salt size | 256 bits | Argon2id salt |
|
| Salt size | 256 bits | Argon2id salt |
|
||||||
| CSPRNG | `crypto/rand` | Keys, salts, nonces |
|
| CSPRNG | `crypto/rand` | Keys, salts, nonces |
|
||||||
| Constant-time comparison | `crypto/subtle` | Password & token comparison |
|
| Constant-time comparison | `crypto/subtle` | Password & token comparison |
|
||||||
| Zeroization | Explicit overwrite | MEK, KWK, passwords in memory |
|
| DEK wrapping | AES-256-GCM | MEK wraps per-engine DEKs |
|
||||||
|
| Zeroization | Explicit overwrite | MEK, KWK, DEKs, passwords in memory |
|
||||||
|
|
||||||
### Key Hierarchy
|
### Key Hierarchy
|
||||||
|
|
||||||
@@ -123,24 +124,76 @@ User Password (not stored)
|
|||||||
Master Encryption Key (MEK) 256-bit, held in memory only when unsealed
|
Master Encryption Key (MEK) 256-bit, held in memory only when unsealed
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌──────────────────────┐
|
┌──────────────────────────┐
|
||||||
│ Barrier Entries │ Each entry encrypted individually
|
│ barrier_keys table │ MEK wraps per-engine DEKs
|
||||||
│ ├── Policy rules │ with MEK via AES-256-GCM
|
│ ├── "system" DEK │ policy rules, mount metadata
|
||||||
│ ├── Engine configs │
|
│ ├── "engine/ca/prod" DEK │ per-engine data encryption
|
||||||
│ └── Engine DEKs │ Per-engine data encryption keys
|
│ ├── "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
|
### Ciphertext Format
|
||||||
|
|
||||||
All encrypted values use a versioned binary format:
|
Two versioned binary formats are supported:
|
||||||
|
|
||||||
|
**v1** (legacy, `0x01`):
|
||||||
```
|
```
|
||||||
[version: 1 byte][nonce: 12 bytes][ciphertext + GCM tag]
|
[version: 1 byte][nonce: 12 bytes][ciphertext + GCM tag]
|
||||||
```
|
```
|
||||||
|
|
||||||
The version byte (currently `0x01`) enables future algorithm migration,
|
**v2** (current, `0x02`):
|
||||||
including post-quantum hybrid schemes.
|
```
|
||||||
|
[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'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -167,8 +220,9 @@ Metacrypt operates as a state machine with four states:
|
|||||||
│ │ • derive KWK = Argon2id(password, salt)
|
│ │ • derive KWK = Argon2id(password, salt)
|
||||||
│ │ • MEK = Decrypt(KWK, encrypted_mek)
|
│ │ • MEK = Decrypt(KWK, encrypted_mek)
|
||||||
│ │ • barrier.Unseal(MEK)
|
│ │ • barrier.Unseal(MEK)
|
||||||
|
│ │ • load & decrypt DEKs from barrier_keys
|
||||||
│ ┌──────────────────┐
|
│ ┌──────────────────┐
|
||||||
└─────────────►│ Sealed │ MEK zeroized; barrier locked
|
└─────────────►│ Sealed │ MEK + DEKs zeroized; barrier locked
|
||||||
└──────────────────┘
|
└──────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -182,7 +236,7 @@ Unseal attempts are rate-limited to mitigate online brute-force:
|
|||||||
### Sealing
|
### Sealing
|
||||||
|
|
||||||
Calling `Seal()` immediately:
|
Calling `Seal()` immediately:
|
||||||
1. Zeroizes the MEK from memory
|
1. Zeroizes all DEKs and the MEK from memory
|
||||||
2. Seals the storage barrier (all reads/writes return `ErrSealed`)
|
2. Seals the storage barrier (all reads/writes return `ErrSealed`)
|
||||||
3. Seals all mounted engines
|
3. Seals all mounted engines
|
||||||
4. Flushes the authentication token cache
|
4. Flushes the authentication token cache
|
||||||
@@ -221,6 +275,9 @@ type Barrier interface {
|
|||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
- **Encryption at rest**: All values encrypted with MEK before database write
|
- **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
|
- **Fresh nonce per write**: Every `Put` generates a new random nonce
|
||||||
- **Atomic upsert**: Uses `INSERT ... ON CONFLICT UPDATE` for Put
|
- **Atomic upsert**: Uses `INSERT ... ON CONFLICT UPDATE` for Put
|
||||||
- **Glob listing**: `List(prefix)` returns relative paths matching the prefix
|
- **Glob listing**: `List(prefix)` returns relative paths matching the prefix
|
||||||
@@ -262,7 +319,7 @@ type Rule struct {
|
|||||||
Usernames []string // match specific users (optional)
|
Usernames []string // match specific users (optional)
|
||||||
Roles []string // match roles (optional)
|
Roles []string // match roles (optional)
|
||||||
Resources []string // glob patterns, e.g. "engine/transit/*" (optional)
|
Resources []string // glob patterns, e.g. "engine/transit/*" (optional)
|
||||||
Actions []string // e.g. "read", "write", "admin" (optional)
|
Actions []string // "any", "read", "write", "encrypt", "decrypt", "sign", "verify", "hmac", "admin" (optional)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -275,7 +332,10 @@ type Rule struct {
|
|||||||
5. **Default deny** if no rules match
|
5. **Default deny** if no rules match
|
||||||
|
|
||||||
Matching is case-insensitive for usernames and roles. Resources use glob
|
Matching is case-insensitive for usernames and roles. Resources use glob
|
||||||
patterns. Empty fields in a rule match everything.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -457,6 +517,15 @@ kept in sync — every operation available via REST has a corresponding gRPC RPC
|
|||||||
|--------|-------------|-------------------------|
|
|--------|-------------|-------------------------|
|
||||||
| POST | `/v1/seal` | Seal service & engines |
|
| 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
|
### Authentication
|
||||||
|
|
||||||
| Method | Path | Description | Auth Required |
|
| Method | Path | Description | Auth Required |
|
||||||
@@ -509,15 +578,6 @@ must be of type `ca`; returns 404 otherwise.
|
|||||||
| POST | `/v1/ca/{mount}/cert/{serial}/revoke` | Revoke a certificate | Admin |
|
| POST | `/v1/ca/{mount}/cert/{serial}/revoke` | Revoke a certificate | Admin |
|
||||||
| DELETE | `/v1/ca/{mount}/cert/{serial}` | Delete a certificate record | Admin |
|
| DELETE | `/v1/ca/{mount}/cert/{serial}` | Delete a certificate record | Admin |
|
||||||
|
|
||||||
### Policy (Authenticated)
|
|
||||||
|
|
||||||
| Method | Path | Description | Auth |
|
|
||||||
|--------|-----------------------|---------------------|-------|
|
|
||||||
| GET | `/v1/policy/rules` | List all rules | User |
|
|
||||||
| POST | `/v1/policy/rules` | Create a rule | User |
|
|
||||||
| GET | `/v1/policy/rule?id=` | Get rule by ID | User |
|
|
||||||
| DELETE | `/v1/policy/rule?id=` | Delete rule by ID | User |
|
|
||||||
|
|
||||||
### ACME (RFC 8555)
|
### ACME (RFC 8555)
|
||||||
|
|
||||||
ACME protocol endpoints are mounted per CA engine instance and require no
|
ACME protocol endpoints are mounted per CA engine instance and require no
|
||||||
@@ -606,6 +666,8 @@ from v1:
|
|||||||
`google.protobuf.Timestamp` instead of RFC3339 strings.
|
`google.protobuf.Timestamp` instead of RFC3339 strings.
|
||||||
- **Message types**: `CertRecord` (full certificate data) and `CertSummary`
|
- **Message types**: `CertRecord` (full certificate data) and `CertSummary`
|
||||||
(lightweight, for list responses) replace the generic struct maps.
|
(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`.
|
- **`ACMEService`**: `CreateEAB`, `SetConfig`, `ListAccounts`, `ListOrders`.
|
||||||
- **`AuthService`**: String timestamps replaced by `google.protobuf.Timestamp`.
|
- **`AuthService`**: String timestamps replaced by `google.protobuf.Timestamp`.
|
||||||
|
|
||||||
@@ -758,9 +820,7 @@ ENTRYPOINT ["metacrypt", "server", "--config", "/data/metacrypt.toml"]
|
|||||||
|
|
||||||
### TLS Configuration
|
### TLS Configuration
|
||||||
|
|
||||||
- Minimum TLS version: 1.2
|
- Minimum TLS version: 1.3 (cipher suites managed by Go's TLS 1.3 implementation)
|
||||||
- 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
|
- Timeouts: read 30s, write 30s, idle 120s
|
||||||
|
|
||||||
### CLI Commands
|
### CLI Commands
|
||||||
@@ -793,7 +853,7 @@ closing connections before exit.
|
|||||||
| Nonce reuse | Fresh random nonce per encryption operation |
|
| Nonce reuse | Fresh random nonce per encryption operation |
|
||||||
| Timing attacks | Constant-time comparison for passwords and tokens |
|
| Timing attacks | Constant-time comparison for passwords and tokens |
|
||||||
| Unauthorized access at rest | Database file permissions 0600; non-root container user |
|
| Unauthorized access at rest | Database file permissions 0600; non-root container user |
|
||||||
| TLS downgrade | Minimum TLS 1.2; only AEAD cipher suites |
|
| TLS downgrade | Minimum TLS 1.3 |
|
||||||
| CA key compromise | CA/issuer keys encrypted in barrier; zeroized on seal; two-tier PKI limits blast radius |
|
| 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 |
|
| Leaf key leakage via storage | Issued cert private keys never persisted; only returned to requester |
|
||||||
|
|
||||||
|
|||||||
169
AUDIT.md
Normal file
169
AUDIT.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Security Audit Report
|
||||||
|
|
||||||
|
**Date**: 2026-03-16
|
||||||
|
**Scope**: ARCHITECTURE.md, engines/sshca.md, engines/transit.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ARCHITECTURE.md
|
||||||
|
|
||||||
|
### Strengths
|
||||||
|
|
||||||
|
- Solid key hierarchy: password → Argon2id → KWK → MEK → per-entry encryption. Defense-in-depth.
|
||||||
|
- Fail-closed design with `ErrSealed` on all operations when sealed.
|
||||||
|
- Fresh nonce per write, constant-time comparisons, explicit zeroization — all correct fundamentals.
|
||||||
|
- Default-deny policy engine with priority-based rule evaluation.
|
||||||
|
- Issued leaf private keys never stored — good principle of least persistence.
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
**1. ~~TLS minimum version should be 1.3, not 1.2~~ RESOLVED**
|
||||||
|
|
||||||
|
Updated all TLS configurations (HTTP server, gRPC server, web server, vault client, Go client library, CLI commands) from `tls.VersionTLS12` to `tls.VersionTLS13`. Removed explicit cipher suite list from HTTP server (TLS 1.3 manages its own). Updated ARCHITECTURE.md TLS section and threat mitigations table.
|
||||||
|
|
||||||
|
**2. ~~Token cache TTL of 30 seconds is a revocation gap~~ ACCEPTED**
|
||||||
|
|
||||||
|
Accepted as an explicit trade-off. The 30-second cache TTL balances MCIAS load against revocation latency. For this system's scale and threat model, the window is acceptable.
|
||||||
|
|
||||||
|
**3. ~~Admin bypass in policy engine is an all-or-nothing model~~ ACCEPTED**
|
||||||
|
|
||||||
|
The all-or-nothing admin model is intentional by design. MCIAS admin users get full access to all engines and operations. This is the desired behavior for this system.
|
||||||
|
|
||||||
|
**4. ~~Policy rule creation is listed as both Admin-only and User-accessible~~ RESOLVED**
|
||||||
|
|
||||||
|
The second policy table in ARCHITECTURE.md incorrectly listed User auth; removed the duplicate. gRPC `adminRequiredMethods` now includes `ListPolicies` and `GetPolicy` to match REST behavior. All policy CRUD is admin-only across both API surfaces.
|
||||||
|
|
||||||
|
**5. ~~No integrity protection on barrier entry paths~~ RESOLVED**
|
||||||
|
|
||||||
|
Updated `crypto.Encrypt`/`crypto.Decrypt` to accept an `additionalData` parameter. The barrier now passes the entry path as GCM AAD on both `Put` and `Get`, binding each ciphertext to its storage path. Seal operations pass `nil` (no path context). Added `TestEncryptDecryptWithAAD` covering correct-AAD, wrong-AAD, and nil-AAD cases. Existing barrier entries will fail to decrypt after this change — a one-off migration tool is needed to re-encrypt all entries (decrypt with nil AAD under old code, re-encrypt with path AAD).
|
||||||
|
|
||||||
|
**6. ~~Single MEK with no rotation mechanism~~ RESOLVED**
|
||||||
|
|
||||||
|
Implemented MEK rotation and per-engine DEKs. The v2 ciphertext format (`0x02`) embeds a key ID that identifies which DEK encrypted each entry. MEK rotation (`POST /v1/barrier/rotate-mek`) re-wraps all DEKs without re-encrypting data. DEK rotation (`POST /v1/barrier/rotate-key`) re-encrypts entries under a specific key. A migration endpoint converts v1 entries to v2 format. The `barrier_keys` table stores MEK-wrapped DEKs with version tracking.
|
||||||
|
|
||||||
|
**7. No audit logging**
|
||||||
|
|
||||||
|
Acknowledged as future work, but for a cryptographic service this is a significant gap. Every certificate issuance, every sign operation, every policy change should be logged with caller identity, timestamp, and operation details. Without this, incident response is blind.
|
||||||
|
|
||||||
|
**8. ~~Rate limiting is in-memory only~~ ACCEPTED**
|
||||||
|
|
||||||
|
The in-memory rate limit protects against remote brute-force over the network, which is the realistic threat. Persisting the counter in the database would not add tamper resistance: the barrier is sealed during unseal attempts so encrypted storage is unavailable, and the unencrypted database could be reset by an attacker with disk access. An attacker who can restart the service already has local system access, making the rate limit moot regardless of persistence. Argon2id cost parameters (128 MiB memory-hard) are the primary brute-force mitigation and are stored in `seal_config`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## engines/sshca.md
|
||||||
|
|
||||||
|
### Strengths
|
||||||
|
|
||||||
|
- Flat CA model is correct for SSH (no intermediate hierarchy needed).
|
||||||
|
- Default principal restriction (users can only sign certs for their own username) is the right default.
|
||||||
|
- `max_ttl` enforced server-side — good.
|
||||||
|
- Key zeroization on seal, no private keys in cert records.
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
**9. ~~User-controllable serial numbers~~ RESOLVED**
|
||||||
|
|
||||||
|
Removed the optional `serial` field from both `sign-host` and `sign-user` request data. Serials are always generated server-side using `crypto/rand` (64-bit). Updated flows and security considerations in sshca.md.
|
||||||
|
|
||||||
|
**10. No explicit extension allowlist for host certificates**
|
||||||
|
|
||||||
|
The `extensions` field for `sign-host` accepts an arbitrary map. SSH extensions have security implications (e.g., `permit-pty`, `permit-port-forwarding`, `permit-user-rc`). Without an allowlist, a user could request extensions that grant more capabilities than intended. The engine should define a default extension set and either:
|
||||||
|
- Restrict to an allowlist, or
|
||||||
|
- Require admin for non-default extensions.
|
||||||
|
|
||||||
|
**11. ~~`critical_options` on user certs is a privilege escalation surface~~ RESOLVED**
|
||||||
|
|
||||||
|
Removed `critical_options` from the `sign-user` request. Critical options can only be applied via admin-defined signing profiles, which are policy-gated (`sshca/{mount}/profile/{name}`, action `read`). Profile CRUD is admin-only. Profiles specify critical options, extensions, optional max TTL, and optional principal restrictions. Security considerations updated accordingly.
|
||||||
|
|
||||||
|
**12. ~~No KRL (Key Revocation List) support~~ RESOLVED**
|
||||||
|
|
||||||
|
Added a full KRL section to sshca.md covering: in-memory KRL generation from revoked serials, barrier persistence at `engine/sshca/{mount}/krl.bin`, automatic rebuild on revoke/delete/unseal, a public `GET /v1/sshca/{mount}/krl` endpoint with ETag and Cache-Control headers, `GetKRL` gRPC RPC, and a pull-based distribution model with example sshd_config and cron fetch.
|
||||||
|
|
||||||
|
**13. ~~Policy resource path uses `ca/` prefix instead of `sshca/`~~ RESOLVED**
|
||||||
|
|
||||||
|
Updated policy check paths in sshca.md from `ca/{mount}/id/...` to `sshca/{mount}/id/...` for both `sign-host` and `sign-user` flows, eliminating the namespace collision with the CA (PKI) engine.
|
||||||
|
|
||||||
|
**14. No source-address restriction by default**
|
||||||
|
|
||||||
|
User certificates should ideally include `source-address` critical options to limit where they can be used from. At minimum, consider a mount-level configuration for default critical options that get applied to all user certs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## engines/transit.md
|
||||||
|
|
||||||
|
### Strengths
|
||||||
|
|
||||||
|
- Ciphertext format with version prefix enables clean key rotation.
|
||||||
|
- `exportable` and `allow_deletion` immutable after creation — prevents policy weakening.
|
||||||
|
- AAD/context binding for AEAD ciphers.
|
||||||
|
- Rewrap never exposes plaintext to caller.
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
**15. ~~No minimum key version enforcement~~ RESOLVED**
|
||||||
|
|
||||||
|
Added `min_decryption_version` per key (default 1). Decryption requests for versions below the minimum are rejected. New `update-key-config` operation (admin-only) advances the minimum (can only increase, cannot exceed current version). New `trim-key` operation permanently deletes versions older than the minimum. Both have corresponding gRPC RPCs and REST endpoints. The rotation cycle is documented: rotate → rewrap → advance min → trim.
|
||||||
|
|
||||||
|
**16. Key version pruning with `max_key_versions` has no safety check**
|
||||||
|
|
||||||
|
If `max_key_versions` is set and data encrypted with an old version hasn't been re-wrapped, pruning that version makes the data permanently unrecoverable. There should be either:
|
||||||
|
- A warning/confirmation mechanism, or
|
||||||
|
- A way to scan for ciphertext referencing a version before pruning, or
|
||||||
|
- At minimum, clear documentation that pruning is destructive.
|
||||||
|
|
||||||
|
**17. ~~RSA encryption without specifying padding scheme~~ RESOLVED**
|
||||||
|
|
||||||
|
RSA key types (`rsa-2048`, `rsa-4096`) removed entirely from the transit engine. Asymmetric encryption belongs in the user engine (via ECDH); RSA signing offers no advantage over Ed25519/ECDSA. `crypto/rsa` removed from dependencies. Rationale documented in key types section and security considerations.
|
||||||
|
|
||||||
|
**18. ~~HMAC keys used for `sign` operation is confusing~~ RESOLVED**
|
||||||
|
|
||||||
|
`sign` and `verify` are now restricted to asymmetric key types (Ed25519, ECDSA). HMAC keys are rejected with an error — HMAC must use the dedicated `hmac` operation. Policy actions are already split: `sign`, `verify`, and `hmac` are separate granular actions, all matched by `any`.
|
||||||
|
|
||||||
|
**19. ~~No batch encrypt/decrypt operations~~ RESOLVED**
|
||||||
|
|
||||||
|
Added `batch-encrypt`, `batch-decrypt`, and `batch-rewrap` operations to the transit engine plan. Each targets a single named key with an array of items; results are returned in order with per-item errors (partial success model). An optional `reference` field lets callers correlate results with source records. Policy is checked once per batch. Added corresponding gRPC RPCs and REST endpoints. `operationAction` maps batch variants to the same granular actions as their single counterparts.
|
||||||
|
|
||||||
|
**20. ~~`read` action maps to `decrypt` and `verify` — semantics are misleading~~ RESOLVED**
|
||||||
|
|
||||||
|
Replaced the coarse `read`/`write` action model with granular per-operation actions: `encrypt`, `decrypt`, `sign`, `verify`, `hmac` for cryptographic operations; `read` for metadata retrieval; `write` for key management; `admin` for administrative operations. Added `any` action that matches all non-admin actions. Added `LintRule` validation that rejects unknown effects and actions. `CreateRule` now validates before storing. Updated `operationAction` mapping and all tests.
|
||||||
|
|
||||||
|
**21. No rate limiting or quota on cryptographic operations**
|
||||||
|
|
||||||
|
A compromised or malicious user token could issue unlimited encrypt/decrypt/sign requests, potentially using the service as a cryptographic oracle. Consider per-user rate limits on transit operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Cutting Issues
|
||||||
|
|
||||||
|
**22. ~~No forward secrecy for stored data~~ RESOLVED**: Per-engine DEKs limit blast radius — compromise of one DEK only exposes that engine's data, not the entire barrier. MEK compromise still exposes all DEKs, but MEK rotation enables periodic re-keying. Each engine mount gets its own DEK created automatically; a `"system"` DEK protects non-engine data. v2 ciphertext format embeds key IDs for DEK lookup.
|
||||||
|
|
||||||
|
**23. ~~Generic `POST /v1/engine/request` bypasses typed route middleware~~ RESOLVED**: Added an `adminOnlyOperations` map to `handleEngineRequest` that mirrors the admin gates on typed REST routes (e.g. `create-issuer`, `delete-cert`, `create-key`, `rotate-key`, `create-profile`, `provision`). Non-admin users are rejected with 403 before policy evaluation or engine dispatch. The v1 gRPC `Execute` RPC is defined in the proto but not registered in the server — only v2 typed RPCs are used, so the gRPC surface is not affected. Tests cover both admin and non-admin paths through the generic endpoint.
|
||||||
|
|
||||||
|
**24. ~~No CSRF protection mentioned for web UI~~ RESOLVED**: Added signed double-submit cookie CSRF protection. A per-server HMAC secret signs random nonce-based tokens. Every form includes a `{{csrfField}}` hidden input; a middleware validates that the form field matches the cookie and has a valid HMAC signature on all POST/PUT/PATCH/DELETE requests. Session cookie upgraded from `SameSite=Lax` to `SameSite=Strict`. CSRF cookie is also `HttpOnly`, `Secure`, `SameSite=Strict`. Tests cover token generation/validation, cross-secret rejection, middleware pass/block/mismatch scenarios.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority Summary
|
||||||
|
|
||||||
|
| Priority | Issue | Location |
|
||||||
|
|----------|-------|----------|
|
||||||
|
| ~~**Critical**~~ | ~~#4 — Policy auth contradiction (admin vs user)~~ **RESOLVED** | ARCHITECTURE.md |
|
||||||
|
| ~~**Critical**~~ | ~~#9 — User-controllable SSH cert serials~~ **RESOLVED** | sshca.md |
|
||||||
|
| ~~**Critical**~~ | ~~#13 — Policy path collision (`ca/` vs `sshca/`)~~ **RESOLVED** | sshca.md |
|
||||||
|
| ~~**High**~~ | ~~#5 — No path AAD in barrier encryption~~ **RESOLVED** | ARCHITECTURE.md |
|
||||||
|
| ~~**High**~~ | ~~#12 — No KRL distribution for SSH revocation~~ **RESOLVED** | sshca.md |
|
||||||
|
| ~~**High**~~ | ~~#15 — No min key version for transit rotation~~ **RESOLVED** | transit.md |
|
||||||
|
| ~~**High**~~ | ~~#17 — RSA padding scheme unspecified~~ **RESOLVED** | transit.md |
|
||||||
|
| ~~**High**~~ | ~~#11 — `critical_options` not restricted~~ **RESOLVED** | sshca.md |
|
||||||
|
| ~~**High**~~ | ~~#6 — Single MEK with no rotation~~ **RESOLVED** | ARCHITECTURE.md |
|
||||||
|
| ~~**High**~~ | ~~#22 — No forward secrecy / per-engine DEKs~~ **RESOLVED** | Cross-cutting |
|
||||||
|
| ~~**Medium**~~ | ~~#2 — Token cache revocation gap~~ **ACCEPTED** | ARCHITECTURE.md |
|
||||||
|
| ~~**Medium**~~ | ~~#3 — Admin all-or-nothing access~~ **ACCEPTED** | ARCHITECTURE.md |
|
||||||
|
| ~~**Medium**~~ | ~~#8 — Unseal rate limit resets on restart~~ **ACCEPTED** | ARCHITECTURE.md |
|
||||||
|
| ~~**Medium**~~ | ~~#20 — `decrypt` mapped to `read` action~~ **RESOLVED** | transit.md |
|
||||||
|
| ~~**Medium**~~ | ~~#24 — No CSRF protection for web UI~~ **RESOLVED** | ARCHITECTURE.md |
|
||||||
|
| ~~**Low**~~ | ~~#1 — TLS 1.2 vs 1.3~~ **RESOLVED** | ARCHITECTURE.md |
|
||||||
|
| ~~**Low**~~ | ~~#19 — No batch transit operations~~ **RESOLVED** | transit.md |
|
||||||
|
| ~~**Low**~~ | ~~#18 — HMAC/sign semantic confusion~~ **RESOLVED** | transit.md |
|
||||||
|
| ~~**Medium**~~ | ~~#23 — Generic endpoint bypasses typed route middleware~~ **RESOLVED** | Cross-cutting |
|
||||||
@@ -94,7 +94,7 @@ func fetchEAB(ctx context.Context, client *http.Client, metacryptURL, mount, tok
|
|||||||
|
|
||||||
// buildHTTPClient creates an HTTP client that optionally trusts a custom CA.
|
// buildHTTPClient creates an HTTP client that optionally trusts a custom CA.
|
||||||
func buildHTTPClient(caCertPath string) *http.Client {
|
func buildHTTPClient(caCertPath string) *http.Client {
|
||||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13}
|
||||||
|
|
||||||
if caCertPath != "" {
|
if caCertPath != "" {
|
||||||
pool := x509.NewCertPool()
|
pool := x509.NewCertPool()
|
||||||
|
|||||||
191
cmd/metacrypt/migrate_aad.go
Normal file
191
cmd/metacrypt/migrate_aad.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/config"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
var migrateAADCmd = &cobra.Command{
|
||||||
|
Use: "migrate-aad",
|
||||||
|
Short: "Migrate barrier entries to path-bound AAD encryption",
|
||||||
|
Long: `Re-encrypts all barrier entries so that each entry's path is included
|
||||||
|
as GCM additional authenticated data (AAD). Entries already encrypted with
|
||||||
|
AAD are left untouched. Requires the unseal password.
|
||||||
|
|
||||||
|
This is a one-off migration for the nil-AAD to path-AAD transition. Run it
|
||||||
|
while the server is stopped to avoid concurrent access.`,
|
||||||
|
RunE: runMigrateAAD,
|
||||||
|
}
|
||||||
|
|
||||||
|
var migrateAADDryRun bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrateAADCmd.Flags().BoolVar(&migrateAADDryRun, "dry-run", false, "report what would be migrated without writing")
|
||||||
|
rootCmd.AddCommand(migrateAADCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrateAAD(cmd *cobra.Command, args []string) error {
|
||||||
|
configPath := cfgFile
|
||||||
|
if configPath == "" {
|
||||||
|
configPath = "/srv/metacrypt/metacrypt.toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
database, err := db.Open(cfg.Database.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = database.Close() }()
|
||||||
|
|
||||||
|
// Read unseal password.
|
||||||
|
fmt.Fprint(os.Stderr, "Unseal password: ")
|
||||||
|
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read password: %w", err)
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(passwordBytes)
|
||||||
|
|
||||||
|
// Load seal config and derive MEK.
|
||||||
|
mek, err := deriveMEK(database, passwordBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer crypto.Zeroize(mek)
|
||||||
|
|
||||||
|
// Enumerate all barrier entries.
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := database.QueryContext(ctx, "SELECT path, value FROM barrier_entries")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("query entries: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
path string
|
||||||
|
value []byte
|
||||||
|
}
|
||||||
|
var toMigrate []entry
|
||||||
|
var alreadyMigrated, total int
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var path string
|
||||||
|
var encrypted []byte
|
||||||
|
if err := rows.Scan(&path, &encrypted); err != nil {
|
||||||
|
return fmt.Errorf("scan entry: %w", err)
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
|
||||||
|
// Try decrypting with path AAD first (already migrated).
|
||||||
|
if _, err := crypto.Decrypt(mek, encrypted, []byte(path)); err == nil {
|
||||||
|
alreadyMigrated++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try decrypting with nil AAD (needs migration).
|
||||||
|
if _, err := crypto.Decrypt(mek, encrypted, nil); err != nil {
|
||||||
|
return fmt.Errorf("entry %q: cannot decrypt with AAD or without AAD — data may be corrupt", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
toMigrate = append(toMigrate, entry{path: path, value: encrypted})
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return fmt.Errorf("iterate entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Total entries: %d\n", total)
|
||||||
|
fmt.Printf("Already migrated: %d\n", alreadyMigrated)
|
||||||
|
fmt.Printf("Need migration: %d\n", len(toMigrate))
|
||||||
|
|
||||||
|
if len(toMigrate) == 0 {
|
||||||
|
fmt.Println("Nothing to migrate.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if migrateAADDryRun {
|
||||||
|
fmt.Println("\nDry run — entries that would be migrated:")
|
||||||
|
for _, e := range toMigrate {
|
||||||
|
fmt.Printf(" %s\n", e.path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate entries in a transaction.
|
||||||
|
tx, err := database.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
stmt, err := tx.PrepareContext(ctx, "UPDATE barrier_entries SET value = ?, updated_at = datetime('now') WHERE path = ?")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("prepare update: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = stmt.Close() }()
|
||||||
|
|
||||||
|
for _, e := range toMigrate {
|
||||||
|
// Decrypt with nil AAD.
|
||||||
|
plaintext, err := crypto.Decrypt(mek, e.value, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decrypt %q: %w", e.path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encrypt with path AAD.
|
||||||
|
newEncrypted, err := crypto.Encrypt(mek, plaintext, []byte(e.path))
|
||||||
|
crypto.Zeroize(plaintext)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypt %q: %w", e.path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := stmt.ExecContext(ctx, newEncrypted, e.path); err != nil {
|
||||||
|
return fmt.Errorf("update %q: %w", e.path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Migrated %d entries.\n", len(toMigrate))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveMEK reads the seal config and derives the MEK from the password.
|
||||||
|
func deriveMEK(database *sql.DB, password []byte) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
encryptedMEK []byte
|
||||||
|
salt []byte
|
||||||
|
argTime, argMem uint32
|
||||||
|
argThreads uint8
|
||||||
|
)
|
||||||
|
err := database.QueryRow(`
|
||||||
|
SELECT encrypted_mek, kdf_salt, argon2_time, argon2_memory, argon2_threads
|
||||||
|
FROM seal_config WHERE id = 1`).Scan(&encryptedMEK, &salt, &argTime, &argMem, &argThreads)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read seal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := crypto.Argon2Params{Time: argTime, Memory: argMem, Threads: argThreads}
|
||||||
|
kwk := crypto.DeriveKey(password, salt, params)
|
||||||
|
defer crypto.Zeroize(kwk)
|
||||||
|
|
||||||
|
mek, err := crypto.Decrypt(kwk, encryptedMEK, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid unseal password")
|
||||||
|
}
|
||||||
|
return mek, nil
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runStatus(cmd *cobra.Command, args []string) error {
|
func runStatus(cmd *cobra.Command, args []string) error {
|
||||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13}
|
||||||
|
|
||||||
if statusCACert != "" {
|
if statusCACert != "" {
|
||||||
pem, err := os.ReadFile(statusCACert) //nolint:gosec
|
pem, err := os.ReadFile(statusCACert) //nolint:gosec
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func runUnseal(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildTLSConfig(caCertPath string) (*tls.Config, error) {
|
func buildTLSConfig(caCertPath string) (*tls.Config, error) {
|
||||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13}
|
||||||
if caCertPath != "" {
|
if caCertPath != "" {
|
||||||
pem, err := os.ReadFile(caCertPath) //nolint:gosec
|
pem, err := os.ReadFile(caCertPath) //nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
353
engines/sshca.md
Normal file
353
engines/sshca.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# 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, rsa |
|
||||||
|
| `key_size` | `0` | Key size (ignored for ed25519; 256/384/521 for ECDSA, 2048/4096 for RSA) |
|
||||||
|
| `max_ttl` | `"87600h"` | Maximum certificate validity |
|
||||||
|
| `default_ttl` | `"24h"` | Default certificate validity |
|
||||||
|
|
||||||
|
## 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.bin Current KRL (OpenSSH format)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key material (`caKey`, `caSigner`) is zeroized on `Seal()`.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
### Initialize
|
||||||
|
|
||||||
|
1. Parse and store config in barrier as `config.json`.
|
||||||
|
2. Generate CA key pair using the configured algorithm.
|
||||||
|
3. Store private key PEM and SSH public key in barrier.
|
||||||
|
4. Load key into memory as `ssh.Signer`.
|
||||||
|
|
||||||
|
### Unseal
|
||||||
|
|
||||||
|
1. Load config from barrier.
|
||||||
|
2. Load CA private key from barrier, parse into `crypto.PrivateKey`.
|
||||||
|
3. Wrap as `ssh.Signer`.
|
||||||
|
|
||||||
|
### Seal
|
||||||
|
|
||||||
|
1. Zeroize `caKey` (same `zeroizeKey` helper used by CA engine).
|
||||||
|
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 |
|
||||||
|
|
||||||
|
### 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 (`IsUser()`); admins bypass policy/ownership checks.
|
||||||
|
2. Parse the supplied SSH public key.
|
||||||
|
3. Generate a 64-bit serial using `crypto/rand`.
|
||||||
|
4. Build `ssh.Certificate` with `CertType: ssh.HostCert`, principals, validity, serial.
|
||||||
|
5. Policy check: `sshca/{mount}/id/{hostname}` for each principal, with ownership
|
||||||
|
rules (same as CA engine — hostname not held by another user's active cert).
|
||||||
|
6. Sign with `caSigner`.
|
||||||
|
7. Store `CertRecord` in barrier (certificate bytes, metadata; **no private key**).
|
||||||
|
8. Return signed certificate in OpenSSH format.
|
||||||
|
|
||||||
|
### 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 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.
|
||||||
|
4. Generate a 64-bit serial using `crypto/rand`.
|
||||||
|
5. Build `ssh.Certificate` with `CertType: ssh.UserCert`, principals, validity, serial.
|
||||||
|
6. If the profile specifies `max_ttl`, enforce it (cap the requested TTL).
|
||||||
|
7. Policy check: `sshca/{mount}/id/{principal}` for each principal.
|
||||||
|
Default rule: a user can only sign certs for their own username as principal,
|
||||||
|
unless a policy grants access to other principals.
|
||||||
|
8. Sign with `caSigner`.
|
||||||
|
9. Store `CertRecord` in barrier (includes profile name if used).
|
||||||
|
10. Return signed certificate in OpenSSH format.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Operations
|
||||||
|
|
||||||
|
| Operation | Auth Required | Description |
|
||||||
|
|------------------|---------------|------------------------------------------|
|
||||||
|
| `create-profile` | Admin | Create a signing profile |
|
||||||
|
| `update-profile` | Admin | Update a signing profile |
|
||||||
|
| `get-profile` | User/Admin | Get profile details |
|
||||||
|
| `list-profiles` | User/Admin | List available profiles |
|
||||||
|
| `delete-profile` | Admin | Delete a signing profile |
|
||||||
|
|
||||||
|
#### 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 format
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 an OpenSSH-format KRL
|
||||||
|
(Key Revocation List) that SSH servers fetch periodically and reference via
|
||||||
|
`RevokedKeys` in `sshd_config`.
|
||||||
|
|
||||||
|
### KRL Generation
|
||||||
|
|
||||||
|
The engine maintains a KRL in memory, rebuilt whenever a certificate is revoked
|
||||||
|
or deleted. The KRL is a binary blob in OpenSSH KRL format
|
||||||
|
(`golang.org/x/crypto/ssh` provides marshalling helpers), containing:
|
||||||
|
|
||||||
|
- **Serial revocations**: Revoked certificate serial numbers, keyed to the CA
|
||||||
|
public key. This is the most compact representation.
|
||||||
|
- **KRL version**: Monotonically increasing counter, incremented on each
|
||||||
|
rebuild. SSH servers can use this to detect stale KRLs.
|
||||||
|
- **Generated-at timestamp**: Included in the KRL for freshness checking.
|
||||||
|
|
||||||
|
The KRL is stored in the barrier at `engine/sshca/{mount}/krl.bin` and cached
|
||||||
|
in memory. It is rebuilt on:
|
||||||
|
- `revoke-cert` — adds the serial to the KRL.
|
||||||
|
- `delete-cert` — if the cert was revoked, the KRL is regenerated from all
|
||||||
|
remaining revoked certs.
|
||||||
|
- Engine unseal — loaded from barrier into memory.
|
||||||
|
|
||||||
|
### 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, OpenSSH format) |
|
||||||
|
|
||||||
|
The response includes:
|
||||||
|
- `Content-Type: application/octet-stream`
|
||||||
|
- `ETag` header derived from the KRL version, enabling conditional fetches.
|
||||||
|
- `Cache-Control: max-age=60` to encourage periodic refresh without
|
||||||
|
overwhelming the server.
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
A helper script or systemd timer can fetch the KRL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -o /etc/ssh/metacrypt_krl \
|
||||||
|
https://metacrypt.example.com:8443/v1/sshca/ssh/krl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operations
|
||||||
|
|
||||||
|
| Operation | Auth Required | Description |
|
||||||
|
|------------|---------------|----------------------------------------------|
|
||||||
|
| `get-krl` | None | Return the current KRL in OpenSSH format |
|
||||||
|
|
||||||
|
## 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 (OpenSSH format) |
|
||||||
|
|
||||||
|
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}/cert/{serial}` | Get cert record |
|
||||||
|
| POST | `/v1/sshca/{mount}/cert/{serial}/revoke` | Revoke cert |
|
||||||
|
| DELETE | `/v1/sshca/{mount}/cert/{serial}` | Delete cert record |
|
||||||
|
|
||||||
|
All operations are also accessible via the generic `POST /v1/engine/request`.
|
||||||
|
|
||||||
|
## 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. **`internal/engine/sshca/`** — Implement `SSHCAEngine` (types, lifecycle,
|
||||||
|
operations). Reuse `zeroizeKey` from `internal/engine/ca/` (move to shared
|
||||||
|
helper or duplicate).
|
||||||
|
2. **Register factory** in `cmd/metacrypt/main.go`:
|
||||||
|
`registry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)`.
|
||||||
|
3. **Proto definitions** — `proto/metacrypt/v2/sshca.proto`, run `make proto`.
|
||||||
|
4. **gRPC handlers** — `internal/grpcserver/sshca.go`.
|
||||||
|
5. **REST routes** — Add to `internal/server/routes.go`.
|
||||||
|
6. **Web UI** — Add template + webserver routes.
|
||||||
|
7. **Tests** — Unit tests with in-memory barrier following the CA test pattern.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `golang.org/x/crypto/ssh` (already in `go.mod` via transitive deps)
|
||||||
|
|
||||||
|
## 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.
|
||||||
468
engines/transit.md
Normal file
468
engines/transit.md
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
# Transit Engine Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The transit engine provides encryption-as-a-service: applications send plaintext
|
||||||
|
to Metacrypt and receive ciphertext (or vice versa), without ever handling raw
|
||||||
|
encryption keys. This enables envelope encryption, key rotation, and centralized
|
||||||
|
key management.
|
||||||
|
|
||||||
|
The design is inspired by HashiCorp Vault's transit secrets engine.
|
||||||
|
|
||||||
|
## Engine Type
|
||||||
|
|
||||||
|
`transit` — registered constant already exists in `internal/engine/engine.go`.
|
||||||
|
|
||||||
|
## Mount Configuration
|
||||||
|
|
||||||
|
Passed as `config` at mount time:
|
||||||
|
|
||||||
|
| Field | Default | Description |
|
||||||
|
|--------------------|---------|------------------------------------------------|
|
||||||
|
| `max_key_versions` | `0` | Maximum key versions to retain (0 = unlimited) |
|
||||||
|
|
||||||
|
No engine-wide key algorithm is configured; each named key specifies its own.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Named Keys
|
||||||
|
|
||||||
|
The transit engine manages **named encryption keys**. Each key has:
|
||||||
|
- A unique name (e.g. `"payments"`, `"session-tokens"`)
|
||||||
|
- A key type (symmetric or asymmetric)
|
||||||
|
- One or more **versions** (for key rotation)
|
||||||
|
- Policy flags (exportable, allow-deletion)
|
||||||
|
|
||||||
|
### Key Types
|
||||||
|
|
||||||
|
| Type | Algorithm | Operations |
|
||||||
|
|-----------------|-------------------|------------------|
|
||||||
|
| `aes256-gcm` | AES-256-GCM | Encrypt, Decrypt |
|
||||||
|
| `chacha20-poly` | ChaCha20-Poly1305 | Encrypt, Decrypt |
|
||||||
|
| `ed25519` | Ed25519 | Sign, Verify |
|
||||||
|
| `ecdsa-p256` | ECDSA P-256 | Sign, Verify |
|
||||||
|
| `ecdsa-p384` | ECDSA P-384 | Sign, Verify |
|
||||||
|
| `hmac-sha256` | HMAC-SHA256 | HMAC |
|
||||||
|
| `hmac-sha512` | HMAC-SHA512 | HMAC |
|
||||||
|
|
||||||
|
RSA key types are intentionally excluded. The transit engine is not the right
|
||||||
|
place for RSA — asymmetric encryption belongs in the user engine (via ECDH),
|
||||||
|
and RSA signing offers no advantage over Ed25519/ECDSA for this use case.
|
||||||
|
|
||||||
|
### Key Rotation
|
||||||
|
|
||||||
|
Each key has a current version and may retain older versions. Encryption always
|
||||||
|
uses the latest version. Decryption selects the version from the ciphertext
|
||||||
|
header.
|
||||||
|
|
||||||
|
Each key tracks a `min_decryption_version` (default 1). Decryption requests
|
||||||
|
for ciphertext encrypted with a version below this minimum are rejected. This
|
||||||
|
lets operators complete a rotation cycle:
|
||||||
|
|
||||||
|
1. Rotate the key (creates version N+1).
|
||||||
|
2. Rewrap all existing ciphertext to the latest version.
|
||||||
|
3. Set `min_decryption_version` to N+1.
|
||||||
|
4. Old key versions at or below the minimum can then be pruned via
|
||||||
|
`max_key_versions` or `trim-key`.
|
||||||
|
|
||||||
|
Until `min_decryption_version` is advanced, old versions must be retained.
|
||||||
|
|
||||||
|
### Ciphertext Format
|
||||||
|
|
||||||
|
Transit ciphertexts use a versioned prefix:
|
||||||
|
|
||||||
|
```
|
||||||
|
metacrypt:v{version}:{base64(nonce + ciphertext + tag)}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `v{version}` identifies which key version to use for decryption.
|
||||||
|
|
||||||
|
## Barrier Storage Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
engine/transit/{mount}/config.json Engine configuration
|
||||||
|
engine/transit/{mount}/keys/{name}/config.json Key configuration + policy
|
||||||
|
engine/transit/{mount}/keys/{name}/v{N}.key Key material for version N
|
||||||
|
```
|
||||||
|
|
||||||
|
## In-Memory State
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TransitEngine struct {
|
||||||
|
barrier barrier.Barrier
|
||||||
|
config *TransitConfig
|
||||||
|
keys map[string]*keyState // loaded named keys
|
||||||
|
mountPath string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyState struct {
|
||||||
|
config *KeyConfig
|
||||||
|
versions map[int]*keyVersion
|
||||||
|
minDecryptionVersion int // reject decrypt for versions below this
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyVersion struct {
|
||||||
|
version int
|
||||||
|
key []byte // symmetric key material
|
||||||
|
privKey crypto.PrivateKey // asymmetric private key (nil for symmetric)
|
||||||
|
pubKey crypto.PublicKey // asymmetric public key (nil for symmetric)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
### Initialize
|
||||||
|
|
||||||
|
1. Parse and store config in barrier.
|
||||||
|
2. No keys are created at init time (keys are created on demand).
|
||||||
|
|
||||||
|
### Unseal
|
||||||
|
|
||||||
|
1. Load config from barrier.
|
||||||
|
2. Discover and load all named keys and their versions from the barrier.
|
||||||
|
|
||||||
|
### Seal
|
||||||
|
|
||||||
|
1. Zeroize all key material (symmetric keys overwritten with zeros,
|
||||||
|
asymmetric keys via `zeroizeKey`).
|
||||||
|
2. Nil out all maps.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
| Operation | Auth Required | Description |
|
||||||
|
|------------------|---------------|-----------------------------------------------|
|
||||||
|
| `create-key` | Admin | Create a new named key |
|
||||||
|
| `delete-key` | Admin | Delete a named key (if `allow_deletion` set) |
|
||||||
|
| `get-key` | User/Admin | Get key metadata (no raw material) |
|
||||||
|
| `list-keys` | User/Admin | List named keys |
|
||||||
|
| `rotate-key` | Admin | Create a new version of a named key |
|
||||||
|
| `update-key-config` | Admin | Update mutable key config (e.g. `min_decryption_version`) |
|
||||||
|
| `trim-key` | Admin | Delete versions older than `min_decryption_version` |
|
||||||
|
| `encrypt` | User+Policy | Encrypt plaintext with a named key |
|
||||||
|
| `decrypt` | User+Policy | Decrypt ciphertext with a named key |
|
||||||
|
| `rewrap` | User+Policy | Re-encrypt ciphertext with the latest key version |
|
||||||
|
| `batch-encrypt` | User+Policy | Encrypt multiple plaintexts with a named key |
|
||||||
|
| `batch-decrypt` | User+Policy | Decrypt multiple ciphertexts with a named key |
|
||||||
|
| `batch-rewrap` | User+Policy | Re-encrypt multiple ciphertexts with latest version |
|
||||||
|
| `sign` | User+Policy | Sign data with an asymmetric key (Ed25519, ECDSA) |
|
||||||
|
| `verify` | User+Policy | Verify an asymmetric signature |
|
||||||
|
| `hmac` | User+Policy | Compute HMAC with an HMAC key |
|
||||||
|
| `get-public-key` | User/Admin | Get public key for asymmetric keys |
|
||||||
|
|
||||||
|
### create-key
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Default | Description |
|
||||||
|
|-------------------|----------|----------------|----------------------------------|
|
||||||
|
| `name` | Yes | | Key name |
|
||||||
|
| `type` | Yes | | Key type (see table above) |
|
||||||
|
| `exportable` | No | `false` | Whether raw key material can be exported |
|
||||||
|
| `allow_deletion` | No | `false` | Whether key can be deleted |
|
||||||
|
|
||||||
|
The key is created at version 1 with `min_decryption_version` = 1.
|
||||||
|
|
||||||
|
### encrypt
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------------|----------|---------------------------------------------------|
|
||||||
|
| `key` | Yes | Named key to use |
|
||||||
|
| `plaintext` | Yes | Base64-encoded plaintext |
|
||||||
|
| `context` | No | Base64-encoded context for AEAD additional data |
|
||||||
|
|
||||||
|
Response: `{ "ciphertext": "metacrypt:v1:..." }`
|
||||||
|
|
||||||
|
### decrypt
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|--------------|----------|---------------------------------------------------|
|
||||||
|
| `key` | Yes | Named key to use |
|
||||||
|
| `ciphertext` | Yes | Ciphertext string from encrypt |
|
||||||
|
| `context` | No | Base64-encoded context (must match encrypt context) |
|
||||||
|
|
||||||
|
Response: `{ "plaintext": "<base64>" }`
|
||||||
|
|
||||||
|
### sign
|
||||||
|
|
||||||
|
Asymmetric keys only (Ed25519, ECDSA). HMAC keys must use the `hmac` operation
|
||||||
|
instead — HMAC is a MAC, not a digital signature, and does not provide
|
||||||
|
non-repudiation.
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------------|----------|--------------------------------------------|
|
||||||
|
| `key` | Yes | Named key (Ed25519 or ECDSA type) |
|
||||||
|
| `input` | Yes | Base64-encoded data to sign |
|
||||||
|
| `algorithm` | No | Hash algorithm (default varies by key type) |
|
||||||
|
|
||||||
|
The engine rejects `sign` requests for HMAC key types with an error.
|
||||||
|
|
||||||
|
Response: `{ "signature": "metacrypt:v1:..." }`
|
||||||
|
|
||||||
|
### verify
|
||||||
|
|
||||||
|
Asymmetric keys only. Rejects HMAC key types (use `hmac` to recompute and
|
||||||
|
compare instead).
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------------|----------|--------------------------------------------|
|
||||||
|
| `key` | Yes | Named key (Ed25519 or ECDSA type) |
|
||||||
|
| `input` | Yes | Base64-encoded original data |
|
||||||
|
| `signature` | Yes | Signature string from sign |
|
||||||
|
|
||||||
|
Response: `{ "valid": true }`
|
||||||
|
|
||||||
|
### update-key-config
|
||||||
|
|
||||||
|
Admin-only. Updates mutable key configuration fields.
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|--------------------------|----------|------------------------------------------|
|
||||||
|
| `key` | Yes | Named key |
|
||||||
|
| `min_decryption_version` | No | Minimum version allowed for decryption |
|
||||||
|
|
||||||
|
`min_decryption_version` can only be increased, never decreased. It cannot
|
||||||
|
exceed the current version (you must always be able to decrypt with the latest).
|
||||||
|
|
||||||
|
### trim-key
|
||||||
|
|
||||||
|
Admin-only. Permanently deletes key versions older than `min_decryption_version`.
|
||||||
|
This is irreversible — ciphertext encrypted with trimmed versions can never be
|
||||||
|
decrypted.
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| `key` | Yes | Named key |
|
||||||
|
|
||||||
|
Response: `{ "trimmed_versions": [1, 2, ...] }`
|
||||||
|
|
||||||
|
## Batch Operations
|
||||||
|
|
||||||
|
The transit engine supports batch variants of `encrypt`, `decrypt`, and
|
||||||
|
`rewrap` for high-throughput use cases (e.g. encrypting many database fields,
|
||||||
|
re-encrypting after key rotation). Without batch support, callers are pushed
|
||||||
|
toward caching keys locally, defeating the purpose of transit encryption.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
Each batch request targets a **single named key** with an array of items.
|
||||||
|
Results are returned in the same order. Errors are **per-item** (partial
|
||||||
|
success model) — a single bad ciphertext does not fail the entire batch.
|
||||||
|
|
||||||
|
Single-key-per-batch simplifies authorization: one policy check per batch
|
||||||
|
request rather than per item. Callers needing multiple keys issue multiple
|
||||||
|
batch requests.
|
||||||
|
|
||||||
|
### batch-encrypt
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---------|----------|---------------------------------------------------|
|
||||||
|
| `key` | Yes | Named key to use |
|
||||||
|
| `items` | Yes | Array of encrypt items (see below) |
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------------|----------|-----------------------------------------------|
|
||||||
|
| `plaintext` | Yes | Base64-encoded plaintext |
|
||||||
|
| `context` | No | Base64-encoded context for AEAD additional data |
|
||||||
|
| `reference` | No | Caller-defined reference string (echoed back) |
|
||||||
|
|
||||||
|
Response: `{ "results": [...] }`
|
||||||
|
|
||||||
|
Each result:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|--------------|------------------------------------------------------|
|
||||||
|
| `ciphertext` | `"metacrypt:v1:..."` on success, empty on error |
|
||||||
|
| `reference` | Echoed from the request item (if provided) |
|
||||||
|
| `error` | Error message on failure, empty on success |
|
||||||
|
|
||||||
|
### batch-decrypt
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---------|----------|---------------------------------------------------|
|
||||||
|
| `key` | Yes | Named key to use |
|
||||||
|
| `items` | Yes | Array of decrypt items (see below) |
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|--------------|----------|-----------------------------------------------|
|
||||||
|
| `ciphertext` | Yes | Ciphertext string from encrypt |
|
||||||
|
| `context` | No | Base64-encoded context (must match encrypt) |
|
||||||
|
| `reference` | No | Caller-defined reference string (echoed back) |
|
||||||
|
|
||||||
|
Response: `{ "results": [...] }`
|
||||||
|
|
||||||
|
Each result:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------------|------------------------------------------------------|
|
||||||
|
| `plaintext` | Base64-encoded plaintext on success, empty on error |
|
||||||
|
| `reference` | Echoed from the request item (if provided) |
|
||||||
|
| `error` | Error message on failure, empty on success |
|
||||||
|
|
||||||
|
### batch-rewrap
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---------|----------|---------------------------------------------------|
|
||||||
|
| `key` | Yes | Named key to use |
|
||||||
|
| `items` | Yes | Array of rewrap items (see below) |
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|--------------|----------|-----------------------------------------------|
|
||||||
|
| `ciphertext` | Yes | Ciphertext to re-encrypt with latest version |
|
||||||
|
| `context` | No | Base64-encoded context (must match original) |
|
||||||
|
| `reference` | No | Caller-defined reference string (echoed back) |
|
||||||
|
|
||||||
|
Response: `{ "results": [...] }`
|
||||||
|
|
||||||
|
Each result:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|--------------|------------------------------------------------------|
|
||||||
|
| `ciphertext` | Re-encrypted ciphertext on success, empty on error |
|
||||||
|
| `reference` | Echoed from the request item (if provided) |
|
||||||
|
| `error` | Error message on failure, empty on success |
|
||||||
|
|
||||||
|
### Implementation Notes
|
||||||
|
|
||||||
|
Batch operations are handled inside the transit engine's `HandleRequest` as
|
||||||
|
three additional operation cases (`batch-encrypt`, `batch-decrypt`,
|
||||||
|
`batch-rewrap`). No changes to the `Engine` interface are needed. The engine
|
||||||
|
loops over items internally, loading the key once and reusing it for all items
|
||||||
|
in the batch.
|
||||||
|
|
||||||
|
The `reference` field is opaque to the engine — it allows callers to correlate
|
||||||
|
results with their source records (e.g. a database row ID) without maintaining
|
||||||
|
positional tracking.
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
Follows the same model as the CA engine:
|
||||||
|
- **Admins**: grant-all for all operations.
|
||||||
|
- **Users**: can encrypt/decrypt/sign/verify/hmac if policy allows.
|
||||||
|
- **Policy resources**: `transit/{mount}/key/{key_name}` with granular actions:
|
||||||
|
`encrypt`, `decrypt`, `sign`, `verify`, `hmac` for cryptographic operations;
|
||||||
|
`read` for metadata (get-key, list-keys, get-public-key); `write` for
|
||||||
|
management (create-key, delete-key, rotate-key, update-key-config, trim-key).
|
||||||
|
The `any` action matches all of the above (but never `admin`).
|
||||||
|
- No ownership concept (transit keys are shared resources); access is purely
|
||||||
|
policy-based.
|
||||||
|
|
||||||
|
## gRPC Service (proto/metacrypt/v2/transit.proto)
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
service TransitService {
|
||||||
|
rpc CreateKey(CreateTransitKeyRequest) returns (CreateTransitKeyResponse);
|
||||||
|
rpc DeleteKey(DeleteTransitKeyRequest) returns (DeleteTransitKeyResponse);
|
||||||
|
rpc GetKey(GetTransitKeyRequest) returns (GetTransitKeyResponse);
|
||||||
|
rpc ListKeys(ListTransitKeysRequest) returns (ListTransitKeysResponse);
|
||||||
|
rpc RotateKey(RotateTransitKeyRequest) returns (RotateTransitKeyResponse);
|
||||||
|
rpc UpdateKeyConfig(UpdateTransitKeyConfigRequest) returns (UpdateTransitKeyConfigResponse);
|
||||||
|
rpc TrimKey(TrimTransitKeyRequest) returns (TrimTransitKeyResponse);
|
||||||
|
rpc Encrypt(TransitEncryptRequest) returns (TransitEncryptResponse);
|
||||||
|
rpc Decrypt(TransitDecryptRequest) returns (TransitDecryptResponse);
|
||||||
|
rpc Rewrap(TransitRewrapRequest) returns (TransitRewrapResponse);
|
||||||
|
rpc BatchEncrypt(BatchTransitEncryptRequest) returns (BatchTransitEncryptResponse);
|
||||||
|
rpc BatchDecrypt(BatchTransitDecryptRequest) returns (BatchTransitDecryptResponse);
|
||||||
|
rpc BatchRewrap(BatchTransitRewrapRequest) returns (BatchTransitRewrapResponse);
|
||||||
|
rpc Sign(TransitSignRequest) returns (TransitSignResponse);
|
||||||
|
rpc Verify(TransitVerifyRequest) returns (TransitVerifyResponse);
|
||||||
|
rpc Hmac(TransitHmacRequest) returns (TransitHmacResponse);
|
||||||
|
rpc GetPublicKey(GetTransitPublicKeyRequest) returns (GetTransitPublicKeyResponse);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST Endpoints
|
||||||
|
|
||||||
|
All auth required:
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|---------------------------------------------|--------------------|
|
||||||
|
| POST | `/v1/transit/{mount}/keys` | Create key |
|
||||||
|
| GET | `/v1/transit/{mount}/keys` | List keys |
|
||||||
|
| GET | `/v1/transit/{mount}/keys/{name}` | Get key metadata |
|
||||||
|
| DELETE | `/v1/transit/{mount}/keys/{name}` | Delete key |
|
||||||
|
| POST | `/v1/transit/{mount}/keys/{name}/rotate` | Rotate key |
|
||||||
|
| PATCH | `/v1/transit/{mount}/keys/{name}/config` | Update key config |
|
||||||
|
| POST | `/v1/transit/{mount}/keys/{name}/trim` | Trim old versions |
|
||||||
|
| POST | `/v1/transit/{mount}/encrypt/{key}` | Encrypt |
|
||||||
|
| POST | `/v1/transit/{mount}/decrypt/{key}` | Decrypt |
|
||||||
|
| POST | `/v1/transit/{mount}/rewrap/{key}` | Rewrap |
|
||||||
|
| POST | `/v1/transit/{mount}/batch/encrypt/{key}` | Batch encrypt |
|
||||||
|
| POST | `/v1/transit/{mount}/batch/decrypt/{key}` | Batch decrypt |
|
||||||
|
| POST | `/v1/transit/{mount}/batch/rewrap/{key}` | Batch rewrap |
|
||||||
|
| POST | `/v1/transit/{mount}/sign/{key}` | Sign |
|
||||||
|
| POST | `/v1/transit/{mount}/verify/{key}` | Verify |
|
||||||
|
| POST | `/v1/transit/{mount}/hmac/{key}` | HMAC |
|
||||||
|
|
||||||
|
All operations are also accessible via the generic `POST /v1/engine/request`.
|
||||||
|
|
||||||
|
## Web UI
|
||||||
|
|
||||||
|
Add to `/dashboard` the ability to mount a transit engine.
|
||||||
|
|
||||||
|
Add a `/transit` page displaying:
|
||||||
|
- Named key list with metadata (type, version, created, exportable)
|
||||||
|
- Key detail view with version history
|
||||||
|
- Encrypt/decrypt form for interactive testing
|
||||||
|
- Key rotation button (admin)
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. **`internal/engine/transit/`** — Implement `TransitEngine`:
|
||||||
|
- `types.go` — Config, KeyConfig, key version types.
|
||||||
|
- `transit.go` — Lifecycle (Initialize, Unseal, Seal, HandleRequest).
|
||||||
|
- `encrypt.go` — Encrypt/Decrypt/Rewrap operations.
|
||||||
|
- `sign.go` — Sign/Verify/HMAC operations.
|
||||||
|
- `keys.go` — Key management (create, delete, rotate, list, get).
|
||||||
|
2. **Register factory** in `cmd/metacrypt/main.go`.
|
||||||
|
3. **Proto definitions** — `proto/metacrypt/v2/transit.proto`, run `make proto`.
|
||||||
|
4. **gRPC handlers** — `internal/grpcserver/transit.go`.
|
||||||
|
5. **REST routes** — Add to `internal/server/routes.go`.
|
||||||
|
6. **Web UI** — Add template + webserver routes.
|
||||||
|
7. **Tests** — Unit tests for each operation, key rotation, rewrap correctness.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `golang.org/x/crypto/chacha20poly1305` (for ChaCha20-Poly1305 key type)
|
||||||
|
- Standard library `crypto/aes`, `crypto/cipher`, `crypto/ecdsa`,
|
||||||
|
`crypto/ed25519`, `crypto/hmac`, `crypto/sha256`, `crypto/sha512`
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- All key material encrypted at rest in the barrier, zeroized on seal.
|
||||||
|
- Symmetric keys generated with `crypto/rand`.
|
||||||
|
- Ciphertext format includes version to support key rotation without data loss.
|
||||||
|
- `exportable` flag is immutable after creation — cannot be enabled later.
|
||||||
|
- `allow_deletion` is immutable after creation.
|
||||||
|
- `max_key_versions` pruning only removes old versions, never the current one.
|
||||||
|
- Rewrap operation never exposes plaintext to the caller.
|
||||||
|
- Context (AAD) binding prevents ciphertext from being used in a different context.
|
||||||
|
- `min_decryption_version` enforces key rotation completion: once advanced,
|
||||||
|
old versions are unusable for decryption and can be permanently trimmed.
|
||||||
|
- RSA key types are excluded to avoid padding scheme vulnerabilities
|
||||||
|
(Bleichenbacher attacks on PKCS#1 v1.5). Asymmetric encryption belongs in
|
||||||
|
the user engine; signing uses Ed25519/ECDSA.
|
||||||
302
engines/user.md
Normal file
302
engines/user.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# User Engine Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The user engine provides end-to-end encryption between Metacircular platform
|
||||||
|
users. Each user has a key pair managed by Metacrypt; the service handles key
|
||||||
|
exchange, encryption, and decryption so that messages/data are encrypted at
|
||||||
|
rest and only readable by the intended recipients.
|
||||||
|
|
||||||
|
The design uses hybrid encryption: an asymmetric key pair per user for key
|
||||||
|
exchange, combined with symmetric encryption for message payloads. This enables
|
||||||
|
multi-recipient encryption without sharing symmetric keys directly.
|
||||||
|
|
||||||
|
## Engine Type
|
||||||
|
|
||||||
|
`user` — registered constant already exists in `internal/engine/engine.go`.
|
||||||
|
|
||||||
|
## Mount Configuration
|
||||||
|
|
||||||
|
Passed as `config` at mount time:
|
||||||
|
|
||||||
|
| Field | Default | Description |
|
||||||
|
|-----------------|-------------|------------------------------------------|
|
||||||
|
| `key_algorithm` | `"x25519"` | Key exchange algorithm: x25519, ecdh-p256, ecdh-p384 |
|
||||||
|
| `sym_algorithm` | `"aes256-gcm"` | Symmetric algorithm for message encryption |
|
||||||
|
|
||||||
|
## Trust Model
|
||||||
|
|
||||||
|
Server-trust: Metacrypt holds all private keys in the barrier, same as every
|
||||||
|
other engine. Access control is enforced at the application layer — no API
|
||||||
|
surface exports private keys, and only the engine accesses them internally
|
||||||
|
during encrypt/decrypt operations. An operator with barrier access could
|
||||||
|
theoretically extract keys, which is accepted and consistent with the barrier
|
||||||
|
trust model used throughout Metacrypt.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### User Provisioning
|
||||||
|
|
||||||
|
Any MCIAS user can have a keypair, whether or not they have ever logged in to
|
||||||
|
Metacrypt. Keypairs are created in three ways:
|
||||||
|
|
||||||
|
1. **Self-registration** — an authenticated user calls `register`.
|
||||||
|
2. **Admin provisioning** — an admin calls `provision` with a username. The
|
||||||
|
user does not need to have logged in.
|
||||||
|
3. **Auto-provisioning on encrypt** — when a sender encrypts to a recipient
|
||||||
|
who has no keypair, the engine generates one automatically. The recipient
|
||||||
|
can decrypt when they eventually authenticate.
|
||||||
|
|
||||||
|
### User Key Pairs
|
||||||
|
|
||||||
|
Each provisioned user has a key exchange key pair. The private key is stored
|
||||||
|
encrypted in the barrier and is only used by the engine on behalf of the owning
|
||||||
|
user (enforced in `HandleRequest`). The public key is available to any
|
||||||
|
authenticated user (needed to encrypt messages to that user).
|
||||||
|
|
||||||
|
### Encryption Flow (Sender → Recipient)
|
||||||
|
|
||||||
|
1. Sender calls `encrypt` with plaintext, recipient username(s), and optional
|
||||||
|
metadata.
|
||||||
|
2. Engine generates a random symmetric data encryption key (DEK).
|
||||||
|
3. Engine encrypts the plaintext with the DEK.
|
||||||
|
4. For each recipient: engine performs key agreement (sender private key +
|
||||||
|
recipient public key → shared secret), derives a wrapping key via HKDF,
|
||||||
|
and wraps the DEK.
|
||||||
|
5. Returns an envelope containing ciphertext + per-recipient wrapped DEKs.
|
||||||
|
|
||||||
|
### Decryption Flow
|
||||||
|
|
||||||
|
1. Recipient calls `decrypt` with the envelope.
|
||||||
|
2. Engine finds the recipient's wrapped DEK entry.
|
||||||
|
3. Engine performs key agreement (recipient private key + sender public key →
|
||||||
|
shared secret), derives the wrapping key, unwraps the DEK.
|
||||||
|
4. Engine decrypts the ciphertext with the DEK.
|
||||||
|
5. Returns plaintext.
|
||||||
|
|
||||||
|
### Envelope Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"sender": "alice",
|
||||||
|
"sym_algorithm": "aes256-gcm",
|
||||||
|
"ciphertext": "<base64(nonce + encrypted_payload + tag)>",
|
||||||
|
"recipients": {
|
||||||
|
"bob": "<base64(wrapped_dek)>",
|
||||||
|
"carol": "<base64(wrapped_dek)>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The envelope is base64-encoded as a single opaque blob for transport.
|
||||||
|
|
||||||
|
## Barrier Storage Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
engine/user/{mount}/config.json Engine configuration
|
||||||
|
engine/user/{mount}/users/{username}/priv.pem User private key
|
||||||
|
engine/user/{mount}/users/{username}/pub.pem User public key
|
||||||
|
engine/user/{mount}/users/{username}/config.json Per-user metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
## In-Memory State
|
||||||
|
|
||||||
|
```go
|
||||||
|
type UserEngine struct {
|
||||||
|
barrier barrier.Barrier
|
||||||
|
config *UserConfig
|
||||||
|
users map[string]*userState
|
||||||
|
mountPath string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type userState struct {
|
||||||
|
privKey crypto.PrivateKey // key exchange private key
|
||||||
|
pubKey crypto.PublicKey // key exchange public key
|
||||||
|
config *UserKeyConfig
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
### Initialize
|
||||||
|
|
||||||
|
1. Parse and store config in barrier.
|
||||||
|
2. No user keys are created at init time (created on demand or via `register`).
|
||||||
|
|
||||||
|
### Unseal
|
||||||
|
|
||||||
|
1. Load config from barrier.
|
||||||
|
2. Discover and load all user key pairs from barrier.
|
||||||
|
|
||||||
|
### Seal
|
||||||
|
|
||||||
|
1. Zeroize all private key material.
|
||||||
|
2. Nil out all maps.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
| Operation | Auth Required | Description |
|
||||||
|
|------------------|---------------|-------------------------------------------------|
|
||||||
|
| `register` | User (self) | Create a key pair for the authenticated user |
|
||||||
|
| `provision` | Admin | Create a key pair for any MCIAS user by username |
|
||||||
|
| `get-public-key` | User/Admin | Get any user's public key |
|
||||||
|
| `list-users` | User/Admin | List registered users |
|
||||||
|
| `encrypt` | User+Policy | Encrypt data for one or more recipients |
|
||||||
|
| `decrypt` | User (self) | Decrypt an envelope addressed to the caller |
|
||||||
|
| `rotate-key` | User (self) | Rotate the caller's key pair |
|
||||||
|
| `delete-user` | Admin | Remove a user's key pair |
|
||||||
|
|
||||||
|
### register
|
||||||
|
|
||||||
|
Creates a key pair for the authenticated caller. No-op if the caller already
|
||||||
|
has a keypair (returns existing public key).
|
||||||
|
|
||||||
|
Request data: none (uses `CallerInfo.Username`).
|
||||||
|
|
||||||
|
Response: `{ "public_key": "<base64>" }`
|
||||||
|
|
||||||
|
### provision
|
||||||
|
|
||||||
|
Admin-only. Creates a key pair for the given username. The user does not need
|
||||||
|
to have logged in to Metacrypt. No-op if the user already has a keypair.
|
||||||
|
|
||||||
|
Request data: `{ "username": "<mcias_username>" }`
|
||||||
|
|
||||||
|
Response: `{ "public_key": "<base64>" }`
|
||||||
|
|
||||||
|
### encrypt
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|--------------|----------|-------------------------------------------|
|
||||||
|
| `recipients` | Yes | List of usernames to encrypt for |
|
||||||
|
| `plaintext` | Yes | Base64-encoded plaintext |
|
||||||
|
| `metadata` | No | Arbitrary string metadata (authenticated) |
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Caller must be provisioned (has a key pair). Auto-provision if not.
|
||||||
|
2. For each recipient without a keypair: auto-provision them.
|
||||||
|
3. Load sender's private key and each recipient's public key.
|
||||||
|
4. Generate random DEK, encrypt plaintext with DEK.
|
||||||
|
5. For each recipient: ECDH(sender_priv, recipient_pub) → shared_secret,
|
||||||
|
HKDF(shared_secret, salt, info) → wrapping_key, AES-KeyWrap(wrapping_key,
|
||||||
|
DEK) → wrapped_dek.
|
||||||
|
6. Build and return envelope.
|
||||||
|
|
||||||
|
Authorization:
|
||||||
|
- Admins: grant-all.
|
||||||
|
- Users: can encrypt to any registered user by default.
|
||||||
|
- Policy can restrict which users a sender can encrypt to:
|
||||||
|
resource `user/{mount}/recipient/{username}`, action `write`.
|
||||||
|
|
||||||
|
### decrypt
|
||||||
|
|
||||||
|
Request data:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|------------|----------|--------------------------------|
|
||||||
|
| `envelope` | Yes | Base64-encoded envelope blob |
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Parse envelope, find the caller's wrapped DEK entry.
|
||||||
|
2. Load sender's public key and caller's private key.
|
||||||
|
3. ECDH(caller_priv, sender_pub) → shared_secret → wrapping_key → DEK.
|
||||||
|
4. Decrypt ciphertext with DEK.
|
||||||
|
5. Return plaintext.
|
||||||
|
|
||||||
|
A user can only decrypt envelopes addressed to themselves.
|
||||||
|
|
||||||
|
### rotate-key
|
||||||
|
|
||||||
|
Generates a new key pair for the caller. The old private key is zeroized and
|
||||||
|
deleted. Old envelopes encrypted with the previous key cannot be decrypted
|
||||||
|
after rotation — callers should re-encrypt any stored data before rotating.
|
||||||
|
|
||||||
|
## gRPC Service (proto/metacrypt/v2/user.proto)
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
service UserService {
|
||||||
|
rpc Register(UserRegisterRequest) returns (UserRegisterResponse);
|
||||||
|
rpc Provision(UserProvisionRequest) returns (UserProvisionResponse);
|
||||||
|
rpc GetPublicKey(UserGetPublicKeyRequest) returns (UserGetPublicKeyResponse);
|
||||||
|
rpc ListUsers(UserListUsersRequest) returns (UserListUsersResponse);
|
||||||
|
rpc Encrypt(UserEncryptRequest) returns (UserEncryptResponse);
|
||||||
|
rpc Decrypt(UserDecryptRequest) returns (UserDecryptResponse);
|
||||||
|
rpc RotateKey(UserRotateKeyRequest) returns (UserRotateKeyResponse);
|
||||||
|
rpc DeleteUser(UserDeleteUserRequest) returns (UserDeleteUserResponse);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST Endpoints
|
||||||
|
|
||||||
|
All auth required:
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|-----------------------------------------|------------------------|
|
||||||
|
| POST | `/v1/user/{mount}/register` | Register caller |
|
||||||
|
| POST | `/v1/user/{mount}/provision` | Provision user (admin) |
|
||||||
|
| GET | `/v1/user/{mount}/keys` | List registered users |
|
||||||
|
| GET | `/v1/user/{mount}/keys/{username}` | Get user's public key |
|
||||||
|
| DELETE | `/v1/user/{mount}/keys/{username}` | Delete user (admin) |
|
||||||
|
| POST | `/v1/user/{mount}/encrypt` | Encrypt for recipients |
|
||||||
|
| POST | `/v1/user/{mount}/decrypt` | Decrypt envelope |
|
||||||
|
| POST | `/v1/user/{mount}/rotate` | Rotate caller's key |
|
||||||
|
|
||||||
|
All operations are also accessible via the generic `POST /v1/engine/request`.
|
||||||
|
|
||||||
|
## Web UI
|
||||||
|
|
||||||
|
Add to `/dashboard` the ability to mount a user engine.
|
||||||
|
|
||||||
|
Add a `/user-crypto` page displaying:
|
||||||
|
- Registration status / register button
|
||||||
|
- Public key display
|
||||||
|
- Encrypt form (select recipients, enter message)
|
||||||
|
- Decrypt form (paste envelope)
|
||||||
|
- Key rotation button with warning
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. **`internal/engine/user/`** — Implement `UserEngine`:
|
||||||
|
- `types.go` — Config types, envelope format.
|
||||||
|
- `user.go` — Lifecycle (Initialize, Unseal, Seal, HandleRequest).
|
||||||
|
- `crypto.go` — ECDH key agreement, HKDF derivation, DEK wrap/unwrap,
|
||||||
|
symmetric encrypt/decrypt.
|
||||||
|
- `keys.go` — User registration, key rotation, deletion.
|
||||||
|
2. **Register factory** in `cmd/metacrypt/main.go`.
|
||||||
|
3. **Proto definitions** — `proto/metacrypt/v2/user.proto`, run `make proto`.
|
||||||
|
4. **gRPC handlers** — `internal/grpcserver/user.go`.
|
||||||
|
5. **REST routes** — Add to `internal/server/routes.go`.
|
||||||
|
6. **Web UI** — Add template + webserver routes.
|
||||||
|
7. **Tests** — Unit tests: register, encrypt/decrypt roundtrip, multi-recipient,
|
||||||
|
key rotation invalidates old envelopes, authorization checks.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `golang.org/x/crypto/hkdf` (for key derivation from ECDH shared secret)
|
||||||
|
- `crypto/ecdh` (Go 1.20+, for X25519 and NIST curve key exchange)
|
||||||
|
- Standard library `crypto/aes`, `crypto/cipher`, `crypto/rand`
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Private keys encrypted at rest in the barrier, zeroized on seal.
|
||||||
|
- DEK is random per-encryption; never reused.
|
||||||
|
- HKDF derivation includes sender and recipient identities in the info string
|
||||||
|
to prevent key confusion attacks:
|
||||||
|
`info = "metacrypt-user-v1:" + sender + ":" + recipient`.
|
||||||
|
- Envelope includes sender identity so the recipient can derive the correct
|
||||||
|
shared secret.
|
||||||
|
- Key rotation is destructive — old data cannot be decrypted. The engine should
|
||||||
|
warn and require explicit confirmation (admin or self only).
|
||||||
|
- Server-trust model: the server holds all private keys in the barrier. No API
|
||||||
|
surface exports private keys. Access control is application-enforced — the
|
||||||
|
engine only uses a private key on behalf of its owner during encrypt/decrypt.
|
||||||
|
- Auto-provisioned users have keypairs waiting for them; their private keys are
|
||||||
|
protected identically to explicitly registered users.
|
||||||
|
- Metadata in the envelope is authenticated (included as additional data in
|
||||||
|
AEAD) but not encrypted — it is visible to anyone holding the envelope.
|
||||||
|
- Post-quantum readiness: the `key_algorithm` config supports future hybrid
|
||||||
|
schemes (e.g. X25519 + ML-KEM). The envelope version field enables migration.
|
||||||
523
gen/metacrypt/v1/barrier.pb.go
Normal file
523
gen/metacrypt/v1/barrier.pb.go
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.11
|
||||||
|
// protoc v3.20.3
|
||||||
|
// source: proto/metacrypt/v1/barrier.proto
|
||||||
|
|
||||||
|
package metacryptv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListKeysRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysRequest) Reset() {
|
||||||
|
*x = ListKeysRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListKeysRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListKeysRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListKeysRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListKeysRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListKeysResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Keys []*BarrierKeyInfo `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) Reset() {
|
||||||
|
*x = ListKeysResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListKeysResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListKeysResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListKeysResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) GetKeys() []*BarrierKeyInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Keys
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BarrierKeyInfo struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
KeyId string `protobuf:"bytes,1,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"`
|
||||||
|
Version int32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"`
|
||||||
|
CreatedAt string `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
|
RotatedAt string `protobuf:"bytes,4,opt,name=rotated_at,json=rotatedAt,proto3" json:"rotated_at,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) Reset() {
|
||||||
|
*x = BarrierKeyInfo{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*BarrierKeyInfo) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[2]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use BarrierKeyInfo.ProtoReflect.Descriptor instead.
|
||||||
|
func (*BarrierKeyInfo) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetKeyId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.KeyId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetVersion() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Version
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetCreatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetRotatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RotatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateMEKRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) Reset() {
|
||||||
|
*x = RotateMEKRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateMEKRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[3]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateMEKRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateMEKRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) GetPassword() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Password
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateMEKResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) Reset() {
|
||||||
|
*x = RotateMEKResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateMEKResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[4]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateMEKResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateMEKResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) GetOk() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ok
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateKeyRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
KeyId string `protobuf:"bytes,1,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) Reset() {
|
||||||
|
*x = RotateKeyRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateKeyRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[5]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateKeyRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateKeyRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) GetKeyId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.KeyId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateKeyResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) Reset() {
|
||||||
|
*x = RotateKeyResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateKeyResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[6]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateKeyResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateKeyResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) GetOk() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ok
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrateBarrierRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierRequest) Reset() {
|
||||||
|
*x = MigrateBarrierRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MigrateBarrierRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[7]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MigrateBarrierRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MigrateBarrierRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrateBarrierResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Migrated int32 `protobuf:"varint,1,opt,name=migrated,proto3" json:"migrated,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) Reset() {
|
||||||
|
*x = MigrateBarrierResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MigrateBarrierResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v1_barrier_proto_msgTypes[8]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MigrateBarrierResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MigrateBarrierResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) GetMigrated() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Migrated
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_proto_metacrypt_v1_barrier_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
const file_proto_metacrypt_v1_barrier_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
" proto/metacrypt/v1/barrier.proto\x12\fmetacrypt.v1\"\x11\n" +
|
||||||
|
"\x0fListKeysRequest\"D\n" +
|
||||||
|
"\x10ListKeysResponse\x120\n" +
|
||||||
|
"\x04keys\x18\x01 \x03(\v2\x1c.metacrypt.v1.BarrierKeyInfoR\x04keys\"\x7f\n" +
|
||||||
|
"\x0eBarrierKeyInfo\x12\x15\n" +
|
||||||
|
"\x06key_id\x18\x01 \x01(\tR\x05keyId\x12\x18\n" +
|
||||||
|
"\aversion\x18\x02 \x01(\x05R\aversion\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"created_at\x18\x03 \x01(\tR\tcreatedAt\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"rotated_at\x18\x04 \x01(\tR\trotatedAt\".\n" +
|
||||||
|
"\x10RotateMEKRequest\x12\x1a\n" +
|
||||||
|
"\bpassword\x18\x01 \x01(\tR\bpassword\"#\n" +
|
||||||
|
"\x11RotateMEKResponse\x12\x0e\n" +
|
||||||
|
"\x02ok\x18\x01 \x01(\bR\x02ok\")\n" +
|
||||||
|
"\x10RotateKeyRequest\x12\x15\n" +
|
||||||
|
"\x06key_id\x18\x01 \x01(\tR\x05keyId\"#\n" +
|
||||||
|
"\x11RotateKeyResponse\x12\x0e\n" +
|
||||||
|
"\x02ok\x18\x01 \x01(\bR\x02ok\"\x17\n" +
|
||||||
|
"\x15MigrateBarrierRequest\"4\n" +
|
||||||
|
"\x16MigrateBarrierResponse\x12\x1a\n" +
|
||||||
|
"\bmigrated\x18\x01 \x01(\x05R\bmigrated2\xcd\x02\n" +
|
||||||
|
"\x0eBarrierService\x12I\n" +
|
||||||
|
"\bListKeys\x12\x1d.metacrypt.v1.ListKeysRequest\x1a\x1e.metacrypt.v1.ListKeysResponse\x12L\n" +
|
||||||
|
"\tRotateMEK\x12\x1e.metacrypt.v1.RotateMEKRequest\x1a\x1f.metacrypt.v1.RotateMEKResponse\x12L\n" +
|
||||||
|
"\tRotateKey\x12\x1e.metacrypt.v1.RotateKeyRequest\x1a\x1f.metacrypt.v1.RotateKeyResponse\x12T\n" +
|
||||||
|
"\aMigrate\x12#.metacrypt.v1.MigrateBarrierRequest\x1a$.metacrypt.v1.MigrateBarrierResponseB>Z<git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v1;metacryptv1b\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_proto_metacrypt_v1_barrier_proto_rawDescOnce sync.Once
|
||||||
|
file_proto_metacrypt_v1_barrier_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_proto_metacrypt_v1_barrier_proto_rawDescGZIP() []byte {
|
||||||
|
file_proto_metacrypt_v1_barrier_proto_rawDescOnce.Do(func() {
|
||||||
|
file_proto_metacrypt_v1_barrier_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v1_barrier_proto_rawDesc), len(file_proto_metacrypt_v1_barrier_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_proto_metacrypt_v1_barrier_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_proto_metacrypt_v1_barrier_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||||
|
var file_proto_metacrypt_v1_barrier_proto_goTypes = []any{
|
||||||
|
(*ListKeysRequest)(nil), // 0: metacrypt.v1.ListKeysRequest
|
||||||
|
(*ListKeysResponse)(nil), // 1: metacrypt.v1.ListKeysResponse
|
||||||
|
(*BarrierKeyInfo)(nil), // 2: metacrypt.v1.BarrierKeyInfo
|
||||||
|
(*RotateMEKRequest)(nil), // 3: metacrypt.v1.RotateMEKRequest
|
||||||
|
(*RotateMEKResponse)(nil), // 4: metacrypt.v1.RotateMEKResponse
|
||||||
|
(*RotateKeyRequest)(nil), // 5: metacrypt.v1.RotateKeyRequest
|
||||||
|
(*RotateKeyResponse)(nil), // 6: metacrypt.v1.RotateKeyResponse
|
||||||
|
(*MigrateBarrierRequest)(nil), // 7: metacrypt.v1.MigrateBarrierRequest
|
||||||
|
(*MigrateBarrierResponse)(nil), // 8: metacrypt.v1.MigrateBarrierResponse
|
||||||
|
}
|
||||||
|
var file_proto_metacrypt_v1_barrier_proto_depIdxs = []int32{
|
||||||
|
2, // 0: metacrypt.v1.ListKeysResponse.keys:type_name -> metacrypt.v1.BarrierKeyInfo
|
||||||
|
0, // 1: metacrypt.v1.BarrierService.ListKeys:input_type -> metacrypt.v1.ListKeysRequest
|
||||||
|
3, // 2: metacrypt.v1.BarrierService.RotateMEK:input_type -> metacrypt.v1.RotateMEKRequest
|
||||||
|
5, // 3: metacrypt.v1.BarrierService.RotateKey:input_type -> metacrypt.v1.RotateKeyRequest
|
||||||
|
7, // 4: metacrypt.v1.BarrierService.Migrate:input_type -> metacrypt.v1.MigrateBarrierRequest
|
||||||
|
1, // 5: metacrypt.v1.BarrierService.ListKeys:output_type -> metacrypt.v1.ListKeysResponse
|
||||||
|
4, // 6: metacrypt.v1.BarrierService.RotateMEK:output_type -> metacrypt.v1.RotateMEKResponse
|
||||||
|
6, // 7: metacrypt.v1.BarrierService.RotateKey:output_type -> metacrypt.v1.RotateKeyResponse
|
||||||
|
8, // 8: metacrypt.v1.BarrierService.Migrate:output_type -> metacrypt.v1.MigrateBarrierResponse
|
||||||
|
5, // [5:9] is the sub-list for method output_type
|
||||||
|
1, // [1:5] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_proto_metacrypt_v1_barrier_proto_init() }
|
||||||
|
func file_proto_metacrypt_v1_barrier_proto_init() {
|
||||||
|
if File_proto_metacrypt_v1_barrier_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v1_barrier_proto_rawDesc), len(file_proto_metacrypt_v1_barrier_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 9,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_proto_metacrypt_v1_barrier_proto_goTypes,
|
||||||
|
DependencyIndexes: file_proto_metacrypt_v1_barrier_proto_depIdxs,
|
||||||
|
MessageInfos: file_proto_metacrypt_v1_barrier_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_proto_metacrypt_v1_barrier_proto = out.File
|
||||||
|
file_proto_metacrypt_v1_barrier_proto_goTypes = nil
|
||||||
|
file_proto_metacrypt_v1_barrier_proto_depIdxs = nil
|
||||||
|
}
|
||||||
235
gen/metacrypt/v1/barrier_grpc.pb.go
Normal file
235
gen/metacrypt/v1/barrier_grpc.pb.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
|
// - protoc v3.20.3
|
||||||
|
// source: proto/metacrypt/v1/barrier.proto
|
||||||
|
|
||||||
|
package metacryptv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
BarrierService_ListKeys_FullMethodName = "/metacrypt.v1.BarrierService/ListKeys"
|
||||||
|
BarrierService_RotateMEK_FullMethodName = "/metacrypt.v1.BarrierService/RotateMEK"
|
||||||
|
BarrierService_RotateKey_FullMethodName = "/metacrypt.v1.BarrierService/RotateKey"
|
||||||
|
BarrierService_Migrate_FullMethodName = "/metacrypt.v1.BarrierService/Migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BarrierServiceClient is the client API for BarrierService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type BarrierServiceClient interface {
|
||||||
|
ListKeys(ctx context.Context, in *ListKeysRequest, opts ...grpc.CallOption) (*ListKeysResponse, error)
|
||||||
|
RotateMEK(ctx context.Context, in *RotateMEKRequest, opts ...grpc.CallOption) (*RotateMEKResponse, error)
|
||||||
|
RotateKey(ctx context.Context, in *RotateKeyRequest, opts ...grpc.CallOption) (*RotateKeyResponse, error)
|
||||||
|
Migrate(ctx context.Context, in *MigrateBarrierRequest, opts ...grpc.CallOption) (*MigrateBarrierResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type barrierServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBarrierServiceClient(cc grpc.ClientConnInterface) BarrierServiceClient {
|
||||||
|
return &barrierServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) ListKeys(ctx context.Context, in *ListKeysRequest, opts ...grpc.CallOption) (*ListKeysResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListKeysResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_ListKeys_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) RotateMEK(ctx context.Context, in *RotateMEKRequest, opts ...grpc.CallOption) (*RotateMEKResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RotateMEKResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_RotateMEK_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) RotateKey(ctx context.Context, in *RotateKeyRequest, opts ...grpc.CallOption) (*RotateKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RotateKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_RotateKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) Migrate(ctx context.Context, in *MigrateBarrierRequest, opts ...grpc.CallOption) (*MigrateBarrierResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(MigrateBarrierResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_Migrate_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BarrierServiceServer is the server API for BarrierService service.
|
||||||
|
// All implementations must embed UnimplementedBarrierServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type BarrierServiceServer interface {
|
||||||
|
ListKeys(context.Context, *ListKeysRequest) (*ListKeysResponse, error)
|
||||||
|
RotateMEK(context.Context, *RotateMEKRequest) (*RotateMEKResponse, error)
|
||||||
|
RotateKey(context.Context, *RotateKeyRequest) (*RotateKeyResponse, error)
|
||||||
|
Migrate(context.Context, *MigrateBarrierRequest) (*MigrateBarrierResponse, error)
|
||||||
|
mustEmbedUnimplementedBarrierServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedBarrierServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedBarrierServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedBarrierServiceServer) ListKeys(context.Context, *ListKeysRequest) (*ListKeysResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListKeys not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) RotateMEK(context.Context, *RotateMEKRequest) (*RotateMEKResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method RotateMEK not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) RotateKey(context.Context, *RotateKeyRequest) (*RotateKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method RotateKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) Migrate(context.Context, *MigrateBarrierRequest) (*MigrateBarrierResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Migrate not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) mustEmbedUnimplementedBarrierServiceServer() {}
|
||||||
|
func (UnimplementedBarrierServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeBarrierServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to BarrierServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeBarrierServiceServer interface {
|
||||||
|
mustEmbedUnimplementedBarrierServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterBarrierServiceServer(s grpc.ServiceRegistrar, srv BarrierServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedBarrierServiceServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&BarrierService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_ListKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListKeysRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).ListKeys(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_ListKeys_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).ListKeys(ctx, req.(*ListKeysRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_RotateMEK_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RotateMEKRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).RotateMEK(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_RotateMEK_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).RotateMEK(ctx, req.(*RotateMEKRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_RotateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RotateKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).RotateKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_RotateKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).RotateKey(ctx, req.(*RotateKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_Migrate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(MigrateBarrierRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).Migrate(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_Migrate_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).Migrate(ctx, req.(*MigrateBarrierRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BarrierService_ServiceDesc is the grpc.ServiceDesc for BarrierService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var BarrierService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "metacrypt.v1.BarrierService",
|
||||||
|
HandlerType: (*BarrierServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ListKeys",
|
||||||
|
Handler: _BarrierService_ListKeys_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RotateMEK",
|
||||||
|
Handler: _BarrierService_RotateMEK_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RotateKey",
|
||||||
|
Handler: _BarrierService_RotateKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Migrate",
|
||||||
|
Handler: _BarrierService_Migrate_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/metacrypt/v1/barrier.proto",
|
||||||
|
}
|
||||||
523
gen/metacrypt/v2/barrier.pb.go
Normal file
523
gen/metacrypt/v2/barrier.pb.go
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.11
|
||||||
|
// protoc v3.20.3
|
||||||
|
// source: proto/metacrypt/v2/barrier.proto
|
||||||
|
|
||||||
|
package metacryptv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListKeysRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysRequest) Reset() {
|
||||||
|
*x = ListKeysRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListKeysRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListKeysRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListKeysRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListKeysRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListKeysResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Keys []*BarrierKeyInfo `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) Reset() {
|
||||||
|
*x = ListKeysResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListKeysResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListKeysResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListKeysResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListKeysResponse) GetKeys() []*BarrierKeyInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Keys
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BarrierKeyInfo struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
KeyId string `protobuf:"bytes,1,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"`
|
||||||
|
Version int32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"`
|
||||||
|
CreatedAt string `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
|
RotatedAt string `protobuf:"bytes,4,opt,name=rotated_at,json=rotatedAt,proto3" json:"rotated_at,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) Reset() {
|
||||||
|
*x = BarrierKeyInfo{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*BarrierKeyInfo) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[2]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use BarrierKeyInfo.ProtoReflect.Descriptor instead.
|
||||||
|
func (*BarrierKeyInfo) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetKeyId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.KeyId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetVersion() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Version
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetCreatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BarrierKeyInfo) GetRotatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RotatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateMEKRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) Reset() {
|
||||||
|
*x = RotateMEKRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateMEKRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[3]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateMEKRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateMEKRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKRequest) GetPassword() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Password
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateMEKResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) Reset() {
|
||||||
|
*x = RotateMEKResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateMEKResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[4]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateMEKResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateMEKResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateMEKResponse) GetOk() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ok
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateKeyRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
KeyId string `protobuf:"bytes,1,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) Reset() {
|
||||||
|
*x = RotateKeyRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateKeyRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[5]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateKeyRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateKeyRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyRequest) GetKeyId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.KeyId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RotateKeyResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) Reset() {
|
||||||
|
*x = RotateKeyResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RotateKeyResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[6]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RotateKeyResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RotateKeyResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RotateKeyResponse) GetOk() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ok
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrateBarrierRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierRequest) Reset() {
|
||||||
|
*x = MigrateBarrierRequest{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MigrateBarrierRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[7]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MigrateBarrierRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MigrateBarrierRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrateBarrierResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Migrated int32 `protobuf:"varint,1,opt,name=migrated,proto3" json:"migrated,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) Reset() {
|
||||||
|
*x = MigrateBarrierResponse{}
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MigrateBarrierResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_metacrypt_v2_barrier_proto_msgTypes[8]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MigrateBarrierResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MigrateBarrierResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MigrateBarrierResponse) GetMigrated() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Migrated
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_proto_metacrypt_v2_barrier_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
const file_proto_metacrypt_v2_barrier_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
" proto/metacrypt/v2/barrier.proto\x12\fmetacrypt.v2\"\x11\n" +
|
||||||
|
"\x0fListKeysRequest\"D\n" +
|
||||||
|
"\x10ListKeysResponse\x120\n" +
|
||||||
|
"\x04keys\x18\x01 \x03(\v2\x1c.metacrypt.v2.BarrierKeyInfoR\x04keys\"\x7f\n" +
|
||||||
|
"\x0eBarrierKeyInfo\x12\x15\n" +
|
||||||
|
"\x06key_id\x18\x01 \x01(\tR\x05keyId\x12\x18\n" +
|
||||||
|
"\aversion\x18\x02 \x01(\x05R\aversion\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"created_at\x18\x03 \x01(\tR\tcreatedAt\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"rotated_at\x18\x04 \x01(\tR\trotatedAt\".\n" +
|
||||||
|
"\x10RotateMEKRequest\x12\x1a\n" +
|
||||||
|
"\bpassword\x18\x01 \x01(\tR\bpassword\"#\n" +
|
||||||
|
"\x11RotateMEKResponse\x12\x0e\n" +
|
||||||
|
"\x02ok\x18\x01 \x01(\bR\x02ok\")\n" +
|
||||||
|
"\x10RotateKeyRequest\x12\x15\n" +
|
||||||
|
"\x06key_id\x18\x01 \x01(\tR\x05keyId\"#\n" +
|
||||||
|
"\x11RotateKeyResponse\x12\x0e\n" +
|
||||||
|
"\x02ok\x18\x01 \x01(\bR\x02ok\"\x17\n" +
|
||||||
|
"\x15MigrateBarrierRequest\"4\n" +
|
||||||
|
"\x16MigrateBarrierResponse\x12\x1a\n" +
|
||||||
|
"\bmigrated\x18\x01 \x01(\x05R\bmigrated2\xcd\x02\n" +
|
||||||
|
"\x0eBarrierService\x12I\n" +
|
||||||
|
"\bListKeys\x12\x1d.metacrypt.v2.ListKeysRequest\x1a\x1e.metacrypt.v2.ListKeysResponse\x12L\n" +
|
||||||
|
"\tRotateMEK\x12\x1e.metacrypt.v2.RotateMEKRequest\x1a\x1f.metacrypt.v2.RotateMEKResponse\x12L\n" +
|
||||||
|
"\tRotateKey\x12\x1e.metacrypt.v2.RotateKeyRequest\x1a\x1f.metacrypt.v2.RotateKeyResponse\x12T\n" +
|
||||||
|
"\aMigrate\x12#.metacrypt.v2.MigrateBarrierRequest\x1a$.metacrypt.v2.MigrateBarrierResponseB>Z<git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2;metacryptv2b\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_proto_metacrypt_v2_barrier_proto_rawDescOnce sync.Once
|
||||||
|
file_proto_metacrypt_v2_barrier_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_proto_metacrypt_v2_barrier_proto_rawDescGZIP() []byte {
|
||||||
|
file_proto_metacrypt_v2_barrier_proto_rawDescOnce.Do(func() {
|
||||||
|
file_proto_metacrypt_v2_barrier_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v2_barrier_proto_rawDesc), len(file_proto_metacrypt_v2_barrier_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_proto_metacrypt_v2_barrier_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_proto_metacrypt_v2_barrier_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||||
|
var file_proto_metacrypt_v2_barrier_proto_goTypes = []any{
|
||||||
|
(*ListKeysRequest)(nil), // 0: metacrypt.v2.ListKeysRequest
|
||||||
|
(*ListKeysResponse)(nil), // 1: metacrypt.v2.ListKeysResponse
|
||||||
|
(*BarrierKeyInfo)(nil), // 2: metacrypt.v2.BarrierKeyInfo
|
||||||
|
(*RotateMEKRequest)(nil), // 3: metacrypt.v2.RotateMEKRequest
|
||||||
|
(*RotateMEKResponse)(nil), // 4: metacrypt.v2.RotateMEKResponse
|
||||||
|
(*RotateKeyRequest)(nil), // 5: metacrypt.v2.RotateKeyRequest
|
||||||
|
(*RotateKeyResponse)(nil), // 6: metacrypt.v2.RotateKeyResponse
|
||||||
|
(*MigrateBarrierRequest)(nil), // 7: metacrypt.v2.MigrateBarrierRequest
|
||||||
|
(*MigrateBarrierResponse)(nil), // 8: metacrypt.v2.MigrateBarrierResponse
|
||||||
|
}
|
||||||
|
var file_proto_metacrypt_v2_barrier_proto_depIdxs = []int32{
|
||||||
|
2, // 0: metacrypt.v2.ListKeysResponse.keys:type_name -> metacrypt.v2.BarrierKeyInfo
|
||||||
|
0, // 1: metacrypt.v2.BarrierService.ListKeys:input_type -> metacrypt.v2.ListKeysRequest
|
||||||
|
3, // 2: metacrypt.v2.BarrierService.RotateMEK:input_type -> metacrypt.v2.RotateMEKRequest
|
||||||
|
5, // 3: metacrypt.v2.BarrierService.RotateKey:input_type -> metacrypt.v2.RotateKeyRequest
|
||||||
|
7, // 4: metacrypt.v2.BarrierService.Migrate:input_type -> metacrypt.v2.MigrateBarrierRequest
|
||||||
|
1, // 5: metacrypt.v2.BarrierService.ListKeys:output_type -> metacrypt.v2.ListKeysResponse
|
||||||
|
4, // 6: metacrypt.v2.BarrierService.RotateMEK:output_type -> metacrypt.v2.RotateMEKResponse
|
||||||
|
6, // 7: metacrypt.v2.BarrierService.RotateKey:output_type -> metacrypt.v2.RotateKeyResponse
|
||||||
|
8, // 8: metacrypt.v2.BarrierService.Migrate:output_type -> metacrypt.v2.MigrateBarrierResponse
|
||||||
|
5, // [5:9] is the sub-list for method output_type
|
||||||
|
1, // [1:5] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_proto_metacrypt_v2_barrier_proto_init() }
|
||||||
|
func file_proto_metacrypt_v2_barrier_proto_init() {
|
||||||
|
if File_proto_metacrypt_v2_barrier_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_metacrypt_v2_barrier_proto_rawDesc), len(file_proto_metacrypt_v2_barrier_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 9,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_proto_metacrypt_v2_barrier_proto_goTypes,
|
||||||
|
DependencyIndexes: file_proto_metacrypt_v2_barrier_proto_depIdxs,
|
||||||
|
MessageInfos: file_proto_metacrypt_v2_barrier_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_proto_metacrypt_v2_barrier_proto = out.File
|
||||||
|
file_proto_metacrypt_v2_barrier_proto_goTypes = nil
|
||||||
|
file_proto_metacrypt_v2_barrier_proto_depIdxs = nil
|
||||||
|
}
|
||||||
235
gen/metacrypt/v2/barrier_grpc.pb.go
Normal file
235
gen/metacrypt/v2/barrier_grpc.pb.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
|
// - protoc v3.20.3
|
||||||
|
// source: proto/metacrypt/v2/barrier.proto
|
||||||
|
|
||||||
|
package metacryptv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
BarrierService_ListKeys_FullMethodName = "/metacrypt.v2.BarrierService/ListKeys"
|
||||||
|
BarrierService_RotateMEK_FullMethodName = "/metacrypt.v2.BarrierService/RotateMEK"
|
||||||
|
BarrierService_RotateKey_FullMethodName = "/metacrypt.v2.BarrierService/RotateKey"
|
||||||
|
BarrierService_Migrate_FullMethodName = "/metacrypt.v2.BarrierService/Migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BarrierServiceClient is the client API for BarrierService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type BarrierServiceClient interface {
|
||||||
|
ListKeys(ctx context.Context, in *ListKeysRequest, opts ...grpc.CallOption) (*ListKeysResponse, error)
|
||||||
|
RotateMEK(ctx context.Context, in *RotateMEKRequest, opts ...grpc.CallOption) (*RotateMEKResponse, error)
|
||||||
|
RotateKey(ctx context.Context, in *RotateKeyRequest, opts ...grpc.CallOption) (*RotateKeyResponse, error)
|
||||||
|
Migrate(ctx context.Context, in *MigrateBarrierRequest, opts ...grpc.CallOption) (*MigrateBarrierResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type barrierServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBarrierServiceClient(cc grpc.ClientConnInterface) BarrierServiceClient {
|
||||||
|
return &barrierServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) ListKeys(ctx context.Context, in *ListKeysRequest, opts ...grpc.CallOption) (*ListKeysResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListKeysResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_ListKeys_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) RotateMEK(ctx context.Context, in *RotateMEKRequest, opts ...grpc.CallOption) (*RotateMEKResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RotateMEKResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_RotateMEK_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) RotateKey(ctx context.Context, in *RotateKeyRequest, opts ...grpc.CallOption) (*RotateKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RotateKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_RotateKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *barrierServiceClient) Migrate(ctx context.Context, in *MigrateBarrierRequest, opts ...grpc.CallOption) (*MigrateBarrierResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(MigrateBarrierResponse)
|
||||||
|
err := c.cc.Invoke(ctx, BarrierService_Migrate_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BarrierServiceServer is the server API for BarrierService service.
|
||||||
|
// All implementations must embed UnimplementedBarrierServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type BarrierServiceServer interface {
|
||||||
|
ListKeys(context.Context, *ListKeysRequest) (*ListKeysResponse, error)
|
||||||
|
RotateMEK(context.Context, *RotateMEKRequest) (*RotateMEKResponse, error)
|
||||||
|
RotateKey(context.Context, *RotateKeyRequest) (*RotateKeyResponse, error)
|
||||||
|
Migrate(context.Context, *MigrateBarrierRequest) (*MigrateBarrierResponse, error)
|
||||||
|
mustEmbedUnimplementedBarrierServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedBarrierServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedBarrierServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedBarrierServiceServer) ListKeys(context.Context, *ListKeysRequest) (*ListKeysResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListKeys not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) RotateMEK(context.Context, *RotateMEKRequest) (*RotateMEKResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method RotateMEK not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) RotateKey(context.Context, *RotateKeyRequest) (*RotateKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method RotateKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) Migrate(context.Context, *MigrateBarrierRequest) (*MigrateBarrierResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Migrate not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedBarrierServiceServer) mustEmbedUnimplementedBarrierServiceServer() {}
|
||||||
|
func (UnimplementedBarrierServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeBarrierServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to BarrierServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeBarrierServiceServer interface {
|
||||||
|
mustEmbedUnimplementedBarrierServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterBarrierServiceServer(s grpc.ServiceRegistrar, srv BarrierServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedBarrierServiceServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&BarrierService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_ListKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListKeysRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).ListKeys(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_ListKeys_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).ListKeys(ctx, req.(*ListKeysRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_RotateMEK_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RotateMEKRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).RotateMEK(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_RotateMEK_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).RotateMEK(ctx, req.(*RotateMEKRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_RotateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RotateKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).RotateKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_RotateKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).RotateKey(ctx, req.(*RotateKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _BarrierService_Migrate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(MigrateBarrierRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BarrierServiceServer).Migrate(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: BarrierService_Migrate_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BarrierServiceServer).Migrate(ctx, req.(*MigrateBarrierRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BarrierService_ServiceDesc is the grpc.ServiceDesc for BarrierService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var BarrierService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "metacrypt.v2.BarrierService",
|
||||||
|
HandlerType: (*BarrierServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ListKeys",
|
||||||
|
Handler: _BarrierService_ListKeys_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RotateMEK",
|
||||||
|
Handler: _BarrierService_RotateMEK_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RotateKey",
|
||||||
|
Handler: _BarrierService_RotateKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Migrate",
|
||||||
|
Handler: _BarrierService_Migrate_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/metacrypt/v2/barrier.proto",
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrSealed = errors.New("barrier: sealed")
|
ErrSealed = errors.New("barrier: sealed")
|
||||||
ErrNotFound = errors.New("barrier: entry not found")
|
ErrNotFound = errors.New("barrier: entry not found")
|
||||||
|
ErrKeyNotFound = errors.New("barrier: key not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Barrier is the encrypted storage barrier interface.
|
// Barrier is the encrypted storage barrier interface.
|
||||||
@@ -36,10 +37,19 @@ type Barrier interface {
|
|||||||
List(ctx context.Context, prefix string) ([]string, error)
|
List(ctx context.Context, prefix string) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyInfo holds metadata about a barrier key (DEK).
|
||||||
|
type KeyInfo struct {
|
||||||
|
KeyID string `json:"key_id"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
RotatedAt string `json:"rotated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// AESGCMBarrier implements Barrier using AES-256-GCM encryption.
|
// AESGCMBarrier implements Barrier using AES-256-GCM encryption.
|
||||||
type AESGCMBarrier struct {
|
type AESGCMBarrier struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
mek []byte
|
mek []byte
|
||||||
|
keys map[string][]byte // key_id → plaintext DEK
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +61,56 @@ func NewAESGCMBarrier(db *sql.DB) *AESGCMBarrier {
|
|||||||
func (b *AESGCMBarrier) Unseal(mek []byte) error {
|
func (b *AESGCMBarrier) Unseal(mek []byte) error {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
k := make([]byte, len(mek))
|
k := make([]byte, len(mek))
|
||||||
copy(k, mek)
|
copy(k, mek)
|
||||||
b.mek = k
|
b.mek = k
|
||||||
|
b.keys = make(map[string][]byte)
|
||||||
|
|
||||||
|
// Load DEKs from barrier_keys table.
|
||||||
|
if err := b.loadKeys(); err != nil {
|
||||||
|
// If the table doesn't exist yet (pre-migration), that's OK.
|
||||||
|
// The barrier will use MEK directly for v1 entries.
|
||||||
|
b.keys = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadKeys decrypts all DEKs from the barrier_keys table into memory.
|
||||||
|
// Caller must hold b.mu.
|
||||||
|
func (b *AESGCMBarrier) loadKeys() error {
|
||||||
|
rows, err := b.db.Query("SELECT key_id, encrypted_dek FROM barrier_keys")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var keyID string
|
||||||
|
var encDEK []byte
|
||||||
|
if err := rows.Scan(&keyID, &encDEK); err != nil {
|
||||||
|
return fmt.Errorf("barrier: scan key %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
dek, err := crypto.Decrypt(b.mek, encDEK, []byte(keyID))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("barrier: decrypt key %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
b.keys[keyID] = dek
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *AESGCMBarrier) Seal() error {
|
func (b *AESGCMBarrier) Seal() error {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Zeroize all DEKs.
|
||||||
|
for _, dek := range b.keys {
|
||||||
|
crypto.Zeroize(dek)
|
||||||
|
}
|
||||||
|
b.keys = nil
|
||||||
|
|
||||||
if b.mek != nil {
|
if b.mek != nil {
|
||||||
crypto.Zeroize(b.mek)
|
crypto.Zeroize(b.mek)
|
||||||
b.mek = nil
|
b.mek = nil
|
||||||
@@ -73,9 +124,22 @@ func (b *AESGCMBarrier) IsSealed() bool {
|
|||||||
return b.mek == nil
|
return b.mek == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveKeyID determines the key ID for a given barrier path.
|
||||||
|
func resolveKeyID(path string) string {
|
||||||
|
// Paths under engine/{type}/{mount}/... use per-engine DEKs.
|
||||||
|
if strings.HasPrefix(path, "engine/") {
|
||||||
|
parts := strings.SplitN(path, "/", 4) // engine/{type}/{mount}/...
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
return "engine/" + parts[1] + "/" + parts[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "system"
|
||||||
|
}
|
||||||
|
|
||||||
func (b *AESGCMBarrier) Get(ctx context.Context, path string) ([]byte, error) {
|
func (b *AESGCMBarrier) Get(ctx context.Context, path string) ([]byte, error) {
|
||||||
b.mu.RLock()
|
b.mu.RLock()
|
||||||
mek := b.mek
|
mek := b.mek
|
||||||
|
keys := b.keys
|
||||||
b.mu.RUnlock()
|
b.mu.RUnlock()
|
||||||
if mek == nil {
|
if mek == nil {
|
||||||
return nil, ErrSealed
|
return nil, ErrSealed
|
||||||
@@ -91,22 +155,52 @@ func (b *AESGCMBarrier) Get(ctx context.Context, path string) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("barrier: get %q: %w", path, err)
|
return nil, fmt.Errorf("barrier: get %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plaintext, err := crypto.Decrypt(mek, encrypted)
|
// Check version byte to determine decryption strategy.
|
||||||
|
if len(encrypted) > 0 && encrypted[0] == crypto.BarrierVersionV2 {
|
||||||
|
keyID, err := crypto.ExtractKeyID(encrypted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("barrier: extract key ID %q: %w", path, err)
|
||||||
|
}
|
||||||
|
dek, ok := keys[keyID]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("barrier: %w: %q for path %q", ErrKeyNotFound, keyID, path)
|
||||||
|
}
|
||||||
|
pt, _, err := crypto.DecryptV2(dek, encrypted, []byte(path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("barrier: decrypt %q: %w", path, err)
|
return nil, fmt.Errorf("barrier: decrypt %q: %w", path, err)
|
||||||
}
|
}
|
||||||
return plaintext, nil
|
return pt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1 ciphertext — use MEK directly (backward compat).
|
||||||
|
pt, err := crypto.Decrypt(mek, encrypted, []byte(path))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("barrier: decrypt %q: %w", path, err)
|
||||||
|
}
|
||||||
|
return pt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *AESGCMBarrier) Put(ctx context.Context, path string, value []byte) error {
|
func (b *AESGCMBarrier) Put(ctx context.Context, path string, value []byte) error {
|
||||||
b.mu.RLock()
|
b.mu.RLock()
|
||||||
mek := b.mek
|
mek := b.mek
|
||||||
|
keys := b.keys
|
||||||
b.mu.RUnlock()
|
b.mu.RUnlock()
|
||||||
if mek == nil {
|
if mek == nil {
|
||||||
return ErrSealed
|
return ErrSealed
|
||||||
}
|
}
|
||||||
|
|
||||||
encrypted, err := crypto.Encrypt(mek, value)
|
keyID := resolveKeyID(path)
|
||||||
|
|
||||||
|
var encrypted []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if dek, ok := keys[keyID]; ok {
|
||||||
|
// Use v2 format with the appropriate DEK.
|
||||||
|
encrypted, err = crypto.EncryptV2(dek, keyID, value, []byte(path))
|
||||||
|
} else {
|
||||||
|
// No DEK registered for this key ID — fall back to MEK with v1 format.
|
||||||
|
encrypted, err = crypto.Encrypt(mek, value, []byte(path))
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("barrier: encrypt %q: %w", path, err)
|
return fmt.Errorf("barrier: encrypt %q: %w", path, err)
|
||||||
}
|
}
|
||||||
@@ -159,9 +253,394 @@ func (b *AESGCMBarrier) List(ctx context.Context, prefix string) ([]string, erro
|
|||||||
if err := rows.Scan(&p); err != nil {
|
if err := rows.Scan(&p); err != nil {
|
||||||
return nil, fmt.Errorf("barrier: list scan: %w", err)
|
return nil, fmt.Errorf("barrier: list scan: %w", err)
|
||||||
}
|
}
|
||||||
// Strip the prefix and return just the next segment.
|
|
||||||
remainder := strings.TrimPrefix(p, prefix)
|
remainder := strings.TrimPrefix(p, prefix)
|
||||||
paths = append(paths, remainder)
|
paths = append(paths, remainder)
|
||||||
}
|
}
|
||||||
return paths, rows.Err()
|
return paths, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateKey generates a new DEK for the given key ID, wraps it with MEK,
|
||||||
|
// and stores it in the barrier_keys table.
|
||||||
|
func (b *AESGCMBarrier) CreateKey(ctx context.Context, keyID string) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.mek == nil {
|
||||||
|
return ErrSealed
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := b.keys[keyID]; exists {
|
||||||
|
return nil // Already exists.
|
||||||
|
}
|
||||||
|
|
||||||
|
dek, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("barrier: generate DEK %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encDEK, err := crypto.Encrypt(b.mek, dek, []byte(keyID))
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(dek)
|
||||||
|
return fmt.Errorf("barrier: wrap DEK %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO barrier_keys (key_id, version, encrypted_dek)
|
||||||
|
VALUES (?, 1, ?)
|
||||||
|
ON CONFLICT(key_id) DO NOTHING`,
|
||||||
|
keyID, encDEK)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(dek)
|
||||||
|
return fmt.Errorf("barrier: store DEK %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.keys[keyID] = dek
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateKey generates a new DEK for the given key ID and re-encrypts all
|
||||||
|
// barrier entries under that key ID's prefix with the new DEK.
|
||||||
|
func (b *AESGCMBarrier) RotateKey(ctx context.Context, keyID string) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.mek == nil {
|
||||||
|
return ErrSealed
|
||||||
|
}
|
||||||
|
|
||||||
|
oldDEK, ok := b.keys[keyID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("barrier: %w: %q", ErrKeyNotFound, keyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new DEK.
|
||||||
|
newDEK, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("barrier: generate DEK: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap new DEK with MEK.
|
||||||
|
encDEK, err := crypto.Encrypt(b.mek, newDEK, []byte(keyID))
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: wrap DEK: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the prefix for entries encrypted with this key.
|
||||||
|
prefix := keyID + "/"
|
||||||
|
if keyID == "system" {
|
||||||
|
// System key covers non-engine paths. Re-encrypt everything
|
||||||
|
// that doesn't start with "engine/".
|
||||||
|
prefix = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encrypt all entries under this key ID.
|
||||||
|
tx, err := b.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
// Update the key in barrier_keys.
|
||||||
|
_, err = tx.ExecContext(ctx, `
|
||||||
|
UPDATE barrier_keys SET encrypted_dek = ?, version = version + 1, rotated_at = datetime('now')
|
||||||
|
WHERE key_id = ?`, encDEK, keyID)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: update key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and re-encrypt entries.
|
||||||
|
var query string
|
||||||
|
var args []interface{}
|
||||||
|
if keyID == "system" {
|
||||||
|
query = "SELECT path, value FROM barrier_entries WHERE path NOT LIKE 'engine/%'"
|
||||||
|
} else {
|
||||||
|
query = "SELECT path, value FROM barrier_entries WHERE path LIKE ?"
|
||||||
|
args = append(args, prefix+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: query entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
path string
|
||||||
|
value []byte
|
||||||
|
}
|
||||||
|
var entries []entry
|
||||||
|
for rows.Next() {
|
||||||
|
var e entry
|
||||||
|
if err := rows.Scan(&e.path, &e.value); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: scan entry: %w", err)
|
||||||
|
}
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: rows error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
// Decrypt with old DEK (handle v1 or v2).
|
||||||
|
var plaintext []byte
|
||||||
|
if len(e.value) > 0 && e.value[0] == crypto.BarrierVersionV2 {
|
||||||
|
pt, _, decErr := crypto.DecryptV2(oldDEK, e.value, []byte(e.path))
|
||||||
|
if decErr != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: decrypt %q during rotation: %w", e.path, decErr)
|
||||||
|
}
|
||||||
|
plaintext = pt
|
||||||
|
} else {
|
||||||
|
// v1: encrypted with MEK.
|
||||||
|
pt, decErr := crypto.Decrypt(b.mek, e.value, []byte(e.path))
|
||||||
|
if decErr != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: decrypt v1 %q during rotation: %w", e.path, decErr)
|
||||||
|
}
|
||||||
|
plaintext = pt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encrypt with new DEK using v2 format.
|
||||||
|
newCiphertext, encErr := crypto.EncryptV2(newDEK, keyID, plaintext, []byte(e.path))
|
||||||
|
if encErr != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: re-encrypt %q: %w", e.path, encErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx,
|
||||||
|
"UPDATE barrier_entries SET value = ?, updated_at = datetime('now') WHERE path = ?",
|
||||||
|
newCiphertext, e.path)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: update %q: %w", e.path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
crypto.Zeroize(newDEK)
|
||||||
|
return fmt.Errorf("barrier: commit rotation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap the in-memory key.
|
||||||
|
crypto.Zeroize(oldDEK)
|
||||||
|
b.keys[keyID] = newDEK
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListKeys returns metadata about all registered barrier keys.
|
||||||
|
func (b *AESGCMBarrier) ListKeys(ctx context.Context) ([]KeyInfo, error) {
|
||||||
|
b.mu.RLock()
|
||||||
|
mek := b.mek
|
||||||
|
b.mu.RUnlock()
|
||||||
|
if mek == nil {
|
||||||
|
return nil, ErrSealed
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := b.db.QueryContext(ctx,
|
||||||
|
"SELECT key_id, version, created_at, rotated_at FROM barrier_keys ORDER BY key_id")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("barrier: list keys: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var keys []KeyInfo
|
||||||
|
for rows.Next() {
|
||||||
|
var ki KeyInfo
|
||||||
|
if err := rows.Scan(&ki.KeyID, &ki.Version, &ki.CreatedAt, &ki.RotatedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("barrier: scan key info: %w", err)
|
||||||
|
}
|
||||||
|
keys = append(keys, ki)
|
||||||
|
}
|
||||||
|
return keys, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateToV2 creates per-engine DEKs and re-encrypts entries from v1
|
||||||
|
// (MEK-encrypted) to v2 (DEK-encrypted) format. On first call after upgrade,
|
||||||
|
// it creates a "system" DEK equal to the MEK for zero-cost backward compat,
|
||||||
|
// then creates per-engine DEKs and re-encrypts those entries.
|
||||||
|
func (b *AESGCMBarrier) MigrateToV2(ctx context.Context) (int, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.mek == nil {
|
||||||
|
return 0, ErrSealed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the "system" key exists.
|
||||||
|
if _, ok := b.keys["system"]; !ok {
|
||||||
|
if err := b.createKeyLocked(ctx, "system"); err != nil {
|
||||||
|
return 0, fmt.Errorf("barrier: create system DEK: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all entries still in v1 format.
|
||||||
|
rows, err := b.db.QueryContext(ctx, "SELECT path, value FROM barrier_entries")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("barrier: query entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
path string
|
||||||
|
value []byte
|
||||||
|
}
|
||||||
|
var toMigrate []entry
|
||||||
|
for rows.Next() {
|
||||||
|
var e entry
|
||||||
|
if err := rows.Scan(&e.path, &e.value); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return 0, fmt.Errorf("barrier: scan: %w", err)
|
||||||
|
}
|
||||||
|
if len(e.value) > 0 && e.value[0] == crypto.BarrierVersionV1 {
|
||||||
|
toMigrate = append(toMigrate, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toMigrate) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := b.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("barrier: begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
migrated := 0
|
||||||
|
for _, e := range toMigrate {
|
||||||
|
// Decrypt with MEK (v1).
|
||||||
|
plaintext, decErr := crypto.Decrypt(b.mek, e.value, []byte(e.path))
|
||||||
|
if decErr != nil {
|
||||||
|
return migrated, fmt.Errorf("barrier: decrypt %q: %w", e.path, decErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyID := resolveKeyID(e.path)
|
||||||
|
|
||||||
|
// Ensure the DEK exists for this key ID.
|
||||||
|
if _, ok := b.keys[keyID]; !ok {
|
||||||
|
if err := b.createKeyLockedTx(ctx, tx, keyID); err != nil {
|
||||||
|
return migrated, fmt.Errorf("barrier: create DEK %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dek := b.keys[keyID]
|
||||||
|
newCiphertext, encErr := crypto.EncryptV2(dek, keyID, plaintext, []byte(e.path))
|
||||||
|
if encErr != nil {
|
||||||
|
return migrated, fmt.Errorf("barrier: encrypt v2 %q: %w", e.path, encErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx,
|
||||||
|
"UPDATE barrier_entries SET value = ?, updated_at = datetime('now') WHERE path = ?",
|
||||||
|
newCiphertext, e.path)
|
||||||
|
if err != nil {
|
||||||
|
return migrated, fmt.Errorf("barrier: update %q: %w", e.path, err)
|
||||||
|
}
|
||||||
|
migrated++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return migrated, fmt.Errorf("barrier: commit migration: %w", err)
|
||||||
|
}
|
||||||
|
return migrated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createKeyLocked generates and stores a new DEK. Caller must hold b.mu.
|
||||||
|
func (b *AESGCMBarrier) createKeyLocked(ctx context.Context, keyID string) error {
|
||||||
|
dek, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encDEK, err := crypto.Encrypt(b.mek, dek, []byte(keyID))
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(dek)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO barrier_keys (key_id, version, encrypted_dek)
|
||||||
|
VALUES (?, 1, ?) ON CONFLICT(key_id) DO NOTHING`, keyID, encDEK)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(dek)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.keys[keyID] = dek
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createKeyLockedTx is like createKeyLocked but uses an existing transaction.
|
||||||
|
func (b *AESGCMBarrier) createKeyLockedTx(ctx context.Context, tx *sql.Tx, keyID string) error {
|
||||||
|
dek, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encDEK, err := crypto.Encrypt(b.mek, dek, []byte(keyID))
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(dek)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO barrier_keys (key_id, version, encrypted_dek)
|
||||||
|
VALUES (?, 1, ?) ON CONFLICT(key_id) DO NOTHING`, keyID, encDEK)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(dek)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.keys[keyID] = dek
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReWrapKeys re-encrypts all DEKs with a new MEK. Called during MEK rotation.
|
||||||
|
// The new MEK is already set in b.mek by the caller.
|
||||||
|
func (b *AESGCMBarrier) ReWrapKeys(ctx context.Context, newMEK []byte) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.mek == nil {
|
||||||
|
return ErrSealed
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := b.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("barrier: begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
for keyID, dek := range b.keys {
|
||||||
|
encDEK, err := crypto.Encrypt(newMEK, dek, []byte(keyID))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("barrier: re-wrap key %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
_, err = tx.ExecContext(ctx,
|
||||||
|
"UPDATE barrier_keys SET encrypted_dek = ?, rotated_at = datetime('now') WHERE key_id = ?",
|
||||||
|
encDEK, keyID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("barrier: update key %q: %w", keyID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("barrier: commit re-wrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the MEK in memory.
|
||||||
|
crypto.Zeroize(b.mek)
|
||||||
|
k := make([]byte, len(newMEK))
|
||||||
|
copy(k, newMEK)
|
||||||
|
b.mek = k
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,3 +158,309 @@ func TestBarrierOverwrite(t *testing.T) {
|
|||||||
t.Fatalf("overwrite: got %q, want %q", got, "v2")
|
t.Fatalf("overwrite: got %q, want %q", got, "v2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- DEK / Key Registry Tests ---
|
||||||
|
|
||||||
|
func TestBarrierCreateKey(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
if err := b.CreateKey(ctx, "engine/ca/prod"); err != nil {
|
||||||
|
t.Fatalf("CreateKey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate should be a no-op.
|
||||||
|
if err := b.CreateKey(ctx, "engine/ca/prod"); err != nil {
|
||||||
|
t.Fatalf("CreateKey duplicate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := b.ListKeys(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListKeys: %v", err)
|
||||||
|
}
|
||||||
|
if len(keys) != 1 {
|
||||||
|
t.Fatalf("expected 1 key, got %d", len(keys))
|
||||||
|
}
|
||||||
|
if keys[0].KeyID != "engine/ca/prod" {
|
||||||
|
t.Fatalf("key ID: got %q, want %q", keys[0].KeyID, "engine/ca/prod")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierDEKEncryption(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
// Create a DEK for ca/prod.
|
||||||
|
_ = b.CreateKey(ctx, "engine/ca/prod")
|
||||||
|
|
||||||
|
// Write data under the engine path — should use DEK.
|
||||||
|
data := []byte("engine secret data")
|
||||||
|
if err := b.Put(ctx, "engine/ca/prod/config.json", data); err != nil {
|
||||||
|
t.Fatalf("Put: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := b.Get(ctx, "engine/ca/prod/config.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != string(data) {
|
||||||
|
t.Fatalf("roundtrip: got %q, want %q", got, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the raw ciphertext is v2 format.
|
||||||
|
var raw []byte
|
||||||
|
err = b.db.QueryRowContext(ctx,
|
||||||
|
"SELECT value FROM barrier_entries WHERE path = ?",
|
||||||
|
"engine/ca/prod/config.json").Scan(&raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read raw: %v", err)
|
||||||
|
}
|
||||||
|
if raw[0] != crypto.BarrierVersionV2 {
|
||||||
|
t.Fatalf("expected v2 ciphertext, got version %d", raw[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierV1FallbackWithoutDEK(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
// Write system data without any DEK — should use v1 with MEK.
|
||||||
|
data := []byte("system data")
|
||||||
|
if err := b.Put(ctx, "policy/rule1", data); err != nil {
|
||||||
|
t.Fatalf("Put: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := b.Get(ctx, "policy/rule1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != string(data) {
|
||||||
|
t.Fatalf("roundtrip: got %q, want %q", got, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierSystemDEK(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
// Create system DEK.
|
||||||
|
_ = b.CreateKey(ctx, "system")
|
||||||
|
|
||||||
|
// Write system data — should use system DEK with v2 format.
|
||||||
|
data := []byte("system with dek")
|
||||||
|
if err := b.Put(ctx, "policy/rule1", data); err != nil {
|
||||||
|
t.Fatalf("Put: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := b.Get(ctx, "policy/rule1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != string(data) {
|
||||||
|
t.Fatalf("roundtrip: got %q, want %q", got, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify v2 format.
|
||||||
|
var raw []byte
|
||||||
|
_ = b.db.QueryRowContext(ctx,
|
||||||
|
"SELECT value FROM barrier_entries WHERE path = ?",
|
||||||
|
"policy/rule1").Scan(&raw)
|
||||||
|
if raw[0] != crypto.BarrierVersionV2 {
|
||||||
|
t.Fatalf("expected v2 ciphertext, got version %d", raw[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierRotateKey(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
_ = b.CreateKey(ctx, "engine/ca/prod")
|
||||||
|
|
||||||
|
// Write some data.
|
||||||
|
_ = b.Put(ctx, "engine/ca/prod/cert1", []byte("cert-data-1"))
|
||||||
|
_ = b.Put(ctx, "engine/ca/prod/cert2", []byte("cert-data-2"))
|
||||||
|
|
||||||
|
// Rotate the key.
|
||||||
|
if err := b.RotateKey(ctx, "engine/ca/prod"); err != nil {
|
||||||
|
t.Fatalf("RotateKey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data should still be readable.
|
||||||
|
got, err := b.Get(ctx, "engine/ca/prod/cert1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get after rotation: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "cert-data-1" {
|
||||||
|
t.Fatalf("data corrupted after rotation: %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
got2, err := b.Get(ctx, "engine/ca/prod/cert2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get after rotation: %v", err)
|
||||||
|
}
|
||||||
|
if string(got2) != "cert-data-2" {
|
||||||
|
t.Fatalf("data corrupted after rotation: %q", got2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check key version incremented.
|
||||||
|
keys, _ := b.ListKeys(ctx)
|
||||||
|
for _, k := range keys {
|
||||||
|
if k.KeyID == "engine/ca/prod" && k.Version != 2 {
|
||||||
|
t.Fatalf("expected version 2 after rotation, got %d", k.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierRotateKeyNotFound(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
err := b.RotateKey(ctx, "nonexistent")
|
||||||
|
if !errors.Is(err, ErrKeyNotFound) {
|
||||||
|
t.Fatalf("expected ErrKeyNotFound, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierMigrateToV2(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
// Write v1 data (no DEKs registered, so it uses MEK).
|
||||||
|
_ = b.Put(ctx, "policy/rule1", []byte("policy-data"))
|
||||||
|
_ = b.Put(ctx, "engine/ca/prod/config", []byte("ca-config"))
|
||||||
|
_ = b.Put(ctx, "engine/transit/main/key1", []byte("transit-key"))
|
||||||
|
|
||||||
|
// Migrate.
|
||||||
|
migrated, err := b.MigrateToV2(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MigrateToV2: %v", err)
|
||||||
|
}
|
||||||
|
if migrated != 3 {
|
||||||
|
t.Fatalf("expected 3 entries migrated, got %d", migrated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All data should still be readable.
|
||||||
|
got, err := b.Get(ctx, "policy/rule1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get policy after migration: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "policy-data" {
|
||||||
|
t.Fatalf("policy data: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err = b.Get(ctx, "engine/ca/prod/config")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get engine data after migration: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "ca-config" {
|
||||||
|
t.Fatalf("engine data: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Running again should migrate 0 (all already v2).
|
||||||
|
migrated2, err := b.MigrateToV2(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MigrateToV2 second run: %v", err)
|
||||||
|
}
|
||||||
|
if migrated2 != 0 {
|
||||||
|
t.Fatalf("expected 0 entries on second migration, got %d", migrated2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierSealUnsealPreservesDEKs(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
// Create DEK and write data.
|
||||||
|
_ = b.CreateKey(ctx, "engine/ca/prod")
|
||||||
|
_ = b.Put(ctx, "engine/ca/prod/secret", []byte("my-secret"))
|
||||||
|
|
||||||
|
// Seal and unseal.
|
||||||
|
_ = b.Seal()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
// Data should still be readable (DEKs reloaded from barrier_keys).
|
||||||
|
got, err := b.Get(ctx, "engine/ca/prod/secret")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get after seal/unseal: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "my-secret" {
|
||||||
|
t.Fatalf("data after seal/unseal: got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBarrierReWrapKeys(t *testing.T) {
|
||||||
|
b, cleanup := setupBarrier(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mek, _ := crypto.GenerateKey()
|
||||||
|
_ = b.Unseal(mek)
|
||||||
|
|
||||||
|
_ = b.CreateKey(ctx, "system")
|
||||||
|
_ = b.CreateKey(ctx, "engine/ca/prod")
|
||||||
|
|
||||||
|
_ = b.Put(ctx, "policy/rule1", []byte("policy"))
|
||||||
|
_ = b.Put(ctx, "engine/ca/prod/cert", []byte("cert"))
|
||||||
|
|
||||||
|
// Re-wrap with new MEK.
|
||||||
|
newMEK, _ := crypto.GenerateKey()
|
||||||
|
if err := b.ReWrapKeys(ctx, newMEK); err != nil {
|
||||||
|
t.Fatalf("ReWrapKeys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data should still be readable.
|
||||||
|
got, _ := b.Get(ctx, "policy/rule1")
|
||||||
|
if string(got) != "policy" {
|
||||||
|
t.Fatalf("policy after rewrap: got %q", got)
|
||||||
|
}
|
||||||
|
got2, _ := b.Get(ctx, "engine/ca/prod/cert")
|
||||||
|
if string(got2) != "cert" {
|
||||||
|
t.Fatalf("cert after rewrap: got %q", got2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seal and unseal with new MEK should work.
|
||||||
|
_ = b.Seal()
|
||||||
|
if err := b.Unseal(newMEK); err != nil {
|
||||||
|
t.Fatalf("Unseal with new MEK: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got3, err := b.Get(ctx, "engine/ca/prod/cert")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get after unseal with new MEK: %v", err)
|
||||||
|
}
|
||||||
|
if string(got3) != "cert" {
|
||||||
|
t.Fatalf("data after new MEK unseal: got %q", got3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,8 +20,16 @@ const (
|
|||||||
// SaltSize is the size of Argon2id salts in bytes.
|
// SaltSize is the size of Argon2id salts in bytes.
|
||||||
SaltSize = 32
|
SaltSize = 32
|
||||||
|
|
||||||
// BarrierVersion is the version byte prefix for encrypted barrier entries.
|
// BarrierVersionV1 is the v1 format: [version][nonce][ciphertext+tag].
|
||||||
BarrierVersion byte = 0x01
|
BarrierVersionV1 byte = 0x01
|
||||||
|
// BarrierVersionV2 is the v2 format: [version][key_id_len][key_id][nonce][ciphertext+tag].
|
||||||
|
BarrierVersionV2 byte = 0x02
|
||||||
|
|
||||||
|
// BarrierVersion is kept for backward compatibility (alias for V1).
|
||||||
|
BarrierVersion = BarrierVersionV1
|
||||||
|
|
||||||
|
// MaxKeyIDLen is the maximum length of a key ID in the v2 format.
|
||||||
|
MaxKeyIDLen = 255
|
||||||
|
|
||||||
// Default Argon2id parameters.
|
// Default Argon2id parameters.
|
||||||
DefaultArgon2Time = 3
|
DefaultArgon2Time = 3
|
||||||
@@ -32,6 +40,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
ErrInvalidCiphertext = errors.New("crypto: invalid ciphertext")
|
ErrInvalidCiphertext = errors.New("crypto: invalid ciphertext")
|
||||||
ErrDecryptionFailed = errors.New("crypto: decryption failed")
|
ErrDecryptionFailed = errors.New("crypto: decryption failed")
|
||||||
|
ErrKeyIDTooLong = errors.New("crypto: key ID exceeds maximum length")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Argon2Params holds Argon2id KDF parameters.
|
// Argon2Params holds Argon2id KDF parameters.
|
||||||
@@ -74,8 +83,10 @@ func GenerateSalt() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt encrypts plaintext with AES-256-GCM using the given key.
|
// Encrypt encrypts plaintext with AES-256-GCM using the given key.
|
||||||
|
// The additionalData parameter is authenticated but not encrypted (AAD);
|
||||||
|
// pass nil when no binding context is needed.
|
||||||
// Returns: [version byte][12-byte nonce][ciphertext+tag]
|
// Returns: [version byte][12-byte nonce][ciphertext+tag]
|
||||||
func Encrypt(key, plaintext []byte) ([]byte, error) {
|
func Encrypt(key, plaintext, additionalData []byte) ([]byte, error) {
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("crypto: new cipher: %w", err)
|
return nil, fmt.Errorf("crypto: new cipher: %w", err)
|
||||||
@@ -88,7 +99,7 @@ func Encrypt(key, plaintext []byte) ([]byte, error) {
|
|||||||
if _, err := rand.Read(nonce); err != nil {
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
return nil, fmt.Errorf("crypto: generate nonce: %w", err)
|
return nil, fmt.Errorf("crypto: generate nonce: %w", err)
|
||||||
}
|
}
|
||||||
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
ciphertext := gcm.Seal(nil, nonce, plaintext, additionalData)
|
||||||
|
|
||||||
// Format: [version][nonce][ciphertext+tag]
|
// Format: [version][nonce][ciphertext+tag]
|
||||||
result := make([]byte, 1+NonceSize+len(ciphertext))
|
result := make([]byte, 1+NonceSize+len(ciphertext))
|
||||||
@@ -99,7 +110,8 @@ func Encrypt(key, plaintext []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt decrypts ciphertext produced by Encrypt.
|
// Decrypt decrypts ciphertext produced by Encrypt.
|
||||||
func Decrypt(key, data []byte) ([]byte, error) {
|
// The additionalData must match the value provided during encryption.
|
||||||
|
func Decrypt(key, data, additionalData []byte) ([]byte, error) {
|
||||||
if len(data) < 1+NonceSize+aes.BlockSize {
|
if len(data) < 1+NonceSize+aes.BlockSize {
|
||||||
return nil, ErrInvalidCiphertext
|
return nil, ErrInvalidCiphertext
|
||||||
}
|
}
|
||||||
@@ -117,13 +129,114 @@ func Decrypt(key, data []byte) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("crypto: new gcm: %w", err)
|
return nil, fmt.Errorf("crypto: new gcm: %w", err)
|
||||||
}
|
}
|
||||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, additionalData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrDecryptionFailed
|
return nil, ErrDecryptionFailed
|
||||||
}
|
}
|
||||||
return plaintext, nil
|
return plaintext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncryptV2 encrypts plaintext with AES-256-GCM, embedding a key ID in the ciphertext.
|
||||||
|
// Format: [0x02][key_id_len:1][key_id:N][nonce:12][ciphertext+tag]
|
||||||
|
func EncryptV2(key []byte, keyID string, plaintext, additionalData []byte) ([]byte, error) {
|
||||||
|
if len(keyID) > MaxKeyIDLen {
|
||||||
|
return nil, ErrKeyIDTooLong
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("crypto: new cipher: %w", err)
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("crypto: new gcm: %w", err)
|
||||||
|
}
|
||||||
|
nonce := make([]byte, NonceSize)
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return nil, fmt.Errorf("crypto: generate nonce: %w", err)
|
||||||
|
}
|
||||||
|
ciphertext := gcm.Seal(nil, nonce, plaintext, additionalData)
|
||||||
|
|
||||||
|
kidLen := len(keyID)
|
||||||
|
// Format: [version][key_id_len][key_id][nonce][ciphertext+tag]
|
||||||
|
result := make([]byte, 1+1+kidLen+NonceSize+len(ciphertext))
|
||||||
|
result[0] = BarrierVersionV2
|
||||||
|
result[1] = byte(kidLen)
|
||||||
|
copy(result[2:2+kidLen], keyID)
|
||||||
|
copy(result[2+kidLen:2+kidLen+NonceSize], nonce)
|
||||||
|
copy(result[2+kidLen+NonceSize:], ciphertext)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptV2 decrypts ciphertext that may be in v1 or v2 format.
|
||||||
|
// For v2 format, it extracts the key ID and returns it alongside the plaintext.
|
||||||
|
// For v1 format, it returns an empty key ID.
|
||||||
|
func DecryptV2(key, data, additionalData []byte) (plaintext []byte, keyID string, err error) {
|
||||||
|
if len(data) < 1 {
|
||||||
|
return nil, "", ErrInvalidCiphertext
|
||||||
|
}
|
||||||
|
|
||||||
|
switch data[0] {
|
||||||
|
case BarrierVersionV1:
|
||||||
|
pt, err := Decrypt(key, data, additionalData)
|
||||||
|
return pt, "", err
|
||||||
|
|
||||||
|
case BarrierVersionV2:
|
||||||
|
if len(data) < 2 {
|
||||||
|
return nil, "", ErrInvalidCiphertext
|
||||||
|
}
|
||||||
|
kidLen := int(data[1])
|
||||||
|
headerLen := 2 + kidLen
|
||||||
|
if len(data) < headerLen+NonceSize+aes.BlockSize {
|
||||||
|
return nil, "", ErrInvalidCiphertext
|
||||||
|
}
|
||||||
|
|
||||||
|
keyID = string(data[2 : 2+kidLen])
|
||||||
|
nonce := data[headerLen : headerLen+NonceSize]
|
||||||
|
ciphertext := data[headerLen+NonceSize:]
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("crypto: new cipher: %w", err)
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("crypto: new gcm: %w", err)
|
||||||
|
}
|
||||||
|
pt, err := gcm.Open(nil, nonce, ciphertext, additionalData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", ErrDecryptionFailed
|
||||||
|
}
|
||||||
|
return pt, keyID, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, "", fmt.Errorf("crypto: unsupported version: %d", data[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractKeyID returns the key ID from a v2 ciphertext without decrypting.
|
||||||
|
// Returns empty string for v1 ciphertext.
|
||||||
|
func ExtractKeyID(data []byte) (string, error) {
|
||||||
|
if len(data) < 1 {
|
||||||
|
return "", ErrInvalidCiphertext
|
||||||
|
}
|
||||||
|
switch data[0] {
|
||||||
|
case BarrierVersionV1:
|
||||||
|
return "", nil
|
||||||
|
case BarrierVersionV2:
|
||||||
|
if len(data) < 2 {
|
||||||
|
return "", ErrInvalidCiphertext
|
||||||
|
}
|
||||||
|
kidLen := int(data[1])
|
||||||
|
if len(data) < 2+kidLen {
|
||||||
|
return "", ErrInvalidCiphertext
|
||||||
|
}
|
||||||
|
return string(data[2 : 2+kidLen]), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("crypto: unsupported version: %d", data[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Zeroize overwrites a byte slice with zeros.
|
// Zeroize overwrites a byte slice with zeros.
|
||||||
func Zeroize(b []byte) {
|
func Zeroize(b []byte) {
|
||||||
for i := range b {
|
for i := range b {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestEncryptDecrypt(t *testing.T) {
|
|||||||
key, _ := GenerateKey()
|
key, _ := GenerateKey()
|
||||||
plaintext := []byte("hello, metacrypt!")
|
plaintext := []byte("hello, metacrypt!")
|
||||||
|
|
||||||
ciphertext, err := Encrypt(key, plaintext)
|
ciphertext, err := Encrypt(key, plaintext, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Encrypt: %v", err)
|
t.Fatalf("Encrypt: %v", err)
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func TestEncryptDecrypt(t *testing.T) {
|
|||||||
t.Fatalf("version byte: got %d, want %d", ciphertext[0], BarrierVersion)
|
t.Fatalf("version byte: got %d, want %d", ciphertext[0], BarrierVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypted, err := Decrypt(key, ciphertext)
|
decrypted, err := Decrypt(key, ciphertext, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Decrypt: %v", err)
|
t.Fatalf("Decrypt: %v", err)
|
||||||
}
|
}
|
||||||
@@ -54,13 +54,45 @@ func TestEncryptDecrypt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptWithAAD(t *testing.T) {
|
||||||
|
key, _ := GenerateKey()
|
||||||
|
plaintext := []byte("hello, metacrypt!")
|
||||||
|
aad := []byte("engine/ca/pki/root/cert.pem")
|
||||||
|
|
||||||
|
ciphertext, err := Encrypt(key, plaintext, aad)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encrypt with AAD: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt with correct AAD succeeds.
|
||||||
|
decrypted, err := Decrypt(key, ciphertext, aad)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decrypt with AAD: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(plaintext, decrypted) {
|
||||||
|
t.Fatalf("roundtrip failed: got %q, want %q", decrypted, plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt with wrong AAD fails.
|
||||||
|
_, err = Decrypt(key, ciphertext, []byte("wrong/path"))
|
||||||
|
if !errors.Is(err, ErrDecryptionFailed) {
|
||||||
|
t.Fatalf("expected ErrDecryptionFailed with wrong AAD, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt with nil AAD fails.
|
||||||
|
_, err = Decrypt(key, ciphertext, nil)
|
||||||
|
if !errors.Is(err, ErrDecryptionFailed) {
|
||||||
|
t.Fatalf("expected ErrDecryptionFailed with nil AAD, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDecryptWrongKey(t *testing.T) {
|
func TestDecryptWrongKey(t *testing.T) {
|
||||||
key1, _ := GenerateKey()
|
key1, _ := GenerateKey()
|
||||||
key2, _ := GenerateKey()
|
key2, _ := GenerateKey()
|
||||||
plaintext := []byte("secret data")
|
plaintext := []byte("secret data")
|
||||||
|
|
||||||
ciphertext, _ := Encrypt(key1, plaintext)
|
ciphertext, _ := Encrypt(key1, plaintext, nil)
|
||||||
_, err := Decrypt(key2, ciphertext)
|
_, err := Decrypt(key2, ciphertext, nil)
|
||||||
if !errors.Is(err, ErrDecryptionFailed) {
|
if !errors.Is(err, ErrDecryptionFailed) {
|
||||||
t.Fatalf("expected ErrDecryptionFailed, got: %v", err)
|
t.Fatalf("expected ErrDecryptionFailed, got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -68,7 +100,7 @@ func TestDecryptWrongKey(t *testing.T) {
|
|||||||
|
|
||||||
func TestDecryptInvalidCiphertext(t *testing.T) {
|
func TestDecryptInvalidCiphertext(t *testing.T) {
|
||||||
key, _ := GenerateKey()
|
key, _ := GenerateKey()
|
||||||
_, err := Decrypt(key, []byte("short"))
|
_, err := Decrypt(key, []byte("short"), nil)
|
||||||
if !errors.Is(err, ErrInvalidCiphertext) {
|
if !errors.Is(err, ErrInvalidCiphertext) {
|
||||||
t.Fatalf("expected ErrInvalidCiphertext, got: %v", err)
|
t.Fatalf("expected ErrInvalidCiphertext, got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -124,10 +156,141 @@ func TestEncryptProducesDifferentCiphertext(t *testing.T) {
|
|||||||
key, _ := GenerateKey()
|
key, _ := GenerateKey()
|
||||||
plaintext := []byte("same data")
|
plaintext := []byte("same data")
|
||||||
|
|
||||||
ct1, _ := Encrypt(key, plaintext)
|
ct1, _ := Encrypt(key, plaintext, nil)
|
||||||
ct2, _ := Encrypt(key, plaintext)
|
ct2, _ := Encrypt(key, plaintext, nil)
|
||||||
|
|
||||||
if bytes.Equal(ct1, ct2) {
|
if bytes.Equal(ct1, ct2) {
|
||||||
t.Fatal("two encryptions of same plaintext produced identical ciphertext (nonce reuse)")
|
t.Fatal("two encryptions of same plaintext produced identical ciphertext (nonce reuse)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestV2EncryptDecryptRoundtrip(t *testing.T) {
|
||||||
|
key, _ := GenerateKey()
|
||||||
|
plaintext := []byte("v2 test data")
|
||||||
|
keyID := "engine/ca/prod"
|
||||||
|
aad := []byte("engine/ca/prod/config.json")
|
||||||
|
|
||||||
|
ciphertext, err := EncryptV2(key, keyID, plaintext, aad)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncryptV2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ciphertext[0] != BarrierVersionV2 {
|
||||||
|
t.Fatalf("version byte: got %d, want %d", ciphertext[0], BarrierVersionV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pt, gotKeyID, err := DecryptV2(key, ciphertext, aad)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecryptV2: %v", err)
|
||||||
|
}
|
||||||
|
if gotKeyID != keyID {
|
||||||
|
t.Fatalf("key ID: got %q, want %q", gotKeyID, keyID)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(plaintext, pt) {
|
||||||
|
t.Fatalf("roundtrip failed: got %q, want %q", pt, plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV2DecryptV1Compat(t *testing.T) {
|
||||||
|
key, _ := GenerateKey()
|
||||||
|
plaintext := []byte("v1 legacy data")
|
||||||
|
|
||||||
|
// Encrypt with v1.
|
||||||
|
v1ct, err := Encrypt(key, plaintext, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encrypt v1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptV2 should handle v1 ciphertext.
|
||||||
|
pt, keyID, err := DecryptV2(key, v1ct, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecryptV2 with v1 ciphertext: %v", err)
|
||||||
|
}
|
||||||
|
if keyID != "" {
|
||||||
|
t.Fatalf("expected empty key ID for v1, got %q", keyID)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(plaintext, pt) {
|
||||||
|
t.Fatalf("roundtrip failed: got %q, want %q", pt, plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV2WrongAAD(t *testing.T) {
|
||||||
|
key, _ := GenerateKey()
|
||||||
|
plaintext := []byte("data")
|
||||||
|
aad := []byte("correct/path")
|
||||||
|
|
||||||
|
ct, _ := EncryptV2(key, "system", plaintext, aad)
|
||||||
|
|
||||||
|
_, _, err := DecryptV2(key, ct, []byte("wrong/path"))
|
||||||
|
if !errors.Is(err, ErrDecryptionFailed) {
|
||||||
|
t.Fatalf("expected ErrDecryptionFailed with wrong AAD, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV2WrongKey(t *testing.T) {
|
||||||
|
key1, _ := GenerateKey()
|
||||||
|
key2, _ := GenerateKey()
|
||||||
|
plaintext := []byte("data")
|
||||||
|
|
||||||
|
ct, _ := EncryptV2(key1, "system", plaintext, nil)
|
||||||
|
|
||||||
|
_, _, err := DecryptV2(key2, ct, nil)
|
||||||
|
if !errors.Is(err, ErrDecryptionFailed) {
|
||||||
|
t.Fatalf("expected ErrDecryptionFailed, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractKeyID(t *testing.T) {
|
||||||
|
key, _ := GenerateKey()
|
||||||
|
|
||||||
|
// v1: empty key ID.
|
||||||
|
v1ct, _ := Encrypt(key, []byte("data"), nil)
|
||||||
|
kid, err := ExtractKeyID(v1ct)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractKeyID v1: %v", err)
|
||||||
|
}
|
||||||
|
if kid != "" {
|
||||||
|
t.Fatalf("expected empty key ID for v1, got %q", kid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// v2: embedded key ID.
|
||||||
|
v2ct, _ := EncryptV2(key, "engine/transit/main", []byte("data"), nil)
|
||||||
|
kid, err = ExtractKeyID(v2ct)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractKeyID v2: %v", err)
|
||||||
|
}
|
||||||
|
if kid != "engine/transit/main" {
|
||||||
|
t.Fatalf("key ID: got %q, want %q", kid, "engine/transit/main")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV2KeyIDTooLong(t *testing.T) {
|
||||||
|
key, _ := GenerateKey()
|
||||||
|
longID := string(make([]byte, MaxKeyIDLen+1))
|
||||||
|
|
||||||
|
_, err := EncryptV2(key, longID, []byte("data"), nil)
|
||||||
|
if !errors.Is(err, ErrKeyIDTooLong) {
|
||||||
|
t.Fatalf("expected ErrKeyIDTooLong, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV2EmptyKeyID(t *testing.T) {
|
||||||
|
key, _ := GenerateKey()
|
||||||
|
plaintext := []byte("data with empty key id")
|
||||||
|
|
||||||
|
ct, err := EncryptV2(key, "", plaintext, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncryptV2 empty key ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pt, keyID, err := DecryptV2(key, ct, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecryptV2 empty key ID: %v", err)
|
||||||
|
}
|
||||||
|
if keyID != "" {
|
||||||
|
t.Fatalf("expected empty key ID, got %q", keyID)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(plaintext, pt) {
|
||||||
|
t.Fatalf("roundtrip failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestOpenAndMigrate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify tables exist.
|
// Verify tables exist.
|
||||||
tables := []string{"seal_config", "barrier_entries", "schema_migrations"}
|
tables := []string{"seal_config", "barrier_entries", "schema_migrations", "barrier_keys"}
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
var name string
|
var name string
|
||||||
err := database.QueryRow(
|
err := database.QueryRow(
|
||||||
@@ -38,7 +38,7 @@ func TestOpenAndMigrate(t *testing.T) {
|
|||||||
// Check migration version.
|
// Check migration version.
|
||||||
var version int
|
var version int
|
||||||
_ = database.QueryRow("SELECT MAX(version) FROM schema_migrations").Scan(&version)
|
_ = database.QueryRow("SELECT MAX(version) FROM schema_migrations").Scan(&version)
|
||||||
if version != 1 {
|
if version != 2 {
|
||||||
t.Errorf("migration version: got %d, want 1", version)
|
t.Errorf("migration version: got %d, want 2", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ var migrations = []string{
|
|||||||
version INTEGER PRIMARY KEY,
|
version INTEGER PRIMARY KEY,
|
||||||
applied_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
applied_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
||||||
);`,
|
);`,
|
||||||
|
|
||||||
|
// Version 2: barrier key registry for per-engine DEKs
|
||||||
|
`CREATE TABLE IF NOT EXISTS barrier_keys (
|
||||||
|
key_id TEXT PRIMARY KEY,
|
||||||
|
version INTEGER NOT NULL DEFAULT 1,
|
||||||
|
encrypted_dek BLOB NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
|
||||||
|
rotated_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate applies all pending migrations.
|
// Migrate applies all pending migrations.
|
||||||
|
|||||||
@@ -147,6 +147,14 @@ func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType
|
|||||||
eng := factory()
|
eng := factory()
|
||||||
mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name)
|
mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name)
|
||||||
|
|
||||||
|
// Create a per-engine DEK in the barrier for this mount.
|
||||||
|
if aesBarrier, ok := r.barrier.(*barrier.AESGCMBarrier); ok {
|
||||||
|
dekKeyID := fmt.Sprintf("engine/%s/%s", engineType, name)
|
||||||
|
if err := aesBarrier.CreateKey(ctx, dekKeyID); err != nil {
|
||||||
|
return fmt.Errorf("engine: create DEK %q: %w", dekKeyID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := eng.Initialize(ctx, r.barrier, mountPath, config); err != nil {
|
if err := eng.Initialize(ctx, r.barrier, mountPath, config); err != nil {
|
||||||
return fmt.Errorf("engine: initialize %q: %w", name, err)
|
return fmt.Errorf("engine: initialize %q: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|||||||
85
internal/grpcserver/barrier.go
Normal file
85
internal/grpcserver/barrier.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type barrierServer struct {
|
||||||
|
pb.UnimplementedBarrierServiceServer
|
||||||
|
s *GRPCServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *barrierServer) ListKeys(ctx context.Context, _ *pb.ListKeysRequest) (*pb.ListKeysResponse, error) {
|
||||||
|
keys, err := bs.s.sealMgr.Barrier().ListKeys(ctx)
|
||||||
|
if err != nil {
|
||||||
|
bs.s.logger.Error("grpc: list barrier keys", "error", err)
|
||||||
|
return nil, status.Error(codes.Internal, "failed to list keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &pb.ListKeysResponse{}
|
||||||
|
for _, k := range keys {
|
||||||
|
resp.Keys = append(resp.Keys, &pb.BarrierKeyInfo{
|
||||||
|
KeyId: k.KeyID,
|
||||||
|
Version: int32(k.Version),
|
||||||
|
CreatedAt: k.CreatedAt,
|
||||||
|
RotatedAt: k.RotatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *barrierServer) RotateMEK(ctx context.Context, req *pb.RotateMEKRequest) (*pb.RotateMEKResponse, error) {
|
||||||
|
if req.Password == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bs.s.sealMgr.RotateMEK(ctx, []byte(req.Password)); err != nil {
|
||||||
|
if errors.Is(err, seal.ErrInvalidPassword) {
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "invalid password")
|
||||||
|
}
|
||||||
|
if errors.Is(err, seal.ErrSealed) {
|
||||||
|
return nil, status.Error(codes.FailedPrecondition, "sealed")
|
||||||
|
}
|
||||||
|
bs.s.logger.Error("grpc: rotate MEK", "error", err)
|
||||||
|
return nil, status.Error(codes.Internal, "rotation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
bs.s.logger.Info("audit: MEK rotated")
|
||||||
|
return &pb.RotateMEKResponse{Ok: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *barrierServer) RotateKey(ctx context.Context, req *pb.RotateKeyRequest) (*pb.RotateKeyResponse, error) {
|
||||||
|
if req.KeyId == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "key_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bs.s.sealMgr.Barrier().RotateKey(ctx, req.KeyId); err != nil {
|
||||||
|
if errors.Is(err, barrier.ErrKeyNotFound) {
|
||||||
|
return nil, status.Error(codes.NotFound, "key not found")
|
||||||
|
}
|
||||||
|
bs.s.logger.Error("grpc: rotate key", "key_id", req.KeyId, "error", err)
|
||||||
|
return nil, status.Error(codes.Internal, "rotation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
bs.s.logger.Info("audit: DEK rotated", "key_id", req.KeyId)
|
||||||
|
return &pb.RotateKeyResponse{Ok: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *barrierServer) Migrate(ctx context.Context, _ *pb.MigrateBarrierRequest) (*pb.MigrateBarrierResponse, error) {
|
||||||
|
migrated, err := bs.s.sealMgr.Barrier().MigrateToV2(ctx)
|
||||||
|
if err != nil {
|
||||||
|
bs.s.logger.Error("grpc: barrier migration", "error", err)
|
||||||
|
return nil, status.Error(codes.Internal, "migration failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
bs.s.logger.Info("audit: barrier migrated to v2", "entries_migrated", migrated)
|
||||||
|
return &pb.MigrateBarrierResponse{Migrated: int32(migrated)}, nil
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ func (s *GRPCServer) Start() error {
|
|||||||
}
|
}
|
||||||
tlsCfg := &tls.Config{
|
tlsCfg := &tls.Config{
|
||||||
Certificates: []tls.Certificate{tlsCert},
|
Certificates: []tls.Certificate{tlsCert},
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS13,
|
||||||
}
|
}
|
||||||
creds := credentials.NewTLS(tlsCfg)
|
creds := credentials.NewTLS(tlsCfg)
|
||||||
|
|
||||||
@@ -81,6 +81,7 @@ func (s *GRPCServer) Start() error {
|
|||||||
pb.RegisterPKIServiceServer(s.srv, &pkiServer{s: s})
|
pb.RegisterPKIServiceServer(s.srv, &pkiServer{s: s})
|
||||||
pb.RegisterCAServiceServer(s.srv, &caServer{s: s})
|
pb.RegisterCAServiceServer(s.srv, &caServer{s: s})
|
||||||
pb.RegisterPolicyServiceServer(s.srv, &policyServer{s: s})
|
pb.RegisterPolicyServiceServer(s.srv, &policyServer{s: s})
|
||||||
|
pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s})
|
||||||
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
|
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr)
|
lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr)
|
||||||
@@ -137,6 +138,10 @@ func sealRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||||
|
"/metacrypt.v2.BarrierService/ListKeys": true,
|
||||||
|
"/metacrypt.v2.BarrierService/RotateMEK": true,
|
||||||
|
"/metacrypt.v2.BarrierService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.BarrierService/Migrate": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +172,10 @@ func authRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||||
|
"/metacrypt.v2.BarrierService/ListKeys": true,
|
||||||
|
"/metacrypt.v2.BarrierService/RotateMEK": true,
|
||||||
|
"/metacrypt.v2.BarrierService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.BarrierService/Migrate": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,9 +191,15 @@ func adminRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.CAService/RevokeCert": true,
|
"/metacrypt.v2.CAService/RevokeCert": true,
|
||||||
"/metacrypt.v2.CAService/DeleteCert": true,
|
"/metacrypt.v2.CAService/DeleteCert": true,
|
||||||
"/metacrypt.v2.PolicyService/CreatePolicy": true,
|
"/metacrypt.v2.PolicyService/CreatePolicy": true,
|
||||||
|
"/metacrypt.v2.PolicyService/ListPolicies": true,
|
||||||
|
"/metacrypt.v2.PolicyService/GetPolicy": true,
|
||||||
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
"/metacrypt.v2.PolicyService/DeletePolicy": true,
|
||||||
"/metacrypt.v2.ACMEService/SetConfig": true,
|
"/metacrypt.v2.ACMEService/SetConfig": true,
|
||||||
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
"/metacrypt.v2.ACMEService/ListAccounts": true,
|
||||||
"/metacrypt.v2.ACMEService/ListOrders": true,
|
"/metacrypt.v2.ACMEService/ListOrders": true,
|
||||||
|
"/metacrypt.v2.BarrierService/ListKeys": true,
|
||||||
|
"/metacrypt.v2.BarrierService/RotateMEK": true,
|
||||||
|
"/metacrypt.v2.BarrierService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.BarrierService/Migrate": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,38 @@ const (
|
|||||||
EffectDeny Effect = "deny"
|
EffectDeny Effect = "deny"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Action constants for policy evaluation.
|
||||||
|
const (
|
||||||
|
ActionAny = "any" // matches all non-admin actions
|
||||||
|
ActionRead = "read" // retrieve/list operations
|
||||||
|
ActionWrite = "write" // create/update/delete operations
|
||||||
|
ActionEncrypt = "encrypt" // encrypt data
|
||||||
|
ActionDecrypt = "decrypt" // decrypt data
|
||||||
|
ActionSign = "sign" // sign data
|
||||||
|
ActionVerify = "verify" // verify signatures
|
||||||
|
ActionHMAC = "hmac" // compute HMAC
|
||||||
|
ActionAdmin = "admin" // administrative operations (never matched by "any")
|
||||||
|
)
|
||||||
|
|
||||||
|
// validEffects is the set of recognized effects.
|
||||||
|
var validEffects = map[Effect]bool{
|
||||||
|
EffectAllow: true,
|
||||||
|
EffectDeny: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// validActions is the set of recognized actions.
|
||||||
|
var validActions = map[string]bool{
|
||||||
|
ActionAny: true,
|
||||||
|
ActionRead: true,
|
||||||
|
ActionWrite: true,
|
||||||
|
ActionEncrypt: true,
|
||||||
|
ActionDecrypt: true,
|
||||||
|
ActionSign: true,
|
||||||
|
ActionVerify: true,
|
||||||
|
ActionHMAC: true,
|
||||||
|
ActionAdmin: true,
|
||||||
|
}
|
||||||
|
|
||||||
// Rule is a policy rule stored in the barrier.
|
// Rule is a policy rule stored in the barrier.
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -88,8 +120,34 @@ func (e *Engine) Match(ctx context.Context, req *Request) (Effect, bool, error)
|
|||||||
return EffectDeny, false, nil // default deny, no matching rule
|
return EffectDeny, false, nil // default deny, no matching rule
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRule stores a new policy rule.
|
// LintRule validates a rule's effect and actions. It returns a list of problems
|
||||||
|
// (empty if the rule is valid). This does not check resource patterns or other
|
||||||
|
// fields — only the enumerated values that must come from a known set.
|
||||||
|
func LintRule(rule *Rule) []string {
|
||||||
|
var problems []string
|
||||||
|
|
||||||
|
if rule.ID == "" {
|
||||||
|
problems = append(problems, "rule ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validEffects[rule.Effect] {
|
||||||
|
problems = append(problems, fmt.Sprintf("invalid effect %q (must be %q or %q)", rule.Effect, EffectAllow, EffectDeny))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range rule.Actions {
|
||||||
|
if !validActions[strings.ToLower(a)] {
|
||||||
|
problems = append(problems, fmt.Sprintf("invalid action %q", a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRule validates and stores a new policy rule.
|
||||||
func (e *Engine) CreateRule(ctx context.Context, rule *Rule) error {
|
func (e *Engine) CreateRule(ctx context.Context, rule *Rule) error {
|
||||||
|
if problems := LintRule(rule); len(problems) > 0 {
|
||||||
|
return fmt.Errorf("policy: invalid rule: %s", strings.Join(problems, "; "))
|
||||||
|
}
|
||||||
data, err := json.Marshal(rule)
|
data, err := json.Marshal(rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("policy: marshal rule: %w", err)
|
return fmt.Errorf("policy: marshal rule: %w", err)
|
||||||
@@ -157,14 +215,28 @@ func matchesRule(rule *Rule, req *Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check action match.
|
// Check action match. The "any" action matches all non-admin actions.
|
||||||
if len(rule.Actions) > 0 && !containsString(rule.Actions, req.Action) {
|
if len(rule.Actions) > 0 && !matchesAction(rule.Actions, req.Action) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matchesAction checks whether any of the rule's actions match the requested action.
|
||||||
|
// The special "any" action matches all actions except "admin".
|
||||||
|
func matchesAction(ruleActions []string, reqAction string) bool {
|
||||||
|
for _, a := range ruleActions {
|
||||||
|
if strings.EqualFold(a, reqAction) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.EqualFold(a, ActionAny) && !strings.EqualFold(reqAction, ActionAdmin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func containsString(haystack []string, needle string) bool {
|
func containsString(haystack []string, needle string) bool {
|
||||||
for _, s := range haystack {
|
for _, s := range haystack {
|
||||||
if strings.EqualFold(s, needle) {
|
if strings.EqualFold(s, needle) {
|
||||||
|
|||||||
@@ -141,6 +141,96 @@ func TestPolicyPriorityOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionAnyMatchesNonAdmin(t *testing.T) {
|
||||||
|
e, cleanup := setupPolicy(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_ = e.CreateRule(ctx, &Rule{
|
||||||
|
ID: "any-rule",
|
||||||
|
Priority: 100,
|
||||||
|
Effect: EffectAllow,
|
||||||
|
Roles: []string{"user"},
|
||||||
|
Resources: []string{"transit/*"},
|
||||||
|
Actions: []string{ActionAny},
|
||||||
|
})
|
||||||
|
|
||||||
|
// "any" should match encrypt, decrypt, sign, verify, hmac, read, write.
|
||||||
|
for _, action := range []string{ActionEncrypt, ActionDecrypt, ActionSign, ActionVerify, ActionHMAC, ActionRead, ActionWrite} {
|
||||||
|
effect, _ := e.Evaluate(ctx, &Request{
|
||||||
|
Username: "alice",
|
||||||
|
Roles: []string{"user"},
|
||||||
|
Resource: "transit/default",
|
||||||
|
Action: action,
|
||||||
|
})
|
||||||
|
if effect != EffectAllow {
|
||||||
|
t.Errorf("action %q should be allowed by 'any', got: %s", action, effect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "any" must NOT match "admin".
|
||||||
|
effect, _ := e.Evaluate(ctx, &Request{
|
||||||
|
Username: "alice",
|
||||||
|
Roles: []string{"user"},
|
||||||
|
Resource: "transit/default",
|
||||||
|
Action: ActionAdmin,
|
||||||
|
})
|
||||||
|
if effect != EffectDeny {
|
||||||
|
t.Fatalf("action 'admin' should not be matched by 'any', got: %s", effect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLintRule(t *testing.T) {
|
||||||
|
// Valid rule.
|
||||||
|
problems := LintRule(&Rule{
|
||||||
|
ID: "ok",
|
||||||
|
Effect: EffectAllow,
|
||||||
|
Actions: []string{ActionAny, ActionEncrypt},
|
||||||
|
})
|
||||||
|
if len(problems) > 0 {
|
||||||
|
t.Errorf("expected no problems, got: %v", problems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing ID.
|
||||||
|
problems = LintRule(&Rule{Effect: EffectAllow})
|
||||||
|
if len(problems) != 1 {
|
||||||
|
t.Errorf("expected 1 problem for missing ID, got: %v", problems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid effect.
|
||||||
|
problems = LintRule(&Rule{ID: "bad-effect", Effect: "maybe"})
|
||||||
|
if len(problems) != 1 {
|
||||||
|
t.Errorf("expected 1 problem for bad effect, got: %v", problems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid action.
|
||||||
|
problems = LintRule(&Rule{ID: "bad-action", Effect: EffectAllow, Actions: []string{"destroy"}})
|
||||||
|
if len(problems) != 1 {
|
||||||
|
t.Errorf("expected 1 problem for bad action, got: %v", problems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple problems.
|
||||||
|
problems = LintRule(&Rule{Effect: "bogus", Actions: []string{"nope"}})
|
||||||
|
if len(problems) != 3 { // missing ID + bad effect + bad action
|
||||||
|
t.Errorf("expected 3 problems, got: %v", problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRuleRejectsInvalid(t *testing.T) {
|
||||||
|
e, cleanup := setupPolicy(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := e.CreateRule(ctx, &Rule{
|
||||||
|
ID: "bad",
|
||||||
|
Effect: EffectAllow,
|
||||||
|
Actions: []string{"obliterate"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid action, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPolicyUsernameMatch(t *testing.T) {
|
func TestPolicyUsernameMatch(t *testing.T) {
|
||||||
e, cleanup := setupPolicy(t)
|
e, cleanup := setupPolicy(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (m *Manager) Initialize(ctx context.Context, password []byte, params crypto
|
|||||||
defer crypto.Zeroize(kwk)
|
defer crypto.Zeroize(kwk)
|
||||||
|
|
||||||
// Encrypt MEK with KWK.
|
// Encrypt MEK with KWK.
|
||||||
encryptedMEK, err := crypto.Encrypt(kwk, mek)
|
encryptedMEK, err := crypto.Encrypt(kwk, mek, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
crypto.Zeroize(mek)
|
crypto.Zeroize(mek)
|
||||||
return fmt.Errorf("seal: encrypt mek: %w", err)
|
return fmt.Errorf("seal: encrypt mek: %w", err)
|
||||||
@@ -220,7 +220,7 @@ func (m *Manager) Unseal(password []byte) error {
|
|||||||
kwk := crypto.DeriveKey(password, salt, params)
|
kwk := crypto.DeriveKey(password, salt, params)
|
||||||
defer crypto.Zeroize(kwk)
|
defer crypto.Zeroize(kwk)
|
||||||
|
|
||||||
mek, err := crypto.Decrypt(kwk, encryptedMEK)
|
mek, err := crypto.Decrypt(kwk, encryptedMEK, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Debug("unseal failed: invalid password")
|
m.logger.Debug("unseal failed: invalid password")
|
||||||
return ErrInvalidPassword
|
return ErrInvalidPassword
|
||||||
@@ -239,6 +239,79 @@ func (m *Manager) Unseal(password []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RotateMEK generates a new MEK, re-wraps all DEKs in the barrier, and
|
||||||
|
// updates the encrypted MEK in seal_config. The password is required to
|
||||||
|
// derive the KWK for re-encrypting the new MEK.
|
||||||
|
func (m *Manager) RotateMEK(ctx context.Context, password []byte) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.state != StateUnsealed {
|
||||||
|
return ErrSealed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read seal config for KDF params.
|
||||||
|
var (
|
||||||
|
salt []byte
|
||||||
|
argTime, argMem uint32
|
||||||
|
argThreads uint8
|
||||||
|
)
|
||||||
|
err := m.db.QueryRow(`
|
||||||
|
SELECT kdf_salt, argon2_time, argon2_memory, argon2_threads
|
||||||
|
FROM seal_config WHERE id = 1`).Scan(&salt, &argTime, &argMem, &argThreads)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("seal: read config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password by decrypting existing MEK.
|
||||||
|
params := crypto.Argon2Params{Time: argTime, Memory: argMem, Threads: argThreads}
|
||||||
|
kwk := crypto.DeriveKey(password, salt, params)
|
||||||
|
defer crypto.Zeroize(kwk)
|
||||||
|
|
||||||
|
var encryptedMEK []byte
|
||||||
|
err = m.db.QueryRow("SELECT encrypted_mek FROM seal_config WHERE id = 1").Scan(&encryptedMEK)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("seal: read encrypted mek: %w", err)
|
||||||
|
}
|
||||||
|
_, err = crypto.Decrypt(kwk, encryptedMEK, nil)
|
||||||
|
if err != nil {
|
||||||
|
return ErrInvalidPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new MEK.
|
||||||
|
newMEK, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("seal: generate new mek: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-wrap all DEKs with new MEK.
|
||||||
|
if err := m.barrier.ReWrapKeys(ctx, newMEK); err != nil {
|
||||||
|
crypto.Zeroize(newMEK)
|
||||||
|
return fmt.Errorf("seal: re-wrap keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt new MEK with KWK.
|
||||||
|
newEncMEK, err := crypto.Encrypt(kwk, newMEK, nil)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(newMEK)
|
||||||
|
return fmt.Errorf("seal: encrypt new mek: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update seal_config.
|
||||||
|
_, err = m.db.ExecContext(ctx,
|
||||||
|
"UPDATE seal_config SET encrypted_mek = ? WHERE id = 1", newEncMEK)
|
||||||
|
if err != nil {
|
||||||
|
crypto.Zeroize(newMEK)
|
||||||
|
return fmt.Errorf("seal: update seal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap in-memory MEK.
|
||||||
|
crypto.Zeroize(m.mek)
|
||||||
|
m.mek = newMEK
|
||||||
|
m.logger.Info("MEK rotated successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Seal seals the service: zeroizes MEK, seals the barrier.
|
// Seal seals the service: zeroizes MEK, seals the barrier.
|
||||||
func (m *Manager) Seal() error {
|
func (m *Manager) Seal() error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
|||||||
@@ -120,6 +120,94 @@ func TestSealCheckInitializedPersists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSealRotateMEK(t *testing.T) {
|
||||||
|
mgr, cleanup := setupSeal(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
_ = mgr.CheckInitialized()
|
||||||
|
|
||||||
|
password := []byte("test-password")
|
||||||
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
||||||
|
_ = mgr.Initialize(ctx, password, params)
|
||||||
|
|
||||||
|
// Create a DEK and write data through the barrier.
|
||||||
|
b := mgr.Barrier()
|
||||||
|
_ = b.CreateKey(ctx, "system")
|
||||||
|
_ = b.CreateKey(ctx, "engine/ca/prod")
|
||||||
|
_ = b.Put(ctx, "policy/rule1", []byte("policy-data"))
|
||||||
|
_ = b.Put(ctx, "engine/ca/prod/cert", []byte("cert-data"))
|
||||||
|
|
||||||
|
// Rotate MEK.
|
||||||
|
if err := mgr.RotateMEK(ctx, password); err != nil {
|
||||||
|
t.Fatalf("RotateMEK: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data should still be readable.
|
||||||
|
got, err := b.Get(ctx, "policy/rule1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get after MEK rotation: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "policy-data" {
|
||||||
|
t.Fatalf("data: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
got2, err := b.Get(ctx, "engine/ca/prod/cert")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get engine data after MEK rotation: %v", err)
|
||||||
|
}
|
||||||
|
if string(got2) != "cert-data" {
|
||||||
|
t.Fatalf("data: got %q", got2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seal and unseal with the same password should work
|
||||||
|
// (the new MEK is now encrypted with the KWK).
|
||||||
|
if err := mgr.Seal(); err != nil {
|
||||||
|
t.Fatalf("Seal: %v", err)
|
||||||
|
}
|
||||||
|
if err := mgr.Unseal(password); err != nil {
|
||||||
|
t.Fatalf("Unseal after MEK rotation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got3, err := b.Get(ctx, "engine/ca/prod/cert")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get after seal/unseal: %v", err)
|
||||||
|
}
|
||||||
|
if string(got3) != "cert-data" {
|
||||||
|
t.Fatalf("data after seal/unseal: got %q", got3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSealRotateMEKWrongPassword(t *testing.T) {
|
||||||
|
mgr, cleanup := setupSeal(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
_ = mgr.CheckInitialized()
|
||||||
|
|
||||||
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
||||||
|
_ = mgr.Initialize(ctx, []byte("correct"), params)
|
||||||
|
|
||||||
|
err := mgr.RotateMEK(ctx, []byte("wrong"))
|
||||||
|
if !errors.Is(err, ErrInvalidPassword) {
|
||||||
|
t.Fatalf("expected ErrInvalidPassword, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSealRotateMEKWhenSealed(t *testing.T) {
|
||||||
|
mgr, cleanup := setupSeal(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
_ = mgr.CheckInitialized()
|
||||||
|
|
||||||
|
params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1}
|
||||||
|
_ = mgr.Initialize(ctx, []byte("password"), params)
|
||||||
|
_ = mgr.Seal()
|
||||||
|
|
||||||
|
err := mgr.RotateMEK(ctx, []byte("password"))
|
||||||
|
if !errors.Is(err, ErrSealed) {
|
||||||
|
t.Fatalf("expected ErrSealed, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSealStateString(t *testing.T) {
|
func TestSealStateString(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
want string
|
want string
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/ca"
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/ca"
|
||||||
@@ -45,6 +46,11 @@ func (s *Server) registerRoutes(r chi.Router) {
|
|||||||
r.Get("/v1/pki/{mount}/issuer/{name}", s.requireUnseal(s.handlePKIIssuer))
|
r.Get("/v1/pki/{mount}/issuer/{name}", s.requireUnseal(s.handlePKIIssuer))
|
||||||
r.Get("/v1/pki/{mount}/issuer/{name}/crl", s.requireUnseal(s.handlePKICRL))
|
r.Get("/v1/pki/{mount}/issuer/{name}/crl", s.requireUnseal(s.handlePKICRL))
|
||||||
|
|
||||||
|
r.Get("/v1/barrier/keys", s.requireAdmin(s.handleBarrierKeys))
|
||||||
|
r.Post("/v1/barrier/rotate-mek", s.requireAdmin(s.handleRotateMEK))
|
||||||
|
r.Post("/v1/barrier/rotate-key", s.requireAdmin(s.handleRotateKey))
|
||||||
|
r.Post("/v1/barrier/migrate", s.requireAdmin(s.handleBarrierMigrate))
|
||||||
|
|
||||||
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
|
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
|
||||||
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
|
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
|
||||||
s.registerACMERoutes(r)
|
s.registerACMERoutes(r)
|
||||||
@@ -253,6 +259,31 @@ func (s *Server) handleEngineUnmount(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adminOnlyOperations lists engine operations that require admin role.
|
||||||
|
// This enforces the same gates as the typed REST routes, ensuring the
|
||||||
|
// generic endpoint cannot bypass admin requirements.
|
||||||
|
var adminOnlyOperations = map[string]bool{
|
||||||
|
// CA engine.
|
||||||
|
"import-root": true,
|
||||||
|
"create-issuer": true,
|
||||||
|
"delete-issuer": true,
|
||||||
|
"revoke-cert": true,
|
||||||
|
"delete-cert": true,
|
||||||
|
// Transit engine.
|
||||||
|
"create-key": true,
|
||||||
|
"delete-key": true,
|
||||||
|
"rotate-key": true,
|
||||||
|
"update-key-config": true,
|
||||||
|
"trim-key": true,
|
||||||
|
// SSH CA engine.
|
||||||
|
"create-profile": true,
|
||||||
|
"update-profile": true,
|
||||||
|
"delete-profile": true,
|
||||||
|
// User engine.
|
||||||
|
"provision": true,
|
||||||
|
"delete-user": true,
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]interface{} `json:"data"`
|
||||||
@@ -271,6 +302,12 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
info := TokenInfoFromContext(r.Context())
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
|
||||||
|
// Enforce admin requirement for operations that have admin-only typed routes.
|
||||||
|
if adminOnlyOperations[req.Operation] && !info.IsAdmin {
|
||||||
|
http.Error(w, `{"error":"forbidden: admin required"}`, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Evaluate policy before dispatching to the engine.
|
// Evaluate policy before dispatching to the engine.
|
||||||
policyReq := &policy.Request{
|
policyReq := &policy.Request{
|
||||||
Username: info.Username,
|
Username: info.Username,
|
||||||
@@ -412,6 +449,90 @@ func (s *Server) handlePolicyRule(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Barrier Key Management Handlers ---
|
||||||
|
|
||||||
|
func (s *Server) handleBarrierKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
keys, err := s.seal.Barrier().ListKeys(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("list barrier keys", "error", err)
|
||||||
|
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if keys == nil {
|
||||||
|
keys = []barrier.KeyInfo{}
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleRotateMEK(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Password == "" {
|
||||||
|
http.Error(w, `{"error":"password is required"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.seal.RotateMEK(r.Context(), []byte(req.Password)); err != nil {
|
||||||
|
if errors.Is(err, seal.ErrInvalidPassword) {
|
||||||
|
http.Error(w, `{"error":"invalid password"}`, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, seal.ErrSealed) {
|
||||||
|
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Error("rotate MEK", "error", err)
|
||||||
|
http.Error(w, `{"error":"rotation failed"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleRotateKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
KeyID string `json:"key_id"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.KeyID == "" {
|
||||||
|
http.Error(w, `{"error":"key_id is required"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.seal.Barrier().RotateKey(r.Context(), req.KeyID); err != nil {
|
||||||
|
if errors.Is(err, barrier.ErrKeyNotFound) {
|
||||||
|
http.Error(w, `{"error":"key not found"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Error("rotate key", "key_id", req.KeyID, "error", err)
|
||||||
|
http.Error(w, `{"error":"rotation failed"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleBarrierMigrate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
migrated, err := s.seal.Barrier().MigrateToV2(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("barrier migration", "error", err)
|
||||||
|
http.Error(w, `{"error":"migration failed"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"migrated": migrated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- CA Certificate Handlers ---
|
// --- CA Certificate Handlers ---
|
||||||
|
|
||||||
func (s *Server) handleGetCert(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetCert(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -608,13 +729,29 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) {
|
|||||||
return caEng, nil
|
return caEng, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// operationAction maps an engine operation name to a policy action ("read" or "write").
|
// operationAction maps an engine operation name to a policy action.
|
||||||
func operationAction(op string) string {
|
func operationAction(op string) string {
|
||||||
switch op {
|
switch op {
|
||||||
case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer":
|
// Read operations.
|
||||||
return "read"
|
case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer",
|
||||||
|
"list-keys", "get-key", "get-public-key", "list-users", "get-profile", "list-profiles":
|
||||||
|
return policy.ActionRead
|
||||||
|
|
||||||
|
// Granular cryptographic operations (including batch variants).
|
||||||
|
case "encrypt", "batch-encrypt":
|
||||||
|
return policy.ActionEncrypt
|
||||||
|
case "decrypt", "batch-decrypt":
|
||||||
|
return policy.ActionDecrypt
|
||||||
|
case "sign", "sign-host", "sign-user":
|
||||||
|
return policy.ActionSign
|
||||||
|
case "verify":
|
||||||
|
return policy.ActionVerify
|
||||||
|
case "hmac":
|
||||||
|
return policy.ActionHMAC
|
||||||
|
|
||||||
|
// Everything else is a write.
|
||||||
default:
|
default:
|
||||||
return "write"
|
return policy.ActionWrite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,13 +58,7 @@ func (s *Server) Start() error {
|
|||||||
s.registerRoutes(r)
|
s.registerRoutes(r)
|
||||||
|
|
||||||
tlsCfg := &tls.Config{
|
tlsCfg := &tls.Config{
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS13,
|
||||||
CipherSuites: []uint16{
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.httpSrv = &http.Server{
|
s.httpSrv = &http.Server{
|
||||||
|
|||||||
@@ -241,18 +241,84 @@ func TestEngineRequestPolicyAllowsWithRule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOperationAction verifies the read/write classification of operations.
|
// TestEngineRequestAdminOnlyBlocksNonAdmin verifies that admin-only operations
|
||||||
|
// via the generic endpoint are rejected for non-admin users.
|
||||||
|
func TestEngineRequestAdminOnlyBlocksNonAdmin(t *testing.T) {
|
||||||
|
srv, sealMgr, _ := setupTestServer(t)
|
||||||
|
unsealServer(t, sealMgr, nil)
|
||||||
|
|
||||||
|
for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} {
|
||||||
|
body := makeEngineRequest("test-mount", op)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||||
|
req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.handleEngineRequest(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("operation %q: expected 403 for non-admin, got %d", op, w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineRequestAdminOnlyAllowsAdmin verifies that admin-only operations
|
||||||
|
// via the generic endpoint are allowed for admin users.
|
||||||
|
func TestEngineRequestAdminOnlyAllowsAdmin(t *testing.T) {
|
||||||
|
srv, sealMgr, _ := setupTestServer(t)
|
||||||
|
unsealServer(t, sealMgr, nil)
|
||||||
|
|
||||||
|
for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} {
|
||||||
|
body := makeEngineRequest("test-mount", op)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body))
|
||||||
|
req = withTokenInfo(req, &auth.TokenInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.handleEngineRequest(w, req)
|
||||||
|
|
||||||
|
// Admin passes the admin check; will get 404 (mount not found) not 403.
|
||||||
|
if w.Code == http.StatusForbidden {
|
||||||
|
t.Errorf("operation %q: admin should not be forbidden, got 403", op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOperationAction verifies the action classification of operations.
|
||||||
func TestOperationAction(t *testing.T) {
|
func TestOperationAction(t *testing.T) {
|
||||||
readOps := []string{"list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer"}
|
tests := map[string]string{
|
||||||
for _, op := range readOps {
|
// Read operations.
|
||||||
if got := operationAction(op); got != "read" {
|
"list-issuers": policy.ActionRead,
|
||||||
t.Errorf("operationAction(%q) = %q, want %q", op, got, "read")
|
"list-certs": policy.ActionRead,
|
||||||
|
"get-cert": policy.ActionRead,
|
||||||
|
"get-root": policy.ActionRead,
|
||||||
|
"get-chain": policy.ActionRead,
|
||||||
|
"get-issuer": policy.ActionRead,
|
||||||
|
"list-keys": policy.ActionRead,
|
||||||
|
"get-key": policy.ActionRead,
|
||||||
|
"get-public-key": policy.ActionRead,
|
||||||
|
"list-users": policy.ActionRead,
|
||||||
|
"get-profile": policy.ActionRead,
|
||||||
|
"list-profiles": policy.ActionRead,
|
||||||
|
|
||||||
|
// Granular crypto operations (including batch variants).
|
||||||
|
"encrypt": policy.ActionEncrypt,
|
||||||
|
"batch-encrypt": policy.ActionEncrypt,
|
||||||
|
"decrypt": policy.ActionDecrypt,
|
||||||
|
"batch-decrypt": policy.ActionDecrypt,
|
||||||
|
"sign": policy.ActionSign,
|
||||||
|
"sign-host": policy.ActionSign,
|
||||||
|
"sign-user": policy.ActionSign,
|
||||||
|
"verify": policy.ActionVerify,
|
||||||
|
"hmac": policy.ActionHMAC,
|
||||||
|
|
||||||
|
// Write operations.
|
||||||
|
"issue": policy.ActionWrite,
|
||||||
|
"renew": policy.ActionWrite,
|
||||||
|
"create-issuer": policy.ActionWrite,
|
||||||
|
"delete-issuer": policy.ActionWrite,
|
||||||
|
"sign-csr": policy.ActionWrite,
|
||||||
|
"revoke": policy.ActionWrite,
|
||||||
}
|
}
|
||||||
}
|
for op, want := range tests {
|
||||||
writeOps := []string{"issue", "renew", "create-issuer", "delete-issuer", "sign-csr", "revoke"}
|
if got := operationAction(op); got != want {
|
||||||
for _, op := range writeOps {
|
t.Errorf("operationAction(%q) = %q, want %q", op, got, want)
|
||||||
if got := operationAction(op); got != "write" {
|
|
||||||
t.Errorf("operationAction(%q) = %q, want %q", op, got, "write")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ func newTestWebServer(t *testing.T, vault vaultBackend) *WebServer {
|
|||||||
vault: vault,
|
vault: vault,
|
||||||
logger: slog.Default(),
|
logger: slog.Default(),
|
||||||
staticFS: staticFS,
|
staticFS: staticFS,
|
||||||
|
csrf: newCSRFProtect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type VaultClient struct {
|
|||||||
func NewVaultClient(addr, caCertPath string, logger *slog.Logger) (*VaultClient, error) {
|
func NewVaultClient(addr, caCertPath string, logger *slog.Logger) (*VaultClient, error) {
|
||||||
logger.Debug("connecting to vault", "addr", addr, "ca_cert", caCertPath)
|
logger.Debug("connecting to vault", "addr", addr, "ca_cert", caCertPath)
|
||||||
|
|
||||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13}
|
||||||
if caCertPath != "" {
|
if caCertPath != "" {
|
||||||
logger.Debug("loading vault CA certificate", "path", caCertPath)
|
logger.Debug("loading vault CA certificate", "path", caCertPath)
|
||||||
pemData, err := os.ReadFile(caCertPath) //nolint:gosec
|
pemData, err := os.ReadFile(caCertPath) //nolint:gosec
|
||||||
|
|||||||
119
internal/webserver/csrf.go
Normal file
119
internal/webserver/csrf.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
csrfCookieName = "metacrypt_csrf"
|
||||||
|
csrfFieldName = "csrf_token"
|
||||||
|
csrfTokenLen = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// csrfProtect provides CSRF protection using the signed double-submit cookie
|
||||||
|
// pattern. A random secret is generated at startup. CSRF tokens are an HMAC of
|
||||||
|
// a random nonce, sent as both a cookie and a hidden form field. On POST the
|
||||||
|
// middleware verifies that the form field matches the cookie's HMAC.
|
||||||
|
type csrfProtect struct {
|
||||||
|
secret []byte
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCSRFProtect() *csrfProtect {
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
panic("csrf: failed to generate secret: " + err.Error())
|
||||||
|
}
|
||||||
|
return &csrfProtect{secret: secret}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateToken creates a new CSRF token: base64(nonce) + "." + base64(hmac(nonce)).
|
||||||
|
func (c *csrfProtect) generateToken() (string, error) {
|
||||||
|
nonce := make([]byte, csrfTokenLen)
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return "", fmt.Errorf("csrf: generate nonce: %w", err)
|
||||||
|
}
|
||||||
|
nonceB64 := base64.RawURLEncoding.EncodeToString(nonce)
|
||||||
|
mac := hmac.New(sha256.New, c.secret)
|
||||||
|
mac.Write(nonce)
|
||||||
|
sigB64 := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
return nonceB64 + "." + sigB64, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validToken checks that a token has a valid HMAC signature.
|
||||||
|
func (c *csrfProtect) validToken(token string) bool {
|
||||||
|
parts := strings.SplitN(token, ".", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nonce, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil || len(nonce) != csrfTokenLen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sig, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mac := hmac.New(sha256.New, c.secret)
|
||||||
|
mac.Write(nonce)
|
||||||
|
return hmac.Equal(mac.Sum(nil), sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setToken generates a new CSRF token, sets it as a cookie, and returns it
|
||||||
|
// for embedding in a form.
|
||||||
|
func (c *csrfProtect) setToken(w http.ResponseWriter) string {
|
||||||
|
token, err := c.generateToken()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: csrfCookieName,
|
||||||
|
Value: token,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
})
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// middleware returns an HTTP middleware that enforces CSRF validation on
|
||||||
|
// mutation requests (POST, PUT, PATCH, DELETE). GET/HEAD/OPTIONS are passed
|
||||||
|
// through. The HTMX hx-post for /v1/seal is excluded since it hits the API
|
||||||
|
// server directly and uses token auth, not cookies.
|
||||||
|
func (c *csrfProtect) middleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read token from form field (works for both regular forms and
|
||||||
|
// multipart forms since ParseForm/ParseMultipartForm will have
|
||||||
|
// been called or the field is available via FormValue).
|
||||||
|
formToken := r.FormValue(csrfFieldName)
|
||||||
|
|
||||||
|
// Read token from cookie.
|
||||||
|
cookie, err := r.Cookie(csrfCookieName)
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
http.Error(w, "CSRF validation failed", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both tokens must be present, match each other, and be validly signed.
|
||||||
|
if formToken == "" || formToken != cookie.Value || !c.validToken(formToken) {
|
||||||
|
http.Error(w, "CSRF validation failed", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
105
internal/webserver/csrf_test.go
Normal file
105
internal/webserver/csrf_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCSRFTokenGenerateAndValidate(t *testing.T) {
|
||||||
|
c := newCSRFProtect()
|
||||||
|
token, err := c.generateToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generateToken: %v", err)
|
||||||
|
}
|
||||||
|
if !c.validToken(token) {
|
||||||
|
t.Fatal("valid token rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRFTokenInvalidFormats(t *testing.T) {
|
||||||
|
c := newCSRFProtect()
|
||||||
|
for _, bad := range []string{"", "nodot", "a.b.c", "abc.def"} {
|
||||||
|
if c.validToken(bad) {
|
||||||
|
t.Errorf("should reject %q", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRFTokenCrossSecret(t *testing.T) {
|
||||||
|
c1 := newCSRFProtect()
|
||||||
|
c2 := newCSRFProtect()
|
||||||
|
token, _ := c1.generateToken()
|
||||||
|
if c2.validToken(token) {
|
||||||
|
t.Fatal("token from different secret should be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRFMiddlewareAllowsGET(t *testing.T) {
|
||||||
|
c := newCSRFProtect()
|
||||||
|
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET should pass through, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRFMiddlewareBlocksPOSTWithoutToken(t *testing.T) {
|
||||||
|
c := newCSRFProtect()
|
||||||
|
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar"))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("POST without CSRF token should be forbidden, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRFMiddlewareAllowsPOSTWithValidToken(t *testing.T) {
|
||||||
|
c := newCSRFProtect()
|
||||||
|
token, _ := c.generateToken()
|
||||||
|
|
||||||
|
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
body := csrfFieldName + "=" + token
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: token})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("POST with valid CSRF token should pass, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRFMiddlewareRejectsMismatch(t *testing.T) {
|
||||||
|
c := newCSRFProtect()
|
||||||
|
token1, _ := c.generateToken()
|
||||||
|
token2, _ := c.generateToken()
|
||||||
|
|
||||||
|
handler := c.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
body := csrfFieldName + "=" + token1
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: token2})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("POST with mismatched tokens should be forbidden, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,7 +188,7 @@ func (ws *WebServer) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
})
|
})
|
||||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type WebServer struct {
|
|||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
httpSrv *http.Server
|
httpSrv *http.Server
|
||||||
staticFS fs.FS
|
staticFS fs.FS
|
||||||
|
csrf *csrfProtect
|
||||||
tgzCache sync.Map // key: UUID string → *tgzEntry
|
tgzCache sync.Map // key: UUID string → *tgzEntry
|
||||||
userCache sync.Map // key: UUID string → *cachedUsername
|
userCache sync.Map // key: UUID string → *cachedUsername
|
||||||
}
|
}
|
||||||
@@ -125,6 +126,7 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
|
|||||||
vault: vault,
|
vault: vault,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
staticFS: staticFS,
|
staticFS: staticFS,
|
||||||
|
csrf: newCSRFProtect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if tok := cfg.MCIAS.ServiceToken; tok != "" {
|
if tok := cfg.MCIAS.ServiceToken; tok != "" {
|
||||||
@@ -188,6 +190,7 @@ func (lw *loggingResponseWriter) Unwrap() http.ResponseWriter {
|
|||||||
func (ws *WebServer) Start() error {
|
func (ws *WebServer) Start() error {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(ws.loggingMiddleware)
|
r.Use(ws.loggingMiddleware)
|
||||||
|
r.Use(ws.csrf.middleware)
|
||||||
ws.registerRoutes(r)
|
ws.registerRoutes(r)
|
||||||
|
|
||||||
ws.httpSrv = &http.Server{
|
ws.httpSrv = &http.Server{
|
||||||
@@ -201,7 +204,7 @@ func (ws *WebServer) Start() error {
|
|||||||
ws.logger.Info("starting web server", "addr", ws.cfg.Web.ListenAddr)
|
ws.logger.Info("starting web server", "addr", ws.cfg.Web.ListenAddr)
|
||||||
|
|
||||||
if ws.cfg.Web.TLSCert != "" && ws.cfg.Web.TLSKey != "" {
|
if ws.cfg.Web.TLSCert != "" && ws.cfg.Web.TLSKey != "" {
|
||||||
ws.httpSrv.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
|
ws.httpSrv.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS13}
|
||||||
err := ws.httpSrv.ListenAndServeTLS(ws.cfg.Web.TLSCert, ws.cfg.Web.TLSKey)
|
err := ws.httpSrv.ListenAndServeTLS(ws.cfg.Web.TLSCert, ws.cfg.Web.TLSKey)
|
||||||
if err != nil && err != http.ErrServerClosed {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
return fmt.Errorf("webserver: %w", err)
|
return fmt.Errorf("webserver: %w", err)
|
||||||
@@ -226,7 +229,18 @@ func (ws *WebServer) Shutdown(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) renderTemplate(w http.ResponseWriter, name string, data interface{}) {
|
func (ws *WebServer) renderTemplate(w http.ResponseWriter, name string, data interface{}) {
|
||||||
tmpl, err := template.ParseFS(webui.FS,
|
csrfToken := ws.csrf.setToken(w)
|
||||||
|
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"csrfField": func() template.HTML {
|
||||||
|
return template.HTML(fmt.Sprintf(
|
||||||
|
`<input type="hidden" name="%s" value="%s">`,
|
||||||
|
csrfFieldName, template.HTMLEscapeString(csrfToken),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.New("").Funcs(funcMap).ParseFS(webui.FS,
|
||||||
"templates/layout.html",
|
"templates/layout.html",
|
||||||
"templates/"+name,
|
"templates/"+name,
|
||||||
)
|
)
|
||||||
|
|||||||
43
proto/metacrypt/v1/barrier.proto
Normal file
43
proto/metacrypt/v1/barrier.proto
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package metacrypt.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v1;metacryptv1";
|
||||||
|
|
||||||
|
service BarrierService {
|
||||||
|
rpc ListKeys(ListKeysRequest) returns (ListKeysResponse);
|
||||||
|
rpc RotateMEK(RotateMEKRequest) returns (RotateMEKResponse);
|
||||||
|
rpc RotateKey(RotateKeyRequest) returns (RotateKeyResponse);
|
||||||
|
rpc Migrate(MigrateBarrierRequest) returns (MigrateBarrierResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListKeysRequest {}
|
||||||
|
message ListKeysResponse {
|
||||||
|
repeated BarrierKeyInfo keys = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BarrierKeyInfo {
|
||||||
|
string key_id = 1;
|
||||||
|
int32 version = 2;
|
||||||
|
string created_at = 3;
|
||||||
|
string rotated_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RotateMEKRequest {
|
||||||
|
string password = 1;
|
||||||
|
}
|
||||||
|
message RotateMEKResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RotateKeyRequest {
|
||||||
|
string key_id = 1;
|
||||||
|
}
|
||||||
|
message RotateKeyResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MigrateBarrierRequest {}
|
||||||
|
message MigrateBarrierResponse {
|
||||||
|
int32 migrated = 1;
|
||||||
|
}
|
||||||
43
proto/metacrypt/v2/barrier.proto
Normal file
43
proto/metacrypt/v2/barrier.proto
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package metacrypt.v2;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2;metacryptv2";
|
||||||
|
|
||||||
|
service BarrierService {
|
||||||
|
rpc ListKeys(ListKeysRequest) returns (ListKeysResponse);
|
||||||
|
rpc RotateMEK(RotateMEKRequest) returns (RotateMEKResponse);
|
||||||
|
rpc RotateKey(RotateKeyRequest) returns (RotateKeyResponse);
|
||||||
|
rpc Migrate(MigrateBarrierRequest) returns (MigrateBarrierResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListKeysRequest {}
|
||||||
|
message ListKeysResponse {
|
||||||
|
repeated BarrierKeyInfo keys = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BarrierKeyInfo {
|
||||||
|
string key_id = 1;
|
||||||
|
int32 version = 2;
|
||||||
|
string created_at = 3;
|
||||||
|
string rotated_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RotateMEKRequest {
|
||||||
|
string password = 1;
|
||||||
|
}
|
||||||
|
message RotateMEKResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RotateKeyRequest {
|
||||||
|
string key_id = 1;
|
||||||
|
}
|
||||||
|
message RotateKeyResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MigrateBarrierRequest {}
|
||||||
|
message MigrateBarrierResponse {
|
||||||
|
int32 migrated = 1;
|
||||||
|
}
|
||||||
@@ -45,11 +45,13 @@
|
|||||||
{{if not .Cert.Revoked}}
|
{{if not .Cert.Revoked}}
|
||||||
<form method="POST" action="/pki/cert/{{.Cert.Serial}}/revoke"
|
<form method="POST" action="/pki/cert/{{.Cert.Serial}}/revoke"
|
||||||
onsubmit="return confirm('Revoke certificate {{.Cert.Serial}}? This cannot be undone.');">
|
onsubmit="return confirm('Revoke certificate {{.Cert.Serial}}? This cannot be undone.');">
|
||||||
|
{{csrfField}}
|
||||||
<button type="submit" class="btn btn-danger">Revoke Certificate</button>
|
<button type="submit" class="btn btn-danger">Revoke Certificate</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
<form method="POST" action="/pki/cert/{{.Cert.Serial}}/delete"
|
<form method="POST" action="/pki/cert/{{.Cert.Serial}}/delete"
|
||||||
onsubmit="return confirm('Permanently delete certificate record {{.Cert.Serial}}? This cannot be undone.');">
|
onsubmit="return confirm('Permanently delete certificate record {{.Cert.Serial}}? This cannot be undone.');">
|
||||||
|
{{csrfField}}
|
||||||
<button type="submit" class="btn btn-danger">Delete Record</button>
|
<button type="submit" class="btn btn-danger">Delete Record</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
<details>
|
<details>
|
||||||
<summary>Mount a CA engine</summary>
|
<summary>Mount a CA engine</summary>
|
||||||
<form method="post" action="/dashboard/mount-ca" enctype="multipart/form-data">
|
<form method="post" action="/dashboard/mount-ca" enctype="multipart/form-data">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="mount_name">Mount Name</label>
|
<label for="mount_name">Mount Name</label>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<p>Set the seal password for this instance. This password will be required to unseal the service after each restart.</p>
|
<p>Set the seal password for this instance. This password will be required to unseal the service after each restart.</p>
|
||||||
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
||||||
<form method="POST" action="/init">
|
<form method="POST" action="/init">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Seal Password</label>
|
<label for="password">Seal Password</label>
|
||||||
<input type="password" id="password" name="password" required autofocus autocomplete="new-password">
|
<input type="password" id="password" name="password" required autofocus autocomplete="new-password">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<p>Authenticate with your MCIAS credentials.</p>
|
<p>Authenticate with your MCIAS credentials.</p>
|
||||||
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" id="username" name="username" required autofocus autocomplete="username">
|
<input type="text" id="username" name="username" required autofocus autocomplete="username">
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
<div class="card-title">Import Root CA</div>
|
<div class="card-title">Import Root CA</div>
|
||||||
<p>{{if .RootExpired}}The current root CA has expired. Import a new one to continue issuing certificates.{{else}}No root CA is present. Import one to get started.{{end}}</p>
|
<p>{{if .RootExpired}}The current root CA has expired. Import a new one to continue issuing certificates.{{else}}No root CA is present. Import one to get started.{{end}}</p>
|
||||||
<form method="post" action="/pki/import-root" enctype="multipart/form-data">
|
<form method="post" action="/pki/import-root" enctype="multipart/form-data">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cert_file">Certificate PEM file</label>
|
<label for="cert_file">Certificate PEM file</label>
|
||||||
@@ -99,6 +100,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Issue Certificate</div>
|
<div class="card-title">Issue Certificate</div>
|
||||||
<form method="post" action="/pki/issue">
|
<form method="post" action="/pki/issue">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="issue_cn">Common Name</label>
|
<label for="issue_cn">Common Name</label>
|
||||||
@@ -191,6 +193,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<form method="post" action="/pki/sign-csr">
|
<form method="post" action="/pki/sign-csr">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sign_issuer">Issuer</label>
|
<label for="sign_issuer">Issuer</label>
|
||||||
@@ -228,6 +231,7 @@
|
|||||||
<div class="card-title">Create Issuer</div>
|
<div class="card-title">Create Issuer</div>
|
||||||
{{if .IssuerError}}<div class="error">{{.IssuerError}}</div>{{end}}
|
{{if .IssuerError}}<div class="error">{{.IssuerError}}</div>{{end}}
|
||||||
<form method="post" action="/pki/create-issuer">
|
<form method="post" action="/pki/create-issuer">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="issuer_name">Issuer Name</label>
|
<label for="issuer_name">Issuer Name</label>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<td>{{if .Actions}}{{range $i, $a := .Actions}}{{if $i}}, {{end}}{{$a}}{{end}}{{else}}<em>any</em>{{end}}</td>
|
<td>{{if .Actions}}{{range $i, $a := .Actions}}{{if $i}}, {{end}}{{$a}}{{end}}{{else}}<em>any</em>{{end}}</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" action="/policy/delete" style="display:inline">
|
<form method="post" action="/policy/delete" style="display:inline">
|
||||||
|
{{csrfField}}
|
||||||
<input type="hidden" name="id" value="{{.ID}}">
|
<input type="hidden" name="id" value="{{.ID}}">
|
||||||
<button type="submit" class="btn-danger btn-sm" onclick="return confirm('Delete rule {{.ID}}?')">Delete</button>
|
<button type="submit" class="btn-danger btn-sm" onclick="return confirm('Delete rule {{.ID}}?')">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
<details>
|
<details>
|
||||||
<summary>Add a new policy rule</summary>
|
<summary>Add a new policy rule</summary>
|
||||||
<form method="post" action="/policy">
|
<form method="post" action="/policy">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="rule_id">Rule ID <span class="required">*</span></label>
|
<label for="rule_id">Rule ID <span class="required">*</span></label>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<p>The service is sealed. Enter the seal password to restore access.</p>
|
<p>The service is sealed. Enter the seal password to restore access.</p>
|
||||||
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
|
||||||
<form method="POST" action="/unseal">
|
<form method="POST" action="/unseal">
|
||||||
|
{{csrfField}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Seal Password</label>
|
<label for="password">Seal Password</label>
|
||||||
<input type="password" id="password" name="password" required autofocus autocomplete="current-password">
|
<input type="password" id="password" name="password" required autofocus autocomplete="current-password">
|
||||||
|
|||||||
Reference in New Issue
Block a user