github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cli/command/image/trust.go (about)

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"sort"
    10  
    11  	"github.com/docker/cli/cli/command"
    12  	"github.com/docker/cli/cli/trust"
    13  	"github.com/docker/distribution/reference"
    14  	"github.com/docker/docker/api/types"
    15  	registrytypes "github.com/docker/docker/api/types/registry"
    16  	"github.com/docker/docker/pkg/jsonmessage"
    17  	"github.com/docker/docker/registry"
    18  	digest "github.com/opencontainers/go-digest"
    19  	"github.com/pkg/errors"
    20  	"github.com/sirupsen/logrus"
    21  	"github.com/theupdateframework/notary/client"
    22  	"github.com/theupdateframework/notary/tuf/data"
    23  )
    24  
    25  type target struct {
    26  	name   string
    27  	digest digest.Digest
    28  	size   int64
    29  }
    30  
    31  // TrustedPush handles content trust pushing of an image
    32  func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
    33  	responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege)
    34  	if err != nil {
    35  		return err
    36  	}
    37  
    38  	defer responseBody.Close()
    39  
    40  	return PushTrustedReference(cli, repoInfo, ref, authConfig, responseBody)
    41  }
    42  
    43  // PushTrustedReference pushes a canonical reference to the trust server.
    44  // nolint: gocyclo
    45  func PushTrustedReference(streams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error {
    46  	// If it is a trusted push we would like to find the target entry which match the
    47  	// tag provided in the function and then do an AddTarget later.
    48  	target := &client.Target{}
    49  	// Count the times of calling for handleTarget,
    50  	// if it is called more that once, that should be considered an error in a trusted push.
    51  	cnt := 0
    52  	handleTarget := func(msg jsonmessage.JSONMessage) {
    53  		cnt++
    54  		if cnt > 1 {
    55  			// handleTarget should only be called once. This will be treated as an error.
    56  			return
    57  		}
    58  
    59  		var pushResult types.PushResult
    60  		err := json.Unmarshal(*msg.Aux, &pushResult)
    61  		if err == nil && pushResult.Tag != "" {
    62  			if dgst, err := digest.Parse(pushResult.Digest); err == nil {
    63  				h, err := hex.DecodeString(dgst.Hex())
    64  				if err != nil {
    65  					target = nil
    66  					return
    67  				}
    68  				target.Name = pushResult.Tag
    69  				target.Hashes = data.Hashes{string(dgst.Algorithm()): h}
    70  				target.Length = int64(pushResult.Size)
    71  			}
    72  		}
    73  	}
    74  
    75  	var tag string
    76  	switch x := ref.(type) {
    77  	case reference.Canonical:
    78  		return errors.New("cannot push a digest reference")
    79  	case reference.NamedTagged:
    80  		tag = x.Tag()
    81  	default:
    82  		// We want trust signatures to always take an explicit tag,
    83  		// otherwise it will act as an untrusted push.
    84  		if err := jsonmessage.DisplayJSONMessagesToStream(in, streams.Out(), nil); err != nil {
    85  			return err
    86  		}
    87  		fmt.Fprintln(streams.Err(), "No tag specified, skipping trust metadata push")
    88  		return nil
    89  	}
    90  
    91  	if err := jsonmessage.DisplayJSONMessagesToStream(in, streams.Out(), handleTarget); err != nil {
    92  		return err
    93  	}
    94  
    95  	if cnt > 1 {
    96  		return errors.Errorf("internal error: only one call to handleTarget expected")
    97  	}
    98  
    99  	if target == nil {
   100  		return errors.Errorf("no targets found, please provide a specific tag in order to sign it")
   101  	}
   102  
   103  	fmt.Fprintln(streams.Out(), "Signing and pushing trust metadata")
   104  
   105  	repo, err := trust.GetNotaryRepository(streams.In(), streams.Out(), command.UserAgent(), repoInfo, &authConfig, "push", "pull")
   106  	if err != nil {
   107  		return errors.Wrap(err, "error establishing connection to trust repository")
   108  	}
   109  
   110  	// get the latest repository metadata so we can figure out which roles to sign
   111  	_, err = repo.ListTargets()
   112  
   113  	switch err.(type) {
   114  	case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
   115  		keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole)
   116  		var rootKeyID string
   117  		// always select the first root key
   118  		if len(keys) > 0 {
   119  			sort.Strings(keys)
   120  			rootKeyID = keys[0]
   121  		} else {
   122  			rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey)
   123  			if err != nil {
   124  				return err
   125  			}
   126  			rootKeyID = rootPublicKey.ID()
   127  		}
   128  
   129  		// Initialize the notary repository with a remotely managed snapshot key
   130  		if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
   131  			return trust.NotaryError(repoInfo.Name.Name(), err)
   132  		}
   133  		fmt.Fprintf(streams.Out(), "Finished initializing %q\n", repoInfo.Name.Name())
   134  		err = repo.AddTarget(target, data.CanonicalTargetsRole)
   135  	case nil:
   136  		// already initialized and we have successfully downloaded the latest metadata
   137  		err = AddTargetToAllSignableRoles(repo, target)
   138  	default:
   139  		return trust.NotaryError(repoInfo.Name.Name(), err)
   140  	}
   141  
   142  	if err == nil {
   143  		err = repo.Publish()
   144  	}
   145  
   146  	if err != nil {
   147  		err = errors.Wrapf(err, "failed to sign %s:%s", repoInfo.Name.Name(), tag)
   148  		return trust.NotaryError(repoInfo.Name.Name(), err)
   149  	}
   150  
   151  	fmt.Fprintf(streams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag)
   152  	return nil
   153  }
   154  
   155  // AddTargetToAllSignableRoles attempts to add the image target to all the top level delegation roles we can
   156  // (based on whether we have the signing key and whether the role's path allows
   157  // us to).
   158  // If there are no delegation roles, we add to the targets role.
   159  func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target) error {
   160  	signableRoles, err := trust.GetSignableRoles(repo, target)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	return repo.AddTarget(target, signableRoles...)
   166  }
   167  
   168  // imagePushPrivileged push the image
   169  func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref reference.Reference, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) {
   170  	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	options := types.ImagePushOptions{
   175  		RegistryAuth:  encodedAuth,
   176  		PrivilegeFunc: requestPrivilege,
   177  	}
   178  
   179  	return cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
   180  }
   181  
   182  // trustedPull handles content trust pulling of an image
   183  func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, platform string) error {
   184  	refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	ref := imgRefAndAuth.Reference()
   190  	for i, r := range refs {
   191  		displayTag := r.name
   192  		if displayTag != "" {
   193  			displayTag = ":" + displayTag
   194  		}
   195  		fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), reference.FamiliarName(ref), displayTag, r.digest)
   196  
   197  		trustedRef, err := reference.WithDigest(reference.TrimNamed(ref), r.digest)
   198  		if err != nil {
   199  			return err
   200  		}
   201  		updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, AuthResolver(cli), trustedRef.String())
   202  		if err != nil {
   203  			return err
   204  		}
   205  		if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false, platform); err != nil {
   206  			return err
   207  		}
   208  
   209  		tagged, err := reference.WithTag(reference.TrimNamed(ref), r.name)
   210  		if err != nil {
   211  			return err
   212  		}
   213  
   214  		if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil {
   215  			return err
   216  		}
   217  	}
   218  	return nil
   219  }
   220  
   221  func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) ([]target, error) {
   222  	notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
   223  	if err != nil {
   224  		return nil, errors.Wrap(err, "error establishing connection to trust repository")
   225  	}
   226  
   227  	ref := imgRefAndAuth.Reference()
   228  	tagged, isTagged := ref.(reference.NamedTagged)
   229  	if !isTagged {
   230  		// List all targets
   231  		targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
   232  		if err != nil {
   233  			return nil, trust.NotaryError(ref.Name(), err)
   234  		}
   235  		var refs []target
   236  		for _, tgt := range targets {
   237  			t, err := convertTarget(tgt.Target)
   238  			if err != nil {
   239  				fmt.Fprintf(cli.Err(), "Skipping target for %q\n", reference.FamiliarName(ref))
   240  				continue
   241  			}
   242  			// Only list tags in the top level targets role or the releases delegation role - ignore
   243  			// all other delegation roles
   244  			if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole {
   245  				continue
   246  			}
   247  			refs = append(refs, t)
   248  		}
   249  		if len(refs) == 0 {
   250  			return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name()))
   251  		}
   252  		return refs, nil
   253  	}
   254  
   255  	t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
   256  	if err != nil {
   257  		return nil, trust.NotaryError(ref.Name(), err)
   258  	}
   259  	// Only get the tag if it's in the top level targets role or the releases delegation role
   260  	// ignore it if it's in any other delegation roles
   261  	if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
   262  		return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag()))
   263  	}
   264  
   265  	logrus.Debugf("retrieving target for %s role", t.Role)
   266  	r, err := convertTarget(t.Target)
   267  	return []target{r}, err
   268  }
   269  
   270  // imagePullPrivileged pulls the image and displays it to the output
   271  func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool, platform string) error {
   272  	ref := reference.FamiliarString(imgRefAndAuth.Reference())
   273  
   274  	encodedAuth, err := command.EncodeAuthToBase64(*imgRefAndAuth.AuthConfig())
   275  	if err != nil {
   276  		return err
   277  	}
   278  	requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull")
   279  	options := types.ImagePullOptions{
   280  		RegistryAuth:  encodedAuth,
   281  		PrivilegeFunc: requestPrivilege,
   282  		All:           all,
   283  		Platform:      platform,
   284  	}
   285  	responseBody, err := cli.Client().ImagePull(ctx, ref, options)
   286  	if err != nil {
   287  		return err
   288  	}
   289  	defer responseBody.Close()
   290  
   291  	return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil)
   292  }
   293  
   294  // TrustedReference returns the canonical trusted reference for an image reference
   295  func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) {
   296  	imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, rs, AuthResolver(cli), ref.String())
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  
   301  	notaryRepo, err := cli.NotaryClient(imgRefAndAuth, []string{"pull"})
   302  	if err != nil {
   303  		return nil, errors.Wrap(err, "error establishing connection to trust repository")
   304  	}
   305  
   306  	t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
   307  	if err != nil {
   308  		return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err)
   309  	}
   310  	// Only list tags in the top level targets role or the releases delegation role - ignore
   311  	// all other delegation roles
   312  	if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
   313  		return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), client.ErrNoSuchTarget(ref.Tag()))
   314  	}
   315  	r, err := convertTarget(t.Target)
   316  	if err != nil {
   317  		return nil, err
   318  
   319  	}
   320  	return reference.WithDigest(reference.TrimNamed(ref), r.digest)
   321  }
   322  
   323  func convertTarget(t client.Target) (target, error) {
   324  	h, ok := t.Hashes["sha256"]
   325  	if !ok {
   326  		return target{}, errors.New("no valid hash, expecting sha256")
   327  	}
   328  	return target{
   329  		name:   t.Name,
   330  		digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
   331  		size:   t.Length,
   332  	}, nil
   333  }
   334  
   335  // TagTrusted tags a trusted ref
   336  // nolint: interfacer
   337  func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error {
   338  	// Use familiar references when interacting with client and output
   339  	familiarRef := reference.FamiliarString(ref)
   340  	trustedFamiliarRef := reference.FamiliarString(trustedRef)
   341  
   342  	fmt.Fprintf(cli.Err(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef)
   343  
   344  	return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef)
   345  }
   346  
   347  // AuthResolver returns an auth resolver function from a command.Cli
   348  func AuthResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
   349  	return func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
   350  		return command.ResolveAuthConfig(ctx, cli, index)
   351  	}
   352  }