Files
metacrypt/internal/webserver/routes.go
Kyle Isom 4deb469a9d Fix missing gRPC interceptor registrations for RevokeCert, DeleteCert, SignCSR
RevokeCert and DeleteCert were not registered in sealRequired, authRequired,
or adminRequired method sets, so the auth interceptor never ran for those
calls and CallerInfo arrived as nil, producing "authentication required".
SignCSR had the same gap in sealRequired and authRequired.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 13:42:43 -07:00

807 lines
23 KiB
Go

package webserver
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// splitLines splits a newline-delimited string into non-empty trimmed lines.
func splitLines(s string) []interface{} {
var out []interface{}
for _, line := range strings.Split(s, "\n") {
if v := strings.TrimSpace(line); v != "" {
out = append(out, v)
}
}
return out
}
func (ws *WebServer) registerRoutes(r chi.Router) {
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(ws.staticFS))))
r.Get("/", ws.handleRoot)
r.HandleFunc("/init", ws.handleInit)
r.HandleFunc("/unseal", ws.handleUnseal)
r.HandleFunc("/login", ws.handleLogin)
r.Get("/dashboard", ws.requireAuth(ws.handleDashboard))
r.Post("/dashboard/mount-ca", ws.requireAuth(ws.handleDashboardMountCA))
r.Route("/pki", func(r chi.Router) {
r.Get("/", ws.requireAuth(ws.handlePKI))
r.Post("/import-root", ws.requireAuth(ws.handleImportRoot))
r.Post("/create-issuer", ws.requireAuth(ws.handleCreateIssuer))
r.Post("/issue", ws.requireAuth(ws.handleIssueCert))
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))
})
}
// requireAuth validates the token cookie against the vault and injects TokenInfo.
func (ws *WebServer) requireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state, err := ws.vault.Status(r.Context())
if err != nil || state != "unsealed" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
token := extractCookie(r)
if token == "" {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
info, err := ws.vault.ValidateToken(r.Context(), token)
if err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
r = r.WithContext(withTokenInfo(r.Context(), info))
next(w, r)
}
}
func (ws *WebServer) handleRoot(w http.ResponseWriter, r *http.Request) {
state, err := ws.vault.Status(r.Context())
if err != nil {
ws.renderTemplate(w, "unseal.html", map[string]interface{}{"Error": "Cannot reach vault"})
return
}
switch state {
case "uninitialized", "initializing":
http.Redirect(w, r, "/init", http.StatusFound)
case "sealed":
http.Redirect(w, r, "/unseal", http.StatusFound)
case "unsealed":
http.Redirect(w, r, "/dashboard", http.StatusFound)
default:
http.Redirect(w, r, "/unseal", http.StatusFound)
}
}
func (ws *WebServer) handleInit(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
state, _ := ws.vault.Status(r.Context())
if state != "uninitialized" && state != "" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
ws.renderTemplate(w, "init.html", nil)
case http.MethodPost:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
password := r.FormValue("password")
if password == "" {
ws.renderTemplate(w, "init.html", map[string]interface{}{"Error": "Password is required"})
return
}
if err := ws.vault.Init(r.Context(), password); err != nil {
ws.renderTemplate(w, "init.html", map[string]interface{}{"Error": grpcMessage(err)})
return
}
http.Redirect(w, r, "/dashboard", http.StatusFound)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (ws *WebServer) handleUnseal(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
state, _ := ws.vault.Status(r.Context())
if state == "uninitialized" {
http.Redirect(w, r, "/init", http.StatusFound)
return
}
if state == "unsealed" {
http.Redirect(w, r, "/dashboard", http.StatusFound)
return
}
ws.renderTemplate(w, "unseal.html", nil)
case http.MethodPost:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
password := r.FormValue("password")
if err := ws.vault.Unseal(r.Context(), password); err != nil {
msg := "Invalid password"
if st, ok := status.FromError(err); ok && st.Code() == codes.ResourceExhausted {
msg = "Too many attempts. Please wait 60 seconds."
}
ws.renderTemplate(w, "unseal.html", map[string]interface{}{"Error": msg})
return
}
http.Redirect(w, r, "/dashboard", http.StatusFound)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (ws *WebServer) handleLogin(w http.ResponseWriter, r *http.Request) {
state, _ := ws.vault.Status(r.Context())
if state != "unsealed" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
switch r.Method {
case http.MethodGet:
ws.renderTemplate(w, "login.html", nil)
case http.MethodPost:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
token, err := ws.vault.Login(r.Context(),
r.FormValue("username"),
r.FormValue("password"),
r.FormValue("totp_code"),
)
if err != nil {
ws.renderTemplate(w, "login.html", map[string]interface{}{"Error": "Invalid credentials"})
return
}
http.SetCookie(w, &http.Cookie{
Name: "metacrypt_token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
http.Redirect(w, r, "/dashboard", http.StatusFound)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (ws *WebServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context())
token := extractCookie(r)
mounts, _ := ws.vault.ListMounts(r.Context(), token)
state, _ := ws.vault.Status(r.Context())
ws.renderTemplate(w, "dashboard.html", map[string]interface{}{
"Username": info.Username,
"IsAdmin": info.IsAdmin,
"Roles": info.Roles,
"Mounts": mounts,
"State": state,
})
}
func (ws *WebServer) handleDashboardMountCA(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context())
if !info.IsAdmin {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
if err := r.ParseMultipartForm(1 << 20); err != nil {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
}
mountName := r.FormValue("name")
if mountName == "" {
ws.renderDashboardWithError(w, r, info, "Mount name is required")
return
}
cfg := map[string]interface{}{}
if org := r.FormValue("organization"); org != "" {
cfg["organization"] = org
}
var certPEM, keyPEM string
if f, _, err := r.FormFile("cert_file"); err == nil {
defer func() { _ = f.Close() }()
data, _ := io.ReadAll(io.LimitReader(f, 1<<20))
certPEM = string(data)
}
if f, _, err := r.FormFile("key_file"); err == nil {
defer func() { _ = f.Close() }()
data, _ := io.ReadAll(io.LimitReader(f, 1<<20))
keyPEM = string(data)
}
if certPEM != "" && keyPEM != "" {
cfg["root_cert_pem"] = certPEM
cfg["root_key_pem"] = keyPEM
}
token := extractCookie(r)
if err := ws.vault.Mount(r.Context(), token, mountName, "ca", cfg); err != nil {
ws.renderDashboardWithError(w, r, info, grpcMessage(err))
return
}
http.Redirect(w, r, "/pki", http.StatusFound)
}
func (ws *WebServer) renderDashboardWithError(w http.ResponseWriter, r *http.Request, info *TokenInfo, errMsg string) {
token := extractCookie(r)
mounts, _ := ws.vault.ListMounts(r.Context(), token)
state, _ := ws.vault.Status(r.Context())
ws.renderTemplate(w, "dashboard.html", map[string]interface{}{
"Username": info.Username,
"IsAdmin": info.IsAdmin,
"Roles": info.Roles,
"Mounts": mounts,
"State": state,
"MountError": errMsg,
})
}
func (ws *WebServer) handlePKI(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context())
token := extractCookie(r)
mountName, err := ws.findCAMount(r, token)
if err != nil {
http.Redirect(w, r, "/dashboard", http.StatusFound)
return
}
data := map[string]interface{}{
"Username": info.Username,
"IsAdmin": info.IsAdmin,
"MountName": mountName,
}
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
if cert, err := parsePEMCert(rootPEM); err == nil {
data["RootCN"] = cert.Subject.CommonName
data["RootOrg"] = strings.Join(cert.Subject.Organization, ", ")
data["RootNotBefore"] = cert.NotBefore.Format(time.RFC3339)
data["RootNotAfter"] = cert.NotAfter.Format(time.RFC3339)
data["RootExpired"] = time.Now().After(cert.NotAfter)
data["HasRoot"] = true
}
}
if issuers, err := ws.vault.ListIssuers(r.Context(), token, mountName); err == nil {
data["Issuers"] = issuers
}
ws.renderTemplate(w, "pki.html", data)
}
func (ws *WebServer) handleImportRoot(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
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
if err := r.ParseMultipartForm(1 << 20); err != nil {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
}
certPEM := r.FormValue("cert_pem")
keyPEM := r.FormValue("key_pem")
if certPEM == "" {
if f, _, err := r.FormFile("cert_file"); err == nil {
defer func() { _ = f.Close() }()
data, _ := io.ReadAll(io.LimitReader(f, 1<<20))
certPEM = string(data)
}
}
if keyPEM == "" {
if f, _, err := r.FormFile("key_file"); err == nil {
defer func() { _ = f.Close() }()
data, _ := io.ReadAll(io.LimitReader(f, 1<<20))
keyPEM = string(data)
}
}
if certPEM == "" || keyPEM == "" {
ws.renderPKIWithError(w, r, mountName, info, "Certificate and private key are required")
return
}
if err = ws.vault.ImportRoot(r.Context(), token, mountName, certPEM, keyPEM); err != nil {
ws.renderPKIWithError(w, r, mountName, info, grpcMessage(err))
return
}
http.Redirect(w, r, "/pki", http.StatusFound)
}
func (ws *WebServer) handleCreateIssuer(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
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
name := r.FormValue("name")
if name == "" {
ws.renderPKIWithError(w, r, mountName, info, "Issuer name is required")
return
}
issuerReq := CreateIssuerRequest{
Mount: mountName,
Name: name,
}
if v := r.FormValue("expiry"); v != "" {
issuerReq.Expiry = v
}
if v := r.FormValue("max_ttl"); v != "" {
issuerReq.MaxTTL = v
}
if v := r.FormValue("key_algorithm"); v != "" {
issuerReq.KeyAlgorithm = v
}
if v := r.FormValue("key_size"); v != "" {
var size int32
if _, err := fmt.Sscanf(v, "%d", &size); err == nil {
issuerReq.KeySize = size
}
}
if err = ws.vault.CreateIssuer(r.Context(), token, issuerReq); err != nil {
ws.renderPKIWithError(w, r, mountName, info, grpcMessage(err))
return
}
http.Redirect(w, r, "/pki", http.StatusFound)
}
func (ws *WebServer) handlePKIIssuer(w http.ResponseWriter, r *http.Request) {
token := extractCookie(r)
mountName, err := ws.findCAMount(r, token)
if err != nil {
http.Error(w, "no CA engine mounted", http.StatusNotFound)
return
}
issuerName := chi.URLParam(r, "issuer")
certPEM, err := ws.vault.GetIssuerCert(r.Context(), mountName, issuerName)
if err != nil {
http.Error(w, "issuer not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.pem", issuerName))
_, _ = w.Write(certPEM) //nolint:gosec
}
func (ws *WebServer) handleIssuerDetail(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context())
token := extractCookie(r)
mountName, err := ws.findCAMount(r, token)
if err != nil {
http.Error(w, "no CA engine mounted", http.StatusNotFound)
return
}
issuerName := chi.URLParam(r, "issuer")
allCerts, err := ws.vault.ListCerts(r.Context(), token, mountName)
if err != nil {
http.Error(w, "failed to list certificates", http.StatusInternalServerError)
return
}
nameFilter := strings.ToLower(r.URL.Query().Get("name"))
sortBy := r.URL.Query().Get("sort")
if sortBy == "" {
sortBy = "cn"
}
var certs []CertSummary
for _, cs := range allCerts {
if cs.Issuer != issuerName {
continue
}
if nameFilter != "" && !strings.Contains(strings.ToLower(cs.CommonName), strings.ToLower(nameFilter)) {
continue
}
certs = append(certs, cs)
}
// Sort: by expiry date or by common name (default).
if sortBy == "expiry" {
for i := 1; i < len(certs); i++ {
for j := i; j > 0; j-- {
if certs[j-1].ExpiresAt > certs[j].ExpiresAt {
certs[j-1], certs[j] = certs[j], certs[j-1]
}
}
}
} else {
for i := 1; i < len(certs); i++ {
for j := i; j > 0; j-- {
if strings.ToLower(certs[j-1].CommonName) > strings.ToLower(certs[j].CommonName) {
certs[j-1], certs[j] = certs[j], certs[j-1]
}
}
}
}
data := map[string]interface{}{
"Username": info.Username,
"IsAdmin": info.IsAdmin,
"MountName": mountName,
"IssuerName": issuerName,
"Certs": certs,
"NameFilter": r.URL.Query().Get("name"),
"SortBy": sortBy,
}
ws.renderTemplate(w, "issuer_detail.html", data)
}
func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
// Disable the server-wide write deadline for this handler: it streams a
// tgz response only after several serial gRPC calls, which can easily
// consume the 30 s WriteTimeout before we start writing. We set our own
// 60 s deadline just before the write phase below.
_ = http.NewResponseController(w).SetWriteDeadline(time.Time{})
info := tokenInfoFromContext(r.Context())
token := extractCookie(r)
mountName, err := ws.findCAMount(r, token)
if err != nil {
http.Error(w, "no CA engine mounted", http.StatusNotFound)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
commonName := r.FormValue("common_name")
if commonName == "" {
ws.renderPKIWithError(w, r, mountName, info, "Common name is required")
return
}
issuer := r.FormValue("issuer")
if issuer == "" {
ws.renderPKIWithError(w, r, mountName, info, "Issuer is required")
return
}
certReq := IssueCertRequest{
Mount: mountName,
Issuer: issuer,
CommonName: commonName,
}
if v := r.FormValue("profile"); v != "" {
certReq.Profile = v
}
if v := r.FormValue("ttl"); v != "" {
certReq.TTL = v
}
if lines := splitLines(r.FormValue("dns_names")); len(lines) > 0 {
for _, l := range lines {
certReq.DNSNames = append(certReq.DNSNames, l.(string))
}
}
if lines := splitLines(r.FormValue("ip_addresses")); len(lines) > 0 {
for _, l := range lines {
certReq.IPAddresses = append(certReq.IPAddresses, l.(string))
}
}
certReq.KeyUsages = r.Form["key_usages"]
certReq.ExtKeyUsages = r.Form["ext_key_usages"]
issuedCert, err := ws.vault.IssueCert(r.Context(), token, certReq)
if err != nil {
ws.renderPKIWithError(w, r, mountName, info, grpcMessage(err))
return
}
// 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+"\"")
gw := gzip.NewWriter(w)
tw := tar.NewWriter(gw)
writeTarFile := func(name string, data []byte) error {
hdr := &tar.Header{
Name: name,
Mode: 0600,
Size: int64(len(data)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
_, err := tw.Write(data)
return err
}
if err := writeTarFile("key.pem", []byte(issuedCert.KeyPEM)); err != nil {
ws.logger.Error("write key to tgz", "error", err)
return
}
if err := writeTarFile("cert.pem", []byte(issuedCert.CertPEM)); err != nil {
ws.logger.Error("write cert to tgz", "error", err)
return
}
_ = tw.Close()
_ = gw.Close()
}
func (ws *WebServer) handleCertDetail(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context())
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")
cert, err := ws.vault.GetCert(r.Context(), token, mountName, serial)
if 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
}
ws.renderTemplate(w, "cert_detail.html", map[string]interface{}{
"Username": info.Username,
"IsAdmin": info.IsAdmin,
"MountName": mountName,
"Cert": cert,
})
}
func (ws *WebServer) handleCertDownload(w http.ResponseWriter, r *http.Request) {
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")
cert, err := ws.vault.GetCert(r.Context(), token, mountName, serial)
if 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
}
w.Header().Set("Content-Type", "application/x-pem-file")
w.Header().Set("Content-Disposition", "attachment; filename=\""+serial+".pem\"")
_, _ = 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)
mountName, err := ws.findCAMount(r, token)
if err != nil {
http.Error(w, "no CA engine mounted", http.StatusNotFound)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
_ = r.ParseForm()
issuer := r.FormValue("issuer")
if issuer == "" {
ws.renderPKIWithError(w, r, mountName, info, "Issuer is required")
return
}
csrPEM := r.FormValue("csr_pem")
if csrPEM == "" {
ws.renderPKIWithError(w, r, mountName, info, "CSR PEM is required")
return
}
req := SignCSRRequest{
Mount: mountName,
Issuer: issuer,
CSRPEM: csrPEM,
Profile: r.FormValue("profile"),
TTL: r.FormValue("ttl"),
}
signed, err := ws.vault.SignCSR(r.Context(), token, req)
if err != nil {
ws.renderPKIWithError(w, r, mountName, info, grpcMessage(err))
return
}
data := map[string]interface{}{
"Username": info.Username,
"IsAdmin": info.IsAdmin,
"MountName": mountName,
"SignedCert": signed,
}
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
if cert, err := parsePEMCert(rootPEM); err == nil {
data["RootCN"] = cert.Subject.CommonName
data["RootOrg"] = strings.Join(cert.Subject.Organization, ", ")
data["RootNotBefore"] = cert.NotBefore.Format(time.RFC3339)
data["RootNotAfter"] = cert.NotAfter.Format(time.RFC3339)
data["RootExpired"] = time.Now().After(cert.NotAfter)
data["HasRoot"] = true
}
}
if issuers, err := ws.vault.ListIssuers(r.Context(), token, mountName); err == nil {
data["Issuers"] = issuers
}
ws.renderTemplate(w, "pki.html", data)
}
func (ws *WebServer) renderPKIWithError(w http.ResponseWriter, r *http.Request, mountName string, info *TokenInfo, errMsg string) {
token := extractCookie(r)
data := map[string]interface{}{
"Username": info.Username,
"IsAdmin": info.IsAdmin,
"MountName": mountName,
"Error": errMsg,
}
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
if cert, err := parsePEMCert(rootPEM); err == nil {
data["RootCN"] = cert.Subject.CommonName
data["RootOrg"] = strings.Join(cert.Subject.Organization, ", ")
data["RootNotBefore"] = cert.NotBefore.Format(time.RFC3339)
data["RootNotAfter"] = cert.NotAfter.Format(time.RFC3339)
data["RootExpired"] = time.Now().After(cert.NotAfter)
data["HasRoot"] = true
}
}
if issuers, err := ws.vault.ListIssuers(r.Context(), token, mountName); err == nil {
data["Issuers"] = issuers
}
ws.renderTemplate(w, "pki.html", data)
}
func (ws *WebServer) findCAMount(r *http.Request, token string) (string, error) {
mounts, err := ws.vault.ListMounts(r.Context(), token)
if err != nil {
return "", err
}
for _, m := range mounts {
if m.Type == "ca" {
return m.Name, nil
}
}
return "", fmt.Errorf("no CA engine mounted")
}
// grpcMessage extracts a human-readable message from a gRPC error.
func grpcMessage(err error) string {
if st, ok := status.FromError(err); ok {
return st.Message()
}
return err.Error()
}