From 1919f33344b1592d755c9eb5504cc1da301f1c25 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Mon, 11 Jul 2016 10:36:30 -0700 Subject: [PATCH] re-implement import/export Signed-off-by: David Lawrence (github: endophage) --- cmd/notary/keys.go | 124 +++++++++++- cmd/notary/keys_nonpkcs11.go | 10 + cmd/notary/keys_nonpkcs11_test.go | 81 ++++++++ cmd/notary/keys_pkcs11.go | 26 ++- cmd/notary/keys_pkcs11_test.go | 107 ++++++++++ cmd/notary/keys_test.go | 244 ++++++++++++++++++++++- const.go | 2 + storage/filestore.go | 7 + storage/httpstore_test.go | 2 +- trustmanager/interfaces.go | 4 - trustmanager/keystore.go | 3 +- trustmanager/yubikey/import.go | 55 ++++++ tuf/utils/utils.go | 35 ---- utils/keys.go | 127 ++++++++++++ utils/keys_test.go | 317 ++++++++++++++++++++++++++++++ 15 files changed, 1093 insertions(+), 51 deletions(-) create mode 100644 cmd/notary/keys_nonpkcs11_test.go create mode 100644 cmd/notary/keys_pkcs11_test.go create mode 100644 trustmanager/yubikey/import.go create mode 100644 utils/keys.go create mode 100644 utils/keys_test.go diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index ef8b3f2ec0..8344cec787 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -9,12 +9,15 @@ import ( notaryclient "github.com/docker/notary/client" "github.com/docker/notary/cryptoservice" + store "github.com/docker/notary/storage" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/utils" "github.com/docker/notary" "github.com/docker/notary/tuf/data" "github.com/spf13/cobra" "github.com/spf13/viper" + "os" ) var cmdKeyTemplate = usageTemplate{ @@ -53,6 +56,18 @@ var cmdKeyPasswdTemplate = usageTemplate{ Long: "Changes the passphrase for the key with the given keyID. Will require validation of the old passphrase.", } +var cmdKeyImportTemplate = usageTemplate{ + Use: "import pemfile [ pemfile ... ]", + Short: "Imports all keys from all provided .pem files", + Long: "Imports all keys from all provided .pem files by reading each PEM block from the file and writing that block to a unique object in the local keystore", +} + +var cmdKeyExportTemplate = usageTemplate{ + Use: "export", + Short: "Exports all keys from all local keystores. Can be filtered using the --key and --gun flags.", + Long: "Exports all keys from all local keystores. Which keys are exported can be restricted by using the --key or --gun flags. By default the result is sent to stdout, it can be directed to a file with the -o flag.", +} + type keyCommander struct { // these need to be set configGetter func() (*viper.Viper, error) @@ -63,6 +78,10 @@ type keyCommander struct { rotateKeyServerManaged bool input io.Reader + + exportGUNs []string + exportKeyIDs []string + outFile string } func (k *keyCommander) GetCommand() *cobra.Command { @@ -78,6 +97,28 @@ func (k *keyCommander) GetCommand() *cobra.Command { "Required for timestamp role, optional for snapshot role") cmd.AddCommand(cmdRotateKey) + cmd.AddCommand(cmdKeyImportTemplate.ToCommand(k.importKeys)) + cmdExport := cmdKeyExportTemplate.ToCommand(k.exportKeys) + cmdExport.Flags().StringSliceVar( + &k.exportGUNs, + "gun", + nil, + "GUNs for which to export keys", + ) + cmdExport.Flags().StringSliceVar( + &k.exportKeyIDs, + "key", + nil, + "Key IDs to export", + ) + cmdExport.Flags().StringVarP( + &k.outFile, + "output", + "o", + "", + "Filepath to write export output to", + ) + cmd.AddCommand(cmdExport) return cmd } @@ -345,14 +386,91 @@ func (k *keyCommander) keyPassphraseChange(cmd *cobra.Command, args []string) er if err != nil { return err } - cmd.Println("") - cmd.Printf("Successfully updated passphrase for key ID: %s", keyID) - cmd.Println("") + cmd.Printf("\nSuccessfully updated passphrase for key ID: %s\n", keyID) + return nil +} + +func (k *keyCommander) importKeys(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + cmd.Usage() + return fmt.Errorf("must specify at least one input file to import keys from") + } + config, err := k.configGetter() + if err != nil { + return err + } + + directory := config.GetString("trust_dir") + importers, err := getImporters(directory, k.getRetriever()) + if err != nil { + return err + } + for _, file := range args { + from, err := os.OpenFile(file, os.O_RDONLY, notary.PrivKeyPerms) + defer from.Close() + + if err = utils.ImportKeys(from, importers); err != nil { + return err + } + } + return nil +} + +func (k *keyCommander) exportKeys(cmd *cobra.Command, args []string) error { + var ( + out io.Writer + err error + ) + if len(args) > 0 { + cmd.Usage() + return fmt.Errorf("export does not take any positional arguments") + } + config, err := k.configGetter() + if err != nil { + return err + } + + if k.outFile == "" { + out = cmd.Out() + } else { + f, err := os.OpenFile(k.outFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, notary.PrivKeyPerms) + if err != nil { + return err + } + defer f.Close() + out = f + } + + directory := config.GetString("trust_dir") + fileStore, err := store.NewPrivateKeyFileStorage(directory, notary.KeyExtension) + if err != nil { + return err + } + if len(k.exportGUNs) > 0 { + if len(k.exportKeyIDs) > 0 { + return fmt.Errorf("Only the --gun or --key flag may be provided, not a mix of the two flags") + } + for _, gun := range k.exportGUNs { + gunPath := filepath.Join(notary.NonRootKeysSubdir, gun) + return utils.ExportKeysByGUN(out, fileStore, gunPath) + } + } else if len(k.exportKeyIDs) > 0 { + return utils.ExportKeysByID(out, fileStore, k.exportKeyIDs) + } + // export everything + keys := fileStore.ListFiles() + for _, k := range keys { + err := utils.ExportKeys(out, fileStore, k) + if err != nil { + return err + } + } return nil } func (k *keyCommander) getKeyStores( config *viper.Viper, withHardware, hardwareBackup bool) ([]trustmanager.KeyStore, error) { + retriever := k.getRetriever() directory := config.GetString("trust_dir") diff --git a/cmd/notary/keys_nonpkcs11.go b/cmd/notary/keys_nonpkcs11.go index a6bde6fac2..eaa1e6411c 100644 --- a/cmd/notary/keys_nonpkcs11.go +++ b/cmd/notary/keys_nonpkcs11.go @@ -6,9 +6,19 @@ import ( "errors" "github.com/docker/notary" + store "github.com/docker/notary/storage" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/utils" ) func getYubiStore(fileKeyStore trustmanager.KeyStore, ret notary.PassRetriever) (trustmanager.KeyStore, error) { return nil, errors.New("Not built with hardware support") } + +func getImporters(baseDir string, _ notary.PassRetriever) ([]utils.Importer, error) { + fileStore, err := store.NewPrivateKeyFileStorage(baseDir, notary.KeyExtension) + if err != nil { + return nil, err + } + return []utils.Importer{fileStore}, nil +} diff --git a/cmd/notary/keys_nonpkcs11_test.go b/cmd/notary/keys_nonpkcs11_test.go new file mode 100644 index 0000000000..1104944035 --- /dev/null +++ b/cmd/notary/keys_nonpkcs11_test.go @@ -0,0 +1,81 @@ +//+build !pkcs11 + +package main + +import ( + "encoding/pem" + "github.com/docker/notary" + "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/passphrase" + store "github.com/docker/notary/storage" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "io/ioutil" + "os" + "testing" +) + +func TestImportKeysNoYubikey(t *testing.T) { + setUp(t) + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err) + defer os.RemoveAll(tempBaseDir) + input, err := ioutil.TempFile("/tmp", "notary-test-import-") + require.NoError(t, err) + defer os.RemoveAll(input.Name()) + k := &keyCommander{ + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.SetDefault("trust_dir", tempBaseDir) + return v, nil + }, + getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, + } + + memStore := store.NewMemoryStore(nil) + ks := trustmanager.NewGenericKeyStore(memStore, k.getRetriever()) + cs := cryptoservice.NewCryptoService(ks) + + pubK, err := cs.Create(data.CanonicalRootRole, "ankh", data.ECDSAKey) + require.NoError(t, err) + bytes, err := memStore.Get(notary.RootKeysSubdir + "/" + pubK.ID()) + require.NoError(t, err) + b, _ := pem.Decode(bytes) + b.Headers["path"] = "ankh" + + pubK, err = cs.Create(data.CanonicalTargetsRole, "morpork", data.ECDSAKey) + require.NoError(t, err) + bytes, err = memStore.Get(notary.NonRootKeysSubdir + "/morpork/" + pubK.ID()) + require.NoError(t, err) + c, _ := pem.Decode(bytes) + c.Headers["path"] = "morpork" + + bBytes := pem.EncodeToMemory(b) + cBytes := pem.EncodeToMemory(c) + input.Write(bBytes) + input.Write(cBytes) + + file := input.Name() + err = input.Close() // close so import can open + require.NoError(t, err) + + err = k.importKeys(&cobra.Command{}, []string{file}) + require.NoError(t, err) + + fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) + bResult, err := fileStore.Get("ankh") + require.NoError(t, err) + cResult, err := fileStore.Get("morpork") + require.NoError(t, err) + + block, rest := pem.Decode(bResult) + require.Equal(t, b.Bytes, block.Bytes) + require.Len(t, rest, 0) + + block, rest = pem.Decode(cResult) + require.Equal(t, c.Bytes, block.Bytes) + require.Len(t, rest, 0) +} diff --git a/cmd/notary/keys_pkcs11.go b/cmd/notary/keys_pkcs11.go index 88bdcc359a..d1f3e753a1 100644 --- a/cmd/notary/keys_pkcs11.go +++ b/cmd/notary/keys_pkcs11.go @@ -4,10 +4,34 @@ package main import ( "github.com/docker/notary" + store "github.com/docker/notary/storage" "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustmanager/yubikey" + "github.com/docker/notary/utils" ) -func getYubiStore(fileKeyStore trustmanager.KeyStore, ret notary.PassRetriever) (trustmanager.KeyStore, error) { +func getYubiStore(fileKeyStore trustmanager.KeyStore, ret notary.PassRetriever) (*yubikey.YubiStore, error) { return yubikey.NewYubiStore(fileKeyStore, ret) } + +func getImporters(baseDir string, ret notary.PassRetriever) ([]utils.Importer, error) { + + var importers []utils.Importer + yubiStore, err := getYubiStore(nil, ret) + if err == nil { + importers = append( + importers, + yubikey.NewImporter(yubiStore, ret), + ) + } + fileStore, err := store.NewPrivateKeyFileStorage(baseDir, notary.KeyExtension) + if err == nil { + importers = append( + importers, + fileStore, + ) + } else if len(importers) == 0 { + return nil, err // couldn't initialize any stores + } + return importers, nil +} diff --git a/cmd/notary/keys_pkcs11_test.go b/cmd/notary/keys_pkcs11_test.go new file mode 100644 index 0000000000..c6b4e1aad4 --- /dev/null +++ b/cmd/notary/keys_pkcs11_test.go @@ -0,0 +1,107 @@ +// +build pkcs11 + +package main + +import ( + "encoding/pem" + "io/ioutil" + "os" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/docker/notary" + "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/passphrase" + store "github.com/docker/notary/storage" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustmanager/yubikey" + "github.com/docker/notary/tuf/data" +) + +func TestImportWithYubikey(t *testing.T) { + if !yubikey.IsAccessible() { + t.Skip("Must have Yubikey access.") + } + setUp(t) + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err) + defer os.RemoveAll(tempBaseDir) + input, err := ioutil.TempFile("/tmp", "notary-test-import-") + require.NoError(t, err) + defer os.RemoveAll(input.Name()) + k := &keyCommander{ + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.SetDefault("trust_dir", tempBaseDir) + return v, nil + }, + getRetriever: func() notary.PassRetriever { return passphrase.ConstantRetriever("pass") }, + } + + memStore := store.NewMemoryStore(nil) + ks := trustmanager.NewGenericKeyStore(memStore, k.getRetriever()) + cs := cryptoservice.NewCryptoService(ks) + + pubK, err := cs.Create(data.CanonicalRootRole, "ankh", data.ECDSAKey) + require.NoError(t, err) + bID := pubK.ID() // need to check presence in yubikey later + require.NoError(t, err) + bytes, err := memStore.Get(notary.RootKeysSubdir + "/" + pubK.ID()) + require.NoError(t, err) + b, _ := pem.Decode(bytes) + b.Headers["path"] = "ankh" + require.Equal(t, "root", b.Headers["role"]) + + pubK, err = cs.Create(data.CanonicalTargetsRole, "morpork", data.ECDSAKey) + require.NoError(t, err) + cID := pubK.ID() + bytes, err = memStore.Get(notary.NonRootKeysSubdir + "/morpork/" + pubK.ID()) + require.NoError(t, err) + c, _ := pem.Decode(bytes) + c.Headers["path"] = "morpork" + + bBytes := pem.EncodeToMemory(b) + cBytes := pem.EncodeToMemory(c) + input.Write(bBytes) + input.Write(cBytes) + + file := input.Name() + err = input.Close() // close so import can open + require.NoError(t, err) + + err = k.importKeys(&cobra.Command{}, []string{file}) + require.NoError(t, err) + + yks, err := yubikey.NewYubiStore(nil, k.getRetriever()) + require.NoError(t, err) + _, _, err = yks.GetKey(bID) + require.NoError(t, err) + _, _, err = yks.GetKey(cID) + require.Error(t, err) // c is non-root, should not be in yubikey + + fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) + _, err = fileStore.Get("ankh") + require.Error(t, err) // b should only be in yubikey, not in filestore + + cResult, err := fileStore.Get("morpork") + require.NoError(t, err) + + block, rest := pem.Decode(cResult) + require.Equal(t, c.Bytes, block.Bytes) + require.Len(t, rest, 0) +} + +func TestGetImporters(t *testing.T) { + if !yubikey.IsAccessible() { + t.Skip("Must have Yubikey access.") + } + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err) + defer os.RemoveAll(tempBaseDir) + importers, err := getImporters(tempBaseDir, passphrase.ConstantRetriever("pass")) + require.NoError(t, err) + require.Len(t, importers, 2) +} diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index cde7209fd8..418f511c84 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "crypto/rand" + "encoding/pem" "fmt" "io/ioutil" "net/http" @@ -11,23 +12,25 @@ import ( "strings" "testing" - "golang.org/x/net/context" - "github.com/Sirupsen/logrus" ctxu "github.com/docker/distribution/context" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + "github.com/docker/notary" "github.com/docker/notary/client" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/passphrase" "github.com/docker/notary/server" "github.com/docker/notary/server/storage" + store "github.com/docker/notary/storage" "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/utils" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/stretchr/testify/require" + "path/filepath" ) var ret = passphrase.ConstantRetriever("pass") @@ -534,6 +537,237 @@ func TestChangeKeyPassphraseNonexistentID(t *testing.T) { require.Contains(t, err.Error(), "could not retrieve local key for key ID provided") } +func TestExportKeys(t *testing.T) { + setUp(t) + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err) + defer os.RemoveAll(tempBaseDir) + output, err := ioutil.TempFile("/tmp", "notary-test-import-") + require.NoError(t, err) + defer os.RemoveAll(output.Name()) + k := &keyCommander{ + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.SetDefault("trust_dir", tempBaseDir) + return v, nil + }, + } + k.outFile = output.Name() + err = output.Close() // close so export can open + require.NoError(t, err) + + b := &pem.Block{} + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + c := &pem.Block{} + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + + bBytes := pem.EncodeToMemory(b) + cBytes := pem.EncodeToMemory(c) + require.NoError(t, err) + + fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) + err = fileStore.Set("ankh", bBytes) + require.NoError(t, err) + err = fileStore.Set("morpork", cBytes) + require.NoError(t, err) + + err = k.exportKeys(&cobra.Command{}, nil) + require.NoError(t, err) + + outRes, err := ioutil.ReadFile(k.outFile) + require.NoError(t, err) + + block, rest := pem.Decode(outRes) + require.Equal(t, b.Bytes, block.Bytes) + require.Equal(t, "ankh", block.Headers["path"]) + + block, rest = pem.Decode(rest) + require.Equal(t, c.Bytes, block.Bytes) + require.Equal(t, "morpork", block.Headers["path"]) + require.Len(t, rest, 0) + + // test no outFile uses stdout (or our replace buffer) + k.outFile = "" + cmd := &cobra.Command{} + out := bytes.NewBuffer(make([]byte, 0, 3000)) + cmd.SetOutput(out) + err = k.exportKeys(cmd, nil) + require.NoError(t, err) + + bufOut, err := ioutil.ReadAll(out) + require.NoError(t, err) + require.Equal(t, outRes, bufOut) // should be identical output to file earlier +} + +func TestExportKeysByGUN(t *testing.T) { + setUp(t) + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err) + defer os.RemoveAll(tempBaseDir) + output, err := ioutil.TempFile("/tmp", "notary-test-import-") + require.NoError(t, err) + defer os.RemoveAll(output.Name()) + k := &keyCommander{ + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.SetDefault("trust_dir", tempBaseDir) + return v, nil + }, + } + k.outFile = output.Name() + err = output.Close() // close so export can open + require.NoError(t, err) + k.exportGUNs = []string{"ankh"} + + b := &pem.Block{} + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + b2 := &pem.Block{} + b2.Bytes = make([]byte, 1000) + rand.Read(b2.Bytes) + + c := &pem.Block{} + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + + bBytes := pem.EncodeToMemory(b) + b2Bytes := pem.EncodeToMemory(b2) + cBytes := pem.EncodeToMemory(c) + require.NoError(t, err) + + fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) + // we have to manually prepend the NonRootKeysSubdir because + // KeyStore would be expected to do this for us. + err = fileStore.Set( + filepath.Join(notary.NonRootKeysSubdir, "ankh/one"), + bBytes, + ) + require.NoError(t, err) + err = fileStore.Set( + filepath.Join(notary.NonRootKeysSubdir, "ankh/two"), + b2Bytes, + ) + require.NoError(t, err) + err = fileStore.Set( + filepath.Join(notary.NonRootKeysSubdir, "morpork/three"), + cBytes, + ) + require.NoError(t, err) + + err = k.exportKeys(&cobra.Command{}, nil) + require.NoError(t, err) + + outRes, err := ioutil.ReadFile(k.outFile) + require.NoError(t, err) + + block, rest := pem.Decode(outRes) + require.Equal(t, b.Bytes, block.Bytes) + require.Equal( + t, + filepath.Join(notary.NonRootKeysSubdir, "ankh/one"), + block.Headers["path"], + ) + + block, rest = pem.Decode(rest) + require.Equal(t, b2.Bytes, block.Bytes) + require.Equal( + t, + filepath.Join(notary.NonRootKeysSubdir, "ankh/two"), + block.Headers["path"], + ) + require.Len(t, rest, 0) +} + +func TestExportKeysByID(t *testing.T) { + setUp(t) + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err) + defer os.RemoveAll(tempBaseDir) + output, err := ioutil.TempFile("/tmp", "notary-test-import-") + require.NoError(t, err) + defer os.RemoveAll(output.Name()) + k := &keyCommander{ + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.SetDefault("trust_dir", tempBaseDir) + return v, nil + }, + } + k.outFile = output.Name() + err = output.Close() // close so export can open + require.NoError(t, err) + k.exportKeyIDs = []string{"one", "three"} + + b := &pem.Block{} + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + b2 := &pem.Block{} + b2.Bytes = make([]byte, 1000) + rand.Read(b2.Bytes) + + c := &pem.Block{} + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + + bBytes := pem.EncodeToMemory(b) + b2Bytes := pem.EncodeToMemory(b2) + cBytes := pem.EncodeToMemory(c) + require.NoError(t, err) + + fileStore, err := store.NewPrivateKeyFileStorage(tempBaseDir, notary.KeyExtension) + err = fileStore.Set("ankh/one", bBytes) + require.NoError(t, err) + err = fileStore.Set("ankh/two", b2Bytes) + require.NoError(t, err) + err = fileStore.Set("morpork/three", cBytes) + require.NoError(t, err) + + err = k.exportKeys(&cobra.Command{}, nil) + require.NoError(t, err) + + outRes, err := ioutil.ReadFile(k.outFile) + require.NoError(t, err) + + block, rest := pem.Decode(outRes) + require.Equal(t, b.Bytes, block.Bytes) + require.Equal(t, "ankh/one", block.Headers["path"]) + + block, rest = pem.Decode(rest) + require.Equal(t, c.Bytes, block.Bytes) + require.Equal(t, "morpork/three", block.Headers["path"]) + require.Len(t, rest, 0) +} + +func TestExportKeysBadFlagCombo(t *testing.T) { + setUp(t) + tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-") + require.NoError(t, err) + defer os.RemoveAll(tempBaseDir) + output, err := ioutil.TempFile("/tmp", "notary-test-import-") + require.NoError(t, err) + defer os.RemoveAll(output.Name()) + k := &keyCommander{ + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.SetDefault("trust_dir", tempBaseDir) + return v, nil + }, + } + k.outFile = output.Name() + err = output.Close() // close so export can open + require.NoError(t, err) + k.exportGUNs = []string{"ankh"} + k.exportKeyIDs = []string{"one", "three"} + + err = k.exportKeys(&cobra.Command{}, nil) + require.Error(t, err) +} + func generateTempTestKeyFile(t *testing.T, role string) string { setUp(t) privKey, err := utils.GenerateECDSAKey(rand.Reader) diff --git a/const.go b/const.go index c6d136301d..3a0a01cd8d 100644 --- a/const.go +++ b/const.go @@ -36,6 +36,8 @@ const ( RootKeysSubdir = "root_keys" // NonRootKeysSubdir is the subdirectory under PrivDir where non-root private keys are stored NonRootKeysSubdir = "tuf_keys" + // KeyExtension is the file extension to use for private key files + KeyExtension = "key" // Day is a duration of one day Day = 24 * time.Hour diff --git a/storage/filestore.go b/storage/filestore.go index b99079bcc6..8d2cf1c98b 100644 --- a/storage/filestore.go +++ b/storage/filestore.go @@ -41,6 +41,13 @@ func NewSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) { return NewFileStore(baseDir, fileExt, notary.PubCertPerms) } +// NewPrivateKeyFileStorage initializes a new filestore for private keys, appending +// the notary.PrivDir to the baseDir. +func NewPrivateKeyFileStorage(baseDir, fileExt string) (*FilesystemStore, error) { + baseDir = filepath.Join(baseDir, notary.PrivDir) + return NewFileStore(baseDir, fileExt, notary.PrivKeyPerms) +} + // NewPrivateSimpleFileStore is a wrapper to create an owner readable/writeable // _only_ filestore func NewPrivateSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) { diff --git a/storage/httpstore_test.go b/storage/httpstore_test.go index 9000af57e8..53e79274b6 100644 --- a/storage/httpstore_test.go +++ b/storage/httpstore_test.go @@ -48,7 +48,7 @@ func TestHTTPStoreGetSized(t *testing.T) { require.NoError(t, err) } -// Test that passing -1 to httpstore's GetMeta will return all content +// Test that passing -1 to httpstore's GetSized will return all content func TestHTTPStoreGetAllMeta(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(testRoot)) diff --git a/trustmanager/interfaces.go b/trustmanager/interfaces.go index 2611d436a5..34bc128d26 100644 --- a/trustmanager/interfaces.go +++ b/trustmanager/interfaces.go @@ -62,10 +62,6 @@ func (err ErrKeyNotFound) Error() string { return fmt.Sprintf("signing key not found: %s", err.KeyID) } -const ( - keyExtension = "key" -) - // KeyStore is a generic interface for private key storage type KeyStore interface { // AddKey adds a key to the KeyStore, and if the key already exists, diff --git a/trustmanager/keystore.go b/trustmanager/keystore.go index c57d28f44c..dca0667699 100644 --- a/trustmanager/keystore.go +++ b/trustmanager/keystore.go @@ -36,8 +36,7 @@ type GenericKeyStore struct { // NewKeyFileStore returns a new KeyFileStore creating a private directory to // hold the keys. func NewKeyFileStore(baseDir string, p notary.PassRetriever) (*GenericKeyStore, error) { - baseDir = filepath.Join(baseDir, notary.PrivDir) - fileStore, err := store.NewPrivateSimpleFileStore(baseDir, keyExtension) + fileStore, err := store.NewPrivateKeyFileStorage(baseDir, notary.KeyExtension) if err != nil { return nil, err } diff --git a/trustmanager/yubikey/import.go b/trustmanager/yubikey/import.go new file mode 100644 index 0000000000..d73e1b6772 --- /dev/null +++ b/trustmanager/yubikey/import.go @@ -0,0 +1,55 @@ +// +build pkcs11 + +package yubikey + +import ( + "encoding/pem" + "errors" + "github.com/docker/notary" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/utils" +) + +// YubiImport is a wrapper around the YubiStore that allows us to import private +// keys to the yubikey +type YubiImport struct { + dest *YubiStore + passRetriever notary.PassRetriever +} + +func NewImporter(ys *YubiStore, ret notary.PassRetriever) *YubiImport { + return &YubiImport{ + dest: ys, + passRetriever: ret, + } +} + +// Set determines if we are allowed to set the given key on the Yubikey and +// calls through to YubiStore.AddKey if it's valid +func (s *YubiImport) Set(name string, bytes []byte) error { + block, _ := pem.Decode(bytes) + if block == nil { + return errors.New("invalid PEM data, could not parse") + } + role, ok := block.Headers["role"] + if !ok { + return errors.New("no role found for key") + } + ki := trustmanager.KeyInfo{ + // GUN is ignored by YubiStore + Role: role, + } + privKey, err := utils.ParsePEMPrivateKey(bytes, "") + if err != nil { + privKey, _, err = trustmanager.GetPasswdDecryptBytes( + s.passRetriever, + bytes, + name, + ki.Role, + ) + if err != nil { + return err + } + } + return s.dest.AddKey(ki, privKey) +} diff --git a/tuf/utils/utils.go b/tuf/utils/utils.go index 6cf499c470..8de72b6797 100644 --- a/tuf/utils/utils.go +++ b/tuf/utils/utils.go @@ -5,7 +5,6 @@ import ( "crypto/sha512" "crypto/tls" "encoding/hex" - "errors" "fmt" "io" "net/http" @@ -151,37 +150,3 @@ func ConsistentName(role string, hashSha256 []byte) string { } return role } - -// ErrWrongLength indicates the length was different to that expected -var ErrWrongLength = errors.New("wrong length") - -// ErrWrongHash indicates the hash was different to that expected -type ErrWrongHash struct { - Type string - Expected []byte - Actual []byte -} - -// Error implements error interface -func (e ErrWrongHash) Error() string { - return fmt.Sprintf("wrong %s hash, expected %#x got %#x", e.Type, e.Expected, e.Actual) -} - -// ErrNoCommonHash indicates the metadata did not provide any hashes this -// client recognizes -type ErrNoCommonHash struct { - Expected data.Hashes - Actual data.Hashes -} - -// Error implements error interface -func (e ErrNoCommonHash) Error() string { - types := func(a data.Hashes) []string { - t := make([]string, 0, len(a)) - for typ := range a { - t = append(t, typ) - } - return t - } - return fmt.Sprintf("no common hash function, expected one of %s, got %s", types(e.Expected), types(e.Actual)) -} diff --git a/utils/keys.go b/utils/keys.go new file mode 100644 index 0000000000..4442944d2c --- /dev/null +++ b/utils/keys.go @@ -0,0 +1,127 @@ +package utils + +import ( + "encoding/pem" + "github.com/Sirupsen/logrus" + "io" + "io/ioutil" + "path/filepath" +) + +// Exporter is a simple interface for the two functions we need from the Storage interface +type Exporter interface { + Get(string) ([]byte, error) + ListFiles() []string +} + +// Importer is a simple interface for the one function we need from the Storage interface +type Importer interface { + Set(string, []byte) error +} + +// ExportKeysByGUN exports all keys filtered to a GUN +func ExportKeysByGUN(to io.Writer, s Exporter, gun string) error { + keys := s.ListFiles() + for _, k := range keys { + dir := filepath.Dir(k) + if dir == gun { // must be full GUN match + if err := ExportKeys(to, s, k); err != nil { + return err + } + } + } + return nil +} + +// ExportKeysByID exports all keys matching the given ID +func ExportKeysByID(to io.Writer, s Exporter, ids []string) error { + want := make(map[string]struct{}) + for _, id := range ids { + want[id] = struct{}{} + } + keys := s.ListFiles() + for _, k := range keys { + id := filepath.Base(k) + if _, ok := want[id]; ok { + if err := ExportKeys(to, s, k); err != nil { + return err + } + } + } + return nil +} + +// ExportKeys copies a key from the store to the io.Writer +func ExportKeys(to io.Writer, s Exporter, from string) error { + // get PEM block + k, err := s.Get(from) + if err != nil { + return err + } + + // parse PEM blocks if there are more than one + for block, rest := pem.Decode(k); block != nil; block, rest = pem.Decode(rest) { + // add from path in a header for later import + block.Headers["path"] = from + // write serialized PEM + err = pem.Encode(to, block) + if err != nil { + return err + } + } + return nil +} + +// ImportKeys expects an io.Reader containing one or more PEM blocks. +// It reads PEM blocks one at a time until pem.Decode returns a nil +// block. +// Each block is written to the subpath indicated in the "path" PEM +// header. If the file already exists, the file is truncated. Multiple +// PEMs with the same "path" header are appended together. +func ImportKeys(from io.Reader, to []Importer) error { + data, err := ioutil.ReadAll(from) + if err != nil { + return err + } + var ( + writeTo string + toWrite []byte + ) + for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { + loc, ok := block.Headers["path"] + if !ok || loc == "" { + continue // don't know where to copy this key. Skip it. + } + if loc != writeTo { + // next location is different from previous one. We've finished aggregating + // data for the previous file. If we have data, write the previous file, + // the clear toWrite and set writeTo to the next path we're going to write + if toWrite != nil { + if err = importToStores(to, writeTo, toWrite); err != nil { + return err + } + } + // set up for aggregating next file's data + toWrite = nil + writeTo = loc + } + delete(block.Headers, "path") + toWrite = append(toWrite, pem.EncodeToMemory(block)...) + } + if toWrite != nil { // close out final iteration if there's data left + return importToStores(to, writeTo, toWrite) + } + return nil +} + +func importToStores(to []Importer, path string, bytes []byte) error { + var err error + for _, i := range to { + if err = i.Set(path, bytes); err != nil { + logrus.Errorf("failed to import key to store: %s", err.Error()) + continue + } + break + } + return err +} diff --git a/utils/keys_test.go b/utils/keys_test.go new file mode 100644 index 0000000000..4e6a979e63 --- /dev/null +++ b/utils/keys_test.go @@ -0,0 +1,317 @@ +package utils + +import ( + "bytes" + "crypto/rand" + "encoding/pem" + "errors" + "github.com/stretchr/testify/require" + "io/ioutil" + "testing" +) + +type TestImportStore struct { + data map[string][]byte +} + +func NewTestImportStore() *TestImportStore { + return &TestImportStore{ + data: make(map[string][]byte), + } +} + +func (s *TestImportStore) Set(name string, data []byte) error { + s.data[name] = data + return nil +} + +type TestExportStore struct { + data map[string][]byte +} + +func NewTestExportStore() *TestExportStore { + return &TestExportStore{ + data: make(map[string][]byte), + } +} + +func (s *TestExportStore) Get(name string) ([]byte, error) { + if data, ok := s.data[name]; ok { + return data, nil + } + return nil, errors.New("Not Found") +} + +func (s *TestExportStore) ListFiles() []string { + files := make([]string, 0, len(s.data)) + for k := range s.data { + files = append(files, k) + } + return files +} + +func TestExportKeys(t *testing.T) { + s := NewTestExportStore() + + b := &pem.Block{} + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + c := &pem.Block{} + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + + bBytes := pem.EncodeToMemory(b) + cBytes := pem.EncodeToMemory(c) + + s.data["ankh"] = bBytes + s.data["morpork"] = cBytes + + buf := bytes.NewBuffer(nil) + + err := ExportKeys(buf, s, "ankh") + require.NoError(t, err) + + err = ExportKeys(buf, s, "morpork") + require.NoError(t, err) + + out, err := ioutil.ReadAll(buf) + require.NoError(t, err) + + bFinal, rest := pem.Decode(out) + require.Equal(t, b.Bytes, bFinal.Bytes) + require.Equal(t, "ankh", bFinal.Headers["path"]) + + cFinal, rest := pem.Decode(rest) + require.Equal(t, c.Bytes, cFinal.Bytes) + require.Equal(t, "morpork", cFinal.Headers["path"]) + require.Len(t, rest, 0) +} + +func TestExportKeysByGUN(t *testing.T) { + s := NewTestExportStore() + + b := &pem.Block{} + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + b2 := &pem.Block{} + b2.Bytes = make([]byte, 1000) + rand.Read(b2.Bytes) + + c := &pem.Block{} + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + + bBytes := pem.EncodeToMemory(b) + b2Bytes := pem.EncodeToMemory(b2) + cBytes := pem.EncodeToMemory(c) + + s.data["ankh/one"] = bBytes + s.data["ankh/two"] = b2Bytes + s.data["morpork/three"] = cBytes + + buf := bytes.NewBuffer(nil) + + err := ExportKeysByGUN(buf, s, "ankh") + require.NoError(t, err) + + out, err := ioutil.ReadAll(buf) + require.NoError(t, err) + + bFinal, rest := pem.Decode(out) + require.Equal(t, b.Bytes, bFinal.Bytes) + require.Equal(t, "ankh/one", bFinal.Headers["path"]) + + b2Final, rest := pem.Decode(rest) + require.Equal(t, b2.Bytes, b2Final.Bytes) + require.Equal(t, "ankh/two", b2Final.Headers["path"]) + require.Len(t, rest, 0) +} + +func TestExportKeysByID(t *testing.T) { + s := NewTestExportStore() + + b := &pem.Block{} + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + c := &pem.Block{} + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + + bBytes := pem.EncodeToMemory(b) + cBytes := pem.EncodeToMemory(c) + + s.data["ankh"] = bBytes + s.data["morpork/identifier"] = cBytes + + buf := bytes.NewBuffer(nil) + + err := ExportKeysByID(buf, s, []string{"identifier"}) + require.NoError(t, err) + + out, err := ioutil.ReadAll(buf) + require.NoError(t, err) + + cFinal, rest := pem.Decode(out) + require.Equal(t, c.Bytes, cFinal.Bytes) + require.Equal(t, "morpork/identifier", cFinal.Headers["path"]) + require.Len(t, rest, 0) +} + +func TestExport2InOneFile(t *testing.T) { + s := NewTestExportStore() + + b := &pem.Block{} + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + b2 := &pem.Block{} + b2.Bytes = make([]byte, 1000) + rand.Read(b2.Bytes) + + c := &pem.Block{} + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + + bBytes := pem.EncodeToMemory(b) + b2Bytes := pem.EncodeToMemory(b2) + bBytes = append(bBytes, b2Bytes...) + cBytes := pem.EncodeToMemory(c) + + s.data["ankh"] = bBytes + s.data["morpork"] = cBytes + + buf := bytes.NewBuffer(nil) + + err := ExportKeys(buf, s, "ankh") + require.NoError(t, err) + + err = ExportKeys(buf, s, "morpork") + require.NoError(t, err) + + out, err := ioutil.ReadAll(buf) + require.NoError(t, err) + + bFinal, rest := pem.Decode(out) + require.Equal(t, b.Bytes, bFinal.Bytes) + require.Equal(t, "ankh", bFinal.Headers["path"]) + + b2Final, rest := pem.Decode(rest) + require.Equal(t, b2.Bytes, b2Final.Bytes) + require.Equal(t, "ankh", b2Final.Headers["path"]) + + cFinal, rest := pem.Decode(rest) + require.Equal(t, c.Bytes, cFinal.Bytes) + require.Equal(t, "morpork", cFinal.Headers["path"]) + require.Len(t, rest, 0) +} + +func TestImportKeys(t *testing.T) { + s := NewTestImportStore() + + b := &pem.Block{ + Headers: make(map[string]string), + } + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + b.Headers["path"] = "ankh" + + c := &pem.Block{ + Headers: make(map[string]string), + } + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + c.Headers["path"] = "morpork" + + bBytes := pem.EncodeToMemory(b) + cBytes := pem.EncodeToMemory(c) + + byt := append(bBytes, cBytes...) + + in := bytes.NewBuffer(byt) + + err := ImportKeys(in, []Importer{s}) + require.NoError(t, err) + + bFinal, bRest := pem.Decode(s.data["ankh"]) + require.Equal(t, b.Bytes, bFinal.Bytes) + require.Len(t, bFinal.Headers, 0) // path header is stripped during import + require.Len(t, bRest, 0) + + cFinal, cRest := pem.Decode(s.data["morpork"]) + require.Equal(t, c.Bytes, cFinal.Bytes) + require.Len(t, cFinal.Headers, 0) + require.Len(t, cRest, 0) +} + +func TestImportNoPath(t *testing.T) { + s := NewTestImportStore() + + b := &pem.Block{ + Headers: make(map[string]string), + } + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + + bBytes := pem.EncodeToMemory(b) + + in := bytes.NewBuffer(bBytes) + + err := ImportKeys(in, []Importer{s}) + require.NoError(t, err) + + require.Len(t, s.data, 0) +} + +func TestImportKeys2InOneFile(t *testing.T) { + s := NewTestImportStore() + + b := &pem.Block{ + Headers: make(map[string]string), + } + b.Bytes = make([]byte, 1000) + rand.Read(b.Bytes) + b.Headers["path"] = "ankh" + + b2 := &pem.Block{ + Headers: make(map[string]string), + } + b2.Bytes = make([]byte, 1000) + rand.Read(b2.Bytes) + b2.Headers["path"] = "ankh" + + c := &pem.Block{ + Headers: make(map[string]string), + } + c.Bytes = make([]byte, 1000) + rand.Read(c.Bytes) + c.Headers["path"] = "morpork" + + bBytes := pem.EncodeToMemory(b) + b2Bytes := pem.EncodeToMemory(b2) + bBytes = append(bBytes, b2Bytes...) + cBytes := pem.EncodeToMemory(c) + + byt := append(bBytes, cBytes...) + + in := bytes.NewBuffer(byt) + + err := ImportKeys(in, []Importer{s}) + require.NoError(t, err) + + bFinal, bRest := pem.Decode(s.data["ankh"]) + require.Equal(t, b.Bytes, bFinal.Bytes) + require.Len(t, bFinal.Headers, 0) // path header is stripped during import + + b2Final, b2Rest := pem.Decode(bRest) + require.Equal(t, b2.Bytes, b2Final.Bytes) + require.Len(t, b2Final.Headers, 0) // path header is stripped during import + require.Len(t, b2Rest, 0) + + cFinal, cRest := pem.Decode(s.data["morpork"]) + require.Equal(t, c.Bytes, cFinal.Bytes) + require.Len(t, cFinal.Headers, 0) + require.Len(t, cRest, 0) +}