Add initial support for yubikey.

This commit is contained in:
Mariano Cano 2020-05-07 18:22:09 -07:00
parent 9f1d95d8bf
commit 6868190fff
7 changed files with 563 additions and 0 deletions

View file

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

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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:

View file

@ -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
}

View file

@ -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)
}

244
kms/yubikey/yubikey.go Normal file
View file

@ -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
}