From 1c967ee060bd75dfc8114e91eb1a8229c3be7508 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 | 119 +++++++++++++- cmd/notary/keys_test.go | 280 +++++++++++++++++++++++++++++++- const.go | 2 + trustmanager/interfaces.go | 4 - trustmanager/keystore.go | 2 +- utils/keys.go | 115 ++++++++++++++ utils/keys_test.go | 317 +++++++++++++++++++++++++++++++++++++ 7 files changed, 826 insertions(+), 13 deletions(-) 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..df606a9860 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,86 @@ 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") + fileStore, err := store.NewPrivateSimpleFileStore(directory, notary.KeyExtension) + 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, fileStore); err != nil { + return err + } + } + return nil +} + +func (k *keyCommander) exportKeys(cmd *cobra.Command, args []string) error { + var ( + out io.Writer + err error + ) + 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.NewPrivateSimpleFileStore(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 { + return utils.ExportKeysByGUN(out, fileStore, gun) + } + } 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_test.go b/cmd/notary/keys_test.go index cde7209fd8..b79d11204a 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,24 @@ 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" ) var ret = passphrase.ConstantRetriever("pass") @@ -534,6 +536,274 @@ func TestChangeKeyPassphraseNonexistentID(t *testing.T) { require.Contains(t, err.Error(), "could not retrieve local key for key ID provided") } +func TestImportKeys(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 + }, + } + 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) + 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.NewPrivateSimpleFileStore(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) +} + +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.NewPrivateSimpleFileStore(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.NewPrivateSimpleFileStore(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, b2.Bytes, block.Bytes) + require.Equal(t, "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.NewPrivateSimpleFileStore(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/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..3e8ddc0e38 100644 --- a/trustmanager/keystore.go +++ b/trustmanager/keystore.go @@ -37,7 +37,7 @@ type GenericKeyStore struct { // 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.NewPrivateSimpleFileStore(baseDir, notary.KeyExtension) if err != nil { return nil, err } diff --git a/utils/keys.go b/utils/keys.go new file mode 100644 index 0000000000..e1cea6011e --- /dev/null +++ b/utils/keys.go @@ -0,0 +1,115 @@ +package utils + +import ( + "encoding/pem" + "io" + "io/ioutil" + "path/filepath" +) + +// simple interface for the one function we need from the Storage interface +type exportStore interface { + Get(string) ([]byte, error) + ListFiles() []string +} + +type importStore interface { + Set(string, []byte) error +} + +// ExportKeysByGUN exports all keys filtered to a GUN +func ExportKeysByGUN(to io.Writer, s exportStore, 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 exportStore, 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 exportStore, 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 importStore) 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 = to.Set(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 + if err = to.Set(writeTo, toWrite); err != nil { + return err + } + } + return nil +} diff --git a/utils/keys_test.go b/utils/keys_test.go new file mode 100644 index 0000000000..7fea1dcfc6 --- /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, 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, 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, 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) +}