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..a09054c4 --- /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: root.Subject, + 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[:] +} diff --git a/docs/kms.md b/docs/kms.md new file mode 100644 index 00000000..2b0ab768 --- /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. + +To use `step-cloudkms-init` just enable Cloud KMS in your project and run: + +```sh +$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json +$ step-cloudkms-init --project your-project-id --ssh +Creating PKI ... +✔ Root Key: projects/your-project-id/locations/global/keyRings/pki/cryptoKeys/root/cryptoKeyVersions/1 +✔ Root Certificate: root_ca.crt +✔ 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/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/your-project-id/locations/global/keyRings/pki/cryptoKeys/ssh-host-key/cryptoKeyVersions/1 +``` + +See `step-cloudkms-init --help` for more options.