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