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