forked from TrueCloudLab/certificates
commit
806abb6232
3 changed files with 356 additions and 3 deletions
19
Makefile
19
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; \
|
||||
|
|
273
cmd/step-cloudkms-init/main.go
Normal file
273
cmd/step-cloudkms-init/main.go
Normal 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: 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[:]
|
||||
}
|
67
docs/kms.md
Normal file
67
docs/kms.md
Normal file
|
@ -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/<project-id>/locations/global/keyRings/<ring-id>/cryptoKeys/<key-id>/cryptoKeyVersions/<version-number>",
|
||||
...
|
||||
"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/<project-id>/locations/global/keyRings/<ring-id>/cryptoKeys/<key-id>/cryptoKeyVersions/<version-number>",
|
||||
"userKey": "projects/<project-id>/locations/global/keyRings/<ring-id>/cryptoKeys/<key-id>/cryptoKeyVersions/<version-number>"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
Loading…
Reference in a new issue