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 }