From 9f1d95d8bfd5cf7673ce13104e2e69e4caaf461e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 May 2020 18:21:11 -0700 Subject: [PATCH 01/16] Fix renew of certificate at the start of the server. --- ca/renew.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ca/renew.go b/ca/renew.go index 13f96ff7..7d574748 100644 --- a/ca/renew.go +++ b/ca/renew.go @@ -51,6 +51,7 @@ func NewTLSRenewer(cert *tls.Certificate, fn RenewFunc, opts ...tlsRenewerOption r := &TLSRenewer{ RenewCertificate: fn, cert: cert, + certNotAfter: cert.Leaf.NotAfter.Add(-1 * time.Minute), } for _, f := range opts { From 6868190fff9da9c40871e40d023e02a1698dc7f4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 May 2020 18:22:09 -0700 Subject: [PATCH 02/16] Add initial support for yubikey. --- cmd/step-yubikey-init/main.go | 289 ++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + kms/apiv1/options.go | 3 + kms/apiv1/requests.go | 14 ++ kms/kms.go | 10 ++ kms/yubikey/yubikey.go | 244 ++++++++++++++++++++++++++++ 7 files changed, 563 insertions(+) create mode 100644 cmd/step-yubikey-init/main.go create mode 100644 kms/yubikey/yubikey.go diff --git a/cmd/step-yubikey-init/main.go b/cmd/step-yubikey-init/main.go new file mode 100644 index 00000000..9a5370c0 --- /dev/null +++ b/cmd/step-yubikey-init/main.go @@ -0,0 +1,289 @@ +package main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "math/big" + "os" + "time" + + "github.com/pkg/errors" + + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/yubikey" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/ui" + "github.com/smallstep/cli/utils" +) + +type Config struct { + RootOnly bool + RootSlot string + CrtSlot string + RootFile string + KeyFile string + Pin string +} + +func (c *Config) Validate() error { + switch { + case c.RootFile != "" && c.KeyFile == "": + return errors.New("flag `--root` requires flag `--key`") + case c.KeyFile != "" && c.RootFile == "": + return errors.New("flag `--key` requires flag `--root`") + case c.RootOnly && c.RootFile != "": + return errors.New("flag `--root-only` is incompatible with flag `--root`") + case c.RootSlot == c.CrtSlot: + return errors.New("flat `--root-slot` and flag `--crt-slot` cannot be the same") + default: + return nil + } +} + +func main() { + var c Config + flag.BoolVar(&c.RootOnly, "root-only", false, "Slot only the root certificate and sign and intermediate.") + flag.StringVar(&c.RootSlot, "root-slot", "9a", "Slot to store the root certificate.") + flag.StringVar(&c.CrtSlot, "crt-slot", "9c", "Slot to store the intermediate certificate.") + flag.StringVar(&c.RootFile, "root", "", "Path to the root certificate to use.") + flag.StringVar(&c.KeyFile, "key", "", "Path to the root key to use.") + flag.Usage = usage + flag.Parse() + + if err := c.Validate(); err != nil { + fatal(err) + } + + pin, err := ui.PromptPassword("What is the YubiKey PIN?") + if err != nil { + fatal(err) + } + c.Pin = string(pin) + + k, err := yubikey.New(context.Background(), apiv1.Options{ + Type: string(apiv1.YubiKey), + Pin: c.Pin, + }) + if err != nil { + fatal(err) + } + + if err := createPKI(k, c); err != nil { + fatal(err) + } + + defer func() { + _ = k.Close() + }() +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func usage() { + fmt.Fprintln(os.Stderr, "Usage: step-yubikey-init") + fmt.Fprintln(os.Stderr, ` +The step-yubikey-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(k *yubikey.YubiKey, c Config) error { + var err error + ui.Println("Creating PKI ...") + now := time.Now() + + // Root Certificate + var signer crypto.Signer + var root *x509.Certificate + if c.RootFile != "" && c.KeyFile != "" { + root, err = pemutil.ReadCertificate(c.RootFile) + if err != nil { + return err + } + + key, err := pemutil.Read(c.KeyFile) + if err != nil { + return err + } + + var ok bool + if signer, ok = key.(crypto.Signer); !ok { + return errors.Errorf("key type '%T' does not implement a signer", key) + } + } else { + resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: c.RootSlot, + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + + signer, err = k.CreateSigner(&resp.CreateSignerRequest) + if err != nil { + return err + } + + template := &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: "YubiKey Smallstep Root"}, + Subject: pkix.Name{CommonName: "YubiKey Smallstep Root"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + } + + b, err := x509.CreateCertificate(rand.Reader, template, template, resp.PublicKey, signer) + if err != nil { + return err + } + + root, err = x509.ParseCertificate(b) + if err != nil { + return errors.Wrap(err, "error parsing root certificate") + } + + if err = k.StoreCertificate(&apiv1.StoreCertificateRequest{ + Name: c.RootSlot, + Certificate: root, + }); 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", "yubikey:slot-id="+resp.Name) + ui.PrintSelected("Root Certificate", "root_ca.crt") + } + + // Intermediate Certificate + var keyName string + var publicKey crypto.PublicKey + if c.RootOnly { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return errors.Wrap(err, "error creating intermediate public key") + } + + pass, err := ui.PromptPasswordGenerate("What do you want your password to be? [leave empty and we'll generate one]", + ui.WithRichPrompt()) + if err != nil { + return err + } + + _, err = pemutil.Serialize(priv, pemutil.WithPassword(pass), pemutil.ToFile("intermediate_ca_key", 0600)) + if err != nil { + return err + } + + publicKey = priv.Public() + } else { + resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: c.CrtSlot, + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + publicKey = resp.PublicKey + keyName = resp.Name + } + + template := &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: "YubiKey Smallstep Intermediate"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(publicKey), + } + + b, err := x509.CreateCertificate(rand.Reader, template, root, publicKey, signer) + if err != nil { + return err + } + + intermediate, err := x509.ParseCertificate(b) + if err != nil { + return errors.Wrap(err, "error parsing intermediate certificate") + } + + if err = k.StoreCertificate(&apiv1.StoreCertificateRequest{ + Name: c.CrtSlot, + Certificate: intermediate, + }); err != nil { + return err + } + + if err = utils.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: b, + }), 0600); err != nil { + return err + } + + if c.RootOnly { + ui.PrintSelected("Intermediate Key", "intermediate_ca_key") + } else { + ui.PrintSelected("Intermediate Key", "yubikey:slot-id="+keyName) + } + + ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt") + + 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/go.mod b/go.mod index 9cbf1418..c81244cf 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( cloud.google.com/go v0.51.0 github.com/Masterminds/sprig/v3 v3.0.0 github.com/go-chi/chi v4.0.2+incompatible + github.com/go-piv/piv-go v1.5.0 github.com/googleapis/gax-go/v2 v2.0.5 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect diff --git a/go.sum b/go.sum index 8bb4b771..7a9caaef 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTD github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-piv/piv-go v1.5.0 h1:UtHPfrJsZKY+Z3UIjmJLh6DY+KtmNOl/9b/zt4N81pM= +github.com/go-piv/piv-go v1.5.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index a46db037..62ca97e9 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -32,6 +32,8 @@ const ( AmazonKMS Type = "awskms" // PKCS11 is a KMS implementation using the PKCS11 standard. PKCS11 Type = "pkcs11" + // YubiKey is a KMS implementation using a YubiKey PIV. + YubiKey Type = "yubikey" ) type Options struct { @@ -56,6 +58,7 @@ func (o *Options) Validate() error { switch Type(strings.ToLower(o.Type)) { case DefaultKMS, SoftKMS, CloudKMS: + case YubiKey: case AmazonKMS: return ErrNotImplemented{"support for AmazonKMS is not yet implemented"} case PKCS11: diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go index 35c2fcae..bbee4cfc 100644 --- a/kms/apiv1/requests.go +++ b/kms/apiv1/requests.go @@ -2,6 +2,7 @@ package apiv1 import ( "crypto" + "crypto/x509" "fmt" ) @@ -124,3 +125,16 @@ type CreateSignerRequest struct { PublicKeyPEM []byte Password []byte } + +// LoadCertificateRequest is the parameter used in the LoadCertificate method of +// a CertificateManager. +type LoadCertificateRequest struct { + Name string +} + +// StoreCertificateRequest is the parameter used in the StoreCertificate method +// of a CertificateManager. +type StoreCertificateRequest struct { + Name string + Certificate *x509.Certificate +} diff --git a/kms/kms.go b/kms/kms.go index 209783e5..880bb9df 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -3,12 +3,14 @@ package kms import ( "context" "crypto" + "crypto/x509" "strings" "github.com/pkg/errors" "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/kms/cloudkms" "github.com/smallstep/certificates/kms/softkms" + "github.com/smallstep/certificates/kms/yubikey" ) // KeyManager is the interface implemented by all the KMS. @@ -19,6 +21,12 @@ type KeyManager interface { Close() error } +// CertificateManager is the interface implemented by the KMS that can load and store x509.Certificates. +type CertificateManager interface { + LoadCerticate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) + StoreCertificate(req *apiv1.StoreCertificateRequest) error +} + // New initializes a new KMS from the given type. func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { if err := opts.Validate(); err != nil { @@ -30,6 +38,8 @@ func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { return softkms.New(ctx, opts) case apiv1.CloudKMS: return cloudkms.New(ctx, opts) + case apiv1.YubiKey: + return yubikey.New(ctx, opts) default: return nil, errors.Errorf("unsupported kms type '%s'", opts.Type) } diff --git a/kms/yubikey/yubikey.go b/kms/yubikey/yubikey.go new file mode 100644 index 00000000..c435f676 --- /dev/null +++ b/kms/yubikey/yubikey.go @@ -0,0 +1,244 @@ +package yubikey + +import ( + "context" + "crypto" + "crypto/x509" + "net/url" + "strings" + + "github.com/go-piv/piv-go/piv" + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" +) + +// YubiKey implements the KMS interface on a YubiKey. +type YubiKey struct { + yk *piv.YubiKey + pin string +} + +// New initializes a new YubiKey. +// TODO(mariano): only one card is currently supported. +func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) { + cards, err := piv.Cards() + if err != nil { + return nil, err + } + if len(cards) == 0 { + return nil, errors.New("error detecting yubikey") + } + + yk, err := piv.Open(cards[0]) + if err != nil { + return nil, errors.Wrap(err, "error opening yubikey") + } + + return &YubiKey{ + yk: yk, + pin: opts.Pin, + }, nil +} + +// LoadCertificate implements kms.CertificateManager and loads a certificate +// from the YubiKey. +func (k *YubiKey) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) { + slot, err := getSlot(req.Name) + if err != nil { + return nil, err + } + + cert, err := k.yk.Certificate(slot) + if err != nil { + return nil, errors.Wrap(err, "error retrieving certificate") + } + + return cert, nil +} + +// StoreCertificate implements kms.CertificateManager and stores a certificate +// in the YubiKey. +func (k *YubiKey) StoreCertificate(req *apiv1.StoreCertificateRequest) error { + if req.Certificate == nil { + return errors.New("storeCertificateRequest 'Certificate' cannot be nil") + } + + slot, err := getSlot(req.Name) + if err != nil { + return err + } + + err = k.yk.SetCertificate(piv.DefaultManagementKey, slot, req.Certificate) + if err != nil { + return errors.Wrap(err, "error storing certificate") + } + + return nil +} + +// GetPublicKey returns the public key present in the YubiKey signature slot. +func (k *YubiKey) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + slot, err := getSlot(req.Name) + if err != nil { + return nil, err + } + + cert, err := k.yk.Certificate(slot) + if err != nil { + return nil, errors.Wrap(err, "error retrieving certificate") + } + + return cert.PublicKey, nil +} + +// CreateKey generates a new key in the YubiKey and returns the public key. +func (k *YubiKey) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + alg, err := getSignatureAlgorithm(req.SignatureAlgorithm, req.Bits) + if err != nil { + return nil, err + } + slot, name, err := getSlotAndName(req.Name) + if err != nil { + return nil, err + } + + pub, err := k.yk.GenerateKey(piv.DefaultManagementKey, slot, piv.Key{ + Algorithm: alg, + PINPolicy: piv.PINPolicyAlways, + TouchPolicy: piv.TouchPolicyNever, + }) + if err != nil { + return nil, errors.Wrap(err, "error generating key") + } + return &apiv1.CreateKeyResponse{ + Name: name, + PublicKey: pub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: name, + }, + }, nil +} + +// CreateSigner creates a signer using the key present in the YubiKey signature +// slot. +func (k *YubiKey) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + slot, err := getSlot(req.SigningKey) + if err != nil { + return nil, err + } + + cert, err := k.yk.Certificate(slot) + if err != nil { + return nil, errors.Wrap(err, "error retrieving certificate") + } + + priv, err := k.yk.PrivateKey(slot, cert.PublicKey, piv.KeyAuth{ + PIN: k.pin, + }) + if err != nil { + return nil, errors.Wrap(err, "error retrieving private key") + } + + signer, ok := priv.(crypto.Signer) + if !ok { + return nil, errors.New("private key is not a crypto.Signer") + } + return signer, nil +} + +// Close releases the connection to the YubiKey. +func (k *YubiKey) Close() error { + return errors.Wrap(k.yk.Close(), "error closing yubikey") +} + +// signatureAlgorithmMapping is a mapping between the step signature algorithm, +// and bits for RSA keys, with yubikey ones. +var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ + apiv1.UnspecifiedSignAlgorithm: piv.AlgorithmEC256, + apiv1.SHA256WithRSA: map[int]piv.Algorithm{ + 0: piv.AlgorithmRSA2048, + 1024: piv.AlgorithmRSA1024, + 2048: piv.AlgorithmRSA2048, + }, + apiv1.SHA512WithRSA: map[int]piv.Algorithm{ + 0: piv.AlgorithmRSA2048, + 1024: piv.AlgorithmRSA1024, + 2048: piv.AlgorithmRSA2048, + }, + apiv1.SHA256WithRSAPSS: map[int]piv.Algorithm{ + 0: piv.AlgorithmRSA2048, + 1024: piv.AlgorithmRSA1024, + 2048: piv.AlgorithmRSA2048, + }, + apiv1.SHA512WithRSAPSS: map[int]piv.Algorithm{ + 0: piv.AlgorithmRSA2048, + 1024: piv.AlgorithmRSA1024, + 2048: piv.AlgorithmRSA2048, + }, + apiv1.ECDSAWithSHA256: piv.AlgorithmEC256, + apiv1.ECDSAWithSHA384: piv.AlgorithmEC384, +} + +func getSignatureAlgorithm(alg apiv1.SignatureAlgorithm, bits int) (piv.Algorithm, error) { + v, ok := signatureAlgorithmMapping[alg] + if !ok { + return 0, errors.Errorf("YubiKey does not support signature algorithm '%s'", alg) + } + + switch v := v.(type) { + case piv.Algorithm: + return v, nil + case map[int]piv.Algorithm: + signatureAlgorithm, ok := v[bits] + if !ok { + return 0, errors.Errorf("YubiKey does not support signature algorithm '%s' with '%d' bits", alg, bits) + } + return signatureAlgorithm, nil + default: + return 0, errors.Errorf("unexpected error: this should not happen") + } +} + +var slotMapping = map[string]piv.Slot{ + "9a": piv.SlotAuthentication, + "9c": piv.SlotSignature, + "9e": piv.SlotCardAuthentication, + "9d": piv.SlotKeyManagement, +} + +func getSlot(name string) (piv.Slot, error) { + slot, _, err := getSlotAndName(name) + return slot, err +} + +func getSlotAndName(name string) (piv.Slot, string, error) { + if name == "" { + return piv.SlotSignature, "yubikey:slot-id=9c", nil + } + + var slotID string + name = strings.ToLower(name) + if strings.HasPrefix(name, "yubikey:") { + u, err := url.Parse(name) + if err != nil { + return piv.Slot{}, "", errors.Wrapf(err, "error parsing '%s'", name) + } + v, err := url.ParseQuery(u.Opaque) + if err != nil { + return piv.Slot{}, "", errors.Wrapf(err, "error parsing '%s'", name) + } + if slotID = v.Get("slot-id"); slotID == "" { + return piv.Slot{}, "", errors.Wrapf(err, "error parsing '%s': slot-id is missing", name) + } + } else { + slotID = name + } + + s, ok := slotMapping[slotID] + if !ok { + return piv.Slot{}, "", errors.Errorf("usupported slot-id '%s'", name) + } + + name = "yubikey:slot-id=" + url.QueryEscape(slotID) + return s, name, nil +} From 677e99793e9f338cffca4a15f798524cc2375ce5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 May 2020 18:35:10 -0700 Subject: [PATCH 03/16] Create alternative yubikey file when cgo is not enabled. This yubikey will always fail. --- kms/yubikey/yubikey.go | 2 ++ kms/yubikey/yubikey_no_cgo.go | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 kms/yubikey/yubikey_no_cgo.go diff --git a/kms/yubikey/yubikey.go b/kms/yubikey/yubikey.go index c435f676..f97a677c 100644 --- a/kms/yubikey/yubikey.go +++ b/kms/yubikey/yubikey.go @@ -1,3 +1,5 @@ +// +build cgo + package yubikey import ( diff --git a/kms/yubikey/yubikey_no_cgo.go b/kms/yubikey/yubikey_no_cgo.go new file mode 100644 index 00000000..2d6f7bfa --- /dev/null +++ b/kms/yubikey/yubikey_no_cgo.go @@ -0,0 +1,52 @@ +// +build !cgo + +package yubikey + +import ( + "context" + "crypto" + "crypto/x509" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" +) + +// +build !cgo + +// YubiKey implements the KMS interface on a YubiKey. +type YubiKey struct{} + +// New always fails without CGO. +func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) { + return nil, errors.New("YubiKey is not supported without cgo") +} + +// LoadCertificate always fails without CGO. +func (k *YubiKey) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) { + return nil, errors.New("YubiKey is not supported without cgo") +} + +// StoreCertificate always fails without CGO. +func (k *YubiKey) StoreCertificate(req *apiv1.StoreCertificateRequest) error { + return errors.New("YubiKey is not supported without cgo") +} + +// GetPublicKey always fails without CGO. +func (k *YubiKey) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + return nil, errors.New("YubiKey is not supported without cgo") +} + +// CreateKey always fails without CGO. +func (k *YubiKey) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + return nil, errors.New("YubiKey is not supported without cgo") +} + +// CreateSigner always fails without CGO. +func (k *YubiKey) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + return nil, errors.New("YubiKey is not supported without cgo") +} + +// Close always fails without CGO. +func (k *YubiKey) Close() error { + return errors.New("YubiKey is not supported without cgo") +} From ef2b13b163950502d30af8cd2ef7219bf275e084 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 May 2020 18:40:36 -0700 Subject: [PATCH 04/16] Add step-yubikey-init as a target. --- Makefile | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index a86ea0ee..1cc94894 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ 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 +YUBIKEY_BINNAME?=step-yubikey-init +YUBIKEY_PKG?=github.com/smallstep/certificates/cmd/step-yubikey-init # Set V to 1 for verbose output from the Makefile Q=$(if $V,,@) @@ -64,7 +66,7 @@ GOFLAGS := CGO_ENABLED=0 download: $Q go mod download -build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) +build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) @echo "Build Complete!" $(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) @@ -75,12 +77,12 @@ $(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) +$(PREFIX)bin/$(YUBIKEY_BINNAME): download $(call rwildcard,*.go) + $Q mkdir -p $(@D) + $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(YUBIKEY_BINNAME) $(LDFLAGS) $(YUBIKEY_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!" +simple: build .PHONY: download build simple From 029483463b72307e2a196a441b955c9a5b568114 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 May 2020 18:51:41 -0700 Subject: [PATCH 05/16] Remove extra +build statement. --- kms/yubikey/yubikey_no_cgo.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/kms/yubikey/yubikey_no_cgo.go b/kms/yubikey/yubikey_no_cgo.go index 2d6f7bfa..916cf151 100644 --- a/kms/yubikey/yubikey_no_cgo.go +++ b/kms/yubikey/yubikey_no_cgo.go @@ -11,8 +11,6 @@ import ( "github.com/smallstep/certificates/kms/apiv1" ) -// +build !cgo - // YubiKey implements the KMS interface on a YubiKey. type YubiKey struct{} From c02fe779983313ff0b9a3d08501553b2fa45f47c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 7 May 2020 18:59:30 -0700 Subject: [PATCH 06/16] Close the key manager before shutting down. --- authority/authority.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/authority/authority.go b/authority/authority.go index 828adf2f..cdf37953 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "log" "sync" "time" @@ -327,5 +328,8 @@ func (a *Authority) GetDatabase() db.AuthDB { // Shutdown safely shuts down any clients, databases, etc. held by the Authority. func (a *Authority) Shutdown() error { + if err := a.keyManager.Close(); err != nil { + log.Printf("error closing the key manager: %v", err) + } return a.db.Shutdown() } From 63e36ecd7a860ca847b8fd7c20dd0cc761d9638e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 May 2020 18:47:22 -0700 Subject: [PATCH 07/16] Refactor the initialization of KeyManagers. --- kms/apiv1/options.go | 17 +++++++++++++ kms/apiv1/registry.go | 27 +++++++++++++++++++++ kms/cloudkms/cloudkms.go | 6 +++++ kms/kms.go | 36 ++++++++++------------------ kms/softkms/softkms.go | 6 +++++ kms/yubikey/yubikey.go | 6 +++++ kms/yubikey/yubikey_no_cgo.go | 45 ++++++----------------------------- 7 files changed, 81 insertions(+), 62 deletions(-) create mode 100644 kms/apiv1/registry.go diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index 62ca97e9..c136e3f4 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -1,11 +1,28 @@ package apiv1 import ( + "crypto" + "crypto/x509" "strings" "github.com/pkg/errors" ) +// KeyManager is the interface implemented by all the KMS. +type KeyManager interface { + GetPublicKey(req *GetPublicKeyRequest) (crypto.PublicKey, error) + CreateKey(req *CreateKeyRequest) (*CreateKeyResponse, error) + CreateSigner(req *CreateSignerRequest) (crypto.Signer, error) + Close() error +} + +// CertificateManager is the interface implemented by the KMS that can load and +// store x509.Certificates. +type CertificateManager interface { + LoadCerticate(req *LoadCertificateRequest) (*x509.Certificate, error) + StoreCertificate(req *StoreCertificateRequest) error +} + // ErrNotImplemented type ErrNotImplemented struct { msg string diff --git a/kms/apiv1/registry.go b/kms/apiv1/registry.go new file mode 100644 index 00000000..5a8cf4db --- /dev/null +++ b/kms/apiv1/registry.go @@ -0,0 +1,27 @@ +package apiv1 + +import ( + "context" + "sync" +) + +var registry = new(sync.Map) + +// KeyManagerNewFunc is the type that represents the method to initialize a new +// KeyManager. +type KeyManagerNewFunc func(ctx context.Context, opts Options) (KeyManager, error) + +// Register adds to the registry a method to create a KeyManager of type t. +func Register(t Type, fn KeyManagerNewFunc) { + registry.Store(t, fn) +} + +// LoadKeyManagerNewFunc returns the function initialize a KayManager. +func LoadKeyManagerNewFunc(t Type) (KeyManagerNewFunc, bool) { + v, ok := registry.Load(t) + if !ok { + return nil, false + } + fn, ok := v.(KeyManagerNewFunc) + return fn, ok +} diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index d434ce48..01cbcab2 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -93,6 +93,12 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) { }, nil } +func init() { + apiv1.Register(apiv1.CloudKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + // NewCloudKMS creates a CloudKMS with a given client. func NewCloudKMS(client KeyManagementClient) *CloudKMS { return &CloudKMS{ diff --git a/kms/kms.go b/kms/kms.go index 880bb9df..8daf148f 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -2,30 +2,18 @@ package kms import ( "context" - "crypto" - "crypto/x509" "strings" "github.com/pkg/errors" "github.com/smallstep/certificates/kms/apiv1" - "github.com/smallstep/certificates/kms/cloudkms" - "github.com/smallstep/certificates/kms/softkms" - "github.com/smallstep/certificates/kms/yubikey" ) // KeyManager is the interface implemented by all the KMS. -type KeyManager interface { - GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) - CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) - CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) - Close() error -} +type KeyManager = apiv1.KeyManager -// CertificateManager is the interface implemented by the KMS that can load and store x509.Certificates. -type CertificateManager interface { - LoadCerticate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) - StoreCertificate(req *apiv1.StoreCertificateRequest) error -} +// CertificateManager is the interface implemented by the KMS that can load and +// store x509.Certificates. +type CertificateManager = apiv1.CertificateManager // New initializes a new KMS from the given type. func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { @@ -33,14 +21,14 @@ func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { return nil, err } - switch apiv1.Type(strings.ToLower(opts.Type)) { - case apiv1.DefaultKMS, apiv1.SoftKMS: - return softkms.New(ctx, opts) - case apiv1.CloudKMS: - return cloudkms.New(ctx, opts) - case apiv1.YubiKey: - return yubikey.New(ctx, opts) - default: + t := apiv1.Type(strings.ToLower(opts.Type)) + if t == apiv1.DefaultKMS { + t = apiv1.SoftKMS + } + + fn, ok := apiv1.LoadKeyManagerNewFunc(t) + if !ok { return nil, errors.Errorf("unsupported kms type '%s'", opts.Type) } + return fn(ctx, opts) } diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go index fb38a1c5..3db9cbcc 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -52,6 +52,12 @@ func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) { return &SoftKMS{}, nil } +func init() { + apiv1.Register(apiv1.SoftKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + // Close is a noop that just returns nil. func (k *SoftKMS) Close() error { return nil diff --git a/kms/yubikey/yubikey.go b/kms/yubikey/yubikey.go index f97a677c..adde5fe6 100644 --- a/kms/yubikey/yubikey.go +++ b/kms/yubikey/yubikey.go @@ -42,6 +42,12 @@ func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) { }, nil } +func init() { + apiv1.Register(apiv1.YubiKey, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + // LoadCertificate implements kms.CertificateManager and loads a certificate // from the YubiKey. func (k *YubiKey) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) { diff --git a/kms/yubikey/yubikey_no_cgo.go b/kms/yubikey/yubikey_no_cgo.go index 916cf151..6ed7c630 100644 --- a/kms/yubikey/yubikey_no_cgo.go +++ b/kms/yubikey/yubikey_no_cgo.go @@ -4,47 +4,16 @@ package yubikey import ( "context" - "crypto" - "crypto/x509" + "os" + "path/filepath" "github.com/pkg/errors" "github.com/smallstep/certificates/kms/apiv1" ) -// YubiKey implements the KMS interface on a YubiKey. -type YubiKey struct{} - -// New always fails without CGO. -func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) { - return nil, errors.New("YubiKey is not supported without cgo") -} - -// LoadCertificate always fails without CGO. -func (k *YubiKey) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) { - return nil, errors.New("YubiKey is not supported without cgo") -} - -// StoreCertificate always fails without CGO. -func (k *YubiKey) StoreCertificate(req *apiv1.StoreCertificateRequest) error { - return errors.New("YubiKey is not supported without cgo") -} - -// GetPublicKey always fails without CGO. -func (k *YubiKey) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { - return nil, errors.New("YubiKey is not supported without cgo") -} - -// CreateKey always fails without CGO. -func (k *YubiKey) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { - return nil, errors.New("YubiKey is not supported without cgo") -} - -// CreateSigner always fails without CGO. -func (k *YubiKey) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { - return nil, errors.New("YubiKey is not supported without cgo") -} - -// Close always fails without CGO. -func (k *YubiKey) Close() error { - return errors.New("YubiKey is not supported without cgo") +func init() { + apiv1.Register(apiv1.YubiKey, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + name := filepath.Base(os.Args[0]) + return nil, errors.Errorf("unsupported kms type 'yubikey': %s is compiled without cgo support", name) + }) } From 22b86c3fccb051d9bbfc9ffa6bc7a3b9233adfe7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 May 2020 19:40:12 -0700 Subject: [PATCH 08/16] Only rewrite keys with --force. --- cmd/step-yubikey-init/main.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/cmd/step-yubikey-init/main.go b/cmd/step-yubikey-init/main.go index 9a5370c0..5bac830d 100644 --- a/cmd/step-yubikey-init/main.go +++ b/cmd/step-yubikey-init/main.go @@ -17,7 +17,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/kms/yubikey" "github.com/smallstep/cli/crypto/pemutil" @@ -32,6 +31,7 @@ type Config struct { RootFile string KeyFile string Pin string + Force bool } func (c *Config) Validate() error { @@ -43,8 +43,16 @@ func (c *Config) Validate() error { case c.RootOnly && c.RootFile != "": return errors.New("flag `--root-only` is incompatible with flag `--root`") case c.RootSlot == c.CrtSlot: - return errors.New("flat `--root-slot` and flag `--crt-slot` cannot be the same") + return errors.New("flag `--root-slot` and flag `--crt-slot` cannot be the same") + case c.RootFile == "" && c.RootSlot == "": + return errors.New("one of flag `--root` or `--root-slot` is required") default: + if c.RootFile != "" { + c.RootSlot = "" + } + if c.RootOnly { + c.CrtSlot = "" + } return nil } } @@ -56,6 +64,7 @@ func main() { flag.StringVar(&c.CrtSlot, "crt-slot", "9c", "Slot to store the intermediate certificate.") flag.StringVar(&c.RootFile, "root", "", "Path to the root certificate to use.") flag.StringVar(&c.KeyFile, "key", "", "Path to the root key to use.") + flag.BoolVar(&c.Force, "force", false, "Force the delete of previous keys.") flag.Usage = usage flag.Parse() @@ -77,6 +86,16 @@ func main() { fatal(err) } + // Check if the slots are empty, fail if they are not + if !c.Force { + switch { + case c.RootSlot != "": + checkSlot(k, c.RootSlot) + case c.CrtSlot != "": + checkSlot(k, c.CrtSlot) + } + } + if err := createPKI(k, c); err != nil { fatal(err) } @@ -109,6 +128,16 @@ COPYRIGHT os.Exit(1) } +func checkSlot(k *yubikey.YubiKey, slot string) { + if _, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{ + Name: slot, + }); err == nil { + fmt.Fprintf(os.Stderr, "⚠️ Your YubiKey already has a key in the slot %s.\n", slot) + fmt.Fprintln(os.Stderr, " If you want to delete it and start fresh, use `--force`.") + os.Exit(1) + } +} + func createPKI(k *yubikey.YubiKey, c Config) error { var err error ui.Println("Creating PKI ...") From 025c0aa20f4ac4506bc2a79b5c79506b777ffb46 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 May 2020 19:42:21 -0700 Subject: [PATCH 09/16] Display the proper yubikey uri. --- cmd/step-yubikey-init/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/step-yubikey-init/main.go b/cmd/step-yubikey-init/main.go index 5bac830d..c79e85af 100644 --- a/cmd/step-yubikey-init/main.go +++ b/cmd/step-yubikey-init/main.go @@ -213,7 +213,7 @@ func createPKI(k *yubikey.YubiKey, c Config) error { return err } - ui.PrintSelected("Root Key", "yubikey:slot-id="+resp.Name) + ui.PrintSelected("Root Key", resp.Name) ui.PrintSelected("Root Certificate", "root_ca.crt") } @@ -291,7 +291,7 @@ func createPKI(k *yubikey.YubiKey, c Config) error { if c.RootOnly { ui.PrintSelected("Intermediate Key", "intermediate_ca_key") } else { - ui.PrintSelected("Intermediate Key", "yubikey:slot-id="+keyName) + ui.PrintSelected("Intermediate Key", keyName) } ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt") From 7d61c0003ced2bcad1649aa57cf4a18bccfcee1d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 15 May 2020 11:32:12 -0700 Subject: [PATCH 10/16] Enable softkms and cloudkms. --- kms/kms.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/kms/kms.go b/kms/kms.go index 8daf148f..310214f0 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -6,6 +6,13 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/kms/apiv1" + + // Enabled kms interfaces. + _ "github.com/smallstep/certificates/kms/cloudkms" + _ "github.com/smallstep/certificates/kms/softkms" + + // Experimental kms interfaces. + _ "github.com/smallstep/certificates/kms/yubikey" ) // KeyManager is the interface implemented by all the KMS. @@ -28,7 +35,7 @@ func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { fn, ok := apiv1.LoadKeyManagerNewFunc(t) if !ok { - return nil, errors.Errorf("unsupported kms type '%s'", opts.Type) + return nil, errors.Errorf("unsupported kms type '%s'", t) } return fn(ctx, opts) } From 012a4734bfde52d4ce70c2e84e80a7f25bca132e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 15 May 2020 11:33:04 -0700 Subject: [PATCH 11/16] Add better messaging when yubikey is not detected. --- kms/yubikey/yubikey.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kms/yubikey/yubikey.go b/kms/yubikey/yubikey.go index adde5fe6..28c41f95 100644 --- a/kms/yubikey/yubikey.go +++ b/kms/yubikey/yubikey.go @@ -28,7 +28,7 @@ func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) { return nil, err } if len(cards) == 0 { - return nil, errors.New("error detecting yubikey") + return nil, errors.New("error detecting yubikey: try removing and reconnecting the device") } yk, err := piv.Open(cards[0]) From 03a6789f0e44886c0a6b16dd330a7d43e550aa0b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 15 May 2020 11:33:22 -0700 Subject: [PATCH 12/16] Fix compile errors without cgo support. --- cmd/step-yubikey-init/main.go | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/cmd/step-yubikey-init/main.go b/cmd/step-yubikey-init/main.go index c79e85af..b4c1f97b 100644 --- a/cmd/step-yubikey-init/main.go +++ b/cmd/step-yubikey-init/main.go @@ -17,8 +17,8 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/kms" "github.com/smallstep/certificates/kms/apiv1" - "github.com/smallstep/certificates/kms/yubikey" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/ui" "github.com/smallstep/cli/utils" @@ -78,7 +78,7 @@ func main() { } c.Pin = string(pin) - k, err := yubikey.New(context.Background(), apiv1.Options{ + k, err := kms.New(context.Background(), apiv1.Options{ Type: string(apiv1.YubiKey), Pin: c.Pin, }) @@ -128,7 +128,7 @@ COPYRIGHT os.Exit(1) } -func checkSlot(k *yubikey.YubiKey, slot string) { +func checkSlot(k kms.KeyManager, slot string) { if _, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{ Name: slot, }); err == nil { @@ -138,7 +138,7 @@ func checkSlot(k *yubikey.YubiKey, slot string) { } } -func createPKI(k *yubikey.YubiKey, c Config) error { +func createPKI(k kms.KeyManager, c Config) error { var err error ui.Println("Creating PKI ...") now := time.Now() @@ -199,11 +199,13 @@ func createPKI(k *yubikey.YubiKey, c Config) error { return errors.Wrap(err, "error parsing root certificate") } - if err = k.StoreCertificate(&apiv1.StoreCertificateRequest{ - Name: c.RootSlot, - Certificate: root, - }); err != nil { - return err + if cm, ok := k.(kms.CertificateManager); ok { + if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + Name: c.RootSlot, + Certificate: root, + }); err != nil { + return err + } } if err = utils.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ @@ -274,11 +276,13 @@ func createPKI(k *yubikey.YubiKey, c Config) error { return errors.Wrap(err, "error parsing intermediate certificate") } - if err = k.StoreCertificate(&apiv1.StoreCertificateRequest{ - Name: c.CrtSlot, - Certificate: intermediate, - }); err != nil { - return err + if cm, ok := k.(kms.CertificateManager); ok { + if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + Name: c.CrtSlot, + Certificate: intermediate, + }); err != nil { + return err + } } if err = utils.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ From d95c055163416494e589c8820f87d02041ffefc7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 15 May 2020 11:40:26 -0700 Subject: [PATCH 13/16] piv-go requires libpcsclite-dev on linux. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1be30e69..665470ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ addons: - debhelper - fakeroot - bash-completion + - libpcsclite-dev env: global: - V=1 From 3e40cb89a72a5410b0243bb102a95be990e00654 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 15 May 2020 12:24:25 -0700 Subject: [PATCH 14/16] Add some docs for YubiKey configuration. --- docs/kms.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/kms.md b/docs/kms.md index 2b0ab768..76412081 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -6,7 +6,7 @@ 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. +## 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 @@ -65,3 +65,76 @@ Creating SSH Keys ... ``` See `step-cloudkms-init --help` for more options. + +## YubiKey + +And incomplete and experimental support for [YubiKeys](https://www.yubico.com) +is also available. Support for YubiKeys is not enabled by default and only TLS +signing can be configured. + +The YubiKey implementation requires cgo, and our build system does not produce +binaries with it. To enable YubiKey download the source code and run: + +```sh +make build GOFLAGS="" +``` + +The implementation uses [piv-go](https://github.com/go-piv/piv-go), and it +requires PCSC support, this is available by default on macOS and Windows +operating systems, but on Linux piv-go requires PCSC lite. + +To install on Debian-based distributions, run: + +```sh +sudo apt-get install libpcsclite-dev +``` + +On Fedora: + +```sh +sudo yum install pcsc-lite-devel +``` + +On CentOS: + +```sh +sudo yum install 'dnf-command(config-manager)' +sudo yum config-manager --set-enabled PowerTools +sudo yum install pcsc-lite-devel +``` + +The initialization of the public key infrastructure (PKI) for YubiKeys, is not +currently integrated into [step](https://github.com/smallstep/cli), but an +experimental tool named `step-yubikey-init` is available for this use case. At +some point this tool will be integrated into `step` and it will be deleted. + +To configure your YubiKey just run: + +```sh +$ bin/step-yubikey-init +What is the YubiKey PIN?: +Creating PKI ... +✔ Root Key: yubikey:slot-id=9a +✔ Root Certificate: root_ca.crt +✔ Intermediate Key: yubikey:slot-id=9c +✔ Intermediate Certificate: intermediate_ca.crt +``` + +See `step-yubikey-init --help` for more options. + +Finally to enable it in the ca.json, point the `root` and `crt` to the generated +certificates, set the `key` with the yubikey URI generated in the previous step +and configure the `kms` property with the `type` and your `pin` in it. + +```json +{ + "root": "/path/to/root_ca.crt", + "crt": "/path/to/intermediate_ca.crt", + "key": "yubikey:slot-id=9c", + "kms": { + "type": "yubikey", + "pin": "123456" + }, + ... +} +``` From 97508ca21521c261061abaa52bc523a929ada0cf Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 19 May 2020 13:05:55 -0700 Subject: [PATCH 15/16] Add AuthorityKeyId to root certificate. Fix error string. --- cmd/step-yubikey-init/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/step-yubikey-init/main.go b/cmd/step-yubikey-init/main.go index b4c1f97b..dc486f4d 100644 --- a/cmd/step-yubikey-init/main.go +++ b/cmd/step-yubikey-init/main.go @@ -187,6 +187,7 @@ func createPKI(k kms.KeyManager, c Config) error { Subject: pkix.Name{CommonName: "YubiKey Smallstep Root"}, SerialNumber: mustSerialNumber(), SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + AuthorityKeyId: mustSubjectKeyID(resp.PublicKey), } b, err := x509.CreateCertificate(rand.Reader, template, template, resp.PublicKey, signer) @@ -225,7 +226,7 @@ func createPKI(k kms.KeyManager, c Config) error { if c.RootOnly { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return errors.Wrap(err, "error creating intermediate public key") + return errors.Wrap(err, "error creating intermediate key") } pass, err := ui.PromptPasswordGenerate("What do you want your password to be? [leave empty and we'll generate one]", From 89e164dad6f2a281f074b7ea76108117d4881227 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 19 May 2020 13:15:09 -0700 Subject: [PATCH 16/16] Add AuthorityKeyId to cloudkms root cert. --- cmd/step-cloudkms-init/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/step-cloudkms-init/main.go b/cmd/step-cloudkms-init/main.go index a09054c4..eb23b048 100644 --- a/cmd/step-cloudkms-init/main.go +++ b/cmd/step-cloudkms-init/main.go @@ -138,6 +138,7 @@ func createPKI(c *cloudkms.CloudKMS, project, location, keyRing string, protecti Subject: pkix.Name{CommonName: "Smallstep Root"}, SerialNumber: mustSerialNumber(), SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + AuthorityKeyId: mustSubjectKeyID(resp.PublicKey), } b, err := x509.CreateCertificate(rand.Reader, root, root, resp.PublicKey, signer)