Merge transit engine branch, resolve conflicts in shared files

This commit is contained in:
2026-03-16 19:50:47 -07:00
14 changed files with 7969 additions and 0 deletions

354
REMEDIATION.md Normal file
View 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.

View 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
}

View File

@@ -17,6 +17,7 @@ import (
"git.wntrmute.dev/kyle/metacrypt/internal/engine" "git.wntrmute.dev/kyle/metacrypt/internal/engine"
"git.wntrmute.dev/kyle/metacrypt/internal/engine/ca" "git.wntrmute.dev/kyle/metacrypt/internal/engine/ca"
"git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca" "git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca"
"git.wntrmute.dev/kyle/metacrypt/internal/engine/transit"
"git.wntrmute.dev/kyle/metacrypt/internal/grpcserver" "git.wntrmute.dev/kyle/metacrypt/internal/grpcserver"
"git.wntrmute.dev/kyle/metacrypt/internal/policy" "git.wntrmute.dev/kyle/metacrypt/internal/policy"
"git.wntrmute.dev/kyle/metacrypt/internal/seal" "git.wntrmute.dev/kyle/metacrypt/internal/seal"
@@ -76,6 +77,7 @@ func runServer(cmd *cobra.Command, args []string) error {
engineRegistry := engine.NewRegistry(b, logger) engineRegistry := engine.NewRegistry(b, logger)
engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine) engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine) engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
engineRegistry.RegisterFactory(engine.EngineTypeTransit, transit.NewTransitEngine)
srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version) srv := server.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, version)
grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger) grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger)

View 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>/`.

File diff suppressed because it is too large Load Diff

View 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",
}

View 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
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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"`
}

View File

@@ -84,6 +84,7 @@ func (s *GRPCServer) Start() error {
pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s}) pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s})
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s}) pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s}) pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s})
pb.RegisterTransitServiceServer(s.srv, &transitServer{s: s})
lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr) lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr)
if err != nil { if err != nil {
@@ -157,6 +158,24 @@ func sealRequiredMethods() map[string]bool {
"/metacrypt.v2.SSHCAService/RevokeCert": true, "/metacrypt.v2.SSHCAService/RevokeCert": true,
"/metacrypt.v2.SSHCAService/DeleteCert": true, "/metacrypt.v2.SSHCAService/DeleteCert": true,
"/metacrypt.v2.SSHCAService/GetKRL": true, "/metacrypt.v2.SSHCAService/GetKRL": true,
// Transit.
"/metacrypt.v2.TransitService/CreateKey": true,
"/metacrypt.v2.TransitService/DeleteKey": true,
"/metacrypt.v2.TransitService/GetKey": true,
"/metacrypt.v2.TransitService/ListKeys": true,
"/metacrypt.v2.TransitService/RotateKey": true,
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
"/metacrypt.v2.TransitService/TrimKey": true,
"/metacrypt.v2.TransitService/Encrypt": true,
"/metacrypt.v2.TransitService/Decrypt": true,
"/metacrypt.v2.TransitService/Rewrap": true,
"/metacrypt.v2.TransitService/BatchEncrypt": true,
"/metacrypt.v2.TransitService/BatchDecrypt": true,
"/metacrypt.v2.TransitService/BatchRewrap": true,
"/metacrypt.v2.TransitService/Sign": true,
"/metacrypt.v2.TransitService/Verify": true,
"/metacrypt.v2.TransitService/Hmac": true,
"/metacrypt.v2.TransitService/GetPublicKey": true,
} }
} }
@@ -203,6 +222,24 @@ func authRequiredMethods() map[string]bool {
"/metacrypt.v2.SSHCAService/ListCerts": true, "/metacrypt.v2.SSHCAService/ListCerts": true,
"/metacrypt.v2.SSHCAService/RevokeCert": true, "/metacrypt.v2.SSHCAService/RevokeCert": true,
"/metacrypt.v2.SSHCAService/DeleteCert": true, "/metacrypt.v2.SSHCAService/DeleteCert": true,
// Transit.
"/metacrypt.v2.TransitService/CreateKey": true,
"/metacrypt.v2.TransitService/DeleteKey": true,
"/metacrypt.v2.TransitService/GetKey": true,
"/metacrypt.v2.TransitService/ListKeys": true,
"/metacrypt.v2.TransitService/RotateKey": true,
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
"/metacrypt.v2.TransitService/TrimKey": true,
"/metacrypt.v2.TransitService/Encrypt": true,
"/metacrypt.v2.TransitService/Decrypt": true,
"/metacrypt.v2.TransitService/Rewrap": true,
"/metacrypt.v2.TransitService/BatchEncrypt": true,
"/metacrypt.v2.TransitService/BatchDecrypt": true,
"/metacrypt.v2.TransitService/BatchRewrap": true,
"/metacrypt.v2.TransitService/Sign": true,
"/metacrypt.v2.TransitService/Verify": true,
"/metacrypt.v2.TransitService/Hmac": true,
"/metacrypt.v2.TransitService/GetPublicKey": true,
} }
} }
@@ -234,5 +271,11 @@ func adminRequiredMethods() map[string]bool {
"/metacrypt.v2.SSHCAService/DeleteProfile": true, "/metacrypt.v2.SSHCAService/DeleteProfile": true,
"/metacrypt.v2.SSHCAService/RevokeCert": true, "/metacrypt.v2.SSHCAService/RevokeCert": true,
"/metacrypt.v2.SSHCAService/DeleteCert": true, "/metacrypt.v2.SSHCAService/DeleteCert": true,
// Transit.
"/metacrypt.v2.TransitService/CreateKey": true,
"/metacrypt.v2.TransitService/DeleteKey": true,
"/metacrypt.v2.TransitService/RotateKey": true,
"/metacrypt.v2.TransitService/UpdateKeyConfig": true,
"/metacrypt.v2.TransitService/TrimKey": true,
} }
} }

