diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index ef8b3f2ec0..b61c7a4e35 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,87 @@ 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.NewPrivateKeyFileStorage(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.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_test.go b/cmd/notary/keys_test.go index cde7209fd8..f793bc3033 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,293 @@ 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.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) +} + +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..f2b3aba6a7 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) } +// NewPrivateKeyFileStore 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/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..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) +}