From 7e9b53fd429213388210b7b8fca6943ff22808a0 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Wed, 15 Nov 2017 11:50:33 -0800 Subject: [PATCH 1/3] add docker trust inspect command for JSON viewing Signed-off-by: Riyaz Faizullabhoy Upstream-commit: cd38d39d0de71225009fc0acb3d4973f3b9c3dc4 Component: cli --- components/cli/cli/command/trust/cmd.go | 1 + components/cli/cli/command/trust/inspect.go | 83 ++++++ .../cli/cli/command/trust/inspect_test.go | 124 +++++++++ components/cli/cli/command/trust/sign.go | 2 +- .../testdata/trust-inspect-empty-repo.golden | 1 + .../trust-inspect-full-repo-no-signers.golden | 7 +- ...rust-inspect-full-repo-with-signers.golden | 15 +- .../trust-inspect-one-tag-no-signers.golden | 7 +- ...t-inspect-unsigned-tag-with-signers.golden | 14 +- .../trust-view-full-repo-no-signers.golden | 6 + .../trust-view-full-repo-with-signers.golden | 14 + .../trust-view-one-tag-no-signers.golden | 6 + ...rust-view-unsigned-tag-with-signers.golden | 13 + components/cli/cli/command/trust/view.go | 95 ++++--- components/cli/cli/command/trust/view_test.go | 48 ++-- .../reference/commandline/trust_inspect.md | 253 ++++++++++++++++++ 16 files changed, 583 insertions(+), 106 deletions(-) create mode 100644 components/cli/cli/command/trust/inspect.go create mode 100644 components/cli/cli/command/trust/inspect_test.go create mode 100644 components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden create mode 100644 components/cli/cli/command/trust/testdata/trust-view-full-repo-no-signers.golden create mode 100644 components/cli/cli/command/trust/testdata/trust-view-full-repo-with-signers.golden create mode 100644 components/cli/cli/command/trust/testdata/trust-view-one-tag-no-signers.golden create mode 100644 components/cli/cli/command/trust/testdata/trust-view-unsigned-tag-with-signers.golden create mode 100644 components/cli/docs/reference/commandline/trust_inspect.md diff --git a/components/cli/cli/command/trust/cmd.go b/components/cli/cli/command/trust/cmd.go index cb8408d1e7..11131be6df 100644 --- a/components/cli/cli/command/trust/cmd.go +++ b/components/cli/cli/command/trust/cmd.go @@ -20,6 +20,7 @@ func NewTrustCommand(dockerCli command.Cli) *cobra.Command { newSignCommand(dockerCli), newTrustKeyCommand(dockerCli), newTrustSignerCommand(dockerCli), + newInspectCommand(dockerCli), ) return cmd } diff --git a/components/cli/cli/command/trust/inspect.go b/components/cli/cli/command/trust/inspect.go new file mode 100644 index 0000000000..4f343b5ac1 --- /dev/null +++ b/components/cli/cli/command/trust/inspect.go @@ -0,0 +1,83 @@ +package trust + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/theupdateframework/notary/tuf/data" +) + +func newInspectCommand(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect IMAGE[:TAG]", + Short: "Return low-level information about keys and signatures", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return inspectTrustInfo(dockerCli, args[0]) + }, + } + return cmd +} + +func inspectTrustInfo(cli command.Cli, remote string) error { + signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote) + if err != nil { + return err + } + // process the signatures to include repo admin if signed by the base targets role + for idx, sig := range signatureRows { + if len(sig.Signers) == 0 { + signatureRows[idx].Signers = append(sig.Signers, releasedRoleName) + } + } + + signerList, adminList := []trustSigner{}, []trustSigner{} + + signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles) + + for signerName, signerKeys := range signerRoleToKeyIDs { + signerList = append(signerList, trustSigner{signerName, signerKeys}) + } + sort.Slice(signerList, func(i, j int) bool { return signerList[i].Name > signerList[j].Name }) + + for _, adminRole := range adminRolesWithSigs { + switch adminRole.Name { + case data.CanonicalRootRole: + adminList = append(adminList, trustSigner{"Root", adminRole.KeyIDs}) + case data.CanonicalTargetsRole: + adminList = append(adminList, trustSigner{"Repository", adminRole.KeyIDs}) + } + } + sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name }) + + trustRepoInfo := &trustRepo{ + SignedTags: signatureRows, + Signers: signerList, + AdminstrativeKeys: adminList, + } + trustInspectJSON, err := json.Marshal(trustRepoInfo) + if err != nil { + return errors.Wrap(err, "error while serializing trusted repository info") + } + fmt.Fprintf(cli.Out(), string(trustInspectJSON)) + return nil +} + +// trustRepo represents consumable information about a trusted repository +type trustRepo struct { + SignedTags trustTagRowList `json:",omitempty"` + Signers []trustSigner `json:",omitempty"` + AdminstrativeKeys []trustSigner `json:",omitempty"` +} + +// trustSigner represents a trusted signer in a trusted repository +// a signer is defined by a name and list of key IDs +type trustSigner struct { + Name string `json:",omitempty"` + Keys []string `json:",omitempty"` +} diff --git a/components/cli/cli/command/trust/inspect_test.go b/components/cli/cli/command/trust/inspect_test.go new file mode 100644 index 0000000000..ac2a819139 --- /dev/null +++ b/components/cli/cli/command/trust/inspect_test.go @@ -0,0 +1,124 @@ +package trust + +import ( + "io/ioutil" + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/cli/internal/test/testutil" + "github.com/gotestyourself/gotestyourself/golden" + "github.com/stretchr/testify/assert" +) + +func TestTrustInspectCommandErrors(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: "invalid-img-reference", + args: []string{"ALPINE"}, + expectedError: "invalid reference format", + }, + } + for _, tc := range testCases { + cmd := newViewCommand( + test.NewFakeCli(&fakeClient{})) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestTrustInspectCommandOfflineErrors(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getOfflineNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"nonexistent-reg-name.io/image"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") + + cli = test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getOfflineNotaryRepository) + cmd = newInspectCommand(cli) + cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") +} + +func TestTrustInspectCommandUninitializedErrors(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getUninitializedNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"reg/unsigned-img"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img") + + cli = test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getUninitializedNotaryRepository) + cmd = newInspectCommand(cli) + cmd.SetArgs([]string{"reg/unsigned-img:tag"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag") +} + +func TestTrustInspectCommandEmptyNotaryRepo(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getEmptyTargetsNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"reg/img:unsigned-tag"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-empty-repo.golden") +} + +func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"signed-repo"}) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-no-signers.golden") +} + +func TestTrustInspectCommandOneTagWithoutSigners(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"signed-repo:green"}) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-one-tag-no-signers.golden") +} + +func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getLoadedNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"signed-repo"}) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-with-signers.golden") +} + +func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getLoadedNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"signed-repo:unsigned"}) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-unsigned-tag-with-signers.golden") +} diff --git a/components/cli/cli/command/trust/sign.go b/components/cli/cli/command/trust/sign.go index 60ad0126cc..2fc233da71 100644 --- a/components/cli/cli/command/trust/sign.go +++ b/components/cli/cli/command/trust/sign.go @@ -176,7 +176,7 @@ func getExistingSignatureInfoForReleasedTag(notaryRepo client.Repository, tag st func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) { sort.Strings(existingSigInfo.Signers) joinedSigners := strings.Join(existingSigInfo.Signers, ", ") - fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.TagName, existingSigInfo.HashHex, joinedSigners) + fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.SignedTag, existingSigInfo.Digest, joinedSigners) } func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.RoleName) error { diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden b/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden index 6fb3b5b34b..a19aa01715 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden @@ -1,6 +1 @@ -SIGNED TAG DIGEST SIGNERS -green 677265656e2d646967657374 (Repo Admin) - -Administrative keys for signed-repo: -Repository Key: targetsID -Root Key: rootID +{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden index 7e73f06726..4b1571d085 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden @@ -1,14 +1 @@ -SIGNED TAG DIGEST SIGNERS -blue 626c75652d646967657374 alice -green 677265656e2d646967657374 (Repo Admin) -red 7265642d646967657374 alice, bob - -List of signers and their keys for signed-repo: - -SIGNER KEYS -alice A -bob B - -Administrative keys for signed-repo: -Repository Key: targetsID -Root Key: rootID +{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden index 6fb3b5b34b..a19aa01715 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden @@ -1,6 +1 @@ -SIGNED TAG DIGEST SIGNERS -green 677265656e2d646967657374 (Repo Admin) - -Administrative keys for signed-repo: -Repository Key: targetsID -Root Key: rootID +{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden index c00a9feecf..d72e709538 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden @@ -1,13 +1 @@ - -No signatures for signed-repo:unsigned - - -List of signers and their keys for signed-repo: - -SIGNER KEYS -alice A -bob B - -Administrative keys for signed-repo: -Repository Key: targetsID -Root Key: rootID +{"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-view-full-repo-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-view-full-repo-no-signers.golden new file mode 100644 index 0000000000..6fb3b5b34b --- /dev/null +++ b/components/cli/cli/command/trust/testdata/trust-view-full-repo-no-signers.golden @@ -0,0 +1,6 @@ +SIGNED TAG DIGEST SIGNERS +green 677265656e2d646967657374 (Repo Admin) + +Administrative keys for signed-repo: +Repository Key: targetsID +Root Key: rootID diff --git a/components/cli/cli/command/trust/testdata/trust-view-full-repo-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-view-full-repo-with-signers.golden new file mode 100644 index 0000000000..7e73f06726 --- /dev/null +++ b/components/cli/cli/command/trust/testdata/trust-view-full-repo-with-signers.golden @@ -0,0 +1,14 @@ +SIGNED TAG DIGEST SIGNERS +blue 626c75652d646967657374 alice +green 677265656e2d646967657374 (Repo Admin) +red 7265642d646967657374 alice, bob + +List of signers and their keys for signed-repo: + +SIGNER KEYS +alice A +bob B + +Administrative keys for signed-repo: +Repository Key: targetsID +Root Key: rootID diff --git a/components/cli/cli/command/trust/testdata/trust-view-one-tag-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-view-one-tag-no-signers.golden new file mode 100644 index 0000000000..6fb3b5b34b --- /dev/null +++ b/components/cli/cli/command/trust/testdata/trust-view-one-tag-no-signers.golden @@ -0,0 +1,6 @@ +SIGNED TAG DIGEST SIGNERS +green 677265656e2d646967657374 (Repo Admin) + +Administrative keys for signed-repo: +Repository Key: targetsID +Root Key: rootID diff --git a/components/cli/cli/command/trust/testdata/trust-view-unsigned-tag-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-view-unsigned-tag-with-signers.golden new file mode 100644 index 0000000000..c00a9feecf --- /dev/null +++ b/components/cli/cli/command/trust/testdata/trust-view-unsigned-tag-with-signers.golden @@ -0,0 +1,13 @@ + +No signatures for signed-repo:unsigned + + +List of signers and their keys for signed-repo: + +SIGNER KEYS +alice A +bob B + +Administrative keys for signed-repo: +Repository Key: targetsID +Root Key: rootID diff --git a/components/cli/cli/command/trust/view.go b/components/cli/cli/command/trust/view.go index 8dd2e47347..a4711b1f6f 100644 --- a/components/cli/cli/command/trust/view.go +++ b/components/cli/cli/command/trust/view.go @@ -22,8 +22,8 @@ import ( // trustTagKey represents a unique signed tag and hex-encoded hash pair type trustTagKey struct { - TagName string - HashHex string + SignedTag string + Digest string } // trustTagRow encodes all human-consumable information for a signed tag, including signers @@ -39,7 +39,7 @@ func (tagComparator trustTagRowList) Len() int { } func (tagComparator trustTagRowList) Less(i, j int) bool { - return tagComparator[i].TagName < tagComparator[j].TagName + return tagComparator[i].SignedTag < tagComparator[j].SignedTag } func (tagComparator trustTagRowList) Swap(i, j int) { @@ -52,39 +52,18 @@ func newViewCommand(dockerCli command.Cli) *cobra.Command { Short: "Display detailed information about keys and signatures", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return lookupTrustInfo(dockerCli, args[0]) + return viewTrustInfo(dockerCli, args[0]) }, } return cmd } -func lookupTrustInfo(cli command.Cli, remote string) error { - ctx := context.Background() - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote) +func viewTrustInfo(cli command.Cli, remote string) error { + signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote) if err != nil { return err } - tag := imgRefAndAuth.Tag() - notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly) - if err != nil { - return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) - } - if err = clearChangeList(notaryRepo); err != nil { - return err - } - defer clearChangeList(notaryRepo) - - // Retrieve all released signatures, match them, and pretty print them - allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag) - if err != nil { - logrus.Debug(trust.NotaryError(imgRefAndAuth.Reference().Name(), err)) - // print an empty table if we don't have signed targets, but have an initialized notary repo - if _, ok := err.(client.ErrNoSuchTarget); !ok { - return fmt.Errorf("No signatures or cannot access %s", remote) - } - } - signatureRows := matchReleasedSignatures(allSignedTargets) if len(signatureRows) > 0 { if err := printSignatures(cli.Out(), signatureRows); err != nil { return err @@ -92,18 +71,6 @@ func lookupTrustInfo(cli command.Cli, remote string) error { } else { fmt.Fprintf(cli.Out(), "\nNo signatures for %s\n\n", remote) } - - // get the administrative roles - adminRolesWithSigs, err := notaryRepo.ListRoles() - if err != nil { - return fmt.Errorf("No signers for %s", remote) - } - - // get delegation roles with the canonical key IDs - delegationRoles, err := notaryRepo.GetDelegationRoles() - if err != nil { - logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err) - } signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles) // If we do not have additional signers, do not display @@ -117,10 +84,54 @@ func lookupTrustInfo(cli command.Cli, remote string) error { // This will always have the root and targets information fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0]) printSortedAdminKeys(cli.Out(), adminRolesWithSigs) - return nil } +// lookupTrustInfo returns processed signature and role information about a notary repository. +// This information is to be pretty printed or serialized into a machine-readable format. +func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.RoleWithSignatures, []data.Role, error) { + ctx := context.Background() + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote) + if err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err + } + tag := imgRefAndAuth.Tag() + notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly) + if err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err) + } + + if err = clearChangeList(notaryRepo); err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err + } + defer clearChangeList(notaryRepo) + + // Retrieve all released signatures, match them, and pretty print them + allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag) + if err != nil { + logrus.Debug(trust.NotaryError(remote, err)) + // print an empty table if we don't have signed targets, but have an initialized notary repo + if _, ok := err.(client.ErrNoSuchTarget); !ok { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote) + } + } + signatureRows := matchReleasedSignatures(allSignedTargets) + + // get the administrative roles + adminRolesWithSigs, err := notaryRepo.ListRoles() + if err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signers for %s", remote) + } + + // get delegation roles with the canonical key IDs + delegationRoles, err := notaryRepo.GetDelegationRoles() + if err != nil { + logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err) + } + + return signatureRows, adminRolesWithSigs, delegationRoles, nil +} + func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) { sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name }) for _, adminRole := range adminRoles { @@ -201,8 +212,8 @@ func printSignatures(out io.Writer, signatureRows trustTagRowList) error { formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName)) } formattedTags = append(formattedTags, formatter.SignedTagInfo{ - Name: sigRow.TagName, - Digest: sigRow.HashHex, + Name: sigRow.SignedTag, + Digest: sigRow.Digest, Signers: formattedSigners, }) } diff --git a/components/cli/cli/command/trust/view_test.go b/components/cli/cli/command/trust/view_test.go index b22fbce458..a8a69d207b 100644 --- a/components/cli/cli/command/trust/view_test.go +++ b/components/cli/cli/command/trust/view_test.go @@ -20,7 +20,7 @@ type fakeClient struct { dockerClient.Client } -func TestTrustInspectCommandErrors(t *testing.T) { +func TestTrustViewCommandErrors(t *testing.T) { testCases := []struct { name string args []string @@ -55,7 +55,7 @@ func TestTrustInspectCommandErrors(t *testing.T) { } } -func TestTrustInspectCommandOfflineErrors(t *testing.T) { +func TestTrustViewCommandOfflineErrors(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getOfflineNotaryRepository) cmd := newViewCommand(cli) @@ -71,7 +71,7 @@ func TestTrustInspectCommandOfflineErrors(t *testing.T) { testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") } -func TestTrustInspectCommandUninitializedErrors(t *testing.T) { +func TestTrustViewCommandUninitializedErrors(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getUninitializedNotaryRepository) cmd := newViewCommand(cli) @@ -87,7 +87,7 @@ func TestTrustInspectCommandUninitializedErrors(t *testing.T) { testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag") } -func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) { +func TestTrustViewCommandEmptyNotaryRepoErrors(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getEmptyTargetsNotaryRepository) cmd := newViewCommand(cli) @@ -107,44 +107,44 @@ func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) { assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:") } -func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) { +func TestTrustViewCommandFullRepoWithoutSigners(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository) cmd := newViewCommand(cli) cmd.SetArgs([]string{"signed-repo"}) assert.NoError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-no-signers.golden") + golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-no-signers.golden") } -func TestTrustInspectCommandOneTagWithoutSigners(t *testing.T) { +func TestTrustViewCommandOneTagWithoutSigners(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository) cmd := newViewCommand(cli) cmd.SetArgs([]string{"signed-repo:green"}) assert.NoError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-one-tag-no-signers.golden") + golden.Assert(t, cli.OutBuffer().String(), "trust-view-one-tag-no-signers.golden") } -func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) { +func TestTrustViewCommandFullRepoWithSigners(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getLoadedNotaryRepository) cmd := newViewCommand(cli) cmd.SetArgs([]string{"signed-repo"}) assert.NoError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-with-signers.golden") + golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-with-signers.golden") } -func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) { +func TestTrustViewCommandUnsignedTagInSignedRepo(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getLoadedNotaryRepository) cmd := newViewCommand(cli) cmd.SetArgs([]string{"signed-repo:unsigned"}) assert.NoError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-unsigned-tag-with-signers.golden") + golden.Assert(t, cli.OutBuffer().String(), "trust-view-unsigned-tag-with-signers.golden") } func TestNotaryRoleToSigner(t *testing.T) { @@ -224,8 +224,8 @@ func TestMatchOneReleasedSingleSignature(t *testing.T) { outputRow := matchedSigRows[0] // Empty signers because "targets/releases" doesn't show up assert.Empty(t, outputRow.Signers) - assert.Equal(t, releasedTgt.Name, outputRow.TagName) - assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex) + assert.Equal(t, releasedTgt.Name, outputRow.SignedTag) + assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest) } func TestMatchOneReleasedMultiSignature(t *testing.T) { @@ -249,8 +249,8 @@ func TestMatchOneReleasedMultiSignature(t *testing.T) { outputRow := matchedSigRows[0] // We should have three signers assert.Equal(t, outputRow.Signers, []string{"a", "b", "c"}) - assert.Equal(t, releasedTgt.Name, outputRow.TagName) - assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex) + assert.Equal(t, releasedTgt.Name, outputRow.SignedTag) + assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest) } func TestMatchMultiReleasedMultiSignature(t *testing.T) { @@ -288,18 +288,18 @@ func TestMatchMultiReleasedMultiSignature(t *testing.T) { // note that the output is sorted by tag name, so we can reliably index to validate data: outputTargetA := matchedSigRows[0] assert.Equal(t, outputTargetA.Signers, []string{"a"}) - assert.Equal(t, targetA.Name, outputTargetA.TagName) - assert.Equal(t, hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.HashHex) + assert.Equal(t, targetA.Name, outputTargetA.SignedTag) + assert.Equal(t, hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.Digest) outputTargetB := matchedSigRows[1] assert.Equal(t, outputTargetB.Signers, []string{"a", "b"}) - assert.Equal(t, targetB.Name, outputTargetB.TagName) - assert.Equal(t, hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.HashHex) + assert.Equal(t, targetB.Name, outputTargetB.SignedTag) + assert.Equal(t, hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.Digest) outputTargetC := matchedSigRows[2] assert.Equal(t, outputTargetC.Signers, []string{"a", "b", "c"}) - assert.Equal(t, targetC.Name, outputTargetC.TagName) - assert.Equal(t, hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.HashHex) + assert.Equal(t, targetC.Name, outputTargetC.SignedTag) + assert.Equal(t, hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.Digest) } func TestMatchReleasedSignatureFromTargets(t *testing.T) { @@ -313,8 +313,8 @@ func TestMatchReleasedSignatureFromTargets(t *testing.T) { outputRow := matchedSigRows[0] // Empty signers because "targets" doesn't show up assert.Empty(t, outputRow.Signers) - assert.Equal(t, releasedTgt.Name, outputRow.TagName) - assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex) + assert.Equal(t, releasedTgt.Name, outputRow.SignedTag) + assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest) } func TestGetSignerRolesWithKeyIDs(t *testing.T) { diff --git a/components/cli/docs/reference/commandline/trust_inspect.md b/components/cli/docs/reference/commandline/trust_inspect.md new file mode 100644 index 0000000000..8e3cf51f3a --- /dev/null +++ b/components/cli/docs/reference/commandline/trust_inspect.md @@ -0,0 +1,253 @@ +--- +title: "trust inspect" +description: "The inspect command description and usage" +keywords: "view, notary, trust" +--- + + + +# trust inspect + +```markdown +Usage: docker trust inspect IMAGE[:TAG] + +Return low-level information about keys and signatures + +``` + +## Description + +`docker trust inspect` provides low-level JSON information on signed repositories. +This includes all image tags that are signed, who signed them, and who can sign +new tags. + +`docker trust inspect` is intended to be used for integrations into other systems, whereas `docker trust view` provides human-friendly output. + +`docker trust inspect` is currently experimental. + + +## Examples + +### Get low-level details about signatures for a single image tag + + +```bash +$ docker trust inspect alpine:latest | jq +{ + "SignedTags": [ + { + "SignedTag": "latest", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + ] + }, + { + "Name": "Root", + "Keys": [ + "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + ] + } + ] +} +``` + +The `SignedTags` key will list the `SignedTag` name, its `Digest`, and the `Signers` responsible for the signature. + +`AdministrativeKeys` will list the `Repository` and `Root` keys. + +This format mirrors the output of `docker trust view` + +If signers are set up for the repository via other `docker trust` commands, `docker trust inspect` includes a `Signers` key: + +```bash + +$ docker trust inspect my-image:purple | jq +{ + "SignedTags": [ + { + "SignedTag": "purple", + "Digest": "941d3dba358621ce3c41ef67b47cf80f701ff80cdf46b5cc86587eaebfe45557", + "Signers": [ + "alice", + "bob", + "carol" + ] + } + ], + "Signers": [ + { + "Name": "alice", + "Keys": [ + "04dd031411ed671ae1e12f47ddc8646d98f135090b01e54c3561e843084484a3", + "6a11e4898a4014d400332ab0e096308c844584ff70943cdd1d6628d577f45fd8" + ] + }, + { + "Name": "bob", + "Keys": [ + "433e245c656ae9733cdcc504bfa560f90950104442c4528c9616daa45824ccba" + ] + }, + { + "Name": "carol", + "Keys": [ + "d32fa8b5ca08273a2880f455fcb318da3dc80aeae1a30610815140deef8f30d9", + "9a8bbec6ba2af88a5fad6047d428d17e6d05dbdd03d15b4fc8a9a0e8049cd606" + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + "27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44" + ] + }, + { + "Name": "Root", + "Keys": [ + "40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f" + ] + } + ] +} +``` + +If the image tag is unsigned or unavailable, `docker trust inspect` does not display any signed tags. + +```bash +$ docker trust inspect unsigned-img +No signatures or cannot access unsigned-img +``` + +However, if other tags are signed in the same image repository, `docker trust inspect` reports relevant key information and omits the `SignedTags` key. + +```bash +$ docker trust inspect alpine:unsigned | jq +{ + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + ] + }, + { + "Name": "Root", + "Keys": [ + "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + ] + } + ] +} +``` + +### Get details about signatures for all image tags in a repository + +```bash +$ docker trust inspect alpine | jq +{ + "SignedTags": [ + { + "SignedTag": "2.6", + "Digest": "9ace551613070689a12857d62c30ef0daa9a376107ec0fff0e34786cedb3399b", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "2.7", + "Digest": "9f08005dff552038f0ad2f46b8e65ff3d25641747d3912e3ea8da6785046561a", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.1", + "Digest": "2d74cbc2fbe3d261fdcca45d493ce1e3f3efd270114a62e383a8e45caeb48788", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.2", + "Digest": "8565a58be8238ef688dbd90e43ec8e080114f1e1db846399116543eb8ef7d7b7", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.3", + "Digest": "06fa785d55c35050241c60274e24ad57025683d5e939b3a31cc94193ca24740b", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.4", + "Digest": "915b0ffca1d76ac57d83f28d568bcb516b6c274843ea8df7fac4b247440f796b", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.5", + "Digest": "b007a354427e1880de9cdba533e8e57382b7f2853a68a478a17d447b302c219c", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.6", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "edge", + "Digest": "23e7d843e63a3eee29b6b8cfcd10e23dd1ef28f47251a985606a31040bf8e096", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "latest", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + ] + }, + { + "Name": "Root", + "Keys": [ + "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + ] + } + ] +} +``` From b1e7ee7a829ad6b105ad395c821568d48aef2964 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Thu, 16 Nov 2017 10:09:57 -0800 Subject: [PATCH 2/3] support multiple arguments to trust inspect Signed-off-by: Riyaz Faizullabhoy Upstream-commit: 1eb87cc096adf8eff401c138f3c2b84c259973e2 Component: cli --- .../cli/cli/command/trust/client_test.go | 19 +- components/cli/cli/command/trust/inspect.go | 40 ++- .../cli/cli/command/trust/inspect_test.go | 9 + .../testdata/trust-inspect-empty-repo.golden | 2 +- .../trust-inspect-full-repo-no-signers.golden | 2 +- ...rust-inspect-full-repo-with-signers.golden | 2 +- ...inspect-multiple-repos-with-signers.golden | 1 + .../trust-inspect-one-tag-no-signers.golden | 2 +- ...t-inspect-unsigned-tag-with-signers.golden | 2 +- .../reference/commandline/trust_inspect.md | 284 +++++++----------- 10 files changed, 165 insertions(+), 198 deletions(-) create mode 100644 components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden diff --git a/components/cli/cli/command/trust/client_test.go b/components/cli/cli/command/trust/client_test.go index cc4fec5074..726dd89761 100644 --- a/components/cli/cli/command/trust/client_test.go +++ b/components/cli/cli/command/trust/client_test.go @@ -192,7 +192,24 @@ func (e EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([ } func (e EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { - return []client.RoleWithSignatures{}, nil + rootRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"rootID"}, + Threshold: 1, + }, + Name: data.CanonicalRootRole, + } + + targetsRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"targetsID"}, + Threshold: 1, + }, + Name: data.CanonicalTargetsRole, + } + return []client.RoleWithSignatures{ + {Role: rootRole}, + {Role: targetsRole}}, nil } func (e EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) { diff --git a/components/cli/cli/command/trust/inspect.go b/components/cli/cli/command/trust/inspect.go index 4f343b5ac1..efb6604fc5 100644 --- a/components/cli/cli/command/trust/inspect.go +++ b/components/cli/cli/command/trust/inspect.go @@ -14,20 +14,40 @@ import ( func newInspectCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ - Use: "inspect IMAGE[:TAG]", + Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]", Short: "Return low-level information about keys and signatures", - Args: cli.ExactArgs(1), + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return inspectTrustInfo(dockerCli, args[0]) + return inspectTrustInfo(dockerCli, args) }, } return cmd } -func inspectTrustInfo(cli command.Cli, remote string) error { +func inspectTrustInfo(cli command.Cli, remotes []string) error { + trustRepoInfoList := []trustRepo{} + for _, remote := range remotes { + trustInfo, err := getRepoTrustInfo(cli, remote) + if err != nil { + return err + } + if trustInfo == nil { + continue + } + trustRepoInfoList = append(trustRepoInfoList, *trustInfo) + } + trustInspectJSON, err := json.Marshal(trustRepoInfoList) + if err != nil { + return errors.Wrap(err, "error while serializing trusted repository info") + } + fmt.Fprintf(cli.Out(), string(trustInspectJSON)) + return nil +} + +func getRepoTrustInfo(cli command.Cli, remote string) (*trustRepo, error) { signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote) if err != nil { - return err + return nil, err } // process the signatures to include repo admin if signed by the base targets role for idx, sig := range signatureRows { @@ -55,17 +75,11 @@ func inspectTrustInfo(cli command.Cli, remote string) error { } sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name }) - trustRepoInfo := &trustRepo{ + return &trustRepo{ SignedTags: signatureRows, Signers: signerList, AdminstrativeKeys: adminList, - } - trustInspectJSON, err := json.Marshal(trustRepoInfo) - if err != nil { - return errors.Wrap(err, "error while serializing trusted repository info") - } - fmt.Fprintf(cli.Out(), string(trustInspectJSON)) - return nil + }, nil } // trustRepo represents consumable information about a trusted repository diff --git a/components/cli/cli/command/trust/inspect_test.go b/components/cli/cli/command/trust/inspect_test.go index ac2a819139..aa0051283f 100644 --- a/components/cli/cli/command/trust/inspect_test.go +++ b/components/cli/cli/command/trust/inspect_test.go @@ -114,6 +114,15 @@ func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) { golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-with-signers.golden") } +func TestTrustInspectCommandMultipleFullReposWithSigners(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(getLoadedNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"signed-repo", "signed-repo"}) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-multiple-repos-with-signers.golden") +} + func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getLoadedNotaryRepository) diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden b/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden index 9e26dfeeb6..25a4eba17c 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden @@ -1 +1 @@ -{} \ No newline at end of file +[{"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden index a19aa01715..c55881c624 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden @@ -1 +1 @@ -{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file +[{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden index 4b1571d085..ce0550dc48 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden @@ -1 +1 @@ -{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file +[{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden new file mode 100644 index 0000000000..73e211cc65 --- /dev/null +++ b/components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden @@ -0,0 +1 @@ +[{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]},{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden index a19aa01715..c55881c624 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden @@ -1 +1 @@ -{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file +[{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden index d72e709538..94d83d7627 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden @@ -1 +1 @@ -{"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]} \ No newline at end of file +[{"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file diff --git a/components/cli/docs/reference/commandline/trust_inspect.md b/components/cli/docs/reference/commandline/trust_inspect.md index 8e3cf51f3a..66dd1e86b7 100644 --- a/components/cli/docs/reference/commandline/trust_inspect.md +++ b/components/cli/docs/reference/commandline/trust_inspect.md @@ -16,7 +16,7 @@ keywords: "view, notary, trust" # trust inspect ```markdown -Usage: docker trust inspect IMAGE[:TAG] +Usage: docker trust inspect IMAGE[:TAG] [IMAGE[:TAG]...] Return low-level information about keys and signatures @@ -40,31 +40,33 @@ new tags. ```bash $ docker trust inspect alpine:latest | jq -{ - "SignedTags": [ - { - "SignedTag": "latest", - "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", - "Signers": [ - "Repo Admin" - ] - } - ], - "AdminstrativeKeys": [ - { - "Name": "Repository", - "Keys": [ - "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" - ] - }, - { - "Name": "Root", - "Keys": [ - "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" - ] - } - ] -} +[ + { + "SignedTags": [ + { + "SignedTag": "latest", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + ] + }, + { + "Name": "Root", + "Keys": [ + "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + ] + } + ] + } +] ``` The `SignedTags` key will list the `SignedTag` name, its `Digest`, and the `Signers` responsible for the signature. @@ -78,55 +80,57 @@ If signers are set up for the repository via other `docker trust` commands, `doc ```bash $ docker trust inspect my-image:purple | jq -{ - "SignedTags": [ - { - "SignedTag": "purple", - "Digest": "941d3dba358621ce3c41ef67b47cf80f701ff80cdf46b5cc86587eaebfe45557", - "Signers": [ - "alice", - "bob", - "carol" - ] - } - ], - "Signers": [ - { - "Name": "alice", - "Keys": [ - "04dd031411ed671ae1e12f47ddc8646d98f135090b01e54c3561e843084484a3", - "6a11e4898a4014d400332ab0e096308c844584ff70943cdd1d6628d577f45fd8" - ] - }, - { - "Name": "bob", - "Keys": [ - "433e245c656ae9733cdcc504bfa560f90950104442c4528c9616daa45824ccba" - ] - }, - { - "Name": "carol", - "Keys": [ - "d32fa8b5ca08273a2880f455fcb318da3dc80aeae1a30610815140deef8f30d9", - "9a8bbec6ba2af88a5fad6047d428d17e6d05dbdd03d15b4fc8a9a0e8049cd606" - ] - } - ], - "AdminstrativeKeys": [ - { - "Name": "Repository", - "Keys": [ - "27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44" - ] - }, - { - "Name": "Root", - "Keys": [ - "40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f" - ] - } - ] -} +[ + { + "SignedTags": [ + { + "SignedTag": "purple", + "Digest": "941d3dba358621ce3c41ef67b47cf80f701ff80cdf46b5cc86587eaebfe45557", + "Signers": [ + "alice", + "bob", + "carol" + ] + } + ], + "Signers": [ + { + "Name": "alice", + "Keys": [ + "04dd031411ed671ae1e12f47ddc8646d98f135090b01e54c3561e843084484a3", + "6a11e4898a4014d400332ab0e096308c844584ff70943cdd1d6628d577f45fd8" + ] + }, + { + "Name": "bob", + "Keys": [ + "433e245c656ae9733cdcc504bfa560f90950104442c4528c9616daa45824ccba" + ] + }, + { + "Name": "carol", + "Keys": [ + "d32fa8b5ca08273a2880f455fcb318da3dc80aeae1a30610815140deef8f30d9", + "9a8bbec6ba2af88a5fad6047d428d17e6d05dbdd03d15b4fc8a9a0e8049cd606" + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + "27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44" + ] + }, + { + "Name": "Root", + "Keys": [ + "40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f" + ] + } + ] + } +] ``` If the image tag is unsigned or unavailable, `docker trust inspect` does not display any signed tags. @@ -140,114 +144,36 @@ However, if other tags are signed in the same image repository, `docker trust in ```bash $ docker trust inspect alpine:unsigned | jq -{ - "AdminstrativeKeys": [ - { - "Name": "Repository", - "Keys": [ - "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" - ] - }, - { - "Name": "Root", - "Keys": [ - "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" - ] - } - ] -} +[ + { + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + ] + }, + { + "Name": "Root", + "Keys": [ + "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + ] + } + ] + } +] ``` ### Get details about signatures for all image tags in a repository ```bash $ docker trust inspect alpine | jq -{ - "SignedTags": [ - { - "SignedTag": "2.6", - "Digest": "9ace551613070689a12857d62c30ef0daa9a376107ec0fff0e34786cedb3399b", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "2.7", - "Digest": "9f08005dff552038f0ad2f46b8e65ff3d25641747d3912e3ea8da6785046561a", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "3.1", - "Digest": "2d74cbc2fbe3d261fdcca45d493ce1e3f3efd270114a62e383a8e45caeb48788", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "3.2", - "Digest": "8565a58be8238ef688dbd90e43ec8e080114f1e1db846399116543eb8ef7d7b7", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "3.3", - "Digest": "06fa785d55c35050241c60274e24ad57025683d5e939b3a31cc94193ca24740b", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "3.4", - "Digest": "915b0ffca1d76ac57d83f28d568bcb516b6c274843ea8df7fac4b247440f796b", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "3.5", - "Digest": "b007a354427e1880de9cdba533e8e57382b7f2853a68a478a17d447b302c219c", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "3.6", - "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "edge", - "Digest": "23e7d843e63a3eee29b6b8cfcd10e23dd1ef28f47251a985606a31040bf8e096", - "Signers": [ - "Repo Admin" - ] - }, - { - "SignedTag": "latest", - "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", - "Signers": [ - "Repo Admin" - ] - } - ], - "AdminstrativeKeys": [ - { - "Name": "Repository", - "Keys": [ - "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" - ] - }, - { - "Name": "Root", - "Keys": [ - "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" - ] - } - ] -} + +``` + + +### Get details about signatures for multiple images + +```bash +$ docker trust inspect alpine ubuntu | jq ``` From f8c7f6621d3317c9f8b7e51a5f992841afa3715b Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Fri, 17 Nov 2017 12:04:51 -0800 Subject: [PATCH 3/3] Use default inspect formatting, remove omitempty, update docs Signed-off-by: Riyaz Faizullabhoy Upstream-commit: a9428285f000b79fed75c68f6f78febb5d43588c Component: cli --- components/cli/cli/command/trust/common.go | 167 +++++++++++++ components/cli/cli/command/trust/inspect.go | 68 +++--- .../cli/cli/command/trust/inspect_test.go | 2 + .../testdata/trust-inspect-empty-repo.golden | 26 +- .../trust-inspect-full-repo-no-signers.golden | 34 ++- ...rust-inspect-full-repo-with-signers.golden | 66 ++++- ...inspect-multiple-repos-with-signers.golden | 129 +++++++++- .../trust-inspect-one-tag-no-signers.golden | 34 ++- .../trust-inspect-uninitialized.golden | 1 + ...t-inspect-unsigned-tag-with-signers.golden | 43 +++- components/cli/cli/command/trust/view.go | 137 ----------- .../reference/commandline/trust_inspect.md | 225 ++++++++++++++++-- 12 files changed, 728 insertions(+), 204 deletions(-) create mode 100644 components/cli/cli/command/trust/common.go create mode 100644 components/cli/cli/command/trust/testdata/trust-inspect-uninitialized.golden diff --git a/components/cli/cli/command/trust/common.go b/components/cli/cli/command/trust/common.go new file mode 100644 index 0000000000..29b0ee622e --- /dev/null +++ b/components/cli/cli/command/trust/common.go @@ -0,0 +1,167 @@ +package trust + +import ( + "context" + "encoding/hex" + "fmt" + "sort" + "strings" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/image" + "github.com/docker/cli/cli/trust" + "github.com/sirupsen/logrus" + "github.com/theupdateframework/notary" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/tuf/data" +) + +// trustTagKey represents a unique signed tag and hex-encoded hash pair +type trustTagKey struct { + SignedTag string + Digest string +} + +// trustTagRow encodes all human-consumable information for a signed tag, including signers +type trustTagRow struct { + trustTagKey + Signers []string +} + +type trustTagRowList []trustTagRow + +func (tagComparator trustTagRowList) Len() int { + return len(tagComparator) +} + +func (tagComparator trustTagRowList) Less(i, j int) bool { + return tagComparator[i].SignedTag < tagComparator[j].SignedTag +} + +func (tagComparator trustTagRowList) Swap(i, j int) { + tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i] +} + +// trustRepo represents consumable information about a trusted repository +type trustRepo struct { + Name string + SignedTags trustTagRowList + Signers []trustSigner + AdminstrativeKeys []trustSigner +} + +// trustSigner represents a trusted signer in a trusted repository +// a signer is defined by a name and list of trustKeys +type trustSigner struct { + Name string `json:",omitempty"` + Keys []trustKey `json:",omitempty"` +} + +// trustKey contains information about trusted keys +type trustKey struct { + ID string `json:",omitempty"` +} + +// lookupTrustInfo returns processed signature and role information about a notary repository. +// This information is to be pretty printed or serialized into a machine-readable format. +func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.RoleWithSignatures, []data.Role, error) { + ctx := context.Background() + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote) + if err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err + } + tag := imgRefAndAuth.Tag() + notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly) + if err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err) + } + + if err = clearChangeList(notaryRepo); err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err + } + defer clearChangeList(notaryRepo) + + // Retrieve all released signatures, match them, and pretty print them + allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag) + if err != nil { + logrus.Debug(trust.NotaryError(remote, err)) + // print an empty table if we don't have signed targets, but have an initialized notary repo + if _, ok := err.(client.ErrNoSuchTarget); !ok { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote) + } + } + signatureRows := matchReleasedSignatures(allSignedTargets) + + // get the administrative roles + adminRolesWithSigs, err := notaryRepo.ListRoles() + if err != nil { + return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signers for %s", remote) + } + + // get delegation roles with the canonical key IDs + delegationRoles, err := notaryRepo.GetDelegationRoles() + if err != nil { + logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err) + } + + return signatureRows, adminRolesWithSigs, delegationRoles, nil +} + +func formatAdminRole(roleWithSigs client.RoleWithSignatures) string { + adminKeyList := roleWithSigs.KeyIDs + sort.Strings(adminKeyList) + + var role string + switch roleWithSigs.Name { + case data.CanonicalTargetsRole: + role = "Repository Key" + case data.CanonicalRootRole: + role = "Root Key" + default: + return "" + } + return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", ")) +} + +func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]string { + signerRoleToKeyIDs := make(map[string][]string) + for _, delRole := range rawDelegationRoles { + switch delRole.Name { + case trust.ReleasesRole, data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole, data.CanonicalTimestampRole: + continue + default: + signerRoleToKeyIDs[notaryRoleToSigner(delRole.Name)] = delRole.KeyIDs + } + } + return signerRoleToKeyIDs +} + +// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been +// signed into the "targets" or "targets/releases" role. Output is sorted by tag name +func matchReleasedSignatures(allTargets []client.TargetSignedStruct) trustTagRowList { + signatureRows := trustTagRowList{} + // do a first pass to get filter on tags signed into "targets" or "targets/releases" + releasedTargetRows := map[trustTagKey][]string{} + for _, tgt := range allTargets { + if isReleasedTarget(tgt.Role.Name) { + releasedKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} + releasedTargetRows[releasedKey] = []string{} + } + } + + // now fill out all signers on released keys + for _, tgt := range allTargets { + targetKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} + // only considered released targets + if _, ok := releasedTargetRows[targetKey]; ok && !isReleasedTarget(tgt.Role.Name) { + releasedTargetRows[targetKey] = append(releasedTargetRows[targetKey], notaryRoleToSigner(tgt.Role.Name)) + } + } + + // compile the final output as a sorted slice + for targetKey, signers := range releasedTargetRows { + signatureRows = append(signatureRows, trustTagRow{targetKey, signers}) + } + sort.Sort(signatureRows) + return signatureRows +} diff --git a/components/cli/cli/command/trust/inspect.go b/components/cli/cli/command/trust/inspect.go index efb6604fc5..772c95e888 100644 --- a/components/cli/cli/command/trust/inspect.go +++ b/components/cli/cli/command/trust/inspect.go @@ -2,12 +2,11 @@ package trust import ( "encoding/json" - "fmt" "sort" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/pkg/errors" + "github.com/docker/cli/cli/command/inspect" "github.com/spf13/cobra" "github.com/theupdateframework/notary/tuf/data" ) @@ -18,36 +17,24 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { Short: "Return low-level information about keys and signatures", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return inspectTrustInfo(dockerCli, args) + return runInspect(dockerCli, args) }, } return cmd } -func inspectTrustInfo(cli command.Cli, remotes []string) error { - trustRepoInfoList := []trustRepo{} - for _, remote := range remotes { - trustInfo, err := getRepoTrustInfo(cli, remote) - if err != nil { - return err - } - if trustInfo == nil { - continue - } - trustRepoInfoList = append(trustRepoInfoList, *trustInfo) +func runInspect(dockerCli command.Cli, remotes []string) error { + getRefFunc := func(ref string) (interface{}, []byte, error) { + i, err := getRepoTrustInfo(dockerCli, ref) + return nil, i, err } - trustInspectJSON, err := json.Marshal(trustRepoInfoList) - if err != nil { - return errors.Wrap(err, "error while serializing trusted repository info") - } - fmt.Fprintf(cli.Out(), string(trustInspectJSON)) - return nil + return inspect.Inspect(dockerCli.Out(), remotes, "", getRefFunc) } -func getRepoTrustInfo(cli command.Cli, remote string) (*trustRepo, error) { +func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) { signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote) if err != nil { - return nil, err + return []byte{}, err } // process the signatures to include repo admin if signed by the base targets role for idx, sig := range signatureRows { @@ -61,37 +48,36 @@ func getRepoTrustInfo(cli command.Cli, remote string) (*trustRepo, error) { signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles) for signerName, signerKeys := range signerRoleToKeyIDs { - signerList = append(signerList, trustSigner{signerName, signerKeys}) + signerKeyList := []trustKey{} + for _, keyID := range signerKeys { + signerKeyList = append(signerKeyList, trustKey{ID: keyID}) + } + signerList = append(signerList, trustSigner{signerName, signerKeyList}) } sort.Slice(signerList, func(i, j int) bool { return signerList[i].Name > signerList[j].Name }) for _, adminRole := range adminRolesWithSigs { switch adminRole.Name { case data.CanonicalRootRole: - adminList = append(adminList, trustSigner{"Root", adminRole.KeyIDs}) + rootKeys := []trustKey{} + for _, keyID := range adminRole.KeyIDs { + rootKeys = append(rootKeys, trustKey{ID: keyID}) + } + adminList = append(adminList, trustSigner{"Root", rootKeys}) case data.CanonicalTargetsRole: - adminList = append(adminList, trustSigner{"Repository", adminRole.KeyIDs}) + targetKeys := []trustKey{} + for _, keyID := range adminRole.KeyIDs { + targetKeys = append(targetKeys, trustKey{ID: keyID}) + } + adminList = append(adminList, trustSigner{"Repository", targetKeys}) } } sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name }) - return &trustRepo{ + return json.Marshal(trustRepo{ + Name: remote, SignedTags: signatureRows, Signers: signerList, AdminstrativeKeys: adminList, - }, nil -} - -// trustRepo represents consumable information about a trusted repository -type trustRepo struct { - SignedTags trustTagRowList `json:",omitempty"` - Signers []trustSigner `json:",omitempty"` - AdminstrativeKeys []trustSigner `json:",omitempty"` -} - -// trustSigner represents a trusted signer in a trusted repository -// a signer is defined by a name and list of key IDs -type trustSigner struct { - Name string `json:",omitempty"` - Keys []string `json:",omitempty"` + }) } diff --git a/components/cli/cli/command/trust/inspect_test.go b/components/cli/cli/command/trust/inspect_test.go index aa0051283f..cb2ee800a8 100644 --- a/components/cli/cli/command/trust/inspect_test.go +++ b/components/cli/cli/command/trust/inspect_test.go @@ -68,6 +68,7 @@ func TestTrustInspectCommandUninitializedErrors(t *testing.T) { cmd.SetArgs([]string{"reg/unsigned-img"}) cmd.SetOutput(ioutil.Discard) testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img") + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden") cli = test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(getUninitializedNotaryRepository) @@ -75,6 +76,7 @@ func TestTrustInspectCommandUninitializedErrors(t *testing.T) { cmd.SetArgs([]string{"reg/unsigned-img:tag"}) cmd.SetOutput(ioutil.Discard) testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag") + golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden") } func TestTrustInspectCommandEmptyNotaryRepo(t *testing.T) { diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden b/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden index 25a4eba17c..ae6fd9c880 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden @@ -1 +1,25 @@ -[{"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file +[ + { + "Name": "reg/img:unsigned-tag", + "SignedTags": [], + "Signers": [], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "rootID" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "targetsID" + } + ] + } + ] + } +] diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden index c55881c624..cda9b40e0e 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden @@ -1 +1,33 @@ -[{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file +[ + { + "Name": "signed-repo", + "SignedTags": [ + { + "SignedTag": "green", + "Digest": "677265656e2d646967657374", + "Signers": [ + "Repo Admin" + ] + } + ], + "Signers": [], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "rootID" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "targetsID" + } + ] + } + ] + } +] diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden index ce0550dc48..496b312bdc 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden @@ -1 +1,65 @@ -[{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file +[ + { + "Name": "signed-repo", + "SignedTags": [ + { + "SignedTag": "blue", + "Digest": "626c75652d646967657374", + "Signers": [ + "alice" + ] + }, + { + "SignedTag": "green", + "Digest": "677265656e2d646967657374", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "red", + "Digest": "7265642d646967657374", + "Signers": [ + "alice", + "bob" + ] + } + ], + "Signers": [ + { + "Name": "bob", + "Keys": [ + { + "ID": "B" + } + ] + }, + { + "Name": "alice", + "Keys": [ + { + "ID": "A" + } + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "rootID" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "targetsID" + } + ] + } + ] + } +] diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden index 73e211cc65..fd87979e5c 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden @@ -1 +1,128 @@ -[{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]},{"SignedTags":[{"SignedTag":"blue","Digest":"626c75652d646967657374","Signers":["alice"]},{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]},{"SignedTag":"red","Digest":"7265642d646967657374","Signers":["alice","bob"]}],"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file +[ + { + "Name": "signed-repo", + "SignedTags": [ + { + "SignedTag": "blue", + "Digest": "626c75652d646967657374", + "Signers": [ + "alice" + ] + }, + { + "SignedTag": "green", + "Digest": "677265656e2d646967657374", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "red", + "Digest": "7265642d646967657374", + "Signers": [ + "alice", + "bob" + ] + } + ], + "Signers": [ + { + "Name": "bob", + "Keys": [ + { + "ID": "B" + } + ] + }, + { + "Name": "alice", + "Keys": [ + { + "ID": "A" + } + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "rootID" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "targetsID" + } + ] + } + ] + }, + { + "Name": "signed-repo", + "SignedTags": [ + { + "SignedTag": "blue", + "Digest": "626c75652d646967657374", + "Signers": [ + "alice" + ] + }, + { + "SignedTag": "green", + "Digest": "677265656e2d646967657374", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "red", + "Digest": "7265642d646967657374", + "Signers": [ + "alice", + "bob" + ] + } + ], + "Signers": [ + { + "Name": "bob", + "Keys": [ + { + "ID": "B" + } + ] + }, + { + "Name": "alice", + "Keys": [ + { + "ID": "A" + } + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "rootID" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "targetsID" + } + ] + } + ] + } +] diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden index c55881c624..b1745d5c75 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden @@ -1 +1,33 @@ -[{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file +[ + { + "Name": "signed-repo:green", + "SignedTags": [ + { + "SignedTag": "green", + "Digest": "677265656e2d646967657374", + "Signers": [ + "Repo Admin" + ] + } + ], + "Signers": [], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "rootID" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "targetsID" + } + ] + } + ] + } +] diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-uninitialized.golden b/components/cli/cli/command/trust/testdata/trust-inspect-uninitialized.golden new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/components/cli/cli/command/trust/testdata/trust-inspect-uninitialized.golden @@ -0,0 +1 @@ +[] diff --git a/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden b/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden index 94d83d7627..8c0a84eb29 100644 --- a/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden +++ b/components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden @@ -1 +1,42 @@ -[{"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}] \ No newline at end of file +[ + { + "Name": "signed-repo:unsigned", + "SignedTags": [], + "Signers": [ + { + "Name": "bob", + "Keys": [ + { + "ID": "B" + } + ] + }, + { + "Name": "alice", + "Keys": [ + { + "ID": "A" + } + ] + } + ], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "rootID" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "targetsID" + } + ] + } + ] + } +] diff --git a/components/cli/cli/command/trust/view.go b/components/cli/cli/command/trust/view.go index a4711b1f6f..16e7f13f72 100644 --- a/components/cli/cli/command/trust/view.go +++ b/components/cli/cli/command/trust/view.go @@ -1,8 +1,6 @@ package trust import ( - "context" - "encoding/hex" "fmt" "io" "sort" @@ -11,41 +9,10 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/trust" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/tuf/data" ) -// trustTagKey represents a unique signed tag and hex-encoded hash pair -type trustTagKey struct { - SignedTag string - Digest string -} - -// trustTagRow encodes all human-consumable information for a signed tag, including signers -type trustTagRow struct { - trustTagKey - Signers []string -} - -type trustTagRowList []trustTagRow - -func (tagComparator trustTagRowList) Len() int { - return len(tagComparator) -} - -func (tagComparator trustTagRowList) Less(i, j int) bool { - return tagComparator[i].SignedTag < tagComparator[j].SignedTag -} - -func (tagComparator trustTagRowList) Swap(i, j int) { - tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i] -} - func newViewCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "view IMAGE[:TAG]", @@ -87,51 +54,6 @@ func viewTrustInfo(cli command.Cli, remote string) error { return nil } -// lookupTrustInfo returns processed signature and role information about a notary repository. -// This information is to be pretty printed or serialized into a machine-readable format. -func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.RoleWithSignatures, []data.Role, error) { - ctx := context.Background() - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote) - if err != nil { - return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err - } - tag := imgRefAndAuth.Tag() - notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly) - if err != nil { - return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err) - } - - if err = clearChangeList(notaryRepo); err != nil { - return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err - } - defer clearChangeList(notaryRepo) - - // Retrieve all released signatures, match them, and pretty print them - allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag) - if err != nil { - logrus.Debug(trust.NotaryError(remote, err)) - // print an empty table if we don't have signed targets, but have an initialized notary repo - if _, ok := err.(client.ErrNoSuchTarget); !ok { - return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote) - } - } - signatureRows := matchReleasedSignatures(allSignedTargets) - - // get the administrative roles - adminRolesWithSigs, err := notaryRepo.ListRoles() - if err != nil { - return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signers for %s", remote) - } - - // get delegation roles with the canonical key IDs - delegationRoles, err := notaryRepo.GetDelegationRoles() - if err != nil { - logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err) - } - - return signatureRows, adminRolesWithSigs, delegationRoles, nil -} - func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) { sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name }) for _, adminRole := range adminRoles { @@ -139,65 +61,6 @@ func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) } } -func formatAdminRole(roleWithSigs client.RoleWithSignatures) string { - adminKeyList := roleWithSigs.KeyIDs - sort.Strings(adminKeyList) - - var role string - switch roleWithSigs.Name { - case data.CanonicalTargetsRole: - role = "Repository Key" - case data.CanonicalRootRole: - role = "Root Key" - default: - return "" - } - return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", ")) -} - -func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]string { - signerRoleToKeyIDs := make(map[string][]string) - for _, delRole := range rawDelegationRoles { - switch delRole.Name { - case trust.ReleasesRole, data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole, data.CanonicalTimestampRole: - continue - default: - signerRoleToKeyIDs[notaryRoleToSigner(delRole.Name)] = delRole.KeyIDs - } - } - return signerRoleToKeyIDs -} - -// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been -// signed into the "targets" or "targets/releases" role. Output is sorted by tag name -func matchReleasedSignatures(allTargets []client.TargetSignedStruct) trustTagRowList { - signatureRows := trustTagRowList{} - // do a first pass to get filter on tags signed into "targets" or "targets/releases" - releasedTargetRows := map[trustTagKey][]string{} - for _, tgt := range allTargets { - if isReleasedTarget(tgt.Role.Name) { - releasedKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} - releasedTargetRows[releasedKey] = []string{} - } - } - - // now fill out all signers on released keys - for _, tgt := range allTargets { - targetKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} - // only considered released targets - if _, ok := releasedTargetRows[targetKey]; ok && !isReleasedTarget(tgt.Role.Name) { - releasedTargetRows[targetKey] = append(releasedTargetRows[targetKey], notaryRoleToSigner(tgt.Role.Name)) - } - } - - // compile the final output as a sorted slice - for targetKey, signers := range releasedTargetRows { - signatureRows = append(signatureRows, trustTagRow{targetKey, signers}) - } - sort.Sort(signatureRows) - return signatureRows -} - // pretty print with ordered rows func printSignatures(out io.Writer, signatureRows trustTagRowList) error { trustTagCtx := formatter.Context{ diff --git a/components/cli/docs/reference/commandline/trust_inspect.md b/components/cli/docs/reference/commandline/trust_inspect.md index 66dd1e86b7..db970a7d6c 100644 --- a/components/cli/docs/reference/commandline/trust_inspect.md +++ b/components/cli/docs/reference/commandline/trust_inspect.md @@ -28,7 +28,8 @@ Return low-level information about keys and signatures This includes all image tags that are signed, who signed them, and who can sign new tags. -`docker trust inspect` is intended to be used for integrations into other systems, whereas `docker trust view` provides human-friendly output. +`docker trust inspect` prints the trust information in a machine-readable format. Refer to +[`docker trust view`](trust_view.md) for a human-friendly output. `docker trust inspect` is currently experimental. @@ -37,11 +38,14 @@ new tags. ### Get low-level details about signatures for a single image tag +Use the `docker trust inspect` to get trust information about an image. The +following example prints trust information for the `alpine:latest` image: ```bash -$ docker trust inspect alpine:latest | jq +$ docker trust inspect alpine:latest [ { + "Name": "alpine:latest", "SignedTags": [ { "SignedTag": "latest", @@ -51,17 +55,22 @@ $ docker trust inspect alpine:latest | jq ] } ], + "Signers": [], "AdminstrativeKeys": [ { "Name": "Repository", "Keys": [ - "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + { + "ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + } ] }, { "Name": "Root", "Keys": [ - "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + { + "ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + } ] } ] @@ -78,10 +87,10 @@ This format mirrors the output of `docker trust view` If signers are set up for the repository via other `docker trust` commands, `docker trust inspect` includes a `Signers` key: ```bash - -$ docker trust inspect my-image:purple | jq +$ docker trust inspect my-image:purple [ { + "Name": "my-image:purple", "SignedTags": [ { "SignedTag": "purple", @@ -97,21 +106,31 @@ $ docker trust inspect my-image:purple | jq { "Name": "alice", "Keys": [ - "04dd031411ed671ae1e12f47ddc8646d98f135090b01e54c3561e843084484a3", - "6a11e4898a4014d400332ab0e096308c844584ff70943cdd1d6628d577f45fd8" + { + "ID": "04dd031411ed671ae1e12f47ddc8646d98f135090b01e54c3561e843084484a3" + }, + { + "ID": "6a11e4898a4014d400332ab0e096308c844584ff70943cdd1d6628d577f45fd8" + } ] }, { "Name": "bob", "Keys": [ - "433e245c656ae9733cdcc504bfa560f90950104442c4528c9616daa45824ccba" + { + "ID": "433e245c656ae9733cdcc504bfa560f90950104442c4528c9616daa45824ccba" + } ] }, { "Name": "carol", "Keys": [ - "d32fa8b5ca08273a2880f455fcb318da3dc80aeae1a30610815140deef8f30d9", - "9a8bbec6ba2af88a5fad6047d428d17e6d05dbdd03d15b4fc8a9a0e8049cd606" + { + "ID": "d32fa8b5ca08273a2880f455fcb318da3dc80aeae1a30610815140deef8f30d9" + }, + { + "ID": "9a8bbec6ba2af88a5fad6047d428d17e6d05dbdd03d15b4fc8a9a0e8049cd606" + } ] } ], @@ -119,13 +138,17 @@ $ docker trust inspect my-image:purple | jq { "Name": "Repository", "Keys": [ - "27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44" + { + "ID": "27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44" + } ] }, { "Name": "Root", "Keys": [ - "40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f" + { + "ID": "40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f" + } ] } ] @@ -140,23 +163,29 @@ $ docker trust inspect unsigned-img No signatures or cannot access unsigned-img ``` -However, if other tags are signed in the same image repository, `docker trust inspect` reports relevant key information and omits the `SignedTags` key. +However, if other tags are signed in the same image repository, `docker trust inspect` reports relevant key information: ```bash -$ docker trust inspect alpine:unsigned | jq +$ docker trust inspect alpine:unsigned [ { + "Name": "alpine:unsigned", + "Signers": [], "AdminstrativeKeys": [ { "Name": "Repository", "Keys": [ - "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + { + "ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + } ] }, { "Name": "Root", "Keys": [ - "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + { + "ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + } ] } ] @@ -166,14 +195,170 @@ $ docker trust inspect alpine:unsigned | jq ### Get details about signatures for all image tags in a repository -```bash -$ docker trust inspect alpine | jq +If no tag is specified, `docker trust inspect` will report details for all signed tags in the repository: +```bash +$ docker trust inspect alpine +[ + { + "Name": "alpine", + "SignedTags": [ + { + "SignedTag": "3.5", + "Digest": "b007a354427e1880de9cdba533e8e57382b7f2853a68a478a17d447b302c219c", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.6", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "edge", + "Digest": "23e7d843e63a3eee29b6b8cfcd10e23dd1ef28f47251a985606a31040bf8e096", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "latest", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + } + ], + "Signers": [], + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + { + "ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + } + ] + }, + { + "Name": "Root", + "Keys": [ + { + "ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + } + ] + } + ] + } +] ``` ### Get details about signatures for multiple images +`docker trust inspect` can take multiple repositories and images as arguments, and reports the results in an ordered list: + ```bash -$ docker trust inspect alpine ubuntu | jq +$ docker trust inspect alpine notary +[ + { + "Name": "alpine", + "SignedTags": [ + { + "SignedTag": "3.5", + "Digest": "b007a354427e1880de9cdba533e8e57382b7f2853a68a478a17d447b302c219c", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "3.6", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "edge", + "Digest": "23e7d843e63a3eee29b6b8cfcd10e23dd1ef28f47251a985606a31040bf8e096", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "integ-test-base", + "Digest": "3952dc48dcc4136ccdde37fbef7e250346538a55a0366e3fccc683336377e372", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "latest", + "Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478", + "Signers": [ + "Repo Admin" + ] + } + ], + "Signers": [], + "AdminstrativeKeys": [ + { + "Name": "Repository", + "Keys": [ + { + "ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd" + } + ] + }, + { + "Name": "Root", + "Keys": [ + { + "ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce" + } + ] + } + ] + }, + { + "Name": "notary", + "SignedTags": [ + { + "SignedTag": "server", + "Digest": "71f64ab718a3331dee103bc5afc6bc492914738ce37c2d2f127a8133714ecf5c", + "Signers": [ + "Repo Admin" + ] + }, + { + "SignedTag": "signer", + "Digest": "a6122d79b1e74f70b5dd933b18a6d1f99329a4728011079f06b245205f158fe8", + "Signers": [ + "Repo Admin" + ] + } + ], + "Signers": [], + "AdminstrativeKeys": [ + { + "Name": "Root", + "Keys": [ + { + "ID": "8cdcdef5bd039f4ab5a029126951b5985eebf57cabdcdc4d21f5b3be8bb4ce92" + } + ] + }, + { + "Name": "Repository", + "Keys": [ + { + "ID": "85bfd031017722f950d480a721f845a2944db26a3dc084040a70f1b0d9bbb3df" + } + ] + } + ] + } +] ```