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/engine/sshca" "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 SSH CA routes (no auth required, but must be unsealed). r.Get("/v1/sshca/{mount}/ca", s.requireUnseal(s.handleSSHCAPubkey)) r.Get("/v1/sshca/{mount}/krl", s.requireUnseal(s.handleSSHCAKRL)) // SSH CA auth-required routes. r.Post("/v1/sshca/{mount}/sign-host", s.requireAuth(s.handleSSHCASignHost)) r.Post("/v1/sshca/{mount}/sign-user", s.requireAuth(s.handleSSHCASignUser)) r.Post("/v1/sshca/{mount}/profiles", s.requireAdmin(s.handleSSHCACreateProfile)) r.Put("/v1/sshca/{mount}/profiles/{name}", s.requireAdmin(s.handleSSHCAUpdateProfile)) r.Get("/v1/sshca/{mount}/profiles/{name}", s.requireAuth(s.handleSSHCAGetProfile)) r.Get("/v1/sshca/{mount}/profiles", s.requireAuth(s.handleSSHCAListProfiles)) r.Delete("/v1/sshca/{mount}/profiles/{name}", s.requireAdmin(s.handleSSHCADeleteProfile)) r.Get("/v1/sshca/{mount}/certs/{serial}", s.requireAuth(s.handleSSHCAGetCert)) r.Get("/v1/sshca/{mount}/certs", s.requireAuth(s.handleSSHCAListCerts)) r.Post("/v1/sshca/{mount}/certs/{serial}/revoke", s.requireAdmin(s.handleSSHCARevokeCert)) r.Delete("/v1/sshca/{mount}/certs/{serial}", s.requireAdmin(s.handleSSHCADeleteCert)) // 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)) // User-to-user encryption routes (auth required). r.Post("/v1/user/{mount}/register", s.requireAuth(s.handleUserRegister)) r.Post("/v1/user/{mount}/provision", s.requireAdmin(s.handleUserProvision)) r.Get("/v1/user/{mount}/keys", s.requireAuth(s.handleUserListUsers)) r.Get("/v1/user/{mount}/keys/{username}", s.requireAuth(s.handleUserGetPublicKey)) r.Delete("/v1/user/{mount}/keys/{username}", s.requireAdmin(s.handleUserDeleteUser)) r.Post("/v1/user/{mount}/encrypt", s.requireAuth(s.handleUserEncrypt)) r.Post("/v1/user/{mount}/decrypt", s.requireAuth(s.handleUserDecrypt)) r.Post("/v1/user/{mount}/re-encrypt", s.requireAuth(s.handleUserReEncrypt)) r.Post("/v1/user/{mount}/rotate", s.requireAuth(s.handleUserRotateKey)) 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) } // --- 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, } // adminExemptOperations lists engineType:operation pairs that are exempt from // the unqualified admin-only check (e.g. user:rotate-key is user-self, not admin). var adminExemptOperations = map[string]bool{ "user:rotate-key": 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()) // Resolve engine type from mount. mount, err := s.engines.GetMount(req.Mount) // Enforce admin requirement for operations that are admin-only. // Check exemptions for engine-specific overrides (e.g. user:rotate-key is user-self). if adminOnlyOperations[req.Operation] && !info.IsAdmin { exempt := err == nil && adminExemptOperations[string(mount.Type)+":"+req.Operation] if !exempt { http.Error(w, `{"error":"forbidden: admin required"}`, http.StatusForbidden) return } } if err != nil { http.Error(w, `{"error":"mount not found"}`, http.StatusNotFound) 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 } // --- 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 { s.writeEngineError(w, err) 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}) } // --- User-to-User Encryption Handlers --- func (s *Server) handleUserRegister(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "register", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleUserProvision(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") var req struct { Username string `json:"username"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.Username == "" { http.Error(w, `{"error":"username is required"}`, http.StatusBadRequest) return } info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "provision", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, Data: map[string]interface{}{"username": req.Username}, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleUserListUsers(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "list-users", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleUserGetPublicKey(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") username := chi.URLParam(r, "username") info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "get-public-key", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, Data: map[string]interface{}{"username": username}, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleUserDeleteUser(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") username := chi.URLParam(r, "username") info := TokenInfoFromContext(r.Context()) _, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "delete-user", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, Data: map[string]interface{}{"username": username}, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true}) } func (s *Server) handleUserEncrypt(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") var req struct { Plaintext string `json:"plaintext"` Metadata string `json:"metadata"` Recipients []string `json:"recipients"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } recipients := make([]interface{}, len(req.Recipients)) for i, r := range req.Recipients { recipients[i] = r } 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 } data := map[string]interface{}{ "plaintext": req.Plaintext, "recipients": recipients, } if req.Metadata != "" { data["metadata"] = req.Metadata } resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "encrypt", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, CheckPolicy: policyChecker, Data: data, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleUserDecrypt(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") var req struct { Envelope string `json:"envelope"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "decrypt", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, Data: map[string]interface{}{"envelope": req.Envelope}, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleUserReEncrypt(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") var req struct { Envelope string `json:"envelope"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "re-encrypt", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, Data: map[string]interface{}{"envelope": req.Envelope}, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleUserRotateKey(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "rotate-key", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } // 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", "get-ca-pubkey", "get-krl": return policy.ActionRead // Granular cryptographic operations (including batch variants). case "encrypt", "batch-encrypt": return policy.ActionEncrypt case "decrypt", "batch-decrypt", "rewrap", "batch-rewrap", "re-encrypt": 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) } // --- SSH CA Handlers --- func (s *Server) getSSHCAEngine(mountName string) (*sshca.SSHCAEngine, error) { mount, err := s.engines.GetMount(mountName) if err != nil { return nil, err } if mount.Type != engine.EngineTypeSSHCA { return nil, errors.New("mount is not an SSH CA engine") } eng, ok := mount.Engine.(*sshca.SSHCAEngine) if !ok { return nil, errors.New("mount is not an SSH CA engine") } return eng, nil } func (s *Server) handleSSHCAPubkey(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") eng, err := s.getSSHCAEngine(mountName) if err != nil { http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound) return } pubKey, err := eng.GetCAPubkey(r.Context()) if err != nil { http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "text/plain") _, _ = w.Write(pubKey) //nolint:gosec } func (s *Server) handleSSHCAKRL(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") eng, err := s.getSSHCAEngine(mountName) if err != nil { http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound) return } krlData, err := eng.GetKRL() if err != nil { http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "application/octet-stream") _, _ = w.Write(krlData) //nolint:gosec } func (s *Server) handleSSHCASignHost(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") var req struct { PublicKey string `json:"public_key"` Hostname string `json:"hostname"` TTL string `json:"ttl"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } info := TokenInfoFromContext(r.Context()) data := map[string]interface{}{ "public_key": req.PublicKey, "hostname": req.Hostname, } if req.TTL != "" { data["ttl"] = req.TTL } resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "sign-host", Data: data, CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCASignUser(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") var req struct { PublicKey string `json:"public_key"` Principals []string `json:"principals"` Profile string `json:"profile"` TTL string `json:"ttl"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } info := TokenInfoFromContext(r.Context()) data := map[string]interface{}{ "public_key": req.PublicKey, } if len(req.Principals) > 0 { principals := make([]interface{}, len(req.Principals)) for i, p := range req.Principals { principals[i] = p } data["principals"] = principals } if req.Profile != "" { data["profile"] = req.Profile } if req.TTL != "" { data["ttl"] = req.TTL } 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(), mountName, &engine.Request{ Operation: "sign-user", Data: data, CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, CheckPolicy: policyChecker, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCACreateProfile(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") var req struct { Name string `json:"name"` CriticalOptions map[string]string `json:"critical_options"` Extensions map[string]string `json:"extensions"` MaxTTL string `json:"max_ttl"` AllowedPrincipals []string `json:"allowed_principals"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } info := TokenInfoFromContext(r.Context()) data := map[string]interface{}{"name": req.Name} if req.CriticalOptions != nil { opts := make(map[string]interface{}, len(req.CriticalOptions)) for k, v := range req.CriticalOptions { opts[k] = v } data["critical_options"] = opts } if req.Extensions != nil { exts := make(map[string]interface{}, len(req.Extensions)) for k, v := range req.Extensions { exts[k] = v } data["extensions"] = exts } if req.MaxTTL != "" { data["max_ttl"] = req.MaxTTL } if len(req.AllowedPrincipals) > 0 { principals := make([]interface{}, len(req.AllowedPrincipals)) for i, p := range req.AllowedPrincipals { principals[i] = p } data["allowed_principals"] = principals } resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "create-profile", Data: data, CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusCreated, resp.Data) } func (s *Server) handleSSHCAUpdateProfile(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") name := chi.URLParam(r, "name") var req struct { CriticalOptions map[string]string `json:"critical_options"` Extensions map[string]string `json:"extensions"` MaxTTL string `json:"max_ttl"` AllowedPrincipals []string `json:"allowed_principals"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } info := TokenInfoFromContext(r.Context()) data := map[string]interface{}{"name": name} if req.CriticalOptions != nil { opts := make(map[string]interface{}, len(req.CriticalOptions)) for k, v := range req.CriticalOptions { opts[k] = v } data["critical_options"] = opts } if req.Extensions != nil { exts := make(map[string]interface{}, len(req.Extensions)) for k, v := range req.Extensions { exts[k] = v } data["extensions"] = exts } if req.MaxTTL != "" { data["max_ttl"] = req.MaxTTL } if len(req.AllowedPrincipals) > 0 { principals := make([]interface{}, len(req.AllowedPrincipals)) for i, p := range req.AllowedPrincipals { principals[i] = p } data["allowed_principals"] = principals } resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "update-profile", Data: data, CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCAGetProfile(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") name := chi.URLParam(r, "name") info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "get-profile", Data: map[string]interface{}{"name": name}, CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCAListProfiles(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "list-profiles", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCADeleteProfile(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") name := chi.URLParam(r, "name") info := TokenInfoFromContext(r.Context()) _, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "delete-profile", Data: map[string]interface{}{"name": name}, CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusNoContent, nil) } func (s *Server) handleSSHCAGetCert(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 { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCAListCerts(w http.ResponseWriter, r *http.Request) { mountName := chi.URLParam(r, "mount") info := TokenInfoFromContext(r.Context()) resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{ Operation: "list-certs", CallerInfo: &engine.CallerInfo{ Username: info.Username, Roles: info.Roles, IsAdmin: info.IsAdmin, }, }) if err != nil { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCARevokeCert(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 { s.writeEngineError(w, err) return } writeJSON(w, http.StatusOK, resp.Data) } func (s *Server) handleSSHCADeleteCert(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 { s.writeEngineError(w, err) return } writeJSON(w, http.StatusNoContent, nil) } func (s *Server) writeEngineError(w http.ResponseWriter, err error) { status := http.StatusInternalServerError switch { case errors.Is(err, engine.ErrMountNotFound): status = http.StatusNotFound case errors.Is(err, sshca.ErrCertNotFound): status = http.StatusNotFound case errors.Is(err, sshca.ErrProfileNotFound): status = http.StatusNotFound case errors.Is(err, sshca.ErrProfileExists): status = http.StatusConflict case errors.Is(err, sshca.ErrForbidden): status = http.StatusForbidden case errors.Is(err, sshca.ErrUnauthorized): status = http.StatusUnauthorized case strings.Contains(err.Error(), "forbidden"), strings.Contains(err.Error(), "not allowed"): 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(), "unsupported"), strings.Contains(err.Error(), "invalid"), strings.Contains(err.Error(), "too many"): status = http.StatusBadRequest } http.Error(w, `{"error":"`+err.Error()+`"}`, status) }