diff --git a/client/client_test_pkcs11.go b/client/client_pkcs11_test.go similarity index 100% rename from client/client_test_pkcs11.go rename to client/client_pkcs11_test.go diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index aab55651ba..46b832a46f 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -2,11 +2,13 @@ package main import ( "archive/zip" + "bufio" "fmt" "io" "os" "path/filepath" "sort" + "strconv" "strings" notaryclient "github.com/docker/notary/client" @@ -30,6 +32,7 @@ func init() { cmdKey.AddCommand(cmdKeyImport) cmdKey.AddCommand(cmdKeyImportRoot) cmdKey.AddCommand(cmdRotateKey) + cmdKey.AddCommand(cmdKeyRemove) } var cmdKey = &cobra.Command{ @@ -91,6 +94,13 @@ var cmdKeyImportRoot = &cobra.Command{ Run: keysImportRoot, } +var cmdKeyRemove = &cobra.Command{ + 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.", + Run: keyRemove, +} + func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string { if len(str) <= maxWidth { return str @@ -411,6 +421,101 @@ func keysRotate(cmd *cobra.Command, args []string) { } } +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 keyRemove(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Usage() + fatalf("must specify the key ID of the key to remove") + } + + parseConfig() + keyID := args[0] + + // This is an invalid ID + if len(keyID) != idSize { + fatalf("invalid key ID provided: %s", keyID) + } + stores := getKeyStores(cmd, mainViper.GetString("trust_dir"), retriever, true) + cmd.Println("") + err := removeKeyInteractively(stores, keyID, os.Stdin, cmd.Out()) + cmd.Println("") + if err != nil { + fatalf(err.Error()) + } +} + func getKeyStores(cmd *cobra.Command, directory string, ret passphrase.Retriever, withHardware bool) []trustmanager.KeyStore { diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index 64a0abb43c..836eafb608 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -16,6 +16,8 @@ import ( "github.com/stretchr/testify/assert" ) +var ret = passphrase.ConstantRetriever("pass") + func TestTruncateWithEllipsis(t *testing.T) { digits := "1234567890" // do not truncate @@ -133,3 +135,206 @@ func TestPrettyPrintZeroKeys(t *testing.T) { assert.Len(t, lines, 1) assert.Equal(t, "No signing keys found.", lines[0]) } + +// If there are no keys, removeKeyInteractively will just return an error about +// there not being any key +func TestRemoveIfNoKey(t *testing.T) { + var buf bytes.Buffer + stores := []trustmanager.KeyStore{trustmanager.NewKeyMemoryStore(nil)} + err := removeKeyInteractively(stores, "12345", &buf, &buf) + assert.Error(t, err) + assert.Contains(t, err.Error(), "No key with ID") +} + +// If there is one key, asking to remove it will ask for confirmation. Passing +// anything other than 'yes'/'y'/'' response will abort the deletion and +// not delete the key. +func TestRemoveOneKeyAbort(t *testing.T) { + nos := []string{"no", "NO", "AAAARGH", " N "} + store := trustmanager.NewKeyMemoryStore(ret) + + key, err := trustmanager.GenerateED25519Key(rand.Reader) + assert.NoError(t, err) + err = store.AddKey(key.ID(), "root", key) + assert.NoError(t, err) + + stores := []trustmanager.KeyStore{store} + + for _, noAnswer := range nos { + var out bytes.Buffer + in := bytes.NewBuffer([]byte(noAnswer + "\n")) + + err := removeKeyInteractively(stores, key.ID(), in, &out) + assert.NoError(t, err) + text, err := ioutil.ReadAll(&out) + assert.NoError(t, err) + + output := string(text) + assert.Contains(t, output, "Are you sure") + assert.Contains(t, output, "Aborting action") + assert.Len(t, store.ListKeys(), 1) + } +} + +// If there is one key, asking to remove it will ask for confirmation. Passing +// 'yes'/'y'/'' response will continue the deletion. +func TestRemoveOneKeyConfirm(t *testing.T) { + yesses := []string{"yes", " Y ", "yE", " ", ""} + + for _, yesAnswer := range yesses { + store := trustmanager.NewKeyMemoryStore(ret) + + key, err := trustmanager.GenerateED25519Key(rand.Reader) + assert.NoError(t, err) + err = store.AddKey(key.ID(), "root", key) + assert.NoError(t, err) + + var out bytes.Buffer + in := bytes.NewBuffer([]byte(yesAnswer + "\n")) + + err = removeKeyInteractively( + []trustmanager.KeyStore{store}, key.ID(), in, &out) + assert.NoError(t, err) + text, err := ioutil.ReadAll(&out) + assert.NoError(t, err) + + output := string(text) + assert.Contains(t, output, "Are you sure") + assert.Contains(t, output, "Deleted "+key.ID()) + assert.Len(t, store.ListKeys(), 0) + } +} + +// If there is more than one key, removeKeyInteractively will ask which key to +// delete and will do so over and over until the user quits if the answer is +// invalid. +func TestRemoveMultikeysInvalidInput(t *testing.T) { + in := bytes.NewBuffer([]byte("nota number\n9999\n-3\n0")) + + key, err := trustmanager.GenerateED25519Key(rand.Reader) + assert.NoError(t, err) + + stores := []trustmanager.KeyStore{ + trustmanager.NewKeyMemoryStore(ret), + trustmanager.NewKeyMemoryStore(ret), + } + + err = stores[0].AddKey(key.ID(), "root", key) + assert.NoError(t, err) + + err = stores[1].AddKey("gun/"+key.ID(), "target", key) + assert.NoError(t, err) + + var out bytes.Buffer + + err = removeKeyInteractively(stores, key.ID(), in, &out) + assert.Error(t, err) + text, err := ioutil.ReadAll(&out) + assert.NoError(t, err) + + assert.Len(t, stores[0].ListKeys(), 1) + assert.Len(t, stores[1].ListKeys(), 1) + + // It should have listed the keys over and over, asking which key the user + // wanted to delete + output := string(text) + assert.Contains(t, output, "Found the following matching keys") + var rootCount, targetCount int + for _, line := range strings.Split(output, "\n") { + if strings.Contains(line, key.ID()) { + if strings.Contains(line, "target") { + targetCount++ + } else { + rootCount++ + } + } + } + assert.Equal(t, rootCount, targetCount) + assert.Equal(t, 4, rootCount) // for each of the 4 invalid inputs +} + +// If there is more than one key, removeKeyInteractively will ask which key to +// delete. Then it will confirm whether they want to delete, and the user can +// abort at that confirmation. +func TestRemoveMultikeysAbortChoice(t *testing.T) { + in := bytes.NewBuffer([]byte("1\nn\n")) + + key, err := trustmanager.GenerateED25519Key(rand.Reader) + assert.NoError(t, err) + + stores := []trustmanager.KeyStore{ + trustmanager.NewKeyMemoryStore(ret), + trustmanager.NewKeyMemoryStore(ret), + } + + err = stores[0].AddKey(key.ID(), "root", key) + assert.NoError(t, err) + + err = stores[1].AddKey("gun/"+key.ID(), "target", key) + assert.NoError(t, err) + + var out bytes.Buffer + + err = removeKeyInteractively(stores, key.ID(), in, &out) + assert.NoError(t, err) // no error to abort deleting + text, err := ioutil.ReadAll(&out) + assert.NoError(t, err) + + assert.Len(t, stores[0].ListKeys(), 1) + assert.Len(t, stores[1].ListKeys(), 1) + + // It should have listed the keys, asked whether the user really wanted to + // delete, and then aborted. + output := string(text) + assert.Contains(t, output, "Found the following matching keys") + assert.Contains(t, output, "Are you sure") + assert.Contains(t, output, "Aborting action") +} + +// If there is more than one key, removeKeyInteractively will ask which key to +// delete. Then it will confirm whether they want to delete, and if the user +// confirms, will remove it from the correct key store. +func TestRemoveMultikeysRemoveOnlyChosenKey(t *testing.T) { + in := bytes.NewBuffer([]byte("1\ny\n")) + + key, err := trustmanager.GenerateED25519Key(rand.Reader) + assert.NoError(t, err) + + stores := []trustmanager.KeyStore{ + trustmanager.NewKeyMemoryStore(ret), + trustmanager.NewKeyMemoryStore(ret), + } + + err = stores[0].AddKey(key.ID(), "root", key) + assert.NoError(t, err) + + err = stores[1].AddKey("gun/"+key.ID(), "target", key) + assert.NoError(t, err) + + var out bytes.Buffer + + err = removeKeyInteractively(stores, key.ID(), in, &out) + assert.NoError(t, err) + text, err := ioutil.ReadAll(&out) + assert.NoError(t, err) + + // It should have listed the keys, asked whether the user really wanted to + // delete, and then deleted. + output := string(text) + assert.Contains(t, output, "Found the following matching keys") + assert.Contains(t, output, "Are you sure") + assert.Contains(t, output, "Deleted "+key.ID()) + + // figure out which one we picked to delete, and assert it was deleted + for _, line := range strings.Split(output, "\n") { + if strings.HasPrefix(line, "\t1.") { // we picked the first item + if strings.Contains(line, "root") { // first key store + assert.Len(t, stores[0].ListKeys(), 0) + assert.Len(t, stores[1].ListKeys(), 1) + } else { + assert.Len(t, stores[0].ListKeys(), 1) + assert.Len(t, stores[1].ListKeys(), 0) + } + } + } +}