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
}