Files
metacrypt/internal/server/routes.go
Kyle Isom 64d921827e Add MEK rotation, per-engine DEKs, and v2 ciphertext format (audit #6, #22)
Implement a two-level key hierarchy: the MEK now wraps per-engine DEKs
stored in a new barrier_keys table, rather than encrypting all barrier
entries directly. A v2 ciphertext format (0x02) embeds the key ID so the
barrier can resolve which DEK to use on decryption. v1 ciphertext remains
supported for backward compatibility.

Key changes:
- crypto: EncryptV2/DecryptV2/ExtractKeyID for v2 ciphertext with key IDs
- barrier: key registry (CreateKey, RotateKey, ListKeys, MigrateToV2, ReWrapKeys)
- seal: RotateMEK re-wraps DEKs without re-encrypting data
- engine: Mount auto-creates per-engine DEK
- REST + gRPC: barrier/keys, barrier/rotate-mek, barrier/rotate-key, barrier/migrate
- proto: BarrierService (v1 + v2) with ListKeys, RotateMEK, RotateKey, Migrate
- db: migration v2 adds barrier_keys table

Also includes: security audit report, CSRF protection, engine design specs
(sshca, transit, user), path-bound AAD migration tool, policy engine
enhancements, and ARCHITECTURE.md updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:27:44 -07:00

772 lines
23 KiB
Go

package server
import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
"git.wntrmute.dev/kyle/metacrypt/internal/engine/ca"
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
)
func (s *Server) registerRoutes(r chi.Router) {
// REST API routes — web UI served by metacrypt-web.
r.Get("/v1/status", s.handleStatus)
r.Post("/v1/init", s.handleInit)
r.Post("/v1/unseal", s.handleUnseal)
r.Post("/v1/seal", s.requireAdmin(s.handleSeal))
r.Post("/v1/auth/login", s.handleLogin)
r.Post("/v1/auth/logout", s.requireAuth(s.handleLogout))
r.Get("/v1/auth/tokeninfo", s.requireAuth(s.handleTokenInfo))
r.Get("/v1/engine/mounts", s.requireAuth(s.handleEngineMounts))
r.Post("/v1/engine/mount", s.requireAdmin(s.handleEngineMount))
r.Post("/v1/engine/unmount", s.requireAdmin(s.handleEngineUnmount))
r.Post("/v1/engine/request", s.requireAuth(s.handleEngineRequest))
// CA certificate routes (auth required).
r.Get("/v1/ca/{mount}/cert/{serial}", s.requireAuth(s.handleGetCert))
r.Post("/v1/ca/{mount}/cert/{serial}/revoke", s.requireAdmin(s.handleRevokeCert))
r.Delete("/v1/ca/{mount}/cert/{serial}", s.requireAdmin(s.handleDeleteCert))
// Public PKI routes (no auth required, but must be unsealed).
r.Get("/v1/pki/{mount}/ca", s.requireUnseal(s.handlePKIRoot))
r.Get("/v1/pki/{mount}/ca/chain", s.requireUnseal(s.handlePKIChain))
r.Get("/v1/pki/{mount}/issuer/{name}", s.requireUnseal(s.handlePKIIssuer))
r.Get("/v1/pki/{mount}/issuer/{name}/crl", s.requireUnseal(s.handlePKICRL))
r.Get("/v1/barrier/keys", s.requireAdmin(s.handleBarrierKeys))
r.Post("/v1/barrier/rotate-mek", s.requireAdmin(s.handleRotateMEK))
r.Post("/v1/barrier/rotate-key", s.requireAdmin(s.handleRotateKey))
r.Post("/v1/barrier/migrate", s.requireAdmin(s.handleBarrierMigrate))
r.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
s.registerACMERoutes(r)
}
// --- API Handlers ---
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]interface{}{
"state": s.seal.State().String(),
})
}
func (s *Server) handleInit(w http.ResponseWriter, r *http.Request) {
var req struct {
Password string `json:"password"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if req.Password == "" {
http.Error(w, `{"error":"password is required"}`, http.StatusBadRequest)
return
}
params := crypto.Argon2Params{
Time: s.cfg.Seal.Argon2Time,
Memory: s.cfg.Seal.Argon2Memory,
Threads: s.cfg.Seal.Argon2Threads,
}
if err := s.seal.Initialize(r.Context(), []byte(req.Password), params); err != nil {
if errors.Is(err, seal.ErrAlreadyInitialized) {
http.Error(w, `{"error":"already initialized"}`, http.StatusConflict)
return
}
s.logger.Error("init failed", "error", err)
http.Error(w, `{"error":"initialization failed"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"state": s.seal.State().String(),
})
}
func (s *Server) handleUnseal(w http.ResponseWriter, r *http.Request) {
var req struct {
Password string `json:"password"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if err := s.seal.Unseal([]byte(req.Password)); err != nil {
if errors.Is(err, seal.ErrNotInitialized) {
s.logger.Warn("unseal attempt on uninitialized service", "remote_addr", r.RemoteAddr)
http.Error(w, `{"error":"not initialized"}`, http.StatusPreconditionFailed)
} else if errors.Is(err, seal.ErrInvalidPassword) {
s.logger.Warn("unseal attempt with invalid password", "remote_addr", r.RemoteAddr)
http.Error(w, `{"error":"invalid password"}`, http.StatusUnauthorized)
} else if errors.Is(err, seal.ErrRateLimited) {
s.logger.Warn("unseal attempt rate limited", "remote_addr", r.RemoteAddr)
http.Error(w, `{"error":"too many attempts, try again later"}`, http.StatusTooManyRequests)
} else if errors.Is(err, seal.ErrNotSealed) {
s.logger.Warn("unseal attempt on already-unsealed service", "remote_addr", r.RemoteAddr)
http.Error(w, `{"error":"already unsealed"}`, http.StatusConflict)
} else {
s.logger.Error("unseal failed", "remote_addr", r.RemoteAddr, "error", err)
http.Error(w, `{"error":"unseal failed"}`, http.StatusInternalServerError)
}
return
}
if err := s.engines.UnsealAll(r.Context()); err != nil {
s.logger.Error("engine unseal failed", "error", err)
http.Error(w, `{"error":"engine unseal failed"}`, http.StatusInternalServerError)
return
}
s.logger.Info("service unsealed", "remote_addr", r.RemoteAddr)
writeJSON(w, http.StatusOK, map[string]interface{}{
"state": s.seal.State().String(),
})
}
func (s *Server) handleSeal(w http.ResponseWriter, r *http.Request) {
if err := s.engines.SealAll(); err != nil {
s.logger.Error("seal engines failed", "error", err)
}
if err := s.seal.Seal(); err != nil {
s.logger.Error("seal failed", "error", err)
http.Error(w, `{"error":"seal failed"}`, http.StatusInternalServerError)
return
}
s.auth.ClearCache()
writeJSON(w, http.StatusOK, map[string]interface{}{
"state": s.seal.State().String(),
})
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if s.seal.State() != seal.StateUnsealed {
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
return
}
var req struct {
Username string `json:"username"`
Password string `json:"password"`
TOTPCode string `json:"totp_code"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
token, expiresAt, err := s.auth.Login(req.Username, req.Password, req.TOTPCode)
if err != nil {
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"token": token,
"expires_at": expiresAt,
})
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
token := extractToken(r)
client, err := mcias.New(s.cfg.MCIAS.ServerURL, mcias.Options{
CACertPath: s.cfg.MCIAS.CACert,
Token: token,
})
if err == nil {
_ = s.auth.Logout(client)
}
// Clear cookie.
http.SetCookie(w, &http.Cookie{
Name: "metacrypt_token",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
func (s *Server) handleTokenInfo(w http.ResponseWriter, r *http.Request) {
info := TokenInfoFromContext(r.Context())
writeJSON(w, http.StatusOK, map[string]interface{}{
"username": info.Username,
"roles": info.Roles,
"is_admin": info.IsAdmin,
})
}
func (s *Server) handleEngineMounts(w http.ResponseWriter, r *http.Request) {
mounts := s.engines.ListMounts()
writeJSON(w, http.StatusOK, mounts)
}
func (s *Server) handleEngineMount(w http.ResponseWriter, r *http.Request) {
var req struct {
Config map[string]interface{} `json:"config"`
Name string `json:"name"`
Type string `json:"type"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if req.Name == "" || req.Type == "" {
http.Error(w, `{"error":"name and type are required"}`, http.StatusBadRequest)
return
}
if err := s.engines.Mount(r.Context(), req.Name, engine.EngineType(req.Type), req.Config); err != nil {
s.logger.Error("mount engine", "name", req.Name, "type", req.Type, "error", err)
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusBadRequest)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
func (s *Server) handleEngineUnmount(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if err := s.engines.Unmount(r.Context(), req.Name); err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
// adminOnlyOperations lists engine operations that require admin role.
// This enforces the same gates as the typed REST routes, ensuring the
// generic endpoint cannot bypass admin requirements.
var adminOnlyOperations = map[string]bool{
// CA engine.
"import-root": true,
"create-issuer": true,
"delete-issuer": true,
"revoke-cert": true,
"delete-cert": true,
// Transit engine.
"create-key": true,
"delete-key": true,
"rotate-key": true,
"update-key-config": true,
"trim-key": true,
// SSH CA engine.
"create-profile": true,
"update-profile": true,
"delete-profile": true,
// User engine.
"provision": true,
"delete-user": true,
}
func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
var req struct {
Data map[string]interface{} `json:"data"`
Mount string `json:"mount"`
Operation string `json:"operation"`
Path string `json:"path"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if req.Mount == "" || req.Operation == "" {
http.Error(w, `{"error":"mount and operation are required"}`, http.StatusBadRequest)
return
}
info := TokenInfoFromContext(r.Context())
// Enforce admin requirement for operations that have admin-only typed routes.
if adminOnlyOperations[req.Operation] && !info.IsAdmin {
http.Error(w, `{"error":"forbidden: admin required"}`, http.StatusForbidden)
return
}
// Evaluate policy before dispatching to the engine.
policyReq := &policy.Request{
Username: info.Username,
Roles: info.Roles,
Resource: "engine/" + req.Mount + "/" + req.Operation,
Action: operationAction(req.Operation),
}
effect, err := s.policy.Evaluate(r.Context(), policyReq)
if err != nil {
s.logger.Error("policy evaluation failed", "error", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
if effect != policy.EffectAllow {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
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
}
engReq := &engine.Request{
Operation: req.Operation,
Path: req.Path,
Data: req.Data,
CallerInfo: &engine.CallerInfo{
Username: info.Username,
Roles: info.Roles,
IsAdmin: info.IsAdmin,
},
CheckPolicy: policyChecker,
}
resp, err := s.engines.HandleRequest(r.Context(), req.Mount, engReq)
if err != nil {
status := http.StatusInternalServerError
switch {
case errors.Is(err, engine.ErrMountNotFound):
status = http.StatusNotFound
case errors.Is(err, ca.ErrIdentifierInUse):
status = http.StatusConflict
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
}
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
return
}
writeJSON(w, http.StatusOK, resp.Data)
}
func (s *Server) handlePolicyRules(w http.ResponseWriter, r *http.Request) {
info := TokenInfoFromContext(r.Context())
switch r.Method {
case http.MethodGet:
if !info.IsAdmin {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
rules, err := s.policy.ListRules(r.Context())
if err != nil {
s.logger.Error("list policies", "error", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
if rules == nil {
rules = []policy.Rule{}
}
writeJSON(w, http.StatusOK, rules)
case http.MethodPost:
if !info.IsAdmin {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
var rule policy.Rule
if err := readJSON(r, &rule); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if rule.ID == "" {
http.Error(w, `{"error":"id is required"}`, http.StatusBadRequest)
return
}
if err := s.policy.CreateRule(r.Context(), &rule); err != nil {
s.logger.Error("create policy", "error", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusCreated, rule)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
func (s *Server) handlePolicyRule(w http.ResponseWriter, r *http.Request) {
info := TokenInfoFromContext(r.Context())
if !info.IsAdmin {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, `{"error":"id parameter required"}`, http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
rule, err := s.policy.GetRule(r.Context(), id)
if err != nil {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, rule)
case http.MethodDelete:
if err := s.policy.DeleteRule(r.Context(), id); err != nil {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
// --- Barrier Key Management Handlers ---
func (s *Server) handleBarrierKeys(w http.ResponseWriter, r *http.Request) {
keys, err := s.seal.Barrier().ListKeys(r.Context())
if err != nil {
s.logger.Error("list barrier keys", "error", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
if keys == nil {
keys = []barrier.KeyInfo{}
}
writeJSON(w, http.StatusOK, keys)
}
func (s *Server) handleRotateMEK(w http.ResponseWriter, r *http.Request) {
var req struct {
Password string `json:"password"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if req.Password == "" {
http.Error(w, `{"error":"password is required"}`, http.StatusBadRequest)
return
}
if err := s.seal.RotateMEK(r.Context(), []byte(req.Password)); err != nil {
if errors.Is(err, seal.ErrInvalidPassword) {
http.Error(w, `{"error":"invalid password"}`, http.StatusUnauthorized)
return
}
if errors.Is(err, seal.ErrSealed) {
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
return
}
s.logger.Error("rotate MEK", "error", err)
http.Error(w, `{"error":"rotation failed"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
func (s *Server) handleRotateKey(w http.ResponseWriter, r *http.Request) {
var req struct {
KeyID string `json:"key_id"`
}
if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
if req.KeyID == "" {
http.Error(w, `{"error":"key_id is required"}`, http.StatusBadRequest)
return
}
if err := s.seal.Barrier().RotateKey(r.Context(), req.KeyID); err != nil {
if errors.Is(err, barrier.ErrKeyNotFound) {
http.Error(w, `{"error":"key not found"}`, http.StatusNotFound)
return
}
s.logger.Error("rotate key", "key_id", req.KeyID, "error", err)
http.Error(w, `{"error":"rotation failed"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
}
func (s *Server) handleBarrierMigrate(w http.ResponseWriter, r *http.Request) {
migrated, err := s.seal.Barrier().MigrateToV2(r.Context())
if err != nil {
s.logger.Error("barrier migration", "error", err)
http.Error(w, `{"error":"migration failed"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"migrated": migrated,
})
}
// --- CA Certificate Handlers ---
func (s *Server) handleGetCert(w http.ResponseWriter, r *http.Request) {
mountName := chi.URLParam(r, "mount")
serial := chi.URLParam(r, "serial")
info := TokenInfoFromContext(r.Context())
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
Operation: "get-cert",
Data: map[string]interface{}{"serial": serial},
CallerInfo: &engine.CallerInfo{
Username: info.Username,
Roles: info.Roles,
IsAdmin: info.IsAdmin,
},
})
if err != nil {
if errors.Is(err, ca.ErrCertNotFound) {
http.Error(w, `{"error":"certificate not found"}`, http.StatusNotFound)
return
}
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, resp.Data)
}
func (s *Server) handleRevokeCert(w http.ResponseWriter, r *http.Request) {
mountName := chi.URLParam(r, "mount")
serial := chi.URLParam(r, "serial")
info := TokenInfoFromContext(r.Context())
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
Operation: "revoke-cert",
Data: map[string]interface{}{"serial": serial},
CallerInfo: &engine.CallerInfo{
Username: info.Username,
Roles: info.Roles,
IsAdmin: info.IsAdmin,
},
})
if err != nil {
if errors.Is(err, ca.ErrCertNotFound) {
http.Error(w, `{"error":"certificate not found"}`, http.StatusNotFound)
return
}
if errors.Is(err, ca.ErrForbidden) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, resp.Data)
}
func (s *Server) handleDeleteCert(w http.ResponseWriter, r *http.Request) {
mountName := chi.URLParam(r, "mount")
serial := chi.URLParam(r, "serial")
info := TokenInfoFromContext(r.Context())
_, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
Operation: "delete-cert",
Data: map[string]interface{}{"serial": serial},
CallerInfo: &engine.CallerInfo{
Username: info.Username,
Roles: info.Roles,
IsAdmin: info.IsAdmin,
},
})
if err != nil {
if errors.Is(err, ca.ErrCertNotFound) {
http.Error(w, `{"error":"certificate not found"}`, http.StatusNotFound)
return
}
if errors.Is(err, ca.ErrForbidden) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusNoContent, nil)
}
// --- Public PKI Handlers ---
func (s *Server) handlePKIRoot(w http.ResponseWriter, r *http.Request) {
mountName := chi.URLParam(r, "mount")
caEng, err := s.getCAEngine(mountName)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
return
}
certPEM, err := caEng.GetRootCertPEM()
if err != nil {
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
_, _ = w.Write(certPEM) //nolint:gosec
}
func (s *Server) handlePKIChain(w http.ResponseWriter, r *http.Request) {
mountName := chi.URLParam(r, "mount")
issuerName := r.URL.Query().Get("issuer")
if issuerName == "" {
http.Error(w, `{"error":"issuer query parameter required"}`, http.StatusBadRequest)
return
}
caEng, err := s.getCAEngine(mountName)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
return
}
chainPEM, err := caEng.GetChainPEM(issuerName)
if err != nil {
if errors.Is(err, ca.ErrIssuerNotFound) {
http.Error(w, `{"error":"issuer not found"}`, http.StatusNotFound)
return
}
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
_, _ = w.Write(chainPEM) //nolint:gosec
}
func (s *Server) handlePKIIssuer(w http.ResponseWriter, r *http.Request) {
mountName := chi.URLParam(r, "mount")
issuerName := chi.URLParam(r, "name")
caEng, err := s.getCAEngine(mountName)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
return
}
certPEM, err := caEng.GetIssuerCertPEM(issuerName)
if err != nil {
if errors.Is(err, ca.ErrIssuerNotFound) {
http.Error(w, `{"error":"issuer not found"}`, http.StatusNotFound)
return
}
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
_, _ = w.Write(certPEM) //nolint:gosec
}
func (s *Server) handlePKICRL(w http.ResponseWriter, r *http.Request) {
mountName := chi.URLParam(r, "mount")
issuerName := chi.URLParam(r, "name")
caEng, err := s.getCAEngine(mountName)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
return
}
crlDER, err := caEng.GetCRLDER(r.Context(), issuerName)
if err != nil {
if errors.Is(err, ca.ErrIssuerNotFound) {
http.Error(w, `{"error":"issuer not found"}`, http.StatusNotFound)
return
}
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/pkix-crl")
_, _ = w.Write(crlDER) //nolint:gosec
}
func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) {
mount, err := s.engines.GetMount(mountName)
if err != nil {
return nil, err
}
if mount.Type != engine.EngineTypeCA {
return nil, errors.New("mount is not a CA engine")
}
caEng, ok := mount.Engine.(*ca.CAEngine)
if !ok {
return nil, errors.New("mount is not a CA engine")
}
return caEng, nil
}
// operationAction maps an engine operation name to a policy action.
func operationAction(op string) string {
switch op {
// Read operations.
case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer",
"list-keys", "get-key", "get-public-key", "list-users", "get-profile", "list-profiles":
return policy.ActionRead
// Granular cryptographic operations (including batch variants).
case "encrypt", "batch-encrypt":
return policy.ActionEncrypt
case "decrypt", "batch-decrypt":
return policy.ActionDecrypt
case "sign", "sign-host", "sign-user":
return policy.ActionSign
case "verify":
return policy.ActionVerify
case "hmac":
return policy.ActionHMAC
// Everything else is a write.
default:
return policy.ActionWrite
}
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func readJSON(r *http.Request, v interface{}) error {
defer func() { _ = r.Body.Close() }()
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit
if err != nil {
return err
}
return json.Unmarshal(body, v)
}