Add ImportKeysZip and test coverage

This function reads a zip file and populates the keystores with the keys
in the zip file. Root keys are left encrypted, and non-root keys are
decrypted before being added to the keystore.

The unit test first exports a repo's keys to a zip file, then imports it
into another repo. It checks that all the correct keys exist in the new
repo after the import operation.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This commit is contained in:
Aaron Lehmann
2015-07-15 11:02:55 -07:00
parent 878a8a083d
commit eb8a7a0e25
2 changed files with 138 additions and 12 deletions

View File

@@ -11,7 +11,9 @@ import (
"path/filepath"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/notary/trustmanager"
"github.com/endophage/gotuf/data"
)
func moveKeysWithNewPassphrase(oldKeyStore, newKeyStore *trustmanager.KeyFileStore, outputPassphrase string) error {
@@ -138,10 +140,75 @@ func (km *KeyStoreManager) ExportAllKeys(dest io.Writer, outputPassphrase string
return nil
}
// ImportZip imports keys from a zip file provided as an io.Reader. The keys
// in the root_keys directory are left encrypted, but the other keys are
// ImportKeysZip imports keys from a zip file provided as an io.ReaderAt. The
// keys in the root_keys directory are left encrypted, but the other keys are
// decrypted with the specified passphrase.
func (km *KeyStoreManager) ImportZip(zip io.Reader, passphrase string) error {
// TODO(aaronl)
func (km *KeyStoreManager) ImportKeysZip(zipReader zip.Reader, passphrase string) error {
// Temporarily store the keys in maps, so we can bail early if there's
// an error (for example, wrong passphrase), without leaving the key
// store in an inconsistent state
newRootKeys := make(map[string][]byte)
newNonRootKeys := make(map[string]*data.PrivateKey)
// Iterate through the files in the archive. Don't add the keys
for _, f := range zipReader.File {
fNameTrimmed := strings.TrimSuffix(f.Name, filepath.Ext(f.Name))
// Note that using / as a separator is okay here - the zip
// package guarantees that the separator will be /
keysPrefix := privDir + "/"
if !strings.HasPrefix(fNameTrimmed, keysPrefix) {
// This path inside the zip archive doesn't start with
// "private". That's unexpected, because all keys
// should be in that subdirectory. To avoid adding a
// file to the filestore that we won't be able to use,
// skip this file in the import.
logrus.Warnf("skipping import of key with a path that doesn't begin with %s: %s", keysPrefix, f.Name)
continue
}
fNameTrimmed = strings.TrimPrefix(fNameTrimmed, keysPrefix)
rc, err := f.Open()
if err != nil {
return err
}
pemBytes, err := ioutil.ReadAll(rc)
if err != nil {
return nil
}
// Is this in the root_keys directory?
// Note that using / as a separator is okay here - the zip
// package guarantees that the separator will be /
rootKeysPrefix := rootKeysSubdir + "/"
if strings.HasPrefix(fNameTrimmed, rootKeysPrefix) {
// Root keys are preserved without decrypting
keyName := strings.TrimPrefix(fNameTrimmed, rootKeysPrefix)
newRootKeys[keyName] = pemBytes
} else {
// Non-root keys need to be decrypted
key, err := trustmanager.ParsePEMPrivateKey(pemBytes, passphrase)
if err != nil {
return err
}
newNonRootKeys[fNameTrimmed] = key
}
rc.Close()
}
for keyName, pemBytes := range newRootKeys {
if err := km.rootKeyStore.Add(keyName, pemBytes); err != nil {
return err
}
}
for keyName, privKey := range newNonRootKeys {
if err := km.nonRootKeyStore.AddKey(keyName, privKey); err != nil {
return err
}
}
return nil
}

View File

@@ -35,8 +35,11 @@ func createTestServer(t *testing.T) (*httptest.Server, *http.ServeMux) {
return ts, mux
}
func TestExportKeys(t *testing.T) {
func TestImportExportZip(t *testing.T) {
gun := "docker.com/notary"
oldPassphrase := "oldPassphrase"
exportPassphrase := "exportPassphrase"
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
@@ -49,10 +52,10 @@ func TestExportKeys(t *testing.T) {
repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport)
assert.NoError(t, err, "error creating repo: %s", err)
rootKeyID, err := repo.KeyStoreManager.GenRootKey(data.ECDSAKey.String(), "oldPassphrase")
rootKeyID, err := repo.KeyStoreManager.GenRootKey(data.ECDSAKey.String(), oldPassphrase)
assert.NoError(t, err, "error generating root key: %s", err)
rootCryptoService, err := repo.KeyStoreManager.GetRootCryptoService(rootKeyID, "oldPassphrase")
rootCryptoService, err := repo.KeyStoreManager.GetRootCryptoService(rootKeyID, oldPassphrase)
assert.NoError(t, err, "error retrieving root key: %s", err)
err = repo.Initialize(rootCryptoService)
@@ -62,14 +65,13 @@ func TestExportKeys(t *testing.T) {
tempZipFilePath := tempZipFile.Name()
defer os.Remove(tempZipFilePath)
err = repo.KeyStoreManager.ExportAllKeys(tempZipFile, "exportPassphrase")
err = repo.KeyStoreManager.ExportAllKeys(tempZipFile, exportPassphrase)
tempZipFile.Close()
assert.NoError(t, err)
// Check the contents of the zip file
zipReader, err := zip.OpenReader(tempZipFilePath)
assert.NoError(t, err, "could not open zip file")
defer zipReader.Close()
// Map of files to expect in the zip file, with the passphrases
passphraseByFile := make(map[string]string)
@@ -79,13 +81,13 @@ func TestExportKeys(t *testing.T) {
privKeyList := repo.KeyStoreManager.NonRootKeyStore().ListFiles(false)
for _, privKeyName := range privKeyList {
relName := strings.TrimPrefix(privKeyName, tempBaseDir+string(filepath.Separator))
passphraseByFile[relName] = "exportPassphrase"
passphraseByFile[relName] = exportPassphrase
}
// Add root key to the map. This will use the old passphrase because it
// won't be reencrypted.
relRootKey := filepath.Join("private", "root_keys", rootCryptoService.ID()+".key")
passphraseByFile[relRootKey] = "oldPassphrase"
passphraseByFile[relRootKey] = oldPassphrase
// Iterate through the files in the archive, checking that the files
// exist and are encrypted with the expected passphrase.
@@ -109,8 +111,65 @@ func TestExportKeys(t *testing.T) {
rc.Close()
}
zipReader.Close()
// Are there any keys that didn't make it to the zip?
for fileNotFound, _ := range passphraseByFile {
for fileNotFound := range passphraseByFile {
t.Fatalf("%s not found in zip", fileNotFound)
}
// Create new repo to test import
tempBaseDir2, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir2)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
repo2, err := client.NewNotaryRepository(tempBaseDir2, gun, ts.URL, http.DefaultTransport)
assert.NoError(t, err, "error creating repo: %s", err)
rootKeyID2, err := repo2.KeyStoreManager.GenRootKey(data.ECDSAKey.String(), "oldPassphrase")
assert.NoError(t, err, "error generating root key: %s", err)
rootCryptoService2, err := repo2.KeyStoreManager.GetRootCryptoService(rootKeyID2, "oldPassphrase")
assert.NoError(t, err, "error retrieving root key: %s", err)
err = repo2.Initialize(rootCryptoService2)
assert.NoError(t, err, "error creating repository: %s", err)
// Reopen the zip file for importing
zipReader, err = zip.OpenReader(tempZipFilePath)
assert.NoError(t, err, "could not open zip file")
// First try with an incorrect passphrase
err = repo2.KeyStoreManager.ImportKeysZip(zipReader.Reader, "wrongpassphrase")
// Don't use EqualError here because occasionally decrypting with the
// wrong passphrase returns a parse error
assert.Error(t, err)
zipReader.Close()
// Reopen the zip file for importing
zipReader, err = zip.OpenReader(tempZipFilePath)
assert.NoError(t, err, "could not open zip file")
// Now try with a valid passphrase. This time it should succeed.
err = repo2.KeyStoreManager.ImportKeysZip(zipReader.Reader, exportPassphrase)
assert.NoError(t, err)
zipReader.Close()
// Look for repo's keys in repo2
// Look for keys in private. The filenames should match the key IDs
// in the repo's private key store.
for _, privKeyName := range privKeyList {
privKeyRel := strings.TrimPrefix(privKeyName, tempBaseDir)
_, err := os.Stat(filepath.Join(tempBaseDir2, privKeyRel))
assert.NoError(t, err, "missing private key: %s", privKeyName)
}
// Look for keys in root_keys
// There should be a file named after the key ID of the root key we
// passed in.
rootKeyFilename := rootCryptoService.ID() + ".key"
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "root_keys", rootKeyFilename))
assert.NoError(t, err, "missing root key")
}