add docker trust inspect command for JSON viewing
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
parent
9022ea549d
commit
cd38d39d0d
@ -20,6 +20,7 @@ func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
|
|||||||
newSignCommand(dockerCli),
|
newSignCommand(dockerCli),
|
||||||
newTrustKeyCommand(dockerCli),
|
newTrustKeyCommand(dockerCli),
|
||||||
newTrustSignerCommand(dockerCli),
|
newTrustSignerCommand(dockerCli),
|
||||||
|
newInspectCommand(dockerCli),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
83
cli/command/trust/inspect.go
Normal file
83
cli/command/trust/inspect.go
Normal file
@ -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"`
|
||||||
|
}
|
124
cli/command/trust/inspect_test.go
Normal file
124
cli/command/trust/inspect_test.go
Normal file
@ -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")
|
||||||
|
}
|
@ -176,7 +176,7 @@ func getExistingSignatureInfoForReleasedTag(notaryRepo client.Repository, tag st
|
|||||||
func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) {
|
func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) {
|
||||||
sort.Strings(existingSigInfo.Signers)
|
sort.Strings(existingSigInfo.Signers)
|
||||||
joinedSigners := strings.Join(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 {
|
func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.RoleName) error {
|
||||||
|
1
cli/command/trust/testdata/trust-inspect-empty-repo.golden
vendored
Normal file
1
cli/command/trust/testdata/trust-inspect-empty-repo.golden
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -1,6 +1 @@
|
|||||||
SIGNED TAG DIGEST SIGNERS
|
{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}
|
||||||
green 677265656e2d646967657374 (Repo Admin)
|
|
||||||
|
|
||||||
Administrative keys for signed-repo:
|
|
||||||
Repository Key: targetsID
|
|
||||||
Root Key: rootID
|
|
@ -1,14 +1 @@
|
|||||||
SIGNED TAG DIGEST SIGNERS
|
{"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"]}]}
|
||||||
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
|
|
@ -1,6 +1 @@
|
|||||||
SIGNED TAG DIGEST SIGNERS
|
{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}
|
||||||
green 677265656e2d646967657374 (Repo Admin)
|
|
||||||
|
|
||||||
Administrative keys for signed-repo:
|
|
||||||
Repository Key: targetsID
|
|
||||||
Root Key: rootID
|
|
@ -1,13 +1 @@
|
|||||||
|
{"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}
|
||||||
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
|
|
6
cli/command/trust/testdata/trust-view-full-repo-no-signers.golden
vendored
Normal file
6
cli/command/trust/testdata/trust-view-full-repo-no-signers.golden
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
SIGNED TAG DIGEST SIGNERS
|
||||||
|
green 677265656e2d646967657374 (Repo Admin)
|
||||||
|
|
||||||
|
Administrative keys for signed-repo:
|
||||||
|
Repository Key: targetsID
|
||||||
|
Root Key: rootID
|
14
cli/command/trust/testdata/trust-view-full-repo-with-signers.golden
vendored
Normal file
14
cli/command/trust/testdata/trust-view-full-repo-with-signers.golden
vendored
Normal file
@ -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
|
6
cli/command/trust/testdata/trust-view-one-tag-no-signers.golden
vendored
Normal file
6
cli/command/trust/testdata/trust-view-one-tag-no-signers.golden
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
SIGNED TAG DIGEST SIGNERS
|
||||||
|
green 677265656e2d646967657374 (Repo Admin)
|
||||||
|
|
||||||
|
Administrative keys for signed-repo:
|
||||||
|
Repository Key: targetsID
|
||||||
|
Root Key: rootID
|
13
cli/command/trust/testdata/trust-view-unsigned-tag-with-signers.golden
vendored
Normal file
13
cli/command/trust/testdata/trust-view-unsigned-tag-with-signers.golden
vendored
Normal file
@ -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
|
@ -22,8 +22,8 @@ import (
|
|||||||
|
|
||||||
// trustTagKey represents a unique signed tag and hex-encoded hash pair
|
// trustTagKey represents a unique signed tag and hex-encoded hash pair
|
||||||
type trustTagKey struct {
|
type trustTagKey struct {
|
||||||
TagName string
|
SignedTag string
|
||||||
HashHex string
|
Digest string
|
||||||
}
|
}
|
||||||
|
|
||||||
// trustTagRow encodes all human-consumable information for a signed tag, including signers
|
// 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 {
|
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) {
|
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",
|
Short: "Display detailed information about keys and signatures",
|
||||||
Args: cli.ExactArgs(1),
|
Args: cli.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return lookupTrustInfo(dockerCli, args[0])
|
return viewTrustInfo(dockerCli, args[0])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupTrustInfo(cli command.Cli, remote string) error {
|
func viewTrustInfo(cli command.Cli, remote string) error {
|
||||||
ctx := context.Background()
|
signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote)
|
||||||
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 len(signatureRows) > 0 {
|
||||||
if err := printSignatures(cli.Out(), signatureRows); err != nil {
|
if err := printSignatures(cli.Out(), signatureRows); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -92,18 +71,6 @@ func lookupTrustInfo(cli command.Cli, remote string) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(cli.Out(), "\nNo signatures for %s\n\n", remote)
|
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)
|
signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles)
|
||||||
|
|
||||||
// If we do not have additional signers, do not display
|
// 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
|
// This will always have the root and targets information
|
||||||
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0])
|
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0])
|
||||||
printSortedAdminKeys(cli.Out(), adminRolesWithSigs)
|
printSortedAdminKeys(cli.Out(), adminRolesWithSigs)
|
||||||
|
|
||||||
return nil
|
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) {
|
func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) {
|
||||||
sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name })
|
sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name })
|
||||||
for _, adminRole := range adminRoles {
|
for _, adminRole := range adminRoles {
|
||||||
@ -201,8 +212,8 @@ func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
|
|||||||
formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName))
|
formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName))
|
||||||
}
|
}
|
||||||
formattedTags = append(formattedTags, formatter.SignedTagInfo{
|
formattedTags = append(formattedTags, formatter.SignedTagInfo{
|
||||||
Name: sigRow.TagName,
|
Name: sigRow.SignedTag,
|
||||||
Digest: sigRow.HashHex,
|
Digest: sigRow.Digest,
|
||||||
Signers: formattedSigners,
|
Signers: formattedSigners,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ type fakeClient struct {
|
|||||||
dockerClient.Client
|
dockerClient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrustInspectCommandErrors(t *testing.T) {
|
func TestTrustViewCommandErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
args []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 := test.NewFakeCli(&fakeClient{})
|
||||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||||
cmd := newViewCommand(cli)
|
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")
|
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 := test.NewFakeCli(&fakeClient{})
|
||||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||||
cmd := newViewCommand(cli)
|
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")
|
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 := test.NewFakeCli(&fakeClient{})
|
||||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||||
cmd := newViewCommand(cli)
|
cmd := newViewCommand(cli)
|
||||||
@ -107,44 +107,44 @@ func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) {
|
|||||||
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
|
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 := test.NewFakeCli(&fakeClient{})
|
||||||
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
|
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
|
||||||
cmd := newViewCommand(cli)
|
cmd := newViewCommand(cli)
|
||||||
cmd.SetArgs([]string{"signed-repo"})
|
cmd.SetArgs([]string{"signed-repo"})
|
||||||
assert.NoError(t, cmd.Execute())
|
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 := test.NewFakeCli(&fakeClient{})
|
||||||
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
|
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
|
||||||
cmd := newViewCommand(cli)
|
cmd := newViewCommand(cli)
|
||||||
cmd.SetArgs([]string{"signed-repo:green"})
|
cmd.SetArgs([]string{"signed-repo:green"})
|
||||||
assert.NoError(t, cmd.Execute())
|
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 := test.NewFakeCli(&fakeClient{})
|
||||||
cli.SetNotaryClient(getLoadedNotaryRepository)
|
cli.SetNotaryClient(getLoadedNotaryRepository)
|
||||||
cmd := newViewCommand(cli)
|
cmd := newViewCommand(cli)
|
||||||
cmd.SetArgs([]string{"signed-repo"})
|
cmd.SetArgs([]string{"signed-repo"})
|
||||||
assert.NoError(t, cmd.Execute())
|
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 := test.NewFakeCli(&fakeClient{})
|
||||||
cli.SetNotaryClient(getLoadedNotaryRepository)
|
cli.SetNotaryClient(getLoadedNotaryRepository)
|
||||||
cmd := newViewCommand(cli)
|
cmd := newViewCommand(cli)
|
||||||
cmd.SetArgs([]string{"signed-repo:unsigned"})
|
cmd.SetArgs([]string{"signed-repo:unsigned"})
|
||||||
assert.NoError(t, cmd.Execute())
|
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) {
|
func TestNotaryRoleToSigner(t *testing.T) {
|
||||||
@ -224,8 +224,8 @@ func TestMatchOneReleasedSingleSignature(t *testing.T) {
|
|||||||
outputRow := matchedSigRows[0]
|
outputRow := matchedSigRows[0]
|
||||||
// Empty signers because "targets/releases" doesn't show up
|
// Empty signers because "targets/releases" doesn't show up
|
||||||
assert.Empty(t, outputRow.Signers)
|
assert.Empty(t, outputRow.Signers)
|
||||||
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
|
assert.Equal(t, releasedTgt.Name, outputRow.SignedTag)
|
||||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
|
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchOneReleasedMultiSignature(t *testing.T) {
|
func TestMatchOneReleasedMultiSignature(t *testing.T) {
|
||||||
@ -249,8 +249,8 @@ func TestMatchOneReleasedMultiSignature(t *testing.T) {
|
|||||||
outputRow := matchedSigRows[0]
|
outputRow := matchedSigRows[0]
|
||||||
// We should have three signers
|
// We should have three signers
|
||||||
assert.Equal(t, outputRow.Signers, []string{"a", "b", "c"})
|
assert.Equal(t, outputRow.Signers, []string{"a", "b", "c"})
|
||||||
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
|
assert.Equal(t, releasedTgt.Name, outputRow.SignedTag)
|
||||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
|
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchMultiReleasedMultiSignature(t *testing.T) {
|
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:
|
// note that the output is sorted by tag name, so we can reliably index to validate data:
|
||||||
outputTargetA := matchedSigRows[0]
|
outputTargetA := matchedSigRows[0]
|
||||||
assert.Equal(t, outputTargetA.Signers, []string{"a"})
|
assert.Equal(t, outputTargetA.Signers, []string{"a"})
|
||||||
assert.Equal(t, targetA.Name, outputTargetA.TagName)
|
assert.Equal(t, targetA.Name, outputTargetA.SignedTag)
|
||||||
assert.Equal(t, hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.HashHex)
|
assert.Equal(t, hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.Digest)
|
||||||
|
|
||||||
outputTargetB := matchedSigRows[1]
|
outputTargetB := matchedSigRows[1]
|
||||||
assert.Equal(t, outputTargetB.Signers, []string{"a", "b"})
|
assert.Equal(t, outputTargetB.Signers, []string{"a", "b"})
|
||||||
assert.Equal(t, targetB.Name, outputTargetB.TagName)
|
assert.Equal(t, targetB.Name, outputTargetB.SignedTag)
|
||||||
assert.Equal(t, hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.HashHex)
|
assert.Equal(t, hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.Digest)
|
||||||
|
|
||||||
outputTargetC := matchedSigRows[2]
|
outputTargetC := matchedSigRows[2]
|
||||||
assert.Equal(t, outputTargetC.Signers, []string{"a", "b", "c"})
|
assert.Equal(t, outputTargetC.Signers, []string{"a", "b", "c"})
|
||||||
assert.Equal(t, targetC.Name, outputTargetC.TagName)
|
assert.Equal(t, targetC.Name, outputTargetC.SignedTag)
|
||||||
assert.Equal(t, hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.HashHex)
|
assert.Equal(t, hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.Digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchReleasedSignatureFromTargets(t *testing.T) {
|
func TestMatchReleasedSignatureFromTargets(t *testing.T) {
|
||||||
@ -313,8 +313,8 @@ func TestMatchReleasedSignatureFromTargets(t *testing.T) {
|
|||||||
outputRow := matchedSigRows[0]
|
outputRow := matchedSigRows[0]
|
||||||
// Empty signers because "targets" doesn't show up
|
// Empty signers because "targets" doesn't show up
|
||||||
assert.Empty(t, outputRow.Signers)
|
assert.Empty(t, outputRow.Signers)
|
||||||
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
|
assert.Equal(t, releasedTgt.Name, outputRow.SignedTag)
|
||||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
|
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetSignerRolesWithKeyIDs(t *testing.T) {
|
func TestGetSignerRolesWithKeyIDs(t *testing.T) {
|
||||||
|
253
docs/reference/commandline/trust_inspect.md
Normal file
253
docs/reference/commandline/trust_inspect.md
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
---
|
||||||
|
title: "trust inspect"
|
||||||
|
description: "The inspect command description and usage"
|
||||||
|
keywords: "view, notary, trust"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- This file is maintained within the docker/cli GitHub
|
||||||
|
repository at https://github.com/docker/cli/. Make all
|
||||||
|
pull requests against that repo. If you see this file in
|
||||||
|
another repository, consider it read-only there, as it will
|
||||||
|
periodically be overwritten by the definitive file. Pull
|
||||||
|
requests which include edits to this file in other repositories
|
||||||
|
will be rejected.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
Loading…
x
Reference in New Issue
Block a user