mirror of https://github.com/docker/docs.git
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:
parent
f6ecd1c1ca
commit
7108450a21
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue