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