addressing review comments

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
Riyaz Faizullabhoy 2016-02-17 10:23:45 -08:00
parent 2a37590ea6
commit c7bccd79e3
5 changed files with 107 additions and 72 deletions

View File

@ -3,7 +3,6 @@ package cryptoservice
import ( import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"path/filepath"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/notary/trustmanager" "github.com/docker/notary/trustmanager"
@ -113,18 +112,22 @@ func (cs *CryptoService) RemoveKey(keyID string) (err error) {
// AddKey adds a private key to a specified role. // AddKey adds a private key to a specified role.
// The GUN is inferred from the cryptoservice itself for non-root roles // The GUN is inferred from the cryptoservice itself for non-root roles
func (cs *CryptoService) AddKey(key data.PrivateKey, role string) (err error) { func (cs *CryptoService) AddKey(key data.PrivateKey, role string) (err error) {
keyID := key.ID() // First check if this key already exists in any of our keystores
if role != data.CanonicalRootRole {
keyID = filepath.Join(cs.gun, key.ID())
}
for _, ks := range cs.keyStores { for _, ks := range cs.keyStores {
if keyInfo, err := ks.GetKeyInfo(keyID); err == nil { if keyInfo, err := ks.GetKeyInfo(key.ID()); err == nil {
if keyInfo.Role != role { if keyInfo.Role != role {
return fmt.Errorf("key with same ID already exists for role: %s", keyInfo.Role) return fmt.Errorf("key with same ID already exists for role: %s", keyInfo.Role)
} }
continue logrus.Debugf("key with same ID %s and role %s already exists", key.ID(), keyInfo.Role)
return nil
}
}
// If the key didn't exist in any of our keystores, add and return on the first successful keystore
for _, ks := range cs.keyStores {
// Try to add to this keystore, return if successful
if err = ks.AddKey(key, trustmanager.KeyInfo{Role: role, Gun: cs.gun}); err == nil {
return nil
} }
ks.AddKey(key, trustmanager.KeyInfo{Role: role, Gun: cs.gun})
} }
return // returns whatever the final values were return // returns whatever the final values were
} }

View File

@ -300,6 +300,53 @@ func (c CryptoServiceTester) TestListFromMultipleKeystores(t *testing.T) {
} }
} }
// asserts that adding a key adds to all keystores
// and adding an existing key either succeeds if the role matches or fails if it does not
func (c CryptoServiceTester) TestAddKey(t *testing.T) {
cryptoService := c.cryptoServiceFactory()
cryptoService.keyStores = append(cryptoService.keyStores,
trustmanager.NewKeyMemoryStore(passphraseRetriever))
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
assert.NoError(t, err)
// Add the key to the targets role
assert.NoError(t, cryptoService.AddKey(privKey, data.CanonicalTargetsRole))
// Check that we added the key and its info to only the first keystore
retrievedKey, retrievedRole, err := cryptoService.keyStores[0].GetKey(privKey.ID())
assert.NoError(t, err)
assert.Equal(t, privKey.Private(), retrievedKey.Private())
assert.Equal(t, data.CanonicalTargetsRole, retrievedRole)
retrievedKeyInfo, err := cryptoService.keyStores[0].GetKeyInfo(privKey.ID())
assert.NoError(t, err)
assert.Equal(t, data.CanonicalTargetsRole, retrievedKeyInfo.Role)
assert.Equal(t, cryptoService.gun, retrievedKeyInfo.Gun)
// The key should not exist in the second keystore
_, _, err = cryptoService.keyStores[1].GetKey(privKey.ID())
assert.Error(t, err)
_, err = cryptoService.keyStores[1].GetKeyInfo(privKey.ID())
assert.Error(t, err)
// We should be able to successfully get the key from the cryptoservice level
retrievedKey, retrievedRole, err = cryptoService.GetPrivateKey(privKey.ID())
assert.NoError(t, err)
assert.Equal(t, privKey.Private(), retrievedKey.Private())
assert.Equal(t, data.CanonicalTargetsRole, retrievedRole)
retrievedKeyInfo, err = cryptoService.GetKeyInfo(privKey.ID())
assert.NoError(t, err)
assert.Equal(t, data.CanonicalTargetsRole, retrievedKeyInfo.Role)
assert.Equal(t, cryptoService.gun, retrievedKeyInfo.Gun)
// Add the same key to the targets role, since the info is the same we should have no error
assert.NoError(t, cryptoService.AddKey(privKey, data.CanonicalTargetsRole))
// Try to add the same key to the snapshot role, which should error due to the role mismatch
assert.Error(t, cryptoService.AddKey(privKey, data.CanonicalSnapshotRole))
}
// Prints out an error message with information about the key algorithm, // Prints out an error message with information about the key algorithm,
// role, and test name. Ideally we could generate different tests given // role, and test name. Ideally we could generate different tests given
// data, without having to put for loops in one giant test function, but // data, without having to put for loops in one giant test function, but
@ -330,6 +377,7 @@ func testCryptoService(t *testing.T, gun string) {
keyAlgo: algo, keyAlgo: algo,
gun: gun, gun: gun,
} }
cst.TestAddKey(t)
cst.TestCreateAndGetKey(t) cst.TestCreateAndGetKey(t)
cst.TestCreateAndGetWhenMultipleKeystores(t) cst.TestCreateAndGetWhenMultipleKeystores(t)
cst.TestGetNonexistentKey(t) cst.TestGetNonexistentKey(t)

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/notary"
"github.com/docker/notary/trustmanager" "github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/data"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -80,13 +81,13 @@ func TestImportExportZip(t *testing.T) {
if alias == data.CanonicalRootRole { if alias == data.CanonicalRootRole {
continue continue
} }
relKeyPath := filepath.Join("tuf_keys", privKeyName+".key") relKeyPath := filepath.Join(notary.NonRootKeysSubdir, privKeyName+".key")
passphraseByFile[relKeyPath] = exportPassphrase passphraseByFile[relKeyPath] = exportPassphrase
} }
// Add root key to the map. This will use the export passphrase because it // Add root key to the map. This will use the export passphrase because it
// will be reencrypted. // will be reencrypted.
relRootKey := filepath.Join("root_keys", rootKeyID+".key") relRootKey := filepath.Join(notary.RootKeysSubdir, rootKeyID+".key")
passphraseByFile[relRootKey] = exportPassphrase passphraseByFile[relRootKey] = exportPassphrase
// Iterate through the files in the archive, checking that the files // Iterate through the files in the archive, checking that the files
@ -141,8 +142,8 @@ func TestImportExportZip(t *testing.T) {
if alias == data.CanonicalRootRole { if alias == data.CanonicalRootRole {
continue continue
} }
relKeyPath := filepath.Join("tuf_keys", privKeyName+".key") relKeyPath := filepath.Join(notary.NonRootKeysSubdir, privKeyName+".key")
privKeyFileName := filepath.Join(tempBaseDir2, "private", relKeyPath) privKeyFileName := filepath.Join(tempBaseDir2, notary.PrivDir, relKeyPath)
_, err = os.Stat(privKeyFileName) _, err = os.Stat(privKeyFileName)
assert.NoError(t, err, "missing private key for role %s: %s", alias, privKeyName) assert.NoError(t, err, "missing private key for role %s: %s", alias, privKeyName)
} }
@ -151,7 +152,7 @@ func TestImportExportZip(t *testing.T) {
// There should be a file named after the key ID of the root key we // There should be a file named after the key ID of the root key we
// passed in. // passed in.
rootKeyFilename := rootKeyID + ".key" rootKeyFilename := rootKeyID + ".key"
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "root_keys", rootKeyFilename)) _, err = os.Stat(filepath.Join(tempBaseDir2, notary.PrivDir, notary.RootKeysSubdir, rootKeyFilename))
assert.NoError(t, err, "missing root key") assert.NoError(t, err, "missing root key")
} }
@ -199,7 +200,7 @@ func TestImportExportGUN(t *testing.T) {
if alias == data.CanonicalRootRole { if alias == data.CanonicalRootRole {
continue continue
} }
relKeyPath := filepath.Join("tuf_keys", gun, privKeyName+".key") relKeyPath := filepath.Join(notary.NonRootKeysSubdir, gun, privKeyName+".key")
passphraseByFile[relKeyPath] = exportPassphrase passphraseByFile[relKeyPath] = exportPassphrase
} }
@ -258,8 +259,8 @@ func TestImportExportGUN(t *testing.T) {
if alias == data.CanonicalRootRole { if alias == data.CanonicalRootRole {
continue continue
} }
relKeyPath := filepath.Join("tuf_keys", gun, privKeyName+".key") relKeyPath := filepath.Join(notary.NonRootKeysSubdir, gun, privKeyName+".key")
privKeyFileName := filepath.Join(tempBaseDir2, "private", relKeyPath) privKeyFileName := filepath.Join(tempBaseDir2, notary.PrivDir, relKeyPath)
_, err = os.Stat(privKeyFileName) _, err = os.Stat(privKeyFileName)
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -307,7 +308,7 @@ func TestImportExportRootKey(t *testing.T) {
// There should be a file named after the key ID of the root key we // There should be a file named after the key ID of the root key we
// imported. // imported.
rootKeyFilename := rootKeyID + ".key" rootKeyFilename := rootKeyID + ".key"
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "root_keys", rootKeyFilename)) _, err = os.Stat(filepath.Join(tempBaseDir2, notary.PrivDir, notary.RootKeysSubdir, rootKeyFilename))
assert.NoError(t, err, "missing root key") assert.NoError(t, err, "missing root key")
// Try to import a decrypted version of the root key and make sure it // Try to import a decrypted version of the root key and make sure it
@ -375,7 +376,7 @@ func TestImportExportRootKeyReencrypt(t *testing.T) {
// There should be a file named after the key ID of the root key we // There should be a file named after the key ID of the root key we
// imported. // imported.
rootKeyFilename := rootKeyID + ".key" rootKeyFilename := rootKeyID + ".key"
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "root_keys", rootKeyFilename)) _, err = os.Stat(filepath.Join(tempBaseDir2, notary.PrivDir, notary.RootKeysSubdir, rootKeyFilename))
assert.NoError(t, err, "missing root key") assert.NoError(t, err, "missing root key")
// Should be able to unlock the root key with the new password // Should be able to unlock the root key with the new password
@ -430,7 +431,7 @@ func TestImportExportNonRootKey(t *testing.T) {
// There should be a file named after the key ID of the targets key we // There should be a file named after the key ID of the targets key we
// imported. // imported.
targetsKeyFilename := targetsKeyID + ".key" targetsKeyFilename := targetsKeyID + ".key"
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "tuf_keys", "docker.com/notary", targetsKeyFilename)) _, err = os.Stat(filepath.Join(tempBaseDir2, notary.PrivDir, notary.NonRootKeysSubdir, "docker.com/notary", targetsKeyFilename))
assert.NoError(t, err, "missing targets key") assert.NoError(t, err, "missing targets key")
// Check that the key is the same // Check that the key is the same
@ -485,7 +486,7 @@ func TestImportExportNonRootKeyReencrypt(t *testing.T) {
// There should be a file named after the key ID of the snapshot key we // There should be a file named after the key ID of the snapshot key we
// imported. // imported.
snapshotKeyFilename := snapshotKeyID + ".key" snapshotKeyFilename := snapshotKeyID + ".key"
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "tuf_keys", "docker.com/notary", snapshotKeyFilename)) _, err = os.Stat(filepath.Join(tempBaseDir2, notary.PrivDir, notary.NonRootKeysSubdir, "docker.com/notary", snapshotKeyFilename))
assert.NoError(t, err, "missing snapshot key") assert.NoError(t, err, "missing snapshot key")
// Should be able to unlock the root key with the new password // Should be able to unlock the root key with the new password

