@marcel91200 / caddy Mirror · read-only / commits / 78e381b2
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 13, 21:45 UTC

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

Matthew Holt committed Mar 2, 2022 at 13:00 UTC 78e381b29f64c07a0572a172ff5f89876e8d84db
2 files changed +132 -70
modules/caddypki/adminpki.go
+125 -64
@@ -16,7 +16,6 @@ package caddypki
16
17 import (
18 "encoding/json"
19 - "encoding/pem"
19 "fmt"
20 "net/http"
21 "strings"
@@ -26,27 +25,27 @@ import (
25 )
26
27 func init() {
29 - caddy.RegisterModule(adminPKI{})
28 + caddy.RegisterModule(adminAPI{})
29 }
30
32 -// adminPKI is a module that serves a PKI endpoint to retrieve
31 +// adminAPI is a module that serves PKI endpoints to retrieve
32 // information about the CAs being managed by Caddy.
34 -type adminPKI struct {
33 +type adminAPI struct {
34 ctx caddy.Context
35 log *zap.Logger
36 pkiApp *PKI
37 }
38
39 // CaddyModule returns the Caddy module information.
41 -func (adminPKI) CaddyModule() caddy.ModuleInfo {
40 +func (adminAPI) CaddyModule() caddy.ModuleInfo {
41 return caddy.ModuleInfo{
42 ID: "admin.api.pki",
44 - New: func() caddy.Module { return new(adminPKI) },
43 + New: func() caddy.Module { return new(adminAPI) },
44 }
45 }
46
48 -// Provision sets up the adminPKI module.
49 -func (a *adminPKI) Provision(ctx caddy.Context) error {
47 +// Provision sets up the adminAPI module.
48 +func (a *adminAPI) Provision(ctx caddy.Context) error {
49 a.ctx = ctx
50 a.log = ctx.Logger(a)
51
@@ -69,52 +68,128 @@ func (a *adminPKI) Provision(ctx caddy.Context) error {
68 }
69
70 // Routes returns the admin routes for the PKI app.
72 -func (a *adminPKI) Routes() []caddy.AdminRoute {
71 +func (a *adminAPI) Routes() []caddy.AdminRoute {
72 return []caddy.AdminRoute{
73 {
75 - Pattern: adminPKICertificatesEndpoint,
76 - Handler: caddy.AdminHandlerFunc(a.handleCertificates),
74 + Pattern: adminPKIEndpointBase,
75 + Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints),
76 },
77 }
78 }
79
81 -// handleCertificates returns certificate information about a particular
82 -// CA, by its ID. If the CA ID is the default, then the CA will be
80 +// handleAPIEndpoints routes API requests within adminPKIEndpointBase.
81 +func (a *adminAPI) handleAPIEndpoints(w http.ResponseWriter, r *http.Request) error {
82 + uri := strings.TrimPrefix(r.URL.Path, "/pki/")
83 + parts := strings.Split(uri, "/")
84 + switch {
85 + case len(parts) == 2 && parts[0] == "ca" && parts[1] != "":
86 + return a.handleCAInfo(w, r)
87 + case len(parts) == 3 && parts[0] == "ca" && parts[1] != "" && parts[2] == "certificates":
88 + return a.handleCACerts(w, r)
89 + }
90 + return caddy.APIError{
91 + HTTPStatus: http.StatusNotFound,
92 + Err: fmt.Errorf("resource not found: %v", r.URL.Path),
93 + }
94 +}
95 +
96 +// handleCAInfo returns cinformation about a particular
97 +// CA by its ID. If the CA ID is the default, then the CA will be
98 // provisioned if it has not already been. Other CA IDs will return an
99 // error if they have not been previously provisioned.
85 -func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) error {
100 +func (a *adminAPI) handleCAInfo(w http.ResponseWriter, r *http.Request) error {
101 if r.Method != http.MethodGet {
102 return caddy.APIError{
103 HTTPStatus: http.StatusMethodNotAllowed,
89 - Err: fmt.Errorf("method not allowed"),
104 + Err: fmt.Errorf("method not allowed: %v", r.Method),
105 + }
106 + }
107 +
108 + ca, err := a.getCAFromAPIRequestPath(r)
109 + if err != nil {
110 + return err
111 + }
112 +
113 + rootCert, interCert, err := rootAndIntermediatePEM(ca)
114 + if err != nil {
115 + return caddy.APIError{
116 + HTTPStatus: http.StatusInternalServerError,
117 + Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
118 + }
119 + }
120 +
121 + repl := ca.newReplacer()
122 +
123 + response := caInfo{
124 + ID: ca.ID,
125 + Name: ca.Name,
126 + RootCN: repl.ReplaceAll(ca.RootCommonName, ""),
127 + IntermediateCN: repl.ReplaceAll(ca.IntermediateCommonName, ""),
128 + RootCert: string(rootCert),
129 + IntermediateCert: string(interCert),
130 + }
131 +
132 + encoded, err := json.Marshal(response)
133 + if err != nil {
134 + return caddy.APIError{
135 + HTTPStatus: http.StatusInternalServerError,
136 + Err: err,
137 }
138 }
139
93 - // Prep for a JSON response
140 w.Header().Set("Content-Type", "application/json")
95 - enc := json.NewEncoder(w)
141 + w.Write(encoded)
142
97 - idPath := r.URL.Path
143 + return nil
144 +}
145
99 - // Grab the CA ID from the request path, it should be the 4th segment
100 - parts := strings.Split(idPath, "/")
101 - if len(parts) < 4 || parts[3] == "" {
146 +// handleCACerts returns cinformation about a particular
147 +// CA by its ID. If the CA ID is the default, then the CA will be
148 +// provisioned if it has not already been. Other CA IDs will return an
149 +// error if they have not been previously provisioned.
150 +func (a *adminAPI) handleCACerts(w http.ResponseWriter, r *http.Request) error {
151 + if r.Method != http.MethodGet {
152 return caddy.APIError{
103 - HTTPStatus: http.StatusBadRequest,
104 - Err: fmt.Errorf("request path is missing the CA ID"),
153 + HTTPStatus: http.StatusMethodNotAllowed,
154 + Err: fmt.Errorf("method not allowed: %v", r.Method),
155 }
156 }
107 - if parts[0] != "" || parts[1] != "pki" || parts[2] != "certificates" {
157 +
158 + ca, err := a.getCAFromAPIRequestPath(r)
159 + if err != nil {
160 + return err
161 + }
162 +
163 + rootCert, interCert, err := rootAndIntermediatePEM(ca)
164 + if err != nil {
165 return caddy.APIError{
166 + HTTPStatus: http.StatusInternalServerError,
167 + Err: fmt.Errorf("failed to get root and intermediate cert for CA %s: %v", ca.ID, err),
168 + }
169 + }
170 +
171 + w.Header().Set("Content-Type", "application/pem-certificate-chain")
172 + _, err = w.Write(interCert)
173 + if err == nil {
174 + w.Write(rootCert)
175 + }
176 +
177 + return nil
178 +}
179 +
180 +func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) {
181 + // Grab the CA ID from the request path, it should be the 4th segment (/pki/ca/<ca>)
182 + id := strings.Split(r.URL.Path, "/")[3]
183 + if id == "" {
184 + return nil, caddy.APIError{
185 HTTPStatus: http.StatusBadRequest,
110 - Err: fmt.Errorf("malformed object path"),
186 + Err: fmt.Errorf("missing CA in path"),
187 }
188 }
113 - id := parts[3]
189
190 // Find the CA by ID, if PKI is configured
191 var ca *CA
117 - ok := false
192 + var ok bool
193 if a.pkiApp != nil {
194 ca, ok = a.pkiApp.CAs[id]
195 }
@@ -127,7 +202,7 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er
202 // if they actually requested the local CA ID.
203 if !ok {
204 if id != DefaultCAID {
130 - return caddy.APIError{
205 + return nil, caddy.APIError{
206 HTTPStatus: http.StatusNotFound,
207 Err: fmt.Errorf("no certificate authority configured with id: %s", id),
208 }
@@ -138,57 +213,43 @@ func (a *adminPKI) handleCertificates(w http.ResponseWriter, r *http.Request) er
213 ca = new(CA)
214 err := ca.Provision(a.ctx, id, a.log)
215 if err != nil {
141 - return caddy.APIError{
216 + return nil, caddy.APIError{
217 HTTPStatus: http.StatusInternalServerError,
218 Err: fmt.Errorf("failed to provision CA %s, %w", id, err),
219 }
220 }
221 }
222
148 - // Convert the root certificate to PEM
149 - rootPem := string(pem.EncodeToMemory(&pem.Block{
150 - Type: "CERTIFICATE",
151 - Bytes: ca.RootCertificate().Raw,
152 - }))
153 -
154 - // Convert the intermediate certificate to PEM
155 - interPem := string(pem.EncodeToMemory(&pem.Block{
156 - Type: "CERTIFICATE",
157 - Bytes: ca.IntermediateCertificate().Raw,
158 - }))
223 + return ca, nil
224 +}
225
160 - // Build the response
161 - response := CAInfo{
162 - ID: ca.ID,
163 - Name: ca.Name,
164 - Root: rootPem,
165 - Intermediate: interPem,
226 +func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) {
227 + root, err = pemEncodeCert(ca.RootCertificate().Raw)
228 + if err != nil {
229 + return
230 }
167 -
168 - // Encode and write the JSON response
169 - err := enc.Encode(response)
231 + inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw)
232 if err != nil {
171 - return caddy.APIError{
172 - HTTPStatus: http.StatusInternalServerError,
173 - Err: err,
174 - }
233 + return
234 }
176 -
177 - return nil
235 + return
236 }
237
180 -// CAInfo is the response from the certificates API endpoint
181 -type CAInfo struct {
182 - ID string `json:"id"`
183 - Name string `json:"name"`
184 - Root string `json:"root"`
185 - Intermediate string `json:"intermediate"`
238 +// caInfo is the response structure for the CA info API endpoint.
239 +type caInfo struct {
240 + ID string `json:"id"`
241 + Name string `json:"name"`
242 + RootCN string `json:"root_common_name"`
243 + IntermediateCN string `json:"intermediate_common_name"`
244 + RootCert string `json:"root_certificate"`
245 + IntermediateCert string `json:"intermediate_certificate"`
246 }
247
188 -const adminPKICertificatesEndpoint = "/pki/certificates/"
248 +// adminPKIEndpointBase is the base admin endpoint under which all PKI admin endpoints exist.
249 +const adminPKIEndpointBase = "/pki/"
250
251 // Interface guards
252 var (
192 - _ caddy.AdminRouter = (*adminPKI)(nil)
193 - _ caddy.Provisioner = (*adminPKI)(nil)
253 + _ caddy.AdminRouter = (*adminAPI)(nil)
254 + _ caddy.Provisioner = (*adminAPI)(nil)
255 )
modules/caddypki/command.go
+7 -6
@@ -22,6 +22,7 @@ import (
22 "fmt"
23 "net/http"
24 "os"
25 + "path"
26
27 "github.com/caddyserver/caddy/v2"
28 caddycmd "github.com/caddyserver/caddy/v2/cmd"
@@ -132,7 +133,7 @@ func cmdTrust(fl caddycmd.Flags) (int, error) {
133 ca := CA{
134 log: caddy.Log(),
135 root: rootCert,
135 - rootCertPath: adminAddr + adminPKICertificatesEndpoint + caID,
136 + rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, caID, "certificates"),
137 }
138
139 // Install the cert!
@@ -204,9 +205,9 @@ func cmdUntrust(fl caddycmd.Flags) (int, error) {
205 return caddy.ExitCodeSuccess, nil
206 }
207
207 -// rootCertFromAdmin makes the API request to fetch the
208 +// rootCertFromAdmin makes the API request to fetch the root certificate for the named CA via admin API.
209 func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) {
209 - uri := adminPKICertificatesEndpoint + caID
210 + uri := path.Join(adminPKIEndpointBase, caID, "certificates")
211
212 // Make the request to fetch the CA info
213 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)
217 defer resp.Body.Close()
218
219 // Decode the resposne
219 - caInfo := new(CAInfo)
220 + caInfo := new(caInfo)
221 err = json.NewDecoder(resp.Body).Decode(caInfo)
222 if err != nil {
223 return nil, fmt.Errorf("failed to decode JSON response: %v", err)
224 }
225
225 - // Decode the root
226 - rootBlock, _ := pem.Decode([]byte(caInfo.Root))
226 + // Decode the root cert
227 + rootBlock, _ := pem.Decode([]byte(caInfo.RootCert))
228 if rootBlock == nil {
229 return nil, fmt.Errorf("failed to decode root certificate: %v", err)
230 }