github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/trust/sign.go (about)

     1  package trust
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"path"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/docker/cli/cli"
    12  	"github.com/docker/cli/cli/command"
    13  	"github.com/docker/cli/cli/command/image"
    14  	"github.com/docker/cli/cli/trust"
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/pkg/errors"
    17  	"github.com/spf13/cobra"
    18  	"github.com/theupdateframework/notary/client"
    19  	"github.com/theupdateframework/notary/tuf/data"
    20  )
    21  
    22  type signOptions struct {
    23  	local     bool
    24  	imageName string
    25  }
    26  
    27  func newSignCommand(dockerCli command.Cli) *cobra.Command {
    28  	options := signOptions{}
    29  	cmd := &cobra.Command{
    30  		Use:   "sign IMAGE:TAG",
    31  		Short: "Sign an image",
    32  		Args:  cli.ExactArgs(1),
    33  		RunE: func(cmd *cobra.Command, args []string) error {
    34  			options.imageName = args[0]
    35  			return runSignImage(dockerCli, options)
    36  		},
    37  	}
    38  	flags := cmd.Flags()
    39  	flags.BoolVar(&options.local, "local", false, "Sign a locally tagged image")
    40  	return cmd
    41  }
    42  
    43  func runSignImage(cli command.Cli, options signOptions) error {
    44  	imageName := options.imageName
    45  	ctx := context.Background()
    46  	imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, image.AuthResolver(cli), imageName)
    47  	if err != nil {
    48  		return err
    49  	}
    50  	if err := validateTag(imgRefAndAuth); err != nil {
    51  		return err
    52  	}
    53  
    54  	notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
    55  	if err != nil {
    56  		return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
    57  	}
    58  	if err = clearChangeList(notaryRepo); err != nil {
    59  		return err
    60  	}
    61  	defer clearChangeList(notaryRepo)
    62  
    63  	// get the latest repository metadata so we can figure out which roles to sign
    64  	if _, err = notaryRepo.ListTargets(); err != nil {
    65  		switch err.(type) {
    66  		case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
    67  			// before initializing a new repo, check that the image exists locally:
    68  			if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
    69  				return err
    70  			}
    71  
    72  			userRole := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), imgRefAndAuth.AuthConfig().Username))
    73  			if err := initNotaryRepoWithSigners(notaryRepo, userRole); err != nil {
    74  				return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
    75  			}
    76  
    77  			fmt.Fprintf(cli.Out(), "Created signer: %s\n", imgRefAndAuth.AuthConfig().Username)
    78  			fmt.Fprintf(cli.Out(), "Finished initializing signed repository for %s\n", imageName)
    79  		default:
    80  			return trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err)
    81  		}
    82  	}
    83  	requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "push")
    84  	target, err := createTarget(notaryRepo, imgRefAndAuth.Tag())
    85  	if err != nil || options.local {
    86  		switch err := err.(type) {
    87  		// If the error is nil then the local flag is set
    88  		case client.ErrNoSuchTarget, client.ErrRepositoryNotExist, nil:
    89  			// Fail fast if the image doesn't exist locally
    90  			if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
    91  				return err
    92  			}
    93  			fmt.Fprintf(cli.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName)
    94  
    95  			authConfig := command.ResolveAuthConfig(ctx, cli, imgRefAndAuth.RepoInfo().Index)
    96  			encodedAuth, err := command.EncodeAuthToBase64(authConfig)
    97  			if err != nil {
    98  				return err
    99  			}
   100  			options := types.ImagePushOptions{
   101  				RegistryAuth:  encodedAuth,
   102  				PrivilegeFunc: requestPrivilege,
   103  			}
   104  			return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), options)
   105  		default:
   106  			return err
   107  		}
   108  	}
   109  	return signAndPublishToTarget(cli.Out(), imgRefAndAuth, notaryRepo, target)
   110  }
   111  
   112  func signAndPublishToTarget(out io.Writer, imgRefAndAuth trust.ImageRefAndAuth, notaryRepo client.Repository, target client.Target) error {
   113  	tag := imgRefAndAuth.Tag()
   114  	fmt.Fprintf(out, "Signing and pushing trust metadata for %s\n", imgRefAndAuth.Name())
   115  	existingSigInfo, err := getExistingSignatureInfoForReleasedTag(notaryRepo, tag)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	err = image.AddTargetToAllSignableRoles(notaryRepo, &target)
   120  	if err == nil {
   121  		prettyPrintExistingSignatureInfo(out, existingSigInfo)
   122  		err = notaryRepo.Publish()
   123  	}
   124  	if err != nil {
   125  		return errors.Wrapf(err, "failed to sign %s:%s", imgRefAndAuth.RepoInfo().Name.Name(), tag)
   126  	}
   127  	fmt.Fprintf(out, "Successfully signed %s:%s\n", imgRefAndAuth.RepoInfo().Name.Name(), tag)
   128  	return nil
   129  }
   130  
   131  func validateTag(imgRefAndAuth trust.ImageRefAndAuth) error {
   132  	tag := imgRefAndAuth.Tag()
   133  	if tag == "" {
   134  		if imgRefAndAuth.Digest() != "" {
   135  			return errors.New("cannot use a digest reference for IMAGE:TAG")
   136  		}
   137  		return fmt.Errorf("no tag specified for %s", imgRefAndAuth.Name())
   138  	}
   139  	return nil
   140  }
   141  
   142  func checkLocalImageExistence(ctx context.Context, cli command.Cli, imageName string) error {
   143  	_, _, err := cli.Client().ImageInspectWithRaw(ctx, imageName)
   144  	return err
   145  }
   146  
   147  func createTarget(notaryRepo client.Repository, tag string) (client.Target, error) {
   148  	target := &client.Target{}
   149  	var err error
   150  	if tag == "" {
   151  		return *target, errors.New("no tag specified")
   152  	}
   153  	target.Name = tag
   154  	target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, tag)
   155  	return *target, err
   156  }
   157  
   158  func getSignedManifestHashAndSize(notaryRepo client.Repository, tag string) (data.Hashes, int64, error) {
   159  	targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
   160  	if err != nil {
   161  		return nil, 0, err
   162  	}
   163  	return getReleasedTargetHashAndSize(targets, tag)
   164  }
   165  
   166  func getReleasedTargetHashAndSize(targets []client.TargetSignedStruct, tag string) (data.Hashes, int64, error) {
   167  	for _, tgt := range targets {
   168  		if isReleasedTarget(tgt.Role.Name) {
   169  			return tgt.Target.Hashes, tgt.Target.Length, nil
   170  		}
   171  	}
   172  	return nil, 0, client.ErrNoSuchTarget(tag)
   173  }
   174  
   175  func getExistingSignatureInfoForReleasedTag(notaryRepo client.Repository, tag string) (trustTagRow, error) {
   176  	targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
   177  	if err != nil {
   178  		return trustTagRow{}, err
   179  	}
   180  	releasedTargetInfoList := matchReleasedSignatures(targets)
   181  	if len(releasedTargetInfoList) == 0 {
   182  		return trustTagRow{}, nil
   183  	}
   184  	return releasedTargetInfoList[0], nil
   185  }
   186  
   187  func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) {
   188  	sort.Strings(existingSigInfo.Signers)
   189  	joinedSigners := strings.Join(existingSigInfo.Signers, ", ")
   190  	fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.SignedTag, existingSigInfo.Digest, joinedSigners)
   191  }
   192  
   193  func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.RoleName) error {
   194  	rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	rootKeyID := rootKey.ID()
   199  
   200  	// Initialize the notary repository with a remotely managed snapshot key
   201  	if err := notaryRepo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
   202  		return err
   203  	}
   204  
   205  	signerKey, err := getOrGenerateNotaryKey(notaryRepo, newSigner)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	if err := addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey}); err != nil {
   210  		return errors.Wrapf(err, "could not add signer to repo: %s", strings.TrimPrefix(newSigner.String(), "targets/"))
   211  	}
   212  
   213  	return notaryRepo.Publish()
   214  }
   215  
   216  // generates an ECDSA key without a GUN for the specified role
   217  func getOrGenerateNotaryKey(notaryRepo client.Repository, role data.RoleName) (data.PublicKey, error) {
   218  	// use the signer name in the PEM headers if this is a delegation key
   219  	if data.IsDelegation(role) {
   220  		role = data.RoleName(notaryRoleToSigner(role))
   221  	}
   222  	keys := notaryRepo.GetCryptoService().ListKeys(role)
   223  	var err error
   224  	var key data.PublicKey
   225  	// always select the first key by ID
   226  	if len(keys) > 0 {
   227  		sort.Strings(keys)
   228  		keyID := keys[0]
   229  		privKey, _, err := notaryRepo.GetCryptoService().GetPrivateKey(keyID)
   230  		if err != nil {
   231  			return nil, err
   232  		}
   233  		key = data.PublicKeyFromPrivate(privKey)
   234  	} else {
   235  		key, err = notaryRepo.GetCryptoService().Create(role, "", data.ECDSAKey)
   236  		if err != nil {
   237  			return nil, err
   238  		}
   239  	}
   240  	return key, nil
   241  }
   242  
   243  // stages changes to add a signer with the specified name and key(s).  Adds to targets/<name> and targets/releases
   244  func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, signerKeys []data.PublicKey) error {
   245  	// create targets/<username>
   246  	if err := notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys); err != nil {
   247  		return err
   248  	}
   249  	if err := notaryRepo.AddDelegationPaths(newSigner, []string{""}); err != nil {
   250  		return err
   251  	}
   252  
   253  	// create targets/releases
   254  	if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil {
   255  		return err
   256  	}
   257  	return notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
   258  }