View 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}
}

View File

@@ -71,6 +71,26 @@ func (s *Server) registerRoutes(r chi.Router) {
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules)) r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule)) r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
// Transit engine routes.
r.Post("/v1/transit/{mount}/keys", s.requireAdmin(s.handleTransitCreateKey))
r.Get("/v1/transit/{mount}/keys", s.requireAuth(s.handleTransitListKeys))
r.Get("/v1/transit/{mount}/keys/{name}", s.requireAuth(s.handleTransitGetKey))
r.Delete("/v1/transit/{mount}/keys/{name}", s.requireAdmin(s.handleTransitDeleteKey))
r.Post("/v1/transit/{mount}/keys/{name}/rotate", s.requireAdmin(s.handleTransitRotateKey))
r.Post("/v1/transit/{mount}/keys/{name}/config", s.requireAdmin(s.handleTransitUpdateKeyConfig))
r.Post("/v1/transit/{mount}/keys/{name}/trim", s.requireAdmin(s.handleTransitTrimKey))
r.Post("/v1/transit/{mount}/encrypt/{key}", s.requireAuth(s.handleTransitEncrypt))
r.Post("/v1/transit/{mount}/decrypt/{key}", s.requireAuth(s.handleTransitDecrypt))
r.Post("/v1/transit/{mount}/rewrap/{key}", s.requireAuth(s.handleTransitRewrap))
r.Post("/v1/transit/{mount}/batch/encrypt/{key}", s.requireAuth(s.handleTransitBatchEncrypt))
r.Post("/v1/transit/{mount}/batch/decrypt/{key}", s.requireAuth(s.handleTransitBatchDecrypt))
r.Post("/v1/transit/{mount}/batch/rewrap/{key}", s.requireAuth(s.handleTransitBatchRewrap))
r.Post("/v1/transit/{mount}/sign/{key}", s.requireAuth(s.handleTransitSign))
r.Post("/v1/transit/{mount}/verify/{key}", s.requireAuth(s.handleTransitVerify))
r.Post("/v1/transit/{mount}/hmac/{key}", s.requireAuth(s.handleTransitHmac))
r.Get("/v1/transit/{mount}/keys/{name}/public-key", s.requireAuth(s.handleTransitGetPublicKey))
s.registerACMERoutes(r) s.registerACMERoutes(r)
} }
@@ -757,6 +777,262 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) {
return caEng, nil return caEng, nil
} }
// --- Transit Engine Handlers ---
func (s *Server) transitRequest(w http.ResponseWriter, r *http.Request, mount, operation string, data map[string]interface{}) {
info := TokenInfoFromContext(r.Context())
policyChecker := func(resource, action string) (string, bool) {
pReq := &policy.Request{
Username: info.Username,
Roles: info.Roles,
Resource: resource,
Action: action,
}
eff, matched, pErr := s.policy.Match(r.Context(), pReq)
if pErr != nil {
return string(policy.EffectDeny), false
}
return string(eff), matched
}
resp, err := s.engines.HandleRequest(r.Context(), mount, &engine.Request{
Operation: operation,
CallerInfo: &engine.CallerInfo{Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin},
CheckPolicy: policyChecker,
Data: data,
})
if err != nil {
status := http.StatusInternalServerError
switch {
case errors.Is(err, engine.ErrMountNotFound):
status = http.StatusNotFound
case strings.Contains(err.Error(), "forbidden"):
status = http.StatusForbidden
case strings.Contains(err.Error(), "authentication required"):
status = http.StatusUnauthorized
case strings.Contains(err.Error(), "not found"):
status = http.StatusNotFound
case strings.Contains(err.Error(), "not allowed"):
status = http.StatusForbidden
case strings.Contains(err.Error(), "unsupported"):
status = http.StatusBadRequest
case strings.Contains(err.Error(), "invalid"):
status = http.StatusBadRequest
}
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
return
}
writeJSON(w, http.StatusOK, resp.Data)
}
func (s *Server) handleTransitCreateKey(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
var req struct {
Name string `json:"name"`
Type string `json:"type"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
s.transitRequest(w, r, mount, "create-key", map[string]interface{}{"name": req.Name, "type": req.Type})
}
func (s *Server) handleTransitDeleteKey(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
name := chi.URLParam(r, "name")
s.transitRequest(w, r, mount, "delete-key", map[string]interface{}{"name": name})
}
func (s *Server) handleTransitGetKey(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
name := chi.URLParam(r, "name")
s.transitRequest(w, r, mount, "get-key", map[string]interface{}{"name": name})
}
func (s *Server) handleTransitListKeys(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
s.transitRequest(w, r, mount, "list-keys", nil)
}
func (s *Server) handleTransitRotateKey(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
name := chi.URLParam(r, "name")
s.transitRequest(w, r, mount, "rotate-key", map[string]interface{}{"name": name})
}
func (s *Server) handleTransitUpdateKeyConfig(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
name := chi.URLParam(r, "name")
var req struct {
MinDecryptionVersion *float64 `json:"min_decryption_version"`
AllowDeletion *bool `json:"allow_deletion"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
data := map[string]interface{}{"name": name}
if req.MinDecryptionVersion != nil {
data["min_decryption_version"] = *req.MinDecryptionVersion
}
if req.AllowDeletion != nil {
data["allow_deletion"] = *req.AllowDeletion
}
s.transitRequest(w, r, mount, "update-key-config", data)
}
func (s *Server) handleTransitTrimKey(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
name := chi.URLParam(r, "name")
s.transitRequest(w, r, mount, "trim-key", map[string]interface{}{"name": name})
}
func (s *Server) handleTransitEncrypt(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Plaintext string `json:"plaintext"`
Context string `json:"context"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
data := map[string]interface{}{"key": key, "plaintext": req.Plaintext}
if req.Context != "" {
data["context"] = req.Context
}
s.transitRequest(w, r, mount, "encrypt", data)
}
func (s *Server) handleTransitDecrypt(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Ciphertext string `json:"ciphertext"`
Context string `json:"context"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext}
if req.Context != "" {
data["context"] = req.Context
}
s.transitRequest(w, r, mount, "decrypt", data)
}
func (s *Server) handleTransitRewrap(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Ciphertext string `json:"ciphertext"`
Context string `json:"context"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
data := map[string]interface{}{"key": key, "ciphertext": req.Ciphertext}
if req.Context != "" {
data["context"] = req.Context
}
s.transitRequest(w, r, mount, "rewrap", data)
}
func (s *Server) handleTransitBatchEncrypt(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Items []interface{} `json:"items"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
s.transitRequest(w, r, mount, "batch-encrypt", map[string]interface{}{"key": key, "items": req.Items})
}
func (s *Server) handleTransitBatchDecrypt(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Items []interface{} `json:"items"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
s.transitRequest(w, r, mount, "batch-decrypt", map[string]interface{}{"key": key, "items": req.Items})
}
func (s *Server) handleTransitBatchRewrap(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Items []interface{} `json:"items"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
s.transitRequest(w, r, mount, "batch-rewrap", map[string]interface{}{"key": key, "items": req.Items})
}
func (s *Server) handleTransitSign(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Input string `json:"input"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
s.transitRequest(w, r, mount, "sign", map[string]interface{}{"key": key, "input": req.Input})
}
func (s *Server) handleTransitVerify(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Input string `json:"input"`
Signature string `json:"signature"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
s.transitRequest(w, r, mount, "verify", map[string]interface{}{"key": key, "input": req.Input, "signature": req.Signature})
}
func (s *Server) handleTransitHmac(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
key := chi.URLParam(r, "key")
var req struct {
Input string `json:"input"`
HMAC string `json:"hmac"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
data := map[string]interface{}{"key": key, "input": req.Input}
if req.HMAC != "" {
data["hmac"] = req.HMAC
}
s.transitRequest(w, r, mount, "hmac", data)
}
func (s *Server) handleTransitGetPublicKey(w http.ResponseWriter, r *http.Request) {
mount := chi.URLParam(r, "mount")
name := chi.URLParam(r, "name")
s.transitRequest(w, r, mount, "get-public-key", map[string]interface{}{"name": name})
}
// operationAction maps an engine operation name to a policy action. // operationAction maps an engine operation name to a policy action.
func operationAction(op string) string { func operationAction(op string) string {
switch op { switch op {

View 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;
}