diff --git a/client/foundries_pki.go b/client/foundries_pki.go index d18b185c..ca1afb4d 100644 --- a/client/foundries_pki.go +++ b/client/foundries_pki.go @@ -16,8 +16,12 @@ type CaCerts struct { EstCrt string `json:"est-tls-crt,omitempty"` TlsCrt string `json:"tls-crt,omitempty"` - CaRevokeCrl string `json:"ca-revoke-crl,omitempty"` - CaDisabled []string `json:"disabled-ca-serials,omitempty"` // readonly + CaRevokeCrl string `json:"ca-revoke-crl,omitempty"` + CaDisabled []string `json:"disabled-ca-serials,omitempty"` // readonly + ActiveRoot string `json:"active-root-serial,omitempty"` + RootRevokeCrl string `json:"root-revoke-crl,omitempty"` + + RootRenewalCorrelationId string `json:"root-renewal-correlation-id,omitempty"` // readonly ChangeMeta ChangeMeta `json:"change-meta"` } @@ -89,11 +93,21 @@ func (a *Api) FactoryEstUrl(factory string, port int, resource string) (string, if err != nil { return "", err } - if len(cert.EstCrt) == 0 { - return "", errors.New("EST server is not configured. Please see `fioctl keys est`") + return cert.GetEstUrl(port, resource, false) +} + +func (certs *CaCerts) GetEstUrl(port int, resource string, fallbackToGateway bool) (string, error) { + crtPem := certs.EstCrt + if len(crtPem) == 0 { + if fallbackToGateway { + // If the factory PKI was configured, the TLS cert is always present. + crtPem = certs.TlsCrt + } else { + return "", errors.New("EST server is not configured. Please see `fioctl keys est`") + } } - block, _ := pem.Decode([]byte(cert.EstCrt)) + block, _ := pem.Decode([]byte(crtPem)) c, err := x509.ParseCertificate(block.Bytes) if err != nil { return "", fmt.Errorf("Failed to parse certificate: %w", err) diff --git a/subcommands/common_config.go b/subcommands/common_config.go index e691cabb..24c42f4c 100644 --- a/subcommands/common_config.go +++ b/subcommands/common_config.go @@ -159,6 +159,30 @@ func PrintConfig(cfg *client.DeviceConfig, showAppliedAt, highlightFirstLine boo } } +type RenewRootOptions struct { + Reason string + CorrelationId string + EstServer string +} + +func (o RenewRootOptions) AsConfig() client.ConfigCreateRequest { + b := new(bytes.Buffer) + fmt.Fprintf(b, "ESTSERVER=%s\n", o.EstServer) + fmt.Fprintf(b, "ROTATIONID=%s\n", o.CorrelationId) + + return client.ConfigCreateRequest{ + Reason: o.Reason, + Files: []client.ConfigFile{ + { + Name: "fio-renew-root", + Value: b.String(), + Unencrypted: true, + OnChanged: []string{"/usr/share/fioconfig/handlers/renew-root-cert"}, + }, + }, + } +} + type RotateCertOptions struct { Reason string EstServer string diff --git a/subcommands/config/renew_root.go b/subcommands/config/renew_root.go new file mode 100644 index 00000000..257fc1dc --- /dev/null +++ b/subcommands/config/renew_root.go @@ -0,0 +1,85 @@ +package config + +import ( + "errors" + "fmt" + + "github.com/foundriesio/fioctl/subcommands" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func init() { + renewCmd := &cobra.Command{ + Use: "renew-root", + Short: "Renew a Factory root CA on the devices (in this group) used to verify the device gateway TLS certificate", + Run: doRenewRoot, + Long: `This command will send a fioconfig change to a device to instruct it to perform +a root CA renewal using the EST server configured with "fioctl keys est". +If there is no configured EST server, it will instruct the device to renew a root CA from the device gateway. + +This command will only work for devices running LmP version 95 and later.`, + } + cmd.AddCommand(renewCmd) + renewCmd.Flags().StringP("group", "g", "", "Device group to use") + renewCmd.Flags().StringP("est-resource", "e", "/.well-known/cacerts", "The path the to EST resource on your server") + renewCmd.Flags().IntP("est-port", "p", 8443, "The EST server port") + renewCmd.Flags().StringP("reason", "r", "", "The reason for triggering the root CA renewal") + renewCmd.Flags().StringP("server-name", "", "", "EST server name when not using the Foundries managed server. e.g. est.example.com") + renewCmd.Flags().BoolP("dryrun", "", false, "Show what the fioconfig entry will be and exit") + _ = renewCmd.MarkFlagRequired("reason") +} + +func doRenewRoot(cmd *cobra.Command, args []string) { + factory := viper.GetString("factory") + estResource, _ := cmd.Flags().GetString("est-resource") + estPort, _ := cmd.Flags().GetInt("est-port") + group, _ := cmd.Flags().GetString("group") + reason, _ := cmd.Flags().GetString("reason") + serverName, _ := cmd.Flags().GetString("server-name") + dryRun, _ := cmd.Flags().GetBool("dryrun") + + if estResource[0] != '/' { + estResource = "/" + estResource + } + + if len(group) > 0 { + logrus.Debugf("Renewing device root CA for devices in group %s", group) + } else { + logrus.Debug("Renewing device root CA for all factory devices") + } + + certs, err := api.FactoryGetCA(factory) + subcommands.DieNotNil(err) + if len(certs.RootRenewalCorrelationId) == 0 { + subcommands.DieNotNil(errors.New("There is no Factory root renewal. Use 'fioctl keys ca renewal' to start it.")) + } + + var url string + if len(serverName) > 0 { + url = fmt.Sprintf("https://%s:%d%s", serverName, estPort, estResource) + } else { + url, err = certs.GetEstUrl(estPort, estResource, true) + subcommands.DieNotNil(err) + } + logrus.Debugf("Using EST server: %s", url) + + opts := subcommands.RenewRootOptions{ + Reason: reason, + CorrelationId: certs.RootRenewalCorrelationId, + EstServer: url, + } + + ccr := opts.AsConfig() + if dryRun { + fmt.Println("Config file would be:") + fmt.Println(ccr.Files[0].Value) + return + } + if len(group) > 0 { + subcommands.DieNotNil(api.GroupPatchConfig(factory, group, ccr, false)) + } else { + subcommands.DieNotNil(api.FactoryPatchConfig(factory, ccr, false)) + } +} diff --git a/subcommands/devices/config_renew_root.go b/subcommands/devices/config_renew_root.go new file mode 100644 index 00000000..c1088dc8 --- /dev/null +++ b/subcommands/devices/config_renew_root.go @@ -0,0 +1,78 @@ +package devices + +import ( + "errors" + "fmt" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/foundriesio/fioctl/subcommands" +) + +func init() { + cmd := &cobra.Command{ + Use: "renew-root ", + Short: "Renew a Factory root CA on the device used to verify the device gateway TLS certificate", + Args: cobra.ExactArgs(1), + Run: doConfigRenewRoot, + Long: `This command will send a fioconfig change to a device to instruct it to perform +a root CA renewal using the EST server configured with "fioctl keys est". +If there is no configured EST server, it will instruct the device to renew a root CA from the device gateway. + +This command will only work for devices running LmP version 95 and later.`, + } + cmd.Flags().StringP("est-resource", "e", "/.well-known/cacerts", "The path the to EST resource on your server") + cmd.Flags().IntP("est-port", "p", 8443, "The EST server port") + cmd.Flags().StringP("reason", "r", "", "The reason for triggering the root CA renewal") + cmd.Flags().StringP("server-name", "", "", "EST server name when not using the Foundries managed server. e.g. est.example.com") + cmd.Flags().BoolP("dryrun", "", false, "Show what the fioconfig entry will be and exit") + configCmd.AddCommand(cmd) + _ = cmd.MarkFlagRequired("reason") +} + +func doConfigRenewRoot(cmd *cobra.Command, args []string) { + name := args[0] + estResource, _ := cmd.Flags().GetString("est-resource") + estPort, _ := cmd.Flags().GetInt("est-port") + reason, _ := cmd.Flags().GetString("reason") + serverName, _ := cmd.Flags().GetString("server-name") + dryRun, _ := cmd.Flags().GetBool("dryrun") + + if estResource[0] != '/' { + estResource = "/" + estResource + } + + logrus.Debugf("Renewing device root CA for %s", name) + + // Quick sanity check for device + d := getDevice(cmd, name) + certs, err := api.FactoryGetCA(d.Factory) + subcommands.DieNotNil(err) + if len(certs.RootRenewalCorrelationId) == 0 { + subcommands.DieNotNil(errors.New("There is no Factory root renewal. Use 'fioctl keys ca renewal' to start it.")) + } + + var url string + if len(serverName) > 0 { + url = fmt.Sprintf("https://%s:%d%s", serverName, estPort, estResource) + } else { + url, err = certs.GetEstUrl(estPort, estResource, true) + subcommands.DieNotNil(err) + } + logrus.Debugf("Using EST server: %s", url) + + opts := subcommands.RenewRootOptions{ + Reason: reason, + CorrelationId: certs.RootRenewalCorrelationId, + EstServer: url, + } + + ccr := opts.AsConfig() + if dryRun { + fmt.Println("Config file would be:") + fmt.Println(ccr.Files[0].Value) + return + } + subcommands.DieNotNil(d.Api.PatchConfig(ccr, false)) +} diff --git a/subcommands/el2g/gateway.go b/subcommands/el2g/gateway.go index 32574dcc..22cd1d9a 100644 --- a/subcommands/el2g/gateway.go +++ b/subcommands/el2g/gateway.go @@ -38,7 +38,7 @@ func doDeviceGateway(cmd *cobra.Command, args []string) { factory := viper.GetString("factory") subcommands.DieNotNil(os.Chdir(pkiDir)) - hsm, err := x509.ValidateHsmArgs( + hsm, err := validateHsmArgs( hsmModule, hsmPin, hsmTokenLabel, "--hsm-module", "--hsm-pin", "--hsm-token-label") subcommands.DieNotNil(err) x509.InitHsm(hsm) @@ -63,3 +63,16 @@ func doDeviceGateway(cmd *cobra.Command, args []string) { certs := client.CaCerts{CaCrt: newCa} subcommands.DieNotNil(api.FactoryPatchCA(factory, certs)) } + +func validateHsmArgs(hsmModule, hsmPin, hsmTokenLabel, moduleArg, pinArg, tokenArg string) (*x509.HsmInfo, error) { + if len(hsmModule) > 0 { + if len(hsmPin) == 0 { + return nil, fmt.Errorf("%s is required with %s", pinArg, moduleArg) + } + if len(hsmTokenLabel) == 0 { + return nil, fmt.Errorf("%s is required with %s", tokenArg, moduleArg) + } + return &x509.HsmInfo{Module: hsmModule, Pin: hsmPin, TokenLabel: hsmTokenLabel}, nil + } + return nil, nil +} diff --git a/subcommands/keys/ca_add_device_ca.go b/subcommands/keys/ca_add_device_ca.go index 071485df..eb62a042 100644 --- a/subcommands/keys/ca_add_device_ca.go +++ b/subcommands/keys/ca_add_device_ca.go @@ -44,10 +44,7 @@ All such CAs will be added to the list of device CAs trusted by the device gatew cmd.Flags().StringP("local-ca-filename", "", x509.DeviceCaCertFile, fmt.Sprintf("A file name of the local CA (only needed if the %s file already exists)", x509.DeviceCaCertFile)) _ = cmd.MarkFlagFilename("local-ca-filename") - // HSM variables defined in ca_create.go - cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Load a root CA key from a PKCS#11 compatible HSM using this module") - cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to log into the HSM") - cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token containing the root CA key") + addStandardHsmFlags(cmd) } func assertFileName(flagName, value string) { @@ -71,8 +68,7 @@ func doAddDeviceCa(cmd *cobra.Command, args []string) { assertFileName("--local-ca-filename", localCaFilename) subcommands.DieNotNil(os.Chdir(args[0])) - hsm, err := x509.ValidateHsmArgs( - hsmModule, hsmPin, hsmTokenLabel, "--hsm-module", "--hsm-pin", "--hsm-token-label") + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) subcommands.DieNotNil(err) x509.InitHsm(hsm) diff --git a/subcommands/keys/ca_create.go b/subcommands/keys/ca_create.go index c42fb335..1733a31c 100644 --- a/subcommands/keys/ca_create.go +++ b/subcommands/keys/ca_create.go @@ -16,9 +16,6 @@ import ( var ( createOnlineCA bool createLocalCA bool - hsmModule string - hsmPin string - hsmTokenLabel string ) func init() { @@ -60,6 +57,7 @@ This is optional.`, caCmd.AddCommand(cmd) cmd.Flags().BoolVarP(&createOnlineCA, "online-ca", "", true, "Create an online CA owned by Foundries that works with lmp-device-register") cmd.Flags().BoolVarP(&createLocalCA, "local-ca", "", true, "Create a local CA that you can use for signing your own device certificates") + // HSM variable descriptions differ from the standard defined in addStandardHsmFlags cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Create a root CA key on a PKCS#11 compatible HSM using this module") cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to log into the HSM") cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token created for the root CA key") @@ -76,8 +74,7 @@ func doCreateCA(cmd *cobra.Command, args []string) { certsDir := args[0] subcommands.DieNotNil(os.Chdir(certsDir)) - hsm, err := x509.ValidateHsmArgs( - hsmModule, hsmPin, hsmTokenLabel, "--hsm-module", "--hsm-pin", "--hsm-token-label") + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) subcommands.DieNotNil(err) x509.InitHsm(hsm) diff --git a/subcommands/keys/ca_renewal.go b/subcommands/keys/ca_renewal.go new file mode 100644 index 00000000..0b792668 --- /dev/null +++ b/subcommands/keys/ca_renewal.go @@ -0,0 +1,20 @@ +package keys + +import ( + "github.com/spf13/cobra" +) + +var caRenewalCmd = &cobra.Command{ + Use: "renewal", + Short: "Renew the root of trust for your factory PKI", + Long: `These sub-commands allow you to gradually renew a root of trust for your factory PKI. + +A guided process allows you to: +- Generate a new root of trust. +- Create an EST standard compliant root CA renewal bundle.- Re-sign all necessary factory PKI certificates. +- Provision a new root of trust to all your devices without service interruption.`, +} + +func init() { + caCmd.AddCommand(caRenewalCmd) +} diff --git a/subcommands/keys/ca_renewal_activate.go b/subcommands/keys/ca_renewal_activate.go new file mode 100644 index 00000000..09838c2b --- /dev/null +++ b/subcommands/keys/ca_renewal_activate.go @@ -0,0 +1,56 @@ +package keys + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/foundriesio/fioctl/client" + "github.com/foundriesio/fioctl/subcommands" + "github.com/foundriesio/fioctl/x509" +) + +func init() { + cmd := &cobra.Command{ + Use: "activate ", + Short: "Activate a new root CA for your Factory PKI", + Run: doActivateCaRenewal, + Args: cobra.ExactArgs(1), + Long: `Activate a new root CA for your Factory PKI. + +This commands activates a new root CA by superseding a previous one. +An active root CA is the one used to issue, sign, and verify all device CAs and TLS certificates. +There can be many root CAs, recognized as valid by devices. +But, only one of them can be active at a time. + +This command can be used many times to switch between currently active root CAs. +Cross-signed root CA certificates (which are a part of a renewal bundle) cannot be activated. + +Typically, this command is used after deploying the renewal bundle to all devices, and before re-signing device CAs.`, + } + caRenewalCmd.AddCommand(cmd) + addStandardHsmFlags(cmd) +} + +func doActivateCaRenewal(cmd *cobra.Command, args []string) { + factory := viper.GetString("factory") + certsDir := args[0] + subcommands.DieNotNil(os.Chdir(certsDir)) + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) + subcommands.DieNotNil(err) + x509.InitHsm(hsm) + + newRootOnDisk := x509.LoadCertFromFile(x509.FactoryCaCertFile) + newCerts := client.CaCerts{ActiveRoot: newRootOnDisk.SerialNumber.Text(10)} + oldCerts, err := api.FactoryGetCA(factory) + subcommands.DieNotNil(err) + if oldCerts.ActiveRoot == newCerts.ActiveRoot { + subcommands.DieNotNil(fmt.Errorf("A given root CA with serial %s is already active", oldCerts.ActiveRoot)) + } + + toRevoke := map[string]int{oldCerts.ActiveRoot: x509.CrlRootSupersede} + newCerts.RootRevokeCrl = x509.CreateCrl(toRevoke) + subcommands.DieNotNil(api.FactoryPatchCA(factory, newCerts)) +} diff --git a/subcommands/keys/ca_renewal_resign_device_ca.go b/subcommands/keys/ca_renewal_resign_device_ca.go new file mode 100644 index 00000000..622b6fc7 --- /dev/null +++ b/subcommands/keys/ca_renewal_resign_device_ca.go @@ -0,0 +1,173 @@ +package keys + +import ( + "crypto/ecdsa" + cryptoX509 "crypto/x509" + "encoding/pem" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/foundriesio/fioctl/client" + "github.com/foundriesio/fioctl/subcommands" + "github.com/foundriesio/fioctl/x509" +) + +func init() { + cmd := &cobra.Command{ + Use: "re-sign-device-ca []", + Short: "Re-sign all existing Device CAs with a new root CA for your Factory PKI", + Run: doReSignDeviceCaRenewal, + Args: cobra.RangeArgs(1, 2), + Long: `Re-sign all existing Device CAs with a new root CA for your Factory PKI. + +Both currently active and disabled Device CAs are being re-signed. +All their properties are preserved, including a serial number. +Only the signature and authority key ID are being changed. +This allows old certificates (issued by a previous root CA) to continue being used to issue device client certificates. + +Re-signed device CA certificates are stored in the provided PKI directory. +An old PKI directory is used to locate corresponding private keys, and copy them into the PKI directory. +Each located device CA gets the same file name, as it was in the old PKI directory. +If a device CA certificate cannot be located in an old PKI directory - it does not get stored locally. +If an old PKI directory argument is not provided, new certificates are not stored locally. +`, + } + caRenewalCmd.AddCommand(cmd) + addStandardHsmFlags(cmd) +} + +func doReSignDeviceCaRenewal(cmd *cobra.Command, args []string) { + factory := viper.GetString("factory") + certsDir := args[0] + + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) + subcommands.DieNotNil(err) + x509.InitHsm(hsm) + + var oldKeys []fsKey + if len(args) > 1 { + oldCertsDir := args[1] + cwd, err := os.Getwd() + subcommands.DieNotNil(err) + subcommands.DieNotNil(os.Chdir(oldCertsDir)) + oldKeys = loadEcKeys() + subcommands.DieNotNil(os.Chdir(cwd)) + } + subcommands.DieNotNil(os.Chdir(certsDir)) + newKeys := loadEcKeys() + + fmt.Println("Fetching a list of existing device CAs") + resp, err := api.FactoryGetCA(factory) + subcommands.DieNotNil(err) + + // A safety measure to make sure that the provided PKI directory belongs to the currently active Root CA. + // This helps preventing an accidental file rewrite in the "wrong" directory. + fsRootCAData, err := os.ReadFile(x509.FactoryCaCertFile) + subcommands.DieNotNil(err, "Could not load a Root CA cert file") + if certs := parseCertList(string(fsRootCAData)); len(certs) != 1 { + subcommands.DieNotNil(fmt.Errorf("There must be exactly one cert in a Root CA file: %s", x509.FactoryCaCertFile)) + } else if resp.ActiveRoot != certs[0].SerialNumber.Text(10) { + subcommands.DieNotNil(fmt.Errorf("A provided PKI directory is not a currently active factory PKI: %s", certsDir)) + } + + fmt.Println("Re-signing existing device CAs using a new Root CA key") + certs := client.CaCerts{} + storedFiles := make(map[string][2]string, 0) + missingFiles := make([]string, 0) + for _, ca := range parseCertList(resp.CaCrt) { + newCaPem := x509.ReSignCrt(ca) + if len(certs.CaCrt) > 0 { + certs.CaCrt += "\n" + } + certs.CaCrt += newCaPem + + // Locate cert/key files by comparing public keys of a server-side cert and a private key file. + // This is the most reliable (and secure) way to identify corresponding key files. + var found bool + serial := ca.SerialNumber.Text(10) + for _, key := range newKeys { + if key.pubkey.Equal(ca.PublicKey) { + // A key file is already present in a new PKI folder. Simply overwrite a cert file. + certFile := strings.TrimSuffix(key.filename, ".key") + ".pem" + subcommands.DieNotNil(os.WriteFile(certFile, []byte(newCaPem), 0400)) + storedFiles[serial] = [2]string{certFile, key.filename} + found = true + break + } + } + + if found { + continue + } + found = false + for _, key := range oldKeys { + if key.pubkey.Equal(ca.PublicKey) { + // A key file is found in an old PKI folder. Copy a private key and write a cert file. + certFile := strings.TrimSuffix(key.filename, ".key") + ".pem" + subcommands.DieNotNil(os.WriteFile(key.filename, key.content, 0400)) + subcommands.DieNotNil(os.WriteFile(certFile, []byte(newCaPem), 0400)) + storedFiles[serial] = [2]string{certFile, key.filename} + found = true + break + } + } + if !found { + missingFiles = append(missingFiles, serial) + } + } + + fmt.Println("Uploading re-signed certs to Foundries.io") + subcommands.DieNotNil(api.FactoryPatchCA(factory, certs)) + + if len(storedFiles) > 0 { + fmt.Println("Stored the following Device CA files into a new PKI directory:") + for serial, filenames := range storedFiles { + fmt.Printf("\t- Serial %s -> %s, %s\n", serial, filenames[0], filenames[1]) + } + } + if len(missingFiles) > 0 { + fmt.Printf(`Could not find private key files for the following Device CA serials: + - %s +Corresponding Device CA certificates were not stored on your filesystem. +You may copy those private key files manually from an old PKI directory. +Alternatively, you can re-run this command again providing an old PKI directory containing these files. +`, strings.Join(missingFiles, ", ")) + } +} + +type fsKey struct { + filename string + content []byte + pubkey ecdsa.PublicKey +} + +func loadEcKeys() []fsKey { + dir, err := os.Getwd() + subcommands.DieNotNil(err) + files, err := os.ReadDir(dir) + subcommands.DieNotNil(err) + res := make([]fsKey, 0, len(files)) + for _, file := range files { + if file.IsDir() || !file.Type().IsRegular() || !strings.HasSuffix(file.Name(), ".key") { + continue + } + data, err := os.ReadFile(file.Name()) + subcommands.DieNotNil(err, "Failed to open key file: "+file.Name()) + block, rest := pem.Decode(data) + if block == nil || len(rest) > 0 { + // This might be some custom user's key file format; skip it. + continue + } + key, err := cryptoX509.ParseECPrivateKey(block.Bytes) + if err != nil { + // This might be a non-EC key file; skip it. + continue + } + res = append(res, fsKey{file.Name(), data, key.PublicKey}) + } + return res +} diff --git a/subcommands/keys/ca_renewal_resign_tls.go b/subcommands/keys/ca_renewal_resign_tls.go new file mode 100644 index 00000000..4ab0a66f --- /dev/null +++ b/subcommands/keys/ca_renewal_resign_tls.go @@ -0,0 +1,57 @@ +package keys + +import ( + "errors" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/foundriesio/fioctl/client" + "github.com/foundriesio/fioctl/subcommands" + "github.com/foundriesio/fioctl/x509" +) + +func init() { + cmd := &cobra.Command{ + Use: "re-sign-tls ", + Short: "Re-sign the TLS certificates used by Device Gateway, OSTree Server, and EST Server, if applicable", + Run: doReSignTlsRenewal, + Args: cobra.ExactArgs(1), + } + caRenewalCmd.AddCommand(cmd) + addStandardHsmFlags(cmd) +} + +func doReSignTlsRenewal(cmd *cobra.Command, args []string) { + factory := viper.GetString("factory") + subcommands.DieNotNil(os.Chdir(args[0])) + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) + subcommands.DieNotNil(err) + x509.InitHsm(hsm) + + fmt.Println("Requesting existing TLS certificates") + resp, err := api.FactoryGetCA(factory) + subcommands.DieNotNil(err) + + fmt.Println("Re-signing Foundries TLS certificate") + certs := parseCertList(resp.TlsCrt) + if len(certs) != 1 { + subcommands.DieNotNil(errors.New("There must be exactly one TLS certificate")) + } + req := client.CaCerts{TlsCrt: x509.ReSignCrt(certs[0])} + subcommands.DieNotNil(os.WriteFile(x509.TlsCertFile, []byte(req.TlsCrt), 0400)) + + if len(resp.EstCrt) > 0 { + fmt.Println("Re-signing Foundries EST certificate") + certs := parseCertList(resp.EstCrt) + if len(certs) != 1 { + subcommands.DieNotNil(errors.New("There must be zero or one EST certificate")) + } + req.EstCrt = x509.ReSignCrt(certs[0]) + } + + fmt.Println("Uploading signed certs to Foundries") + subcommands.DieNotNil(api.FactoryPatchCA(factory, req)) +} diff --git a/subcommands/keys/ca_renewal_revoke_root.go b/subcommands/keys/ca_renewal_revoke_root.go new file mode 100644 index 00000000..fffc908b --- /dev/null +++ b/subcommands/keys/ca_renewal_revoke_root.go @@ -0,0 +1,51 @@ +package keys + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/foundriesio/fioctl/client" + "github.com/foundriesio/fioctl/subcommands" + "github.com/foundriesio/fioctl/x509" +) + +var serialToRevoke string + +func init() { + cmd := &cobra.Command{ + Use: "revoke-root ", + Short: "Revoke an inactive root CA for your Factory PKI", + Run: doRevokeRootRenewal, + Args: cobra.ExactArgs(1), + Long: `Revoke an inactive root CAA for your Factory PKI. + +This command removes a Root CA with a given serial and associated cross-signed CA certificates. +After that point a given Root CA can no longer be re-activated. +Devices which were not updated with a new Root CA will instantly lose connectivity to Foundries.io services. + +This action is irreversible, so plan properly as to when you perform it. +It is usually the last step of the Root CA rotation, which is optional unless your old Root CA was compromised.`, + } + caRenewalCmd.AddCommand(cmd) + cmd.Flags().StringVarP(&serialToRevoke, "serial", "", "", + "A serial number of the Root CA to revoke in hexadecimal format") + _ = cmd.MarkFlagRequired("serial") + addStandardHsmFlags(cmd) +} + +func doRevokeRootRenewal(cmd *cobra.Command, args []string) { + factory := viper.GetString("factory") + certsDir := args[0] + subcommands.DieNotNil(os.Chdir(certsDir)) + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) + subcommands.DieNotNil(err) + x509.InitHsm(hsm) + + toRevoke := map[string]int{serialToRevoke: x509.CrlRootRevoke} + // The API will determine and revoke cross-signed certificates as well. + // It will also check that a user is not revoking a currently active Root CA. + req := client.CaCerts{RootRevokeCrl: x509.CreateCrl(toRevoke)} + subcommands.DieNotNil(api.FactoryPatchCA(factory, req)) +} diff --git a/subcommands/keys/ca_renewal_start.go b/subcommands/keys/ca_renewal_start.go new file mode 100644 index 00000000..d6abad9e --- /dev/null +++ b/subcommands/keys/ca_renewal_start.go @@ -0,0 +1,127 @@ +package keys + +import ( + "errors" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/foundriesio/fioctl/client" + "github.com/foundriesio/fioctl/subcommands" + "github.com/foundriesio/fioctl/x509" +) + +var ( + hsmOldModule string + hsmOldPin string + hsmOldTokenLabel string +) + +func init() { + cmd := &cobra.Command{ + Use: "start ", + Short: "Start a root of trust renewal for your factory PKI", + Run: doStartCaRenewal, + Args: cobra.ExactArgs(2), + Long: `Start a root of trust renewal for your factory PKI. + +This command does a few things:. +1. First, it generates a new root of trust for your factory. +2. Second, it cross-signs a new root of trust using an old root of trust to prepare a CA renewal bundle. + This CA renewal bundle is compliant with the EST standard, necessary for a secure root CA update on devices. +3. Finally, it uploads the CA renewal bundle to the backend API for validation and storage. + +A new root of trust needs to be stored in a separate directory from the previous root of trust. +By the end of the renewal process, all necessary PKI pieces are migrated into this new directory. +If you are using an HSM device - a new private key should be stored under a different label, possibly a new device. +This extreme level of isolation is necessary to prevent an accidental rewrite of the old root of trust. + +Once this command completes successfully, a root of trust renewal process is started.`, + } + caRenewalCmd.AddCommand(cmd) + // HSM variable descriptions differ from the standard defined in addStandardHsmFlags + cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Create a root CA key on a PKCS#11 compatible HSM using this module") + cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to log into the HSM") + cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token created for the root CA key") + cmd.Flags().StringVarP(&hsmOldModule, "hsm-old-module", "", "", + "A PKCS#11 compatible HSM module storing an old root CA key. By default use --hsm-module.") + cmd.Flags().StringVarP(&hsmOldPin, "hsm-old-pin", "", "", + "The PKCS#11 PIN to log into the HSM storing an old root CA key. By default use --hsm-pin.") + cmd.Flags().StringVarP(&hsmOldTokenLabel, "hsm-old-token-label", "", "", + "The label of the HSM token containing an old root CA key.") +} + +func doStartCaRenewal(cmd *cobra.Command, args []string) { + factory := viper.GetString("factory") + newCertsDir := args[0] + oldCertsDir := args[1] + if oldCertsDir == newCertsDir { + subcommands.DieNotNil(errors.New(`A new PKI directory must be different that an old PKI directory. +A root CA renewal migrates your Factory PKI to a new (initially empty) folder. +This ensures that no sensitive data can be accidentally tampered or erased.`)) + } + + cwd, err := os.Getwd() + subcommands.DieNotNil(err) + + newHsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) + subcommands.DieNotNil(err) + oldHsm, err := validateHsmArgs( + hsmOldModule, hsmOldPin, hsmOldTokenLabel, "--hsm-old-module", "--hsm-old-pin", "--hsm-old-token-label") + subcommands.DieNotNil(err) + if hsmModule == hsmOldModule && hsmTokenLabel == hsmOldTokenLabel { + subcommands.DieNotNil(errors.New(`When using HSM devices, a new private key must be stored on: +- either a new HSM device with the same or a different label; +- or the same HSM device with a different label. +This warrants that an existing private key is not accidentally overwritten or erased.`)) + } + + // Phase 1 - Load existing Root CA and check if it is the active one. + // Do not check if the user actually has access to its private key - that is verified later by signing certificates. + fmt.Println("Verifying access to old root CA for Factory") + subcommands.DieNotNil(os.Chdir(oldCertsDir)) + oldCerts, err := api.FactoryGetCA(factory) + subcommands.DieNotNil(err, "Failed to fetch current root CA for Factory") + oldRootOnDisk := x509.LoadCertFromFile(x509.FactoryCaCertFile) + oldRootOnDiskSerial := oldRootOnDisk.SerialNumber.Text(10) + if oldRootOnDiskSerial != oldCerts.ActiveRoot { + subcommands.DieNotNil(fmt.Errorf( + "Old PKI directory has root CA with serial %s but %s is expected", oldRootOnDiskSerial, oldCerts.ActiveRoot)) + } + + // Phase 2 - Generate a new Root CA. + certs := client.CaCerts{RootCrt: oldCerts.RootCrt} + subcommands.DieNotNil(os.Chdir(cwd)) + subcommands.DieNotNil(os.Chdir(newCertsDir)) + if _, err := os.Stat(x509.FactoryCaCertFile); err == nil { + subcommands.DieNotNil(fmt.Errorf( + "Factory CA file already exists inside %s. Cancelling to prevent accidental rewrite", newCertsDir)) + } else if !os.IsNotExist(err) { + subcommands.DieNotNil(err) + } + fmt.Println("Creating new offline root CA for Factory") + x509.InitHsm(newHsm) + certs.RootCrt += x509.CreateFactoryCa(factory) + newRootOnDisk := x509.LoadCertFromFile(x509.FactoryCaCertFile) + + // Phase 2 - Generate 2 cross-signed Root CAs. + fmt.Println("Generating two cross-signed root CAs for Factory") + // Old CA cross-signed by a new CA. + certs.RootCrt += x509.CreateFactoryCrossCa(factory, oldRootOnDisk.PublicKey) + + // New CA cross-signed by an old CA. + subcommands.DieNotNil(os.Chdir(cwd)) + subcommands.DieNotNil(os.Chdir(oldCertsDir)) + x509.InitHsm(oldHsm) + certs.RootCrt += x509.CreateFactoryCrossCa(factory, newRootOnDisk.PublicKey) + + fmt.Printf("Saving new root CA bundle for Factory into %s\n", x509.FactoryCaPackFile) + subcommands.DieNotNil(os.Chdir(cwd)) + subcommands.DieNotNil(os.Chdir(newCertsDir)) + subcommands.DieNotNil(os.WriteFile(x509.FactoryCaPackFile, []byte(certs.RootCrt), 0400)) + + fmt.Println("Uploading signed certs to Foundries") + subcommands.DieNotNil(api.FactoryPatchCA(factory, certs)) +} diff --git a/subcommands/keys/ca_revoke_device_ca.go b/subcommands/keys/ca_revoke_device_ca.go index bb1fa913..4309ec38 100644 --- a/subcommands/keys/ca_revoke_device_ca.go +++ b/subcommands/keys/ca_revoke_device_ca.go @@ -100,10 +100,7 @@ func addRevokeCmdFlags(cmd *cobra.Command, op string) { cmd.Flags().StringArrayP("ca-serial", "", nil, "A serial number (base 10) of the device CA to "+op+". Can be used multiple times to "+op+" several device CAs") _ = cmd.MarkFlagFilename("ca-file") - // HSM variables defined in ca_create.go - cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Load a root CA key from a PKCS#11 compatible HSM using this module") - cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to log into the HSM") - cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token containing the root CA key") + addStandardHsmFlags(cmd) } func doRevokeDeviceCa(cmd *cobra.Command, args []string) { @@ -125,8 +122,7 @@ func doRevokeDeviceCa(cmd *cobra.Command, args []string) { } subcommands.DieNotNil(os.Chdir(args[0])) - hsm, err := x509.ValidateHsmArgs( - hsmModule, hsmPin, hsmTokenLabel, "--hsm-module", "--hsm-pin", "--hsm-token-label") + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) subcommands.DieNotNil(err) x509.InitHsm(hsm) diff --git a/subcommands/keys/ca_rotate_tls.go b/subcommands/keys/ca_rotate_tls.go index ffc43221..2ed0a8b6 100644 --- a/subcommands/keys/ca_rotate_tls.go +++ b/subcommands/keys/ca_rotate_tls.go @@ -20,18 +20,14 @@ func init() { Args: cobra.ExactArgs(1), } caCmd.AddCommand(cmd) - // HSM variables defined in ca_create.go - cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Load a root CA key from a PKCS#11 compatible HSM using this module") - cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to log into the HSM") - cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token containing the root CA key") + addStandardHsmFlags(cmd) } func doRotateTls(cmd *cobra.Command, args []string) { factory := viper.GetString("factory") subcommands.DieNotNil(os.Chdir(args[0])) - hsm, err := x509.ValidateHsmArgs( - hsmModule, hsmPin, hsmTokenLabel, "--hsm-module", "--hsm-pin", "--hsm-token-label") + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) subcommands.DieNotNil(err) x509.InitHsm(hsm) diff --git a/subcommands/keys/ca_show.go b/subcommands/keys/ca_show.go index 17155493..326af0b9 100644 --- a/subcommands/keys/ca_show.go +++ b/subcommands/keys/ca_show.go @@ -6,9 +6,7 @@ import ( "crypto/x509" "encoding/asn1" "encoding/hex" - "encoding/pem" "fmt" - "strings" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -77,9 +75,12 @@ func doShowCA(cmd *cobra.Command, args []string) { fmt.Println("Updated by:", resp.ChangeMeta.UpdatedBy) } - fmt.Println("## Factory root certificate") + fmt.Println("\n## Factory root certificates") printOneCert(resp.RootCrt) - fmt.Println("## Server TLS Certificate") + if len(resp.ActiveRoot) > 0 { + fmt.Println("Active factory root serial number:", resp.ActiveRoot) + } + fmt.Println("\n## Server TLS Certificate") printOneCert(resp.TlsCrt) fmt.Println("\n## Device Authentication Certificate(s)") printOneCert(resp.CaCrt) @@ -152,28 +153,6 @@ func extKeyUsage(ext []x509.ExtKeyUsage) string { return vals } -func parseCertList(pemData string) (certs []*x509.Certificate) { - for len(pemData) > 0 { - block, remaining := pem.Decode([]byte(pemData)) - if block == nil { - // could be excessive whitespace - if pemData = strings.TrimSpace(string(remaining)); len(pemData) == len(remaining) { - fmt.Println("Failed to parse remaining certificates: invalid PEM data") - break - } - continue - } - pemData = string(remaining) - c, err := x509.ParseCertificate(block.Bytes) - if err != nil { - fmt.Println("Failed to parse certificate:" + err.Error()) - continue - } - certs = append(certs, c) - } - return -} - func prettyPrint(cert string) { for _, c := range parseCertList(cert) { fmt.Println("Certificate:") diff --git a/subcommands/keys/ca_utils.go b/subcommands/keys/ca_utils.go new file mode 100644 index 00000000..dea6c6cd --- /dev/null +++ b/subcommands/keys/ca_utils.go @@ -0,0 +1,63 @@ +package keys + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + + "github.com/spf13/cobra" + + X509 "github.com/foundriesio/fioctl/x509" +) + +var ( + hsmModule string + hsmPin string + hsmTokenLabel string +) + +func addStandardHsmFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Load a root CA key from a PKCS#11 compatible HSM using this module") + cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to log into the HSM") + cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token containing the root CA key") +} + +func validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel string) (*X509.HsmInfo, error) { + return validateHsmArgs(hsmModule, hsmPin, hsmTokenLabel, "--hsm-module", "--hsm-pin", "--hsm-token-label") +} + +func validateHsmArgs(hsmModule, hsmPin, hsmTokenLabel, moduleArg, pinArg, tokenArg string) (*X509.HsmInfo, error) { + if len(hsmModule) > 0 { + if len(hsmPin) == 0 { + return nil, fmt.Errorf("%s is required with %s", pinArg, moduleArg) + } + if len(hsmTokenLabel) == 0 { + return nil, fmt.Errorf("%s is required with %s", tokenArg, moduleArg) + } + return &X509.HsmInfo{Module: hsmModule, Pin: hsmPin, TokenLabel: hsmTokenLabel}, nil + } + return nil, nil +} + +func parseCertList(pemData string) (certs []*x509.Certificate) { + for len(pemData) > 0 { + block, remaining := pem.Decode([]byte(pemData)) + if block == nil { + // could be excessive whitespace + if pemData = strings.TrimSpace(string(remaining)); len(pemData) == len(remaining) { + fmt.Println("Failed to parse remaining certificates: invalid PEM data") + break + } + continue + } + pemData = string(remaining) + c, err := x509.ParseCertificate(block.Bytes) + if err != nil { + fmt.Println("Failed to parse certificate:" + err.Error()) + continue + } + certs = append(certs, c) + } + return +} diff --git a/subcommands/keys/est.go b/subcommands/keys/est.go index b81a95f1..782d1ca8 100644 --- a/subcommands/keys/est.go +++ b/subcommands/keys/est.go @@ -4,12 +4,13 @@ import ( "fmt" "os" - "github.com/foundriesio/fioctl/client" - "github.com/foundriesio/fioctl/subcommands" - "github.com/foundriesio/fioctl/x509" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/foundriesio/fioctl/client" + "github.com/foundriesio/fioctl/subcommands" + "github.com/foundriesio/fioctl/x509" ) func init() { @@ -33,10 +34,7 @@ func init() { * Upload the resultant TLS certificate to api.foundries.io`, } estCmd.AddCommand(cmd) - // HSM variables defined in ca_create.go - cmd.Flags().StringVarP(&hsmModule, "hsm-module", "", "", "Load a root CA key from a PKCS#11 compatible HSM using this module") - cmd.Flags().StringVarP(&hsmPin, "hsm-pin", "", "", "The PKCS#11 PIN to log into the HSM") - cmd.Flags().StringVarP(&hsmTokenLabel, "hsm-token-label", "", "", "The label of the HSM token containing the root CA key") + addStandardHsmFlags(cmd) } func doShowEst(cmd *cobra.Command, args []string) { @@ -58,8 +56,7 @@ func doAuthorizeEst(cmd *cobra.Command, args []string) { factory := viper.GetString("factory") subcommands.DieNotNil(os.Chdir(args[0])) - hsm, err := x509.ValidateHsmArgs( - hsmModule, hsmPin, hsmTokenLabel, "--hsm-module", "--hsm-pin", "--hsm-token-label") + hsm, err := validateStandardHsmArgs(hsmModule, hsmPin, hsmTokenLabel) subcommands.DieNotNil(err) x509.InitHsm(hsm) diff --git a/x509/bash.go b/x509/bash.go index 0c60fb2a..34812407 100644 --- a/x509/bash.go +++ b/x509/bash.go @@ -6,6 +6,8 @@ package x509 import ( + "crypto" + "crypto/x509" "os" "os/exec" @@ -95,6 +97,10 @@ rm ca.cnf return readFile(FactoryCaCertFile) } +func CreateFactoryCrossCa(ou string, pubkey crypto.PublicKey) string { + return neverland() +} + func CreateDeviceCa(cn, ou string) string { return CreateDeviceCaExt(cn, ou, DeviceCaKeyFile, DeviceCaCertFile) } @@ -159,7 +165,15 @@ func SignEl2GoCsr(csrPem string) string { return signCaCsr("el2g-*", csrPem) } +func ReSignCrt(crt *x509.Certificate) string { + return neverland() +} + func CreateCrl(serials map[string]int) string { + return neverland() +} + +func neverland() string { if true { panic("This function is not implemented in Bash implementation") } diff --git a/x509/common.go b/x509/common.go index 5334c8c0..608f618b 100644 --- a/x509/common.go +++ b/x509/common.go @@ -4,7 +4,6 @@ import ( "crypto/x509" "encoding/pem" "errors" - "fmt" "os" "github.com/foundriesio/fioctl/subcommands" @@ -14,6 +13,7 @@ const ( FactoryCaKeyFile string = "factory_ca.key" FactoryCaKeyLabel string = "root-ca" FactoryCaCertFile string = "factory_ca.pem" + FactoryCaPackFile string = "factory_ca.bundle.pem" DeviceCaKeyFile string = "local-ca.key" DeviceCaCertFile string = "local-ca.pem" TlsCertFile string = "tls-crt" @@ -38,6 +38,21 @@ const ( // Renew the previously disabled device CA, so that new device registrations are possible again. // This value is currently not used by Fioctl. It is here for the reference of API integrators. CrlCaRenew = 8 // RFC 5280 - removeFromCRL + + // CRL constants for root CA revokation. + + // Revoke the root CA, so that it is no longer available in the root CAs list. + // Note: the API will revoke the root CA for any reason other than 4, 6, or 8. + // Reasons 6 and 8 are ignored. + // This action is permanent. + CrlRootRevoke = 5 // RFC 5280 - cessationOfOperation + + // Supersede the root CA by another root CA. + // This is used to switch the currently active root CA, used to sign/verify device CAs and TLS certificates. + // The superseded CA serial be a currently active root CA, and the CRL must be signed by a newly activated root CA. + // Otherwise, the API will reject this CRL. + // This action can be (re-)applied many times to switch active root CA back and forth. + CrlRootSupersede = 4 // RFC 5280 - superseded ) func readFile(filename string) string { @@ -74,19 +89,6 @@ func InitHsm(hsm *HsmInfo) { } } -func ValidateHsmArgs(hsmModule, hsmPin, hsmTokenLabel, moduleArg, pinArg, tokenArg string) (*HsmInfo, error) { - if len(hsmModule) > 0 { - if len(hsmPin) == 0 { - return nil, fmt.Errorf("%s is required with %s", pinArg, moduleArg) - } - if len(hsmTokenLabel) == 0 { - return nil, fmt.Errorf("%s is required with %s", tokenArg, moduleArg) - } - return &HsmInfo{Module: hsmModule, Pin: hsmPin, TokenLabel: hsmTokenLabel}, nil - } - return nil, nil -} - func parseOnePemBlock(pemBlock string) *pem.Block { first, rest := pem.Decode([]byte(pemBlock)) if first == nil || len(rest) > 0 { diff --git a/x509/golang.go b/x509/golang.go index 5ba8b16b..2be9ccd6 100644 --- a/x509/golang.go +++ b/x509/golang.go @@ -21,107 +21,46 @@ type KeyStorage interface { loadKey() crypto.Signer } -func genRandomSerialNumber() *big.Int { - // Generate a 160 bits serial number (20 octets) - max := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(160), nil) - serial, err := rand.Int(rand.Reader, max) - subcommands.DieNotNil(err) - return serial -} - -func genCertificate( - crtTemplate *x509.Certificate, caCrt *x509.Certificate, pub crypto.PublicKey, signerKey crypto.Signer, -) string { - certRaw, err := x509.CreateCertificate(rand.Reader, crtTemplate, caCrt, pub, signerKey) - subcommands.DieNotNil(err) - - certPemBlock := pem.Block{Type: "CERTIFICATE", Bytes: certRaw} - var certRow bytes.Buffer - err = pem.Encode(&certRow, &certPemBlock) - subcommands.DieNotNil(err) - - return certRow.String() -} - -func parsePemCertificateRequest(csrPem string) *x509.CertificateRequest { - pemBlock := parseOnePemBlock(csrPem) - clientCSR, err := x509.ParseCertificateRequest(pemBlock.Bytes) - subcommands.DieNotNil(err) - err = clientCSR.CheckSignature() - subcommands.DieNotNil(err) - return clientCSR -} - -func marshalSubject(cn string, ou string) pkix.Name { - // In it's simpler form, this function would be replaced by - // pkix.Name{CommonName: cn, OrganizationalUnit: []string{ou}} - // However, x509 library uses PrintableString instead of UTF8String - // as ASN.1 field type. This function forces UTF8String instead, to - // avoid compatibility issues when using a device certificate created - // with libraries such as MbedTLS. - // x509 library also encodes OU and CN in a different order if compared - // to OpenSSL, which is less of an issue, but still worth to adjust - // while we are at it. - cnBytes, err := asn1.MarshalWithParams(cn, "utf8") - subcommands.DieNotNil(err) - ouBytes, err := asn1.MarshalWithParams(ou, "utf8") - subcommands.DieNotNil(err) - var ( - oidCommonName = []int{2, 5, 4, 3} - oidOrganizationalUnit = []int{2, 5, 4, 11} - ) - pkixAttrTypeValue := []pkix.AttributeTypeAndValue{ - { - Type: oidCommonName, - Value: asn1.RawValue{FullBytes: cnBytes}, - }, - { - Type: oidOrganizationalUnit, - Value: asn1.RawValue{FullBytes: ouBytes}, - }, - } - return pkix.Name{ExtraNames: pkixAttrTypeValue} -} - func CreateFactoryCa(ou string) string { priv := factoryCaKeyStorage.genAndSaveKey() - crtTemplate := x509.Certificate{ - SerialNumber: genRandomSerialNumber(), - Subject: marshalSubject(factoryCaName, ou), - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(20, 0, 0), - - BasicConstraintsValid: true, - IsCA: true, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, - } - factoryCaString := genCertificate(&crtTemplate, &crtTemplate, priv.Public(), priv) + crtTemplate := genFactoryCaTemplate(marshalSubject(factoryCaName, ou)) + factoryCaString := genCertificate(crtTemplate, crtTemplate, priv.Public(), priv) writeFile(FactoryCaCertFile, factoryCaString) return factoryCaString } +func CreateFactoryCrossCa(ou string, pubkey crypto.PublicKey) string { + // Cross-signed factory CA has all the same properties as a factory CA, but is signed by another factory CA. + // This function does an inverse: produces a factory CA with a public key "borrowed" from another factory CA. + // The end result is the same, but we don't need to export the internal key storage interface. + // This certificate is not written to disk, as it is only needed intermittently. + // Cannot use a ReSignCrt as we need a new certificate here (with e.g. a new serial number). + priv := factoryCaKeyStorage.loadKey() + crtTemplate := genFactoryCaTemplate(marshalSubject(factoryCaName, ou)) + return genCertificate(crtTemplate, crtTemplate, pubkey, priv) +} + func CreateDeviceCa(cn, ou string) string { return CreateDeviceCaExt(cn, ou, DeviceCaKeyFile, DeviceCaCertFile) } func CreateDeviceCaExt(cn, ou, keyFile, certFile string) string { priv := genAndSaveKeyToFile(keyFile) - crtPem := genCaCert(marshalSubject(cn, ou), priv.Public()) + crtPem := genDeviceCaCert(marshalSubject(cn, ou), priv.Public()) writeFile(certFile, crtPem) return crtPem } func SignCaCsr(csrPem string) string { csr := parsePemCertificateRequest(csrPem) - crtPem := genCaCert(csr.Subject, csr.PublicKey) + crtPem := genDeviceCaCert(csr.Subject, csr.PublicKey) writeFile(OnlineCaCertFile, crtPem) return crtPem } func SignEl2GoCsr(csrPem string) string { csr := parsePemCertificateRequest(csrPem) - return genCaCert(csr.Subject, csr.PublicKey) + return genDeviceCaCert(csr.Subject, csr.PublicKey) } func SignTlsCsr(csrPem string) string { @@ -136,6 +75,13 @@ func SignEstCsr(csrPem string) string { return genTlsCert(csr.Subject, csr.DNSNames, csr.PublicKey) } +func ReSignCrt(crt *x509.Certificate) string { + // Use an input certificate as a template for a new certificate, preserving all its properties except a signature. + factoryKey := factoryCaKeyStorage.loadKey() + factoryCa := LoadCertFromFile(FactoryCaCertFile) + return genCertificate(crt, factoryCa, crt.PublicKey, factoryKey) +} + var oidExtensionReasonCode = []int{2, 5, 29, 21} func CreateCrl(serials map[string]int) string { @@ -174,6 +120,20 @@ func CreateCrl(serials map[string]int) string { return pemBuffer.String() } +func genFactoryCaTemplate(subject pkix.Name) *x509.Certificate { + return &x509.Certificate{ + SerialNumber: genRandomSerialNumber(), + Subject: subject, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(20, 0, 0), + + BasicConstraintsValid: true, + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + } +} + func genTlsCert(subject pkix.Name, dnsNames []string, pubkey crypto.PublicKey) string { factoryKey := factoryCaKeyStorage.loadKey() factoryCa := LoadCertFromFile(FactoryCaCertFile) @@ -192,7 +152,7 @@ func genTlsCert(subject pkix.Name, dnsNames []string, pubkey crypto.PublicKey) s return genCertificate(&crtTemplate, factoryCa, pubkey, factoryKey) } -func genCaCert(subject pkix.Name, pubkey crypto.PublicKey) string { +func genDeviceCaCert(subject pkix.Name, pubkey crypto.PublicKey) string { factoryKey := factoryCaKeyStorage.loadKey() factoryCa := LoadCertFromFile(FactoryCaCertFile) crtTemplate := x509.Certificate{ @@ -209,3 +169,65 @@ func genCaCert(subject pkix.Name, pubkey crypto.PublicKey) string { } return genCertificate(&crtTemplate, factoryCa, pubkey, factoryKey) } + +func genCertificate( + crtTemplate *x509.Certificate, caCrt *x509.Certificate, pub crypto.PublicKey, signerKey crypto.Signer, +) string { + certRaw, err := x509.CreateCertificate(rand.Reader, crtTemplate, caCrt, pub, signerKey) + subcommands.DieNotNil(err) + + certPemBlock := pem.Block{Type: "CERTIFICATE", Bytes: certRaw} + var certRow bytes.Buffer + err = pem.Encode(&certRow, &certPemBlock) + subcommands.DieNotNil(err) + + return certRow.String() +} + +func parsePemCertificateRequest(csrPem string) *x509.CertificateRequest { + pemBlock := parseOnePemBlock(csrPem) + clientCSR, err := x509.ParseCertificateRequest(pemBlock.Bytes) + subcommands.DieNotNil(err) + err = clientCSR.CheckSignature() + subcommands.DieNotNil(err) + return clientCSR +} + +func marshalSubject(cn string, ou string) pkix.Name { + // In it's simpler form, this function would be replaced by + // pkix.Name{CommonName: cn, OrganizationalUnit: []string{ou}} + // However, x509 library uses PrintableString instead of UTF8String + // as ASN.1 field type. This function forces UTF8String instead, to + // avoid compatibility issues when using a device certificate created + // with libraries such as MbedTLS. + // x509 library also encodes OU and CN in a different order if compared + // to OpenSSL, which is less of an issue, but still worth to adjust + // while we are at it. + cnBytes, err := asn1.MarshalWithParams(cn, "utf8") + subcommands.DieNotNil(err) + ouBytes, err := asn1.MarshalWithParams(ou, "utf8") + subcommands.DieNotNil(err) + var ( + oidCommonName = []int{2, 5, 4, 3} + oidOrganizationalUnit = []int{2, 5, 4, 11} + ) + pkixAttrTypeValue := []pkix.AttributeTypeAndValue{ + { + Type: oidCommonName, + Value: asn1.RawValue{FullBytes: cnBytes}, + }, + { + Type: oidOrganizationalUnit, + Value: asn1.RawValue{FullBytes: ouBytes}, + }, + } + return pkix.Name{ExtraNames: pkixAttrTypeValue} +} + +func genRandomSerialNumber() *big.Int { + // Generate a 160 bits serial number (20 octets) + max := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(160), nil) + serial, err := rand.Int(rand.Reader, max) + subcommands.DieNotNil(err) + return serial +}