From 1535e95d89576503a338de672444c37d784c34f5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 18 Feb 2020 19:07:12 -0800 Subject: [PATCH 1/6] Add tool to initialize pki in cloud kms. --- Makefile | 19 ++- cmd/step-cloudkms-init/main.go | 273 +++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 cmd/step-cloudkms-init/main.go diff --git a/Makefile b/Makefile index bd11222e..10c3c381 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ PKG?=github.com/smallstep/certificates/cmd/step-ca BINNAME?=step-ca +CLOUDKMS_BINNAME?=step-cloudkms-init +CLOUDKMS_PKG?=github.com/smallstep/certificates/cmd/step-cloudkms-init # Set V to 1 for verbose output from the Makefile Q=$(if $V,,@) @@ -56,17 +58,22 @@ GOFLAGS := CGO_ENABLED=0 download: $Q go mod download -build: $(PREFIX)bin/$(BINNAME) +build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) @echo "Build Complete!" $(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) $Q mkdir -p $(@D) $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) +$(PREFIX)bin/$(CLOUDKMS_BINNAME): download $(call rwildcard,*.go) + $Q mkdir -p $(@D) + $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(LDFLAGS) $(CLOUDKMS_PKG) + # Target to force a build of step-ca without running tests simple: $Q mkdir -p $(PREFIX)bin $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) + $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(LDFLAGS) $(CLOUDKMS_PKG) @echo "Build Complete!" .PHONY: download build simple @@ -113,11 +120,13 @@ lint: INSTALL_PREFIX?=/usr/ -install: $(PREFIX)bin/$(BINNAME) +install: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $Q install -D $(PREFIX)bin/$(BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(BINNAME) + $Q install -D $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(CLOUDKMS_BINNAME) uninstall: $Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME) + $Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(CLOUDKMS_BINNAME) .PHONY: install uninstall @@ -129,6 +138,9 @@ clean: ifneq ($(BINNAME),"") $Q rm -f bin/$(BINNAME) endif +ifneq ($(CLOUDKMS_BINNAME),"") + $Q rm -f bin/$(CLOUDKMS_BINNAME) +endif .PHONY: clean @@ -228,7 +240,7 @@ distclean: clean ################################################# BINARY_OUTPUT=$(OUTPUT_ROOT)binary/ -BUNDLE_MAKE=v=$v GOOS_OVERRIDE='GOOS=$(1) GOARCH=$(2)' PREFIX=$(3) make $(3)bin/$(BINNAME) +BUNDLE_MAKE=v=$v GOOS_OVERRIDE='GOOS=$(1) GOARCH=$(2)' PREFIX=$(3) make $(3)bin/$(BINNAME) $(3)bin/$(CLOUDKMS_BINNAME) RELEASE=./.travis-releases binary-linux: @@ -246,6 +258,7 @@ define BUNDLE newdir=$$TMP/$$stepName; \ mkdir -p $$newdir/bin; \ cp $(BINARY_OUTPUT)$(1)/bin/$(BINNAME) $$newdir/bin/; \ + cp $(BINARY_OUTPUT)$(1)/bin/$(CLOUDKMS_BINNAME) $$newdir/bin/; \ cp README.md $$newdir/; \ NEW_BUNDLE=$(RELEASE)/step-certificates_$(2)_$(1)_$(3).tar.gz; \ rm -f $$NEW_BUNDLE; \ diff --git a/cmd/step-cloudkms-init/main.go b/cmd/step-cloudkms-init/main.go new file mode 100644 index 00000000..0832d649 --- /dev/null +++ b/cmd/step-cloudkms-init/main.go @@ -0,0 +1,273 @@ +package main + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "math/big" + "os" + "strings" + "time" + + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/cloudkms" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/ui" + "github.com/smallstep/cli/utils" + "golang.org/x/crypto/ssh" +) + +func main() { + var credentialsFile string + var project, location, ring string + var protectionLevelName string + var ssh bool + flag.StringVar(&credentialsFile, "credentials-file", "", "Path to the `file` containing the Google's Cloud KMS credentials.") + flag.StringVar(&project, "project", "", "Google Cloud Project ID.") + flag.StringVar(&location, "location", "global", "Cloud KMS location name.") + flag.StringVar(&ring, "ring", "pki", "Cloud KMS ring name.") + flag.StringVar(&protectionLevelName, "protection-level", "SOFTWARE", "Protection level to use, SOFTWARE or HSM.") + flag.BoolVar(&ssh, "ssh", false, "Create SSH keys.") + flag.Usage = usage + flag.Parse() + + switch { + case project == "": + usage() + case location == "": + fmt.Fprintln(os.Stderr, "flag `--location` is required") + os.Exit(1) + case ring == "": + fmt.Fprintln(os.Stderr, "flag `--ring` is required") + os.Exit(1) + case protectionLevelName == "": + fmt.Fprintln(os.Stderr, "flag `--protection-level` is required") + os.Exit(1) + } + + var protectionLevel apiv1.ProtectionLevel + switch strings.ToUpper(protectionLevelName) { + case "SOFTWARE": + protectionLevel = apiv1.Software + case "HSM": + protectionLevel = apiv1.HSM + default: + fmt.Fprintf(os.Stderr, "invalid value `%s` for flag `--protection-level`; options are `SOFTWARE` or `HSM`\n", protectionLevelName) + os.Exit(1) + } + + c, err := cloudkms.New(context.Background(), apiv1.Options{ + Type: string(apiv1.CloudKMS), + CredentialsFile: credentialsFile, + }) + if err != nil { + fatal(err) + } + + if err := createPKI(c, project, location, ring, protectionLevel); err != nil { + fatal(err) + } + + if ssh { + ui.Println() + if err := createSSH(c, project, location, ring, protectionLevel); err != nil { + fatal(err) + } + } +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func usage() { + fmt.Fprintln(os.Stderr, "Usage: step-cloudkms-init --project ") + fmt.Fprintln(os.Stderr, ` +The step-cloudkms-init command initializes a public key infrastructure (PKI) +to be used by step-ca. + +This tool is experimental and in the future it will be integrated in step cli. + +OPTIONS`) + fmt.Fprintln(os.Stderr) + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, ` +COPYRIGHT + + (c) 2018-2020 Smallstep Labs, Inc.`) + os.Exit(1) +} + +func createPKI(c *cloudkms.CloudKMS, project, location, keyRing string, protectionLevel apiv1.ProtectionLevel) error { + ui.Println("Creating PKI ...") + + parent := "projects/" + project + "/locations/" + location + "/keyRings/" + keyRing + "/cryptoKeys" + + // Root Certificate + resp, err := c.CreateKey(&apiv1.CreateKeyRequest{ + Name: parent + "/root", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + ProtectionLevel: protectionLevel, + }) + if err != nil { + return err + } + + signer, err := c.CreateSigner(&resp.CreateSignerRequest) + if err != nil { + return err + } + + now := time.Now() + root := &x509.Certificate{ + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 1, + MaxPathLenZero: false, + Issuer: pkix.Name{CommonName: "Smallstep Root"}, + Subject: pkix.Name{CommonName: "Smallstep Root"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + } + + b, err := x509.CreateCertificate(rand.Reader, root, root, resp.PublicKey, signer) + if err != nil { + return err + } + + if err = utils.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: b, + }), 0600); err != nil { + return err + } + + ui.PrintSelected("Root Key", resp.Name) + ui.PrintSelected("Root Certificate", "root_ca.crt") + + root, err = pemutil.ReadCertificate("root_ca.crt") + if err != nil { + return err + } + + // Intermediate Certificate + resp, err = c.CreateKey(&apiv1.CreateKeyRequest{ + Name: parent + "/intermediate", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + ProtectionLevel: protectionLevel, + }) + if err != nil { + return err + } + + intermediate := &x509.Certificate{ + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 0, + MaxPathLenZero: true, + Issuer: pkix.Name{CommonName: "Smallstep Root"}, + Subject: pkix.Name{CommonName: "Smallstep Intermediate"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + } + + b, err = x509.CreateCertificate(rand.Reader, intermediate, root, resp.PublicKey, signer) + if err != nil { + return err + } + + if err = utils.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: b, + }), 0600); err != nil { + return err + } + + ui.PrintSelected("Intermediate Key", resp.Name) + ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt") + + return nil +} + +func createSSH(c *cloudkms.CloudKMS, project, location, keyRing string, protectionLevel apiv1.ProtectionLevel) error { + ui.Println("Creating SSH Keys ...") + + parent := "projects/" + project + "/locations/" + location + "/keyRings/" + keyRing + "/cryptoKeys" + + // User Key + resp, err := c.CreateKey(&apiv1.CreateKeyRequest{ + Name: parent + "/ssh-user-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + ProtectionLevel: protectionLevel, + }) + if err != nil { + return err + } + + key, err := ssh.NewPublicKey(resp.PublicKey) + if err != nil { + return err + } + + if err = utils.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + return err + } + + ui.PrintSelected("SSH User Public Key", "ssh_user_ca_key.pub") + ui.PrintSelected("SSH User Private Key", resp.Name) + + // Host Key + resp, err = c.CreateKey(&apiv1.CreateKeyRequest{ + Name: parent + "/ssh-host-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + ProtectionLevel: apiv1.Software, + }) + if err != nil { + return err + } + + key, err = ssh.NewPublicKey(resp.PublicKey) + if err != nil { + return err + } + + if err = utils.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + return err + } + + ui.PrintSelected("SSH Host Public Key", "ssh_host_ca_key.pub") + ui.PrintSelected("SSH Host Private Key", resp.Name) + + return nil +} + +func mustSerialNumber() *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + sn, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + panic(err) + } + return sn +} + +func mustSubjectKeyID(key crypto.PublicKey) []byte { + b, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + panic(err) + } + hash := sha1.Sum(b) + return hash[:] +} From 55e661bd26147e280c9da1060e6fa8b24bac34ef Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 18 Feb 2020 19:07:42 -0800 Subject: [PATCH 2/6] Add initial docs for cloud kms. --- docs/kms.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/kms.md diff --git a/docs/kms.md b/docs/kms.md new file mode 100644 index 00000000..f6ca2cfc --- /dev/null +++ b/docs/kms.md @@ -0,0 +1,67 @@ +# Key Management Services + +This document describes how to use a key management service or KMS to store the +private keys and sign certificates. + +Support for multiple KMS are planned, but currently the only supported one is +Google's Cloud KMS. + +## Google's Cloud KMS. + +[Cloud KMS](https://cloud.google.com/kms) is the Google's cloud-hosted KMS that +allows you to store the cryptographic keys, and sign certificates using their +infrastructure. Cloud KMS supports two different protection levels, SOFTWARE and +HSM. + +To configure Cloud KMS in your CA you need add the `"kms"` property to you +`ca.json`, and replace the property`"key"` with the Cloud KMS key name of your +intermediate key: + +```json +{ + ... + "key": "projects//locations/global/keyRings//cryptoKeys//cryptoKeyVersions/", + ... + "kms": { + "type": "cloudkms", + "credentialsFile": "path/to/credentials.json" + } +} +``` + +In a similar way, for SSH certificate, the SSH keys must be Cloud KMS names: + +```json +{ + ... + "ssh": { + "hostKey": "projects//locations/global/keyRings//cryptoKeys//cryptoKeyVersions/", + "userKey": "projects//locations/global/keyRings//cryptoKeys//cryptoKeyVersions/" + }, +} +``` + +Currently [step](https://github.com/smallstep/cli) does not provide an automatic +way to initialize the public key infrastructure (PKI) using Cloud KMS, but an +experimental tool named `step-cloudkms-init` is available for this use case. At +some point this tool will be integrated into `step` and it will be deleted. + +The use `step-cloudkms-init` just enable Cloud KMS and run: + +```sh +$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json +$ step-cloudkms-init --project your-project-id --ssh +Creating PKI ... +✔ Root Key: projects/test-kms/locations/global/keyRings/pki/cryptoKeys/root/cryptoKeyVersions/1 +✔ Root Certificate: root_ca.crt +✔ Intermediate Key: projects/mariano-kms/locations/global/keyRings/pki/cryptoKeys/intermediate/cryptoKeyVersions/1 +✔ Intermediate Certificate: intermediate_ca.crt + +Creating SSH Keys ... +✔ SSH User Public Key: ssh_user_ca_key.pub +✔ SSH User Private Key: projects/mariano-kms/locations/global/keyRings/pki/cryptoKeys/ssh-user-key/cryptoKeyVersions/1 +✔ SSH Host Public Key: ssh_host_ca_key.pub +✔ SSH Host Private Key: projects/mariano-kms/locations/global/keyRings/pki/cryptoKeys/ssh-host-key/cryptoKeyVersions/1 +``` + +See `step-cloudkms-init --help` for more options. From 8604c31818a8bc199e02949bec180f19567bed48 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 21 Feb 2020 10:51:43 -0800 Subject: [PATCH 3/6] Fix in documentation. --- docs/kms.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/kms.md b/docs/kms.md index f6ca2cfc..408df8de 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -46,7 +46,8 @@ way to initialize the public key infrastructure (PKI) using Cloud KMS, but an experimental tool named `step-cloudkms-init` is available for this use case. At some point this tool will be integrated into `step` and it will be deleted. -The use `step-cloudkms-init` just enable Cloud KMS and run: +To use `step-cloudkms-init` just enable Cloud KMS in your step-ca config and +run: ```sh $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json From 334d1915634e28f9fdcac3d387515e37ab4b1c37 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 21 Feb 2020 10:53:22 -0800 Subject: [PATCH 4/6] Fix docs. --- docs/kms.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/kms.md b/docs/kms.md index 408df8de..8902a140 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -46,8 +46,7 @@ way to initialize the public key infrastructure (PKI) using Cloud KMS, but an experimental tool named `step-cloudkms-init` is available for this use case. At some point this tool will be integrated into `step` and it will be deleted. -To use `step-cloudkms-init` just enable Cloud KMS in your step-ca config and -run: +To use `step-cloudkms-init` just enable Cloud KMS in your project and run: ```sh $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json From 32c2558b586bb3485ca02c90b9f5d1f08562a57d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 21 Feb 2020 10:55:42 -0800 Subject: [PATCH 5/6] Replace project in output. --- docs/kms.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/kms.md b/docs/kms.md index 8902a140..2b0ab768 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -52,16 +52,16 @@ To use `step-cloudkms-init` just enable Cloud KMS in your project and run: $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json $ step-cloudkms-init --project your-project-id --ssh Creating PKI ... -✔ Root Key: projects/test-kms/locations/global/keyRings/pki/cryptoKeys/root/cryptoKeyVersions/1 +✔ Root Key: projects/your-project-id/locations/global/keyRings/pki/cryptoKeys/root/cryptoKeyVersions/1 ✔ Root Certificate: root_ca.crt -✔ Intermediate Key: projects/mariano-kms/locations/global/keyRings/pki/cryptoKeys/intermediate/cryptoKeyVersions/1 +✔ Intermediate Key: projects/your-project-id/locations/global/keyRings/pki/cryptoKeys/intermediate/cryptoKeyVersions/1 ✔ Intermediate Certificate: intermediate_ca.crt Creating SSH Keys ... ✔ SSH User Public Key: ssh_user_ca_key.pub -✔ SSH User Private Key: projects/mariano-kms/locations/global/keyRings/pki/cryptoKeys/ssh-user-key/cryptoKeyVersions/1 +✔ SSH User Private Key: projects/your-project-id/locations/global/keyRings/pki/cryptoKeys/ssh-user-key/cryptoKeyVersions/1 ✔ SSH Host Public Key: ssh_host_ca_key.pub -✔ SSH Host Private Key: projects/mariano-kms/locations/global/keyRings/pki/cryptoKeys/ssh-host-key/cryptoKeyVersions/1 +✔ SSH Host Private Key: projects/your-project-id/locations/global/keyRings/pki/cryptoKeys/ssh-host-key/cryptoKeyVersions/1 ``` See `step-cloudkms-init --help` for more options. From 6b01128bccf17b466942f9f5e06c0b44c70d455a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 21 Feb 2020 11:14:11 -0800 Subject: [PATCH 6/6] Reference root.Subject instead of hardcoding it. --- cmd/step-cloudkms-init/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/step-cloudkms-init/main.go b/cmd/step-cloudkms-init/main.go index 0832d649..a09054c4 100644 --- a/cmd/step-cloudkms-init/main.go +++ b/cmd/step-cloudkms-init/main.go @@ -178,7 +178,7 @@ func createPKI(c *cloudkms.CloudKMS, project, location, keyRing string, protecti BasicConstraintsValid: true, MaxPathLen: 0, MaxPathLenZero: true, - Issuer: pkix.Name{CommonName: "Smallstep Root"}, + Issuer: root.Subject, Subject: pkix.Name{CommonName: "Smallstep Intermediate"}, SerialNumber: mustSerialNumber(), SubjectKeyId: mustSubjectKeyID(resp.PublicKey),