From 878a8a083d31bc588d2a68b97e0d3a18edab3abd Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 14 Jul 2015 17:34:00 -0700 Subject: [PATCH] Add ExportAllKeys function This allows all keys to be exported to a zip file. Keys that were already encrypted are kept as-is, and keys that weren't encrypted are encrypted with the specified passphrase. Also add a unit test that creates the zip file and checks the expected keys all exist, and are all encrypted with the expected passphrase. Signed-off-by: Aaron Lehmann --- keystoremanager/export_test.go | 116 ++++++++++++++++++++++++ keystoremanager/import_export.go | 147 +++++++++++++++++++++++++++++++ trustmanager/filestore.go | 6 ++ 3 files changed, 269 insertions(+) create mode 100644 keystoremanager/export_test.go create mode 100644 keystoremanager/import_export.go diff --git a/keystoremanager/export_test.go b/keystoremanager/export_test.go new file mode 100644 index 0000000000..be72f06078 --- /dev/null +++ b/keystoremanager/export_test.go @@ -0,0 +1,116 @@ +package keystoremanager_test + +import ( + "archive/zip" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/docker/notary/client" + "github.com/docker/notary/trustmanager" + "github.com/endophage/gotuf/data" + "github.com/stretchr/testify/assert" +) + +const timestampECDSAKeyJSON = ` +{"keytype":"ecdsa","keyval":{"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw==","private":"MHcCAQEEIDqtcdzU7H3AbIPSQaxHl9+xYECt7NpK7B1+6ep5cv9CoAoGCCqGSM49AwEHoUQDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw=="}}` + +func createTestServer(t *testing.T) (*httptest.Server, *http.ServeMux) { + mux := http.NewServeMux() + // TUF will request /v2/docker.com/notary/_trust/tuf/timestamp.key + // Return a canned timestamp.key + mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.key", func(w http.ResponseWriter, r *http.Request) { + // Also contains the private key, but for the purpose of this + // test, we don't care + fmt.Fprint(w, timestampECDSAKeyJSON) + }) + + ts := httptest.NewServer(mux) + + return ts, mux +} + +func TestExportKeys(t *testing.T) { + gun := "docker.com/notary" + // 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.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) + + // Add keys in private 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" + } + + // 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" + + // 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() + } + + // Are there any keys that didn't make it to the zip? + for fileNotFound, _ := range passphraseByFile { + t.Fatalf("%s not found in zip", fileNotFound) + } +} diff --git a/keystoremanager/import_export.go b/keystoremanager/import_export.go new file mode 100644 index 0000000000..c46c2704c0 --- /dev/null +++ b/keystoremanager/import_export.go @@ -0,0 +1,147 @@ +package keystoremanager + +import ( + "archive/zip" + "crypto/x509" + "encoding/pem" + "errors" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/docker/notary/trustmanager" +) + +func moveKeysWithNewPassphrase(oldKeyStore, newKeyStore *trustmanager.KeyFileStore, 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)) + + pemBytes, err := oldKeyStore.Get(relKeyPath) + if err != nil { + return err + } + + block, _ := pem.Decode(pemBytes) + if block == nil { + return errors.New("no valid private key found") + } + + if !x509.IsEncryptedPEMBlock(block) { + // 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) + } else { + // Encrypted key - pass it through without + // decrypting + err = newKeyStore.Add(relKeyPath, pemBytes) + } + + if err != nil { + return err + } + } + + return nil +} + +func addKeysToArchive(zipWriter *zip.Writer, newKeyStore *trustmanager.KeyFileStore, tempBaseDir string, dedup map[string]struct{}) error { + // List all files but no symlinks + for _, fullKeyPath := range newKeyStore.ListFiles(false) { + if _, present := dedup[fullKeyPath]; present { + continue + } + dedup[fullKeyPath] = struct{}{} + + relKeyPath := strings.TrimPrefix(fullKeyPath, tempBaseDir) + relKeyPath = strings.TrimPrefix(relKeyPath, string(filepath.Separator)) + + fi, err := os.Stat(fullKeyPath) + if err != nil { + return err + } + + infoHeader, err := zip.FileInfoHeader(fi) + if err != nil { + return err + } + + infoHeader.Name = relKeyPath + zipFileEntryWriter, err := zipWriter.CreateHeader(infoHeader) + if err != nil { + return err + } + + fileContents, err := ioutil.ReadFile(fullKeyPath) + if err != nil { + return err + } + if _, err = zipFileEntryWriter.Write(fileContents); err != nil { + return err + } + } + + return nil +} + +// ExportAllKeys exports all keys to an io.Writer in zip format. +// outputPassphrase is the new passphrase to use to encrypt the existing keys. +// If blank, the keys will not be encrypted. Note that keys which are already +// encrypted are not re-encrypted. They will be included in the zip with their +// original encryption. +func (km *KeyStoreManager) ExportAllKeys(dest io.Writer, outputPassphrase string) error { + tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") + defer os.RemoveAll(tempBaseDir) + + // Create temporary keystores to use as a staging area + tempNonRootKeysPath := filepath.Join(tempBaseDir, privDir) + tempNonRootKeyStore, err := trustmanager.NewKeyFileStore(tempNonRootKeysPath) + if err != nil { + return err + } + + tempRootKeysPath := filepath.Join(tempBaseDir, privDir, rootKeysSubdir) + tempRootKeyStore, err := trustmanager.NewKeyFileStore(tempRootKeysPath) + if err != nil { + return err + } + + if err := moveKeysWithNewPassphrase(km.rootKeyStore, tempRootKeyStore, outputPassphrase); err != nil { + return err + } + if err := moveKeysWithNewPassphrase(km.nonRootKeyStore, tempNonRootKeyStore, outputPassphrase); err != nil { + return err + } + + zipWriter := zip.NewWriter(dest) + + // Root and non-root stores overlap, so we need to dedup files + dedup := make(map[string]struct{}) + + if err := addKeysToArchive(zipWriter, tempRootKeyStore, tempBaseDir, dedup); err != nil { + return err + } + if err := addKeysToArchive(zipWriter, tempNonRootKeyStore, tempBaseDir, dedup); err != nil { + return err + } + + zipWriter.Close() + + 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 +// decrypted with the specified passphrase. +func (km *KeyStoreManager) ImportZip(zip io.Reader, passphrase string) error { + // TODO(aaronl) + return nil +} diff --git a/trustmanager/filestore.go b/trustmanager/filestore.go index 3701ebece6..8c57020124 100644 --- a/trustmanager/filestore.go +++ b/trustmanager/filestore.go @@ -20,6 +20,7 @@ type FileStore interface { ListFiles(symlinks bool) []string ListDir(directoryName string, symlinks bool) []string Link(src, dst string) error + BaseDir() string } // SimpleFileStore implements FileStore @@ -165,6 +166,11 @@ func (f *SimpleFileStore) Link(oldname, newname string) error { ) } +// BaseDir returns the base directory of the filestore +func (f *SimpleFileStore) BaseDir() string { + return f.baseDir +} + // CreateDirectory uses createDirectory to create a chmod 755 Directory func CreateDirectory(dir string) error { return createDirectory(dir, visible)