diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 353ec750c0..5afd827dd0 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -52,10 +52,10 @@ var cmdKeysBackupTemplate = usageTemplate{ Long: "Backs up all of your accessible of keys. The keys are reencrypted with a new passphrase. The output is a ZIP file. If the --gun option is passed, only signing keys and no root keys will be backed up. Does not work on keys that are only in hardware (e.g. Yubikeys).", } -var cmdKeyExportRootTemplate = usageTemplate{ +var cmdKeyExportTemplate = usageTemplate{ Use: "export [ keyID ] [ pemfilename ]", - Short: "Export a root key on disk to a PEM file.", - Long: "Exports a single root key on disk, without reencrypting. The output is a PEM file. Does not work on keys that are only in hardware (e.g. Yubikeys).", + Short: "Export a private key on disk to a PEM file.", + Long: "Exports a single private key on disk, without reencrypting. The output is a PEM file. Does not work on keys that are only in hardware (e.g. Yubikeys).", } var cmdKeysRestoreTemplate = usageTemplate{ @@ -64,10 +64,10 @@ var cmdKeysRestoreTemplate = usageTemplate{ Long: "Restores one or more keys from a ZIP file. If hardware key storage (e.g. a Yubikey) is available, root keys will be imported into the hardware, but not backed up to disk in the same location as the other, non-root keys.", } -var cmdKeyImportRootTemplate = usageTemplate{ +var cmdKeyImportTemplate = usageTemplate{ Use: "import [ pemfilename ]", - Short: "Imports a root key from a PEM file.", - Long: "Imports a single root key from a PEM file. If a hardware key storage (e.g. Yubikey) is available, the root key will be imported into the hardware but not backed up on disk again.", + Short: "Imports a key from a PEM file.", + Long: "Imports a single key from a PEM file. If a hardware key storage (e.g. Yubikey) is available, the root key will be imported into the hardware but not backed up on disk again.", } var cmdKeyRemoveTemplate = usageTemplate{ @@ -88,7 +88,7 @@ type keyCommander struct { getRetriever func() passphrase.Retriever // these are for command line parsing - no need to set - keysExportRootChangePassphrase bool + keysExportChangePassphrase bool keysExportGUN string rotateKeyRole string rotateKeyServerManaged bool @@ -99,7 +99,8 @@ func (k *keyCommander) GetCommand() *cobra.Command { cmd.AddCommand(cmdKeyListTemplate.ToCommand(k.keysList)) cmd.AddCommand(cmdKeyGenerateRootKeyTemplate.ToCommand(k.keysGenerateRootKey)) cmd.AddCommand(cmdKeysRestoreTemplate.ToCommand(k.keysRestore)) - cmd.AddCommand(cmdKeyImportRootTemplate.ToCommand(k.keysImportRoot)) + cmd.AddCommand(cmdKeyImportTemplate.ToCommand(k.keysImportRoot)) + cmd.AddCommand(cmdKeyRemoveTemplate.ToCommand(k.keyRemove)) cmd.AddCommand(cmdKeyPasswdTemplate.ToCommand(k.keyPassphraseChange)) @@ -108,11 +109,11 @@ func (k *keyCommander) GetCommand() *cobra.Command { &k.keysExportGUN, "gun", "g", "", "Globally Unique Name to export keys for") cmd.AddCommand(cmdKeysBackup) - cmdKeyExportRoot := cmdKeyExportRootTemplate.ToCommand(k.keysExportRoot) - cmdKeyExportRoot.Flags().BoolVarP( - &k.keysExportRootChangePassphrase, "change-passphrase", "p", false, + cmdKeyExport := cmdKeyExportTemplate.ToCommand(k.keysExport) + cmdKeyExport.Flags().BoolVarP( + &k.keysExportChangePassphrase, "change-passphrase", "p", false, "Set a new passphrase for the key being exported") - cmd.AddCommand(cmdKeyExportRoot) + cmd.AddCommand(cmdKeyExport) cmdRotateKey := cmdRotateKeyTemplate.ToCommand(k.keysRotate) cmdRotateKey.Flags().BoolVarP(&k.rotateKeyServerManaged, "server-managed", "r", @@ -236,8 +237,8 @@ func (k *keyCommander) keysBackup(cmd *cobra.Command, args []string) error { return nil } -// keysExportRoot exports a root key by ID to a PEM file -func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error { +// keysExport exports a key by ID to a PEM file +func (k *keyCommander) keysExport(cmd *cobra.Command, args []string) error { if len(args) < 2 { cmd.Usage() return fmt.Errorf("Must specify key ID and output filename for export") @@ -247,7 +248,7 @@ func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error { exportFilename := args[1] if len(keyID) != notary.Sha256HexSize { - return fmt.Errorf("Please specify a valid root key ID") + return fmt.Errorf("Please specify a valid key ID") } config, err := k.configGetter() @@ -258,24 +259,43 @@ func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error { if err != nil { return err } - cs := cryptoservice.NewCryptoService("", ks...) + + // Search for this key, determine whether this key has a GUN + keyGun := "" + keyRole := "" + for _, store := range ks { + for keypath, role := range store.ListKeys() { + if filepath.Base(keypath) == keyID { + keyRole = role + if role != data.CanonicalRootRole { + dirPath := filepath.Dir(keypath) + if dirPath != "." { // no gun + keyGun = dirPath + } + } + break + } + } + } + + cs := cryptoservice.NewCryptoService(keyGun, ks...) exportFile, err := os.Create(exportFilename) if err != nil { return fmt.Errorf("Error creating output file: %v", err) } - if k.keysExportRootChangePassphrase { + if k.keysExportChangePassphrase { // Must use a different passphrase retriever to avoid caching the // unlocking passphrase and reusing that. exportRetriever := k.getRetriever() - err = cs.ExportRootKeyReencrypt(exportFile, keyID, exportRetriever) + err = cs.ExportKeyReencrypt(exportFile, keyID, exportRetriever) } else { - err = cs.ExportRootKey(exportFile, keyID) + err = cs.ExportKey(exportFile, keyID, keyRole) } exportFile.Close() if err != nil { os.Remove(exportFilename) - return fmt.Errorf("Error exporting root key: %v", err) + return fmt.Errorf("Error exporting %s key: %v", keyRole, err) } return nil } diff --git a/cryptoservice/import_export.go b/cryptoservice/import_export.go index f1d454e151..ff99dfc35f 100644 --- a/cryptoservice/import_export.go +++ b/cryptoservice/import_export.go @@ -13,6 +13,7 @@ import ( "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" ) const zipMadeByUNIX = 3 << 8 @@ -31,15 +32,18 @@ var ( ErrNoKeysFoundForGUN = errors.New("no keys found for specified GUN") ) -// ExportRootKey exports the specified root key to an io.Writer in PEM format. +// ExportKey exports the specified key to an io.Writer in PEM format. // The key's existing encryption is preserved. -func (cs *CryptoService) ExportRootKey(dest io.Writer, keyID string) error { +func (cs *CryptoService) ExportKey(dest io.Writer, keyID, role string) error { var ( pemBytes []byte err error ) for _, ks := range cs.keyStores { + if role != data.CanonicalRootRole { + keyID = filepath.Join(cs.gun, keyID) + } pemBytes, err = ks.ExportKey(keyID) if err != nil { continue @@ -59,9 +63,9 @@ func (cs *CryptoService) ExportRootKey(dest io.Writer, keyID string) error { return nil } -// ExportRootKeyReencrypt exports the specified root key to an io.Writer in +// ExportRootKeyReencrypt exports the specified private key to an io.Writer in // PEM format. The key is reencrypted with a new passphrase. -func (cs *CryptoService) ExportRootKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error { +func (cs *CryptoService) ExportKeyReencrypt(dest io.Writer, keyID string, newPassphraseRetriever passphrase.Retriever) error { privateKey, role, err := cs.GetPrivateKey(keyID) if err != nil { return err @@ -110,7 +114,7 @@ func (cs *CryptoService) ImportRootKey(source io.Reader) error { for _, ks := range cs.keyStores { // don't redeclare err, we want the value carried out of the loop - if err = ks.ImportKey(pemBytes, "root"); err == nil { + if err = ks.ImportKey(pemBytes, data.CanonicalRootRole); err == nil { return nil //bail on the first keystore we import to } } diff --git a/cryptoservice/import_export_test.go b/cryptoservice/import_export_test.go index 5b48216277..9409911637 100644 --- a/cryptoservice/import_export_test.go +++ b/cryptoservice/import_export_test.go @@ -284,7 +284,7 @@ func TestImportExportRootKey(t *testing.T) { tempKeyFilePath := tempKeyFile.Name() defer os.Remove(tempKeyFilePath) - err = cs.ExportRootKey(tempKeyFile, rootKeyID) + err = cs.ExportKey(tempKeyFile, rootKeyID, data.CanonicalRootRole) assert.NoError(t, err) tempKeyFile.Close() @@ -352,7 +352,7 @@ func TestImportExportRootKeyReencrypt(t *testing.T) { tempKeyFilePath := tempKeyFile.Name() defer os.Remove(tempKeyFilePath) - err = cs.ExportRootKeyReencrypt(tempKeyFile, rootKeyID, newPassphraseRetriever) + err = cs.ExportKeyReencrypt(tempKeyFile, rootKeyID, newPassphraseRetriever) assert.NoError(t, err) tempKeyFile.Close()