diff --git a/trustmanager/yubikey/yubikeystore_test.go b/trustmanager/yubikey/yubikeystore_test.go index 9d7863d62b..a5c10a0375 100644 --- a/trustmanager/yubikey/yubikeystore_test.go +++ b/trustmanager/yubikey/yubikeystore_test.go @@ -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()) }