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:
@@ -304,6 +304,10 @@ func (e *CAEngine) HandleRequest(ctx context.Context, req *engine.Request) (*eng
|
||||
return e.handleSignCSR(ctx, req)
|
||||
case "import-root":
|
||||
return e.handleImportRoot(ctx, req)
|
||||
case "revoke-cert":
|
||||
return e.handleRevokeCert(ctx, req)
|
||||
case "delete-cert":
|
||||
return e.handleDeleteCert(ctx, req)
|
||||
default:
|
||||
return nil, fmt.Errorf("ca: unknown operation: %s", req.Operation)
|
||||
}
|
||||
@@ -831,19 +835,23 @@ func (e *CAEngine) handleGetCert(ctx context.Context, req *engine.Request) (*eng
|
||||
return nil, fmt.Errorf("ca: parse cert record: %w", err)
|
||||
}
|
||||
|
||||
return &engine.Response{
|
||||
Data: map[string]interface{}{
|
||||
"serial": record.Serial,
|
||||
"issuer": record.Issuer,
|
||||
"cn": record.CN,
|
||||
"sans": record.SANs,
|
||||
"profile": record.Profile,
|
||||
"cert_pem": record.CertPEM,
|
||||
"issued_by": record.IssuedBy,
|
||||
"issued_at": record.IssuedAt.Format(time.RFC3339),
|
||||
"expires_at": record.ExpiresAt.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
data := map[string]interface{}{
|
||||
"serial": record.Serial,
|
||||
"issuer": record.Issuer,
|
||||
"cn": record.CN,
|
||||
"sans": record.SANs,
|
||||
"profile": record.Profile,
|
||||
"cert_pem": record.CertPEM,
|
||||
"issued_by": record.IssuedBy,
|
||||
"issued_at": record.IssuedAt.Format(time.RFC3339),
|
||||
"expires_at": record.ExpiresAt.Format(time.RFC3339),
|
||||
"revoked": record.Revoked,
|
||||
}
|
||||
if record.Revoked {
|
||||
data["revoked_at"] = record.RevokedAt.Format(time.RFC3339)
|
||||
data["revoked_by"] = record.RevokedBy
|
||||
}
|
||||
return &engine.Response{Data: data}, nil
|
||||
}
|
||||
|
||||
func (e *CAEngine) handleListCerts(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||
@@ -1119,6 +1127,99 @@ func (e *CAEngine) handleSignCSR(ctx context.Context, req *engine.Request) (*eng
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CAEngine) handleRevokeCert(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||
if req.CallerInfo == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
if !req.CallerInfo.IsAdmin {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
|
||||
serial, _ := req.Data["serial"].(string)
|
||||
if serial == "" {
|
||||
serial = req.Path
|
||||
}
|
||||
if serial == "" {
|
||||
return nil, fmt.Errorf("ca: serial is required")
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
recordData, err := e.barrier.Get(ctx, e.mountPath+"certs/"+serial+".json")
|
||||
if err != nil {
|
||||
if errors.Is(err, barrier.ErrNotFound) {
|
||||
return nil, ErrCertNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("ca: load cert record: %w", err)
|
||||
}
|
||||
|
||||
var record CertRecord
|
||||
if err := json.Unmarshal(recordData, &record); err != nil {
|
||||
return nil, fmt.Errorf("ca: parse cert record: %w", err)
|
||||
}
|
||||
|
||||
if record.Revoked {
|
||||
return nil, fmt.Errorf("ca: certificate is already revoked")
|
||||
}
|
||||
|
||||
record.Revoked = true
|
||||
record.RevokedAt = time.Now()
|
||||
record.RevokedBy = req.CallerInfo.Username
|
||||
|
||||
updated, err := json.Marshal(&record)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ca: marshal cert record: %w", err)
|
||||
}
|
||||
if err := e.barrier.Put(ctx, e.mountPath+"certs/"+serial+".json", updated); err != nil {
|
||||
return nil, fmt.Errorf("ca: store cert record: %w", err)
|
||||
}
|
||||
|
||||
return &engine.Response{
|
||||
Data: map[string]interface{}{
|
||||
"serial": serial,
|
||||
"revoked_at": record.RevokedAt.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CAEngine) handleDeleteCert(ctx context.Context, req *engine.Request) (*engine.Response, error) {
|
||||
if req.CallerInfo == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
if !req.CallerInfo.IsAdmin {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
|
||||
serial, _ := req.Data["serial"].(string)
|
||||
if serial == "" {
|
||||
serial = req.Path
|
||||
}
|
||||
if serial == "" {
|
||||
return nil, fmt.Errorf("ca: serial is required")
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
// Verify the record exists before deleting.
|
||||
_, err := e.barrier.Get(ctx, e.mountPath+"certs/"+serial+".json")
|
||||
if err != nil {
|
||||
if errors.Is(err, barrier.ErrNotFound) {
|
||||
return nil, ErrCertNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("ca: load cert record: %w", err)
|
||||
}
|
||||
|
||||
if err := e.barrier.Delete(ctx, e.mountPath+"certs/"+serial+".json"); err != nil {
|
||||
return nil, fmt.Errorf("ca: delete cert record: %w", err)
|
||||
}
|
||||
|
||||
return &engine.Response{
|
||||
Data: map[string]interface{}{"ok": true},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func defaultCAConfig() *CAConfig {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ca
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CAConfig is the CA engine configuration stored in the barrier.
|
||||
type CAConfig struct {
|
||||
@@ -27,11 +29,14 @@ type IssuerConfig struct {
|
||||
type CertRecord struct {
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
RevokedAt time.Time `json:"revoked_at,omitempty"`
|
||||
Serial string `json:"serial"`
|
||||
Issuer string `json:"issuer"`
|
||||
CN string `json:"cn"`
|
||||
Profile string `json:"profile"`
|
||||
CertPEM string `json:"cert_pem"`
|
||||
IssuedBy string `json:"issued_by"`
|
||||
RevokedBy string `json:"revoked_by,omitempty"`
|
||||
SANs []string `json:"sans,omitempty"`
|
||||
Revoked bool `json:"revoked,omitempty"`
|
||||
}
|
||||
|
||||
@@ -420,6 +420,45 @@ func (cs *caServer) SignCSR(ctx context.Context, req *pb.SignCSRRequest) (*pb.Si
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *caServer) RevokeCert(ctx context.Context, req *pb.RevokeCertRequest) (*pb.RevokeCertResponse, error) {
|
||||
if req.Mount == "" || req.Serial == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and serial are required")
|
||||
}
|
||||
resp, err := cs.caHandleRequest(ctx, req.Mount, "revoke-cert", &engine.Request{
|
||||
Operation: "revoke-cert",
|
||||
CallerInfo: cs.callerInfo(ctx),
|
||||
Data: map[string]interface{}{"serial": req.Serial},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serial, _ := resp.Data["serial"].(string)
|
||||
var revokedAt *timestamppb.Timestamp
|
||||
if s, ok := resp.Data["revoked_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
revokedAt = timestamppb.New(t)
|
||||
}
|
||||
}
|
||||
cs.s.logger.Info("audit: certificate revoked", "mount", req.Mount, "serial", serial, "username", callerUsername(ctx))
|
||||
return &pb.RevokeCertResponse{Serial: serial, RevokedAt: revokedAt}, nil
|
||||
}
|
||||
|
||||
func (cs *caServer) DeleteCert(ctx context.Context, req *pb.DeleteCertRequest) (*pb.DeleteCertResponse, error) {
|
||||
if req.Mount == "" || req.Serial == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "mount and serial are required")
|
||||
}
|
||||
_, err := cs.caHandleRequest(ctx, req.Mount, "delete-cert", &engine.Request{
|
||||
Operation: "delete-cert",
|
||||
CallerInfo: cs.callerInfo(ctx),
|
||||
Data: map[string]interface{}{"serial": req.Serial},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs.s.logger.Info("audit: certificate deleted", "mount", req.Mount, "serial", req.Serial, "username", callerUsername(ctx))
|
||||
return &pb.DeleteCertResponse{}, nil
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func certRecordFromData(d map[string]interface{}) *pb.CertRecord {
|
||||
@@ -429,8 +468,10 @@ func certRecordFromData(d map[string]interface{}) *pb.CertRecord {
|
||||
profile, _ := d["profile"].(string)
|
||||
issuedBy, _ := d["issued_by"].(string)
|
||||
certPEM, _ := d["cert_pem"].(string)
|
||||
revoked, _ := d["revoked"].(bool)
|
||||
revokedBy, _ := d["revoked_by"].(string)
|
||||
sans := toStringSliceFromInterface(d["sans"])
|
||||
var issuedAt, expiresAt *timestamppb.Timestamp
|
||||
var issuedAt, expiresAt, revokedAt *timestamppb.Timestamp
|
||||
if s, ok := d["issued_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
issuedAt = timestamppb.New(t)
|
||||
@@ -441,6 +482,11 @@ func certRecordFromData(d map[string]interface{}) *pb.CertRecord {
|
||||
expiresAt = timestamppb.New(t)
|
||||
}
|
||||
}
|
||||
if s, ok := d["revoked_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
revokedAt = timestamppb.New(t)
|
||||
}
|
||||
}
|
||||
return &pb.CertRecord{
|
||||
Serial: serial,
|
||||
Issuer: issuer,
|
||||
@@ -451,6 +497,9 @@ func certRecordFromData(d map[string]interface{}) *pb.CertRecord {
|
||||
IssuedAt: issuedAt,
|
||||
ExpiresAt: expiresAt,
|
||||
CertPem: []byte(certPEM),
|
||||
Revoked: revoked,
|
||||
RevokedAt: revokedAt,
|
||||
RevokedBy: revokedBy,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -98,6 +98,14 @@ func (m *mockVault) ListCerts(ctx context.Context, token, mount string) ([]CertS
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockVault) RevokeCert(ctx context.Context, token, mount, serial string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVault) DeleteCert(ctx context.Context, token, mount, serial string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVault) Close() error { return nil }
|
||||
|
||||
// newTestWebServer builds a WebServer wired to the given mock, suitable for unit tests.
|
||||
|
||||
@@ -329,6 +329,9 @@ type CertDetail struct {
|
||||
IssuedAt string
|
||||
ExpiresAt string
|
||||
CertPEM string
|
||||
Revoked bool
|
||||
RevokedAt string
|
||||
RevokedBy string
|
||||
}
|
||||
|
||||
// GetCert retrieves a full certificate record by serial number.
|
||||
@@ -349,6 +352,8 @@ func (c *VaultClient) GetCert(ctx context.Context, token, mount, serial string)
|
||||
Profile: rec.Profile,
|
||||
IssuedBy: rec.IssuedBy,
|
||||
CertPEM: string(rec.CertPem),
|
||||
Revoked: rec.Revoked,
|
||||
RevokedBy: rec.RevokedBy,
|
||||
}
|
||||
if rec.IssuedAt != nil {
|
||||
cd.IssuedAt = rec.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
@@ -356,9 +361,24 @@ func (c *VaultClient) GetCert(ctx context.Context, token, mount, serial string)
|
||||
if rec.ExpiresAt != nil {
|
||||
cd.ExpiresAt = rec.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
if rec.RevokedAt != nil {
|
||||
cd.RevokedAt = rec.RevokedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
return cd, nil
|
||||
}
|
||||
|
||||
// RevokeCert marks a certificate as revoked.
|
||||
func (c *VaultClient) RevokeCert(ctx context.Context, token, mount, serial string) error {
|
||||
_, err := c.ca.RevokeCert(withToken(ctx, token), &pb.RevokeCertRequest{Mount: mount, Serial: serial})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteCert permanently removes a certificate record.
|
||||
func (c *VaultClient) DeleteCert(ctx context.Context, token, mount, serial string) error {
|
||||
_, err := c.ca.DeleteCert(withToken(ctx, token), &pb.DeleteCertRequest{Mount: mount, Serial: serial})
|
||||
return err
|
||||
}
|
||||
|
||||
// CertSummary holds lightweight certificate metadata for list views.
|
||||
type CertSummary struct {
|
||||
Serial string
|
||||
|
||||
@@ -43,6 +43,8 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
|
||||
r.Get("/issuer/{issuer}", ws.requireAuth(ws.handleIssuerDetail))
|
||||
r.Get("/cert/{serial}", ws.requireAuth(ws.handleCertDetail))
|
||||
r.Get("/cert/{serial}/download", ws.requireAuth(ws.handleCertDownload))
|
||||
r.Post("/cert/{serial}/revoke", ws.requireAuth(ws.handleCertRevoke))
|
||||
r.Post("/cert/{serial}/delete", ws.requireAuth(ws.handleCertDelete))
|
||||
r.Get("/{issuer}", ws.requireAuth(ws.handlePKIIssuer))
|
||||
})
|
||||
}
|
||||
@@ -531,6 +533,11 @@ func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Stream a tgz archive containing the private key (PKCS8) and certificate.
|
||||
// Extend the write deadline before streaming so that slow gRPC backends
|
||||
// don't consume the server WriteTimeout before we start writing.
|
||||
rc := http.NewResponseController(w)
|
||||
_ = rc.SetWriteDeadline(time.Now().Add(60 * time.Second))
|
||||
|
||||
filename := issuedCert.Serial + ".tgz"
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||
@@ -621,6 +628,70 @@ func (ws *WebServer) handleCertDownload(w http.ResponseWriter, r *http.Request)
|
||||
_, _ = w.Write([]byte(cert.CertPEM))
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleCertRevoke(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
if !info.IsAdmin {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
token := extractCookie(r)
|
||||
mountName, err := ws.findCAMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
serial := chi.URLParam(r, "serial")
|
||||
if err := ws.vault.RevokeCert(r.Context(), token, mountName, serial); err != nil {
|
||||
st, _ := status.FromError(err)
|
||||
if st.Code() == codes.NotFound {
|
||||
http.Error(w, "certificate not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/pki/cert/"+serial, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleCertDelete(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
if !info.IsAdmin {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
token := extractCookie(r)
|
||||
mountName, err := ws.findCAMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
serial := chi.URLParam(r, "serial")
|
||||
|
||||
// Fetch the cert to get the issuer for the redirect.
|
||||
cert, certErr := ws.vault.GetCert(r.Context(), token, mountName, serial)
|
||||
|
||||
if err := ws.vault.DeleteCert(r.Context(), token, mountName, serial); err != nil {
|
||||
st, _ := status.FromError(err)
|
||||
if st.Code() == codes.NotFound {
|
||||
http.Error(w, "certificate not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if certErr == nil && cert != nil {
|
||||
http.Redirect(w, r, "/pki/issuer/"+cert.Issuer, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/pki", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleSignCSR(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
@@ -37,6 +37,8 @@ type vaultBackend interface {
|
||||
SignCSR(ctx context.Context, token string, req SignCSRRequest) (*SignedCert, error)
|
||||
GetCert(ctx context.Context, token, mount, serial string) (*CertDetail, error)
|
||||
ListCerts(ctx context.Context, token, mount string) ([]CertSummary, error)
|
||||
RevokeCert(ctx context.Context, token, mount, serial string) error
|
||||
DeleteCert(ctx context.Context, token, mount, serial string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -98,6 +100,12 @@ func (lw *loggingResponseWriter) WriteHeader(code int) {
|
||||
lw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying ResponseWriter so that http.ResponseController
|
||||
// can reach it to set deadlines and perform other extended operations.
|
||||
func (lw *loggingResponseWriter) Unwrap() http.ResponseWriter {
|
||||
return lw.ResponseWriter
|
||||
}
|
||||
|
||||
// Start starts the web server. It blocks until the server is closed.
|
||||
func (ws *WebServer) Start() error {
|
||||
r := chi.NewRouter()
|
||||
|
||||
Reference in New Issue
Block a user