Add tool to initialize pki in cloud kms.

This commit is contained in:
Mariano Cano 2020-02-18 19:07:12 -08:00
parent 8e882faf44
commit 1535e95d89
2 changed files with 289 additions and 3 deletions

View file

@ -1,5 +1,7 @@
PKG?=github.com/smallstep/certificates/cmd/step-ca PKG?=github.com/smallstep/certificates/cmd/step-ca
BINNAME?=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 # Set V to 1 for verbose output from the Makefile
Q=$(if $V,,@) Q=$(if $V,,@)
@ -56,17 +58,22 @@ GOFLAGS := CGO_ENABLED=0
download: download:
$Q go mod download $Q go mod download
build: $(PREFIX)bin/$(BINNAME) build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME)
@echo "Build Complete!" @echo "Build Complete!"
$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) $(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go)
$Q mkdir -p $(@D) $Q mkdir -p $(@D)
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) $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 # Target to force a build of step-ca without running tests
simple: simple:
$Q mkdir -p $(PREFIX)bin $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/$(BINNAME) $(LDFLAGS) $(PKG)
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(LDFLAGS) $(CLOUDKMS_PKG)
@echo "Build Complete!" @echo "Build Complete!"
.PHONY: download build simple .PHONY: download build simple
@ -113,11 +120,13 @@ lint:
INSTALL_PREFIX?=/usr/ 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/$(BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(BINNAME)
$Q install -D $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(CLOUDKMS_BINNAME)
uninstall: uninstall:
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME) $Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME)
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(CLOUDKMS_BINNAME)
.PHONY: install uninstall .PHONY: install uninstall
@ -129,6 +138,9 @@ clean:
ifneq ($(BINNAME),"") ifneq ($(BINNAME),"")
$Q rm -f bin/$(BINNAME) $Q rm -f bin/$(BINNAME)
endif endif
ifneq ($(CLOUDKMS_BINNAME),"")
$Q rm -f bin/$(CLOUDKMS_BINNAME)
endif
.PHONY: clean .PHONY: clean
@ -228,7 +240,7 @@ distclean: clean
################################################# #################################################
BINARY_OUTPUT=$(OUTPUT_ROOT)binary/ 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 RELEASE=./.travis-releases
binary-linux: binary-linux:
@ -246,6 +258,7 @@ define BUNDLE
newdir=$$TMP/$$stepName; \ newdir=$$TMP/$$stepName; \
mkdir -p $$newdir/bin; \ mkdir -p $$newdir/bin; \
cp $(BINARY_OUTPUT)$(1)/bin/$(BINNAME) $$newdir/bin/; \ cp $(BINARY_OUTPUT)$(1)/bin/$(BINNAME) $$newdir/bin/; \
cp $(BINARY_OUTPUT)$(1)/bin/$(CLOUDKMS_BINNAME) $$newdir/bin/; \
cp README.md $$newdir/; \ cp README.md $$newdir/; \
NEW_BUNDLE=$(RELEASE)/step-certificates_$(2)_$(1)_$(3).tar.gz; \ NEW_BUNDLE=$(RELEASE)/step-certificates_$(2)_$(1)_$(3).tar.gz; \
rm -f $$NEW_BUNDLE; \ rm -f $$NEW_BUNDLE; \

View file

@ -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 <name>")
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[:]
}