Merge pull request #694 from riyazdf/trust-inspect

docker trust inspect
This commit is contained in:
Sebastiaan van Stijn 2017-11-29 11:33:53 -08:00 committed by GitHub
commit d921d5cc39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1164 additions and 196 deletions

View File

@ -192,7 +192,24 @@ func (e EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([
} }
func (e EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { 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) { func (e EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) {

View File

@ -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
} }

167
cli/command/trust/common.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,83 @@
package trust
import (
"encoding/json"
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/inspect"
"github.com/spf13/cobra"
"github.com/theupdateframework/notary/tuf/data"
)
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]",
Short: "Return low-level information about keys and signatures",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runInspect(dockerCli, args)
},
}
return cmd
}
func runInspect(dockerCli command.Cli, remotes []string) error {
getRefFunc := func(ref string) (interface{}, []byte, error) {
i, err := getRepoTrustInfo(dockerCli, ref)
return nil, i, err
}
return inspect.Inspect(dockerCli.Out(), remotes, "", getRefFunc)
}
func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) {
signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote)
if err != nil {
return []byte{}, 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 {
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:
rootKeys := []trustKey{}
for _, keyID := range adminRole.KeyIDs {
rootKeys = append(rootKeys, trustKey{ID: keyID})
}
adminList = append(adminList, trustSigner{"Root", rootKeys})
case data.CanonicalTargetsRole:
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 json.Marshal(trustRepo{
Name: remote,
SignedTags: signatureRows,
Signers: signerList,
AdminstrativeKeys: adminList,
})
}

View File

@ -0,0 +1,135 @@
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")
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
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")
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
}
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 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)
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")
}

View File

@ -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 {

View File

@ -0,0 +1,25 @@
[
{
"Name": "reg/img:unsigned-tag",
"SignedTags": [],
"Signers": [],
"AdminstrativeKeys": [
{
"Name": "Root",
"Keys": [
{
"ID": "rootID"
}
]
},
{
"Name": "Repository",
"Keys": [
{
"ID": "targetsID"
}
]
}
]
}
]

View File

@ -1,6 +1,33 @@
SIGNED TAG DIGEST SIGNERS [
green 677265656e2d646967657374 (Repo Admin) {
"Name": "signed-repo",
Administrative keys for signed-repo: "SignedTags": [
Repository Key: targetsID {
Root Key: rootID "SignedTag": "green",
"Digest": "677265656e2d646967657374",
"Signers": [
"Repo Admin"
]
}
],
"Signers": [],
"AdminstrativeKeys": [
{
"Name": "Root",
"Keys": [
{
"ID": "rootID"
}
]
},
{
"Name": "Repository",
"Keys": [
{
"ID": "targetsID"
}
]
}
]
}
]

View File

@ -1,14 +1,65 @@
SIGNED TAG DIGEST SIGNERS [
blue 626c75652d646967657374 alice {
green 677265656e2d646967657374 (Repo Admin) "Name": "signed-repo",
red 7265642d646967657374 alice, bob "SignedTags": [
{
List of signers and their keys for signed-repo: "SignedTag": "blue",
"Digest": "626c75652d646967657374",
SIGNER KEYS "Signers": [
alice A "alice"
bob B ]
},
Administrative keys for signed-repo: {
Repository Key: targetsID "SignedTag": "green",
Root Key: rootID "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"
}
]
}
]
}
]

View File

@ -0,0 +1,128 @@
[
{
"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"
}
]
}
]
}
]

View File

@ -1,6 +1,33 @@
SIGNED TAG DIGEST SIGNERS [
green 677265656e2d646967657374 (Repo Admin) {
"Name": "signed-repo:green",
Administrative keys for signed-repo: "SignedTags": [
Repository Key: targetsID {
Root Key: rootID "SignedTag": "green",
"Digest": "677265656e2d646967657374",
"Signers": [
"Repo Admin"
]
}
],
"Signers": [],
"AdminstrativeKeys": [
{
"Name": "Root",
"Keys": [
{
"ID": "rootID"
}
]
},
{
"Name": "Repository",
"Keys": [
{
"ID": "targetsID"
}
]
}
]
}
]

