Add more unit tests for the YubiKeyStore.

Including how it interacts with the backup key store, and with more
assertions against a new YubiKeyStore so that we won't get false
positives or negatives from the cache.

Signed-off-by: Ying Li <ying.li@docker.com>
Signed-off-by: David Lawrence <david.lawrence@docker.com>

Signed-off-by: Ying Li <ying.li@docker.com> (github: endophage)
This commit is contained in:
Ying Li 2015-11-09 19:32:55 -08:00 committed by David Lawrence
parent f6ecd1c1ca
commit 7108450a21
1 changed files with 300 additions and 55 deletions

View File

@ -3,6 +3,7 @@
package yubikey
import (
"bytes"
"crypto/rand"
"reflect"
"testing"
@ -13,11 +14,11 @@ import (
"github.com/stretchr/testify/assert"
)
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) {
// TODO(cyli): this is creating a new yubikey store because for some reason,
// removing and then adding with the same YubiKeyStore causes
// non-deterministic failures at least on Mac OS
ret := passphrase.ConstantRetriever("passphrase")
store, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
@ -53,7 +54,7 @@ func TestEnsurePrivateKeySizePadsLessThanRequiredSizeArrays(t *testing.T) {
assert.True(t, reflect.DeepEqual(expected, result))
}
func testAddKey(t *testing.T, store *YubiKeyStore) (data.PrivateKey, error) {
func testAddKey(t *testing.T, store trustmanager.KeyStore) (data.PrivateKey, error) {
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
assert.NoError(t, err)
@ -61,68 +62,207 @@ func testAddKey(t *testing.T, store *YubiKeyStore) (data.PrivateKey, error) {
return privKey, err
}
func TestAddKeyToNextEmptyYubikeySlot(t *testing.T) {
if !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
ret := passphrase.ConstantRetriever("passphrase")
store, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
func addMaxKeys(t *testing.T, store trustmanager.KeyStore) []string {
keys := make([]string, 0, numSlots)
// create the maximum number of keys
for i := 0; i < numSlots; i++ {
privKey, err := testAddKey(t, store)
assert.NoError(t, err)
keys = append(keys, privKey.ID())
}
// create a new store, to make sure we're not just using the keys cache
store, err = NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
listedKeys := store.ListKeys()
assert.Len(t, listedKeys, numSlots)
for _, k := range keys {
r, ok := listedKeys[k]
assert.True(t, ok)
assert.Equal(t, data.CanonicalRootRole, r)
}
// add another key - should fail because there are no more slots
_, err = testAddKey(t, store)
assert.Error(t, err)
// delete one of the middle keys, and assert we can still create a new key
err = store.RemoveKey(keys[numSlots/2])
assert.NoError(t, err)
_, err = testAddKey(t, store)
assert.NoError(t, err)
return keys
}
// ImportKey imports a key as root without adding it to the backup store
func TestImportKey(t *testing.T) {
// 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 !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
ret := passphrase.ConstantRetriever("passphrase")
backup := trustmanager.NewKeyMemoryStore(ret)
store, err := NewYubiKeyStore(backup, ret)
assert.NoError(t, err)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
// create 4 keys on the original store
backup := trustmanager.NewKeyMemoryStore(ret)
origStore, err := NewYubiKeyStore(backup, ret)
assert.NoError(t, err)
keys := addMaxKeys(t, origStore)
// create a new store, since we want to be sure the original store's cache
// is not masking any issues
cleanStore, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.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{origStore, cleanStore, backup} {
listedKeys := store.ListKeys()
assert.Len(t, listedKeys, numSlots)
for _, k := range keys {
r, ok := listedKeys[k]
assert.True(t, ok)
assert.Equal(t, data.CanonicalRootRole, r)
_, _, err := store.GetKey(k)
assert.NoError(t, err)
}
}
}
// We can't add a key if there are no more slots
func TestYubiAddKeyFailureIfNoMoreSlots(t *testing.T) {
if !YubikeyAccessible() {
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)
origStore, err := NewYubiKeyStore(backup, ret)
assert.NoError(t, err)
addMaxKeys(t, origStore)
// add another key - should fail because there are no more slots
badKey, err := testAddKey(t, origStore)
assert.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 := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.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{origStore, cleanStore, backup} {
// the key that wasn't created should not appear in ListKeys or GetKey
_, _, err := store.GetKey(badKey.ID())
assert.Error(t, err)
for k := range store.ListKeys() {
assert.NotEqual(t, badKey, 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 !YubikeyAccessible() {
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)
origStore, err := NewYubiKeyStore(backup, ret)
assert.NoError(t, err)
keys := addMaxKeys(t, origStore)
// delete one of the middle keys, and assert we can still create a new key
keyIDToDelete := keys[numSlots/2]
err = origStore.RemoveKey(keyIDToDelete)
assert.NoError(t, err)
newKey, err := testAddKey(t, origStore)
assert.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 := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.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{origStore, cleanStore, backup} {
// new key should appear in all stores
gottenKey, _, err := store.GetKey(newKey.ID())
assert.NoError(t, err)
assert.Equal(t, gottenKey.ID(), newKey.ID())
listedKeys := store.ListKeys()
_, ok := listedKeys[newKey.ID()]
assert.True(t, ok)
// old key should not be in the non-backup stores
if store != backup {
_, _, err := store.GetKey(keyIDToDelete)
assert.Error(t, err)
_, ok = listedKeys[keyIDToDelete]
assert.False(t, ok)
}
}
}
// RemoveKey removes a key from the yubikey, but not from the backup store.
func TestYubiRemoveKey(t *testing.T) {
if !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
backup := trustmanager.NewKeyMemoryStore(ret)
origStore, err := NewYubiKeyStore(backup, ret)
assert.NoError(t, err)
key, err := testAddKey(t, origStore)
assert.NoError(t, err)
err = origStore.RemoveKey(key.ID())
assert.NoError(t, err)
// key remains in the backup store
backupKey, role, err := backup.GetKey(key.ID())
assert.NoError(t, err)
assert.Equal(t, data.CanonicalRootRole, role)
assert.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 := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
// key is not in either the original store or the clean store
for _, store := range []*YubiKeyStore{origStore, cleanStore} {
_, _, err := store.GetKey(key.ID())
assert.Error(t, err)
}
}
// ImportKey imports a key as root without adding it to the backup store
func TestYubiImportKey(t *testing.T) {
if !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
backup := trustmanager.NewKeyMemoryStore(ret)
origStore, err := NewYubiKeyStore(backup, ret)
assert.NoError(t, err)
// generate key and import it
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
assert.NoError(t, err)
@ -130,18 +270,123 @@ func TestImportKey(t *testing.T) {
pemBytes, err := trustmanager.EncryptPrivateKey(privKey, "passphrase")
assert.NoError(t, err)
err = store.ImportKey(pemBytes, "root")
err = origStore.ImportKey(pemBytes, "root")
assert.NoError(t, err)
// key is not in backup store
_, _, err = backup.GetKey(privKey.ID())
assert.Error(t, err)
// ensure key is in Yubikey - create a new store, to make sure we're not
// just using the keys cache
store, err = NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
gottenKey, role, err := store.GetKey(privKey.ID())
// create a new store, since we want to be sure the original store's cache
// is not masking any issues
cleanStore, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
for _, store := range []*YubiKeyStore{origStore, cleanStore} {
gottenKey, role, err := store.GetKey(privKey.ID())
assert.NoError(t, err)
assert.Equal(t, data.CanonicalRootRole, role)
assert.Equal(t, privKey.Public(), gottenKey.Public())
}
}
// One cannot export from hardware - it will not export from the backup
func TestYubiExportKeyFails(t *testing.T) {
if !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
store, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
key, err := testAddKey(t, store)
assert.NoError(t, err)
_, err = store.ExportKey(key.ID())
assert.Error(t, err)
}
// 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 !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
backup := trustmanager.NewKeyMemoryStore(ret)
key, err := testAddKey(t, backup)
assert.NoError(t, err)
store, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.Len(t, store.ListKeys(), 0)
_, _, err = store.GetKey(key.ID())
assert.Error(t, err)
}
// Get a YubiPrivateKey. Check that it has the right algorithm, etc, and
// specifically that you cannot get the private bytes out.
func TestYubiKey(t *testing.T) {
if !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
store, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
ecdsaPrivateKey, err := testAddKey(t, store)
assert.NoError(t, err)
yubiPrivateKey, _, err := store.GetKey(ecdsaPrivateKey.ID())
assert.NoError(t, err)
assert.Equal(t, data.ECDSAKey, yubiPrivateKey.Algorithm())
assert.Equal(t, data.ECDSASignature, yubiPrivateKey.SignatureAlgorithm())
assert.Equal(t, ecdsaPrivateKey.Public(), yubiPrivateKey.Public())
assert.Nil(t, yubiPrivateKey.Private())
}
// Get a YubiPrivateKey. Sign something with it.
func TestYubiSigning(t *testing.T) {
// TODO(cyli): the signature should be verified, but the importing the
// verifiers causes an import cycle. A bigger refactor needs to be done
// to fix it.
if !YubikeyAccessible() {
t.Skip("Must have Yubikey access.")
}
clearAllKeys(t)
SetYubikeyKeyMode(KeymodeNone)
defer func() {
SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce)
}()
store, err := NewYubiKeyStore(trustmanager.NewKeyMemoryStore(ret), ret)
assert.NoError(t, err)
ecdsaPrivateKey, err := testAddKey(t, store)
assert.NoError(t, err)
yubiPrivateKey, _, err := store.GetKey(ecdsaPrivateKey.ID())
assert.NoError(t, err)
msg := []byte("Hello there")
_, err = yubiPrivateKey.Sign(bytes.NewBuffer(msg), msg, nil)
assert.NoError(t, err)
assert.Equal(t, data.CanonicalRootRole, role)
assert.Equal(t, privKey.Public(), gottenKey.Public())
}