summaryrefslogtreecommitdiff
path: root/modules/caddypki/command.go
blob: e78f35c88d005d1d33aa0c1411094ff8f0502c57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddypki

import (
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"net/http"
	"os"
	"path"

	"github.com/caddyserver/caddy/v2"
	caddycmd "github.com/caddyserver/caddy/v2/cmd"
	"github.com/smallstep/truststore"
	"github.com/spf13/cobra"
)

func init() {
	caddycmd.RegisterCommand(caddycmd.Command{
		Name:  "trust",
		Usage: "[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]",
		Short: "Installs a CA certificate into local trust stores",
		Long: `
Adds a root certificate into the local trust stores.

Caddy will attempt to install its root certificates into the local
trust stores automatically when they are first generated, but it
might fail if Caddy doesn't have the appropriate permissions to
write to the trust store. This command is necessary to pre-install
the certificates before using them, if the server process runs as an
unprivileged user (such as via systemd).

By default, this command installs the root certificate for Caddy's
default CA (i.e. 'local'). You may specify the ID of another CA
with the --ca flag.

This command will attempt to connect to Caddy's admin API running at
'` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may
explicitly specify the --address, or use the --config flag to load
the admin address from your config, if not using the default.`,
		CobraFunc: func(cmd *cobra.Command) {
			cmd.Flags().StringP("ca", "", "", "The ID of the CA to trust (defaults to 'local')")
			cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)")
			cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)")
			cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)")
			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdTrust)
		},
	})

	caddycmd.RegisterCommand(caddycmd.Command{
		Name:  "untrust",
		Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]",
		Short: "Untrusts a locally-trusted CA certificate",
		Long: `
Untrusts a root certificate from the local trust store(s).

This command uninstalls trust; it does not necessarily delete the
root certificate from trust stores entirely. Thus, repeatedly
trusting and untrusting new certificates can fill up trust databases.

This command does not delete or modify certificate files from Caddy's
configured storage.

This command can be used in one of two ways. Either by specifying
which certificate to untrust by a direct path to the certificate
file with the --cert flag, or by fetching the root certificate for
the CA from the admin API (default behaviour).

If the admin API is used, then the CA defaults to 'local'. You may
specify the ID of another CA with the --ca flag. By default, this
will attempt to connect to the Caddy's admin API running at
'` + caddy.DefaultAdminListen + `' to fetch the root certificate.
You may explicitly specify the --address, or use the --config flag
to load the admin address from your config, if not using the default.`,
		CobraFunc: func(cmd *cobra.Command) {
			cmd.Flags().StringP("cert", "p", "", "The path to the CA certificate to untrust")
			cmd.Flags().StringP("ca", "", "", "The ID of the CA to untrust (defaults to 'local')")
			cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)")
			cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)")
			cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)")
			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdUntrust)
		},
	})
}

func cmdTrust(fl caddycmd.Flags) (int, error) {
	caID := fl.String("ca")
	addrFlag := fl.String("address")
	configFlag := fl.String("config")
	configAdapterFlag := fl.String("adapter")

	// Prepare the URI to the admin endpoint
	if caID == "" {
		caID = DefaultCAID
	}

	// Determine where we're sending the request to get the CA info
	adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
	}

	// Fetch the root cert from the admin API
	rootCert, err := rootCertFromAdmin(adminAddr, caID)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	// Set up the CA struct; we only need to fill in the root
	// because we're only using it to make use of the installRoot()
	// function. Also needs a logger for warnings, and a "cert path"
	// for the root cert; since we're loading from the API and we
	// don't know the actual storage path via this flow, we'll just
	// pass through the admin API address instead.
	ca := CA{
		log:          caddy.Log(),
		root:         rootCert,
		rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, "ca", caID),
	}

	// Install the cert!
	err = ca.installRoot()
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	return caddy.ExitCodeSuccess, nil
}

func cmdUntrust(fl caddycmd.Flags) (int, error) {
	certFile := fl.String("cert")
	caID := fl.String("ca")
	addrFlag := fl.String("address")
	configFlag := fl.String("config")
	configAdapterFlag := fl.String("adapter")

	if certFile != "" && (caID != "" || addrFlag != "" || configFlag != "") {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments, cannot use --cert with other flags")
	}

	// If a file was specified, try to uninstall the cert matching that file
	if certFile != "" {
		// Sanity check, make sure cert file exists first
		_, err := os.Stat(certFile)
		if err != nil {
			return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
		}

		// Uninstall the file!
		err = truststore.UninstallFile(certFile,
			truststore.WithDebug(),
			truststore.WithFirefox(),
			truststore.WithJava())
		if err != nil {
			return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
		}

		return caddy.ExitCodeSuccess, nil
	}

	// Prepare the URI to the admin endpoint
	if caID == "" {
		caID = DefaultCAID
	}

	// Determine where we're sending the request to get the CA info
	adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
	}

	// Fetch the root cert from the admin API
	rootCert, err := rootCertFromAdmin(adminAddr, caID)
	if err != nil {
		return caddy.ExitCodeFailedStartup, err
	}

	// Uninstall the cert!
	err = truststore.Uninstall(rootCert,
		truststore.WithDebug(),
		truststore.WithFirefox(),
		truststore.WithJava())
	if err != nil {
		return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err)
	}

	return caddy.ExitCodeSuccess, nil
}

// 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 := path.Join(adminPKIEndpointBase, "ca", caID)

	// Make the request to fetch the CA info
	resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil)
	if err != nil {
		return nil, fmt.Errorf("requesting CA info: %v", err)
	}
	defer resp.Body.Close()

	// Decode the response
	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 cert
	rootBlock, _ := pem.Decode([]byte(caInfo.RootCert))
	if rootBlock == nil {
		return nil, fmt.Errorf("failed to decode root certificate: %v", err)
	}
	rootCert, err := x509.ParseCertificate(rootBlock.Bytes)
	if err != nil {
		return nil, fmt.Errorf("failed to parse root certificate: %v", err)
	}

	return rootCert, nil
}