Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions client/foundries_pki.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions subcommands/common_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions subcommands/config/renew_root.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we've decided we should stop adding factory wide config operations like this - they are too dangerous.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The majority of users will only want to renew the Root CA on all devices at once.
TBH I only added the ability to do this by group/device level for those few paranoids like me.

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))
}
}
78 changes: 78 additions & 0 deletions subcommands/devices/config_renew_root.go
Original file line number Diff line number Diff line change
@@ -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 <device>",
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))
}
15 changes: 14 additions & 1 deletion subcommands/el2g/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
8 changes: 2 additions & 6 deletions subcommands/keys/ca_add_device_ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)

Expand Down
7 changes: 2 additions & 5 deletions subcommands/keys/ca_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ import (
var (
createOnlineCA bool
createLocalCA bool
hsmModule string
hsmPin string
hsmTokenLabel string
)

func init() {
Expand Down Expand Up @@ -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")
Expand All @@ -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)

Expand Down
20 changes: 20 additions & 0 deletions subcommands/keys/ca_renewal.go
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

factory-> Factory


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line break missing there

- Provision a new root of trust to all your devices without service interruption.`,
}

func init() {
caCmd.AddCommand(caRenewalCmd)
}
56 changes: 56 additions & 0 deletions subcommands/keys/ca_renewal_activate.go
Original file line number Diff line number Diff line change
@@ -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 <PKI Directory>",
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))
}
Loading