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 }