2017-08-24 18:43:55 -04:00
|
|
|
package trust
|
|
|
|
|
|
|
|
import (
|
2017-08-25 17:49:40 -04:00
|
|
|
"context"
|
2017-08-24 18:43:55 -04:00
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
2017-08-25 17:49:40 -04:00
|
|
|
"io"
|
2017-08-24 18:43:55 -04:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/docker/cli/cli"
|
|
|
|
"github.com/docker/cli/cli/command"
|
|
|
|
"github.com/docker/cli/cli/command/formatter"
|
2017-09-26 12:53:21 -04:00
|
|
|
"github.com/docker/cli/cli/command/image"
|
2017-08-24 18:43:55 -04:00
|
|
|
"github.com/docker/cli/cli/trust"
|
2017-09-11 17:07:00 -04:00
|
|
|
"github.com/sirupsen/logrus"
|
2017-08-24 18:43:55 -04:00
|
|
|
"github.com/spf13/cobra"
|
2017-10-30 12:21:41 -04:00
|
|
|
"github.com/theupdateframework/notary"
|
|
|
|
"github.com/theupdateframework/notary/client"
|
|
|
|
"github.com/theupdateframework/notary/tuf/data"
|
2017-08-24 18:43:55 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
}
|
|
|
|
|
2017-09-19 17:59:48 -04:00
|
|
|
func newViewCommand(dockerCli command.Cli) *cobra.Command {
|
2017-08-24 18:43:55 -04:00
|
|
|
cmd := &cobra.Command{
|
2017-09-26 19:15:45 -04:00
|
|
|
Use: "view IMAGE[:TAG]",
|
2017-08-24 18:43:55 -04:00
|
|
|
Short: "Display detailed information about keys and signatures",
|
|
|
|
Args: cli.ExactArgs(1),
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
return lookupTrustInfo(dockerCli, args[0])
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
|
|
|
|
func lookupTrustInfo(cli command.Cli, remote string) error {
|
2017-08-25 17:49:40 -04:00
|
|
|
ctx := context.Background()
|
2017-09-26 12:53:21 -04:00
|
|
|
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote)
|
2017-08-24 18:43:55 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-08-25 17:49:40 -04:00
|
|
|
tag := imgRefAndAuth.Tag()
|
2017-09-26 12:53:21 -04:00
|
|
|
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
|
2017-08-24 18:43:55 -04:00
|
|
|
if err != nil {
|
2017-08-25 17:49:40 -04:00
|
|
|
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
|
2017-08-24 18:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if err = clearChangeList(notaryRepo); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer clearChangeList(notaryRepo)
|
2017-08-25 17:49:40 -04:00
|
|
|
|
2017-08-24 18:43:55 -04:00
|
|
|
// Retrieve all released signatures, match them, and pretty print them
|
|
|
|
allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag)
|
|
|
|
if err != nil {
|
2017-08-25 17:49:40 -04:00
|
|
|
logrus.Debug(trust.NotaryError(imgRefAndAuth.Reference().Name(), err))
|
2017-08-24 18:43:55 -04:00
|
|
|
// 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 {
|
2017-08-25 17:49:40 -04:00
|
|
|
if err := printSignatures(cli.Out(), signatureRows); err != nil {
|
2017-08-24 18:43:55 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fmt.Fprintf(cli.Out(), "\nNo signatures for %s\n\n", remote)
|
|
|
|
}
|
|
|
|
|
|
|
|
// get the administrative roles
|
2017-08-25 17:49:40 -04:00
|
|
|
adminRolesWithSigs, err := notaryRepo.ListRoles()
|
2017-08-24 18:43:55 -04:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("No signers for %s", remote)
|
|
|
|
}
|
|
|
|
|
|
|
|
// get delegation roles with the canonical key IDs
|
|
|
|
delegationRoles, err := notaryRepo.GetDelegationRoles()
|
|
|
|
if err != nil {
|
|
|
|
logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err)
|
|
|
|
}
|
|
|
|
signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles)
|
|
|
|
|
|
|
|
// If we do not have additional signers, do not display
|
|
|
|
if len(signerRoleToKeyIDs) > 0 {
|
2017-09-19 17:59:48 -04:00
|
|
|
fmt.Fprintf(cli.Out(), "\nList of signers and their keys for %s:\n\n", strings.Split(remote, ":")[0])
|
2017-09-26 12:33:35 -04:00
|
|
|
if err := printSignerInfo(cli.Out(), signerRoleToKeyIDs); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-08-24 18:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// This will always have the root and targets information
|
|
|
|
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0])
|
2017-08-25 17:49:40 -04:00
|
|
|
printSortedAdminKeys(cli.Out(), adminRolesWithSigs)
|
2017-08-24 18:43:55 -04:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-08-25 17:49:40 -04:00
|
|
|
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 {
|
|
|
|
fmt.Fprintf(out, "%s", formatAdminRole(adminRole))
|
2017-08-24 18:43:55 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-25 17:49:40 -04:00
|
|
|
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 ""
|
2017-08-24 18:43:55 -04:00
|
|
|
}
|
2017-08-25 17:49:40 -04:00
|
|
|
return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", "))
|
2017-08-24 18:43:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2017-08-25 17:49:40 -04:00
|
|
|
func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
|
2017-08-24 18:43:55 -04:00
|
|
|
trustTagCtx := formatter.Context{
|
2017-08-25 17:49:40 -04:00
|
|
|
Output: out,
|
2017-08-24 18:43:55 -04:00
|
|
|
Format: formatter.NewTrustTagFormat(),
|
|
|
|
}
|
|
|
|
// convert the formatted type before printing
|
|
|
|
formattedTags := []formatter.SignedTagInfo{}
|
|
|
|
for _, sigRow := range signatureRows {
|
|
|
|
formattedSigners := sigRow.Signers
|
|
|
|
if len(formattedSigners) == 0 {
|
|
|
|
formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName))
|
|
|
|
}
|
|
|
|
formattedTags = append(formattedTags, formatter.SignedTagInfo{
|
|
|
|
Name: sigRow.TagName,
|
|
|
|
Digest: sigRow.HashHex,
|
|
|
|
Signers: formattedSigners,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return formatter.TrustTagWrite(trustTagCtx, formattedTags)
|
|
|
|
}
|
|
|
|
|
2017-08-25 17:49:40 -04:00
|
|
|
func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error {
|
2017-08-24 18:43:55 -04:00
|
|
|
signerInfoCtx := formatter.Context{
|
2017-08-25 17:49:40 -04:00
|
|
|
Output: out,
|
2017-08-24 18:43:55 -04:00
|
|
|
Format: formatter.NewSignerInfoFormat(),
|
|
|
|
Trunc: true,
|
|
|
|
}
|
|
|
|
formattedSignerInfo := formatter.SignerInfoList{}
|
|
|
|
for name, keyIDs := range roleToKeyIDs {
|
|
|
|
formattedSignerInfo = append(formattedSignerInfo, formatter.SignerInfo{
|
|
|
|
Name: name,
|
|
|
|
Keys: keyIDs,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
sort.Sort(formattedSignerInfo)
|
|
|
|
return formatter.SignerInfoWrite(signerInfoCtx, formattedSignerInfo)
|
|
|
|
}
|