diff options
Diffstat (limited to 'modules/caddypki/command.go')
-rw-r--r-- | modules/caddypki/command.go | 201 |
1 files changed, 152 insertions, 49 deletions
diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go index 34daefa..fa37ab0 100644 --- a/modules/caddypki/command.go +++ b/modules/caddypki/command.go @@ -15,11 +15,13 @@ package caddypki import ( - "context" + "crypto/x509" + "encoding/json" + "encoding/pem" "flag" "fmt" + "net/http" "os" - "path/filepath" "github.com/caddyserver/caddy/v2" caddycmd "github.com/caddyserver/caddy/v2/cmd" @@ -30,69 +32,110 @@ func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "trust", Func: cmdTrust, + 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. Intended for -development environments only. - -Since Caddy will install its root certificates into the local trust -stores automatically when they are first generated, this command is -only necessary if you need to pre-install the certificates before -using them; for example, if you have elevated privileges at one -point but not later, you will want to use this command so that a -password prompt is not required later. - -This command installs the root certificate only for Caddy's -default CA.`, +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. + +Also, this command 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.`, + Flags: func() *flag.FlagSet { + fs := flag.NewFlagSet("trust", flag.ExitOnError) + fs.String("ca", "", "The ID of the CA to trust (defaults to 'local')") + fs.String("address", "", "Address of the administration API listener (if --config is not used)") + fs.String("config", "", "Configuration file (if --address is not used)") + fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") + return fs + }(), }) caddycmd.RegisterCommand(caddycmd.Command{ Name: "untrust", Func: cmdUntrust, - Usage: "[--ca <id> | --cert <path>]", + 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). Intended -for development environments only. +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. +This command does not delete or modify certificate files from Caddy's +configured storage. -Specify which certificate to untrust either by the ID of its CA with -the --ca flag, or the direct path to the certificate file with the ---cert flag. If the --ca flag is used, only the default storage paths -are assumed (i.e. using --ca flag with custom storage backends or file -paths will not work). +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 no flags are specified, --ca=local is assumed.`, +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.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("untrust", flag.ExitOnError) - fs.String("ca", "", "The ID of the CA to untrust") fs.String("cert", "", "The path to the CA certificate to untrust") + fs.String("ca", "", "The ID of the CA to untrust (defaults to 'local')") + fs.String("address", "", "Address of the administration API listener (if --config is not used)") + fs.String("config", "", "Configuration file (if --address is not used)") + fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") return fs }(), }) } -func cmdTrust(fs caddycmd.Flags) (int, error) { - // we have to create a sort of dummy context so that - // the CA can provision itself... - ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) - defer cancel() +func cmdTrust(fl caddycmd.Flags) (int, error) { + caID := fl.String("ca") + addrFlag := fl.String("address") + configFlag := fl.String("config") + configAdapterFlag := fl.String("adapter") - // provision the CA, which generates and stores a root - // certificate if one doesn't already exist in storage - ca := CA{ - storage: caddy.DefaultStorage, + // Prepare the URI to the admin endpoint + if caID == "" { + caID = DefaultCAID } - err := ca.Provision(ctx, DefaultCAID, caddy.Log()) + + // Determine where we're sending the request to get the CA info + adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, 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 + adminPKICertificatesEndpoint + caID, + } + + // Install the cert! err = ca.installRoot() if err != nil { return caddy.ExitCodeFailedStartup, err @@ -101,33 +144,93 @@ func cmdTrust(fs caddycmd.Flags) (int, error) { return caddy.ExitCodeSuccess, nil } -func cmdUntrust(fs caddycmd.Flags) (int, error) { - ca := fs.String("ca") - cert := fs.String("cert") +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 ca != "" && cert != "" { - return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments") + if certFile != "" && (caID != "" || addrFlag != "" || configFlag != "") { + return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments, cannot use --cert with other flags") } - if ca == "" && cert == "" { - ca = DefaultCAID + + // 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 } - if ca != "" { - cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt") + + // Prepare the URI to the admin endpoint + if caID == "" { + caID = DefaultCAID } - // sanity check, make sure cert file exists first - _, err := os.Stat(cert) + // Determine where we're sending the request to get the CA info + adminAddr, err := caddycmd.DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) if err != nil { - return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err) + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } - err = truststore.UninstallFile(cert, + // 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, err + return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to uninstall certificate file: %v", err) } return caddy.ExitCodeSuccess, nil } + +// rootCertFromAdmin makes the API request to fetch the +func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) { + uri := adminPKICertificatesEndpoint + 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 resposne + 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 + rootBlock, _ := pem.Decode([]byte(caInfo.Root)) + 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 +} |