From 78e381b29f64c07a0572a172ff5f89876e8d84db Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 2 Mar 2022 13:00:37 -0700 Subject: [PATCH] caddypki: Refactor /pki/ admin endpoints Remove /pki/certificates/ endpoint and split into two endpoints: - GET /pki/ca/ to get CA info and certs in JSON format - GET /pki/ca//certificates to get cert in PEM chain --- modules/caddypki/adminpki.go | 193 +++++++++++++++++++++++------------ modules/caddypki/command.go | 13 +-- 2 files changed, 134 insertions(+), 72 deletions(-) diff --git a/modules/caddypki/adminpki.go b/modules/caddypki/adminpki.go index 5933bcd99..a2939d1b5 100644 --- a/modules/caddypki/adminpki.go +++ b/modules/caddypki/adminpki.go @@ -16,7 +16,6 @@ package caddypki import ( "encoding/json" - "encoding/pem" "fmt" "net/http" "strings" @@ -26,27 +25,27 @@ import ( ) func init() { - caddy.RegisterModule(adminPKI{}) + caddy.RegisterModule(adminAPI{}) } -// adminPKI is a module that serves a PKI endpoint to retrieve +// adminAPI is a module that serves PKI endpoints to retrieve // information about the CAs being managed by Caddy. -type adminPKI struct { +type adminAPI struct { ctx caddy.Context log *zap.Logger pkiApp *PKI } // CaddyModule returns the Caddy module information. -func (adminPKI) CaddyModule() caddy.ModuleInfo { +func (adminAPI) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "admin.api.pki", - New: func() caddy.Module { return new(adminPKI) }, + New: func() caddy.Module { return new(adminAPI) }, } } -// Provision sets up the adminPKI module. -func (a *adminPKI) Provision(ctx caddy.Context) error { +// Provision sets up the adminAPI module. +func (a *adminAPI) Provision(ctx caddy.Context) error { a.ctx = ctx a.log = ctx.Logger(a) @@ -69,52 +68,128 @@ func (a *adminPKI) Provision(ctx caddy.Context) error { } // Routes returns the admin routes for the PKI app. -func (a *adminPKI) Routes() []caddy.AdminRoute { +func (a *adminAPI) Routes() []caddy.AdminRoute { return []caddy.AdminRoute{ { - Pattern: adminPKICertificatesEndpoint, - Handler: caddy.AdminHandlerFunc(a.handleCertificates), + Pattern: adminPKIEndpointBase, + Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints), }, } } -// handleCertificates returns certificate information about a particular -// CA, by its ID. If the CA ID is the default, then the CA will be +// handleAPIEndpoints routes API requests within adminPKIEndpointBase. +func (a *adminAPI) handleAPIEndpoints(w http.ResponseWriter, r *http.Request) error { + uri := strings.TrimPrefix(r.URL.Path, "/pki/") + parts := strings.Split(uri, "/") + switch { + case len(parts) == 2 && parts[0] == "ca" && parts[1] != "": + return a.handleCAInfo(w, r) + case len(parts) == 3 && parts[0] == "ca" && parts[1] != "" && parts[2] == "certificates": + return a.handleCACerts(w, r) + } + return caddy.APIError{ + HTTPStatus: http.StatusNotFound, + Err: fmt.Errorf("resource not found: %v", r.URL.Path), + } +} + +// handleCAInfo returns cinformation about a particular +// CA by its ID. If the CA ID is the default, then the CA will be // provisioned if it has not already been. Other CA IDs will return an // error if they have not been previously provisioned. -func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) error { +func (a *adminAPI) handleCAInfo(w http.ResponseWriter, r *http.Request) error { if r.Method != http.MethodGet { return caddy.APIError{ HTTPStatus: http.StatusMethodNotAllowed, - Err: fmt.Errorf("method not allowed"), + Err: fmt.Errorf("method not allowed: %v", r.Method), + } + } + + ca, err := a.getCAFromAPIRequestPath(r) + if err != nil { + return err + } + + rootCert, interCert, err := rootAndIntermediatePEM(ca) + if err != nil { + return caddy.APIError{ + HTTPStatus: http.StatusInternalServerError, + Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err), + } + } + + repl := ca.newReplacer() + + response := caInfo{ + ID: ca.ID, + Name: ca.Name, + RootCN: repl.ReplaceAll(ca.RootCommonName, ""), + IntermediateCN: repl.ReplaceAll(ca.IntermediateCommonName, ""), + RootCert: string(rootCert), + IntermediateCert: string(interCert), + } + + encoded, err := json.Marshal(response) + if err != nil { + return caddy.APIError{ + HTTPStatus: http.StatusInternalServerError, + Err: err, } } - // Prep for a JSON response w.Header().Set("Content-Type", "application/json") - enc := json.NewEncoder(w) + w.Write(encoded) - idPath := r.URL.Path + return nil +} - // Grab the CA ID from the request path, it should be the 4th segment - parts := strings.Split(idPath, "/") - if len(parts) < 4 || parts[3] == "" { +// handleCACerts returns cinformation about a particular +// CA by its ID. If the CA ID is the default, then the CA will be +// provisioned if it has not already been. Other CA IDs will return an +// error if they have not been previously provisioned. +func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error { + if r.Method != http.MethodGet { return caddy.APIError{ - HTTPStatus: http.StatusBadRequest, - Err: fmt.Errorf("request path is missing the CA ID"), + HTTPStatus: http.StatusMethodNotAllowed, + Err: fmt.Errorf("method not allowed: %v", r.Method), } } - if parts[0] != "" || parts[1] != "pki" || parts[2] != "certificates" { + + ca, err := a.getCAFromAPIRequestPath(r) + if err != nil { + return err + } + + rootCert, interCert, err := rootAndIntermediatePEM(ca) + if err != nil { return caddy.APIError{ - HTTPStatus: http.StatusBadRequest, - Err: fmt.Errorf("malformed object path"), + HTTPStatus: http.StatusInternalServerError, + Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err), + } + } + + w.Header().Set("Content-Type", "application/pem-certificate-chain") + _, err = w.Write(interCert) + if err == nil { + w.Write(rootCert) + } + + return nil +} + +func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) { + // Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/) + id := strings.Split(r.URL.Path, "/")[3] + if id == "" { + return nil, caddy.APIError{ + HTTPStatus: http.StatusBadRequest, + Err: fmt.Errorf("missing CA in path"), } } - id := parts[3] // Find the CA by ID, if PKI is configured var ca *CA - ok := false + var ok bool if a.pkiApp != nil { ca, ok = a.pkiApp.CAs[id] } @@ -127,7 +202,7 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er // if they actually requested the local CA ID. if !ok { if id != DefaultCAID { - return caddy.APIError{ + return nil, caddy.APIError{ HTTPStatus: http.StatusNotFound, Err: fmt.Errorf("no certificate authority configured with id: %s", id), } @@ -138,57 +213,43 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er ca = new(CA) err := ca.Provision(a.ctx, id, a.log) if err != nil { - return caddy.APIError{ + return nil, caddy.APIError{ HTTPStatus: http.StatusInternalServerError, Err: fmt.Errorf("failed to provision CA %s, %w", id, err), } } } - // Convert the root certificate to PEM - rootPem := string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: ca.RootCertificate().Raw, - })) + return ca, nil +} - // Convert the intermediate certificate to PEM - interPem := string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: ca.IntermediateCertificate().Raw, - })) - - // Build the response - response := CAInfo{ - ID: ca.ID, - Name: ca.Name, - Root: rootPem, - Intermediate: interPem, - } - - // Encode and write the JSON response - err := enc.Encode(response) +func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) { + root, err = pemEncodeCert(ca.RootCertificate().Raw) if err != nil { - return caddy.APIError{ - HTTPStatus: http.StatusInternalServerError, - Err: err, - } + return } - - return nil + inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw) + if err != nil { + return + } + return } -// CAInfo is the response from the certificates API endpoint -type CAInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Root string `json:"root"` - Intermediate string `json:"intermediate"` +// caInfo is the response structure for the CA info API endpoint. +type caInfo struct { + ID string `json:"id"` + Name string `json:"name"` + RootCN string `json:"root_common_name"` + IntermediateCN string `json:"intermediate_common_name"` + RootCert string `json:"root_certificate"` + IntermediateCert string `json:"intermediate_certificate"` } -const adminPKICertificatesEndpoint = "/pki/certificates/" +// adminPKIEndpointBase is the base admin endpoint under which all PKI admin endpoints exist. +const adminPKIEndpointBase = "/pki/" // Interface guards var ( - _ caddy.AdminRouter = (*adminPKI)(nil) - _ caddy.Provisioner = (*adminPKI)(nil) + _ caddy.AdminRouter = (*adminAPI)(nil) + _ caddy.Provisioner = (*adminAPI)(nil) ) diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go index fa37ab04f..4744c68e6 100644 --- a/modules/caddypki/command.go +++ b/modules/caddypki/command.go @@ -22,6 +22,7 @@ import ( "fmt" "net/http" "os" + "path" "github.com/caddyserver/caddy/v2" caddycmd "github.com/caddyserver/caddy/v2/cmd" @@ -132,7 +133,7 @@ func cmdTrust(fl caddycmd.Flags) (int, error) { ca := CA{ log: caddy.Log(), root: rootCert, - rootCertPath: adminAddr + adminPKICertificatesEndpoint + caID, + rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, caID, "certificates"), } // Install the cert! @@ -204,9 +205,9 @@ func cmdUntrust(fl caddycmd.Flags) (int, error) { return caddy.ExitCodeSuccess, nil } -// rootCertFromAdmin makes the API request to fetch the +// rootCertFromAdmin makes the API request to fetch the root certificate for the named CA via admin API. func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) { - uri := adminPKICertificatesEndpoint + caID + uri := path.Join(adminPKIEndpointBase, caID, "certificates") // Make the request to fetch the CA info resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil) @@ -216,14 +217,14 @@ func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) defer resp.Body.Close() // Decode the resposne - caInfo := new(CAInfo) + caInfo := new(caInfo) err = json.NewDecoder(resp.Body).Decode(caInfo) if err != nil { return nil, fmt.Errorf("failed to decode JSON response: %v", err) } - // Decode the root - rootBlock, _ := pem.Decode([]byte(caInfo.Root)) + // Decode the root cert + rootBlock, _ := pem.Decode([]byte(caInfo.RootCert)) if rootBlock == nil { return nil, fmt.Errorf("failed to decode root certificate: %v", err) }