mirror of https://github.com/docker/cli.git
Use default inspect formatting, remove omitempty, update docs
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
parent
1eb87cc096
commit
a9428285f0
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1 +1,25 @@
|
|||
[{"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}]
|
||||
[
|
||||
{
|
||||
"Name": "reg/img:unsigned-tag",
|
||||
"SignedTags": [],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1 +1,33 @@
|
|||
[{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}]
|
||||
[
|
||||
{
|
||||
"Name": "signed-repo",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "green",
|
||||
"Digest": "677265656e2d646967657374",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"]}]}]
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"]}]}]
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1 +1,33 @@
|
|||
[{"SignedTags":[{"SignedTag":"green","Digest":"677265656e2d646967657374","Signers":["Repo Admin"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}]
|
||||
[
|
||||
{
|
||||
"Name": "signed-repo:green",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "green",
|
||||
"Digest": "677265656e2d646967657374",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -1 +1,42 @@
|
|||
[{"Signers":[{"Name":"bob","Keys":["B"]},{"Name":"alice","Keys":["A"]}],"AdminstrativeKeys":[{"Name":"Root","Keys":["rootID"]},{"Name":"Repository","Keys":["targetsID"]}]}]
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue