diff --git a/client/client.go b/client/client.go index 8835cb17f4..9ec4142ff0 100644 --- a/client/client.go +++ b/client/client.go @@ -95,7 +95,7 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper) (*N gun: gun, baseDir: baseDir, baseURL: baseURL, - tufRepoPath: filepath.Join(baseDir, tufDir, gun), + tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), cryptoService: cryptoService, roundTrip: rt, KeyStoreManager: keyStoreManager, diff --git a/client/client_test.go b/client/client_test.go index 5df2cd1034..7e3312b14d 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -75,13 +75,13 @@ func testInitRepo(t *testing.T, rootType data.KeyAlgorithm) { // Inspect contents of the temporary directory expectedDirs := []string{ "private", - filepath.Join("private", "tuf_keys", gun), + filepath.Join("private", "tuf_keys", filepath.FromSlash(gun)), filepath.Join("private", "root_keys"), "trusted_certificates", - filepath.Join("trusted_certificates", gun), + filepath.Join("trusted_certificates", filepath.FromSlash(gun)), "tuf", - filepath.Join("tuf", gun, "metadata"), - filepath.Join("tuf", gun, "targets"), + filepath.Join("tuf", filepath.FromSlash(gun), "metadata"), + filepath.Join("tuf", filepath.FromSlash(gun), "targets"), } for _, dir := range expectedDirs { fi, err := os.Stat(filepath.Join(tempBaseDir, dir)) @@ -118,16 +118,16 @@ func testInitRepo(t *testing.T, rootType data.KeyAlgorithm) { assert.Equal(t, rootKeyFilename, actualDest, "symlink to root key has wrong destination") // There should be a trusted certificate - _, err = os.Stat(filepath.Join(tempBaseDir, "trusted_certificates", gun, certID+".crt")) + _, err = os.Stat(filepath.Join(tempBaseDir, "trusted_certificates", filepath.FromSlash(gun), certID+".crt")) assert.NoError(t, err, "missing trusted certificate") // Sanity check the TUF metadata files. Verify that they exist, the JSON is // well-formed, and the signatures exist. For the root.json file, also check // that the root, snapshot, and targets key IDs are present. expectedTUFMetadataFiles := []string{ - filepath.Join("tuf", gun, "metadata", "root.json"), - filepath.Join("tuf", gun, "metadata", "snapshot.json"), - filepath.Join("tuf", gun, "metadata", "targets.json"), + filepath.Join("tuf", filepath.FromSlash(gun), "metadata", "root.json"), + filepath.Join("tuf", filepath.FromSlash(gun), "metadata", "snapshot.json"), + filepath.Join("tuf", filepath.FromSlash(gun), "metadata", "targets.json"), } for _, filename := range expectedTUFMetadataFiles { fullPath := filepath.Join(tempBaseDir, filename) @@ -225,7 +225,7 @@ func testAddListTarget(t *testing.T, rootType data.KeyAlgorithm) { assert.NoError(t, err, "error adding target") // Look for the changelist file - changelistDirPath := filepath.Join(tempBaseDir, "tuf", gun, "changelist") + changelistDirPath := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist") changelistDir, err := os.Open(changelistDirPath) assert.NoError(t, err, "could not open changelist directory") @@ -299,7 +299,7 @@ func testAddListTarget(t *testing.T, rootType data.KeyAlgorithm) { // Apply the changelist. Normally, this would be done by Publish // load the changelist for this repo - cl, err := changelist.NewFileChangelist(filepath.Join(tempBaseDir, "tuf", gun, "changelist")) + cl, err := changelist.NewFileChangelist(filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "changelist")) assert.NoError(t, err, "could not open changelist") // apply the changelist to the repo @@ -309,10 +309,10 @@ func testAddListTarget(t *testing.T, rootType data.KeyAlgorithm) { var tempKey data.PrivateKey json.Unmarshal([]byte(timestampECDSAKeyJSON), &tempKey) - repo.KeyStoreManager.NonRootKeyStore().AddKey(filepath.Join(gun, tempKey.ID()), &tempKey) + repo.KeyStoreManager.NonRootKeyStore().AddKey(filepath.Join(filepath.FromSlash(gun), tempKey.ID()), &tempKey) mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json", func(w http.ResponseWriter, r *http.Request) { - rootJSONFile := filepath.Join(tempBaseDir, "tuf", gun, "metadata", "root.json") + rootJSONFile := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "metadata", "root.json") rootFileBytes, err := ioutil.ReadFile(rootJSONFile) assert.NoError(t, err) fmt.Fprint(w, string(rootFileBytes)) @@ -401,7 +401,7 @@ func testValidateRootKey(t *testing.T, rootType data.KeyAlgorithm) { err = repo.Initialize(rootCryptoService) assert.NoError(t, err, "error creating repository: %s", err) - rootJSONFile := filepath.Join(tempBaseDir, "tuf", gun, "metadata", "root.json") + rootJSONFile := filepath.Join(tempBaseDir, "tuf", filepath.FromSlash(gun), "metadata", "root.json") jsonBytes, err := ioutil.ReadFile(rootJSONFile) assert.NoError(t, err, "error reading TUF metadata file %s: %s", rootJSONFile, err) diff --git a/keystoremanager/import_export.go b/keystoremanager/import_export.go index c17452c984..18e2f47ad4 100644 --- a/keystoremanager/import_export.go +++ b/keystoremanager/import_export.go @@ -24,6 +24,14 @@ var ( // ErrRootKeyNotEncrypted is returned if a root key being imported is // unencrypted ErrRootKeyNotEncrypted = errors.New("only encrypted root keys may be imported") + + // ErrNonRootKeyEncrypted is returned if a non-root key is found to + // be encrypted while exporting + ErrNonRootKeyEncrypted = errors.New("found encrypted non-root key") + + // ErrNoKeysFoundForGUN is returned if no keys are found for the + // specified GUN during export + ErrNoKeysFoundForGUN = errors.New("no keys found for specified GUN") ) // ExportRootKey exports the specified root key to an io.Writer in PEM format. @@ -266,3 +274,77 @@ func (km *KeyStoreManager) ImportKeysZip(zipReader zip.Reader, passphrase string return nil } + +func moveKeysByGUN(oldKeyStore, newKeyStore *trustmanager.KeyFileStore, gun, outputPassphrase string) error { + // List all files but no symlinks + for _, f := range oldKeyStore.ListFiles(false) { + fullKeyPath := strings.TrimSpace(strings.TrimSuffix(f, filepath.Ext(f))) + relKeyPath := strings.TrimPrefix(fullKeyPath, oldKeyStore.BaseDir()) + relKeyPath = strings.TrimPrefix(relKeyPath, string(filepath.Separator)) + + // Skip keys that aren't associated with this GUN + if !strings.HasPrefix(relKeyPath, filepath.FromSlash(gun)) { + continue + } + + pemBytes, err := oldKeyStore.Get(relKeyPath) + if err != nil { + return err + } + + block, _ := pem.Decode(pemBytes) + if block == nil { + return ErrNoValidPrivateKey + } + + if x509.IsEncryptedPEMBlock(block) { + return ErrNonRootKeyEncrypted + } + + // Key is not encrypted. Parse it, and add it + // to the temporary store as an encrypted key. + privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") + if err != nil { + return err + } + err = newKeyStore.AddEncryptedKey(relKeyPath, privKey, outputPassphrase) + if err != nil { + return err + } + } + + return nil +} + +// ExportKeysByGUN exports all keys associated with a specified GUN to an +// io.Writer in zip format. outputPassphrase is the new passphrase to use to +// encrypt the keys. If blank, the keys will not be encrypted. +func (km *KeyStoreManager) ExportKeysByGUN(dest io.Writer, gun, outputPassphrase string) error { + tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") + defer os.RemoveAll(tempBaseDir) + + // Create temporary keystore to use as a staging area + tempNonRootKeysPath := filepath.Join(tempBaseDir, privDir, nonRootKeysSubdir) + tempNonRootKeyStore, err := trustmanager.NewKeyFileStore(tempNonRootKeysPath) + if err != nil { + return err + } + + if err := moveKeysByGUN(km.nonRootKeyStore, tempNonRootKeyStore, gun, outputPassphrase); err != nil { + return err + } + + zipWriter := zip.NewWriter(dest) + + if len(tempNonRootKeyStore.ListKeys()) == 0 { + return ErrNoKeysFoundForGUN + } + + if err := addKeysToArchive(zipWriter, tempNonRootKeyStore, tempBaseDir); err != nil { + return err + } + + zipWriter.Close() + + return nil +} diff --git a/keystoremanager/import_export_test.go b/keystoremanager/import_export_test.go index d08ab7fe9f..08dc3cb437 100644 --- a/keystoremanager/import_export_test.go +++ b/keystoremanager/import_export_test.go @@ -78,7 +78,7 @@ func TestImportExportZip(t *testing.T) { // Map of files to expect in the zip file, with the passphrases passphraseByFile := make(map[string]string) - // Add keys in private to the map. These should use the new passphrase + // Add non-root keys to the map. These should use the new passphrase // because they were formerly unencrypted. privKeyList := repo.KeyStoreManager.NonRootKeyStore().ListFiles(false) for _, privKeyName := range privKeyList { @@ -176,6 +176,138 @@ func TestImportExportZip(t *testing.T) { assert.NoError(t, err, "missing root key") } +func TestImportExportGUN(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) + + assert.NoError(t, err, "failed to create a temporary directory: %s", err) + + ts, _ := createTestServer(t) + defer ts.Close() + + 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) + assert.NoError(t, err, "error generating root key: %s", err) + + rootCryptoService, err := repo.KeyStoreManager.GetRootCryptoService(rootKeyID, oldPassphrase) + assert.NoError(t, err, "error retrieving root key: %s", err) + + err = repo.Initialize(rootCryptoService) + assert.NoError(t, err, "error creating repository: %s", err) + + tempZipFile, err := ioutil.TempFile("", "notary-test-export-") + tempZipFilePath := tempZipFile.Name() + defer os.Remove(tempZipFilePath) + + err = repo.KeyStoreManager.ExportKeysByGUN(tempZipFile, gun, exportPassphrase) + assert.NoError(t, err) + + // With an invalid GUN, this should return an error + err = repo.KeyStoreManager.ExportKeysByGUN(tempZipFile, "does.not.exist/in/repository", exportPassphrase) + assert.EqualError(t, err, keystoremanager.ErrNoKeysFoundForGUN.Error()) + + tempZipFile.Close() + + // Reopen the zip file for importing + zipReader, err := zip.OpenReader(tempZipFilePath) + assert.NoError(t, err, "could not open zip file") + + // Map of files to expect in the zip file, with the passphrases + passphraseByFile := make(map[string]string) + + // Add keys non-root keys to the map. These should use the new passphrase + // because they were formerly unencrypted. + privKeyList := repo.KeyStoreManager.NonRootKeyStore().ListFiles(false) + for _, privKeyName := range privKeyList { + relName := strings.TrimPrefix(privKeyName, tempBaseDir+string(filepath.Separator)) + passphraseByFile[relName] = exportPassphrase + } + + // Iterate through the files in the archive, checking that the files + // exist and are encrypted with the expected passphrase. + for _, f := range zipReader.File { + expectedPassphrase, present := passphraseByFile[f.Name] + if !present { + t.Fatalf("unexpected file %s in zip file", f.Name) + } + + delete(passphraseByFile, f.Name) + + rc, err := f.Open() + assert.NoError(t, err, "could not open file inside zip archive") + + pemBytes, err := ioutil.ReadAll(rc) + assert.NoError(t, err, "could not read file from zip") + + _, err = trustmanager.ParsePEMPrivateKey(pemBytes, expectedPassphrase) + assert.NoError(t, err, "PEM not encrypted with the expected passphrase") + + rc.Close() + } + + zipReader.Close() + + // Are there any keys that didn't make it to the zip? + 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 non-root 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) + } +} + func TestImportExportRootKey(t *testing.T) { gun := "docker.com/notary" oldPassphrase := "oldPassphrase"