Merge pull request #62 from docker/import-export

Add key import and export features
This commit is contained in:
Aaron Lehmann 2015-07-15 17:43:19 -07:00
commit c54f2d0768
8 changed files with 882 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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