View File

@ -0,0 +1 @@
[]

View File

@ -1,13 +1,42 @@
[
No signatures for signed-repo:unsigned {
"Name": "signed-repo:unsigned",
"SignedTags": [],
List of signers and their keys for signed-repo: "Signers": [
{
SIGNER KEYS "Name": "bob",
alice A "Keys": [
bob B {
"ID": "B"
Administrative keys for signed-repo: }
Repository Key: targetsID ]
Root Key: rootID },
{
"Name": "alice",
"Keys": [
{
"ID": "A"
}
]
}
],
"AdminstrativeKeys": [
{
"Name": "Root",
"Keys": [
{
"ID": "rootID"
}
]
},
{
"Name": "Repository",
"Keys": [
{
"ID": "targetsID"
}
]
}
]
}
]

View File

@ -0,0 +1,6 @@
SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID

View 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

View File

@ -0,0 +1,6 @@
SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID

View 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

View File

@ -1,8 +1,6 @@
package trust package trust
import ( import (
"context"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"sort" "sort"
@ -11,80 +9,28 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "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/spf13/cobra"
"github.com/theupdateframework/notary"
"github.com/theupdateframework/notary/client" "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 {
TagName string
HashHex 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].TagName < tagComparator[j].TagName
}
func (tagComparator trustTagRowList) Swap(i, j int) {
tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i]
}
func newViewCommand(dockerCli command.Cli) *cobra.Command { func newViewCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "view IMAGE[:TAG]", Use: "view IMAGE[:TAG]",
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 +38,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,7 +51,6 @@ 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
} }
@ -128,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 // pretty print with ordered rows
func printSignatures(out io.Writer, signatureRows trustTagRowList) error { func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
trustTagCtx := formatter.Context{ trustTagCtx := formatter.Context{
@ -201,8 +75,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,
}) })
} }

View File

@ -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) {

View File

@ -0,0 +1,364 @@
---
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] [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` 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.
## Examples
### 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
[
{
"Name": "alpine:latest",
"SignedTags": [
{
"SignedTag": "latest",
"Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478",
"Signers": [
"Repo Admin"
]
}
],
"Signers": [],
"AdminstrativeKeys": [
{
"Name": "Repository",
"Keys": [
{
"ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd"
}
]
},
{
"Name": "Root",
"Keys": [
{
"ID": "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
[
{
"Name": "my-image:purple",
"SignedTags": [
{
"SignedTag": "purple",
"Digest": "941d3dba358621ce3c41ef67b47cf80f701ff80cdf46b5cc86587eaebfe45557",
"Signers": [
"alice",
"bob",
"carol"
]
}
],
"Signers": [
{
"Name": "alice",
"Keys": [
{
"ID": "04dd031411ed671ae1e12f47ddc8646d98f135090b01e54c3561e843084484a3"
},
{
"ID": "6a11e4898a4014d400332ab0e096308c844584ff70943cdd1d6628d577f45fd8"
}
]
},
{
"Name": "bob",
"Keys": [
{
"ID": "433e245c656ae9733cdcc504bfa560f90950104442c4528c9616daa45824ccba"
}
]
},
{
"Name": "carol",
"Keys": [
{
"ID": "d32fa8b5ca08273a2880f455fcb318da3dc80aeae1a30610815140deef8f30d9"
},
{
"ID": "9a8bbec6ba2af88a5fad6047d428d17e6d05dbdd03d15b4fc8a9a0e8049cd606"
}
]
}
],
"AdminstrativeKeys": [
{
"Name": "Repository",
"Keys": [
{
"ID": "27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44"
}
]
},
{
"Name": "Root",
"Keys": [
{
"ID": "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:
```bash
$ docker trust inspect alpine:unsigned
[
{
"Name": "alpine:unsigned",
"Signers": [],
"AdminstrativeKeys": [
{
"Name": "Repository",
"Keys": [
{
"ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd"
}
]
},
{
"Name": "Root",
"Keys": [
{
"ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce"
}
]
}
]
}
]
```
### Get details about signatures for all image tags in a repository
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 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"
}
]
}
]
}
]
```