From 609f8b4b81f1b672c7f6450fe9d1c9f9f8c1eb3d Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Thu, 24 Aug 2017 15:46:01 -0700 Subject: [PATCH] trust revoke: add docker trust revoke command Signed-off-by: Riyaz Faizullabhoy --- cli/command/trust/revoke.go | 126 +++++++++++++++++++++++++++++++ cli/command/trust/revoke_test.go | 93 +++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 cli/command/trust/revoke.go create mode 100644 cli/command/trust/revoke_test.go diff --git a/cli/command/trust/revoke.go b/cli/command/trust/revoke.go new file mode 100644 index 0000000000..b150f402cf --- /dev/null +++ b/cli/command/trust/revoke.go @@ -0,0 +1,126 @@ +package trust + +import ( + "fmt" + "os" + + "github.com/docker/notary/client" + "github.com/docker/notary/tuf/data" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/image" + "github.com/docker/cli/cli/trust" + "github.com/spf13/cobra" +) + +type revokeOptions struct { + forceYes bool +} + +func newRevokeCommand(dockerCli command.Cli) *cobra.Command { + options := revokeOptions{} + cmd := &cobra.Command{ + Use: "revoke [OPTIONS] IMAGE[:TAG]", + Short: "Remove trust for an image", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return revokeTrust(dockerCli, args[0], options) + }, + } + flags := cmd.Flags() + flags.BoolVarP(&options.forceYes, "yes", "y", false, "Answer yes to the removal question (no confirmation)") + return cmd +} + +func revokeTrust(cli command.Cli, remote string, options revokeOptions) error { + _, ref, repoInfo, authConfig, err := getImageReferencesAndAuth(cli, remote) + if err != nil { + return err + } + tag, err := getTag(ref) + if err != nil { + return err + } + if tag == "" && !options.forceYes { + in := os.Stdin + fmt.Fprintf( + cli.Out(), + "Please confirm you would like to delete all signature data for %s? (y/n) ", + remote, + ) + deleteRemote := askConfirm(in) + if !deleteRemote { + fmt.Fprintf(cli.Out(), "\nAborting action.\n") + return nil + } + } + + notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, *authConfig, "push", "pull") + if err != nil { + return err + } + + if err = clearChangeList(notaryRepo); err != nil { + return err + } + defer clearChangeList(notaryRepo) + if err := revokeTestHelper(notaryRepo, tag); err != nil { + return fmt.Errorf("could not remove signature for %s: %s", remote, err) + } + fmt.Fprintf(cli.Out(), "Successfully deleted signature for %s\n", remote) + return nil +} + +func revokeTestHelper(notaryRepo *client.NotaryRepository, tag string) error { + if tag != "" { + // Revoke signature for the specified tag + if err := revokeSingleSig(notaryRepo, tag); err != nil { + return err + } + } else { + // revoke all signatures for the image, as no tag was given + if err := revokeAllSigs(notaryRepo); err != nil { + return err + } + } + + // Publish change + return notaryRepo.Publish() +} + +func revokeSingleSig(notaryRepo *client.NotaryRepository, tag string) error { + releasedTargetWithRole, err := notaryRepo.GetTargetByName(tag, trust.ReleasesRole, data.CanonicalTargetsRole) + if err != nil { + return err + } + releasedTarget := releasedTargetWithRole.Target + return getSignableRolesForTargetAndRemove(releasedTarget, notaryRepo) +} + +func revokeAllSigs(notaryRepo *client.NotaryRepository) error { + + releasedTargetWithRoleList, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole) + if err != nil { + return err + } + + // we need all the roles that signed each released target so we can remove from all roles. + for _, releasedTargetWithRole := range releasedTargetWithRoleList { + // remove from all roles + if err := getSignableRolesForTargetAndRemove(releasedTargetWithRole.Target, notaryRepo); err != nil { + return err + } + } + return nil +} + +// get all the roles that signed the target and removes it from all roles. +func getSignableRolesForTargetAndRemove(releasedTarget client.Target, notaryRepo *client.NotaryRepository) error { + signableRoles, err := image.GetSignableRoles(notaryRepo, &releasedTarget) + if err != nil { + return err + } + // remove from all roles + return notaryRepo.RemoveTarget(releasedTarget.Name, signableRoles...) +} diff --git a/cli/command/trust/revoke_test.go b/cli/command/trust/revoke_test.go new file mode 100644 index 0000000000..68b6f76187 --- /dev/null +++ b/cli/command/trust/revoke_test.go @@ -0,0 +1,93 @@ +package trust + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/cli/internal/test/testutil" + "github.com/docker/notary/client" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustpinning" + "github.com/stretchr/testify/assert" +) + +func TestTrustRevokeErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires exactly 1 argument", + }, + { + name: "too-many-args", + args: []string{"remote1", "remote2"}, + expectedError: "requires exactly 1 argument", + }, + { + name: "sha-reference", + args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, + expectedError: "invalid repository name", + }, + { + name: "trust-data-for-tag-does-not-exist", + args: []string{"alpine:foo"}, + expectedError: "could not remove signature for alpine:foo: No valid trust data for foo", + }, + { + name: "invalid-img-reference", + args: []string{"ALPINE"}, + expectedError: "invalid reference format", + }, + { + name: "unsigned-img-reference", + args: []string{"riyaz/unsigned-img:v1"}, + expectedError: strings.Join([]string{ + "could not remove signature for riyaz/unsigned-img:v1:", + "notary.docker.io does not have trust data for docker.io/riyaz/unsigned-img", + }, " "), + }, + { + name: "no-signing-keys-for-image", + args: []string{"alpine", "-y"}, + expectedError: "could not remove signature for alpine: could not find necessary signing keys", + }, + { + name: "digest-reference", + args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"}, + expectedError: "cannot use a digest reference for IMAGE:TAG", + }, + } + for _, tc := range testCases { + cmd := newRevokeCommand( + test.NewFakeCli(&fakeClient{})) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewRevokeTrustAllSigConfirmation(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cmd := newRevokeCommand(cli) + cmd.SetArgs([]string{"alpine"}) + assert.NoError(t, cmd.Execute()) + + assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for alpine? (y/n) \nAborting action.") +} + +func TestGetSignableRolesForTargetAndRemoveError(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "notary-test-") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + notaryRepo, err := client.NewFileCachedNotaryRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever("password"), trustpinning.TrustPinConfig{}) + target := client.Target{} + err = getSignableRolesForTargetAndRemove(target, notaryRepo) + assert.EqualError(t, err, "client is offline") +}