15
package caddypki
16
17
import (
18
- "context"
18
+ "crypto/x509"
19
+ "encoding/json"
20
+ "encoding/pem"
21
"flag"
22
"fmt"
23
+ "net/http"
24
"os"
22
- "path/filepath"
25
26
"github.com/caddyserver/caddy/v2"
27
caddycmd "github.com/caddyserver/caddy/v2/cmd"
32
caddycmd.RegisterCommand(caddycmd.Command{
33
Name: "trust",
34
Func: cmdTrust,
35
+ Usage: "[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]",
36
Short: "Installs a CA certificate into local trust stores",
37
Long: `
35
-Adds a root certificate into the local trust stores. Intended for
36
-development environments only.
37
-
38
-Since Caddy will install its root certificates into the local trust
39
-stores automatically when they are first generated, this command is
40
-only necessary if you need to pre-install the certificates before
41
-using them; for example, if you have elevated privileges at one
42
-point but not later, you will want to use this command so that a
43
-password prompt is not required later.
44
-
45
-This command installs the root certificate only for Caddy's
46
-default CA.`,
38
+Adds a root certificate into the local trust stores.
39
+
40
+Caddy will attempt to install its root certificates into the local
41
+trust stores automatically when they are first generated, but it
42
+might fail if Caddy doesn't have the appropriate permissions to
43
+write to the trust store. This command is necessary to pre-install
44
+the certificates before using them, if the server process runs as an
45
+unprivileged user (such as via systemd).
46
+
47
+By default, this command installs the root certificate for Caddy's
48
+default CA (i.e. 'local'). You may specify the ID of another CA
49
+with the --ca flag.
50
+
51
+Also, this command will attempt to connect to the Caddy's admin API
52
+running at '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may
53
+explicitly specify the --address, or use the --config flag to load
54
+the admin address from your config, if not using the default.`,
55
+ Flags: func() *flag.FlagSet {
56
+ fs := flag.NewFlagSet("trust", flag.ExitOnError)
57
+ fs.String("ca", "", "The ID of the CA to trust (defaults to 'local')")
58
+ fs.String("address", "", "Address of the administration API listener (if --config is not used)")
59
+ fs.String("config", "", "Configuration file (if --address is not used)")
60
+ fs.String("adapter", "", "Name of config adapter to apply (if --config is used)")
61
+ return fs
62
+ }(),
63
})
64
65
caddycmd.RegisterCommand(caddycmd.Command{
66
Name: "untrust",
67
Func: cmdUntrust,
52
- Usage: "[--ca <id> | --cert <path>]",
68
+ Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]",
69
Short: "Untrusts a locally-trusted CA certificate",
70
Long: `
55
-Untrusts a root certificate from the local trust store(s). Intended
56
-for development environments only.
71
+Untrusts a root certificate from the local trust store(s).
72
73
This command uninstalls trust; it does not necessarily delete the
74
root certificate from trust stores entirely. Thus, repeatedly
75
trusting and untrusting new certificates can fill up trust databases.
76
62
-This command does not delete or modify certificate files.
77
+This command does not delete or modify certificate files from Caddy's
78
+configured storage.
79
64
-Specify which certificate to untrust either by the ID of its CA with
65
-the --ca flag, or the direct path to the certificate file with the
66
---cert flag. If the --ca flag is used, only the default storage paths
67
-are assumed (i.e. using --ca flag with custom storage backends or file
68
-paths will not work).
80
+This command can be used in one of two ways. Either by specifying
81
+which certificate to untrust by a direct path to the certificate
82
+file with the --cert flag, or by fetching the root certificate for
83
+the CA from the admin API (default behaviour).
84
70
-If no flags are specified, --ca=local is assumed.`,
85
+If the admin API is used, then the CA defaults to 'local'. You may
86
+specify the ID of another CA with the --ca flag. By default, this
87
+will attempt to connect to the Caddy's admin API running at
88
+'` + caddy.DefaultAdminListen + `' to fetch the root certificate.
89
+You may explicitly specify the --address, or use the --config flag
90
+to load the admin address from your config, if not using the default.`,
91
Flags: func() *flag.FlagSet {
92
fs := flag.NewFlagSet("untrust", flag.ExitOnError)
73
- fs.String("ca", "", "The ID of the CA to untrust")
93
fs.String("cert", "", "The path to the CA certificate to untrust")
94
+ fs.String("ca", "", "The ID of the CA to untrust (defaults to 'local')")
95
+ fs.String("address", "", "Address of the administration API listener (if --config is not used)")
96
+ fs.String("config", "", "Configuration file (if --address is not used)")
97
+ fs.String("adapter", "", "Name of config adapter to apply (if --config is used)")
98
return fs
99
}(),
100
})
101
}
102
80
-func cmdTrust(fs caddycmd.Flags) (int, error) {
81
- // we have to create a sort of dummy context so that
82
- // the CA can provision itself...
83
- ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
84
- defer cancel()
103
+func cmdTrust(fl caddycmd.Flags) (int, error) {
104
+ caID := fl.String("ca")
105
+ addrFlag := fl.String("address")
106
+ configFlag := fl.String("config")
107
+ configAdapterFlag := fl.String("adapter")
108
86
- // provision the CA, which generates and stores a root
87
- // certificate if one doesn't already exist in storage
88
- ca := CA{
89
- storage: caddy.DefaultStorage,
109
+ // Prepare the URI to the admin endpoint
110
+ if caID == "" {
111
+ caID = DefaultCAID
112
}
91
- err := ca.Provision(ctx, DefaultCAID, caddy.Log())
113
+
114
+ // Determine where we're sending the request to get the CA info
115
+ adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
116
+ if err != nil {
117
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
118
+ }
119
+
120
+ // Fetch the root cert from the admin API
121
+ rootCert, err := rootCertFromAdmin(adminAddr, caID)
122
if err != nil {
123
return caddy.ExitCodeFailedStartup, err
124
}
125
126
+ // Set up the CA struct; we only need to fill in the root
127
+ // because we're only using it to make use of the installRoot()
128
+ // function. Also needs a logger for warnings, and a "cert path"
129
+ // for the root cert; since we're loading from the API and we
130
+ // don't know the actual storage path via this flow, we'll just
131
+ // pass through the admin API address instead.
132
+ ca := CA{
133
+ log: caddy.Log(),
134
+ root: rootCert,
135
+ rootCertPath: adminAddr + adminPKICertificatesEndpoint + caID,
136
+ }
137
+
138
+ // Install the cert!
139
err = ca.installRoot()
140
if err != nil {
141
return caddy.ExitCodeFailedStartup, err
144
return caddy.ExitCodeSuccess, nil
145
}
146
104
-func cmdUntrust(fs caddycmd.Flags) (int, error) {
105
- ca := fs.String("ca")
106
- cert := fs.String("cert")
147
+func cmdUntrust(fl caddycmd.Flags) (int, error) {
148
+ certFile := fl.String("cert")
149
+ caID := fl.String("ca")
150
+ addrFlag := fl.String("address")
151
+ configFlag := fl.String("config")
152
+ configAdapterFlag := fl.String("adapter")
153
108
- if ca != "" && cert != "" {
109
- return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments")
154
+ if certFile != "" && (caID != "" || addrFlag != "" || configFlag != "") {
155
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments, cannot use --cert with other flags")
156
}
111
- if ca == "" && cert == "" {
112
- ca = DefaultCAID
157
+
158
+ // If a file was specified, try to uninstall the cert matching that file
159
+ if certFile != "" {
160
+ // Sanity check, make sure cert file exists first
161
+ _, err := os.Stat(certFile)
162
+ if err != nil {
163
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
164
+ }
165
+
166
+ // Uninstall the file!
167
+ err = truststore.UninstallFile(certFile,
168
+ truststore.WithDebug(),
169
+ truststore.WithFirefox(),
170
+ truststore.WithJava())
171
+ if err != nil {
172
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
173
+ }
174
+
175
+ return caddy.ExitCodeSuccess, nil
176
}
114
- if ca != "" {
115
- cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt")
177
+
178
+ // Prepare the URI to the admin endpoint
179
+ if caID == "" {
180
+ caID = DefaultCAID
181
}
182
118
- // sanity check, make sure cert file exists first
119
- _, err := os.Stat(cert)
183
+ // Determine where we're sending the request to get the CA info
184
+ adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
185
if err != nil {
121
- return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
186
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
187
}
188
124
- err = truststore.UninstallFile(cert,
189
+ // Fetch the root cert from the admin API
190
+ rootCert, err := rootCertFromAdmin(adminAddr, caID)
191
+ if err != nil {
192
+ return caddy.ExitCodeFailedStartup, err
193
+ }
194
+
195
+ // Uninstall the cert!
196
+ err = truststore.Uninstall(rootCert,
197
truststore.WithDebug(),
198
truststore.WithFirefox(),
199
truststore.WithJava())
200
if err != nil {
129
- return caddy.ExitCodeFailedStartup, err
201
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
202
}
203
204
return caddy.ExitCodeSuccess, nil
205
}
206
+
207
+// rootCertFromAdmin makes the API request to fetch the
208
+func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) {
209
+ uri := adminPKICertificatesEndpoint + caID
210
+
211
+ // Make the request to fetch the CA info
212
+ resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil)
213
+ if err != nil {
214
+ return nil, fmt.Errorf("requesting CA info: %v", err)
215
+ }
216
+ defer resp.Body.Close()
217
+
218
+ // Decode the resposne
219
+ caInfo := new(CAInfo)
220
+ err = json.NewDecoder(resp.Body).Decode(caInfo)
221
+ if err != nil {
222
+ return nil, fmt.Errorf("failed to decode JSON response: %v", err)
223
+ }
224
+
225
+ // Decode the root
226
+ rootBlock, _ := pem.Decode([]byte(caInfo.Root))
227
+ if rootBlock == nil {
228
+ return nil, fmt.Errorf("failed to decode root certificate: %v", err)
229
+ }
230
+ rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
231
+ if err != nil {
232
+ return nil, fmt.Errorf("failed to parse root certificate: %v", err)
233
+ }
234
+
235
+ return rootCert, nil
236
+}