View File

@ -65,45 +65,13 @@ func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (
func generateKeyInfoMap(s LimitedFileStore) map[string]KeyInfo { func generateKeyInfoMap(s LimitedFileStore) map[string]KeyInfo {
keyInfoMap := make(map[string]KeyInfo) keyInfoMap := make(map[string]KeyInfo)
for _, keyPath := range s.ListFiles() { for _, keyPath := range s.ListFiles() {
// Remove the prefix of the directory from the filename for GUN/role/ID parsing
var keyIDAndGun, keyRole string
if strings.HasPrefix(keyPath, notary.RootKeysSubdir+"/") {
keyIDAndGun = strings.TrimPrefix(keyPath, notary.RootKeysSubdir+"/")
} else {
keyIDAndGun = strings.TrimPrefix(keyPath, notary.NonRootKeysSubdir+"/")
}
// Separate the ID and GUN (can be empty) from the filepath
keyIDAndGun = strings.TrimSpace(keyIDAndGun)
keyGun := getGunFromFullID(keyIDAndGun)
keyID := filepath.Base(keyIDAndGun)
// If the key does not have a _, we'll attempt to
// read the role from PEM headers
underscoreIndex := strings.LastIndex(keyID, "_")
if underscoreIndex == -1 {
d, err := s.Get(keyPath) d, err := s.Get(keyPath)
if err != nil { if err != nil {
logrus.Error(err) logrus.Error(err)
continue continue
} }
block, _ := pem.Decode(d) keyID, keyInfo := getImportedKeyInfo(d, keyPath)
if block == nil { keyInfoMap[keyID] = keyInfo
continue
}
if role, ok := block.Headers["role"]; ok {
keyRole = role
}
} else {
// This is the legacy KEYID_ROLE filename
// The keyID is the first part of the keyname
// The keyRole is the second part of the keyname
// in a key named abcde_root, abcde is the keyID and root is the KeyAlias
legacyID := keyID
keyID = legacyID[:underscoreIndex]
keyRole = legacyID[underscoreIndex+1:]
}
keyInfoMap[keyID] = KeyInfo{Gun: keyGun, Role: keyRole}
} }
return keyInfoMap return keyInfoMap
} }
@ -173,9 +141,9 @@ func (s *KeyFileStore) GetKey(name string) (data.PrivateKey, string, error) {
return getKey(s, s.Retriever, s.cachedKeys, name) return getKey(s, s.Retriever, s.cachedKeys, name)
} }
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore. // ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap
func (s *KeyFileStore) ListKeys() map[string]KeyInfo { func (s *KeyFileStore) ListKeys() map[string]KeyInfo {
return s.keyInfoMap return copyKeyInfoMap(s.keyInfoMap)
} }
// RemoveKey removes the key from the keyfilestore // RemoveKey removes the key from the keyfilestore
@ -215,7 +183,6 @@ func (s *KeyFileStore) ExportKey(name string) ([]byte, error) {
// ImportKey imports the private key in the encrypted bytes into the keystore // ImportKey imports the private key in the encrypted bytes into the keystore
// with the given key ID and alias. // with the given key ID and alias.
// This is only used for root, so no need to touch the keyInfoMap
func (s *KeyFileStore) ImportKey(pemBytes []byte, alias string) error { func (s *KeyFileStore) ImportKey(pemBytes []byte, alias string) error {
err := importKey(s, s.Retriever, s.cachedKeys, alias, pemBytes) err := importKey(s, s.Retriever, s.cachedKeys, alias, pemBytes)
if err != nil { if err != nil {
@ -276,9 +243,18 @@ func (s *KeyMemoryStore) GetKey(name string) (data.PrivateKey, string, error) {
return getKey(s, s.Retriever, s.cachedKeys, name) return getKey(s, s.Retriever, s.cachedKeys, name)
} }
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore. // ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap
func (s *KeyMemoryStore) ListKeys() map[string]KeyInfo { func (s *KeyMemoryStore) ListKeys() map[string]KeyInfo {
return s.keyInfoMap return copyKeyInfoMap(s.keyInfoMap)
}
// copyKeyInfoMap returns a deep copy of the passed-in keyInfoMap
func copyKeyInfoMap(keyInfoMap map[string]KeyInfo) map[string]KeyInfo {
copyMap := make(map[string]KeyInfo)
for keyID, keyInfo := range keyInfoMap {
copyMap[keyID] = KeyInfo{Role: keyInfo.Role, Gun: keyInfo.Gun}
}
return copyMap
} }
// RemoveKey removes the key from the keystore // RemoveKey removes the key from the keystore
@ -328,21 +304,23 @@ func (s *KeyMemoryStore) ImportKey(pemBytes []byte, alias string) error {
func getImportedKeyInfo(pemBytes []byte, filename string) (string, KeyInfo) { func getImportedKeyInfo(pemBytes []byte, filename string) (string, KeyInfo) {
var keyID, gun, role string var keyID, gun, role string
// Try to read the role and gun from the filepath and PEM headers // Try to read the role and gun from the filepath and PEM headers
if filepath.HasPrefix(filename, "root_keys") { if filepath.HasPrefix(filename, notary.RootKeysSubdir+"/") {
role = data.CanonicalRootRole role = data.CanonicalRootRole
gun = "" gun = ""
filename = strings.TrimPrefix(filename, "root_keys") filename = strings.TrimPrefix(filename, notary.RootKeysSubdir+"/")
} else { } else {
filename = strings.TrimPrefix(filename, "tuf_keys") filename = strings.TrimPrefix(filename, notary.NonRootKeysSubdir+"/")
gun = getGunFromFullID(filename) gun = getGunFromFullID(filename)
} }
keyIDFull := filepath.Base(filename) keyIDFull := filepath.Base(filename)
underscoreIndex := strings.LastIndex(keyIDFull, "_") underscoreIndex := strings.LastIndex(keyIDFull, "_")
if underscoreIndex == -1 { if underscoreIndex == -1 {
block, _ := pem.Decode(pemBytes) block, _ := pem.Decode(pemBytes)
if block != nil {
if keyRole, ok := block.Headers["role"]; ok { if keyRole, ok := block.Headers["role"]; ok {
role = keyRole role = keyRole
} }
}
keyID = filepath.Base(keyIDFull) keyID = filepath.Base(keyIDFull)
} else { } else {
// This is the legacy KEYID_ROLE filename // This is the legacy KEYID_ROLE filename
@ -458,7 +436,7 @@ func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string
// Assumes 2 subdirectories, 1 containing root keys and 1 containing tuf keys // Assumes 2 subdirectories, 1 containing root keys and 1 containing tuf keys
func getSubdir(alias string) string { func getSubdir(alias string) string {
if alias == "root" { if alias == data.CanonicalRootRole {
return notary.RootKeysSubdir return notary.RootKeysSubdir
} }
return notary.NonRootKeysSubdir return notary.NonRootKeysSubdir

View File

@ -169,11 +169,9 @@ func TestGet(t *testing.T) {
// Root role needs to go in the rootKeySubdir to be read. // Root role needs to go in the rootKeySubdir to be read.
// All other roles can go in the nonRootKeysSubdir, possibly under a GUN // All other roles can go in the nonRootKeysSubdir, possibly under a GUN
rootKeysSubdirWithGUN := filepath.Clean(filepath.Join(notary.RootKeysSubdir, gun))
nonRootKeysSubdirWithGUN := filepath.Clean(filepath.Join(notary.NonRootKeysSubdir, gun)) nonRootKeysSubdirWithGUN := filepath.Clean(filepath.Join(notary.NonRootKeysSubdir, gun))
testGetKeyWithRole(t, "", data.CanonicalRootRole, notary.RootKeysSubdir, true) testGetKeyWithRole(t, "", data.CanonicalRootRole, notary.RootKeysSubdir, true)
testGetKeyWithRole(t, gun, data.CanonicalRootRole, rootKeysSubdirWithGUN, true)
for _, role := range nonRootRolesToTest { for _, role := range nonRootRolesToTest {
testGetKeyWithRole(t, "", role, notary.NonRootKeysSubdir, true) testGetKeyWithRole(t, "", role, notary.NonRootKeysSubdir, true)
testGetKeyWithRole(t, gun, role, nonRootKeysSubdirWithGUN, true) testGetKeyWithRole(t, gun, role, nonRootKeysSubdirWithGUN, true)
@ -185,7 +183,6 @@ func TestGet(t *testing.T) {
testGetKeyWithRole(t, gun, data.CanonicalRootRole, nonRootKeysSubdirWithGUN, false) testGetKeyWithRole(t, gun, data.CanonicalRootRole, nonRootKeysSubdirWithGUN, false)
for _, role := range nonRootRolesToTest { for _, role := range nonRootRolesToTest {
testGetKeyWithRole(t, "", role, notary.RootKeysSubdir, false) testGetKeyWithRole(t, "", role, notary.RootKeysSubdir, false)
testGetKeyWithRole(t, gun, role, rootKeysSubdirWithGUN, false)
} }
} }
@ -367,6 +364,14 @@ func TestListKeys(t *testing.T) {
// Check to see if the keystore still lists two keys // Check to see if the keystore still lists two keys
keyMap := store.ListKeys() keyMap := store.ListKeys()
assert.Len(t, keyMap, len(roles)) assert.Len(t, keyMap, len(roles))
// Check that ListKeys() returns a copy of the state
// so modifying its returned information does not change the underlying store's keyInfo
for keyID := range keyMap {
delete(keyMap, keyID)
_, err = store.GetKeyInfo(keyID)
assert.NoError(t, err)
}
} }
func TestAddGetKeyMemStore(t *testing.T) { func TestAddGetKeyMemStore(t *testing.T) {