github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/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/streams"
    13  	"github.com/docker/cli/cli/trust"
    14  	"github.com/docker/distribution/reference"
    15  	"github.com/docker/docker/api/types"
    16  	registrytypes "github.com/docker/docker/api/types/registry"
    17  	"github.com/docker/docker/pkg/jsonmessage"
    18  	"github.com/docker/docker/registry"
    19  	"github.com/opencontainers/go-digest"
    20  	"github.com/pkg/errors"
    21  	"github.com/sirupsen/logrus"
    22  	"github.com/theupdateframework/notary/client"
    23  	"github.com/theupdateframework/notary/tuf/data"
    24  )
    25  
    26  type target struct {
    27  	name   string
    28  	digest digest.Digest
    29  	size   int64
    30  }
    31  
    32  // TrustedPush handles content trust pushing of an image
    33  func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, options types.ImagePushOptions) error {
    34  	responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
    35  	if err != nil {
    36  		return err
    37  	}
    38  
    39  	defer responseBody.Close()
    40  
    41  	return PushTrustedReference(cli, repoInfo, ref, authConfig, responseBody)
    42  }
    43  
    44  // PushTrustedReference pushes a canonical reference to the trust server.
    45  //
    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  // trustedPull handles content trust pulling of an image
   171  func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error {
   172  	refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	ref := imgRefAndAuth.Reference()
   178  	for i, r := range refs {
   179  		displayTag := r.name
   180  		if displayTag != "" {
   181  			displayTag = ":" + displayTag
   182  		}
   183  		fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), reference.FamiliarName(ref), displayTag, r.digest)
   184  
   185  		trustedRef, err := reference.WithDigest(reference.TrimNamed(ref), r.digest)
   186  		if err != nil {
   187  			return err
   188  		}
   189  		updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, AuthResolver(cli), trustedRef.String())
   190  		if err != nil {
   191  			return err
   192  		}
   193  		if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, PullOptions{
   194  			all:      false,
   195  			platform: opts.platform,
   196  			quiet:    opts.quiet,
   197  			remote:   opts.remote,
   198  		}); err != nil {
   199  			return err
   200  		}
   201  
   202  		tagged, err := reference.WithTag(reference.TrimNamed(ref), r.name)
   203  		if err != nil {
   204  			return err
   205  		}
   206  
   207  		if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil {
   208  			return err
   209  		}
   210  	}
   211  	return nil
   212  }
   213  
   214  func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) ([]target, error) {
   215  	notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
   216  	if err != nil {
   217  		return nil, errors.Wrap(err, "error establishing connection to trust repository")
   218  	}
   219  
   220  	ref := imgRefAndAuth.Reference()
   221  	tagged, isTagged := ref.(reference.NamedTagged)
   222  	if !isTagged {
   223  		// List all targets
   224  		targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
   225  		if err != nil {
   226  			return nil, trust.NotaryError(ref.Name(), err)
   227  		}
   228  		var refs []target
   229  		for _, tgt := range targets {
   230  			t, err := convertTarget(tgt.Target)
   231  			if err != nil {
   232  				fmt.Fprintf(cli.Err(), "Skipping target for %q\n", reference.FamiliarName(ref))
   233  				continue
   234  			}
   235  			// Only list tags in the top level targets role or the releases delegation role - ignore
   236  			// all other delegation roles
   237  			if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole {
   238  				continue
   239  			}
   240  			refs = append(refs, t)
   241  		}
   242  		if len(refs) == 0 {
   243  			return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name()))
   244  		}
   245  		return refs, nil
   246  	}
   247  
   248  	t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
   249  	if err != nil {
   250  		return nil, trust.NotaryError(ref.Name(), err)
   251  	}
   252  	// Only get the tag if it's in the top level targets role or the releases delegation role
   253  	// ignore it if it's in any other delegation roles
   254  	if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
   255  		return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag()))
   256  	}
   257  
   258  	logrus.Debugf("retrieving target for %s role", t.Role)
   259  	r, err := convertTarget(t.Target)
   260  	return []target{r}, err
   261  }
   262  
   263  // imagePullPrivileged pulls the image and displays it to the output
   264  func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error {
   265  	ref := reference.FamiliarString(imgRefAndAuth.Reference())
   266  
   267  	encodedAuth, err := command.EncodeAuthToBase64(*imgRefAndAuth.AuthConfig())
   268  	if err != nil {
   269  		return err
   270  	}
   271  	requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull")
   272  	options := types.ImagePullOptions{
   273  		RegistryAuth:  encodedAuth,
   274  		PrivilegeFunc: requestPrivilege,
   275  		All:           opts.all,
   276  		Platform:      opts.platform,
   277  	}
   278  	responseBody, err := cli.Client().ImagePull(ctx, ref, options)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	defer responseBody.Close()
   283  
   284  	out := cli.Out()
   285  	if opts.quiet {
   286  		out = streams.NewOut(io.Discard)
   287  	}
   288  	return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
   289  }
   290  
   291  // TrustedReference returns the canonical trusted reference for an image reference
   292  func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) {
   293  	imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, rs, AuthResolver(cli), ref.String())
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	notaryRepo, err := cli.NotaryClient(imgRefAndAuth, []string{"pull"})
   299  	if err != nil {
   300  		return nil, errors.Wrap(err, "error establishing connection to trust repository")
   301  	}
   302  
   303  	t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
   304  	if err != nil {
   305  		return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err)
   306  	}
   307  	// Only list tags in the top level targets role or the releases delegation role - ignore
   308  	// all other delegation roles
   309  	if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
   310  		return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), client.ErrNoSuchTarget(ref.Tag()))
   311  	}
   312  	r, err := convertTarget(t.Target)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	return reference.WithDigest(reference.TrimNamed(ref), r.digest)
   317  }
   318  
   319  func convertTarget(t client.Target) (target, error) {
   320  	h, ok := t.Hashes["sha256"]
   321  	if !ok {
   322  		return target{}, errors.New("no valid hash, expecting sha256")
   323  	}
   324  	return target{
   325  		name:   t.Name,
   326  		digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
   327  		size:   t.Length,
   328  	}, nil
   329  }
   330  
   331  // TagTrusted tags a trusted ref
   332  func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error {
   333  	// Use familiar references when interacting with client and output
   334  	familiarRef := reference.FamiliarString(ref)
   335  	trustedFamiliarRef := reference.FamiliarString(trustedRef)
   336  
   337  	fmt.Fprintf(cli.Err(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef)
   338  
   339  	return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef)
   340  }
   341  
   342  // AuthResolver returns an auth resolver function from a command.Cli
   343  func AuthResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
   344  	return func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
   345  		return command.ResolveAuthConfig(ctx, cli, index)
   346  	}
   347  }