mirror of https://github.com/docker/docs.git
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 <aaron.lehmann@docker.com>
This commit is contained in:
parent
7331b3a0e8
commit
878a8a083d
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue