smallstep-certificates/kms/yubikey/yubikey.go
Herman Slatman 8c5b12e21d
Add non-TLS server and improve crypto.Decrypter interface
A server without TLS was added to serve the SCEP endpoints. According
to the RFC, SCEP has to be served via HTTP. The `sscep` client, for
example, will stop any URL that does not start with `http://` from
being used, so serving SCEP seems to be the right way to do it.

This commit adds a second server for which no TLS configuration is
configured. A distinct field in the configuration, `insecureAddress`
was added to specify the address for the insecure server.

The SCEP endpoints will also still be served via HTTPS. Some clients
may be able to work with that.

This commit also improves how the crypto.Decrypter interface is
handled for the different types of KMSes supported by step. The
apiv1.Decrypter interface was added. Currently only SoftKMS
implements this interface, providing a crypto.Decrypter required
for SCEP operations.
2021-03-12 14:18:36 +01:00

322 lines
8.4 KiB
Go

// +build cgo
package yubikey
import (
"context"
"crypto"
"crypto/x509"
"encoding/hex"
"net/url"
"strings"
"github.com/go-piv/piv-go/piv"
"github.com/pkg/errors"
"github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/uri"
)
// Scheme is the scheme used in uris.
const Scheme = "yubikey"
// YubiKey implements the KMS interface on a YubiKey.
type YubiKey struct {
yk *piv.YubiKey
pin string
managementKey [24]byte
}
// New initializes a new YubiKey.
// TODO(mariano): only one card is currently supported.
func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) {
managementKey := piv.DefaultManagementKey
if opts.URI != "" {
u, err := uri.ParseWithScheme(Scheme, opts.URI)
if err != nil {
return nil, err
}
if v := u.Pin(); v != "" {
opts.Pin = v
}
if v := u.Get("management-key"); v != "" {
opts.ManagementKey = v
}
}
// Deprecated way to set configuration parameters.
if opts.ManagementKey != "" {
b, err := hex.DecodeString(opts.ManagementKey)
if err != nil {
return nil, errors.Wrap(err, "error decoding managementKey")
}
if len(b) != 24 {
return nil, errors.New("invalid managementKey: length is not 24 bytes")
}
copy(managementKey[:], b[:24])
}
cards, err := piv.Cards()
if err != nil {
return nil, err
}
if len(cards) == 0 {
return nil, errors.New("error detecting yubikey: try removing and reconnecting the device")
}
yk, err := piv.Open(cards[0])
if err != nil {
return nil, errors.Wrap(err, "error opening yubikey")
}
return &YubiKey{
yk: yk,
pin: opts.Pin,
managementKey: managementKey,
}, 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) {
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(k.managementKey, 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
}
pub, err := k.getPublicKey(slot)
if err != nil {
return nil, err
}
return pub, 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(k.managementKey, 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
}
pub, err := k.getPublicKey(slot)
if err != nil {
return nil, err
}
priv, err := k.yk.PrivateKey(slot, pub, piv.KeyAuth{
PIN: k.pin,
PINPolicy: piv.PINPolicyAlways,
})
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")
}
// getPublicKey returns the public key on a slot. First it attempts to do
// attestation to get a certificate with the public key in it, if this succeeds
// means that the key was generated in the device. If not we'll try to get the
// key from a stored certificate in the same slot.
func (k *YubiKey) getPublicKey(slot piv.Slot) (crypto.PublicKey, error) {
cert, err := k.yk.Attest(slot)
if err != nil {
if cert, err = k.yk.Certificate(slot); err != nil {
return nil, errors.Wrap(err, "error retrieving public key")
}
}
return cert.PublicKey, nil
}
// 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,
"82": {Key: 0x82, Object: 0x5FC10D},
"83": {Key: 0x83, Object: 0x5FC10E},
"84": {Key: 0x84, Object: 0x5FC10F},
"85": {Key: 0x85, Object: 0x5FC110},
"86": {Key: 0x86, Object: 0x5FC111},
"87": {Key: 0x87, Object: 0x5FC112},
"88": {Key: 0x88, Object: 0x5FC113},
"89": {Key: 0x89, Object: 0x5FC114},
"8a": {Key: 0x8a, Object: 0x5FC115},
"8b": {Key: 0x8b, Object: 0x5FC116},
"8c": {Key: 0x8c, Object: 0x5FC117},
"8d": {Key: 0x8d, Object: 0x5FC118},
"8e": {Key: 0x8e, Object: 0x5FC119},
"8f": {Key: 0x8f, Object: 0x5FC11A},
"90": {Key: 0x90, Object: 0x5FC11B},
"91": {Key: 0x91, Object: 0x5FC11C},
"92": {Key: 0x92, Object: 0x5FC11D},
"93": {Key: 0x93, Object: 0x5FC11E},
"94": {Key: 0x94, Object: 0x5FC11F},
"95": {Key: 0x95, Object: 0x5FC120},
}
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
}