mirror of https://github.com/docker/docs.git
Add import/export to KeyStore interface so that the import_export code
makes use of this rather than mangle files manually to import/export root keys. (Regular keys it just zips up the whole directory.) Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
parent
566bd3ce67
commit
75b63b84cd
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/notary/pkg/passphrase"
|
"github.com/docker/notary/pkg/passphrase"
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
)
|
)
|
||||||
|
|
@ -35,19 +34,24 @@ var (
|
||||||
// ExportRootKey exports the specified root key to an io.Writer in PEM format.
|
// ExportRootKey exports the specified root key to an io.Writer in PEM format.
|
||||||
// The key's existing encryption is preserved.
|
// The key's existing encryption is preserved.
|
||||||
func (km *KeyStoreManager) ExportRootKey(dest io.Writer, keyID string) error {
|
func (km *KeyStoreManager) ExportRootKey(dest io.Writer, keyID string) error {
|
||||||
pemBytes, err := km.rootKeyStore.Get(keyID + "_root")
|
pemBytes, err := km.KeyStore.ExportKey(keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
nBytes, err := dest.Write(pemBytes)
|
||||||
_, err = dest.Write(pemBytes)
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if nBytes != len(pemBytes) {
|
||||||
|
return errors.New("Unable to finish writing exported key.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportRootKeyReencrypt exports the specified root key to an io.Writer in
|
// ExportRootKeyReencrypt exports the specified root key to an io.Writer in
|
||||||
// PEM format. The key is reencrypted with a new passphrase.
|
// PEM format. The key is reencrypted with a new passphrase.
|
||||||
func (km *KeyStoreManager) ExportRootKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error {
|
func (km *KeyStoreManager) ExportRootKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error {
|
||||||
privateKey, alias, err := km.rootKeyStore.GetKey(keyID)
|
privateKey, alias, err := km.KeyStore.GetKey(keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -56,25 +60,29 @@ func (km *KeyStoreManager) ExportRootKeyReencrypt(dest io.Writer, keyID string,
|
||||||
tempBaseDir, err := ioutil.TempDir("", "notary-key-export-")
|
tempBaseDir, err := ioutil.TempDir("", "notary-key-export-")
|
||||||
defer os.RemoveAll(tempBaseDir)
|
defer os.RemoveAll(tempBaseDir)
|
||||||
|
|
||||||
privRootKeysSubdir := filepath.Join(privDir, rootKeysSubdir)
|
tempKeysPath := filepath.Join(tempBaseDir, privDir)
|
||||||
tempRootKeysPath := filepath.Join(tempBaseDir, privRootKeysSubdir)
|
tempKeyStore, err := trustmanager.NewKeyFileStore(tempKeysPath, newPassphraseRetriever)
|
||||||
tempRootKeyStore, err := trustmanager.NewKeyFileStore(tempRootKeysPath, newPassphraseRetriever)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tempRootKeyStore.AddKey(keyID, alias, privateKey)
|
err = tempKeyStore.AddKey(keyID, alias, privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pemBytes, err := tempRootKeyStore.Get(keyID + "_" + alias)
|
pemBytes, err := tempKeyStore.ExportKey(keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
nBytes, err := dest.Write(pemBytes)
|
||||||
_, err = dest.Write(pemBytes)
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if nBytes != len(pemBytes) {
|
||||||
|
return errors.New("Unable to finish writing exported key.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkRootKeyIsEncrypted makes sure the root key is encrypted. We have
|
// checkRootKeyIsEncrypted makes sure the root key is encrypted. We have
|
||||||
|
|
@ -106,7 +114,7 @@ func (km *KeyStoreManager) ImportRootKey(source io.Reader, keyID string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = km.rootKeyStore.Add(keyID+"_root", pemBytes); err != nil {
|
if err = km.KeyStore.ImportKey(pemBytes, "root"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,35 +178,20 @@ func (km *KeyStoreManager) ExportAllKeys(dest io.Writer, newPassphraseRetriever
|
||||||
tempBaseDir, err := ioutil.TempDir("", "notary-key-export-")
|
tempBaseDir, err := ioutil.TempDir("", "notary-key-export-")
|
||||||
defer os.RemoveAll(tempBaseDir)
|
defer os.RemoveAll(tempBaseDir)
|
||||||
|
|
||||||
privNonRootKeysSubdir := filepath.Join(privDir, nonRootKeysSubdir)
|
// Create temporary keystore to use as a staging area
|
||||||
privRootKeysSubdir := filepath.Join(privDir, rootKeysSubdir)
|
tempKeysPath := filepath.Join(tempBaseDir, privDir)
|
||||||
|
tempKeyStore, err := trustmanager.NewKeyFileStore(tempKeysPath, newPassphraseRetriever)
|
||||||
// Create temporary keystores to use as a staging area
|
|
||||||
tempNonRootKeysPath := filepath.Join(tempBaseDir, privNonRootKeysSubdir)
|
|
||||||
tempNonRootKeyStore, err := trustmanager.NewKeyFileStore(tempNonRootKeysPath, newPassphraseRetriever)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tempRootKeysPath := filepath.Join(tempBaseDir, privRootKeysSubdir)
|
if err := moveKeys(km.KeyStore, tempKeyStore); err != nil {
|
||||||
tempRootKeyStore, err := trustmanager.NewKeyFileStore(tempRootKeysPath, newPassphraseRetriever)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := moveKeys(km.rootKeyStore, tempRootKeyStore); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := moveKeys(km.nonRootKeyStore, tempNonRootKeyStore); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
zipWriter := zip.NewWriter(dest)
|
zipWriter := zip.NewWriter(dest)
|
||||||
|
|
||||||
if err := addKeysToArchive(zipWriter, tempRootKeyStore, privRootKeysSubdir); err != nil {
|
if err := addKeysToArchive(zipWriter, tempKeyStore, privDir); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := addKeysToArchive(zipWriter, tempNonRootKeyStore, privNonRootKeysSubdir); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,13 +207,7 @@ func (km *KeyStoreManager) ImportKeysZip(zipReader zip.Reader) error {
|
||||||
// Temporarily store the keys in maps, so we can bail early if there's
|
// Temporarily store the keys in maps, so we can bail early if there's
|
||||||
// an error (for example, wrong passphrase), without leaving the key
|
// an error (for example, wrong passphrase), without leaving the key
|
||||||
// store in an inconsistent state
|
// store in an inconsistent state
|
||||||
newRootKeys := make(map[string][]byte)
|
newKeys := make(map[string][]byte)
|
||||||
newNonRootKeys := make(map[string][]byte)
|
|
||||||
|
|
||||||
// Note that using / as a separator is okay here - the zip package
|
|
||||||
// guarantees that the separator will be /
|
|
||||||
rootKeysPrefix := privDir + "/" + rootKeysSubdir + "/"
|
|
||||||
nonRootKeysPrefix := privDir + "/" + nonRootKeysSubdir + "/"
|
|
||||||
|
|
||||||
// Iterate through the files in the archive. Don't add the keys
|
// Iterate through the files in the archive. Don't add the keys
|
||||||
for _, f := range zipReader.File {
|
for _, f := range zipReader.File {
|
||||||
|
|
@ -236,29 +223,22 @@ func (km *KeyStoreManager) ImportKeysZip(zipReader zip.Reader) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this in the root_keys directory?
|
|
||||||
// Note that using / as a separator is okay here - the zip
|
// Note that using / as a separator is okay here - the zip
|
||||||
// package guarantees that the separator will be /
|
// package guarantees that the separator will be /
|
||||||
if strings.HasPrefix(fNameTrimmed, rootKeysPrefix) {
|
if strings.HasPrefix(fNameTrimmed, privDir) {
|
||||||
if err = checkRootKeyIsEncrypted(fileBytes); err != nil {
|
if fNameTrimmed[len(fNameTrimmed)-5:] == "_root" {
|
||||||
rc.Close()
|
if err = checkRootKeyIsEncrypted(fileBytes); err != nil {
|
||||||
return err
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Root keys are preserved without decrypting
|
keyName := strings.TrimPrefix(fNameTrimmed, privDir)
|
||||||
keyName := strings.TrimPrefix(fNameTrimmed, rootKeysPrefix)
|
newKeys[keyName] = fileBytes
|
||||||
newRootKeys[keyName] = fileBytes
|
|
||||||
|
|
||||||
} else if strings.HasPrefix(fNameTrimmed, nonRootKeysPrefix) {
|
|
||||||
// Nonroot keys are preserved without decrypting
|
|
||||||
keyName := strings.TrimPrefix(fNameTrimmed, nonRootKeysPrefix)
|
|
||||||
newNonRootKeys[keyName] = fileBytes
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// This path inside the zip archive doesn't look like a
|
// This path inside the zip archive doesn't look like a
|
||||||
// root key, non-root key, or alias. To avoid adding a file
|
// root key, non-root key, or alias. To avoid adding a file
|
||||||
// to the filestore that we won't be able to use, skip
|
// to the filestore that we won't be able to use, skip
|
||||||
// this file in the import.
|
// this file in the import.
|
||||||
logrus.Warnf("skipping import of key with a path that doesn't begin with %s or %s: %s", rootKeysPrefix, nonRootKeysPrefix, f.Name)
|
|
||||||
rc.Close()
|
rc.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -266,14 +246,8 @@ func (km *KeyStoreManager) ImportKeysZip(zipReader zip.Reader) error {
|
||||||
rc.Close()
|
rc.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
for keyName, pemBytes := range newRootKeys {
|
for keyName, pemBytes := range newKeys {
|
||||||
if err := km.rootKeyStore.Add(keyName, pemBytes); err != nil {
|
if err := km.KeyStore.Add(keyName, pemBytes); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for keyName, pemBytes := range newNonRootKeys {
|
|
||||||
if err := km.nonRootKeyStore.Add(keyName, pemBytes); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,26 +283,24 @@ func (km *KeyStoreManager) ExportKeysByGUN(dest io.Writer, gun string, passphras
|
||||||
tempBaseDir, err := ioutil.TempDir("", "notary-key-export-")
|
tempBaseDir, err := ioutil.TempDir("", "notary-key-export-")
|
||||||
defer os.RemoveAll(tempBaseDir)
|
defer os.RemoveAll(tempBaseDir)
|
||||||
|
|
||||||
privNonRootKeysSubdir := filepath.Join(privDir, nonRootKeysSubdir)
|
|
||||||
|
|
||||||
// Create temporary keystore to use as a staging area
|
// Create temporary keystore to use as a staging area
|
||||||
tempNonRootKeysPath := filepath.Join(tempBaseDir, privNonRootKeysSubdir)
|
tempKeysPath := filepath.Join(tempBaseDir, privDir)
|
||||||
tempNonRootKeyStore, err := trustmanager.NewKeyFileStore(tempNonRootKeysPath, passphraseRetriever)
|
tempKeyStore, err := trustmanager.NewKeyFileStore(tempKeysPath, passphraseRetriever)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := moveKeysByGUN(km.nonRootKeyStore, tempNonRootKeyStore, gun); err != nil {
|
if err := moveKeysByGUN(km.KeyStore, tempKeyStore, gun); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
zipWriter := zip.NewWriter(dest)
|
zipWriter := zip.NewWriter(dest)
|
||||||
|
|
||||||
if len(tempNonRootKeyStore.ListKeys()) == 0 {
|
if len(tempKeyStore.ListKeys()) == 0 {
|
||||||
return ErrNoKeysFoundForGUN
|
return ErrNoKeysFoundForGUN
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := addKeysToArchive(zipWriter, tempNonRootKeyStore, privNonRootKeysSubdir); err != nil {
|
if err := addKeysToArchive(zipWriter, tempKeyStore, privDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,14 @@ func TestImportExportZip(t *testing.T) {
|
||||||
|
|
||||||
// Add non-root keys to the map. These should use the new passphrase
|
// Add non-root keys to the map. These should use the new passphrase
|
||||||
// because the passwords were chosen by the newPassphraseRetriever.
|
// because the passwords were chosen by the newPassphraseRetriever.
|
||||||
privKeyMap := repo.KeyStoreManager.NonRootKeyStore().ListKeys()
|
privKeyMap := repo.KeyStoreManager.KeyStore.ListKeys()
|
||||||
for privKeyName := range privKeyMap {
|
for privKeyName := range privKeyMap {
|
||||||
_, alias, err := repo.KeyStoreManager.NonRootKeyStore().GetKey(privKeyName)
|
_, alias, err := repo.KeyStoreManager.KeyStore.GetKey(privKeyName)
|
||||||
assert.NoError(t, err, "privKey %s has no alias", privKeyName)
|
assert.NoError(t, err, "privKey %s has no alias", privKeyName)
|
||||||
|
|
||||||
|
if alias == "root" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
||||||
passphraseByFile[relKeyPath] = exportPassphrase
|
passphraseByFile[relKeyPath] = exportPassphrase
|
||||||
}
|
}
|
||||||
|
|
@ -156,9 +159,12 @@ func TestImportExportZip(t *testing.T) {
|
||||||
// Look for keys in private. The filenames should match the key IDs
|
// Look for keys in private. The filenames should match the key IDs
|
||||||
// in the repo's private key store.
|
// in the repo's private key store.
|
||||||
for privKeyName := range privKeyMap {
|
for privKeyName := range privKeyMap {
|
||||||
_, alias, err := repo.KeyStoreManager.NonRootKeyStore().GetKey(privKeyName)
|
_, alias, err := repo.KeyStoreManager.KeyStore.GetKey(privKeyName)
|
||||||
assert.NoError(t, err, "privKey %s has no alias", privKeyName)
|
assert.NoError(t, err, "privKey %s has no alias", privKeyName)
|
||||||
|
|
||||||
|
if alias == "root" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
||||||
privKeyFileName := filepath.Join(tempBaseDir2, relKeyPath)
|
privKeyFileName := filepath.Join(tempBaseDir2, relKeyPath)
|
||||||
_, err = os.Stat(privKeyFileName)
|
_, err = os.Stat(privKeyFileName)
|
||||||
|
|
@ -219,12 +225,15 @@ func TestImportExportGUN(t *testing.T) {
|
||||||
|
|
||||||
// Add keys non-root keys to the map. These should use the new passphrase
|
// Add keys non-root keys to the map. These should use the new passphrase
|
||||||
// because they were formerly unencrypted.
|
// because they were formerly unencrypted.
|
||||||
privKeyMap := repo.KeyStoreManager.NonRootKeyStore().ListKeys()
|
privKeyMap := repo.KeyStoreManager.KeyStore.ListKeys()
|
||||||
for privKeyName := range privKeyMap {
|
for privKeyName := range privKeyMap {
|
||||||
_, alias, err := repo.KeyStoreManager.NonRootKeyStore().GetKey(privKeyName)
|
_, alias, err := repo.KeyStoreManager.KeyStore.GetKey(privKeyName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("privKey %s has no alias", privKeyName)
|
t.Fatalf("privKey %s has no alias", privKeyName)
|
||||||
}
|
}
|
||||||
|
if alias == "root" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
||||||
|
|
||||||
passphraseByFile[relKeyPath] = exportPassphrase
|
passphraseByFile[relKeyPath] = exportPassphrase
|
||||||
|
|
@ -290,10 +299,13 @@ func TestImportExportGUN(t *testing.T) {
|
||||||
// Look for keys in private. The filenames should match the key IDs
|
// Look for keys in private. The filenames should match the key IDs
|
||||||
// in the repo's private key store.
|
// in the repo's private key store.
|
||||||
for privKeyName := range privKeyMap {
|
for privKeyName := range privKeyMap {
|
||||||
_, alias, err := repo.KeyStoreManager.NonRootKeyStore().GetKey(privKeyName)
|
_, alias, err := repo.KeyStoreManager.KeyStore.GetKey(privKeyName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("privKey %s has no alias", privKeyName)
|
t.Fatalf("privKey %s has no alias", privKeyName)
|
||||||
}
|
}
|
||||||
|
if alias == "root" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
relKeyPath := filepath.Join("private", "tuf_keys", privKeyName+"_"+alias+".key")
|
||||||
privKeyFileName := filepath.Join(tempBaseDir2, relKeyPath)
|
privKeyFileName := filepath.Join(tempBaseDir2, relKeyPath)
|
||||||
_, err = os.Stat(privKeyFileName)
|
_, err = os.Stat(privKeyFileName)
|
||||||
|
|
@ -381,7 +393,7 @@ func TestImportExportRootKey(t *testing.T) {
|
||||||
assert.EqualError(t, err, keystoremanager.ErrNoValidPrivateKey.Error())
|
assert.EqualError(t, err, keystoremanager.ErrNoValidPrivateKey.Error())
|
||||||
|
|
||||||
// Should be able to unlock the root key with the old password
|
// Should be able to unlock the root key with the old password
|
||||||
key, alias, err := repo2.KeyStoreManager.RootKeyStore().GetKey(rootKeyID)
|
key, alias, err := repo2.KeyStoreManager.KeyStore.GetKey(rootKeyID)
|
||||||
assert.NoError(t, err, "could not unlock root key")
|
assert.NoError(t, err, "could not unlock root key")
|
||||||
assert.Equal(t, "root", alias)
|
assert.Equal(t, "root", alias)
|
||||||
assert.Equal(t, rootKeyID, key.ID())
|
assert.Equal(t, rootKeyID, key.ID())
|
||||||
|
|
@ -452,7 +464,7 @@ func TestImportExportRootKeyReencrypt(t *testing.T) {
|
||||||
assert.NoError(t, err, "missing root key")
|
assert.NoError(t, err, "missing root key")
|
||||||
|
|
||||||
// Should be able to unlock the root key with the new password
|
// Should be able to unlock the root key with the new password
|
||||||
key, alias, err := repo2.KeyStoreManager.RootKeyStore().GetKey(rootKeyID)
|
key, alias, err := repo2.KeyStoreManager.KeyStore.GetKey(rootKeyID)
|
||||||
assert.NoError(t, err, "could not unlock root key")
|
assert.NoError(t, err, "could not unlock root key")
|
||||||
assert.Equal(t, "root", alias)
|
assert.Equal(t, "root", alias)
|
||||||
assert.Equal(t, rootKeyID, key.ID())
|
assert.Equal(t, rootKeyID, key.ID())
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package signer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|
@ -196,6 +197,16 @@ func (s *KeyDBStore) RotateKeyPassphrase(name, newPassphraseAlias string) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportKey is currently unimplemented and will always return an error
|
||||||
|
func (s *KeyDBStore) ExportKey(name string) ([]byte, error) {
|
||||||
|
return nil, errors.New("Exporting from a KeyDBStore is not supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportKey is currently unimplemented and will always return an error
|
||||||
|
func (s *KeyDBStore) ImportKey(pemBytes []byte, alias string) error {
|
||||||
|
return errors.New("Importing into a KeyDBStore is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
// HealthCheck verifies that DB exists and is query-able
|
// HealthCheck verifies that DB exists and is query-able
|
||||||
func (s *KeyDBStore) HealthCheck() error {
|
func (s *KeyDBStore) HealthCheck() error {
|
||||||
dbPrivateKey := GormPrivateKey{}
|
dbPrivateKey := GormPrivateKey{}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,22 @@ func (s *KeyFileStore) RemoveKey(name string) error {
|
||||||
return removeKey(s, s.cachedKeys, name)
|
return removeKey(s, s.cachedKeys, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportKey exportes the encrypted bytes from the keystore and writes it to
|
||||||
|
// dest.
|
||||||
|
func (s *KeyFileStore) ExportKey(name string) ([]byte, error) {
|
||||||
|
keyBytes, _, err := getRawKey(s, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return keyBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportKey imports the private key in the encrypted bytes into the keystore
|
||||||
|
// with the given key ID and alias.
|
||||||
|
func (s *KeyFileStore) ImportKey(pemBytes []byte, alias string) error {
|
||||||
|
return importKey(s, s.Retriever, s.cachedKeys, alias, pemBytes)
|
||||||
|
}
|
||||||
|
|
||||||
// NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory
|
// NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory
|
||||||
func NewKeyMemoryStore(passphraseRetriever passphrase.Retriever) *KeyMemoryStore {
|
func NewKeyMemoryStore(passphraseRetriever passphrase.Retriever) *KeyMemoryStore {
|
||||||
memStore := NewMemoryFileStore()
|
memStore := NewMemoryFileStore()
|
||||||
|
|
@ -106,19 +122,33 @@ func (s *KeyMemoryStore) RemoveKey(name string) error {
|
||||||
return removeKey(s, s.cachedKeys, name)
|
return removeKey(s, s.cachedKeys, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, alias string, privKey data.PrivateKey) error {
|
// ExportKey exportes the encrypted bytes from the keystore and writes it to
|
||||||
pemPrivKey, err := KeyToPEM(privKey)
|
// dest.
|
||||||
|
func (s *KeyMemoryStore) ExportKey(name string) ([]byte, error) {
|
||||||
|
keyBytes, _, err := getRawKey(s, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return keyBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
attempts := 0
|
// ImportKey imports the private key in the encrypted bytes into the keystore
|
||||||
chosenPassphrase := ""
|
// with the given key ID and alias.
|
||||||
giveup := false
|
func (s *KeyMemoryStore) ImportKey(pemBytes []byte, alias string) error {
|
||||||
for {
|
return importKey(s, s.Retriever, s.cachedKeys, alias, pemBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, alias string, privKey data.PrivateKey) error {
|
||||||
|
|
||||||
|
var (
|
||||||
|
chosenPassphrase string
|
||||||
|
giveup bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for attempts := 0; ; attempts++ {
|
||||||
chosenPassphrase, giveup, err = passphraseRetriever(name, alias, true, attempts)
|
chosenPassphrase, giveup, err = passphraseRetriever(name, alias, true, attempts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
attempts++
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if giveup {
|
if giveup {
|
||||||
|
|
@ -130,15 +160,7 @@ func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if chosenPassphrase != "" {
|
return encryptAndAddKey(s, chosenPassphrase, cachedKeys, name, alias, privKey)
|
||||||
pemPrivKey, err = EncryptPrivateKey(privKey, chosenPassphrase)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedKeys[name] = &cachedKey{alias: alias, key: privKey}
|
|
||||||
return s.Add(filepath.Join(getSubdir(alias), name+"_"+alias), pemPrivKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyAlias(s LimitedFileStore, keyID string) (string, error) {
|
func getKeyAlias(s LimitedFileStore, keyID string) (string, error) {
|
||||||
|
|
@ -165,14 +187,8 @@ func getKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
|
||||||
if ok {
|
if ok {
|
||||||
return cachedKeyEntry.key, cachedKeyEntry.alias, nil
|
return cachedKeyEntry.key, cachedKeyEntry.alias, nil
|
||||||
}
|
}
|
||||||
keyAlias, err := getKeyAlias(s, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := name + "_" + keyAlias
|
keyBytes, keyAlias, err := getRawKey(s, name)
|
||||||
var keyBytes []byte
|
|
||||||
keyBytes, err = s.Get(filepath.Join(getSubdir(keyAlias), filename))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
@ -181,27 +197,7 @@ func getKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cached
|
||||||
// See if the key is encrypted. If its encrypted we'll fail to parse the private key
|
// See if the key is encrypted. If its encrypted we'll fail to parse the private key
|
||||||
privKey, err := ParsePEMPrivateKey(keyBytes, "")
|
privKey, err := ParsePEMPrivateKey(keyBytes, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// We need to decrypt the key, lets get a passphrase
|
privKey, _, retErr = getPasswdDecryptBytes(s, passphraseRetriever, keyBytes, name, string(keyAlias))
|
||||||
for attempts := 0; ; attempts++ {
|
|
||||||
passphrase, giveup, err := passphraseRetriever(name, string(keyAlias), false, attempts)
|
|
||||||
// Check if the passphrase retriever got an error or if it is telling us to give up
|
|
||||||
if giveup || err != nil {
|
|
||||||
return nil, "", ErrPasswordInvalid{}
|
|
||||||
}
|
|
||||||
if attempts > 10 {
|
|
||||||
return nil, "", ErrAttemptsExceeded{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to convert PEM encoded bytes back to a PrivateKey using the passphrase
|
|
||||||
privKey, err = ParsePEMPrivateKey(keyBytes, passphrase)
|
|
||||||
if err != nil {
|
|
||||||
retErr = ErrPasswordInvalid{}
|
|
||||||
} else {
|
|
||||||
// We managed to parse the PrivateKey. We've succeeded!
|
|
||||||
retErr = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if retErr != nil {
|
if retErr != nil {
|
||||||
return nil, "", retErr
|
return nil, "", retErr
|
||||||
|
|
@ -247,9 +243,98 @@ func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assumes 2 subdirectories, 1 containing root keys and 1 containing tuf keys
|
||||||
func getSubdir(alias string) string {
|
func getSubdir(alias string) string {
|
||||||
if alias == "root" {
|
if alias == "root" {
|
||||||
return rootKeysSubdir
|
return rootKeysSubdir
|
||||||
}
|
}
|
||||||
return nonRootKeysSubdir
|
return nonRootKeysSubdir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a key ID, gets the bytes and alias belonging to that key if the key
|
||||||
|
// exists
|
||||||
|
func getRawKey(s LimitedFileStore, name string) ([]byte, string, error) {
|
||||||
|
keyAlias, err := getKeyAlias(s, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := name + "_" + keyAlias
|
||||||
|
var keyBytes []byte
|
||||||
|
keyBytes, err = s.Get(filepath.Join(getSubdir(keyAlias), filename))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return keyBytes, keyAlias, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the password to decript the given pem bytes. Return the password,
|
||||||
|
// because it is useful for importing
|
||||||
|
func getPasswdDecryptBytes(s LimitedFileStore, passphraseRetriever passphrase.Retriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) {
|
||||||
|
var (
|
||||||
|
passwd string
|
||||||
|
retErr error
|
||||||
|
privKey data.PrivateKey
|
||||||
|
)
|
||||||
|
for attempts := 0; ; attempts++ {
|
||||||
|
var (
|
||||||
|
giveup bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
passwd, giveup, err = passphraseRetriever(name, alias, false, attempts)
|
||||||
|
// Check if the passphrase retriever got an error or if it is telling us to give up
|
||||||
|
if giveup || err != nil {
|
||||||
|
return nil, "", ErrPasswordInvalid{}
|
||||||
|
}
|
||||||
|
if attempts > 10 {
|
||||||
|
return nil, "", ErrAttemptsExceeded{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert PEM encoded bytes back to a PrivateKey using the passphrase
|
||||||
|
privKey, err = ParsePEMPrivateKey(pemBytes, passwd)
|
||||||
|
if err != nil {
|
||||||
|
retErr = ErrPasswordInvalid{}
|
||||||
|
} else {
|
||||||
|
// We managed to parse the PrivateKey. We've succeeded!
|
||||||
|
retErr = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if retErr != nil {
|
||||||
|
return nil, "", retErr
|
||||||
|
}
|
||||||
|
return privKey, passwd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptAndAddKey(s LimitedFileStore, passwd string, cachedKeys map[string]*cachedKey, name, alias string, privKey data.PrivateKey) error {
|
||||||
|
|
||||||
|
var (
|
||||||
|
pemPrivKey []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if passwd != "" {
|
||||||
|
pemPrivKey, err = EncryptPrivateKey(privKey, passwd)
|
||||||
|
} else {
|
||||||
|
pemPrivKey, err = KeyToPEM(privKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedKeys[name] = &cachedKey{alias: alias, key: privKey}
|
||||||
|
return s.Add(filepath.Join(getSubdir(alias), name+"_"+alias), pemPrivKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func importKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, alias string, pemBytes []byte) error {
|
||||||
|
|
||||||
|
privKey, passphrase, err := getPasswdDecryptBytes(s, passphraseRetriever, pemBytes, "imported", alias)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return encryptAndAddKey(
|
||||||
|
s, passphrase, cachedKeys, privKey.ID(), alias, privKey)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,18 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/notary/pkg/passphrase"
|
"github.com/docker/notary/pkg/passphrase"
|
||||||
|
"github.com/endophage/gotuf/data"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cannedPassphrase = "passphrase"
|
||||||
|
|
||||||
var passphraseRetriever = func(keyID string, alias string, createNew bool, numAttempts int) (string, bool, error) {
|
var passphraseRetriever = func(keyID string, alias string, createNew bool, numAttempts int) (string, bool, error) {
|
||||||
if numAttempts > 5 {
|
if numAttempts > 5 {
|
||||||
giveup := true
|
giveup := true
|
||||||
return "", giveup, errors.New("passPhraseRetriever failed after too many requests")
|
return "", giveup, errors.New("passPhraseRetriever failed after too many requests")
|
||||||
}
|
}
|
||||||
return "passphrase", false, nil
|
return cannedPassphrase, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddKey(t *testing.T) {
|
func TestAddKey(t *testing.T) {
|
||||||
|
|
@ -345,3 +348,125 @@ func TestKeysAreCached(t *testing.T) {
|
||||||
}
|
}
|
||||||
assert.Equal(t, 2, numTimesCalled, "numTimesCalled should be 2 -- no additional call to passphraseRetriever")
|
assert.Equal(t, 2, numTimesCalled, "numTimesCalled should be 2 -- no additional call to passphraseRetriever")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exporting a key is successful (it is a valid key)
|
||||||
|
func TestKeyFileStoreExportSuccess(t *testing.T) {
|
||||||
|
// Generate a new Private Key
|
||||||
|
privKey, err := GenerateECDSAKey(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Temporary directory where test files will be created
|
||||||
|
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempBaseDir)
|
||||||
|
|
||||||
|
// Create our FileStore and add the key
|
||||||
|
store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = store.AddKey(privKey.ID(), "root", privKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assertExportKeySuccess(t, store, privKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporting a key that doesn't exist fails (it is a valid key)
|
||||||
|
func TestKeyFileStoreExportNonExistantFailure(t *testing.T) {
|
||||||
|
// Temporary directory where test files will be created
|
||||||
|
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempBaseDir)
|
||||||
|
|
||||||
|
// Create empty FileStore
|
||||||
|
store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = store.ExportKey("12345")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporting a key is successful (it is a valid key)
|
||||||
|
func TestKeyMemoryStoreExportSuccess(t *testing.T) {
|
||||||
|
// Generate a new Private Key
|
||||||
|
privKey, err := GenerateECDSAKey(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Create our MemoryStore and add key to it
|
||||||
|
store := NewKeyMemoryStore(passphraseRetriever)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = store.AddKey(privKey.ID(), "root", privKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assertExportKeySuccess(t, store, privKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporting a key that doesn't exist fails (it is a valid key)
|
||||||
|
func TestKeyMemoryStoreExportNonExistantFailure(t *testing.T) {
|
||||||
|
store := NewKeyMemoryStore(passphraseRetriever)
|
||||||
|
_, err := store.ExportKey("12345")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Importing a key is successful
|
||||||
|
func TestKeyFileStoreImportSuccess(t *testing.T) {
|
||||||
|
// Generate a new Private Key
|
||||||
|
privKey, err := GenerateECDSAKey(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Temporary directory where test files will be created
|
||||||
|
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempBaseDir)
|
||||||
|
|
||||||
|
// Create our FileStore
|
||||||
|
store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assertImportKeySuccess(t, store, privKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Importing a key is successful
|
||||||
|
func TestKeyMemoryStoreImportSuccess(t *testing.T) {
|
||||||
|
// Generate a new Private Key
|
||||||
|
privKey, err := GenerateECDSAKey(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Create our MemoryStore
|
||||||
|
store := NewKeyMemoryStore(passphraseRetriever)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assertImportKeySuccess(t, store, privKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a keystore and expected key that is in the store, export the key
|
||||||
|
// and assert that the exported key is the same and encrypted with the right
|
||||||
|
// password.
|
||||||
|
func assertExportKeySuccess(
|
||||||
|
t *testing.T, s KeyStore, expectedKey data.PrivateKey) {
|
||||||
|
|
||||||
|
pemBytes, err := s.ExportKey(expectedKey.ID())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
reparsedKey, err := ParsePEMPrivateKey(pemBytes, cannedPassphrase)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedKey.Private(), reparsedKey.Private())
|
||||||
|
assert.Equal(t, expectedKey.Public(), reparsedKey.Public())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a keystore and expected key, generate an encrypted PEM of the key
|
||||||
|
// and assert that the then imported key is the same and encrypted with the
|
||||||
|
// right password.
|
||||||
|
func assertImportKeySuccess(
|
||||||
|
t *testing.T, s KeyStore, expectedKey data.PrivateKey) {
|
||||||
|
|
||||||
|
pemBytes, err := EncryptPrivateKey(expectedKey, cannedPassphrase)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.ImportKey(pemBytes, "root")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
reimportedKey, reimportedAlias, err := s.GetKey(expectedKey.ID())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "root", reimportedAlias)
|
||||||
|
assert.Equal(t, expectedKey.Private(), reimportedKey.Private())
|
||||||
|
assert.Equal(t, expectedKey.Public(), reimportedKey.Public())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ type KeyStore interface {
|
||||||
GetKey(name string) (data.PrivateKey, string, error)
|
GetKey(name string) (data.PrivateKey, string, error)
|
||||||
ListKeys() map[string]string
|
ListKeys() map[string]string
|
||||||
RemoveKey(name string) error
|
RemoveKey(name string) error
|
||||||
|
ExportKey(name string) ([]byte, error)
|
||||||
|
ImportKey(pemBytes []byte, alias string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type cachedKey struct {
|
type cachedKey struct {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue