// +build cgo 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 }