mirror of https://github.com/docker/docs.git
Merge pull request #62 from docker/import-export
Add key import and export features
This commit is contained in:
commit
c54f2d0768
|
@ -95,7 +95,7 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper) (*N
|
||||||
gun: gun,
|
gun: gun,
|
||||||
baseDir: baseDir,
|
baseDir: baseDir,
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
tufRepoPath: filepath.Join(baseDir, tufDir, gun),
|
tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)),
|
||||||
cryptoService: cryptoService,
|
cryptoService: cryptoService,
|
||||||
roundTrip: rt,
|
roundTrip: rt,
|
||||||
KeyStoreManager: keyStoreManager,
|
KeyStoreManager: keyStoreManager,
|
||||||
|
|
|
@ -75,13 +75,13 @@ func testInitRepo(t *testing.T, rootType data.KeyAlgorithm) {
|
||||||
// Inspect contents of the temporary directory
|
// Inspect contents of the temporary directory
|
||||||
expectedDirs := []string{
|
expectedDirs := []string{
|
||||||
"private",
|
"private",
|
||||||
filepath.Join("private", gun),
|
filepath.Join("private", "tuf_keys", filepath.FromSlash(gun)),
|
||||||
filepath.Join("private", "root_keys"),
|
filepath.Join("private", "root_keys"),
|
||||||
"trusted_certificates",
|
"trusted_certificates",
|
||||||
filepath.Join("trusted_certificates", gun),
|
filepath.Join("trusted_certificates", filepath.FromSlash(gun)),
|
||||||
"tuf",
|
"tuf",
|
||||||
filepath.Join("tuf", gun, "metadata"),
|
filepath.Join("tuf", filepath.FromSlash(gun), "metadata"),
|
||||||
filepath.Join("tuf", gun, "targets"),
|
filepath.Join("tuf", filepath.FromSlash(gun), "targets"),
|
||||||
}
|
}
|
||||||
for _, dir := range expectedDirs {
|
for _, dir := range expectedDirs {
|
||||||
fi, err := os.Stat(filepath.Join(tempBaseDir, dir))
|
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")
|
assert.Equal(t, rootKeyFilename, actualDest, "symlink to root key has wrong destination")
|
||||||
|
|
||||||
// There should be a trusted certificate
|
// 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")
|
assert.NoError(t, err, "missing trusted certificate")
|
||||||
|
|
||||||
// Sanity check the TUF metadata files. Verify that they exist, the JSON is
|
// 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
|
// well-formed, and the signatures exist. For the root.json file, also check
|
||||||
// that the root, snapshot, and targets key IDs are present.
|
// that the root, snapshot, and targets key IDs are present.
|
||||||
expectedTUFMetadataFiles := []string{
|
expectedTUFMetadataFiles := []string{
|
||||||
filepath.Join("tuf", gun, "metadata", "root.json"),
|
filepath.Join("tuf", filepath.FromSlash(gun), "metadata", "root.json"),
|
||||||
filepath.Join("tuf", gun, "metadata", "snapshot.json"),
|
filepath.Join("tuf", filepath.FromSlash(gun), "metadata", "snapshot.json"),
|
||||||
filepath.Join("tuf", gun, "metadata", "targets.json"),
|
filepath.Join("tuf", filepath.FromSlash(gun), "metadata", "targets.json"),
|
||||||
}
|
}
|
||||||
for _, filename := range expectedTUFMetadataFiles {
|
for _, filename := range expectedTUFMetadataFiles {
|
||||||
fullPath := filepath.Join(tempBaseDir, filename)
|
fullPath := filepath.Join(tempBaseDir, filename)
|
||||||
|
@ -225,7 +225,7 @@ func testAddListTarget(t *testing.T, rootType data.KeyAlgorithm) {
|
||||||
assert.NoError(t, err, "error adding target")
|
assert.NoError(t, err, "error adding target")
|
||||||
|
|
||||||
// Look for the changelist file
|
// 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)
|
changelistDir, err := os.Open(changelistDirPath)
|
||||||
assert.NoError(t, err, "could not open changelist directory")
|
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
|
// Apply the changelist. Normally, this would be done by Publish
|
||||||
|
|
||||||
// load the changelist for this repo
|
// 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")
|
assert.NoError(t, err, "could not open changelist")
|
||||||
|
|
||||||
// apply the changelist to the repo
|
// apply the changelist to the repo
|
||||||
|
@ -309,10 +309,10 @@ func testAddListTarget(t *testing.T, rootType data.KeyAlgorithm) {
|
||||||
var tempKey data.PrivateKey
|
var tempKey data.PrivateKey
|
||||||
json.Unmarshal([]byte(timestampECDSAKeyJSON), &tempKey)
|
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) {
|
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)
|
rootFileBytes, err := ioutil.ReadFile(rootJSONFile)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
fmt.Fprint(w, string(rootFileBytes))
|
fmt.Fprint(w, string(rootFileBytes))
|
||||||
|
@ -401,7 +401,7 @@ func testValidateRootKey(t *testing.T, rootType data.KeyAlgorithm) {
|
||||||
err = repo.Initialize(rootCryptoService)
|
err = repo.Initialize(rootCryptoService)
|
||||||
assert.NoError(t, err, "error creating repository: %s", err)
|
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)
|
jsonBytes, err := ioutil.ReadFile(rootJSONFile)
|
||||||
assert.NoError(t, err, "error reading TUF metadata file %s: %s", rootJSONFile, err)
|
assert.NoError(t, err, "error reading TUF metadata file %s: %s", rootJSONFile, err)
|
||||||
|
|
|
@ -0,0 +1,350 @@
|
||||||
|
package keystoremanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/notary/trustmanager"
|
||||||
|
"github.com/endophage/gotuf/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoValidPrivateKey is returned if a key being imported doesn't
|
||||||
|
// look like a private key
|
||||||
|
ErrNoValidPrivateKey = errors.New("no valid private key found")
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// The key's existing encryption is preserved.
|
||||||
|
func (km *KeyStoreManager) ExportRootKey(dest io.Writer, keyID string) error {
|
||||||
|
pemBytes, err := km.rootKeyStore.Get(keyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dest.Write(pemBytes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRootKeyIsEncrypted makes sure the root key is encrypted. We have
|
||||||
|
// internal assumptions that depend on this.
|
||||||
|
func checkRootKeyIsEncrypted(pemBytes []byte) error {
|
||||||
|
block, _ := pem.Decode(pemBytes)
|
||||||
|
if block == nil {
|
||||||
|
return ErrNoValidPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if !x509.IsEncryptedPEMBlock(block) {
|
||||||
|
return ErrRootKeyNotEncrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportRootKey imports a root in PEM format key from an io.Reader
|
||||||
|
// The key's existing encryption is preserved. The keyID parameter is
|
||||||
|
// necessary because otherwise we'd need the passphrase to decrypt the key
|
||||||
|
// in order to compute the ID.
|
||||||
|
func (km *KeyStoreManager) ImportRootKey(source io.Reader, keyID string) error {
|
||||||
|
pemBytes, err := ioutil.ReadAll(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = checkRootKeyIsEncrypted(pemBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = km.rootKeyStore.Add(keyID, pemBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ErrNoValidPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
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) error {
|
||||||
|
// List all files but no symlinks
|
||||||
|
for _, fullKeyPath := range newKeyStore.ListFiles(false) {
|
||||||
|
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, nonRootKeysSubdir)
|
||||||
|
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)
|
||||||
|
|
||||||
|
if err := addKeysToArchive(zipWriter, tempRootKeyStore, tempBaseDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addKeysToArchive(zipWriter, tempNonRootKeyStore, tempBaseDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zipWriter.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportKeysZip imports keys from a zip file provided as an io.ReaderAt. The
|
||||||
|
// keys in the root_keys directory are left encrypted, but the other keys are
|
||||||
|
// decrypted with the specified passphrase.
|
||||||
|
func (km *KeyStoreManager) ImportKeysZip(zipReader zip.Reader, passphrase string) error {
|
||||||
|
// Temporarily store the keys in maps, so we can bail early if there's
|
||||||
|
// an error (for example, wrong passphrase), without leaving the key
|
||||||
|
// store in an inconsistent state
|
||||||
|
newRootKeys := make(map[string][]byte)
|
||||||
|
newNonRootKeys := make(map[string]*data.PrivateKey)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
for _, f := range zipReader.File {
|
||||||
|
fNameTrimmed := strings.TrimSuffix(f.Name, filepath.Ext(f.Name))
|
||||||
|
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBytes, err := ioutil.ReadAll(rc)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this in the root_keys directory?
|
||||||
|
// Note that using / as a separator is okay here - the zip
|
||||||
|
// package guarantees that the separator will be /
|
||||||
|
if strings.HasPrefix(fNameTrimmed, rootKeysPrefix) {
|
||||||
|
if err = checkRootKeyIsEncrypted(pemBytes); err != nil {
|
||||||
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Root keys are preserved without decrypting
|
||||||
|
keyName := strings.TrimPrefix(fNameTrimmed, rootKeysPrefix)
|
||||||
|
newRootKeys[keyName] = pemBytes
|
||||||
|
} else if strings.HasPrefix(fNameTrimmed, nonRootKeysPrefix) {
|
||||||
|
// Non-root keys need to be decrypted
|
||||||
|
key, err := trustmanager.ParsePEMPrivateKey(pemBytes, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyName := strings.TrimPrefix(fNameTrimmed, nonRootKeysPrefix)
|
||||||
|
newNonRootKeys[keyName] = key
|
||||||
|
} else {
|
||||||
|
// This path inside the zip archive doesn't look like a
|
||||||
|
// root key or a non-root key. To avoid adding a file
|
||||||
|
// to the filestore that we won't be able to use, skip
|
||||||
|
// 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()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for keyName, pemBytes := range newRootKeys {
|
||||||
|
if err := km.rootKeyStore.Add(keyName, pemBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for keyName, privKey := range newNonRootKeys {
|
||||||
|
if err := km.nonRootKeyStore.AddKey(keyName, privKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,392 @@
|
||||||
|
package keystoremanager_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/notary/client"
|
||||||
|
"github.com/docker/notary/keystoremanager"
|
||||||
|
"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 TestImportExportZip(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.ExportAllKeys(tempZipFile, exportPassphrase)
|
||||||
|
tempZipFile.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for keys in root_keys
|
||||||
|
// There should be a file named after the key ID of the root key we
|
||||||
|
// passed in.
|
||||||
|
rootKeyFilename := rootCryptoService.ID() + ".key"
|
||||||
|
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "root_keys", rootKeyFilename))
|
||||||
|
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"
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
tempKeyFile, err := ioutil.TempFile("", "notary-test-export-")
|
||||||
|
tempKeyFilePath := tempKeyFile.Name()
|
||||||
|
defer os.Remove(tempKeyFilePath)
|
||||||
|
|
||||||
|
err = repo.KeyStoreManager.ExportRootKey(tempKeyFile, rootKeyID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
tempKeyFile.Close()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Check the contents of the zip file
|
||||||
|
keyReader, err := os.Open(tempKeyFilePath)
|
||||||
|
assert.NoError(t, err, "could not open key file")
|
||||||
|
|
||||||
|
err = repo2.KeyStoreManager.ImportRootKey(keyReader, rootKeyID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
keyReader.Close()
|
||||||
|
|
||||||
|
// Look for repo's root key in repo2
|
||||||
|
// There should be a file named after the key ID of the root key we
|
||||||
|
// imported.
|
||||||
|
rootKeyFilename := rootKeyID + ".key"
|
||||||
|
_, err = os.Stat(filepath.Join(tempBaseDir2, "private", "root_keys", rootKeyFilename))
|
||||||
|
assert.NoError(t, err, "missing root key")
|
||||||
|
|
||||||
|
// Try to import a decrypted version of the root key and make sure it
|
||||||
|
// doesn't succeed
|
||||||
|
pemBytes, err := ioutil.ReadFile(tempKeyFilePath)
|
||||||
|
assert.NoError(t, err, "could not read key file")
|
||||||
|
privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, oldPassphrase)
|
||||||
|
assert.NoError(t, err, "could not decrypt key file")
|
||||||
|
decryptedPEMBytes, err := trustmanager.KeyToPEM(privKey)
|
||||||
|
assert.NoError(t, err, "could not convert key to PEM")
|
||||||
|
|
||||||
|
err = repo2.KeyStoreManager.ImportRootKey(bytes.NewReader(decryptedPEMBytes), rootKeyID)
|
||||||
|
assert.EqualError(t, err, keystoremanager.ErrRootKeyNotEncrypted.Error())
|
||||||
|
|
||||||
|
// Try to import garbage and make sure it doesn't succeed
|
||||||
|
err = repo2.KeyStoreManager.ImportRootKey(strings.NewReader("this is not PEM"), rootKeyID)
|
||||||
|
assert.EqualError(t, err, keystoremanager.ErrNoValidPrivateKey.Error())
|
||||||
|
}
|
|
@ -32,13 +32,15 @@ const (
|
||||||
trustDir = "trusted_certificates"
|
trustDir = "trusted_certificates"
|
||||||
privDir = "private"
|
privDir = "private"
|
||||||
rootKeysSubdir = "root_keys"
|
rootKeysSubdir = "root_keys"
|
||||||
|
nonRootKeysSubdir = "tuf_keys"
|
||||||
rsaRootKeySize = 4096 // Used for new root keys
|
rsaRootKeySize = 4096 // Used for new root keys
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewKeyStoreManager returns an initialized KeyStoreManager, or an error
|
// NewKeyStoreManager returns an initialized KeyStoreManager, or an error
|
||||||
// if it fails to create the KeyFileStores or load certificates
|
// if it fails to create the KeyFileStores or load certificates
|
||||||
func NewKeyStoreManager(baseDir string) (*KeyStoreManager, error) {
|
func NewKeyStoreManager(baseDir string) (*KeyStoreManager, error) {
|
||||||
nonRootKeyStore, err := trustmanager.NewKeyFileStore(filepath.Join(baseDir, privDir))
|
nonRootKeysPath := filepath.Join(baseDir, privDir, nonRootKeysSubdir)
|
||||||
|
nonRootKeyStore, err := trustmanager.NewKeyFileStore(nonRootKeysPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
package trustmanager
|
package trustmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const visible os.FileMode = 0755
|
const (
|
||||||
const private os.FileMode = 0700
|
visible os.FileMode = 0755
|
||||||
|
private os.FileMode = 0700
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrPathOutsideStore indicates that the returned path would be
|
||||||
|
// outside the store
|
||||||
|
ErrPathOutsideStore = errors.New("path outside file store")
|
||||||
|
)
|
||||||
|
|
||||||
// FileStore is the interface for all FileStores
|
// FileStore is the interface for all FileStores
|
||||||
type FileStore interface {
|
type FileStore interface {
|
||||||
|
@ -16,10 +26,11 @@ type FileStore interface {
|
||||||
Remove(fileName string) error
|
Remove(fileName string) error
|
||||||
RemoveDir(directoryName string) error
|
RemoveDir(directoryName string) error
|
||||||
Get(fileName string) ([]byte, error)
|
Get(fileName string) ([]byte, error)
|
||||||
GetPath(fileName string) string
|
GetPath(fileName string) (string, error)
|
||||||
ListFiles(symlinks bool) []string
|
ListFiles(symlinks bool) []string
|
||||||
ListDir(directoryName string, symlinks bool) []string
|
ListDir(directoryName string, symlinks bool) []string
|
||||||
Link(src, dst string) error
|
Link(src, dst string) error
|
||||||
|
BaseDir() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimpleFileStore implements FileStore
|
// SimpleFileStore implements FileStore
|
||||||
|
@ -31,6 +42,8 @@ type SimpleFileStore struct {
|
||||||
|
|
||||||
// NewSimpleFileStore creates a directory with 755 permissions
|
// NewSimpleFileStore creates a directory with 755 permissions
|
||||||
func NewSimpleFileStore(baseDir string, fileExt string) (FileStore, error) {
|
func NewSimpleFileStore(baseDir string, fileExt string) (FileStore, error) {
|
||||||
|
baseDir = filepath.Clean(baseDir)
|
||||||
|
|
||||||
if err := CreateDirectory(baseDir); err != nil {
|
if err := CreateDirectory(baseDir); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -57,7 +70,10 @@ func NewPrivateSimpleFileStore(baseDir string, fileExt string) (FileStore, error
|
||||||
|
|
||||||
// Add writes data to a file with a given name
|
// Add writes data to a file with a given name
|
||||||
func (f *SimpleFileStore) Add(name string, data []byte) error {
|
func (f *SimpleFileStore) Add(name string, data []byte) error {
|
||||||
filePath := f.genFilePath(name)
|
filePath, err := f.GetPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
createDirectory(filepath.Dir(filePath), f.perms)
|
createDirectory(filepath.Dir(filePath), f.perms)
|
||||||
return ioutil.WriteFile(filePath, data, f.perms)
|
return ioutil.WriteFile(filePath, data, f.perms)
|
||||||
}
|
}
|
||||||
|
@ -65,7 +81,10 @@ func (f *SimpleFileStore) Add(name string, data []byte) error {
|
||||||
// Remove removes a file identified by name
|
// Remove removes a file identified by name
|
||||||
func (f *SimpleFileStore) Remove(name string) error {
|
func (f *SimpleFileStore) Remove(name string) error {
|
||||||
// Attempt to remove
|
// Attempt to remove
|
||||||
filePath := f.genFilePath(name)
|
filePath, err := f.GetPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return os.Remove(filePath)
|
return os.Remove(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +108,10 @@ func (f *SimpleFileStore) RemoveDir(name string) error {
|
||||||
|
|
||||||
// Get returns the data given a file name
|
// Get returns the data given a file name
|
||||||
func (f *SimpleFileStore) Get(name string) ([]byte, error) {
|
func (f *SimpleFileStore) Get(name string) ([]byte, error) {
|
||||||
filePath := f.genFilePath(name)
|
filePath, err := f.GetPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
data, err := ioutil.ReadFile(filePath)
|
data, err := ioutil.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -99,8 +121,14 @@ func (f *SimpleFileStore) Get(name string) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPath returns the full final path of a file with a given name
|
// GetPath returns the full final path of a file with a given name
|
||||||
func (f *SimpleFileStore) GetPath(name string) string {
|
func (f *SimpleFileStore) GetPath(name string) (string, error) {
|
||||||
return f.genFilePath(name)
|
fileName := f.genFileName(name)
|
||||||
|
fullPath := filepath.Clean(filepath.Join(f.baseDir, fileName))
|
||||||
|
|
||||||
|
if !strings.HasPrefix(fullPath, f.baseDir) {
|
||||||
|
return "", ErrPathOutsideStore
|
||||||
|
}
|
||||||
|
return fullPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListFiles lists all the files inside of a store
|
// ListFiles lists all the files inside of a store
|
||||||
|
@ -143,12 +171,6 @@ func (f *SimpleFileStore) list(path string, symlinks bool) []string {
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
// genFilePath returns the full path with extension given a file name
|
|
||||||
func (f *SimpleFileStore) genFilePath(name string) string {
|
|
||||||
fileName := f.genFileName(name)
|
|
||||||
return filepath.Join(f.baseDir, fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// genFileName returns the name using the right extension
|
// genFileName returns the name using the right extension
|
||||||
func (f *SimpleFileStore) genFileName(name string) string {
|
func (f *SimpleFileStore) genFileName(name string) string {
|
||||||
return fmt.Sprintf("%s.%s", name, f.fileExt)
|
return fmt.Sprintf("%s.%s", name, f.fileExt)
|
||||||
|
@ -159,10 +181,17 @@ func (f *SimpleFileStore) genFileName(name string) string {
|
||||||
// We use full path for the source and local for the destination to use relative
|
// We use full path for the source and local for the destination to use relative
|
||||||
// path for the symlink
|
// path for the symlink
|
||||||
func (f *SimpleFileStore) Link(oldname, newname string) error {
|
func (f *SimpleFileStore) Link(oldname, newname string) error {
|
||||||
return os.Symlink(
|
newnamePath, err := f.GetPath(newname)
|
||||||
f.genFileName(oldname),
|
if err != nil {
|
||||||
f.genFilePath(newname),
|
return err
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return os.Symlink(f.genFileName(oldname), newnamePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// CreateDirectory uses createDirectory to create a chmod 755 Directory
|
||||||
|
|
|
@ -286,14 +286,79 @@ func TestGetPath(t *testing.T) {
|
||||||
|
|
||||||
firstPath := "diogomonica.com/openvpn/0xdeadbeef.crt"
|
firstPath := "diogomonica.com/openvpn/0xdeadbeef.crt"
|
||||||
secondPath := "/docker.io/testing-dashes/@#$%^&().crt"
|
secondPath := "/docker.io/testing-dashes/@#$%^&().crt"
|
||||||
if store.GetPath("diogomonica.com/openvpn/0xdeadbeef") != firstPath {
|
|
||||||
|
result, err := store.GetPath("diogomonica.com/openvpn/0xdeadbeef")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from GetPath: %v", err)
|
||||||
|
}
|
||||||
|
if result != firstPath {
|
||||||
t.Fatalf("Expecting: %s", firstPath)
|
t.Fatalf("Expecting: %s", firstPath)
|
||||||
}
|
}
|
||||||
if store.GetPath("/docker.io/testing-dashes/@#$%^&()") != secondPath {
|
|
||||||
|
result, err = store.GetPath("/docker.io/testing-dashes/@#$%^&()")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from GetPath: %v", err)
|
||||||
|
}
|
||||||
|
if result != secondPath {
|
||||||
t.Fatalf("Expecting: %s", secondPath)
|
t.Fatalf("Expecting: %s", secondPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetPathProtection(t *testing.T) {
|
||||||
|
testExt := "crt"
|
||||||
|
perms := os.FileMode(0755)
|
||||||
|
|
||||||
|
// Create our SimpleFileStore
|
||||||
|
store := &SimpleFileStore{
|
||||||
|
baseDir: "/path/to/filestore/",
|
||||||
|
fileExt: testExt,
|
||||||
|
perms: perms,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should deny requests for paths outside the filestore
|
||||||
|
if _, err := store.GetPath("../../etc/passwd"); err != ErrPathOutsideStore {
|
||||||
|
t.Fatalf("expected ErrPathOutsideStore error from GetPath")
|
||||||
|
}
|
||||||
|
if _, err := store.GetPath("private/../../../etc/passwd"); err != ErrPathOutsideStore {
|
||||||
|
t.Fatalf("expected ErrPathOutsideStore error from GetPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convoluted paths should work as long as they end up inside the store
|
||||||
|
expected := "/path/to/filestore/filename.crt"
|
||||||
|
result, err := store.GetPath("private/../../filestore/./filename")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from GetPath: %v", err)
|
||||||
|
}
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expecting: %s (got: %s)", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat tests with a relative baseDir
|
||||||
|
relStore := &SimpleFileStore{
|
||||||
|
baseDir: "relative/file/path",
|
||||||
|
fileExt: testExt,
|
||||||
|
perms: perms,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should deny requests for paths outside the filestore
|
||||||
|
if _, err := relStore.GetPath("../../etc/passwd"); err != ErrPathOutsideStore {
|
||||||
|
t.Fatalf("expected ErrPathOutsideStore error from GetPath")
|
||||||
|
}
|
||||||
|
if _, err := relStore.GetPath("private/../../../etc/passwd"); err != ErrPathOutsideStore {
|
||||||
|
t.Fatalf("expected ErrPathOutsideStore error from GetPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convoluted paths should work as long as they end up inside the store
|
||||||
|
expected = "relative/file/path/filename.crt"
|
||||||
|
result, err = relStore.GetPath("private/../../path/./filename")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from GetPath: %v", err)
|
||||||
|
}
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expecting: %s (got: %s)", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetData(t *testing.T) {
|
func TestGetData(t *testing.T) {
|
||||||
testName := "docker.com/notary/certificate"
|
testName := "docker.com/notary/certificate"
|
||||||
testExt := "crt"
|
testExt := "crt"
|
||||||
|
|
|
@ -85,7 +85,11 @@ func (s X509FileStore) addNamedCert(cert *x509.Certificate) error {
|
||||||
certBytes := CertToPEM(cert)
|
certBytes := CertToPEM(cert)
|
||||||
|
|
||||||
// Save the file to disk if not already there.
|
// Save the file to disk if not already there.
|
||||||
if _, err := os.Stat(s.fileStore.GetPath(fileName)); os.IsNotExist(err) {
|
filePath, err := s.fileStore.GetPath(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
if err := s.fileStore.Add(fileName, certBytes); err != nil {
|
if err := s.fileStore.Add(fileName, certBytes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue