Skip to content

Commit

Permalink
re-implement import/export
Browse files Browse the repository at this point in the history
Signed-off-by: David Lawrence <[email protected]> (github: endophage)
  • Loading branch information
David Lawrence committed Jul 11, 2016
1 parent 1ff0f8c commit 2e34719
Show file tree
Hide file tree
Showing 6 changed files with 550 additions and 8 deletions.
118 changes: 115 additions & 3 deletions cmd/notary/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand All @@ -63,6 +78,10 @@ type keyCommander struct {
rotateKeyServerManaged bool

input io.Reader

exportGUNs []string
exportKeyIDs []string
outFile string
}

func (k *keyCommander) GetCommand() *cobra.Command {
Expand All @@ -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
}

Expand Down Expand Up @@ -345,14 +386,85 @@ 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.WriteCloser
err error
)
config, err := k.configGetter()
if err != nil {
return err
}

if k.outFile == "" {
out = os.Stdout
} else {
out, err = os.OpenFile(k.outFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, notary.PrivKeyPerms)
if err != nil {
return err
}
}
defer out.Close()

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")
Expand Down
2 changes: 2 additions & 0 deletions const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions trustmanager/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion trustmanager/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
115 changes: 115 additions & 0 deletions utils/keys.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 2e34719

Please sign in to comment.