Implement transit encryption engine with versioned key management

Add complete transit engine supporting symmetric encryption (AES-256-GCM,
XChaCha20-Poly1305), asymmetric signing (Ed25519, ECDSA P-256/P-384),
and HMAC (SHA-256/SHA-512) with versioned key rotation, min decryption
version enforcement, key trimming, batch operations, and rewrap.

Includes proto definitions, gRPC handlers, REST routes, and comprehensive
tests covering all 18 operations, auth enforcement, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 19:45:56 -07:00
parent ac4577f778
commit cbd77c58e8
10 changed files with 6718 additions and 4 deletions

View File

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