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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user