Add ExportKeysByGUN function

It exports the keys for a particular GUN to a zip, encrypted with a
specified passphrase.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This commit is contained in:
Aaron Lehmann 2015-07-15 15:37:00 -07:00
parent 6d3d98b873
commit e5a42d4df9
4 changed files with 229 additions and 15 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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
}

View File

@ -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"