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/ca"
|
||||
"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/policy"
|
||||
"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.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
|
||||
engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
|
||||
engineRegistry.RegisterFactory(engine.EngineTypeTransit, transit.NewTransitEngine)
|
||||
|
||||
srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version)
|
||||
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.RegisterACMEServiceServer(s.srv, &acmeServer{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)
|
||||
if err != nil {
|
||||
@@ -157,6 +158,24 @@ func sealRequiredMethods() map[string]bool {
|
||||
"/metacrypt.v2.SSHCAService/RevokeCert": true,
|
||||
"/metacrypt.v2.SSHCAService/DeleteCert": 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/RevokeCert": 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/RevokeCert": 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/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)
|
||||
}
|
||||
|
||||
@@ -757,6 +777,262 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) {
|
||||
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.
|
||||
func operationAction(op string) string {
|
||||
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