caddypki: Refactor /pki/ admin endpoints

Remove /pki/certificates/<ca> endpoint and split into two endpoints:

- GET /pki/ca/<id> to get CA info and certs in JSON format
- GET /pki/ca/<id>/certificates to get cert in PEM chain
This commit is contained in:
Matthew Holt 2022-03-02 13:00:37 -07:00
parent de490c7cad
commit 78e381b29f
No known key found for this signature in database
GPG Key ID: 2A349DD577D586A5
2 changed files with 134 additions and 72 deletions

View File

@ -16,7 +16,6 @@ package caddypki
import ( import (
"encoding/json" "encoding/json"
"encoding/pem"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@ -26,27 +25,27 @@ import (
) )
func init() { 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. // information about the CAs being managed by Caddy.
type adminPKI struct { type adminAPI struct {
ctx caddy.Context ctx caddy.Context
log *zap.Logger log *zap.Logger
pkiApp *PKI pkiApp *PKI
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (adminPKI) CaddyModule() caddy.ModuleInfo { func (adminAPI) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
ID: "admin.api.pki", ID: "admin.api.pki",
New: func() caddy.Module { return new(adminPKI) }, New: func() caddy.Module { return new(adminAPI) },
} }
} }
// Provision sets up the adminPKI module. // Provision sets up the adminAPI module.
func (a *adminPKI) Provision(ctx caddy.Context) error { func (a *adminAPI) Provision(ctx caddy.Context) error {
a.ctx = ctx a.ctx = ctx
a.log = ctx.Logger(a) 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. // Routes returns the admin routes for the PKI app.
func (a *adminPKI) Routes() []caddy.AdminRoute { func (a *adminAPI) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{ return []caddy.AdminRoute{
{ {
Pattern: adminPKICertificatesEndpoint, Pattern: adminPKIEndpointBase,
Handler: caddy.AdminHandlerFunc(a.handleCertificates), Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints),
}, },
} }
} }
// handleCertificates returns certificate information about a particular // handleAPIEndpoints routes API requests within adminPKIEndpointBase.
// CA, by its ID. If the CA ID is the default, then the CA will be 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 // provisioned if it has not already been. Other CA IDs will return an
// error if they have not been previously provisioned. // 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 { if r.Method != http.MethodGet {
return caddy.APIError{ return caddy.APIError{
HTTPStatus: http.StatusMethodNotAllowed, 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") 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 // handleCACerts returns cinformation about a particular
parts := strings.Split(idPath, "/") // CA by its ID. If the CA ID is the default, then the CA will be
if len(parts) < 4 || parts[3] == "" { // 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{ return caddy.APIError{
HTTPStatus: http.StatusBadRequest, HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("request path is missing the CA ID"), 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{ return caddy.APIError{
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/<ca>)
id := strings.Split(r.URL.Path, "/")[3]
if id == "" {
return nil, caddy.APIError{
HTTPStatus: http.StatusBadRequest, HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed object path"), Err: fmt.Errorf("missing CA in path"),
} }
} }
id := parts[3]
// Find the CA by ID, if PKI is configured // Find the CA by ID, if PKI is configured
var ca *CA var ca *CA
ok := false var ok bool
if a.pkiApp != nil { if a.pkiApp != nil {
ca, ok = a.pkiApp.CAs[id] 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 they actually requested the local CA ID.
if !ok { if !ok {
if id != DefaultCAID { if id != DefaultCAID {
return caddy.APIError{ return nil, caddy.APIError{
HTTPStatus: http.StatusNotFound, HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("no certificate authority configured with id: %s", id), 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) ca = new(CA)
err := ca.Provision(a.ctx, id, a.log) err := ca.Provision(a.ctx, id, a.log)
if err != nil { if err != nil {
return caddy.APIError{ return nil, caddy.APIError{
HTTPStatus: http.StatusInternalServerError, HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("failed to provision CA %s, %w", id, err), Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
} }
} }
} }
// Convert the root certificate to PEM return ca, nil
rootPem := string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ca.RootCertificate().Raw,
}))
// 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 func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
err := enc.Encode(response) root, err = pemEncodeCert(ca.RootCertificate().Raw)
if err != nil { if err != nil {
return caddy.APIError{ return
HTTPStatus: http.StatusInternalServerError,
Err: err,
} }
inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
if err != nil {
return
}
return
} }
return nil // caInfo is the response structure for the CA info API endpoint.
} type caInfo struct {
// CAInfo is the response from the certificates API endpoint
type CAInfo struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Root string `json:"root"` RootCN string `json:"root_common_name"`
Intermediate string `json:"intermediate"` 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 // Interface guards
var ( var (
_ caddy.AdminRouter = (*adminPKI)(nil) _ caddy.AdminRouter = (*adminAPI)(nil)
_ caddy.Provisioner = (*adminPKI)(nil) _ caddy.Provisioner = (*adminAPI)(nil)
) )

View File

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
@ -132,7 +133,7 @@ func cmdTrust(fl caddycmd.Flags) (int, error) {
ca := CA{ ca := CA{
log: caddy.Log(), log: caddy.Log(),
root: rootCert, root: rootCert,
rootCertPath: adminAddr + adminPKICertificatesEndpoint + caID, rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, caID, "certificates"),
} }
// Install the cert! // Install the cert!
@ -204,9 +205,9 @@ func cmdUntrust(fl caddycmd.Flags) (int, error) {
return caddy.ExitCodeSuccess, nil 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) { 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 // Make the request to fetch the CA info
resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil) 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() defer resp.Body.Close()
// Decode the resposne // Decode the resposne
caInfo := new(CAInfo) caInfo := new(caInfo)
err = json.NewDecoder(resp.Body).Decode(caInfo) err = json.NewDecoder(resp.Body).Decode(caInfo)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode JSON response: %v", err) return nil, fmt.Errorf("failed to decode JSON response: %v", err)
} }
// Decode the root // Decode the root cert
rootBlock, _ := pem.Decode([]byte(caInfo.Root)) rootBlock, _ := pem.Decode([]byte(caInfo.RootCert))
if rootBlock == nil { if rootBlock == nil {
return nil, fmt.Errorf("failed to decode root certificate: %v", err) return nil, fmt.Errorf("failed to decode root certificate: %v", err)
} }