Add certificate revocation, deletion, and retrieval

Admins can now revoke or delete certificate records from the cert detail
page in the web UI. Revoked certificates display a [REVOKED] badge and
show revocation metadata (time and actor). Deletion redirects to the
issuer page.

The REST API gains three new authenticated endpoints that mirror the
gRPC surface:
  GET    /v1/ca/{mount}/cert/{serial}         (auth required)
  POST   /v1/ca/{mount}/cert/{serial}/revoke  (admin only)
  DELETE /v1/ca/{mount}/cert/{serial}         (admin only)

The CA engine stores revocation state (revoked, revoked_at, revoked_by)
directly in the existing CertRecord barrier entry. The proto CertRecord
message is extended with the same three fields (field numbers 10–12).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 13:37:54 -07:00
parent 74e35ce63e
commit d574685b99
27 changed files with 839 additions and 91 deletions

View File

@@ -34,6 +34,11 @@ func (s *Server) registerRoutes(r chi.Router) {
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))
@@ -370,6 +375,91 @@ func (s *Server) handlePolicyRule(w http.ResponseWriter, r *http.Request) {
}
}
// --- 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) {