mirror of https://github.com/docker/docs.git
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:
parent
6d3d98b873
commit
e5a42d4df9
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue