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 }