diff --git a/keystoremanager/import_export.go b/keystoremanager/import_export.go index c46c2704c0..b36fdf112b 100644 --- a/keystoremanager/import_export.go +++ b/keystoremanager/import_export.go @@ -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 } diff --git a/keystoremanager/export_test.go b/keystoremanager/import_export_test.go similarity index 58% rename from keystoremanager/export_test.go rename to keystoremanager/import_export_test.go index be72f06078..d270098205 100644 --- a/keystoremanager/export_test.go +++ b/keystoremanager/import_export_test.go @@ -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") }