|
@@ -0,0 +1,884 @@
|
|
|
+// +build pkcs11
|
|
|
+
|
|
|
+package yubikey
|
|
|
+
|
|
|
+import (
|
|
|
+ "crypto"
|
|
|
+ "crypto/ecdsa"
|
|
|
+ "crypto/elliptic"
|
|
|
+ "crypto/rand"
|
|
|
+ "crypto/sha256"
|
|
|
+ "crypto/x509"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "math/big"
|
|
|
+ "os"
|
|
|
+
|
|
|
+ "github.com/Sirupsen/logrus"
|
|
|
+ "github.com/docker/notary/passphrase"
|
|
|
+ "github.com/docker/notary/trustmanager"
|
|
|
+ "github.com/docker/notary/tuf/data"
|
|
|
+ "github.com/docker/notary/tuf/signed"
|
|
|
+ "github.com/miekg/pkcs11"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ USER_PIN = "123456"
|
|
|
+ SO_USER_PIN = "010203040506070801020304050607080102030405060708"
|
|
|
+ numSlots = 4 // number of slots in the yubikey
|
|
|
+
|
|
|
+ KeymodeNone = 0
|
|
|
+ KeymodeTouch = 1 // touch enabled
|
|
|
+ KeymodePinOnce = 2 // require pin entry once
|
|
|
+ KeymodePinAlways = 4 // require pin entry all the time
|
|
|
+
|
|
|
+ // the key size, when importing a key into yubikey, MUST be 32 bytes
|
|
|
+ ecdsaPrivateKeySize = 32
|
|
|
+
|
|
|
+ sigAttempts = 5
|
|
|
+)
|
|
|
+
|
|
|
+// what key mode to use when generating keys
|
|
|
+var (
|
|
|
+ yubikeyKeymode = KeymodeTouch | KeymodePinOnce
|
|
|
+ // order in which to prefer token locations on the yubikey.
|
|
|
+ // corresponds to: 9c, 9e, 9d, 9a
|
|
|
+ slotIDs = []int{2, 1, 3, 0}
|
|
|
+)
|
|
|
+
|
|
|
+// SetYubikeyKeyMode - sets the mode when generating yubikey keys.
|
|
|
+// This is to be used for testing. It does nothing if not building with tag
|
|
|
+// pkcs11.
|
|
|
+func SetYubikeyKeyMode(keyMode int) error {
|
|
|
+ // technically 7 (1 | 2 | 4) is valid, but KeymodePinOnce +
|
|
|
+ // KeymdoePinAlways don't really make sense together
|
|
|
+ if keyMode < 0 || keyMode > 5 {
|
|
|
+ return errors.New("Invalid key mode")
|
|
|
+ }
|
|
|
+ yubikeyKeymode = keyMode
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// SetTouchToSignUI - allows configurable UX for notifying a user that they
|
|
|
+// need to touch the yubikey to sign. The callback may be used to provide a
|
|
|
+// mechanism for updating a GUI (such as removing a modal) after the touch
|
|
|
+// has been made
|
|
|
+func SetTouchToSignUI(notifier func(), callback func()) {
|
|
|
+ touchToSignUI = notifier
|
|
|
+ if callback != nil {
|
|
|
+ touchDoneCallback = callback
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+var touchToSignUI = func() {
|
|
|
+ fmt.Println("Please touch the attached Yubikey to perform signing.")
|
|
|
+}
|
|
|
+
|
|
|
+var touchDoneCallback = func() {
|
|
|
+ // noop
|
|
|
+}
|
|
|
+
|
|
|
+var pkcs11Lib string
|
|
|
+
|
|
|
+func init() {
|
|
|
+ for _, loc := range possiblePkcs11Libs {
|
|
|
+ _, err := os.Stat(loc)
|
|
|
+ if err == nil {
|
|
|
+ p := pkcs11.New(loc)
|
|
|
+ if p != nil {
|
|
|
+ pkcs11Lib = loc
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type ErrBackupFailed struct {
|
|
|
+ err string
|
|
|
+}
|
|
|
+
|
|
|
+func (err ErrBackupFailed) Error() string {
|
|
|
+ return fmt.Sprintf("Failed to backup private key to: %s", err.err)
|
|
|
+}
|
|
|
+
|
|
|
+type yubiSlot struct {
|
|
|
+ role string
|
|
|
+ slotID []byte
|
|
|
+}
|
|
|
+
|
|
|
+// YubiPrivateKey represents a private key inside of a yubikey
|
|
|
+type YubiPrivateKey struct {
|
|
|
+ data.ECDSAPublicKey
|
|
|
+ passRetriever passphrase.Retriever
|
|
|
+ slot []byte
|
|
|
+ libLoader pkcs11LibLoader
|
|
|
+}
|
|
|
+
|
|
|
+type YubikeySigner struct {
|
|
|
+ YubiPrivateKey
|
|
|
+}
|
|
|
+
|
|
|
+func NewYubiPrivateKey(slot []byte, pubKey data.ECDSAPublicKey,
|
|
|
+ passRetriever passphrase.Retriever) *YubiPrivateKey {
|
|
|
+
|
|
|
+ return &YubiPrivateKey{
|
|
|
+ ECDSAPublicKey: pubKey,
|
|
|
+ passRetriever: passRetriever,
|
|
|
+ slot: slot,
|
|
|
+ libLoader: defaultLoader,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (ys *YubikeySigner) Public() crypto.PublicKey {
|
|
|
+ publicKey, err := x509.ParsePKIXPublicKey(ys.YubiPrivateKey.Public())
|
|
|
+ if err != nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return publicKey
|
|
|
+}
|
|
|
+
|
|
|
+func (y *YubiPrivateKey) setLibLoader(loader pkcs11LibLoader) {
|
|
|
+ y.libLoader = loader
|
|
|
+}
|
|
|
+
|
|
|
+// CryptoSigner returns a crypto.Signer tha wraps the YubiPrivateKey. Needed for
|
|
|
+// Certificate generation only
|
|
|
+func (y *YubiPrivateKey) CryptoSigner() crypto.Signer {
|
|
|
+ return &YubikeySigner{YubiPrivateKey: *y}
|
|
|
+}
|
|
|
+
|
|
|
+// Private is not implemented in hardware keys
|
|
|
+func (y *YubiPrivateKey) Private() []byte {
|
|
|
+ // We cannot return the private material from a Yubikey
|
|
|
+ // TODO(david): We probably want to return an error here
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (y YubiPrivateKey) SignatureAlgorithm() data.SigAlgorithm {
|
|
|
+ return data.ECDSASignature
|
|
|
+}
|
|
|
+
|
|
|
+func (y *YubiPrivateKey) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) {
|
|
|
+ ctx, session, err := SetupHSMEnv(pkcs11Lib, y.libLoader)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ defer cleanup(ctx, session)
|
|
|
+
|
|
|
+ v := signed.Verifiers[data.ECDSASignature]
|
|
|
+ for i := 0; i < sigAttempts; i++ {
|
|
|
+ sig, err := sign(ctx, session, y.slot, y.passRetriever, msg)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("failed to sign using Yubikey: %v", err)
|
|
|
+ }
|
|
|
+ if err := v.Verify(&y.ECDSAPublicKey, sig, msg); err == nil {
|
|
|
+ return sig, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil, errors.New("Failed to generate signature on Yubikey.")
|
|
|
+}
|
|
|
+
|
|
|
+// If a byte array is less than the number of bytes specified by
|
|
|
+// ecdsaPrivateKeySize, left-zero-pad the byte array until
|
|
|
+// it is the required size.
|
|
|
+func ensurePrivateKeySize(payload []byte) []byte {
|
|
|
+ final := payload
|
|
|
+ if len(payload) < ecdsaPrivateKeySize {
|
|
|
+ final = make([]byte, ecdsaPrivateKeySize)
|
|
|
+ copy(final[ecdsaPrivateKeySize-len(payload):], payload)
|
|
|
+ }
|
|
|
+ return final
|
|
|
+}
|
|
|
+
|
|
|
+// addECDSAKey adds a key to the yubikey
|
|
|
+func addECDSAKey(
|
|
|
+ ctx IPKCS11Ctx,
|
|
|
+ session pkcs11.SessionHandle,
|
|
|
+ privKey data.PrivateKey,
|
|
|
+ pkcs11KeyID []byte,
|
|
|
+ passRetriever passphrase.Retriever,
|
|
|
+ role string,
|
|
|
+) error {
|
|
|
+ logrus.Debugf("Attempting to add key to yubikey with ID: %s", privKey.ID())
|
|
|
+
|
|
|
+ err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SO_USER_PIN)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ defer ctx.Logout(session)
|
|
|
+
|
|
|
+ // Create an ecdsa.PrivateKey out of the private key bytes
|
|
|
+ ecdsaPrivKey, err := x509.ParseECPrivateKey(privKey.Private())
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ ecdsaPrivKeyD := ensurePrivateKeySize(ecdsaPrivKey.D.Bytes())
|
|
|
+
|
|
|
+ template, err := trustmanager.NewCertificate(role)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("failed to create the certificate template: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ certBytes, err := x509.CreateCertificate(rand.Reader, template, template, ecdsaPrivKey.Public(), ecdsaPrivKey)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("failed to create the certificate: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ certTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_VALUE, certBytes),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
|
|
+ }
|
|
|
+
|
|
|
+ privateKeyTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07}),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_VALUE, ecdsaPrivKeyD),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_VENDOR_DEFINED, yubikeyKeymode),
|
|
|
+ }
|
|
|
+
|
|
|
+ _, err = ctx.CreateObject(session, certTemplate)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("error importing: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ _, err = ctx.CreateObject(session, privateKeyTemplate)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("error importing: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func getECDSAKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte) (*data.ECDSAPublicKey, string, error) {
|
|
|
+ findTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY),
|
|
|
+ }
|
|
|
+
|
|
|
+ attrTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, []byte{0}),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{0}),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{0}),
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := ctx.FindObjectsInit(session, findTemplate); err != nil {
|
|
|
+ logrus.Debugf("Failed to init: %s", err.Error())
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+ obj, _, err := ctx.FindObjects(session, 1)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to find objects: %v", err)
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+ if err := ctx.FindObjectsFinal(session); err != nil {
|
|
|
+ logrus.Debugf("Failed to finalize: %s", err.Error())
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+ if len(obj) != 1 {
|
|
|
+ logrus.Debugf("should have found one object")
|
|
|
+ return nil, "", errors.New("no matching keys found inside of yubikey")
|
|
|
+ }
|
|
|
+
|
|
|
+ // Retrieve the public-key material to be able to create a new ECSAKey
|
|
|
+ attr, err := ctx.GetAttributeValue(session, obj[0], attrTemplate)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to get Attribute for: %v", obj[0])
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Iterate through all the attributes of this key and saves CKA_PUBLIC_EXPONENT and CKA_MODULUS. Removes ordering specific issues.
|
|
|
+ var rawPubKey []byte
|
|
|
+ for _, a := range attr {
|
|
|
+ if a.Type == pkcs11.CKA_EC_POINT {
|
|
|
+ rawPubKey = a.Value
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ ecdsaPubKey := ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(rawPubKey[3:35]), Y: new(big.Int).SetBytes(rawPubKey[35:])}
|
|
|
+ pubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPubKey)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to Marshal public key")
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ return data.NewECDSAPublicKey(pubBytes), data.CanonicalRootRole, nil
|
|
|
+}
|
|
|
+
|
|
|
+// Sign returns a signature for a given signature request
|
|
|
+func sign(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, payload []byte) ([]byte, error) {
|
|
|
+ err := login(ctx, session, passRetriever, pkcs11.CKU_USER, USER_PIN)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("error logging in: %v", err)
|
|
|
+ }
|
|
|
+ defer ctx.Logout(session)
|
|
|
+
|
|
|
+ // Define the ECDSA Private key template
|
|
|
+ class := pkcs11.CKO_PRIVATE_KEY
|
|
|
+ privateKeyTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, class),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_ECDSA),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := ctx.FindObjectsInit(session, privateKeyTemplate); err != nil {
|
|
|
+ logrus.Debugf("Failed to init find objects: %s", err.Error())
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ obj, _, err := ctx.FindObjects(session, 1)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to find objects: %v", err)
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if err = ctx.FindObjectsFinal(session); err != nil {
|
|
|
+ logrus.Debugf("Failed to finalize find objects: %s", err.Error())
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if len(obj) != 1 {
|
|
|
+ return nil, errors.New("length of objects found not 1")
|
|
|
+ }
|
|
|
+
|
|
|
+ var sig []byte
|
|
|
+ err = ctx.SignInit(
|
|
|
+ session, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_ECDSA, nil)}, obj[0])
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get the SHA256 of the payload
|
|
|
+ digest := sha256.Sum256(payload)
|
|
|
+
|
|
|
+ if (yubikeyKeymode & KeymodeTouch) > 0 {
|
|
|
+ touchToSignUI()
|
|
|
+ defer touchDoneCallback()
|
|
|
+ }
|
|
|
+ // a call to Sign, whether or not Sign fails, will clear the SignInit
|
|
|
+ sig, err = ctx.Sign(session, digest[:])
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Error while signing: %s", err)
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if sig == nil {
|
|
|
+ return nil, errors.New("Failed to create signature")
|
|
|
+ }
|
|
|
+ return sig[:], nil
|
|
|
+}
|
|
|
+
|
|
|
+func yubiRemoveKey(ctx IPKCS11Ctx, session pkcs11.SessionHandle, pkcs11KeyID []byte, passRetriever passphrase.Retriever, keyID string) error {
|
|
|
+ err := login(ctx, session, passRetriever, pkcs11.CKU_SO, SO_USER_PIN)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ defer ctx.Logout(session)
|
|
|
+
|
|
|
+ template := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
|
|
+ //pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := ctx.FindObjectsInit(session, template); err != nil {
|
|
|
+ logrus.Debugf("Failed to init find objects: %s", err.Error())
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ obj, b, err := ctx.FindObjects(session, 1)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to find objects: %s %v", err.Error(), b)
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if err := ctx.FindObjectsFinal(session); err != nil {
|
|
|
+ logrus.Debugf("Failed to finalize find objects: %s", err.Error())
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if len(obj) != 1 {
|
|
|
+ logrus.Debugf("should have found exactly one object")
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Delete the certificate
|
|
|
+ err = ctx.DestroyObject(session, obj[0])
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to delete cert")
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func yubiListKeys(ctx IPKCS11Ctx, session pkcs11.SessionHandle) (keys map[string]yubiSlot, err error) {
|
|
|
+ keys = make(map[string]yubiSlot)
|
|
|
+ findTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
|
|
+ //pkcs11.NewAttribute(pkcs11.CKA_ID, pkcs11KeyID),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
|
|
|
+ }
|
|
|
+
|
|
|
+ attrTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{0}),
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_VALUE, []byte{0}),
|
|
|
+ }
|
|
|
+
|
|
|
+ if err = ctx.FindObjectsInit(session, findTemplate); err != nil {
|
|
|
+ logrus.Debugf("Failed to init: %s", err.Error())
|
|
|
+ return
|
|
|
+ }
|
|
|
+ objs, b, err := ctx.FindObjects(session, numSlots)
|
|
|
+ for err == nil {
|
|
|
+ var o []pkcs11.ObjectHandle
|
|
|
+ o, b, err = ctx.FindObjects(session, numSlots)
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if len(o) == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ objs = append(objs, o...)
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to find: %s %v", err.Error(), b)
|
|
|
+ if len(objs) == 0 {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if err = ctx.FindObjectsFinal(session); err != nil {
|
|
|
+ logrus.Debugf("Failed to finalize: %s", err.Error())
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if len(objs) == 0 {
|
|
|
+ return nil, errors.New("No keys found in yubikey.")
|
|
|
+ }
|
|
|
+ logrus.Debugf("Found %d objects matching list filters", len(objs))
|
|
|
+ for _, obj := range objs {
|
|
|
+ var (
|
|
|
+ cert *x509.Certificate
|
|
|
+ slot []byte
|
|
|
+ )
|
|
|
+ // Retrieve the public-key material to be able to create a new ECDSA
|
|
|
+ attr, err := ctx.GetAttributeValue(session, obj, attrTemplate)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to get Attribute for: %v", obj)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // Iterate through all the attributes of this key and saves CKA_PUBLIC_EXPONENT and CKA_MODULUS. Removes ordering specific issues.
|
|
|
+ for _, a := range attr {
|
|
|
+ if a.Type == pkcs11.CKA_ID {
|
|
|
+ slot = a.Value
|
|
|
+ }
|
|
|
+ if a.Type == pkcs11.CKA_VALUE {
|
|
|
+ cert, err = x509.ParseCertificate(a.Value)
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if !data.ValidRole(cert.Subject.CommonName) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var ecdsaPubKey *ecdsa.PublicKey
|
|
|
+ switch cert.PublicKeyAlgorithm {
|
|
|
+ case x509.ECDSA:
|
|
|
+ ecdsaPubKey = cert.PublicKey.(*ecdsa.PublicKey)
|
|
|
+ default:
|
|
|
+ logrus.Infof("Unsupported x509 PublicKeyAlgorithm: %d", cert.PublicKeyAlgorithm)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ pubBytes, err := x509.MarshalPKIXPublicKey(ecdsaPubKey)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to Marshal public key")
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ keys[data.NewECDSAPublicKey(pubBytes).ID()] = yubiSlot{
|
|
|
+ role: cert.Subject.CommonName,
|
|
|
+ slotID: slot,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+func getNextEmptySlot(ctx IPKCS11Ctx, session pkcs11.SessionHandle) ([]byte, error) {
|
|
|
+ findTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
|
|
+ }
|
|
|
+ attrTemplate := []*pkcs11.Attribute{
|
|
|
+ pkcs11.NewAttribute(pkcs11.CKA_ID, []byte{0}),
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := ctx.FindObjectsInit(session, findTemplate); err != nil {
|
|
|
+ logrus.Debugf("Failed to init: %s", err.Error())
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ objs, b, err := ctx.FindObjects(session, numSlots)
|
|
|
+ // if there are more objects than `numSlots`, get all of them until
|
|
|
+ // there are no more to get
|
|
|
+ for err == nil {
|
|
|
+ var o []pkcs11.ObjectHandle
|
|
|
+ o, b, err = ctx.FindObjects(session, numSlots)
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if len(o) == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ objs = append(objs, o...)
|
|
|
+ }
|
|
|
+ taken := make(map[int]bool)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to find: %s %v", err.Error(), b)
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if err = ctx.FindObjectsFinal(session); err != nil {
|
|
|
+ logrus.Debugf("Failed to finalize: %s\n", err.Error())
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ for _, obj := range objs {
|
|
|
+ // Retrieve the slot ID
|
|
|
+ attr, err := ctx.GetAttributeValue(session, obj, attrTemplate)
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // Iterate through attributes. If an ID attr was found, mark it as taken
|
|
|
+ for _, a := range attr {
|
|
|
+ if a.Type == pkcs11.CKA_ID {
|
|
|
+ if len(a.Value) < 1 {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ // a byte will always be capable of representing all slot IDs
|
|
|
+ // for the Yubikeys
|
|
|
+ slotNum := int(a.Value[0])
|
|
|
+ if slotNum >= numSlots {
|
|
|
+ // defensive
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ taken[slotNum] = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // iterate the token locations in our preferred order and use the first
|
|
|
+ // available one. Otherwise exit the loop and return an error.
|
|
|
+ for _, loc := range slotIDs {
|
|
|
+ if !taken[loc] {
|
|
|
+ return []byte{byte(loc)}, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil, errors.New("Yubikey has no available slots.")
|
|
|
+}
|
|
|
+
|
|
|
+// YubiKeyStore is a KeyStore for private keys inside a Yubikey
|
|
|
+type YubiKeyStore struct {
|
|
|
+ passRetriever passphrase.Retriever
|
|
|
+ keys map[string]yubiSlot
|
|
|
+ backupStore trustmanager.KeyStore
|
|
|
+ libLoader pkcs11LibLoader
|
|
|
+}
|
|
|
+
|
|
|
+// NewYubiKeyStore returns a YubiKeyStore, given a backup key store to write any
|
|
|
+// generated keys to (usually a KeyFileStore)
|
|
|
+func NewYubiKeyStore(backupStore trustmanager.KeyStore, passphraseRetriever passphrase.Retriever) (
|
|
|
+ *YubiKeyStore, error) {
|
|
|
+
|
|
|
+ s := &YubiKeyStore{
|
|
|
+ passRetriever: passphraseRetriever,
|
|
|
+ keys: make(map[string]yubiSlot),
|
|
|
+ backupStore: backupStore,
|
|
|
+ libLoader: defaultLoader,
|
|
|
+ }
|
|
|
+ s.ListKeys() // populate keys field
|
|
|
+ return s, nil
|
|
|
+}
|
|
|
+
|
|
|
+// Name returns a user friendly name for the location this store
|
|
|
+// keeps its data
|
|
|
+func (s YubiKeyStore) Name() string {
|
|
|
+ return "yubikey"
|
|
|
+}
|
|
|
+
|
|
|
+func (s *YubiKeyStore) setLibLoader(loader pkcs11LibLoader) {
|
|
|
+ s.libLoader = loader
|
|
|
+}
|
|
|
+
|
|
|
+func (s *YubiKeyStore) ListKeys() map[string]string {
|
|
|
+ if len(s.keys) > 0 {
|
|
|
+ return buildKeyMap(s.keys)
|
|
|
+ }
|
|
|
+ ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error())
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ defer cleanup(ctx, session)
|
|
|
+
|
|
|
+ keys, err := yubiListKeys(ctx, session)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to list key from the yubikey: %s", err.Error())
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ s.keys = keys
|
|
|
+
|
|
|
+ return buildKeyMap(keys)
|
|
|
+}
|
|
|
+
|
|
|
+// AddKey puts a key inside the Yubikey, as well as writing it to the backup store
|
|
|
+func (s *YubiKeyStore) AddKey(keyID, role string, privKey data.PrivateKey) error {
|
|
|
+ added, err := s.addKey(keyID, role, privKey)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if added {
|
|
|
+ err = s.backupStore.AddKey(privKey.ID(), role, privKey)
|
|
|
+ if err != nil {
|
|
|
+ defer s.RemoveKey(keyID)
|
|
|
+ return ErrBackupFailed{err: err.Error()}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Only add if we haven't seen the key already. Return whether the key was
|
|
|
+// added.
|
|
|
+func (s *YubiKeyStore) addKey(keyID, role string, privKey data.PrivateKey) (
|
|
|
+ bool, error) {
|
|
|
+
|
|
|
+ // We only allow adding root keys for now
|
|
|
+ if role != data.CanonicalRootRole {
|
|
|
+ return false, fmt.Errorf(
|
|
|
+ "yubikey only supports storing root keys, got %s for key: %s", role, keyID)
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error())
|
|
|
+ return false, err
|
|
|
+ }
|
|
|
+ defer cleanup(ctx, session)
|
|
|
+
|
|
|
+ if k, ok := s.keys[keyID]; ok {
|
|
|
+ if k.role == role {
|
|
|
+ // already have the key and it's associated with the correct role
|
|
|
+ return false, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ slot, err := getNextEmptySlot(ctx, session)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to get an empty yubikey slot: %s", err.Error())
|
|
|
+ return false, err
|
|
|
+ }
|
|
|
+ logrus.Debugf("Attempting to store key using yubikey slot %v", slot)
|
|
|
+
|
|
|
+ err = addECDSAKey(
|
|
|
+ ctx, session, privKey, slot, s.passRetriever, role)
|
|
|
+ if err == nil {
|
|
|
+ s.keys[privKey.ID()] = yubiSlot{
|
|
|
+ role: role,
|
|
|
+ slotID: slot,
|
|
|
+ }
|
|
|
+ return true, nil
|
|
|
+ }
|
|
|
+ logrus.Debugf("Failed to add key to yubikey: %v", err)
|
|
|
+
|
|
|
+ return false, err
|
|
|
+}
|
|
|
+
|
|
|
+// GetKey retrieves a key from the Yubikey only (it does not look inside the
|
|
|
+// backup store)
|
|
|
+func (s *YubiKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) {
|
|
|
+ ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error())
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+ defer cleanup(ctx, session)
|
|
|
+
|
|
|
+ key, ok := s.keys[keyID]
|
|
|
+ if !ok {
|
|
|
+ return nil, "", errors.New("no matching keys found inside of yubikey")
|
|
|
+ }
|
|
|
+
|
|
|
+ pubKey, alias, err := getECDSAKey(ctx, session, key.slotID)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to get key from slot %s: %s", key.slotID, err.Error())
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+ // Check to see if we're returning the intended keyID
|
|
|
+ if pubKey.ID() != keyID {
|
|
|
+ return nil, "", fmt.Errorf("expected root key: %s, but found: %s", keyID, pubKey.ID())
|
|
|
+ }
|
|
|
+ privKey := NewYubiPrivateKey(key.slotID, *pubKey, s.passRetriever)
|
|
|
+ if privKey == nil {
|
|
|
+ return nil, "", errors.New("could not initialize new YubiPrivateKey")
|
|
|
+ }
|
|
|
+
|
|
|
+ return privKey, alias, err
|
|
|
+}
|
|
|
+
|
|
|
+// RemoveKey deletes a key from the Yubikey only (it does not remove it from the
|
|
|
+// backup store)
|
|
|
+func (s *YubiKeyStore) RemoveKey(keyID string) error {
|
|
|
+ ctx, session, err := SetupHSMEnv(pkcs11Lib, s.libLoader)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to initialize PKCS11 environment: %s", err.Error())
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ defer cleanup(ctx, session)
|
|
|
+
|
|
|
+ key, ok := s.keys[keyID]
|
|
|
+ if !ok {
|
|
|
+ return errors.New("Key not present in yubikey")
|
|
|
+ }
|
|
|
+ err = yubiRemoveKey(ctx, session, key.slotID, s.passRetriever, keyID)
|
|
|
+ if err == nil {
|
|
|
+ delete(s.keys, keyID)
|
|
|
+ } else {
|
|
|
+ logrus.Debugf("Failed to remove from the yubikey KeyID %s: %v", keyID, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+// ExportKey doesn't work, because you can't export data from a Yubikey
|
|
|
+func (s *YubiKeyStore) ExportKey(keyID string) ([]byte, error) {
|
|
|
+ logrus.Debugf("Attempting to export: %s key inside of YubiKeyStore", keyID)
|
|
|
+ return nil, errors.New("Keys cannot be exported from a Yubikey.")
|
|
|
+}
|
|
|
+
|
|
|
+// ImportKey imports a root key into a Yubikey
|
|
|
+func (s *YubiKeyStore) ImportKey(pemBytes []byte, keyPath string) error {
|
|
|
+ logrus.Debugf("Attempting to import: %s key inside of YubiKeyStore", keyPath)
|
|
|
+ privKey, _, err := trustmanager.GetPasswdDecryptBytes(
|
|
|
+ s.passRetriever, pemBytes, "", "imported root")
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Failed to get and retrieve a key from: %s", keyPath)
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if keyPath != data.CanonicalRootRole {
|
|
|
+ return fmt.Errorf("yubikey only supports storing root keys")
|
|
|
+ }
|
|
|
+ _, err = s.addKey(privKey.ID(), "root", privKey)
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+func cleanup(ctx IPKCS11Ctx, session pkcs11.SessionHandle) {
|
|
|
+ err := ctx.CloseSession(session)
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Error closing session: %s", err.Error())
|
|
|
+ }
|
|
|
+ finalizeAndDestroy(ctx)
|
|
|
+}
|
|
|
+
|
|
|
+func finalizeAndDestroy(ctx IPKCS11Ctx) {
|
|
|
+ err := ctx.Finalize()
|
|
|
+ if err != nil {
|
|
|
+ logrus.Debugf("Error finalizing: %s", err.Error())
|
|
|
+ }
|
|
|
+ ctx.Destroy()
|
|
|
+}
|
|
|
+
|
|
|
+// SetupHSMEnv is a method that depends on the existences
|
|
|
+func SetupHSMEnv(libraryPath string, libLoader pkcs11LibLoader) (
|
|
|
+ IPKCS11Ctx, pkcs11.SessionHandle, error) {
|
|
|
+
|
|
|
+ if libraryPath == "" {
|
|
|
+ return nil, 0, errors.New("No library found.")
|
|
|
+ }
|
|
|
+ p := libLoader(libraryPath)
|
|
|
+
|
|
|
+ if p == nil {
|
|
|
+ return nil, 0, errors.New("Failed to init library")
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := p.Initialize(); err != nil {
|
|
|
+ defer finalizeAndDestroy(p)
|
|
|
+ return nil, 0, fmt.Errorf("Initialize error %s", err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ slots, err := p.GetSlotList(true)
|
|
|
+ if err != nil {
|
|
|
+ defer finalizeAndDestroy(p)
|
|
|
+ return nil, 0, fmt.Errorf("Failed to list HSM slots %s", err)
|
|
|
+ }
|
|
|
+ // Check to see if we got any slots from the HSM.
|
|
|
+ if len(slots) < 1 {
|
|
|
+ defer finalizeAndDestroy(p)
|
|
|
+ return nil, 0, fmt.Errorf("No HSM Slots found")
|
|
|
+ }
|
|
|
+
|
|
|
+ // CKF_SERIAL_SESSION: TRUE if cryptographic functions are performed in serial with the application; FALSE if the functions may be performed in parallel with the application.
|
|
|
+ // CKF_RW_SESSION: TRUE if the session is read/write; FALSE if the session is read-only
|
|
|
+ session, err := p.OpenSession(slots[0], pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
|
|
|
+ if err != nil {
|
|
|
+ defer cleanup(p, session)
|
|
|
+ return nil, 0, fmt.Errorf("Failed to Start Session with HSM %s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return p, session, nil
|
|
|
+}
|
|
|
+
|
|
|
+// YubikeyAccessible returns true if a Yubikey can be accessed
|
|
|
+func YubikeyAccessible() bool {
|
|
|
+ if pkcs11Lib == "" {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ ctx, session, err := SetupHSMEnv(pkcs11Lib, defaultLoader)
|
|
|
+ if err != nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ defer cleanup(ctx, session)
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+func login(ctx IPKCS11Ctx, session pkcs11.SessionHandle, passRetriever passphrase.Retriever, userFlag uint, defaultPassw string) error {
|
|
|
+ // try default password
|
|
|
+ err := ctx.Login(session, userFlag, defaultPassw)
|
|
|
+ if err == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // default failed, ask user for password
|
|
|
+ for attempts := 0; ; attempts++ {
|
|
|
+ var (
|
|
|
+ giveup bool
|
|
|
+ err error
|
|
|
+ user string
|
|
|
+ )
|
|
|
+ if userFlag == pkcs11.CKU_SO {
|
|
|
+ user = "SO Pin"
|
|
|
+ } else {
|
|
|
+ user = "User Pin"
|
|
|
+ }
|
|
|
+ passwd, giveup, err := passRetriever(user, "yubikey", false, attempts)
|
|
|
+ // Check if the passphrase retriever got an error or if it is telling us to give up
|
|
|
+ if giveup || err != nil {
|
|
|
+ return trustmanager.ErrPasswordInvalid{}
|
|
|
+ }
|
|
|
+ if attempts > 2 {
|
|
|
+ return trustmanager.ErrAttemptsExceeded{}
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase
|
|
|
+ err = ctx.Login(session, userFlag, passwd)
|
|
|
+ if err == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func buildKeyMap(keys map[string]yubiSlot) map[string]string {
|
|
|
+ res := make(map[string]string)
|
|
|
+ for k, v := range keys {
|
|
|
+ res[k] = v.role
|
|
|
+ }
|
|
|
+ return res
|
|
|
+}
|