mirror of https://github.com/docker/docs.git
1021 lines
28 KiB
Go
1021 lines
28 KiB
Go
// +build pkcs11
|
|
|
|
package yubikey
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/docker/notary/passphrase"
|
|
"github.com/docker/notary/trustmanager"
|
|
"github.com/docker/notary/tuf/data"
|
|
"github.com/miekg/pkcs11"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var ret = passphrase.ConstantRetriever("passphrase")
|
|
|
|
// create a new store for clearing out keys, because we don't want to pollute
|
|
// any cache
|
|
func clearAllKeys(t *testing.T) {
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
for k := range store.ListKeys() {
|
|
err := store.RemoveKey(k)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestEnsurePrivateKeySizePassesThroughRightSizeArrays(t *testing.T) {
|
|
fullByteArray := make([]byte, ecdsaPrivateKeySize)
|
|
for i := range fullByteArray {
|
|
fullByteArray[i] = byte(1)
|
|
}
|
|
|
|
result := ensurePrivateKeySize(fullByteArray)
|
|
require.True(t, reflect.DeepEqual(fullByteArray, result))
|
|
}
|
|
|
|
// The pad32Byte helper function left zero-pads byte arrays that are less than
|
|
// ecdsaPrivateKeySize bytes
|
|
func TestEnsurePrivateKeySizePadsLessThanRequiredSizeArrays(t *testing.T) {
|
|
shortByteArray := make([]byte, ecdsaPrivateKeySize/2)
|
|
for i := range shortByteArray {
|
|
shortByteArray[i] = byte(1)
|
|
}
|
|
|
|
expected := append(
|
|
make([]byte, ecdsaPrivateKeySize-ecdsaPrivateKeySize/2),
|
|
shortByteArray...)
|
|
|
|
result := ensurePrivateKeySize(shortByteArray)
|
|
require.True(t, reflect.DeepEqual(expected, result))
|
|
}
|
|
|
|
func testAddKey(t *testing.T, store trustmanager.KeyStore) (data.PrivateKey, error) {
|
|
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, privKey)
|
|
return privKey, err
|
|
}
|
|
|
|
func addMaxKeys(t *testing.T, store trustmanager.KeyStore) []string {
|
|
var keys []string
|
|
// create the maximum number of keys
|
|
for i := 0; i < numSlots; i++ {
|
|
privKey, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
keys = append(keys, privKey.ID())
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// We can add keys enough times to fill up all the slots in the Yubikey.
|
|
// They are backed up, and we can then list them and get the keys.
|
|
func TestYubiAddKeysAndRetrieve(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
// create 4 keys on the original store
|
|
backup := trustmanager.NewKeyMemoryStore(ret)
|
|
store, err := NewYubiStore(backup, ret)
|
|
require.NoError(t, err)
|
|
keys := addMaxKeys(t, store)
|
|
|
|
// create a new store, since we want to be sure the original store's cache
|
|
// is not masking any issues
|
|
cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
// All 4 keys should be in the original store, in the clean store (which
|
|
// makes sure the keys are actually on the Yubikey and not on the original
|
|
// store's cache, and on the backup store)
|
|
for _, store := range []trustmanager.KeyStore{store, cleanStore, backup} {
|
|
listedKeys := store.ListKeys()
|
|
require.Len(t, listedKeys, numSlots)
|
|
for _, k := range keys {
|
|
r, ok := listedKeys[k]
|
|
require.True(t, ok)
|
|
require.Equal(t, data.CanonicalRootRole, r.Role)
|
|
|
|
_, _, err := store.GetKey(k)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test that we can successfully keys enough times to fill up all the slots in the Yubikey, even without a backup store
|
|
func TestYubiAddKeysWithoutBackup(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
// create 4 keys on the original store
|
|
store, err := NewYubiStore(nil, ret)
|
|
require.NoError(t, err)
|
|
keys := addMaxKeys(t, store)
|
|
|
|
// create a new store, since we want to be sure the original store's cache
|
|
// is not masking any issues
|
|
cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
// All 4 keys should be in the original store, in the clean store (which
|
|
// makes sure the keys are actually on the Yubikey and not on the original
|
|
// store's cache)
|
|
for _, store := range []trustmanager.KeyStore{store, cleanStore} {
|
|
listedKeys := store.ListKeys()
|
|
require.Len(t, listedKeys, numSlots)
|
|
for _, k := range keys {
|
|
r, ok := listedKeys[k]
|
|
require.True(t, ok)
|
|
require.Equal(t, data.CanonicalRootRole, r.Role)
|
|
|
|
_, _, err := store.GetKey(k)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// We can't add a key if there are no more slots
|
|
func TestYubiAddKeyFailureIfNoMoreSlots(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
// create 4 keys on the original store
|
|
backup := trustmanager.NewKeyMemoryStore(ret)
|
|
store, err := NewYubiStore(backup, ret)
|
|
require.NoError(t, err)
|
|
addMaxKeys(t, store)
|
|
|
|
// add another key - should fail because there are no more slots
|
|
badKey, err := testAddKey(t, store)
|
|
require.Error(t, err)
|
|
|
|
// create a new store, since we want to be sure the original store's cache
|
|
// is not masking any issues
|
|
cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
// The key should not be in the original store, in the new clean store, or
|
|
// in teh backup store.
|
|
for _, store := range []trustmanager.KeyStore{store, cleanStore, backup} {
|
|
// the key that wasn't created should not appear in ListKeys or GetKey
|
|
_, _, err := store.GetKey(badKey.ID())
|
|
require.Error(t, err)
|
|
for k := range store.ListKeys() {
|
|
require.NotEqual(t, badKey.ID(), k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If some random key in the middle was removed, adding a key will work (keys
|
|
// do not have to be deleted/added in order)
|
|
func TestYubiAddKeyCanAddToMiddleSlot(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
// create 4 keys on the original store
|
|
backup := trustmanager.NewKeyMemoryStore(ret)
|
|
store, err := NewYubiStore(backup, ret)
|
|
require.NoError(t, err)
|
|
keys := addMaxKeys(t, store)
|
|
|
|
// delete one of the middle keys, and assert we can still create a new key
|
|
keyIDToDelete := keys[numSlots/2]
|
|
err = store.RemoveKey(keyIDToDelete)
|
|
require.NoError(t, err)
|
|
|
|
newKey, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
// create a new store, since we want to be sure the original store's cache
|
|
// is not masking any issues
|
|
cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
// The new key should be in the original store, in the new clean store, and
|
|
// in the backup store. The old key should not be in the original store,
|
|
// or the new clean store.
|
|
for _, store := range []trustmanager.KeyStore{store, cleanStore, backup} {
|
|
// new key should appear in all stores
|
|
gottenKey, _, err := store.GetKey(newKey.ID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, gottenKey.ID(), newKey.ID())
|
|
|
|
listedKeys := store.ListKeys()
|
|
_, ok := listedKeys[newKey.ID()]
|
|
require.True(t, ok)
|
|
|
|
// old key should not be in the non-backup stores
|
|
if store != backup {
|
|
_, _, err := store.GetKey(keyIDToDelete)
|
|
require.Error(t, err)
|
|
_, ok = listedKeys[keyIDToDelete]
|
|
require.False(t, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
type nonworkingBackup struct {
|
|
trustmanager.KeyMemoryStore
|
|
}
|
|
|
|
// AddKey stores the contents of a PEM-encoded private key as a PEM block
|
|
func (s *nonworkingBackup) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error {
|
|
return errors.New("Nope!")
|
|
}
|
|
|
|
// If, when adding a key to the Yubikey, we can't back up the key, it should
|
|
// be removed from the Yubikey too because otherwise there is no way for
|
|
// the user to later get a backup of the key.
|
|
func TestYubiAddKeyRollsBackIfCannotBackup(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
backup := &nonworkingBackup{
|
|
KeyMemoryStore: *trustmanager.NewKeyMemoryStore(ret),
|
|
}
|
|
store, err := NewYubiStore(backup, ret)
|
|
require.NoError(t, err)
|
|
|
|
_, err = testAddKey(t, store)
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrBackupFailed{}, err)
|
|
|
|
// there should be no keys on the yubikey
|
|
require.Len(t, cleanListKeys(t), 0)
|
|
}
|
|
|
|
// If, when adding a key to the Yubikey, and it already exists, we succeed
|
|
// without adding it to the backup store.
|
|
func TestYubiAddDuplicateKeySucceedsButDoesNotBackup(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
origStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
key, err := testAddKey(t, origStore)
|
|
require.NoError(t, err)
|
|
|
|
backup := trustmanager.NewKeyMemoryStore(ret)
|
|
cleanStore, err := NewYubiStore(backup, ret)
|
|
require.NoError(t, err)
|
|
require.Len(t, cleanStore.ListKeys(), 1)
|
|
|
|
err = cleanStore.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, key)
|
|
require.NoError(t, err)
|
|
|
|
// there should be just 1 key on the yubikey
|
|
require.Len(t, cleanListKeys(t), 1)
|
|
// nothing was added to the backup
|
|
require.Len(t, backup.ListKeys(), 0)
|
|
}
|
|
|
|
// RemoveKey removes a key from the yubikey, but not from the backup store.
|
|
func TestYubiRemoveKey(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
backup := trustmanager.NewKeyMemoryStore(ret)
|
|
store, err := NewYubiStore(backup, ret)
|
|
require.NoError(t, err)
|
|
|
|
key, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
err = store.RemoveKey(key.ID())
|
|
require.NoError(t, err)
|
|
|
|
// key remains in the backup store
|
|
backupKey, role, err := backup.GetKey(key.ID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, data.CanonicalRootRole, role)
|
|
require.Equal(t, key.ID(), backupKey.ID())
|
|
|
|
// create a new store, since we want to be sure the original store's cache
|
|
// is not masking any issues
|
|
cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
// key is not in either the original store or the clean store
|
|
for _, store := range []*YubiStore{store, cleanStore} {
|
|
_, _, err := store.GetKey(key.ID())
|
|
require.Error(t, err)
|
|
}
|
|
}
|
|
|
|
// One cannot export from hardware - it will not export from the backup
|
|
func TestYubiExportKeyFails(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
key, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
_, err = store.ExportKey(key.ID())
|
|
require.Error(t, err)
|
|
require.Equal(t, "Keys cannot be exported from a Yubikey.", err.Error())
|
|
}
|
|
|
|
// If there are keys in the backup store but no keys in the Yubikey,
|
|
// listing and getting cannot access the keys in the backup store
|
|
func TestYubiListAndGetKeysIgnoresBackup(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
backup := trustmanager.NewKeyMemoryStore(ret)
|
|
key, err := testAddKey(t, backup)
|
|
require.NoError(t, err)
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.Len(t, store.ListKeys(), 0)
|
|
_, _, err = store.GetKey(key.ID())
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// Get a YubiPrivateKey. Check that it has the right algorithm, etc, and
|
|
// specifically that you cannot get the private bytes out. Assume we can
|
|
// sign something.
|
|
func TestYubiKeyAndSign(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
ecdsaPrivateKey, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
yubiPrivateKey, _, err := store.GetKey(ecdsaPrivateKey.ID())
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, data.ECDSAKey, yubiPrivateKey.Algorithm())
|
|
require.Equal(t, data.ECDSASignature, yubiPrivateKey.SignatureAlgorithm())
|
|
require.Equal(t, ecdsaPrivateKey.Public(), yubiPrivateKey.Public())
|
|
require.Nil(t, yubiPrivateKey.Private())
|
|
|
|
// The signature should be verified, but the importing the verifiers causes
|
|
// an import cycle. A bigger refactor needs to be done to fix it.
|
|
msg := []byte("Hello there")
|
|
_, err = yubiPrivateKey.Sign(rand.Reader, msg, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// ----- Negative tests that use stubbed pkcs11 for error injection -----
|
|
|
|
type pkcs11Stubbable interface {
|
|
setLibLoader(pkcs11LibLoader)
|
|
}
|
|
|
|
var setupErrors = []string{"Initialize", "GetSlotList", "OpenSession"}
|
|
|
|
// Create a new store, so that we avoid any cache issues, and list keys
|
|
func cleanListKeys(t *testing.T) map[string]trustmanager.KeyInfo {
|
|
cleanStore, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
return cleanStore.ListKeys()
|
|
}
|
|
|
|
// If an error occurs during login, which only some functions do, the function
|
|
// under test will clean up after itself
|
|
func testYubiFunctionCleansUpOnLoginError(t *testing.T, toStub pkcs11Stubbable,
|
|
functionUnderTest func() error) {
|
|
|
|
toStub.setLibLoader(func(string) IPKCS11Ctx {
|
|
return NewStubCtx(map[string]bool{"Login": true})
|
|
})
|
|
|
|
err := functionUnderTest()
|
|
require.Error(t, err)
|
|
// a lot of these functions wrap other errors
|
|
require.Contains(t, err.Error(), trustmanager.ErrAttemptsExceeded{}.Error())
|
|
|
|
// Set Up another time, to ensure we weren't left in a bad state
|
|
// by the previous runs
|
|
ctx, session, err := SetupHSMEnv(pkcs11Lib, defaultLoader)
|
|
require.NoError(t, err)
|
|
cleanup(ctx, session)
|
|
}
|
|
|
|
// If one of the specified pkcs11 functions errors, the function under test
|
|
// will clean up after itself
|
|
func testYubiFunctionCleansUpOnSpecifiedErrors(t *testing.T,
|
|
toStub pkcs11Stubbable, functionUnderTest func() error,
|
|
dependentFunctions []string, functionShouldError bool) {
|
|
|
|
for _, methodName := range dependentFunctions {
|
|
|
|
toStub.setLibLoader(func(string) IPKCS11Ctx {
|
|
return NewStubCtx(
|
|
map[string]bool{methodName: true})
|
|
})
|
|
|
|
err := functionUnderTest()
|
|
if functionShouldError {
|
|
require.Error(t, err,
|
|
fmt.Sprintf("Didn't error when %s errored.", methodName))
|
|
// a lot of these functions wrap other errors
|
|
require.Contains(t, err.Error(), errInjected{methodName}.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
// Set Up another time, to ensure we weren't left in a bad state
|
|
// by the previous runs
|
|
ctx, session, err := SetupHSMEnv(pkcs11Lib, defaultLoader)
|
|
require.NoError(t, err)
|
|
cleanup(ctx, session)
|
|
}
|
|
|
|
func TestYubiAddKeyCleansUpOnError(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
backup := trustmanager.NewKeyMemoryStore(ret)
|
|
store, err := NewYubiStore(backup, ret)
|
|
require.NoError(t, err)
|
|
|
|
var _addkey = func() error {
|
|
_, err := testAddKey(t, store)
|
|
return err
|
|
}
|
|
|
|
testYubiFunctionCleansUpOnLoginError(t, store, _addkey)
|
|
// all the PKCS11 functions AddKey depends on that aren't the login/logout
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _addkey,
|
|
append(
|
|
setupErrors,
|
|
"FindObjectsInit",
|
|
"FindObjects",
|
|
"FindObjectsFinal",
|
|
"CreateObject",
|
|
), true)
|
|
|
|
// given that everything should have errored, there should be no keys on
|
|
// the yubikey and no keys in backup
|
|
require.Len(t, backup.ListKeys(), 0)
|
|
require.Len(t, cleanListKeys(t), 0)
|
|
|
|
// Logout should not cause a function failure - it s a cleanup failure,
|
|
// which shouldn't break anything, and it should clean up after itself.
|
|
// The key should be added to both stores
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _addkey,
|
|
[]string{"Logout"}, false)
|
|
|
|
listedKeys := cleanListKeys(t)
|
|
require.Len(t, backup.ListKeys(), 1)
|
|
require.Len(t, listedKeys, 1)
|
|
|
|
// Currently, if GetAttributeValue fails, the function succeeds, because if
|
|
// we can't get the attribute value of an object, we don't know what slot
|
|
// it's in, we assume its occupied slot is free (hence this failure will
|
|
// cause the previous key to be overwritten). This behavior may need to
|
|
// be revisited.
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _addkey,
|
|
[]string{"GetAttributeValue"}, false)
|
|
|
|
newListedKeys := cleanListKeys(t)
|
|
// because the original key got overwritten
|
|
require.Len(t, backup.ListKeys(), 2)
|
|
require.Len(t, newListedKeys, 1)
|
|
for k := range newListedKeys {
|
|
_, ok := listedKeys[k]
|
|
require.False(t, ok)
|
|
}
|
|
}
|
|
|
|
func TestYubiGetKeyCleansUpOnError(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
key, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
var _getkey = func() error {
|
|
_, _, err := store.GetKey(key.ID())
|
|
return err
|
|
}
|
|
|
|
// all the PKCS11 functions GetKey depends on
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _getkey,
|
|
append(
|
|
setupErrors,
|
|
"FindObjectsInit",
|
|
"FindObjects",
|
|
"FindObjectsFinal",
|
|
"GetAttributeValue",
|
|
), true)
|
|
}
|
|
|
|
func TestYubiRemoveKeyCleansUpOnError(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
key, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
var _removekey = func() error { return store.RemoveKey(key.ID()) }
|
|
|
|
testYubiFunctionCleansUpOnLoginError(t, store, _removekey)
|
|
// RemoveKey just succeeds if we can't set up the yubikey
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _removekey, setupErrors, false)
|
|
// all the PKCS11 functions RemoveKey depends on that aren't the login/logout
|
|
// or setup/cleanup
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _removekey,
|
|
[]string{
|
|
"FindObjectsInit",
|
|
"FindObjects",
|
|
"FindObjectsFinal",
|
|
"DestroyObject",
|
|
}, true)
|
|
|
|
// given that everything should have errored, there should still be 1 key
|
|
// on the yubikey
|
|
require.Len(t, cleanListKeys(t), 1)
|
|
|
|
// this will not fail, but it should clean up after itself, and the key
|
|
// should be added to both stores
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _removekey,
|
|
[]string{"Logout"}, false)
|
|
|
|
require.Len(t, cleanListKeys(t), 0)
|
|
}
|
|
|
|
func TestYubiListKeyCleansUpOnError(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
// Do not call NewYubiStore, because it list keys immediately to
|
|
// build the cache.
|
|
store := &YubiStore{
|
|
passRetriever: ret,
|
|
keys: make(map[string]yubiSlot),
|
|
backupStore: trustmanager.NewKeyMemoryStore(ret),
|
|
libLoader: defaultLoader,
|
|
}
|
|
|
|
var _listkeys = func() error {
|
|
// ListKeys never fails
|
|
store.ListKeys()
|
|
return nil
|
|
}
|
|
|
|
// all the PKCS11 functions ListKey depends on - list keys never errors
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, store, _listkeys,
|
|
append(
|
|
setupErrors,
|
|
"FindObjectsInit",
|
|
"FindObjects",
|
|
"FindObjectsFinal",
|
|
"GetAttributeValue",
|
|
), false)
|
|
}
|
|
|
|
// export key fails anyway, don't bother testing
|
|
|
|
func TestYubiSignCleansUpOnError(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
key, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
privKey, _, err := store.GetKey(key.ID())
|
|
require.NoError(t, err)
|
|
|
|
yubiPrivateKey, ok := privKey.(*YubiPrivateKey)
|
|
require.True(t, ok)
|
|
|
|
var _sign = func() error {
|
|
_, err = yubiPrivateKey.Sign(rand.Reader, []byte("Hello there"), nil)
|
|
return err
|
|
}
|
|
|
|
testYubiFunctionCleansUpOnLoginError(t, yubiPrivateKey, _sign)
|
|
// all the PKCS11 functions SignKey depends on that is not login/logout
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, yubiPrivateKey, _sign,
|
|
append(
|
|
setupErrors,
|
|
"FindObjectsInit",
|
|
"FindObjects",
|
|
"FindObjectsFinal",
|
|
"SignInit",
|
|
"Sign",
|
|
), true)
|
|
// this will not fail, but it should clean up after itself, and the key
|
|
// should be added to both stores
|
|
testYubiFunctionCleansUpOnSpecifiedErrors(t, yubiPrivateKey, _sign,
|
|
[]string{"Logout"}, false)
|
|
}
|
|
|
|
// If Sign gives us an invalid signature, we retry until successful up to
|
|
// a maximum of 5 times.
|
|
func TestYubiRetrySignUntilSuccess(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
key, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
message := []byte("Hello there")
|
|
goodSig, err := key.Sign(rand.Reader, message, nil)
|
|
require.NoError(t, err)
|
|
|
|
privKey, _, err := store.GetKey(key.ID())
|
|
require.NoError(t, err)
|
|
|
|
yubiPrivateKey, ok := privKey.(*YubiPrivateKey)
|
|
require.True(t, ok)
|
|
|
|
badSigner := &SignInvalidSigCtx{
|
|
Ctx: *pkcs11.New(pkcs11Lib),
|
|
goodSig: goodSig,
|
|
failNum: 2,
|
|
}
|
|
|
|
yubiPrivateKey.setLibLoader(func(string) IPKCS11Ctx { return badSigner })
|
|
|
|
sig, err := yubiPrivateKey.Sign(rand.Reader, message, nil)
|
|
require.NoError(t, err)
|
|
// because the SignInvalidSigCtx returns the good signature, we can just
|
|
// deep equal instead of verifying
|
|
require.True(t, reflect.DeepEqual(goodSig, sig))
|
|
require.Equal(t, 3, badSigner.signCalls)
|
|
}
|
|
|
|
// If Sign gives us an invalid signature, we retry until up to a maximum of 5
|
|
// times, and if it's still invalid, fail.
|
|
func TestYubiRetrySignUntilFail(t *testing.T) {
|
|
if !IsAccessible() {
|
|
t.Skip("Must have Yubikey access.")
|
|
}
|
|
clearAllKeys(t)
|
|
|
|
SetYubikeyKeyMode(KeymodeNone)
|
|
defer func() {
|
|
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
|
|
}()
|
|
|
|
store, err := NewYubiStore(trustmanager.NewKeyMemoryStore(ret), ret)
|
|
require.NoError(t, err)
|
|
|
|
key, err := testAddKey(t, store)
|
|
require.NoError(t, err)
|
|
|
|
message := []byte("Hello there")
|
|
goodSig, err := key.Sign(rand.Reader, message, nil)
|
|
require.NoError(t, err)
|
|
|
|
privKey, _, err := store.GetKey(key.ID())
|
|
require.NoError(t, err)
|
|
|
|
yubiPrivateKey, ok := privKey.(*YubiPrivateKey)
|
|
require.True(t, ok)
|
|
|
|
badSigner := &SignInvalidSigCtx{
|
|
Ctx: *pkcs11.New(pkcs11Lib),
|
|
goodSig: goodSig,
|
|
failNum: sigAttempts + 1,
|
|
}
|
|
|
|
yubiPrivateKey.setLibLoader(func(string) IPKCS11Ctx { return badSigner })
|
|
|
|
_, err = yubiPrivateKey.Sign(rand.Reader, message, nil)
|
|
require.Error(t, err)
|
|
// because the SignInvalidSigCtx returns the good signature, we can just
|
|
// deep equal instead of verifying
|
|
require.Equal(t, sigAttempts, badSigner.signCalls)
|
|
}
|
|
|
|
// ----- Stubbed pkcs11 for testing error conditions ------
|
|
// This is just a passthrough to the underlying pkcs11 library, with optional
|
|
// error injection. This is to ensure that if errors occur during the process
|
|
// of interacting with the Yubikey, that everything gets cleaned up sanely.
|
|
|
|
// Note that this does not actually replicate an actual PKCS11 failure, since
|
|
// who knows what the pkcs11 function call may have done to the key before it
|
|
// errored. This just tests that we handle an error ok.
|
|
|
|
type errInjected struct {
|
|
methodName string
|
|
}
|
|
|
|
func (e errInjected) Error() string {
|
|
return fmt.Sprintf("Injected failure in %s", e.methodName)
|
|
}
|
|
|
|
const (
|
|
uninitialized = 0
|
|
initialized = 1
|
|
sessioned = 2
|
|
loggedin = 3
|
|
)
|
|
|
|
type StubCtx struct {
|
|
ctx IPKCS11Ctx
|
|
functionShouldFail map[string]bool
|
|
}
|
|
|
|
func NewStubCtx(functionShouldFail map[string]bool) *StubCtx {
|
|
realCtx := defaultLoader(pkcs11Lib)
|
|
return &StubCtx{
|
|
ctx: realCtx,
|
|
functionShouldFail: functionShouldFail,
|
|
}
|
|
}
|
|
|
|
// Returns an error if we're supposed to error for this method
|
|
func (s *StubCtx) checkErr(methodName string) error {
|
|
if val, ok := s.functionShouldFail[methodName]; ok && val {
|
|
return errInjected{methodName: methodName}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *StubCtx) Destroy() {
|
|
// can't error
|
|
s.ctx.Destroy()
|
|
}
|
|
|
|
func (s *StubCtx) Initialize() error {
|
|
err := s.checkErr("Initialize")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.Initialize()
|
|
}
|
|
|
|
func (s *StubCtx) Finalize() error {
|
|
err := s.checkErr("Finalize")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.Finalize()
|
|
}
|
|
|
|
func (s *StubCtx) GetSlotList(tokenPresent bool) ([]uint, error) {
|
|
err := s.checkErr("GetSlotList")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.ctx.GetSlotList(tokenPresent)
|
|
}
|
|
|
|
func (s *StubCtx) OpenSession(slotID uint, flags uint) (pkcs11.SessionHandle, error) {
|
|
err := s.checkErr("OpenSession")
|
|
if err != nil {
|
|
return pkcs11.SessionHandle(0), err
|
|
}
|
|
return s.ctx.OpenSession(slotID, flags)
|
|
}
|
|
|
|
func (s *StubCtx) CloseSession(sh pkcs11.SessionHandle) error {
|
|
err := s.checkErr("CloseSession")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.CloseSession(sh)
|
|
}
|
|
|
|
func (s *StubCtx) Login(sh pkcs11.SessionHandle, userType uint, pin string) error {
|
|
err := s.checkErr("Login")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.Login(sh, userType, pin)
|
|
}
|
|
|
|
func (s *StubCtx) Logout(sh pkcs11.SessionHandle) error {
|
|
err := s.checkErr("Logout")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.Logout(sh)
|
|
}
|
|
|
|
func (s *StubCtx) CreateObject(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) (
|
|
pkcs11.ObjectHandle, error) {
|
|
err := s.checkErr("CreateObject")
|
|
if err != nil {
|
|
return pkcs11.ObjectHandle(0), err
|
|
}
|
|
return s.ctx.CreateObject(sh, temp)
|
|
}
|
|
|
|
func (s *StubCtx) DestroyObject(sh pkcs11.SessionHandle, oh pkcs11.ObjectHandle) error {
|
|
err := s.checkErr("DestroyObject")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.DestroyObject(sh, oh)
|
|
}
|
|
|
|
func (s *StubCtx) GetAttributeValue(sh pkcs11.SessionHandle, o pkcs11.ObjectHandle,
|
|
a []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) {
|
|
err := s.checkErr("GetAttributeValue")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.ctx.GetAttributeValue(sh, o, a)
|
|
}
|
|
|
|
func (s *StubCtx) FindObjectsInit(sh pkcs11.SessionHandle, temp []*pkcs11.Attribute) error {
|
|
err := s.checkErr("FindObjectsInit")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.FindObjectsInit(sh, temp)
|
|
}
|
|
|
|
func (s *StubCtx) FindObjects(sh pkcs11.SessionHandle, max int) (
|
|
[]pkcs11.ObjectHandle, bool, error) {
|
|
err := s.checkErr("FindObjects")
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return s.ctx.FindObjects(sh, max)
|
|
}
|
|
|
|
func (s *StubCtx) FindObjectsFinal(sh pkcs11.SessionHandle) error {
|
|
err := s.checkErr("FindObjectsFinal")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.FindObjectsFinal(sh)
|
|
}
|
|
|
|
func (s *StubCtx) SignInit(sh pkcs11.SessionHandle, m []*pkcs11.Mechanism,
|
|
o pkcs11.ObjectHandle) error {
|
|
err := s.checkErr("SignInit")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.SignInit(sh, m, o)
|
|
}
|
|
|
|
func (s *StubCtx) Sign(sh pkcs11.SessionHandle, message []byte) ([]byte, error) {
|
|
// a call to Sign will clear SignInit whether or not it fails, so
|
|
// replicate that by calling Sign, then optionally returning an error.
|
|
sig, sigErr := s.ctx.Sign(sh, message)
|
|
err := s.checkErr("Sign")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sig, sigErr
|
|
}
|
|
|
|
// a different stub Ctx object in which Sign returns an invalid signature some
|
|
// number of times
|
|
type SignInvalidSigCtx struct {
|
|
pkcs11.Ctx
|
|
|
|
// Signature verification is to mitigate against hardware failure while
|
|
// signing - which might occur during testing. So to prevent spurious
|
|
// errors, return a real known good signature in the success case.
|
|
goodSig []byte
|
|
|
|
failNum int // number of calls to fail before succeeding
|
|
signCalls int // number of calls to Sign so far
|
|
}
|
|
|
|
func (s *SignInvalidSigCtx) Sign(sh pkcs11.SessionHandle, message []byte) ([]byte, error) {
|
|
s.signCalls++
|
|
s.Ctx.Sign(sh, message) // clear out the SignInit
|
|
|
|
if s.signCalls > s.failNum {
|
|
return s.goodSig, nil
|
|
}
|
|
return []byte("12345"), nil
|
|
}
|