Add CRL endpoint, sign-CSR web route, and policy-based issuance authorization

- Register handleSignCSR route in webserver (was dead code)
- Add GET /v1/pki/{mount}/issuer/{name}/crl REST endpoint and
  PKIService.GetCRL gRPC RPC for DER-encoded CRL generation
- Replace admin-only gates on issue/renew/sign-csr with policy-based
  access control: admins grant-all, authenticated users subject to
  identifier ownership (CN/SANs not held by another user's active cert)
  and optional policy overrides via ca/{mount}/id/{identifier} resources
- Add PolicyChecker to engine.Request and policy.Match() method to
  distinguish matched rules from default deny
- Update and expand CA engine tests for ownership, revocation freeing,
  and policy override scenarios

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 15:22:04 -07:00
parent fbd6d1af04
commit ac4577f778
11 changed files with 810 additions and 68 deletions

View File

@@ -43,6 +43,7 @@ func (s *Server) registerRoutes(r chi.Router) {
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.HandleFunc("/v1/policy/rules", s.requireAuth(s.handlePolicyRules))
r.HandleFunc("/v1/policy/rule", s.requireAuth(s.handlePolicyRule))
@@ -288,6 +289,20 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
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,
@@ -297,6 +312,7 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
Roles: info.Roles,
IsAdmin: info.IsAdmin,
},
CheckPolicy: policyChecker,
}
resp, err := s.engines.HandleRequest(r.Context(), req.Mount, engReq)
@@ -305,6 +321,8 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
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"):
@@ -551,6 +569,30 @@ func (s *Server) handlePKIIssuer(w http.ResponseWriter, r *http.Request) {
_, _ = 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 {