mirror of https://github.com/docker/docs.git
570 lines
16 KiB
Go
570 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
notaryclient "github.com/docker/notary/client"
|
|
"github.com/docker/notary/cryptoservice"
|
|
"github.com/docker/notary/passphrase"
|
|
"github.com/docker/notary/trustmanager"
|
|
|
|
"github.com/docker/notary"
|
|
"github.com/docker/notary/tuf/data"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
var cmdKeyTemplate = usageTemplate{
|
|
Use: "key",
|
|
Short: "Operates on keys.",
|
|
Long: `Operations on private keys.`,
|
|
}
|
|
|
|
var cmdKeyListTemplate = usageTemplate{
|
|
Use: "list",
|
|
Short: "Lists keys.",
|
|
Long: "Lists all keys known to notary.",
|
|
}
|
|
|
|
var cmdRotateKeyTemplate = usageTemplate{
|
|
Use: "rotate [ GUN ]",
|
|
Short: "Rotate the signing (non-root) keys for the given Globally Unique Name.",
|
|
Long: "Removes all the old signing (non-root) keys for the given Globally Unique Name, and generates new ones. This only makes local changes - please use then `notary publish` to push the key rotation changes to the remote server.",
|
|
}
|
|
|
|
var cmdKeyGenerateRootKeyTemplate = usageTemplate{
|
|
Use: "generate [ algorithm ]",
|
|
Short: "Generates a new root key with a given algorithm.",
|
|
Long: "Generates a new root key with a given algorithm. If hardware key storage (e.g. a Yubikey) is available, the key will be stored both on hardware and on disk (so that it can be backed up). Please make sure to back up and then remove this on-key disk immediately afterwards.",
|
|
}
|
|
|
|
var cmdKeysBackupTemplate = usageTemplate{
|
|
Use: "backup [ zipfilename ]",
|
|
Short: "Backs up all your on-disk keys to a ZIP file.",
|
|
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{
|
|
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).",
|
|
}
|
|
|
|
var cmdKeysRestoreTemplate = usageTemplate{
|
|
Use: "restore [ zipfilename ]",
|
|
Short: "Restore multiple keys from a ZIP file.",
|
|
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{
|
|
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.",
|
|
}
|
|
|
|
var cmdKeyRemoveTemplate = usageTemplate{
|
|
Use: "remove [ keyID ]",
|
|
Short: "Removes the key with the given keyID.",
|
|
Long: "Removes the key with the given keyID. If the key is stored in more than one location, you will be asked which one to remove.",
|
|
}
|
|
|
|
var cmdKeyPasswdTemplate = usageTemplate{
|
|
Use: "passwd [ keyID ]",
|
|
Short: "Changes the passphrase for the root key with the given keyID.",
|
|
Long: "Changes the passphrase for the root key with the given keyID. Will require validation of the old passphrase.",
|
|
}
|
|
|
|
type keyCommander struct {
|
|
// these need to be set
|
|
configGetter func() (*viper.Viper, error)
|
|
getRetriever func() passphrase.Retriever
|
|
|
|
// these are for command line parsing - no need to set
|
|
keysExportRootChangePassphrase bool
|
|
keysExportGUN string
|
|
rotateKeyRole string
|
|
rotateKeyServerManaged bool
|
|
}
|
|
|
|
func (k *keyCommander) GetCommand() *cobra.Command {
|
|
cmd := cmdKeyTemplate.ToCommand(nil)
|
|
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(cmdKeyRemoveTemplate.ToCommand(k.keyRemove))
|
|
cmd.AddCommand(cmdKeyPasswdTemplate.ToCommand(k.keyPassphraseChange))
|
|
|
|
cmdKeysBackup := cmdKeysBackupTemplate.ToCommand(k.keysBackup)
|
|
cmdKeysBackup.Flags().StringVarP(
|
|
&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,
|
|
"Set a new passphrase for the key being exported")
|
|
cmd.AddCommand(cmdKeyExportRoot)
|
|
|
|
cmdRotateKey := cmdRotateKeyTemplate.ToCommand(k.keysRotate)
|
|
cmdRotateKey.Flags().BoolVarP(&k.rotateKeyServerManaged, "server-managed", "r",
|
|
false, "Signing and key management will be handled by the remote server. "+
|
|
"(no key will be generated or stored locally) "+
|
|
"Can only be used in conjunction with --key-type.")
|
|
cmdRotateKey.Flags().StringVarP(&k.rotateKeyRole, "key-type", "t", "",
|
|
`Key type to rotate. Supported values: "targets", "snapshot". `+
|
|
`If not provided, both targets and snapshot keys will be rotated, `+
|
|
`and the new keys will be locally generated and stored.`)
|
|
cmd.AddCommand(cmdRotateKey)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (k *keyCommander) keysList(cmd *cobra.Command, args []string) error {
|
|
if len(args) > 0 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Println("")
|
|
prettyPrintKeys(ks, cmd.Out())
|
|
cmd.Println("")
|
|
return nil
|
|
}
|
|
|
|
func (k *keyCommander) keysGenerateRootKey(cmd *cobra.Command, args []string) error {
|
|
// We require one or no arguments (since we have a default value), but if the
|
|
// user passes in more than one argument, we error out.
|
|
if len(args) > 1 {
|
|
cmd.Usage()
|
|
return fmt.Errorf(
|
|
"Please provide only one Algorithm as an argument to generate (rsa, ecdsa)")
|
|
}
|
|
|
|
// If no param is given to generate, generates an ecdsa key by default
|
|
algorithm := data.ECDSAKey
|
|
|
|
// If we were provided an argument lets attempt to use it as an algorithm
|
|
if len(args) > 0 {
|
|
algorithm = args[0]
|
|
}
|
|
|
|
allowedCiphers := map[string]bool{
|
|
data.ECDSAKey: true,
|
|
data.RSAKey: true,
|
|
}
|
|
|
|
if !allowedCiphers[strings.ToLower(algorithm)] {
|
|
return fmt.Errorf("Algorithm not allowed, possible values are: RSA, ECDSA")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs := cryptoservice.NewCryptoService("", ks...)
|
|
|
|
pubKey, err := cs.Create(data.CanonicalRootRole, algorithm)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create a new root key: %v", err)
|
|
}
|
|
|
|
cmd.Printf("Generated new %s root key with keyID: %s\n", algorithm, pubKey.ID())
|
|
return nil
|
|
}
|
|
|
|
// keysBackup exports a collection of keys to a ZIP file
|
|
func (k *keyCommander) keysBackup(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 1 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("Must specify output filename for export")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exportFilename := args[0]
|
|
|
|
cs := cryptoservice.NewCryptoService("", ks...)
|
|
|
|
exportFile, err := os.Create(exportFilename)
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating output file: %v", err)
|
|
}
|
|
|
|
// Must use a different passphrase retriever to avoid caching the
|
|
// unlocking passphrase and reusing that.
|
|
exportRetriever := k.getRetriever()
|
|
if k.keysExportGUN != "" {
|
|
err = cs.ExportKeysByGUN(exportFile, k.keysExportGUN, exportRetriever)
|
|
} else {
|
|
err = cs.ExportAllKeys(exportFile, exportRetriever)
|
|
}
|
|
|
|
exportFile.Close()
|
|
|
|
if err != nil {
|
|
os.Remove(exportFilename)
|
|
return fmt.Errorf("Error exporting keys: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// keysExportRoot exports a root key by ID to a PEM file
|
|
func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 2 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("Must specify key ID and output filename for export")
|
|
}
|
|
|
|
keyID := args[0]
|
|
exportFilename := args[1]
|
|
|
|
if len(keyID) != notary.Sha256HexSize {
|
|
return fmt.Errorf("Please specify a valid root key ID")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs := cryptoservice.NewCryptoService("", ks...)
|
|
|
|
exportFile, err := os.Create(exportFilename)
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating output file: %v", err)
|
|
}
|
|
if k.keysExportRootChangePassphrase {
|
|
// Must use a different passphrase retriever to avoid caching the
|
|
// unlocking passphrase and reusing that.
|
|
exportRetriever := k.getRetriever()
|
|
err = cs.ExportRootKeyReencrypt(exportFile, keyID, exportRetriever)
|
|
} else {
|
|
err = cs.ExportRootKey(exportFile, keyID)
|
|
}
|
|
exportFile.Close()
|
|
if err != nil {
|
|
os.Remove(exportFilename)
|
|
return fmt.Errorf("Error exporting root key: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// keysRestore imports keys from a ZIP file
|
|
func (k *keyCommander) keysRestore(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 1 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("Must specify input filename for import")
|
|
}
|
|
|
|
importFilename := args[0]
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs := cryptoservice.NewCryptoService("", ks...)
|
|
|
|
zipReader, err := zip.OpenReader(importFilename)
|
|
if err != nil {
|
|
return fmt.Errorf("Opening file for import: %v", err)
|
|
}
|
|
defer zipReader.Close()
|
|
|
|
err = cs.ImportKeysZip(zipReader.Reader)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Error importing keys: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// keysImportRoot imports a root key from a PEM file
|
|
func (k *keyCommander) keysImportRoot(cmd *cobra.Command, args []string) error {
|
|
if len(args) != 1 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("Must specify input filename for import")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs := cryptoservice.NewCryptoService("", ks...)
|
|
|
|
importFilename := args[0]
|
|
|
|
importFile, err := os.Open(importFilename)
|
|
if err != nil {
|
|
return fmt.Errorf("Opening file for import: %v", err)
|
|
}
|
|
defer importFile.Close()
|
|
|
|
err = cs.ImportRootKey(importFile)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Error importing root key: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 1 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("Must specify a GUN")
|
|
}
|
|
rotateKeyRole := strings.ToLower(k.rotateKeyRole)
|
|
|
|
var rolesToRotate []string
|
|
switch rotateKeyRole {
|
|
case "":
|
|
rolesToRotate = []string{data.CanonicalSnapshotRole, data.CanonicalTargetsRole}
|
|
case data.CanonicalSnapshotRole:
|
|
rolesToRotate = []string{data.CanonicalSnapshotRole}
|
|
case data.CanonicalTargetsRole:
|
|
rolesToRotate = []string{data.CanonicalTargetsRole}
|
|
default:
|
|
return fmt.Errorf("key rotation not supported for %s keys", k.rotateKeyRole)
|
|
}
|
|
if k.rotateKeyServerManaged && rotateKeyRole != data.CanonicalSnapshotRole {
|
|
return fmt.Errorf(
|
|
"remote signing/key management is only supported for the snapshot key")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gun := args[0]
|
|
var rt http.RoundTripper
|
|
if k.rotateKeyServerManaged {
|
|
// this does not actually push the changes, just creates the keys, but
|
|
// it creates a key remotely so it needs a transport
|
|
rt, err = getTransport(config, gun, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
nRepo, err := notaryclient.NewNotaryRepository(
|
|
config.GetString("trust_dir"), gun, getRemoteTrustServer(config),
|
|
rt, k.getRetriever())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, role := range rolesToRotate {
|
|
if err := nRepo.RotateKey(role, k.rotateKeyServerManaged); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func removeKeyInteractively(keyStores []trustmanager.KeyStore, keyID string,
|
|
in io.Reader, out io.Writer) error {
|
|
|
|
var foundKeys [][]string
|
|
var storesByIndex []trustmanager.KeyStore
|
|
|
|
for _, store := range keyStores {
|
|
for keypath, role := range store.ListKeys() {
|
|
if filepath.Base(keypath) == keyID {
|
|
foundKeys = append(foundKeys,
|
|
[]string{keypath, role, store.Name()})
|
|
storesByIndex = append(storesByIndex, store)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(foundKeys) == 0 {
|
|
return fmt.Errorf("No key with ID %s found.", keyID)
|
|
}
|
|
|
|
readIn := bufio.NewReader(in)
|
|
|
|
if len(foundKeys) > 1 {
|
|
for {
|
|
// ask the user for which key to delete
|
|
fmt.Fprintf(out, "Found the following matching keys:\n")
|
|
for i, info := range foundKeys {
|
|
fmt.Fprintf(out, "\t%d. %s: %s (%s)\n", i+1, info[0], info[1], info[2])
|
|
}
|
|
fmt.Fprint(out, "Which would you like to delete? Please enter a number: ")
|
|
result, err := readIn.ReadBytes('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
index, err := strconv.Atoi(strings.TrimSpace(string(result)))
|
|
|
|
if err != nil || index > len(foundKeys) || index < 1 {
|
|
fmt.Fprintf(out, "\nInvalid choice: %s\n", string(result))
|
|
continue
|
|
}
|
|
foundKeys = [][]string{foundKeys[index-1]}
|
|
storesByIndex = []trustmanager.KeyStore{storesByIndex[index-1]}
|
|
fmt.Fprintln(out, "")
|
|
break
|
|
}
|
|
}
|
|
// Now the length must be 1 - ask for confirmation.
|
|
keyDescription := fmt.Sprintf("%s (role %s) from %s", foundKeys[0][0],
|
|
foundKeys[0][1], foundKeys[0][2])
|
|
|
|
fmt.Fprintf(out, "Are you sure you want to remove %s? [Y/n] ",
|
|
keyDescription)
|
|
result, err := readIn.ReadBytes('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
yesno := strings.ToLower(strings.TrimSpace(string(result)))
|
|
|
|
if !strings.HasPrefix("yes", yesno) && yesno != "" {
|
|
fmt.Fprintln(out, "\nAborting action.")
|
|
return nil
|
|
}
|
|
|
|
err = storesByIndex[0].RemoveKey(foundKeys[0][0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(out, "\nDeleted %s.\n", keyDescription)
|
|
return nil
|
|
}
|
|
|
|
// keyRemove deletes a private key based on ID
|
|
func (k *keyCommander) keyRemove(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 1 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("must specify the key ID of the key to remove")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keyID := args[0]
|
|
|
|
// This is an invalid ID
|
|
if len(keyID) != notary.Sha256HexSize {
|
|
return fmt.Errorf("invalid key ID provided: %s", keyID)
|
|
}
|
|
cmd.Println("")
|
|
err = removeKeyInteractively(ks, keyID, os.Stdin,
|
|
cmd.Out())
|
|
cmd.Println("")
|
|
return err
|
|
}
|
|
|
|
// keyPassphraseChange changes the passphrase for a root key's private key based on ID
|
|
func (k *keyCommander) keyPassphraseChange(cmd *cobra.Command, args []string) error {
|
|
if len(args) < 1 {
|
|
cmd.Usage()
|
|
return fmt.Errorf("must specify the key ID of the root key to change the passphrase of")
|
|
}
|
|
|
|
config, err := k.configGetter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ks, err := k.getKeyStores(config, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyID := args[0]
|
|
|
|
// This is an invalid ID
|
|
if len(keyID) != notary.Sha256HexSize {
|
|
return fmt.Errorf("invalid key ID provided: %s", keyID)
|
|
}
|
|
|
|
// We only allow for changing the root key, so use no gun
|
|
cs := cryptoservice.NewCryptoService("", ks...)
|
|
privKey, role, err := cs.GetPrivateKey(keyID)
|
|
if err != nil {
|
|
return fmt.Errorf("could not retrieve local root key for key ID provided: %s", keyID)
|
|
}
|
|
|
|
// Must use a different passphrase retriever to avoid caching the
|
|
// unlocking passphrase and reusing that.
|
|
passChangeRetriever := k.getRetriever()
|
|
keyStore, err := trustmanager.NewKeyFileStore(config.GetString("trust_dir"), passChangeRetriever)
|
|
err = keyStore.AddKey(keyID, role, privKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Println("")
|
|
cmd.Printf("Successfully updated passphrase for key ID: %s", keyID)
|
|
cmd.Println("")
|
|
return nil
|
|
}
|
|
|
|
func (k *keyCommander) getKeyStores(
|
|
config *viper.Viper, withHardware bool) ([]trustmanager.KeyStore, error) {
|
|
retriever := k.getRetriever()
|
|
|
|
directory := config.GetString("trust_dir")
|
|
fileKeyStore, err := trustmanager.NewKeyFileStore(directory, retriever)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Failed to create private key store in directory: %s", directory)
|
|
}
|
|
|
|
ks := []trustmanager.KeyStore{fileKeyStore}
|
|
|
|
if withHardware {
|
|
yubiStore, err := getYubiKeyStore(fileKeyStore, retriever)
|
|
if err == nil && yubiStore != nil {
|
|
// Note that the order is important, since we want to prioritize
|
|
// the yubikey store
|
|
ks = []trustmanager.KeyStore{yubiStore, fileKeyStore}
|
|
}
|
|
}
|
|
|
|
return ks, nil
|
|
}
|