Merge transit engine branch, resolve conflicts in shared files
This commit is contained in:
354
REMEDIATION.md
Normal file
354
REMEDIATION.md
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
# Remediation Plan
|
||||||
|
|
||||||
|
**Date**: 2026-03-16
|
||||||
|
**Scope**: Audit findings #25–#38 from engine design review
|
||||||
|
|
||||||
|
This document provides a concrete remediation plan for each open finding. Items
|
||||||
|
are grouped by priority and ordered for efficient implementation (dependencies
|
||||||
|
first).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical
|
||||||
|
|
||||||
|
### #37 — `adminOnlyOperations` name collision blocks user `rotate-key`
|
||||||
|
|
||||||
|
**Problem**: The `adminOnlyOperations` map in `handleEngineRequest`
|
||||||
|
(`internal/server/routes.go:265`) is a flat `map[string]bool` keyed by
|
||||||
|
operation name. The transit engine's `rotate-key` is admin-only, but the user
|
||||||
|
engine's `rotate-key` is user-self. Since the map is checked before engine
|
||||||
|
dispatch, non-admin users are blocked from calling `rotate-key` on any engine
|
||||||
|
mount — including user engine mounts where it should be allowed.
|
||||||
|
|
||||||
|
**Fix**: Replace the flat map with an engine-type-qualified lookup. Two options:
|
||||||
|
|
||||||
|
**Option A — Qualify the map key** (minimal change):
|
||||||
|
|
||||||
|
Change the map type to include the engine type prefix:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var adminOnlyOperations = map[string]bool{
|
||||||
|
"ca:import-root": true,
|
||||||
|
"ca:create-issuer": true,
|
||||||
|
"ca:delete-issuer": true,
|
||||||
|
"ca:revoke-cert": true,
|
||||||
|
"ca:delete-cert": true,
|
||||||
|
"transit:create-key": true,
|
||||||
|
"transit:delete-key": true,
|
||||||
|
"transit:rotate-key": true,
|
||||||
|
"transit:update-key-config": true,
|
||||||
|
"transit:trim-key": true,
|
||||||
|
"sshca:create-profile": true,
|
||||||
|
"sshca:update-profile": true,
|
||||||
|
"sshca:delete-profile": true,
|
||||||
|
"sshca:revoke-cert": true,
|
||||||
|
"sshca:delete-cert": true,
|
||||||
|
"user:provision": true,
|
||||||
|
"user:delete-user": true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `handleEngineRequest`, look up `engineType + ":" + operation` instead of
|
||||||
|
just `operation`. The `engineType` is already known from the mount registry
|
||||||
|
(the generic endpoint resolves the mount to an engine type).
|
||||||
|
|
||||||
|
**Option B — Per-engine admin operations** (cleaner but more code):
|
||||||
|
|
||||||
|
Each engine implements an `AdminOperations() []string` method. The server
|
||||||
|
queries the resolved engine for its admin operations instead of using a global
|
||||||
|
map.
|
||||||
|
|
||||||
|
**Recommendation**: Option A. It requires a one-line change to the lookup and
|
||||||
|
a mechanical update to the map keys. The generic endpoint already resolves the
|
||||||
|
mount to get the engine type.
|
||||||
|
|
||||||
|
**Files to change**:
|
||||||
|
- `internal/server/routes.go` — update map and lookup in `handleEngineRequest`
|
||||||
|
- `engines/sshca.md` — update `adminOnlyOperations` section
|
||||||
|
- `engines/transit.md` — update `adminOnlyOperations` section
|
||||||
|
- `engines/user.md` — update `adminOnlyOperations` section
|
||||||
|
|
||||||
|
**Tests**: Add test case in `internal/server/server_test.go` — non-admin user
|
||||||
|
calling `rotate-key` via generic endpoint on a user engine mount should succeed
|
||||||
|
(policy permitting). Same call on a transit mount should return 403.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High
|
||||||
|
|
||||||
|
### #28 — HMAC output not versioned
|
||||||
|
|
||||||
|
**Problem**: HMAC output is raw base64 with no key version indicator. After key
|
||||||
|
rotation and `min_decryption_version` advancement, old HMACs are unverifiable
|
||||||
|
because the engine doesn't know which key version produced them.
|
||||||
|
|
||||||
|
**Fix**: Use the same versioned prefix format as ciphertext and signatures:
|
||||||
|
|
||||||
|
```
|
||||||
|
metacrypt:v{version}:{base64(mac_bytes)}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the `hmac` operation to include `key_version` in the response. Update
|
||||||
|
internal HMAC verification to parse the version prefix and select the
|
||||||
|
corresponding key version (subject to `min_decryption_version` enforcement).
|
||||||
|
|
||||||
|
**Files to change**:
|
||||||
|
- `engines/transit.md` — update HMAC section, add HMAC output format, update
|
||||||
|
Cryptographic Details section
|
||||||
|
- Implementation: `internal/engine/transit/sign.go` (when implemented)
|
||||||
|
|
||||||
|
### #30 — `max_key_versions` vs `min_decryption_version` unclear
|
||||||
|
|
||||||
|
**Problem**: The spec doesn't define when `max_key_versions` pruning happens or
|
||||||
|
whether it respects `min_decryption_version`. Auto-pruning on rotation could
|
||||||
|
destroy versions that still have unrewrapped ciphertext.
|
||||||
|
|
||||||
|
**Fix**: Define the behavior explicitly in `engines/transit.md`:
|
||||||
|
|
||||||
|
1. `max_key_versions` pruning happens during `rotate-key`, after the new
|
||||||
|
version is created.
|
||||||
|
2. Pruning **only** deletes versions **strictly less than**
|
||||||
|
`min_decryption_version`. If `max_key_versions` would require deleting a
|
||||||
|
version at or above `min_decryption_version`, the version is **retained**
|
||||||
|
and a warning is included in the response:
|
||||||
|
`"warning": "max_key_versions exceeded; advance min_decryption_version to enable pruning"`.
|
||||||
|
3. This means `max_key_versions` is a soft limit — it is only enforceable
|
||||||
|
after the operator completes the rotation cycle (rotate → rewrap → advance
|
||||||
|
min → prune happens automatically on next rotate).
|
||||||
|
|
||||||
|
This resolves the original audit finding #16 as well.
|
||||||
|
|
||||||
|
**Files to change**:
|
||||||
|
- `engines/transit.md` — add `max_key_versions` behavior to Key Rotation
|
||||||
|
section and `rotate-key` flow
|
||||||
|
- `AUDIT.md` — mark #16 as RESOLVED with reference to the new behavior
|
||||||
|
|
||||||
|
### #33 — Auto-provision creates keys for arbitrary usernames
|
||||||
|
|
||||||
|
**Problem**: The encrypt flow auto-provisions recipients without validating
|
||||||
|
that the username exists in MCIAS. Any authenticated user can create barrier
|
||||||
|
entries for non-existent users.
|
||||||
|
|
||||||
|
**Fix**: Before auto-provisioning, validate the recipient username against
|
||||||
|
MCIAS. The engine has access to the auth system via `req.CallerInfo` context.
|
||||||
|
Add an MCIAS user lookup:
|
||||||
|
|
||||||
|
1. Add a `ValidateUsername(username string) (bool, error)` method to the auth
|
||||||
|
client interface. This calls the MCIAS user info endpoint to check if the
|
||||||
|
username exists.
|
||||||
|
2. In the encrypt flow, before auto-provisioning a recipient, call
|
||||||
|
`ValidateUsername`. If the user doesn't exist in MCIAS, return an error:
|
||||||
|
`"recipient not found: {username}"`.
|
||||||
|
3. Document this validation in the encrypt flow and security considerations.
|
||||||
|
|
||||||
|
**Alternative** (simpler, weaker): Skip MCIAS validation but add a
|
||||||
|
rate limit on auto-provisioning (e.g., max 10 new provisions per encrypt
|
||||||
|
request, max 100 total auto-provisions per hour per caller). This prevents
|
||||||
|
storage inflation but doesn't prevent phantom users.
|
||||||
|
|
||||||
|
**Recommendation**: MCIAS validation. It's the correct security boundary —
|
||||||
|
only real MCIAS users should have keypairs.
|
||||||
|
|
||||||
|
**Files to change**:
|
||||||
|
- `engines/user.md` — update encrypt flow step 2, add MCIAS validation
|
||||||
|
- `internal/auth/` — add `ValidateUsername` to auth client (when implemented)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Medium
|
||||||
|
|
||||||
|
### #25 — Missing `list-certs` REST route (SSH CA)
|
||||||
|
|
||||||
|
**Fix**: Add to the REST endpoints table:
|
||||||
|
|
||||||
|
```
|
||||||
|
| GET | `/v1/sshca/{mount}/certs` | List cert records |
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to the route registration code block:
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.Get("/v1/sshca/{mount}/certs", s.requireAuth(s.handleSSHCAListCerts))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files to change**: `engines/sshca.md`
|
||||||
|
|
||||||
|
### #26 — KRL section type description error
|
||||||
|
|
||||||
|
**Fix**: Change the description block from:
|
||||||
|
|
||||||
|
```
|
||||||
|
Section type: KRL_SECTION_CERT_SERIAL_LIST (0x21)
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```
|
||||||
|
Section type: KRL_SECTION_CERTIFICATES (0x01)
|
||||||
|
CA key blob: ssh.MarshalAuthorizedKey(caSigner.PublicKey())
|
||||||
|
Subsection type: KRL_SECTION_CERT_SERIAL_LIST (0x20)
|
||||||
|
```
|
||||||
|
|
||||||
|
This matches the pseudocode comments and the OpenSSH `PROTOCOL.krl` spec.
|
||||||
|
|
||||||
|
**Files to change**: `engines/sshca.md`
|
||||||
|
|
||||||
|
### #27 — Policy check after cert construction (SSH CA)
|
||||||
|
|
||||||
|
**Fix**: Reorder the sign-host flow steps:
|
||||||
|
|
||||||
|
1. Authenticate caller.
|
||||||
|
2. Parse the supplied SSH public key.
|
||||||
|
3. Parse TTL.
|
||||||
|
4. **Policy check**: for each hostname, check policy on
|
||||||
|
`sshca/{mount}/id/{hostname}`, action `sign`.
|
||||||
|
5. Generate serial (only after policy passes).
|
||||||
|
6. Build `ssh.Certificate`.
|
||||||
|
7. Sign, store, return.
|
||||||
|
|
||||||
|
Same reordering for sign-user.
|
||||||
|
|
||||||
|
**Files to change**: `engines/sshca.md`
|
||||||
|
|
||||||
|
### #29 — `rewrap` policy action not specified
|
||||||
|
|
||||||
|
**Fix**: Add `rewrap` as an explicit action in the `operationAction` mapping.
|
||||||
|
`rewrap` maps to `decrypt` (since it requires internal access to plaintext).
|
||||||
|
Batch variants map to the same action.
|
||||||
|
|
||||||
|
Add to the authorization section in `engines/transit.md`:
|
||||||
|
|
||||||
|
> The `rewrap` and `batch-rewrap` operations require the `decrypt` action —
|
||||||
|
> rewrap internally decrypts with the old version and re-encrypts with the
|
||||||
|
> latest, so the caller must have decrypt permission. Alternatively, a
|
||||||
|
> dedicated `rewrap` action could be added for finer-grained control, but
|
||||||
|
> `decrypt` is the safer default (granting `rewrap` without `decrypt` would be
|
||||||
|
> odd since rewrap implies decrypt capability).
|
||||||
|
|
||||||
|
**Recommendation**: Map to `decrypt`. Simpler, and anyone who should rewrap
|
||||||
|
should also be able to decrypt.
|
||||||
|
|
||||||
|
**Files to change**: `engines/transit.md`
|
||||||
|
|
||||||
|
### #31 — Missing `get-public-key` REST route (Transit)
|
||||||
|
|
||||||
|
**Fix**: Add to the REST endpoints table:
|
||||||
|
|
||||||
|
```
|
||||||
|
| GET | `/v1/transit/{mount}/keys/{name}/public-key` | Get public key |
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to the route registration code block:
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.Get("/v1/transit/{mount}/keys/{name}/public-key", s.requireAuth(s.handleTransitGetPublicKey))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files to change**: `engines/transit.md`
|
||||||
|
|
||||||
|
### #34 — No recipient limit on encrypt (User)
|
||||||
|
|
||||||
|
**Fix**: Add a compile-time constant `maxRecipients = 100` to the user engine.
|
||||||
|
Reject requests exceeding this limit with `400 Bad Request` / `InvalidArgument`
|
||||||
|
before any ECDH computation.
|
||||||
|
|
||||||
|
Add to the encrypt flow in `engines/user.md` after step 1:
|
||||||
|
|
||||||
|
> Validate that `len(recipients) <= maxRecipients` (100). Reject with error if
|
||||||
|
> exceeded.
|
||||||
|
|
||||||
|
Add to the security considerations section.
|
||||||
|
|
||||||
|
**Files to change**: `engines/user.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Low
|
||||||
|
|
||||||
|
### #32 — `exportable` flag with no export operation (Transit)
|
||||||
|
|
||||||
|
**Fix**: Add an `export-key` operation to the transit engine:
|
||||||
|
|
||||||
|
- Auth: User+Policy (action `read`).
|
||||||
|
- Only succeeds if the key's `exportable` flag is `true`.
|
||||||
|
- Returns raw key material (base64-encoded) for the current version only.
|
||||||
|
- Asymmetric keys: returns private key in PKCS8 PEM.
|
||||||
|
- Symmetric keys: returns raw key bytes, base64-encoded.
|
||||||
|
- Add to HandleRequest dispatch, gRPC service, REST endpoints.
|
||||||
|
|
||||||
|
Alternatively, if key export is never intended, remove the `exportable` flag
|
||||||
|
from `create-key` to avoid dead code. Given that transit is meant to keep keys
|
||||||
|
server-side, **removing the flag** may be the better choice. Document the
|
||||||
|
decision either way.
|
||||||
|
|
||||||
|
**Recommendation**: Remove `exportable`. Transit's entire value proposition is
|
||||||
|
that keys never leave the service. If export is needed for migration, a
|
||||||
|
dedicated admin-only `export-key` can be added later with appropriate audit
|
||||||
|
logging (#7).
|
||||||
|
|
||||||
|
**Files to change**: `engines/transit.md`
|
||||||
|
|
||||||
|
### #35 — No re-encryption support for user key rotation
|
||||||
|
|
||||||
|
**Fix**: Add a `re-encrypt` operation:
|
||||||
|
|
||||||
|
- Auth: User (self) — only the envelope recipient can re-encrypt.
|
||||||
|
- Input: old envelope.
|
||||||
|
- Flow: decrypt with current key, generate new DEK, re-encrypt, return new
|
||||||
|
envelope.
|
||||||
|
- The old key must still be valid at the time of re-encryption. Document the
|
||||||
|
workflow: re-encrypt all stored envelopes, then rotate-key.
|
||||||
|
|
||||||
|
This is a quality-of-life improvement, not a security fix. The current design
|
||||||
|
(decrypt + encrypt separately) works but requires the caller to handle
|
||||||
|
plaintext.
|
||||||
|
|
||||||
|
**Files to change**: `engines/user.md`
|
||||||
|
|
||||||
|
### #36 — `UserKeyConfig` type undefined
|
||||||
|
|
||||||
|
**Fix**: Add the type definition to the in-memory state section:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type UserKeyConfig struct {
|
||||||
|
Algorithm string `json:"algorithm"` // key exchange algorithm used
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
AutoProvisioned bool `json:"auto_provisioned"` // created via auto-provision
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files to change**: `engines/user.md`
|
||||||
|
|
||||||
|
### #38 — `ZeroizeKey` prerequisite not cross-referenced
|
||||||
|
|
||||||
|
**Fix**: Add to the Implementation Steps section in both `engines/transit.md`
|
||||||
|
and `engines/user.md`:
|
||||||
|
|
||||||
|
> **Prerequisite**: `engine.ZeroizeKey` must exist in
|
||||||
|
> `internal/engine/helpers.go` (created as part of the SSH CA engine
|
||||||
|
> implementation — see `engines/sshca.md` step 1).
|
||||||
|
|
||||||
|
**Files to change**: `engines/transit.md`, `engines/user.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
The remediation items should be implemented in this order to respect
|
||||||
|
dependencies:
|
||||||
|
|
||||||
|
1. **#37** — `adminOnlyOperations` qualification (critical, blocks user engine
|
||||||
|
`rotate-key`). This is a code change to `internal/server/routes.go` plus
|
||||||
|
spec updates. Do first because it affects all engine implementations.
|
||||||
|
|
||||||
|
2. **#28, #29, #30, #31, #32** — Transit spec fixes (can be done as a single
|
||||||
|
spec update pass).
|
||||||
|
|
||||||
|
3. **#25, #26, #27** — SSH CA spec fixes (single spec update pass).
|
||||||
|
|
||||||
|
4. **#33, #34, #35, #36** — User spec fixes (single spec update pass).
|
||||||
|
|
||||||
|
5. **#38** — Cross-reference update (trivial, do with transit and user spec
|
||||||
|
fixes).
|
||||||
|
|
||||||
|
Items within the same group are independent and can be done in parallel.
|
||||||
160
cmd/metacrypt/migrate_barrier.go
Normal file
160
cmd/metacrypt/migrate_barrier.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/config"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
var migrateBarrierCmd = &cobra.Command{
|
||||||
|
Use: "migrate-barrier",
|
||||||
|
Short: "Migrate barrier entries from v1 (MEK) to v2 (per-engine DEKs)",
|
||||||
|
Long: `Converts all v1 barrier entries to v2 format with per-engine data
|
||||||
|
encryption keys (DEKs). Creates a "system" DEK for non-engine data and
|
||||||
|
per-engine DEKs for each engine mount found in the barrier.
|
||||||
|
|
||||||
|
After migration, each engine's data is encrypted with its own DEK rather
|
||||||
|
than the MEK directly, limiting blast radius if a single key is compromised.
|
||||||
|
The MEK only wraps the DEKs.
|
||||||
|
|
||||||
|
Entries already in v2 format are skipped. Run while the server is stopped
|
||||||
|
to avoid concurrent access. Requires the unseal password.`,
|
||||||
|
RunE: runMigrateBarrier,
|
||||||
|
}
|
||||||
|
|
||||||
|
var migrateBarrierDryRun bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrateBarrierCmd.Flags().BoolVar(&migrateBarrierDryRun, "dry-run", false, "report what would be migrated without writing")
|
||||||
|
rootCmd.AddCommand(migrateBarrierCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrateBarrier(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() }()
|
||||||
|
|
||||||
|
// Apply any pending schema migrations (creates barrier_keys table).
|
||||||
|
if err := db.Migrate(database); err != nil {
|
||||||
|
return fmt.Errorf("schema migration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
if migrateBarrierDryRun {
|
||||||
|
return migrateBarrierDryRunReport(database, mek)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unseal the barrier with the MEK (loads existing DEKs).
|
||||||
|
b := barrier.NewAESGCMBarrier(database)
|
||||||
|
if err := b.Unseal(mek); err != nil {
|
||||||
|
return fmt.Errorf("unseal barrier: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = b.Seal() }()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
migrated, err := b.MigrateToV2(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("migration failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if migrated == 0 {
|
||||||
|
fmt.Println("Nothing to migrate — all entries already use v2 format.")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Migrated %d entries to v2 format with per-engine DEKs.\n", migrated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show key summary.
|
||||||
|
keys, err := b.ListKeys(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list keys: %w", err)
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
fmt.Printf("\nBarrier keys (%d):\n", len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
fmt.Printf(" %-30s version=%d created=%s\n", k.KeyID, k.Version, k.CreatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateBarrierDryRunReport(database *sql.DB, mek []byte) error {
|
||||||
|
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() }()
|
||||||
|
|
||||||
|
var v1Count, v2Count, total int
|
||||||
|
var v1Paths []string
|
||||||
|
for rows.Next() {
|
||||||
|
var path string
|
||||||
|
var value []byte
|
||||||
|
if err := rows.Scan(&path, &value); err != nil {
|
||||||
|
return fmt.Errorf("scan: %w", err)
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
if len(value) > 0 && value[0] == crypto.BarrierVersionV2 {
|
||||||
|
v2Count++
|
||||||
|
} else {
|
||||||
|
v1Count++
|
||||||
|
v1Paths = append(v1Paths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return fmt.Errorf("iterate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Total entries: %d\n", total)
|
||||||
|
fmt.Printf("Already v2: %d\n", v2Count)
|
||||||
|
fmt.Printf("Need migration: %d\n", v1Count)
|
||||||
|
|
||||||
|
if len(v1Paths) > 0 {
|
||||||
|
fmt.Println("\nEntries that would be migrated:")
|
||||||
|
for _, p := range v1Paths {
|
||||||
|
fmt.Printf(" %s\n", p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("\nNothing to migrate.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"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"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca"
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/transit"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/grpcserver"
|
"git.wntrmute.dev/kyle/metacrypt/internal/grpcserver"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
||||||
@@ -76,6 +77,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|||||||
engineRegistry := engine.NewRegistry(b, logger)
|
engineRegistry := engine.NewRegistry(b, logger)
|
||||||
engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
|
engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
|
||||||
engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
|
engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
|
||||||
|
engineRegistry.RegisterFactory(engine.EngineTypeTransit, transit.NewTransitEngine)
|
||||||
|
|
||||||
srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version)
|
srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version)
|
||||||
grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger)
|
grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger)
|
||||||
|
|||||||
713
docs/engineering-standards.md
Normal file
713
docs/engineering-standards.md
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
# Metacircular Dynamics — Engineering Standards
|
||||||
|
|
||||||
|
This document describes the standard repository layout, tooling, and software
|
||||||
|
development lifecycle (SDLC) for services built at Metacircular Dynamics. It is
|
||||||
|
derived from the conventions established in Metacrypt and codifies them as the
|
||||||
|
baseline for all new and existing services.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Repository Layout](#repository-layout)
|
||||||
|
2. [Language & Toolchain](#language--toolchain)
|
||||||
|
3. [Build System](#build-system)
|
||||||
|
4. [API Design](#api-design)
|
||||||
|
5. [Authentication & Authorization](#authentication--authorization)
|
||||||
|
6. [Database Conventions](#database-conventions)
|
||||||
|
7. [Configuration](#configuration)
|
||||||
|
8. [Web UI](#web-ui)
|
||||||
|
9. [Testing](#testing)
|
||||||
|
10. [Linting & Static Analysis](#linting--static-analysis)
|
||||||
|
11. [Deployment](#deployment)
|
||||||
|
12. [Documentation](#documentation)
|
||||||
|
13. [Security](#security)
|
||||||
|
14. [Development Workflow](#development-workflow)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
Every service follows a consistent directory structure. Adjust the
|
||||||
|
service-specific directories (e.g. `engines/` in Metacrypt) as appropriate,
|
||||||
|
but the top-level skeleton is fixed.
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── cmd/
|
||||||
|
│ ├── <service>/ CLI entry point (server, subcommands)
|
||||||
|
│ └── <service>-web/ Web UI entry point (if separate binary)
|
||||||
|
├── internal/
|
||||||
|
│ ├── auth/ MCIAS integration (token validation, caching)
|
||||||
|
│ ├── config/ TOML configuration loading & validation
|
||||||
|
│ ├── db/ Database setup, schema migrations
|
||||||
|
│ ├── server/ REST API server, routes, middleware
|
||||||
|
│ ├── grpcserver/ gRPC server, interceptors, service handlers
|
||||||
|
│ ├── webserver/ Web UI server, template routes, HTMX handlers
|
||||||
|
│ └── <domain>/ Service-specific packages
|
||||||
|
├── proto/<service>/
|
||||||
|
│ ├── v1/ Legacy proto definitions (if applicable)
|
||||||
|
│ └── v2/ Current proto definitions
|
||||||
|
├── gen/<service>/
|
||||||
|
│ ├── v1/ Generated Go gRPC/protobuf code
|
||||||
|
│ └── v2/
|
||||||
|
├── web/
|
||||||
|
│ ├── embed.go //go:embed directive for templates and static
|
||||||
|
│ ├── templates/ Go HTML templates
|
||||||
|
│ └── static/ CSS, JS (htmx)
|
||||||
|
├── deploy/
|
||||||
|
│ ├── docker/ Docker Compose configuration
|
||||||
|
│ ├── examples/ Example config files
|
||||||
|
│ ├── scripts/ Install, backup, migration scripts
|
||||||
|
│ └── systemd/ systemd unit files and timers
|
||||||
|
├── docs/ Internal engineering documentation
|
||||||
|
├── Dockerfile.api API server container (if split binary)
|
||||||
|
├── Dockerfile.web Web UI container (if split binary)
|
||||||
|
├── Makefile
|
||||||
|
├── buf.yaml Protobuf linting & breaking-change config
|
||||||
|
├── .golangci.yaml Linter configuration
|
||||||
|
├── .gitignore
|
||||||
|
├── CLAUDE.md AI-assisted development instructions
|
||||||
|
├── ARCHITECTURE.md Full system specification
|
||||||
|
└── <service>.toml.example Example configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Principles
|
||||||
|
|
||||||
|
- **`cmd/`** contains only CLI wiring (cobra commands, flag parsing). No
|
||||||
|
business logic.
|
||||||
|
- **`internal/`** contains all service logic. Nothing in `internal/` is
|
||||||
|
importable by other modules — this is enforced by Go's module system.
|
||||||
|
- **`proto/`** is the source of truth for gRPC definitions. Generated code
|
||||||
|
lives in `gen/`, never edited by hand.
|
||||||
|
- **`deploy/`** contains everything needed to run the service in production.
|
||||||
|
A new engineer should be able to deploy from this directory alone.
|
||||||
|
- **`web/`** is embedded into the binary via `//go:embed`. No external file
|
||||||
|
dependencies at runtime.
|
||||||
|
|
||||||
|
### What Does Not Belong in the Repository
|
||||||
|
|
||||||
|
- Runtime data (databases, certificates, logs) — these live in `/srv/<service>`
|
||||||
|
- Real configuration files with secrets — only examples are committed
|
||||||
|
- IDE configuration (`.idea/`, `.vscode/`) — per-developer, not shared
|
||||||
|
- Vendored dependencies — Go module proxy handles this
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Language & Toolchain
|
||||||
|
|
||||||
|
| Tool | Version | Purpose |
|
||||||
|
|------|---------|---------|
|
||||||
|
| Go | 1.25+ | Primary language |
|
||||||
|
| protoc + protoc-gen-go | Latest | Protobuf/gRPC code generation |
|
||||||
|
| buf | Latest | Proto linting and breaking-change detection |
|
||||||
|
| golangci-lint | v2 | Static analysis and linting |
|
||||||
|
| Docker | Latest | Container builds |
|
||||||
|
|
||||||
|
### Go Conventions
|
||||||
|
|
||||||
|
- **Pure-Go dependencies** where possible. Avoid CGo — it complicates
|
||||||
|
cross-compilation and container builds. Use `modernc.org/sqlite` instead
|
||||||
|
of `mattn/go-sqlite3`.
|
||||||
|
- **`CGO_ENABLED=0`** for all production builds. Statically linked binaries
|
||||||
|
deploy cleanly to Alpine containers.
|
||||||
|
- **Stripped binaries**: Build with `-trimpath -ldflags="-s -w"` to remove
|
||||||
|
debug symbols and reduce image size.
|
||||||
|
- **Version injection**: Pass `git describe --tags --always --dirty` via
|
||||||
|
`-X main.version=...` at build time. Every binary must report its version.
|
||||||
|
|
||||||
|
### Module Path
|
||||||
|
|
||||||
|
Services hosted on `git.wntrmute.dev` use:
|
||||||
|
|
||||||
|
```
|
||||||
|
git.wntrmute.dev/kyle/<service>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build System
|
||||||
|
|
||||||
|
Every repository has a Makefile with these standard targets:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
.PHONY: build test vet lint proto-lint clean docker all
|
||||||
|
|
||||||
|
LDFLAGS := -trimpath -ldflags="-s -w -X main.version=$(shell git describe --tags --always --dirty)"
|
||||||
|
|
||||||
|
<service>:
|
||||||
|
go build $(LDFLAGS) -o <service> ./cmd/<service>
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
proto:
|
||||||
|
protoc --go_out=. --go_opt=module=<module> \
|
||||||
|
--go-grpc_out=. --go-grpc_opt=module=<module> \
|
||||||
|
proto/<service>/v2/*.proto
|
||||||
|
|
||||||
|
proto-lint:
|
||||||
|
buf lint
|
||||||
|
buf breaking --against '.git#branch=master,subdir=proto'
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f <service>
|
||||||
|
|
||||||
|
docker:
|
||||||
|
docker build -t <service> -f Dockerfile.api .
|
||||||
|
|
||||||
|
all: vet lint test <service>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Target Semantics
|
||||||
|
|
||||||
|
| Target | When to Run | CI Gate? |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| `vet` | Every change | Yes |
|
||||||
|
| `lint` | Every change | Yes |
|
||||||
|
| `test` | Every change | Yes |
|
||||||
|
| `proto-lint` | Any proto change | Yes |
|
||||||
|
| `proto` | After editing `.proto` files | No (manual) |
|
||||||
|
| `all` | Pre-push verification | Yes |
|
||||||
|
|
||||||
|
The `all` target is the CI pipeline: `vet → lint → test → build`. If any
|
||||||
|
step fails, the pipeline stops.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Design
|
||||||
|
|
||||||
|
Services expose two synchronized API surfaces:
|
||||||
|
|
||||||
|
### gRPC (Primary)
|
||||||
|
|
||||||
|
- Proto definitions live in `proto/<service>/v2/`.
|
||||||
|
- Use strongly-typed, per-operation RPCs. Avoid generic "execute" patterns.
|
||||||
|
- Use `google.protobuf.Timestamp` for all time fields (not RFC 3339 strings).
|
||||||
|
- Run `buf lint` and `buf breaking` against master before merging proto
|
||||||
|
changes.
|
||||||
|
|
||||||
|
### REST (Secondary)
|
||||||
|
|
||||||
|
- JSON over HTTPS. Routes live in `internal/server/routes.go`.
|
||||||
|
- Use `chi` for routing (lightweight, stdlib-compatible).
|
||||||
|
- Standard error format: `{"error": "description"}`.
|
||||||
|
- Standard HTTP status codes: `401` (unauthenticated), `403` (unauthorized),
|
||||||
|
`412` (precondition failed), `503` (service unavailable).
|
||||||
|
|
||||||
|
### API Sync Rule
|
||||||
|
|
||||||
|
**Every REST endpoint must have a corresponding gRPC RPC, and vice versa.**
|
||||||
|
When adding, removing, or changing an endpoint in either surface, the other
|
||||||
|
must be updated in the same change. This is enforced in code review.
|
||||||
|
|
||||||
|
### gRPC Interceptors
|
||||||
|
|
||||||
|
Access control is enforced via interceptor maps, not per-handler checks:
|
||||||
|
|
||||||
|
| Map | Effect |
|
||||||
|
|-----|--------|
|
||||||
|
| `sealRequiredMethods` | Returns `UNAVAILABLE` if the service is sealed/locked |
|
||||||
|
| `authRequiredMethods` | Validates MCIAS bearer token, populates caller info |
|
||||||
|
| `adminRequiredMethods` | Requires admin role on the caller |
|
||||||
|
|
||||||
|
Adding a new RPC means adding it to the correct interceptor maps. Forgetting
|
||||||
|
this is a security defect.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication & Authorization
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
All services delegate authentication to **MCIAS** (Metacircular Identity and
|
||||||
|
Access Service). No service maintains its own user database.
|
||||||
|
|
||||||
|
- Client sends credentials to the service's `/v1/auth/login` endpoint.
|
||||||
|
- The service forwards them to MCIAS via the client library
|
||||||
|
(`git.wntrmute.dev/kyle/mcias/clients/go`).
|
||||||
|
- On success, MCIAS returns a bearer token. The service returns it to the
|
||||||
|
client and optionally sets it as a cookie for the web UI.
|
||||||
|
- Subsequent requests include the token via `Authorization: Bearer <token>`
|
||||||
|
header or cookie.
|
||||||
|
- Token validation calls MCIAS `ValidateToken()`. Results should be cached
|
||||||
|
(keyed by SHA-256 of the token) with a short TTL (30 seconds or less).
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
Three role levels:
|
||||||
|
|
||||||
|
| Role | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `admin` | Full access to everything. Policy bypass. |
|
||||||
|
| `user` | Access governed by policy rules. Default deny. |
|
||||||
|
| `guest` | Service-dependent restrictions. Default deny. |
|
||||||
|
|
||||||
|
Admin detection is based solely on the MCIAS `admin` role. The service never
|
||||||
|
promotes users locally.
|
||||||
|
|
||||||
|
Services that need fine-grained access control should implement a policy
|
||||||
|
engine (priority-based ACL rules stored in encrypted storage, default deny,
|
||||||
|
admin bypass). See Metacrypt's implementation as the reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Conventions
|
||||||
|
|
||||||
|
### SQLite
|
||||||
|
|
||||||
|
SQLite is the default database for Metacircular services. It is simple to
|
||||||
|
operate, requires no external processes, and backs up cleanly with
|
||||||
|
`VACUUM INTO`.
|
||||||
|
|
||||||
|
Connection settings (applied at open time):
|
||||||
|
|
||||||
|
```go
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
PRAGMA busy_timeout = 5000;
|
||||||
|
```
|
||||||
|
|
||||||
|
File permissions: `0600`. Created by the service on first run.
|
||||||
|
|
||||||
|
### Migrations
|
||||||
|
|
||||||
|
- Migrations are Go functions registered in `internal/db/` and run
|
||||||
|
sequentially at startup.
|
||||||
|
- Each migration is idempotent — `CREATE TABLE IF NOT EXISTS`,
|
||||||
|
`ALTER TABLE ... ADD COLUMN IF NOT EXISTS`.
|
||||||
|
- Applied migrations are tracked in a `schema_migrations` table.
|
||||||
|
- Never modify a migration that has been deployed. Add a new one.
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
Every service must provide a `snapshot` CLI command that creates a consistent
|
||||||
|
backup using `VACUUM INTO`. Automated backups run via a systemd timer
|
||||||
|
(daily, with retention pruning).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
TOML. Parsed with `go-toml/v2`. Environment variable overrides via
|
||||||
|
`SERVICENAME_*` (e.g. `METACRYPT_SERVER_LISTEN_ADDR`).
|
||||||
|
|
||||||
|
### Standard Sections
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[server]
|
||||||
|
listen_addr = ":8443" # HTTPS API
|
||||||
|
grpc_addr = ":9443" # gRPC (optional; disabled if unset)
|
||||||
|
tls_cert = "/srv/<service>/certs/cert.pem"
|
||||||
|
tls_key = "/srv/<service>/certs/key.pem"
|
||||||
|
|
||||||
|
[web]
|
||||||
|
listen_addr = "127.0.0.1:8080" # Web UI (optional; disabled if unset)
|
||||||
|
vault_grpc = "127.0.0.1:9443" # gRPC address of the API server
|
||||||
|
vault_ca_cert = "" # CA cert for verifying API server TLS
|
||||||
|
|
||||||
|
[database]
|
||||||
|
path = "/srv/<service>/<service>.db"
|
||||||
|
|
||||||
|
[mcias]
|
||||||
|
server_url = "https://mcias.metacircular.net:8443"
|
||||||
|
ca_cert = "" # Custom CA for MCIAS TLS
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "info" # debug, info, warn, error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
Required fields are validated at startup. The service refuses to start if
|
||||||
|
any are missing. Do not silently default required values.
|
||||||
|
|
||||||
|
### Data Directory
|
||||||
|
|
||||||
|
All runtime data lives in `/srv/<service>/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/srv/<service>/
|
||||||
|
├── <service>.toml Configuration
|
||||||
|
├── <service>.db SQLite database
|
||||||
|
├── certs/ TLS certificates
|
||||||
|
└── backups/ Database snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
This convention enables straightforward service migration between hosts:
|
||||||
|
copy `/srv/<service>/` and the binary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web UI
|
||||||
|
|
||||||
|
### Technology
|
||||||
|
|
||||||
|
- **Go `html/template`** for server-side rendering. No JavaScript frameworks.
|
||||||
|
- **htmx** for dynamic interactions (form submission, partial page updates)
|
||||||
|
without full page reloads.
|
||||||
|
- Templates and static files are embedded in the binary via `//go:embed`.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
- `web/templates/layout.html` — shared HTML skeleton, navigation, CSS/JS
|
||||||
|
includes. All page templates extend this.
|
||||||
|
- Page templates: one `.html` file per page/feature.
|
||||||
|
- `web/static/` — CSS, htmx. Keep this minimal.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
The web UI runs as a separate binary (`<service>-web`) that communicates
|
||||||
|
with the API server via its gRPC interface. This separation means:
|
||||||
|
|
||||||
|
- The web UI has no direct database access.
|
||||||
|
- The API server enforces all authorization.
|
||||||
|
- The web UI can be deployed independently or omitted entirely.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- CSRF protection via signed double-submit cookies on all mutating requests
|
||||||
|
(POST/PUT/PATCH/DELETE).
|
||||||
|
- Session cookie: `HttpOnly`, `Secure`, `SameSite=Strict`.
|
||||||
|
- All user input is escaped by `html/template` (the default).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Philosophy
|
||||||
|
|
||||||
|
Tests are written using the Go standard library `testing` package. No test
|
||||||
|
frameworks (testify, gomega, etc.) — the standard library is sufficient and
|
||||||
|
keeps dependencies minimal.
|
||||||
|
|
||||||
|
### Patterns
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestFeatureName(t *testing.T) {
|
||||||
|
// Setup: use t.TempDir() for isolated file system state.
|
||||||
|
dir := t.TempDir()
|
||||||
|
database, err := db.Open(filepath.Join(dir, "test.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = database.Close() }()
|
||||||
|
db.Migrate(database)
|
||||||
|
|
||||||
|
// Exercise the code under test.
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Assert with t.Fatal (not t.Error) for precondition failures.
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
- **Use `t.TempDir()`** for all file-system state. Never write to fixed
|
||||||
|
paths. Cleanup is automatic.
|
||||||
|
- **Use `errors.Is`** for error assertions, not string comparison.
|
||||||
|
- **No mocks for databases.** Tests use real SQLite databases created in
|
||||||
|
temp directories. This catches migration bugs that mocks would hide.
|
||||||
|
- **Test files** live alongside the code they test: `barrier.go` and
|
||||||
|
`barrier_test.go` in the same package.
|
||||||
|
- **Test helpers** call `t.Helper()` so failures report the caller's line.
|
||||||
|
|
||||||
|
### What to Test
|
||||||
|
|
||||||
|
| Layer | Test Strategy |
|
||||||
|
|-------|---------------|
|
||||||
|
| Crypto primitives | Roundtrip encryption/decryption, wrong-key rejection, edge cases |
|
||||||
|
| Storage (barrier, DB) | CRUD operations, sealed-state rejection, concurrent access |
|
||||||
|
| API handlers | Request/response correctness, auth enforcement, error codes |
|
||||||
|
| Policy engine | Rule matching, priority ordering, default deny, admin bypass |
|
||||||
|
| CLI commands | Flag parsing, output format (lightweight) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linting & Static Analysis
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Every repository includes a `.golangci.yaml` with this philosophy:
|
||||||
|
**fail loudly for security and correctness; everything else is a warning.**
|
||||||
|
|
||||||
|
### Required Linters
|
||||||
|
|
||||||
|
| Linter | Category | Purpose |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| `errcheck` | Correctness | Unhandled errors are silent failures |
|
||||||
|
| `govet` | Correctness | Printf mismatches, unreachable code, suspicious constructs |
|
||||||
|
| `ineffassign` | Correctness | Dead writes hide logic bugs |
|
||||||
|
| `unused` | Correctness | Unused variables and functions |
|
||||||
|
| `errorlint` | Error handling | Proper `errors.Is`/`errors.As` usage |
|
||||||
|
| `gosec` | Security | Hardcoded secrets, weak RNG, insecure crypto, SQL injection |
|
||||||
|
| `staticcheck` | Security | Deprecated APIs, mutex misuse, deep analysis |
|
||||||
|
| `revive` | Style | Go naming conventions, error return ordering |
|
||||||
|
| `gofmt` | Formatting | Standard Go formatting |
|
||||||
|
| `goimports` | Formatting | Import grouping and ordering |
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
- `errcheck`: `check-type-assertions: true` (catch `x.(*T)` without ok check).
|
||||||
|
- `govet`: all analyzers enabled except `shadow` (too noisy for idiomatic Go).
|
||||||
|
- `gosec`: severity and confidence set to `medium`. Exclude `G104` (overlaps
|
||||||
|
with errcheck).
|
||||||
|
- `max-issues-per-linter: 0` — report everything. No caps.
|
||||||
|
- Test files: allow `G101` (hardcoded credentials) for test fixtures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Container-First
|
||||||
|
|
||||||
|
Services are designed for container deployment but must also run as native
|
||||||
|
systemd services. Both paths are first-class.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Multi-stage builds:
|
||||||
|
|
||||||
|
1. **Builder**: `golang:1.23-alpine`. Compile with `CGO_ENABLED=0`, strip
|
||||||
|
symbols.
|
||||||
|
2. **Runtime**: `alpine:3.21`. Non-root user (`<service>`), minimal attack
|
||||||
|
surface.
|
||||||
|
|
||||||
|
If the service has separate API and web binaries, use separate Dockerfiles
|
||||||
|
(`Dockerfile.api`, `Dockerfile.web`) and a `docker-compose.yml` that wires
|
||||||
|
them together with a shared data volume.
|
||||||
|
|
||||||
|
### systemd
|
||||||
|
|
||||||
|
Every service ships with:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `<service>.service` | Main service unit (API server) |
|
||||||
|
| `<service>-web.service` | Web UI unit (if applicable) |
|
||||||
|
| `<service>-backup.service` | Oneshot backup unit |
|
||||||
|
| `<service>-backup.timer` | Daily backup timer (02:00 UTC, 5-minute jitter) |
|
||||||
|
|
||||||
|
#### Security Hardening
|
||||||
|
|
||||||
|
All service units must include these security directives:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictNamespaces=true
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
ReadWritePaths=/srv/<service>
|
||||||
|
```
|
||||||
|
|
||||||
|
The web UI unit should use `ReadOnlyPaths=/srv/<service>` instead of
|
||||||
|
`ReadWritePaths` — it has no reason to write to the data directory.
|
||||||
|
|
||||||
|
### Install Script
|
||||||
|
|
||||||
|
`deploy/scripts/install.sh` handles:
|
||||||
|
|
||||||
|
1. Create system user/group (idempotent).
|
||||||
|
2. Install binary to `/usr/local/bin/`.
|
||||||
|
3. Create `/srv/<service>/` directory structure.
|
||||||
|
4. Install example config if none exists.
|
||||||
|
5. Install systemd units and reload the daemon.
|
||||||
|
|
||||||
|
### TLS
|
||||||
|
|
||||||
|
- **Minimum TLS version: 1.3.** No exceptions, no fallback cipher suites.
|
||||||
|
Go's TLS 1.3 implementation manages cipher selection automatically.
|
||||||
|
- **Timeouts**: read 30s, write 30s, idle 120s.
|
||||||
|
- Certificate and key paths are required configuration — the service refuses
|
||||||
|
to start without them.
|
||||||
|
|
||||||
|
### Graceful Shutdown
|
||||||
|
|
||||||
|
Services handle `SIGINT` and `SIGTERM`, shutting down cleanly:
|
||||||
|
|
||||||
|
1. Stop accepting new connections.
|
||||||
|
2. Drain in-flight requests (with a timeout).
|
||||||
|
3. Clean up resources (close databases, zeroize secrets if applicable).
|
||||||
|
4. Exit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Required Files
|
||||||
|
|
||||||
|
| File | Purpose | Audience |
|
||||||
|
|------|---------|----------|
|
||||||
|
| `CLAUDE.md` | AI-assisted development context | Claude Code |
|
||||||
|
| `ARCHITECTURE.md` | Full system specification | Engineers |
|
||||||
|
| `deploy/examples/<service>.toml` | Example configuration | Operators |
|
||||||
|
|
||||||
|
### CLAUDE.md
|
||||||
|
|
||||||
|
This file provides context for AI-assisted development. It should contain:
|
||||||
|
|
||||||
|
- Project overview (one paragraph).
|
||||||
|
- Build, test, and lint commands.
|
||||||
|
- High-level architecture summary.
|
||||||
|
- Project structure with directory descriptions.
|
||||||
|
- Ignored directories (runtime data, generated code).
|
||||||
|
- Critical rules (e.g. API sync requirements).
|
||||||
|
|
||||||
|
Keep it concise. AI tools read this on every interaction.
|
||||||
|
|
||||||
|
### ARCHITECTURE.md
|
||||||
|
|
||||||
|
This is the canonical specification for the service. It should cover:
|
||||||
|
|
||||||
|
1. System overview with a layered architecture diagram.
|
||||||
|
2. Cryptographic design (if applicable): algorithms, key hierarchy.
|
||||||
|
3. State machines and lifecycle (if applicable).
|
||||||
|
4. Storage design.
|
||||||
|
5. Authentication and authorization model.
|
||||||
|
6. API surface (REST and gRPC, with tables of every endpoint).
|
||||||
|
7. Web interface routes.
|
||||||
|
8. Database schema (every table, every column).
|
||||||
|
9. Configuration reference.
|
||||||
|
10. Deployment guide.
|
||||||
|
11. Security model: threat mitigations table and security invariants.
|
||||||
|
12. Future work.
|
||||||
|
|
||||||
|
This document is the source of truth. When the code and the spec disagree,
|
||||||
|
one of them has a bug.
|
||||||
|
|
||||||
|
### Engine/Feature Design Documents
|
||||||
|
|
||||||
|
For services with a modular architecture, each module gets its own design
|
||||||
|
document (e.g. `engines/sshca.md`). These are detailed implementation plans
|
||||||
|
that include:
|
||||||
|
|
||||||
|
- Overview and core concepts.
|
||||||
|
- Data model and storage layout.
|
||||||
|
- Lifecycle (initialization, teardown).
|
||||||
|
- Operations table with auth requirements.
|
||||||
|
- API definitions (gRPC and REST).
|
||||||
|
- Implementation steps (file-by-file).
|
||||||
|
- Security considerations.
|
||||||
|
- References to existing code patterns to follow.
|
||||||
|
|
||||||
|
Write these before writing code. They are the blueprint, not the afterthought.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### General Principles
|
||||||
|
|
||||||
|
- **Default deny.** Unauthenticated requests are rejected. Unauthorized
|
||||||
|
requests are rejected. If in doubt, deny.
|
||||||
|
- **Fail closed.** If the service cannot verify authorization, it denies the
|
||||||
|
request. If the database is unavailable, the service is unavailable.
|
||||||
|
- **Least privilege.** Service processes run as non-root. systemd units
|
||||||
|
restrict filesystem access, syscalls, and capabilities.
|
||||||
|
- **No local user databases.** Authentication is always delegated to MCIAS.
|
||||||
|
|
||||||
|
### Cryptographic Standards
|
||||||
|
|
||||||
|
| Purpose | Algorithm | Notes |
|
||||||
|
|---------|-----------|-------|
|
||||||
|
| Symmetric encryption | AES-256-GCM | 12-byte random nonce per operation |
|
||||||
|
| Symmetric alternative | XChaCha20-Poly1305 | For contexts needing nonce misuse resistance |
|
||||||
|
| Key derivation | Argon2id | Memory-hard; tune params to hardware |
|
||||||
|
| Asymmetric signing | Ed25519, ECDSA (P-256, P-384) | Prefer Ed25519 |
|
||||||
|
| CSPRNG | `crypto/rand` | All keys, nonces, salts, tokens |
|
||||||
|
| Constant-time comparison | `crypto/subtle` | All secret comparisons |
|
||||||
|
|
||||||
|
- **Never use RSA for new designs.** Ed25519 and ECDSA are faster, produce
|
||||||
|
smaller keys, and have simpler security models.
|
||||||
|
- **Zeroize secrets** from memory when they are no longer needed. Overwrite
|
||||||
|
byte slices with zeros, nil out pointers.
|
||||||
|
- **Never log secrets.** Keys, passwords, tokens, and plaintext must never
|
||||||
|
appear in log output.
|
||||||
|
|
||||||
|
### Web Security
|
||||||
|
|
||||||
|
- CSRF tokens on all mutating requests.
|
||||||
|
- `SameSite=Strict` on all cookies.
|
||||||
|
- `html/template` for automatic escaping.
|
||||||
|
- Validate all input at system boundaries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and run both servers locally:
|
||||||
|
make devserver
|
||||||
|
|
||||||
|
# Or build everything and run the full pipeline:
|
||||||
|
make all
|
||||||
|
```
|
||||||
|
|
||||||
|
The `devserver` target builds both binaries and runs them against a local
|
||||||
|
config in `srv/`. The `srv/` directory is gitignored — it holds your local
|
||||||
|
database, certificates, and configuration.
|
||||||
|
|
||||||
|
### Pre-Push Checklist
|
||||||
|
|
||||||
|
Before pushing a branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make all # vet → lint → test → build
|
||||||
|
make proto-lint # if proto files changed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proto Changes
|
||||||
|
|
||||||
|
1. Edit `.proto` files in `proto/<service>/v2/`.
|
||||||
|
2. Run `make proto` to regenerate Go code.
|
||||||
|
3. Run `make proto-lint` to check for linting violations and breaking changes.
|
||||||
|
4. Update REST routes to match the new/changed RPCs.
|
||||||
|
5. Update gRPC interceptor maps for any new RPCs.
|
||||||
|
6. Update `ARCHITECTURE.md` API tables.
|
||||||
|
|
||||||
|
### Adding a New Feature
|
||||||
|
|
||||||
|
1. **Design first.** Write or update the relevant design document. For a new
|
||||||
|
engine or major subsystem, create a new doc in `docs/` or `engines/`.
|
||||||
|
2. **Implement.** Follow existing patterns — the design doc should reference
|
||||||
|
specific files and line numbers.
|
||||||
|
3. **Test.** Write tests alongside the implementation.
|
||||||
|
4. **Update docs.** Update `ARCHITECTURE.md`, `CLAUDE.md`, and route tables.
|
||||||
|
5. **Verify.** Run `make all`.
|
||||||
|
|
||||||
|
### CLI Commands
|
||||||
|
|
||||||
|
Every service uses cobra for CLI commands. Standard subcommands:
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `server` | Start the service |
|
||||||
|
| `init` | First-time setup (if applicable) |
|
||||||
|
| `status` | Query a running instance's health |
|
||||||
|
| `snapshot` | Create a database backup |
|
||||||
|
|
||||||
|
Add service-specific subcommands as needed (e.g. `migrate-aad`, `unseal`).
|
||||||
|
Each command lives in its own file in `cmd/<service>/`.
|
||||||
2197
gen/metacrypt/v2/transit.pb.go
Normal file
2197
gen/metacrypt/v2/transit.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
777
gen/metacrypt/v2/transit_grpc.pb.go
Normal file
777
gen/metacrypt/v2/transit_grpc.pb.go
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
// 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/transit.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 (
|
||||||
|
TransitService_CreateKey_FullMethodName = "/metacrypt.v2.TransitService/CreateKey"
|
||||||
|
TransitService_DeleteKey_FullMethodName = "/metacrypt.v2.TransitService/DeleteKey"
|
||||||
|
TransitService_GetKey_FullMethodName = "/metacrypt.v2.TransitService/GetKey"
|
||||||
|
TransitService_ListKeys_FullMethodName = "/metacrypt.v2.TransitService/ListKeys"
|
||||||
|
TransitService_RotateKey_FullMethodName = "/metacrypt.v2.TransitService/RotateKey"
|
||||||
|
TransitService_UpdateKeyConfig_FullMethodName = "/metacrypt.v2.TransitService/UpdateKeyConfig"
|
||||||
|
TransitService_TrimKey_FullMethodName = "/metacrypt.v2.TransitService/TrimKey"
|
||||||
|
TransitService_Encrypt_FullMethodName = "/metacrypt.v2.TransitService/Encrypt"
|
||||||
|
TransitService_Decrypt_FullMethodName = "/metacrypt.v2.TransitService/Decrypt"
|
||||||
|
TransitService_Rewrap_FullMethodName = "/metacrypt.v2.TransitService/Rewrap"
|
||||||
|
TransitService_BatchEncrypt_FullMethodName = "/metacrypt.v2.TransitService/BatchEncrypt"
|
||||||
|
TransitService_BatchDecrypt_FullMethodName = "/metacrypt.v2.TransitService/BatchDecrypt"
|
||||||
|
TransitService_BatchRewrap_FullMethodName = "/metacrypt.v2.TransitService/BatchRewrap"
|
||||||
|
TransitService_Sign_FullMethodName = "/metacrypt.v2.TransitService/Sign"
|
||||||
|
TransitService_Verify_FullMethodName = "/metacrypt.v2.TransitService/Verify"
|
||||||
|
TransitService_Hmac_FullMethodName = "/metacrypt.v2.TransitService/Hmac"
|
||||||
|
TransitService_GetPublicKey_FullMethodName = "/metacrypt.v2.TransitService/GetPublicKey"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransitServiceClient is the client API for TransitService 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.
|
||||||
|
//
|
||||||
|
// TransitService provides typed, authenticated access to transit engine
|
||||||
|
// operations: symmetric encryption, signing, HMAC, and versioned key
|
||||||
|
// management. All RPCs require the service to be unsealed.
|
||||||
|
type TransitServiceClient interface {
|
||||||
|
// CreateKey creates a new named encryption key. Admin only.
|
||||||
|
CreateKey(ctx context.Context, in *CreateTransitKeyRequest, opts ...grpc.CallOption) (*CreateTransitKeyResponse, error)
|
||||||
|
// DeleteKey permanently removes a named key. Admin only.
|
||||||
|
// Only succeeds if allow_deletion is true on the key config.
|
||||||
|
DeleteKey(ctx context.Context, in *DeleteTransitKeyRequest, opts ...grpc.CallOption) (*DeleteTransitKeyResponse, error)
|
||||||
|
// GetKey returns metadata for a named key (no raw material). Auth required.
|
||||||
|
GetKey(ctx context.Context, in *GetTransitKeyRequest, opts ...grpc.CallOption) (*GetTransitKeyResponse, error)
|
||||||
|
// ListKeys returns the names of all configured keys. Auth required.
|
||||||
|
ListKeys(ctx context.Context, in *ListTransitKeysRequest, opts ...grpc.CallOption) (*ListTransitKeysResponse, error)
|
||||||
|
// RotateKey creates a new version of the named key. Admin only.
|
||||||
|
RotateKey(ctx context.Context, in *RotateTransitKeyRequest, opts ...grpc.CallOption) (*RotateTransitKeyResponse, error)
|
||||||
|
// UpdateKeyConfig updates key configuration (e.g. min_decryption_version).
|
||||||
|
// Admin only.
|
||||||
|
UpdateKeyConfig(ctx context.Context, in *UpdateTransitKeyConfigRequest, opts ...grpc.CallOption) (*UpdateTransitKeyConfigResponse, error)
|
||||||
|
// TrimKey deletes versions below min_decryption_version. Admin only.
|
||||||
|
TrimKey(ctx context.Context, in *TrimTransitKeyRequest, opts ...grpc.CallOption) (*TrimTransitKeyResponse, error)
|
||||||
|
// Encrypt encrypts plaintext with the latest key version. Auth required.
|
||||||
|
Encrypt(ctx context.Context, in *TransitEncryptRequest, opts ...grpc.CallOption) (*TransitEncryptResponse, error)
|
||||||
|
// Decrypt decrypts ciphertext. Auth required.
|
||||||
|
Decrypt(ctx context.Context, in *TransitDecryptRequest, opts ...grpc.CallOption) (*TransitDecryptResponse, error)
|
||||||
|
// Rewrap re-encrypts ciphertext with the latest key version without
|
||||||
|
// exposing plaintext. Auth required.
|
||||||
|
Rewrap(ctx context.Context, in *TransitRewrapRequest, opts ...grpc.CallOption) (*TransitRewrapResponse, error)
|
||||||
|
// BatchEncrypt encrypts multiple items in a single request. Auth required.
|
||||||
|
BatchEncrypt(ctx context.Context, in *TransitBatchEncryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error)
|
||||||
|
// BatchDecrypt decrypts multiple items in a single request. Auth required.
|
||||||
|
BatchDecrypt(ctx context.Context, in *TransitBatchDecryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error)
|
||||||
|
// BatchRewrap re-encrypts multiple items in a single request. Auth required.
|
||||||
|
BatchRewrap(ctx context.Context, in *TransitBatchRewrapRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error)
|
||||||
|
// Sign signs input data with an asymmetric key. Auth required.
|
||||||
|
Sign(ctx context.Context, in *TransitSignRequest, opts ...grpc.CallOption) (*TransitSignResponse, error)
|
||||||
|
// Verify verifies a signature against input data. Auth required.
|
||||||
|
Verify(ctx context.Context, in *TransitVerifyRequest, opts ...grpc.CallOption) (*TransitVerifyResponse, error)
|
||||||
|
// Hmac computes or verifies an HMAC. Auth required.
|
||||||
|
Hmac(ctx context.Context, in *TransitHmacRequest, opts ...grpc.CallOption) (*TransitHmacResponse, error)
|
||||||
|
// GetPublicKey returns the public key for an asymmetric key. Auth required.
|
||||||
|
GetPublicKey(ctx context.Context, in *GetTransitPublicKeyRequest, opts ...grpc.CallOption) (*GetTransitPublicKeyResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type transitServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransitServiceClient(cc grpc.ClientConnInterface) TransitServiceClient {
|
||||||
|
return &transitServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) CreateKey(ctx context.Context, in *CreateTransitKeyRequest, opts ...grpc.CallOption) (*CreateTransitKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateTransitKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_CreateKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) DeleteKey(ctx context.Context, in *DeleteTransitKeyRequest, opts ...grpc.CallOption) (*DeleteTransitKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(DeleteTransitKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_DeleteKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) GetKey(ctx context.Context, in *GetTransitKeyRequest, opts ...grpc.CallOption) (*GetTransitKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetTransitKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_GetKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) ListKeys(ctx context.Context, in *ListTransitKeysRequest, opts ...grpc.CallOption) (*ListTransitKeysResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListTransitKeysResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_ListKeys_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) RotateKey(ctx context.Context, in *RotateTransitKeyRequest, opts ...grpc.CallOption) (*RotateTransitKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RotateTransitKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_RotateKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) UpdateKeyConfig(ctx context.Context, in *UpdateTransitKeyConfigRequest, opts ...grpc.CallOption) (*UpdateTransitKeyConfigResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpdateTransitKeyConfigResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_UpdateKeyConfig_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) TrimKey(ctx context.Context, in *TrimTransitKeyRequest, opts ...grpc.CallOption) (*TrimTransitKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TrimTransitKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_TrimKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) Encrypt(ctx context.Context, in *TransitEncryptRequest, opts ...grpc.CallOption) (*TransitEncryptResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitEncryptResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_Encrypt_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) Decrypt(ctx context.Context, in *TransitDecryptRequest, opts ...grpc.CallOption) (*TransitDecryptResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitDecryptResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_Decrypt_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) Rewrap(ctx context.Context, in *TransitRewrapRequest, opts ...grpc.CallOption) (*TransitRewrapResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitRewrapResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_Rewrap_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) BatchEncrypt(ctx context.Context, in *TransitBatchEncryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitBatchResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_BatchEncrypt_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) BatchDecrypt(ctx context.Context, in *TransitBatchDecryptRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitBatchResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_BatchDecrypt_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) BatchRewrap(ctx context.Context, in *TransitBatchRewrapRequest, opts ...grpc.CallOption) (*TransitBatchResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitBatchResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_BatchRewrap_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) Sign(ctx context.Context, in *TransitSignRequest, opts ...grpc.CallOption) (*TransitSignResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitSignResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_Sign_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) Verify(ctx context.Context, in *TransitVerifyRequest, opts ...grpc.CallOption) (*TransitVerifyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitVerifyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_Verify_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) Hmac(ctx context.Context, in *TransitHmacRequest, opts ...grpc.CallOption) (*TransitHmacResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransitHmacResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_Hmac_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *transitServiceClient) GetPublicKey(ctx context.Context, in *GetTransitPublicKeyRequest, opts ...grpc.CallOption) (*GetTransitPublicKeyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetTransitPublicKeyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, TransitService_GetPublicKey_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransitServiceServer is the server API for TransitService service.
|
||||||
|
// All implementations must embed UnimplementedTransitServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// TransitService provides typed, authenticated access to transit engine
|
||||||
|
// operations: symmetric encryption, signing, HMAC, and versioned key
|
||||||
|
// management. All RPCs require the service to be unsealed.
|
||||||
|
type TransitServiceServer interface {
|
||||||
|
// CreateKey creates a new named encryption key. Admin only.
|
||||||
|
CreateKey(context.Context, *CreateTransitKeyRequest) (*CreateTransitKeyResponse, error)
|
||||||
|
// DeleteKey permanently removes a named key. Admin only.
|
||||||
|
// Only succeeds if allow_deletion is true on the key config.
|
||||||
|
DeleteKey(context.Context, *DeleteTransitKeyRequest) (*DeleteTransitKeyResponse, error)
|
||||||
|
// GetKey returns metadata for a named key (no raw material). Auth required.
|
||||||
|
GetKey(context.Context, *GetTransitKeyRequest) (*GetTransitKeyResponse, error)
|
||||||
|
// ListKeys returns the names of all configured keys. Auth required.
|
||||||
|
ListKeys(context.Context, *ListTransitKeysRequest) (*ListTransitKeysResponse, error)
|
||||||
|
// RotateKey creates a new version of the named key. Admin only.
|
||||||
|
RotateKey(context.Context, *RotateTransitKeyRequest) (*RotateTransitKeyResponse, error)
|
||||||
|
// UpdateKeyConfig updates key configuration (e.g. min_decryption_version).
|
||||||
|
// Admin only.
|
||||||
|
UpdateKeyConfig(context.Context, *UpdateTransitKeyConfigRequest) (*UpdateTransitKeyConfigResponse, error)
|
||||||
|
// TrimKey deletes versions below min_decryption_version. Admin only.
|
||||||
|
TrimKey(context.Context, *TrimTransitKeyRequest) (*TrimTransitKeyResponse, error)
|
||||||
|
// Encrypt encrypts plaintext with the latest key version. Auth required.
|
||||||
|
Encrypt(context.Context, *TransitEncryptRequest) (*TransitEncryptResponse, error)
|
||||||
|
// Decrypt decrypts ciphertext. Auth required.
|
||||||
|
Decrypt(context.Context, *TransitDecryptRequest) (*TransitDecryptResponse, error)
|
||||||
|
// Rewrap re-encrypts ciphertext with the latest key version without
|
||||||
|
// exposing plaintext. Auth required.
|
||||||
|
Rewrap(context.Context, *TransitRewrapRequest) (*TransitRewrapResponse, error)
|
||||||
|
// BatchEncrypt encrypts multiple items in a single request. Auth required.
|
||||||
|
BatchEncrypt(context.Context, *TransitBatchEncryptRequest) (*TransitBatchResponse, error)
|
||||||
|
// BatchDecrypt decrypts multiple items in a single request. Auth required.
|
||||||
|
BatchDecrypt(context.Context, *TransitBatchDecryptRequest) (*TransitBatchResponse, error)
|
||||||
|
// BatchRewrap re-encrypts multiple items in a single request. Auth required.
|
||||||
|
BatchRewrap(context.Context, *TransitBatchRewrapRequest) (*TransitBatchResponse, error)
|
||||||
|
// Sign signs input data with an asymmetric key. Auth required.
|
||||||
|
Sign(context.Context, *TransitSignRequest) (*TransitSignResponse, error)
|
||||||
|
// Verify verifies a signature against input data. Auth required.
|
||||||
|
Verify(context.Context, *TransitVerifyRequest) (*TransitVerifyResponse, error)
|
||||||
|
// Hmac computes or verifies an HMAC. Auth required.
|
||||||
|
Hmac(context.Context, *TransitHmacRequest) (*TransitHmacResponse, error)
|
||||||
|
// GetPublicKey returns the public key for an asymmetric key. Auth required.
|
||||||
|
GetPublicKey(context.Context, *GetTransitPublicKeyRequest) (*GetTransitPublicKeyResponse, error)
|
||||||
|
mustEmbedUnimplementedTransitServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedTransitServiceServer 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 UnimplementedTransitServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedTransitServiceServer) CreateKey(context.Context, *CreateTransitKeyRequest) (*CreateTransitKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method CreateKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) DeleteKey(context.Context, *DeleteTransitKeyRequest) (*DeleteTransitKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method DeleteKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) GetKey(context.Context, *GetTransitKeyRequest) (*GetTransitKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) ListKeys(context.Context, *ListTransitKeysRequest) (*ListTransitKeysResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListKeys not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) RotateKey(context.Context, *RotateTransitKeyRequest) (*RotateTransitKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method RotateKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) UpdateKeyConfig(context.Context, *UpdateTransitKeyConfigRequest) (*UpdateTransitKeyConfigResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method UpdateKeyConfig not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) TrimKey(context.Context, *TrimTransitKeyRequest) (*TrimTransitKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method TrimKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) Encrypt(context.Context, *TransitEncryptRequest) (*TransitEncryptResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Encrypt not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) Decrypt(context.Context, *TransitDecryptRequest) (*TransitDecryptResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Decrypt not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) Rewrap(context.Context, *TransitRewrapRequest) (*TransitRewrapResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Rewrap not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) BatchEncrypt(context.Context, *TransitBatchEncryptRequest) (*TransitBatchResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method BatchEncrypt not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) BatchDecrypt(context.Context, *TransitBatchDecryptRequest) (*TransitBatchResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method BatchDecrypt not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) BatchRewrap(context.Context, *TransitBatchRewrapRequest) (*TransitBatchResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method BatchRewrap not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) Sign(context.Context, *TransitSignRequest) (*TransitSignResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Sign not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) Verify(context.Context, *TransitVerifyRequest) (*TransitVerifyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Verify not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) Hmac(context.Context, *TransitHmacRequest) (*TransitHmacResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method Hmac not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) GetPublicKey(context.Context, *GetTransitPublicKeyRequest) (*GetTransitPublicKeyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetPublicKey not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedTransitServiceServer) mustEmbedUnimplementedTransitServiceServer() {}
|
||||||
|
func (UnimplementedTransitServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeTransitServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to TransitServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeTransitServiceServer interface {
|
||||||
|
mustEmbedUnimplementedTransitServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterTransitServiceServer(s grpc.ServiceRegistrar, srv TransitServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedTransitServiceServer 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(&TransitService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_CreateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateTransitKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).CreateKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_CreateKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).CreateKey(ctx, req.(*CreateTransitKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_DeleteKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(DeleteTransitKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).DeleteKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_DeleteKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).DeleteKey(ctx, req.(*DeleteTransitKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_GetKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetTransitKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).GetKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_GetKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).GetKey(ctx, req.(*GetTransitKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_ListKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListTransitKeysRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).ListKeys(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_ListKeys_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).ListKeys(ctx, req.(*ListTransitKeysRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_RotateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RotateTransitKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).RotateKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_RotateKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).RotateKey(ctx, req.(*RotateTransitKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_UpdateKeyConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateTransitKeyConfigRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).UpdateKeyConfig(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_UpdateKeyConfig_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).UpdateKeyConfig(ctx, req.(*UpdateTransitKeyConfigRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_TrimKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TrimTransitKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).TrimKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_TrimKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).TrimKey(ctx, req.(*TrimTransitKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitEncryptRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).Encrypt(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_Encrypt_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).Encrypt(ctx, req.(*TransitEncryptRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitDecryptRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).Decrypt(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_Decrypt_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).Decrypt(ctx, req.(*TransitDecryptRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_Rewrap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitRewrapRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).Rewrap(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_Rewrap_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).Rewrap(ctx, req.(*TransitRewrapRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_BatchEncrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitBatchEncryptRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).BatchEncrypt(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_BatchEncrypt_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).BatchEncrypt(ctx, req.(*TransitBatchEncryptRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_BatchDecrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitBatchDecryptRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).BatchDecrypt(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_BatchDecrypt_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).BatchDecrypt(ctx, req.(*TransitBatchDecryptRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_BatchRewrap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitBatchRewrapRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).BatchRewrap(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_BatchRewrap_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).BatchRewrap(ctx, req.(*TransitBatchRewrapRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitSignRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).Sign(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_Sign_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).Sign(ctx, req.(*TransitSignRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_Verify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitVerifyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).Verify(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_Verify_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).Verify(ctx, req.(*TransitVerifyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_Hmac_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransitHmacRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).Hmac(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_Hmac_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).Hmac(ctx, req.(*TransitHmacRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _TransitService_GetPublicKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetTransitPublicKeyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(TransitServiceServer).GetPublicKey(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: TransitService_GetPublicKey_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(TransitServiceServer).GetPublicKey(ctx, req.(*GetTransitPublicKeyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransitService_ServiceDesc is the grpc.ServiceDesc for TransitService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var TransitService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "metacrypt.v2.TransitService",
|
||||||
|
HandlerType: (*TransitServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "CreateKey",
|
||||||
|
Handler: _TransitService_CreateKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DeleteKey",
|
||||||
|
Handler: _TransitService_DeleteKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetKey",
|
||||||
|
Handler: _TransitService_GetKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListKeys",
|
||||||
|
Handler: _TransitService_ListKeys_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RotateKey",
|
||||||
|
Handler: _TransitService_RotateKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateKeyConfig",
|
||||||
|
Handler: _TransitService_UpdateKeyConfig_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "TrimKey",
|
||||||
|
Handler: _TransitService_TrimKey_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Encrypt",
|
||||||
|
Handler: _TransitService_Encrypt_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Decrypt",
|
||||||
|
Handler: _TransitService_Decrypt_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Rewrap",
|
||||||
|
Handler: _TransitService_Rewrap_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "BatchEncrypt",
|
||||||
|
Handler: _TransitService_BatchEncrypt_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "BatchDecrypt",
|
||||||
|
Handler: _TransitService_BatchDecrypt_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "BatchRewrap",
|
||||||
|
Handler: _TransitService_BatchRewrap_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Sign",
|
||||||
|
Handler: _TransitService_Sign_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Verify",
|
||||||
|
Handler: _TransitService_Verify_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Hmac",
|
||||||
|
Handler: _TransitService_Hmac_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetPublicKey",
|
||||||
|
Handler: _TransitService_GetPublicKey_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "proto/metacrypt/v2/transit.proto",
|
||||||
|
}
|
||||||
28
internal/engine/helpers.go
Normal file
28
internal/engine/helpers.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ZeroizeKey overwrites an asymmetric private key's sensitive material.
|
||||||
|
func ZeroizeKey(key crypto.PrivateKey) {
|
||||||
|
if key == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch k := key.(type) {
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
k.D.SetInt64(0)
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
k.D.SetInt64(0)
|
||||||
|
for _, p := range k.Primes {
|
||||||
|
p.SetInt64(0)
|
||||||
|
}
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
for i := range k {
|
||||||
|
k[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1602
internal/engine/transit/transit.go
Normal file
1602
internal/engine/transit/transit.go
Normal file
File diff suppressed because it is too large
Load Diff
1025
internal/engine/transit/transit_test.go
Normal file
1025
internal/engine/transit/transit_test.go
Normal file
File diff suppressed because it is too large
Load Diff
15
internal/engine/transit/types.go
Normal file
15
internal/engine/transit/types.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package transit
|
||||||
|
|
||||||
|
// TransitConfig is the transit engine configuration stored in the barrier.
|
||||||
|
type TransitConfig struct {
|
||||||
|
MaxKeyVersions int `json:"max_key_versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyConfig is per-key configuration stored in the barrier.
|
||||||
|
type KeyConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"` // aes256-gcm, chacha20-poly, ed25519, ecdsa-p256, ecdsa-p384, hmac-sha256, hmac-sha512
|
||||||
|
CurrentVersion int `json:"current_version"`
|
||||||
|
MinDecryptionVersion int `json:"min_decryption_version"`
|
||||||
|
AllowDeletion bool `json:"allow_deletion"`
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@ func (s *GRPCServer) Start() error {
|
|||||||
pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s})
|
pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s})
|
||||||
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
|
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
|
||||||
pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s})
|
pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s})
|
||||||
|
pb.RegisterTransitServiceServer(s.srv, &transitServer{s: s})
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr)
|
lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -157,6 +158,24 @@ func sealRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
||||||
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
||||||
"/metacrypt.v2.SSHCAService/GetKRL": true,
|
"/metacrypt.v2.SSHCAService/GetKRL": true,
|
||||||
|
// Transit.
|
||||||
|
"/metacrypt.v2.TransitService/CreateKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/DeleteKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/GetKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/ListKeys": true,
|
||||||
|
"/metacrypt.v2.TransitService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
||||||
|
"/metacrypt.v2.TransitService/TrimKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/Encrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/Decrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/Rewrap": true,
|
||||||
|
"/metacrypt.v2.TransitService/BatchEncrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/BatchDecrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/BatchRewrap": true,
|
||||||
|
"/metacrypt.v2.TransitService/Sign": true,
|
||||||
|
"/metacrypt.v2.TransitService/Verify": true,
|
||||||
|
"/metacrypt.v2.TransitService/Hmac": true,
|
||||||
|
"/metacrypt.v2.TransitService/GetPublicKey": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +222,24 @@ func authRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.SSHCAService/ListCerts": true,
|
"/metacrypt.v2.SSHCAService/ListCerts": true,
|
||||||
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
||||||
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
||||||
|
// Transit.
|
||||||
|
"/metacrypt.v2.TransitService/CreateKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/DeleteKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/GetKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/ListKeys": true,
|
||||||
|
"/metacrypt.v2.TransitService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
||||||
|
"/metacrypt.v2.TransitService/TrimKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/Encrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/Decrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/Rewrap": true,
|
||||||
|
"/metacrypt.v2.TransitService/BatchEncrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/BatchDecrypt": true,
|
||||||
|
"/metacrypt.v2.TransitService/BatchRewrap": true,
|
||||||
|
"/metacrypt.v2.TransitService/Sign": true,
|
||||||
|
"/metacrypt.v2.TransitService/Verify": true,
|
||||||
|
"/metacrypt.v2.TransitService/Hmac": true,
|
||||||
|
"/metacrypt.v2.TransitService/GetPublicKey": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,5 +271,11 @@ func adminRequiredMethods() map[string]bool {
|
|||||||
"/metacrypt.v2.SSHCAService/DeleteProfile": true,
|
"/metacrypt.v2.SSHCAService/DeleteProfile": true,
|
||||||
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
||||||
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
"/metacrypt.v2.SSHCAService/DeleteCert": true,
|
||||||
|
// Transit.
|
||||||
|
"/metacrypt.v2.TransitService/CreateKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/DeleteKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/RotateKey": true,
|
||||||
|
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
|
||||||
|
"/metacrypt.v2.TransitService/TrimKey": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
486
internal/grpcserver/transit.go
Normal file
486
internal/grpcserver/transit.go
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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/engine"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/transit"
|
||||||
|
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type transitServer struct {
|
||||||
|
pb.UnimplementedTransitServiceServer
|
||||||
|
s *GRPCServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) transitHandleRequest(ctx context.Context, mount, operation string, req *engine.Request) (*engine.Response, error) {
|
||||||
|
resp, err := ts.s.engines.HandleRequest(ctx, mount, req)
|
||||||
|
if err != nil {
|
||||||
|
st := codes.Internal
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, engine.ErrMountNotFound):
|
||||||
|
st = codes.NotFound
|
||||||
|
case errors.Is(err, transit.ErrKeyNotFound):
|
||||||
|
st = codes.NotFound
|
||||||
|
case errors.Is(err, transit.ErrKeyExists):
|
||||||
|
st = codes.AlreadyExists
|
||||||
|
case errors.Is(err, transit.ErrUnauthorized):
|
||||||
|
st = codes.Unauthenticated
|
||||||
|
case errors.Is(err, transit.ErrForbidden):
|
||||||
|
st = codes.PermissionDenied
|
||||||
|
case errors.Is(err, transit.ErrDeletionDenied):
|
||||||
|
st = codes.FailedPrecondition
|
||||||
|
case errors.Is(err, transit.ErrUnsupportedOp):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case errors.Is(err, transit.ErrDecryptVersion):
|
||||||
|
st = codes.FailedPrecondition
|
||||||
|
case errors.Is(err, transit.ErrInvalidFormat):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case errors.Is(err, transit.ErrBatchTooLarge):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case errors.Is(err, transit.ErrInvalidMinVer):
|
||||||
|
st = codes.InvalidArgument
|
||||||
|
case strings.Contains(err.Error(), "not found"):
|
||||||
|
st = codes.NotFound
|
||||||
|
case strings.Contains(err.Error(), "forbidden"):
|
||||||
|
st = codes.PermissionDenied
|
||||||
|
}
|
||||||
|
ts.s.logger.Error("grpc: transit "+operation, "mount", mount, "error", err)
|
||||||
|
return nil, status.Error(st, err.Error())
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) callerInfo(ctx context.Context) *engine.CallerInfo {
|
||||||
|
ti := tokenInfoFromContext(ctx)
|
||||||
|
if ti == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &engine.CallerInfo{
|
||||||
|
Username: ti.Username,
|
||||||
|
Roles: ti.Roles,
|
||||||
|
IsAdmin: ti.IsAdmin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) policyChecker(ctx context.Context) engine.PolicyChecker {
|
||||||
|
caller := ts.callerInfo(ctx)
|
||||||
|
if caller == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(resource, action string) (string, bool) {
|
||||||
|
pReq := &policy.Request{
|
||||||
|
Username: caller.Username,
|
||||||
|
Roles: caller.Roles,
|
||||||
|
Resource: resource,
|
||||||
|
Action: action,
|
||||||
|
}
|
||||||
|
effect, matched, err := ts.s.policy.Match(ctx, pReq)
|
||||||
|
if err != nil {
|
||||||
|
return string(policy.EffectDeny), false
|
||||||
|
}
|
||||||
|
return string(effect), matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) CreateKey(ctx context.Context, req *pb.CreateTransitKeyRequest) (*pb.CreateTransitKeyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "create-key", &engine.Request{
|
||||||
|
Operation: "create-key",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": req.Name,
|
||||||
|
"type": req.Type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name, _ := resp.Data["name"].(string)
|
||||||
|
keyType, _ := resp.Data["type"].(string)
|
||||||
|
version, _ := resp.Data["version"].(int)
|
||||||
|
ts.s.logger.Info("audit: transit key created", "mount", req.Mount, "key", name, "type", keyType, "username", callerUsername(ctx))
|
||||||
|
return &pb.CreateTransitKeyResponse{Name: name, Type: keyType, Version: int32(version)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) DeleteKey(ctx context.Context, req *pb.DeleteTransitKeyRequest) (*pb.DeleteTransitKeyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||||
|
}
|
||||||
|
_, err := ts.transitHandleRequest(ctx, req.Mount, "delete-key", &engine.Request{
|
||||||
|
Operation: "delete-key",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"name": req.Name},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ts.s.logger.Info("audit: transit key deleted", "mount", req.Mount, "key", req.Name, "username", callerUsername(ctx))
|
||||||
|
return &pb.DeleteTransitKeyResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) GetKey(ctx context.Context, req *pb.GetTransitKeyRequest) (*pb.GetTransitKeyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "get-key", &engine.Request{
|
||||||
|
Operation: "get-key",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"name": req.Name},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name, _ := resp.Data["name"].(string)
|
||||||
|
keyType, _ := resp.Data["type"].(string)
|
||||||
|
currentVersion, _ := resp.Data["current_version"].(int)
|
||||||
|
minDecryptionVersion, _ := resp.Data["min_decryption_version"].(int)
|
||||||
|
allowDeletion, _ := resp.Data["allow_deletion"].(bool)
|
||||||
|
rawVersions, _ := resp.Data["versions"].([]int)
|
||||||
|
versions := make([]int32, len(rawVersions))
|
||||||
|
for i, v := range rawVersions {
|
||||||
|
versions[i] = int32(v)
|
||||||
|
}
|
||||||
|
return &pb.GetTransitKeyResponse{
|
||||||
|
Name: name,
|
||||||
|
Type: keyType,
|
||||||
|
CurrentVersion: int32(currentVersion),
|
||||||
|
MinDecryptionVersion: int32(minDecryptionVersion),
|
||||||
|
AllowDeletion: allowDeletion,
|
||||||
|
Versions: versions,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) ListKeys(ctx context.Context, req *pb.ListTransitKeysRequest) (*pb.ListTransitKeysResponse, error) {
|
||||||
|
if req.Mount == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "list-keys", &engine.Request{
|
||||||
|
Operation: "list-keys",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keys := toStringSliceFromInterface(resp.Data["keys"])
|
||||||
|
return &pb.ListTransitKeysResponse{Keys: keys}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) RotateKey(ctx context.Context, req *pb.RotateTransitKeyRequest) (*pb.RotateTransitKeyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "rotate-key", &engine.Request{
|
||||||
|
Operation: "rotate-key",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"name": req.Name},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name, _ := resp.Data["name"].(string)
|
||||||
|
version, _ := resp.Data["version"].(int)
|
||||||
|
ts.s.logger.Info("audit: transit key rotated", "mount", req.Mount, "key", name, "version", version, "username", callerUsername(ctx))
|
||||||
|
return &pb.RotateTransitKeyResponse{Name: name, Version: int32(version)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) UpdateKeyConfig(ctx context.Context, req *pb.UpdateTransitKeyConfigRequest) (*pb.UpdateTransitKeyConfigResponse, error) {
|
||||||
|
if req.Mount == "" || req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"name": req.Name}
|
||||||
|
if req.MinDecryptionVersion != 0 {
|
||||||
|
data["min_decryption_version"] = float64(req.MinDecryptionVersion)
|
||||||
|
}
|
||||||
|
data["allow_deletion"] = req.AllowDeletion
|
||||||
|
|
||||||
|
_, err := ts.transitHandleRequest(ctx, req.Mount, "update-key-config", &engine.Request{
|
||||||
|
Operation: "update-key-config",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pb.UpdateTransitKeyConfigResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) TrimKey(ctx context.Context, req *pb.TrimTransitKeyRequest) (*pb.TrimTransitKeyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "trim-key", &engine.Request{
|
||||||
|
Operation: "trim-key",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
Data: map[string]interface{}{"name": req.Name},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
trimmed, _ := resp.Data["trimmed"].(int)
|
||||||
|
return &pb.TrimTransitKeyResponse{Trimmed: int32(trimmed)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) Encrypt(ctx context.Context, req *pb.TransitEncryptRequest) (*pb.TransitEncryptResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"key": req.Key,
|
||||||
|
"plaintext": req.Plaintext,
|
||||||
|
}
|
||||||
|
if req.Context != "" {
|
||||||
|
data["context"] = req.Context
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "encrypt", &engine.Request{
|
||||||
|
Operation: "encrypt",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ct, _ := resp.Data["ciphertext"].(string)
|
||||||
|
return &pb.TransitEncryptResponse{Ciphertext: ct}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) Decrypt(ctx context.Context, req *pb.TransitDecryptRequest) (*pb.TransitDecryptResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"key": req.Key,
|
||||||
|
"ciphertext": req.Ciphertext,
|
||||||
|
}
|
||||||
|
if req.Context != "" {
|
||||||
|
data["context"] = req.Context
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "decrypt", &engine.Request{
|
||||||
|
Operation: "decrypt",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pt, _ := resp.Data["plaintext"].(string)
|
||||||
|
return &pb.TransitDecryptResponse{Plaintext: pt}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) Rewrap(ctx context.Context, req *pb.TransitRewrapRequest) (*pb.TransitRewrapResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"key": req.Key,
|
||||||
|
"ciphertext": req.Ciphertext,
|
||||||
|
}
|
||||||
|
if req.Context != "" {
|
||||||
|
data["context"] = req.Context
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "rewrap", &engine.Request{
|
||||||
|
Operation: "rewrap",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ct, _ := resp.Data["ciphertext"].(string)
|
||||||
|
return &pb.TransitRewrapResponse{Ciphertext: ct}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) BatchEncrypt(ctx context.Context, req *pb.TransitBatchEncryptRequest) (*pb.TransitBatchResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
items := protoItemsToInterface(req.Items)
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-encrypt", &engine.Request{
|
||||||
|
Operation: "batch-encrypt",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: map[string]interface{}{"key": req.Key, "items": items},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toBatchResponse(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) BatchDecrypt(ctx context.Context, req *pb.TransitBatchDecryptRequest) (*pb.TransitBatchResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
items := protoItemsToInterface(req.Items)
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-decrypt", &engine.Request{
|
||||||
|
Operation: "batch-decrypt",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: map[string]interface{}{"key": req.Key, "items": items},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toBatchResponse(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) BatchRewrap(ctx context.Context, req *pb.TransitBatchRewrapRequest) (*pb.TransitBatchResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
items := protoItemsToInterface(req.Items)
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "batch-rewrap", &engine.Request{
|
||||||
|
Operation: "batch-rewrap",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: map[string]interface{}{"key": req.Key, "items": items},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toBatchResponse(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) Sign(ctx context.Context, req *pb.TransitSignRequest) (*pb.TransitSignResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "sign", &engine.Request{
|
||||||
|
Operation: "sign",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: map[string]interface{}{"key": req.Key, "input": req.Input},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sig, _ := resp.Data["signature"].(string)
|
||||||
|
return &pb.TransitSignResponse{Signature: sig}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) Verify(ctx context.Context, req *pb.TransitVerifyRequest) (*pb.TransitVerifyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "verify", &engine.Request{
|
||||||
|
Operation: "verify",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"key": req.Key,
|
||||||
|
"input": req.Input,
|
||||||
|
"signature": req.Signature,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valid, _ := resp.Data["valid"].(bool)
|
||||||
|
return &pb.TransitVerifyResponse{Valid: valid}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) Hmac(ctx context.Context, req *pb.TransitHmacRequest) (*pb.TransitHmacResponse, error) {
|
||||||
|
if req.Mount == "" || req.Key == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and key are required")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"key": req.Key,
|
||||||
|
"input": req.Input,
|
||||||
|
}
|
||||||
|
if req.Hmac != "" {
|
||||||
|
data["hmac"] = req.Hmac
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "hmac", &engine.Request{
|
||||||
|
Operation: "hmac",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
CheckPolicy: ts.policyChecker(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hmacStr, _ := resp.Data["hmac"].(string)
|
||||||
|
valid, _ := resp.Data["valid"].(bool)
|
||||||
|
return &pb.TransitHmacResponse{Hmac: hmacStr, Valid: valid}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *transitServer) GetPublicKey(ctx context.Context, req *pb.GetTransitPublicKeyRequest) (*pb.GetTransitPublicKeyResponse, error) {
|
||||||
|
if req.Mount == "" || req.Name == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "mount and name are required")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"name": req.Name}
|
||||||
|
if req.Version != 0 {
|
||||||
|
data["version"] = float64(req.Version)
|
||||||
|
}
|
||||||
|
resp, err := ts.transitHandleRequest(ctx, req.Mount, "get-public-key", &engine.Request{
|
||||||
|
Operation: "get-public-key",
|
||||||
|
CallerInfo: ts.callerInfo(ctx),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pk, _ := resp.Data["public_key"].(string)
|
||||||
|
version, _ := resp.Data["version"].(int)
|
||||||
|
keyType, _ := resp.Data["type"].(string)
|
||||||
|
return &pb.GetTransitPublicKeyResponse{
|
||||||
|
PublicKey: pk,
|
||||||
|
Version: int32(version),
|
||||||
|
Type: keyType,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
func protoItemsToInterface(items []*pb.TransitBatchItem) []interface{} {
|
||||||
|
out := make([]interface{}, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
m := map[string]interface{}{}
|
||||||
|
if item.Plaintext != "" {
|
||||||
|
m["plaintext"] = item.Plaintext
|
||||||
|
}
|
||||||
|
if item.Ciphertext != "" {
|
||||||
|
m["ciphertext"] = item.Ciphertext
|
||||||
|
}
|
||||||
|
if item.Context != "" {
|
||||||
|
m["context"] = item.Context
|
||||||
|
}
|
||||||
|
if item.Reference != "" {
|
||||||
|
m["reference"] = item.Reference
|
||||||
|
}
|
||||||
|
out[i] = m
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBatchResponse(resp *engine.Response) *pb.TransitBatchResponse {
|
||||||
|
raw, _ := resp.Data["results"].([]interface{})
|
||||||
|
results := make([]*pb.TransitBatchResultItem, 0, len(raw))
|
||||||
|
for _, item := range raw {
|
||||||
|
switch r := item.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
pt, _ := r["plaintext"].(string)
|
||||||
|
ct, _ := r["ciphertext"].(string)
|
||||||
|
ref, _ := r["reference"].(string)
|
||||||
|
errStr, _ := r["error"].(string)
|
||||||
|
results = append(results, &pb.TransitBatchResultItem{
|
||||||
|
Plaintext: pt,
|
||||||
|
Ciphertext: ct,
|
||||||
|
Reference: ref,
|
||||||
|
Error: errStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &pb.TransitBatchResponse{Results: results}
|
||||||
|
}
|
||||||
@@ -71,6 +71,26 @@ func (s *Server) registerRoutes(r chi.Router) {
|
|||||||
|
|
||||||
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))
|
||||||
|
|
||||||
|
// Transit engine routes.
|
||||||
|
r.Post("/v1/transit/{mount}/keys", s.requireAdmin(s.handleTransitCreateKey))
|
||||||
|
r.Get("/v1/transit/{mount}/keys", s.requireAuth(s.handleTransitListKeys))
|
||||||
|
r.Get("/v1/transit/{mount}/keys/{name}", s.requireAuth(s.handleTransitGetKey))
|
||||||
|
r.Delete("/v1/transit/{mount}/keys/{name}", s.requireAdmin(s.handleTransitDeleteKey))
|
||||||
|
r.Post("/v1/transit/{mount}/keys/{name}/rotate", s.requireAdmin(s.handleTransitRotateKey))
|
||||||
|
r.Post("/v1/transit/{mount}/keys/{name}/config", s.requireAdmin(s.handleTransitUpdateKeyConfig))
|
||||||
|
r.Post("/v1/transit/{mount}/keys/{name}/trim", s.requireAdmin(s.handleTransitTrimKey))
|
||||||
|
r.Post("/v1/transit/{mount}/encrypt/{key}", s.requireAuth(s.handleTransitEncrypt))
|
||||||
|
r.Post("/v1/transit/{mount}/decrypt/{key}", s.requireAuth(s.handleTransitDecrypt))
|
||||||
|
r.Post("/v1/transit/{mount}/rewrap/{key}", s.requireAuth(s.handleTransitRewrap))
|
||||||
|
r.Post("/v1/transit/{mount}/batch/encrypt/{key}", s.requireAuth(s.handleTransitBatchEncrypt))
|
||||||
|
r.Post("/v1/transit/{mount}/batch/decrypt/{key}", s.requireAuth(s.handleTransitBatchDecrypt))
|
||||||
|
r.Post("/v1/transit/{mount}/batch/rewrap/{key}", s.requireAuth(s.handleTransitBatchRewrap))
|
||||||
|
r.Post("/v1/transit/{mount}/sign/{key}", s.requireAuth(s.handleTransitSign))
|
||||||
|
r.Post("/v1/transit/{mount}/verify/{key}", s.requireAuth(s.handleTransitVerify))
|
||||||
|
r.Post("/v1/transit/{mount}/hmac/{key}", s.requireAuth(s.handleTransitHmac))
|
||||||
|
r.Get("/v1/transit/{mount}/keys/{name}/public-key", s.requireAuth(s.handleTransitGetPublicKey))
|
||||||
|
|
||||||
s.registerACMERoutes(r)
|
s.registerACMERoutes(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +777,262 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) {
|
|||||||
return caEng, nil
|
return caEng, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Transit Engine Handlers ---
|
||||||
|
|
||||||
|
func (s *Server) transitRequest(w http.ResponseWriter, r *http.Request, mount, operation string, data map[string]interface{}) {
|
||||||
|
info := TokenInfoFromContext(r.Context())
|
||||||
|
|
||||||
|
policyChecker := func(resource, action string) (string, bool) {
|
||||||
|
pReq := &policy.Request{
|
||||||
|
Username: info.Username,
|
||||||
|
Roles: info.Roles,
|
||||||
|
Resource: resource,
|
||||||
|
Action: action,
|
||||||
|
}
|
||||||
|
eff, matched, pErr := s.policy.Match(r.Context(), pReq)
|
||||||
|
if pErr != nil {
|
||||||
|
return string(policy.EffectDeny), false
|
||||||
|
}
|
||||||
|
return string(eff), matched
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.engines.HandleRequest(r.Context(), mount, &engine.Request{
|
||||||
|
Operation: operation,
|
||||||
|
CallerInfo: &engine.CallerInfo{Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin},
|
||||||
|
CheckPolicy: policyChecker,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
status := http.StatusInternalServerError
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, engine.ErrMountNotFound):
|
||||||
|
status = http.StatusNotFound
|
||||||
|
case strings.Contains(err.Error(), "forbidden"):
|
||||||
|
status = http.StatusForbidden
|
||||||
|
case strings.Contains(err.Error(), "authentication required"):
|
||||||
|
status = http.StatusUnauthorized
|
||||||
|
case strings.Contains(err.Error(), "not found"):
|
||||||
|
status = http.StatusNotFound
|
||||||
|
case strings.Contains(err.Error(), "not allowed"):
|
||||||
|
status = http.StatusForbidden
|
||||||
|
case strings.Contains(err.Error(), "unsupported"):
|
||||||
|
status = http.StatusBadRequest
|
||||||
|
case strings.Contains(err.Error(), "invalid"):
|
||||||
|
status = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitCreateKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "create-key", map[string]interface{}{"name": req.Name, "type": req.Type})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitDeleteKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
s.transitRequest(w, r, mount, "delete-key", map[string]interface{}{"name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitGetKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
s.transitRequest(w, r, mount, "get-key", map[string]interface{}{"name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitListKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
s.transitRequest(w, r, mount, "list-keys", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitRotateKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
s.transitRequest(w, r, mount, "rotate-key", map[string]interface{}{"name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitUpdateKeyConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
var req struct {
|
||||||
|
MinDecryptionVersion *float64 `json:"min_decryption_version"`
|
||||||
|
AllowDeletion *bool `json:"allow_deletion"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"name": name}
|
||||||
|
if req.MinDecryptionVersion != nil {
|
||||||
|
data["min_decryption_version"] = *req.MinDecryptionVersion
|
||||||
|
}
|
||||||
|
if req.AllowDeletion != nil {
|
||||||
|
data["allow_deletion"] = *req.AllowDeletion
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "update-key-config", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitTrimKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
s.transitRequest(w, r, mount, "trim-key", map[string]interface{}{"name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Plaintext string `json:"plaintext"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"key": key, "plaintext": req.Plaintext}
|
||||||
|
if req.Context != "" {
|
||||||
|
data["context"] = req.Context
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "encrypt", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitDecrypt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Ciphertext string `json:"ciphertext"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext}
|
||||||
|
if req.Context != "" {
|
||||||
|
data["context"] = req.Context
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "decrypt", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitRewrap(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Ciphertext string `json:"ciphertext"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext}
|
||||||
|
if req.Context != "" {
|
||||||
|
data["context"] = req.Context
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "rewrap", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitBatchEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Items []interface{} `json:"items"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "batch-encrypt", map[string]interface{}{"key": key, "items": req.Items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitBatchDecrypt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Items []interface{} `json:"items"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "batch-decrypt", map[string]interface{}{"key": key, "items": req.Items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitBatchRewrap(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Items []interface{} `json:"items"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "batch-rewrap", map[string]interface{}{"key": key, "items": req.Items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitSign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Input string `json:"input"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "sign", map[string]interface{}{"key": key, "input": req.Input})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitVerify(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Input string `json:"input"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "verify", map[string]interface{}{"key": key, "input": req.Input, "signature": req.Signature})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitHmac(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
key := chi.URLParam(r, "key")
|
||||||
|
var req struct {
|
||||||
|
Input string `json:"input"`
|
||||||
|
HMAC string `json:"hmac"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &req); err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"key": key, "input": req.Input}
|
||||||
|
if req.HMAC != "" {
|
||||||
|
data["hmac"] = req.HMAC
|
||||||
|
}
|
||||||
|
s.transitRequest(w, r, mount, "hmac", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTransitGetPublicKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mount := chi.URLParam(r, "mount")
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
s.transitRequest(w, r, mount, "get-public-key", map[string]interface{}{"name": name})
|
||||||
|
}
|
||||||
|
|
||||||
// operationAction maps an engine operation name to a policy action.
|
// operationAction maps an engine operation name to a policy action.
|
||||||
func operationAction(op string) string {
|
func operationAction(op string) string {
|
||||||
switch op {
|
switch op {
|
||||||
|
|||||||
291
proto/metacrypt/v2/transit.proto
Normal file
291
proto/metacrypt/v2/transit.proto
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package metacrypt.v2;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2;metacryptv2";
|
||||||
|
|
||||||
|
// TransitService provides typed, authenticated access to transit engine
|
||||||
|
// operations: symmetric encryption, signing, HMAC, and versioned key
|
||||||
|
// management. All RPCs require the service to be unsealed.
|
||||||
|
service TransitService {
|
||||||
|
// CreateKey creates a new named encryption key. Admin only.
|
||||||
|
rpc CreateKey(CreateTransitKeyRequest) returns (CreateTransitKeyResponse);
|
||||||
|
|
||||||
|
// DeleteKey permanently removes a named key. Admin only.
|
||||||
|
// Only succeeds if allow_deletion is true on the key config.
|
||||||
|
rpc DeleteKey(DeleteTransitKeyRequest) returns (DeleteTransitKeyResponse);
|
||||||
|
|
||||||
|
// GetKey returns metadata for a named key (no raw material). Auth required.
|
||||||
|
rpc GetKey(GetTransitKeyRequest) returns (GetTransitKeyResponse);
|
||||||
|
|
||||||
|
// ListKeys returns the names of all configured keys. Auth required.
|
||||||
|
rpc ListKeys(ListTransitKeysRequest) returns (ListTransitKeysResponse);
|
||||||
|
|
||||||
|
// RotateKey creates a new version of the named key. Admin only.
|
||||||
|
rpc RotateKey(RotateTransitKeyRequest) returns (RotateTransitKeyResponse);
|
||||||
|
|
||||||
|
// UpdateKeyConfig updates key configuration (e.g. min_decryption_version).
|
||||||
|
// Admin only.
|
||||||
|
rpc UpdateKeyConfig(UpdateTransitKeyConfigRequest) returns (UpdateTransitKeyConfigResponse);
|
||||||
|
|
||||||
|
// TrimKey deletes versions below min_decryption_version. Admin only.
|
||||||
|
rpc TrimKey(TrimTransitKeyRequest) returns (TrimTransitKeyResponse);
|
||||||
|
|
||||||
|
// Encrypt encrypts plaintext with the latest key version. Auth required.
|
||||||
|
rpc Encrypt(TransitEncryptRequest) returns (TransitEncryptResponse);
|
||||||
|
|
||||||
|
// Decrypt decrypts ciphertext. Auth required.
|
||||||
|
rpc Decrypt(TransitDecryptRequest) returns (TransitDecryptResponse);
|
||||||
|
|
||||||
|
// Rewrap re-encrypts ciphertext with the latest key version without
|
||||||
|
// exposing plaintext. Auth required.
|
||||||
|
rpc Rewrap(TransitRewrapRequest) returns (TransitRewrapResponse);
|
||||||
|
|
||||||
|
// BatchEncrypt encrypts multiple items in a single request. Auth required.
|
||||||
|
rpc BatchEncrypt(TransitBatchEncryptRequest) returns (TransitBatchResponse);
|
||||||
|
|
||||||
|
// BatchDecrypt decrypts multiple items in a single request. Auth required.
|
||||||
|
rpc BatchDecrypt(TransitBatchDecryptRequest) returns (TransitBatchResponse);
|
||||||
|
|
||||||
|
// BatchRewrap re-encrypts multiple items in a single request. Auth required.
|
||||||
|
rpc BatchRewrap(TransitBatchRewrapRequest) returns (TransitBatchResponse);
|
||||||
|
|
||||||
|
// Sign signs input data with an asymmetric key. Auth required.
|
||||||
|
rpc Sign(TransitSignRequest) returns (TransitSignResponse);
|
||||||
|
|
||||||
|
// Verify verifies a signature against input data. Auth required.
|
||||||
|
rpc Verify(TransitVerifyRequest) returns (TransitVerifyResponse);
|
||||||
|
|
||||||
|
// Hmac computes or verifies an HMAC. Auth required.
|
||||||
|
rpc Hmac(TransitHmacRequest) returns (TransitHmacResponse);
|
||||||
|
|
||||||
|
// GetPublicKey returns the public key for an asymmetric key. Auth required.
|
||||||
|
rpc GetPublicKey(GetTransitPublicKeyRequest) returns (GetTransitPublicKeyResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CreateKey ---
|
||||||
|
|
||||||
|
message CreateTransitKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string name = 2;
|
||||||
|
// type is the key algorithm: aes256-gcm, chacha20-poly, ed25519,
|
||||||
|
// ecdsa-p256, ecdsa-p384, hmac-sha256, hmac-sha512.
|
||||||
|
// Defaults to aes256-gcm if empty.
|
||||||
|
string type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateTransitKeyResponse {
|
||||||
|
string name = 1;
|
||||||
|
string type = 2;
|
||||||
|
int32 version = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DeleteKey ---
|
||||||
|
|
||||||
|
message DeleteTransitKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteTransitKeyResponse {}
|
||||||
|
|
||||||
|
// --- GetKey ---
|
||||||
|
|
||||||
|
message GetTransitKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetTransitKeyResponse {
|
||||||
|
string name = 1;
|
||||||
|
string type = 2;
|
||||||
|
int32 current_version = 3;
|
||||||
|
int32 min_decryption_version = 4;
|
||||||
|
bool allow_deletion = 5;
|
||||||
|
repeated int32 versions = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ListKeys ---
|
||||||
|
|
||||||
|
message ListTransitKeysRequest {
|
||||||
|
string mount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListTransitKeysResponse {
|
||||||
|
repeated string keys = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RotateKey ---
|
||||||
|
|
||||||
|
message RotateTransitKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RotateTransitKeyResponse {
|
||||||
|
string name = 1;
|
||||||
|
int32 version = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UpdateKeyConfig ---
|
||||||
|
|
||||||
|
message UpdateTransitKeyConfigRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string name = 2;
|
||||||
|
int32 min_decryption_version = 3;
|
||||||
|
bool allow_deletion = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateTransitKeyConfigResponse {}
|
||||||
|
|
||||||
|
// --- TrimKey ---
|
||||||
|
|
||||||
|
message TrimTransitKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TrimTransitKeyResponse {
|
||||||
|
int32 trimmed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Encrypt ---
|
||||||
|
|
||||||
|
message TransitEncryptRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
// plaintext is base64-encoded data to encrypt.
|
||||||
|
string plaintext = 3;
|
||||||
|
// context is optional base64-encoded additional authenticated data (AAD).
|
||||||
|
string context = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitEncryptResponse {
|
||||||
|
// ciphertext in format "metacrypt:v{version}:{base64(nonce+ciphertext+tag)}"
|
||||||
|
string ciphertext = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Decrypt ---
|
||||||
|
|
||||||
|
message TransitDecryptRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
string ciphertext = 3;
|
||||||
|
string context = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitDecryptResponse {
|
||||||
|
// plaintext is base64-encoded decrypted data.
|
||||||
|
string plaintext = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rewrap ---
|
||||||
|
|
||||||
|
message TransitRewrapRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
string ciphertext = 3;
|
||||||
|
string context = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitRewrapResponse {
|
||||||
|
string ciphertext = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Batch ---
|
||||||
|
|
||||||
|
message TransitBatchItem {
|
||||||
|
string plaintext = 1;
|
||||||
|
string ciphertext = 2;
|
||||||
|
string context = 3;
|
||||||
|
string reference = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitBatchResultItem {
|
||||||
|
string plaintext = 1;
|
||||||
|
string ciphertext = 2;
|
||||||
|
string reference = 3;
|
||||||
|
string error = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitBatchEncryptRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
repeated TransitBatchItem items = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitBatchDecryptRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
repeated TransitBatchItem items = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitBatchRewrapRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
repeated TransitBatchItem items = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitBatchResponse {
|
||||||
|
repeated TransitBatchResultItem results = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sign ---
|
||||||
|
|
||||||
|
message TransitSignRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
// input is base64-encoded data to sign.
|
||||||
|
string input = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitSignResponse {
|
||||||
|
// signature in format "metacrypt:v{version}:{base64(signature_bytes)}"
|
||||||
|
string signature = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Verify ---
|
||||||
|
|
||||||
|
message TransitVerifyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
string input = 3;
|
||||||
|
string signature = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitVerifyResponse {
|
||||||
|
bool valid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HMAC ---
|
||||||
|
|
||||||
|
message TransitHmacRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string key = 2;
|
||||||
|
// input is base64-encoded data to HMAC.
|
||||||
|
string input = 3;
|
||||||
|
// hmac, if set, switches to verify mode.
|
||||||
|
string hmac = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransitHmacResponse {
|
||||||
|
// hmac is set in compute mode.
|
||||||
|
string hmac = 1;
|
||||||
|
// valid is set in verify mode.
|
||||||
|
bool valid = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GetPublicKey ---
|
||||||
|
|
||||||
|
message GetTransitPublicKeyRequest {
|
||||||
|
string mount = 1;
|
||||||
|
string name = 2;
|
||||||
|
int32 version = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetTransitPublicKeyResponse {
|
||||||
|
// public_key is base64-encoded PKIX DER public key.
|
||||||
|
string public_key = 1;
|
||||||
|
int32 version = 2;
|
||||||
|
string type = 3;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user