From ac73fed78948135e0549a471647a0dd78a393269 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Wed, 17 Jul 2024 20:11:05 +0300 Subject: [PATCH 01/15] Cleanup: move private functions to the bottom on x509/golang.go This is a trivial change which makes the code layout more clear. Further commits will make gradual changes to this module. Hence, keeping the public interface on the top helps a lot. Signed-off-by: Volodymyr Khoroz --- x509/golang.go | 124 ++++++++++++++++++++++++------------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/x509/golang.go b/x509/golang.go index 5ba8b16b..c66741ee 100644 --- a/x509/golang.go +++ b/x509/golang.go @@ -21,68 +21,6 @@ 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{ @@ -209,3 +147,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 +} From 3dd38cc872b9bffadf8da2502428dee4e6cbdffe Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 6 Oct 2024 18:07:33 +0300 Subject: [PATCH 02/15] Cleanup: spin up ca_utils.go and extract parseCerts into it This starts making common CA routines more navigable. There will me more functions added to it during the course of this PR Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_show.go | 24 ------------------------ subcommands/keys/ca_utils.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 subcommands/keys/ca_utils.go diff --git a/subcommands/keys/ca_show.go b/subcommands/keys/ca_show.go index 17155493..a0bbba63 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" @@ -152,28 +150,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..9eb486c0 --- /dev/null +++ b/subcommands/keys/ca_utils.go @@ -0,0 +1,30 @@ +package keys + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "strings" +) + +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 +} From 43f1a9d6160ba2c928e5b3bd5500bf0b407ce423 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 6 Oct 2024 18:20:42 +0300 Subject: [PATCH 03/15] Cleanup: add a function to define standard HSM flags In 80% of use cases they are exactly the same for all PKI related commands. Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_add_device_ca.go | 5 +---- subcommands/keys/ca_create.go | 4 +--- subcommands/keys/ca_revoke_device_ca.go | 5 +---- subcommands/keys/ca_rotate_tls.go | 5 +---- subcommands/keys/ca_utils.go | 14 ++++++++++++++ subcommands/keys/est.go | 5 +---- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/subcommands/keys/ca_add_device_ca.go b/subcommands/keys/ca_add_device_ca.go index 071485df..3b501ee9 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) { diff --git a/subcommands/keys/ca_create.go b/subcommands/keys/ca_create.go index c42fb335..5f002e03 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") diff --git a/subcommands/keys/ca_revoke_device_ca.go b/subcommands/keys/ca_revoke_device_ca.go index bb1fa913..dfe7ab94 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) { diff --git a/subcommands/keys/ca_rotate_tls.go b/subcommands/keys/ca_rotate_tls.go index ffc43221..c75e5826 100644 --- a/subcommands/keys/ca_rotate_tls.go +++ b/subcommands/keys/ca_rotate_tls.go @@ -20,10 +20,7 @@ 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) { diff --git a/subcommands/keys/ca_utils.go b/subcommands/keys/ca_utils.go index 9eb486c0..490ca184 100644 --- a/subcommands/keys/ca_utils.go +++ b/subcommands/keys/ca_utils.go @@ -5,8 +5,22 @@ import ( "encoding/pem" "fmt" "strings" + + "github.com/spf13/cobra" +) + +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 parseCertList(pemData string) (certs []*x509.Certificate) { for len(pemData) > 0 { block, remaining := pem.Decode([]byte(pemData)) diff --git a/subcommands/keys/est.go b/subcommands/keys/est.go index b81a95f1..27027a86 100644 --- a/subcommands/keys/est.go +++ b/subcommands/keys/est.go @@ -33,10 +33,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) { From 154ffb7fc3016446b74ab7f5725f952dfb48c904 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 6 Oct 2024 19:01:51 +0300 Subject: [PATCH 04/15] Cleanup: move HSM args validation into a better place The x509 common package should not know anything about the argument parsing or validation. That is a sole responsibility of the subcommands. Signed-off-by: Volodymyr Khoroz --- subcommands/el2g/gateway.go | 15 ++++++++++++++- subcommands/keys/ca_add_device_ca.go | 3 +-- subcommands/keys/ca_create.go | 3 +-- subcommands/keys/ca_revoke_device_ca.go | 3 +-- subcommands/keys/ca_rotate_tls.go | 3 +-- subcommands/keys/ca_utils.go | 19 +++++++++++++++++++ subcommands/keys/est.go | 10 +++++----- x509/common.go | 14 -------------- 8 files changed, 42 insertions(+), 28 deletions(-) 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 3b501ee9..eb62a042 100644 --- a/subcommands/keys/ca_add_device_ca.go +++ b/subcommands/keys/ca_add_device_ca.go @@ -68,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 5f002e03..1733a31c 100644 --- a/subcommands/keys/ca_create.go +++ b/subcommands/keys/ca_create.go @@ -74,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_revoke_device_ca.go b/subcommands/keys/ca_revoke_device_ca.go index dfe7ab94..4309ec38 100644 --- a/subcommands/keys/ca_revoke_device_ca.go +++ b/subcommands/keys/ca_revoke_device_ca.go @@ -122,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 c75e5826..2ed0a8b6 100644 --- a/subcommands/keys/ca_rotate_tls.go +++ b/subcommands/keys/ca_rotate_tls.go @@ -27,8 +27,7 @@ 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_utils.go b/subcommands/keys/ca_utils.go index 490ca184..dea6c6cd 100644 --- a/subcommands/keys/ca_utils.go +++ b/subcommands/keys/ca_utils.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/spf13/cobra" + + X509 "github.com/foundriesio/fioctl/x509" ) var ( @@ -21,6 +23,23 @@ func addStandardHsmFlags(cmd *cobra.Command) { 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)) diff --git a/subcommands/keys/est.go b/subcommands/keys/est.go index 27027a86..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() { @@ -55,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/common.go b/x509/common.go index 5334c8c0..4504b25b 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" @@ -74,19 +73,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 { From 1356e6871cbc6b487fb11fc356fbbbe7d2e3b5bb Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Thu, 18 Jul 2024 00:16:42 +0300 Subject: [PATCH 05/15] Feature: a facade factory root renewal command A command help and examples will be extended as new sub-commands are being added. Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_renewal.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 subcommands/keys/ca_renewal.go 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) +} From b70fb03f98ecd05c905a7a074735632bc543db0d Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Tue, 30 Jul 2024 20:34:03 +0300 Subject: [PATCH 06/15] Feature: show currently active factory root CA This is the Root CA that is used to sign Device CAs and TLS certificates. Several factory root CAs can be valid at the same time, but only one of them can be active. Signed-off-by: Volodymyr Khoroz --- client/foundries_pki.go | 1 + subcommands/keys/ca_show.go | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/foundries_pki.go b/client/foundries_pki.go index d18b185c..2031cb68 100644 --- a/client/foundries_pki.go +++ b/client/foundries_pki.go @@ -18,6 +18,7 @@ type CaCerts struct { CaRevokeCrl string `json:"ca-revoke-crl,omitempty"` CaDisabled []string `json:"disabled-ca-serials,omitempty"` // readonly + ActiveRoot string `json:"active-root-serial,omitempty"` ChangeMeta ChangeMeta `json:"change-meta"` } diff --git a/subcommands/keys/ca_show.go b/subcommands/keys/ca_show.go index a0bbba63..326af0b9 100644 --- a/subcommands/keys/ca_show.go +++ b/subcommands/keys/ca_show.go @@ -75,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) From 4043eb2780471394d217604dba01416c688a3d85 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sat, 3 Aug 2024 19:46:09 +0300 Subject: [PATCH 07/15] Feature: a command to start root CA renewal process (w/o HSM) This adds the first workflow command of the root CA renewal, which generates the EST compliant CA renewal bundle and uploads it to the server. This is a bare minimum implementation, further extended with auxiliary features in later commits. For example, an HSM support is added in the next commit. That approach allows to decrease the level of complexity while traversing commits. Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_renewal_start.go | 92 ++++++++++++++++++++++++++++ x509/bash.go | 9 +++ x509/golang.go | 46 +++++++++----- 3 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 subcommands/keys/ca_renewal_start.go diff --git a/subcommands/keys/ca_renewal_start.go b/subcommands/keys/ca_renewal_start.go new file mode 100644 index 00000000..2451c75a --- /dev/null +++ b/subcommands/keys/ca_renewal_start.go @@ -0,0 +1,92 @@ +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: "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) +} + +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) + + // 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") + 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)) + certs.RootCrt += x509.CreateFactoryCrossCa(factory, newRootOnDisk.PublicKey) + + fmt.Println("Uploading signed certs to Foundries") + subcommands.DieNotNil(api.FactoryPatchCA(factory, certs)) +} diff --git a/x509/bash.go b/x509/bash.go index 0c60fb2a..65c61163 100644 --- a/x509/bash.go +++ b/x509/bash.go @@ -6,6 +6,7 @@ package x509 import ( + "crypto" "os" "os/exec" @@ -95,6 +96,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) } @@ -160,6 +165,10 @@ func SignEl2GoCsr(csrPem string) string { } 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/golang.go b/x509/golang.go index c66741ee..2546e42f 100644 --- a/x509/golang.go +++ b/x509/golang.go @@ -23,43 +23,43 @@ type KeyStorage interface { 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. + 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 { @@ -112,6 +112,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) @@ -130,7 +144,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{ From ce002f2392bc6158861affc39e8581554ca97f35 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 4 Aug 2024 00:03:16 +0300 Subject: [PATCH 08/15] Feature: support HSM for the PKI root CA renewal Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_renewal_start.go | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/subcommands/keys/ca_renewal_start.go b/subcommands/keys/ca_renewal_start.go index 2451c75a..54b8c4ae 100644 --- a/subcommands/keys/ca_renewal_start.go +++ b/subcommands/keys/ca_renewal_start.go @@ -13,6 +13,12 @@ import ( "github.com/foundriesio/fioctl/x509" ) +var ( + hsmOldModule string + hsmOldPin string + hsmOldTokenLabel string +) + func init() { cmd := &cobra.Command{ Use: "start ", @@ -35,6 +41,16 @@ This extreme level of isolation is necessary to prevent an accidental rewrite of 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) { @@ -50,6 +66,18 @@ 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") @@ -74,6 +102,7 @@ This ensures that no sensitive data can be accidentally tampered or erased.`)) 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) @@ -85,6 +114,7 @@ This ensures that no sensitive data can be accidentally tampered or erased.`)) // 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.Println("Uploading signed certs to Foundries") From 010057845328097c7b2095144b77021ac759cc55 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 4 Aug 2024 00:04:55 +0300 Subject: [PATCH 09/15] Feature: write the root CA renewal bundle to file This is useful from 2 perspectives: 1. A user may want to view the certificates (e.g. using openssl storeutl). 2. A user may need to (re-)upload this file to the API (e.g. while experimenting or fixing a broken PKI). Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_renewal_start.go | 5 +++++ x509/common.go | 1 + 2 files changed, 6 insertions(+) diff --git a/subcommands/keys/ca_renewal_start.go b/subcommands/keys/ca_renewal_start.go index 54b8c4ae..d6abad9e 100644 --- a/subcommands/keys/ca_renewal_start.go +++ b/subcommands/keys/ca_renewal_start.go @@ -117,6 +117,11 @@ This warrants that an existing private key is not accidentally overwritten or er 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/x509/common.go b/x509/common.go index 4504b25b..bac239d0 100644 --- a/x509/common.go +++ b/x509/common.go @@ -13,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" From b2ce7fef7193c0bf715418cd774cdca08188f547 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 4 Aug 2024 14:16:31 +0300 Subject: [PATCH 10/15] Feature: commands to trigger root CA renewal on devices This closely resembles the `rotate-cert` commands layout by adding two new commands: - A `devices config renew-root` for device level config. - A `config renew-root` for group and factory level configs. The key difference is the support for factory-wide config change. That is needed to facilitate the root CA renewal for group-less devices and devices (auto-)registered in the future. The created config needs a correlation ID which is fetched from the server. The server generates a new correlation ID upon any changes to the root CA bundle; entire change log is stored for audit. This is needed so that config updates triggered for the same root CA renewal (but different devices) are counted as one. That allows to accurately calculate a number of already updated devices, before proceeding to the next root CA renewal step. I am not sure if the proposed layout is the best one from the user convenience perspective. The other option was to add single command like `keys ca renewal deploy [-g | -d]`. That has its pros and cons, so I am open to start a discussion on it. Signed-off-by: Volodymyr Khoroz --- client/foundries_pki.go | 18 ++++- subcommands/common_config.go | 24 +++++++ subcommands/config/renew_root.go | 85 ++++++++++++++++++++++++ subcommands/devices/config_renew_root.go | 78 ++++++++++++++++++++++ 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 subcommands/config/renew_root.go create mode 100644 subcommands/devices/config_renew_root.go diff --git a/client/foundries_pki.go b/client/foundries_pki.go index 2031cb68..d3869f5c 100644 --- a/client/foundries_pki.go +++ b/client/foundries_pki.go @@ -20,6 +20,8 @@ type CaCerts struct { CaDisabled []string `json:"disabled-ca-serials,omitempty"` // readonly ActiveRoot string `json:"active-root-serial,omitempty"` + RootRenewalCorrelationId string `json:"root-renewal-correlation-id,omitempty"` // readonly + ChangeMeta ChangeMeta `json:"change-meta"` } @@ -90,11 +92,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)) +} From cbd3f218aed3abb3ff9b864265471f903369f39b Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Tue, 6 Aug 2024 20:59:41 +0300 Subject: [PATCH 11/15] Feature: a command to activate a specific root CA This command can be used two-way to switch between old and new root CA. It is made a standalone command to give the user a better grasp of what is going on. Signed-off-by: Volodymyr Khoroz --- client/foundries_pki.go | 7 ++-- subcommands/keys/ca_renewal_activate.go | 56 +++++++++++++++++++++++++ x509/common.go | 9 ++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 subcommands/keys/ca_renewal_activate.go diff --git a/client/foundries_pki.go b/client/foundries_pki.go index d3869f5c..ca1afb4d 100644 --- a/client/foundries_pki.go +++ b/client/foundries_pki.go @@ -16,9 +16,10 @@ 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 - ActiveRoot string `json:"active-root-serial,omitempty"` + 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 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/x509/common.go b/x509/common.go index bac239d0..37bf691c 100644 --- a/x509/common.go +++ b/x509/common.go @@ -38,6 +38,15 @@ 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. + + // 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 { From 8dd4ef8a2cd41b6b997efb09b4268fd0214a7de4 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Mon, 19 Aug 2024 15:57:19 +0300 Subject: [PATCH 12/15] Feature: a command to re-sign device CAs Signed-off-by: Volodymyr Khoroz --- .../keys/ca_renewal_resign_device_ca.go | 57 +++++++++++++++++++ x509/bash.go | 5 ++ x509/golang.go | 8 +++ 3 files changed, 70 insertions(+) create mode 100644 subcommands/keys/ca_renewal_resign_device_ca.go 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..8c61894c --- /dev/null +++ b/subcommands/keys/ca_renewal_resign_device_ca.go @@ -0,0 +1,57 @@ +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: "re-sign-device-ca ", + Short: "Re-sign all existing Device CAs with a new root CA for your Factory PKI", + Run: doReSignDeviceCaRenewal, + Args: cobra.ExactArgs(1), + 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. +`, + } + caRenewalCmd.AddCommand(cmd) + addStandardHsmFlags(cmd) +} + +func doReSignDeviceCaRenewal(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) + + fmt.Println("Fetching a list of existing device CAs") + resp, err := api.FactoryGetCA(factory) + subcommands.DieNotNil(err) + + fmt.Println("Re-signing existing device CAs using a new Root CA key") + certs := client.CaCerts{} + for _, ca := range parseCertList(resp.CaCrt) { + if len(certs.CaCrt) > 0 { + certs.CaCrt += "\n" + } + certs.CaCrt += x509.ReSignCrt(ca) + } + + fmt.Println("Uploading re-signed certs to Foundries.io") + subcommands.DieNotNil(api.FactoryPatchCA(factory, certs)) +} diff --git a/x509/bash.go b/x509/bash.go index 65c61163..34812407 100644 --- a/x509/bash.go +++ b/x509/bash.go @@ -7,6 +7,7 @@ package x509 import ( "crypto" + "crypto/x509" "os" "os/exec" @@ -164,6 +165,10 @@ 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() } diff --git a/x509/golang.go b/x509/golang.go index 2546e42f..2be9ccd6 100644 --- a/x509/golang.go +++ b/x509/golang.go @@ -34,6 +34,7 @@ func CreateFactoryCrossCa(ou string, pubkey crypto.PublicKey) string { // 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) @@ -74,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 { From e51fd5a3d793195cf8a553ca2e9424522c9d1fe9 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sat, 14 Sep 2024 18:13:35 +0300 Subject: [PATCH 13/15] Feature: copy private key files for Device CAs during Root CA renewal There are several sub-use cases here: - A user may lose all or a part of Device CA private keys. - A user may keep these private key files in different folders. - A user may wish to not copy them into a new PKI folder. All of the above use cases are supported by this extension. Signed-off-by: Volodymyr Khoroz --- .../keys/ca_renewal_resign_device_ca.go | 124 +++++++++++++++++- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/subcommands/keys/ca_renewal_resign_device_ca.go b/subcommands/keys/ca_renewal_resign_device_ca.go index 8c61894c..622b6fc7 100644 --- a/subcommands/keys/ca_renewal_resign_device_ca.go +++ b/subcommands/keys/ca_renewal_resign_device_ca.go @@ -1,8 +1,12 @@ package keys import ( + "crypto/ecdsa" + cryptoX509 "crypto/x509" + "encoding/pem" "fmt" "os" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -14,16 +18,22 @@ import ( func init() { cmd := &cobra.Command{ - Use: "re-sign-device-ca ", + 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.ExactArgs(1), + 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) @@ -33,25 +43,131 @@ This allows old certificates (issued by a previous root CA) to continue being us func doReSignDeviceCaRenewal(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) + 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 += x509.ReSignCrt(ca) + 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 } From 7fec7d27a0d71f845e3ab41a0030f6a6e60ad1a9 Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 6 Oct 2024 17:35:17 +0300 Subject: [PATCH 14/15] Feature: a command to re-sign TLS certificates Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_renewal_resign_tls.go | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 subcommands/keys/ca_renewal_resign_tls.go 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)) +} From 039da9fba9e6673e8b40527661bf6342824e4e4f Mon Sep 17 00:00:00 2001 From: Volodymyr Khoroz Date: Sun, 6 Oct 2024 17:56:49 +0300 Subject: [PATCH 15/15] Feature: an ability to revoke an old Root CA Signed-off-by: Volodymyr Khoroz --- subcommands/keys/ca_renewal_revoke_root.go | 51 ++++++++++++++++++++++ x509/common.go | 6 +++ 2 files changed, 57 insertions(+) create mode 100644 subcommands/keys/ca_renewal_revoke_root.go 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/x509/common.go b/x509/common.go index 37bf691c..608f618b 100644 --- a/x509/common.go +++ b/x509/common.go @@ -41,6 +41,12 @@ const ( // 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.