github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/oci/oci.go (about) 1 // Copyright 2023-2024 The Inspektor Gadget authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package oci 16 17 import ( 18 "archive/tar" 19 "bytes" 20 "context" 21 "crypto" 22 "crypto/x509" 23 "encoding/base64" 24 "encoding/json" 25 "encoding/pem" 26 "errors" 27 "fmt" 28 "io" 29 "os" 30 "path/filepath" 31 "runtime" 32 "strings" 33 "sync" 34 35 "github.com/distribution/reference" 36 "github.com/docker/cli/cli/config" 37 "github.com/docker/cli/cli/config/configfile" 38 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 39 "github.com/sigstore/sigstore/pkg/signature" 40 "github.com/sigstore/sigstore/pkg/signature/payload" 41 log "github.com/sirupsen/logrus" 42 "oras.land/oras-go/v2" 43 "oras.land/oras-go/v2/content" 44 "oras.land/oras-go/v2/content/oci" 45 "oras.land/oras-go/v2/errdef" 46 "oras.land/oras-go/v2/registry/remote" 47 oras_auth "oras.land/oras-go/v2/registry/remote/auth" 48 ) 49 50 type AuthOptions struct { 51 AuthFile string 52 SecretBytes []byte 53 Insecure bool 54 } 55 56 type VerifyOptions struct { 57 VerifyPublicKey bool 58 PublicKey string 59 } 60 61 type ImageOptions struct { 62 AuthOptions 63 VerifyOptions 64 } 65 66 const ( 67 defaultOciStore = "/var/lib/ig/oci-store" 68 DefaultAuthFile = "/var/lib/ig/config.json" 69 70 PullImageAlways = "always" 71 PullImageMissing = "missing" 72 PullImageNever = "never" 73 ) 74 75 const ( 76 defaultDomain = "ghcr.io" 77 officialRepoPrefix = "inspektor-gadget/gadget/" 78 // localhost is treated as a special value for domain-name. Any other 79 // domain-name without a "." or a ":port" are considered a path component. 80 localhost = "localhost" 81 ) 82 83 // getLocalOciStore returns a single local oci store. oci.Store is concurrently safe only 84 // against its own instance inside the same go program 85 var getLocalOciStore = sync.OnceValues(func() (*oci.Store, error) { 86 if err := os.MkdirAll(filepath.Dir(defaultOciStore), 0o700); err != nil { 87 return nil, err 88 } 89 return oci.New(defaultOciStore) 90 }) 91 92 // GadgetImageDesc is the description of a gadget image. 93 type GadgetImageDesc struct { 94 Repository string `column:"repository"` 95 Tag string `column:"tag"` 96 Digest string `column:"digest,width:12,fixed"` 97 Created string `column:"created"` 98 } 99 100 func (d *GadgetImageDesc) String() string { 101 if d.Tag == "" && d.Repository == "" { 102 return fmt.Sprintf("@%s", d.Digest) 103 } 104 return fmt.Sprintf("%s:%s@%s", d.Repository, d.Tag, d.Digest) 105 } 106 107 func getTimeFromAnnotations(annotations map[string]string) string { 108 created, _ := annotations[ocispec.AnnotationCreated] 109 return created 110 } 111 112 // PullGadgetImage pulls the gadget image into the local oci store and returns its descriptor. 113 func PullGadgetImage(ctx context.Context, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) { 114 ociStore, err := getLocalOciStore() 115 if err != nil { 116 return nil, fmt.Errorf("getting oci store: %w", err) 117 } 118 119 return pullGadgetImageToStore(ctx, ociStore, image, authOpts) 120 } 121 122 // pullGadgetImageToStore pulls the gadget image into the given store and returns its descriptor. 123 func pullGadgetImageToStore(ctx context.Context, imageStore oras.Target, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) { 124 targetImage, err := normalizeImageName(image) 125 if err != nil { 126 return nil, fmt.Errorf("normalizing image: %w", err) 127 } 128 repo, err := newRepository(targetImage, authOpts) 129 if err != nil { 130 return nil, fmt.Errorf("creating remote repository: %w", err) 131 } 132 desc, err := oras.Copy(ctx, repo, targetImage.String(), imageStore, 133 targetImage.String(), oras.DefaultCopyOptions) 134 if err != nil { 135 return nil, fmt.Errorf("copying to remote repository: %w", err) 136 } 137 138 imageDesc := &GadgetImageDesc{ 139 Repository: targetImage.Name(), 140 Digest: desc.Digest.String(), 141 Created: "", // Unfortunately, oras.Copy does not return annotations 142 } 143 144 if ref, ok := targetImage.(reference.Tagged); ok { 145 imageDesc.Tag = ref.Tag() 146 } 147 return imageDesc, nil 148 } 149 150 func pullIfNotExist(ctx context.Context, imageStore oras.Target, authOpts *AuthOptions, image string) error { 151 targetImage, err := normalizeImageName(image) 152 if err != nil { 153 return fmt.Errorf("normalizing image: %w", err) 154 } 155 156 _, err = imageStore.Resolve(ctx, targetImage.String()) 157 if err == nil { 158 return nil 159 } 160 if !errors.Is(err, errdef.ErrNotFound) { 161 return fmt.Errorf("resolving image %q: %w", image, err) 162 } 163 164 repo, err := newRepository(targetImage, authOpts) 165 if err != nil { 166 return fmt.Errorf("creating remote repository: %w", err) 167 } 168 _, err = oras.Copy(ctx, repo, targetImage.String(), imageStore, targetImage.String(), oras.DefaultCopyOptions) 169 if err != nil { 170 return fmt.Errorf("downloading to local repository: %w", err) 171 } 172 return nil 173 } 174 175 // PushGadgetImage pushes the gadget image and returns its descriptor. 176 func PushGadgetImage(ctx context.Context, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) { 177 ociStore, err := getLocalOciStore() 178 if err != nil { 179 return nil, fmt.Errorf("getting oci store: %w", err) 180 } 181 182 targetImage, err := normalizeImageName(image) 183 if err != nil { 184 return nil, fmt.Errorf("normalizing image: %w", err) 185 } 186 repo, err := newRepository(targetImage, authOpts) 187 if err != nil { 188 return nil, fmt.Errorf("creating remote repository: %w", err) 189 } 190 desc, err := oras.Copy(context.TODO(), ociStore, targetImage.String(), repo, 191 targetImage.String(), oras.DefaultCopyOptions) 192 if err != nil { 193 return nil, fmt.Errorf("copying to remote repository: %w", err) 194 } 195 196 imageDesc := &GadgetImageDesc{ 197 Repository: targetImage.Name(), 198 Digest: desc.Digest.String(), 199 Created: "", // Unfortunately, oras.Copy does not return annotations 200 } 201 if ref, ok := targetImage.(reference.Tagged); ok { 202 imageDesc.Tag = ref.Tag() 203 } 204 return imageDesc, nil 205 } 206 207 // TagGadgetImage tags the src image with the dst image. 208 func TagGadgetImage(ctx context.Context, srcImage, dstImage string) (*GadgetImageDesc, error) { 209 src, err := normalizeImageName(srcImage) 210 if err != nil { 211 return nil, fmt.Errorf("normalizing src image: %w", err) 212 } 213 dst, err := normalizeImageName(dstImage) 214 if err != nil { 215 return nil, fmt.Errorf("normalizing dst image: %w", err) 216 } 217 218 ociStore, err := getLocalOciStore() 219 if err != nil { 220 return nil, fmt.Errorf("getting oci store: %w", err) 221 } 222 223 targetDescriptor, err := ociStore.Resolve(context.TODO(), src.String()) 224 if err != nil { 225 // Error message not that helpful 226 return nil, fmt.Errorf("resolving src: %w", err) 227 } 228 ociStore.Tag(context.TODO(), targetDescriptor, dst.String()) 229 230 imageDesc := &GadgetImageDesc{ 231 Repository: dst.Name(), 232 Digest: targetDescriptor.Digest.String(), 233 Created: getTimeFromAnnotations(targetDescriptor.Annotations), 234 } 235 if ref, ok := dst.(reference.Tagged); ok { 236 imageDesc.Tag = ref.Tag() 237 } 238 return imageDesc, nil 239 } 240 241 func ExportGadgetImages(ctx context.Context, dstFile string, images ...string) error { 242 ociStore, err := getLocalOciStore() 243 if err != nil { 244 return fmt.Errorf("getting oci store: %w", err) 245 } 246 247 tmpDir, err := os.MkdirTemp("", "gadget-export-") 248 if err != nil { 249 return fmt.Errorf("creating temp dir: %w", err) 250 } 251 defer os.RemoveAll(tmpDir) 252 253 dstStore, err := oci.New(tmpDir) 254 if err != nil { 255 return fmt.Errorf("creating oci storage: %w", err) 256 } 257 258 for _, image := range images { 259 targetImage, err := normalizeImageName(image) 260 if err != nil { 261 return fmt.Errorf("normalizing image: %w", err) 262 } 263 _, err = oras.Copy(ctx, ociStore, targetImage.String(), dstStore, 264 targetImage.String(), oras.DefaultCopyOptions) 265 if err != nil { 266 return fmt.Errorf("copying to remote repository: %w", err) 267 } 268 } 269 270 if err := tarFolderToFile(tmpDir, dstFile); err != nil { 271 return fmt.Errorf("creating tar for gadget image: %w", err) 272 } 273 274 return nil 275 } 276 277 // ImportGadgetImages imports all the tagged gadget images from the src file. 278 func ImportGadgetImages(ctx context.Context, srcFile string) ([]string, error) { 279 src, err := oci.NewFromTar(ctx, srcFile) 280 if err != nil { 281 return nil, fmt.Errorf("loading src bundle: %w", err) 282 } 283 284 ociStore, err := getLocalOciStore() 285 if err != nil { 286 return nil, fmt.Errorf("getting oci store: %w", err) 287 } 288 289 ret := []string{} 290 291 err = src.Tags(ctx, "", func(tags []string) error { 292 for _, tag := range tags { 293 _, err := oras.Copy(ctx, src, tag, ociStore, tag, oras.DefaultCopyOptions) 294 if err != nil { 295 return fmt.Errorf("copying to local repository: %w", err) 296 } 297 298 ret = append(ret, tag) 299 } 300 return nil 301 }) 302 303 return ret, err 304 } 305 306 // based on https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07 307 func tarFolderToFile(src, filePath string) error { 308 file, err := os.Create(filePath) 309 if err != nil { 310 return fmt.Errorf("opening file: %w", err) 311 } 312 313 tw := tar.NewWriter(file) 314 defer tw.Close() 315 316 return filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { 317 if err != nil { 318 return err 319 } 320 321 if !fi.Mode().IsRegular() { 322 return nil 323 } 324 325 header, err := tar.FileInfoHeader(fi, fi.Name()) 326 if err != nil { 327 return err 328 } 329 330 // update the name to correctly reflect the desired destination when untaring 331 header.Name = strings.TrimPrefix(strings.Replace(file, src, "", -1), string(filepath.Separator)) 332 333 if err := tw.WriteHeader(header); err != nil { 334 return err 335 } 336 337 f, err := os.Open(file) 338 if err != nil { 339 return err 340 } 341 342 if _, err := io.Copy(tw, f); err != nil { 343 return err 344 } 345 346 f.Close() 347 348 return nil 349 }) 350 } 351 352 func listGadgetImages(ctx context.Context, store *oci.Store) ([]*GadgetImageDesc, error) { 353 images := []*GadgetImageDesc{} 354 err := store.Tags(ctx, "", func(tags []string) error { 355 for _, fullTag := range tags { 356 parsed, err := reference.Parse(fullTag) 357 if err != nil { 358 log.Debugf("parsing image %q: %s", fullTag, err) 359 continue 360 } 361 362 var repository string 363 if named, ok := parsed.(reference.Named); ok { 364 repository = named.Name() 365 } 366 367 tag := "latest" 368 if tagged, ok := parsed.(reference.Tagged); ok { 369 tag = tagged.Tag() 370 } 371 372 image := &GadgetImageDesc{ 373 Repository: repository, 374 Tag: tag, 375 } 376 377 desc, err := store.Resolve(ctx, fullTag) 378 if err != nil { 379 log.Debugf("Found tag %q but couldn't get a descriptor for it: %v", fullTag, err) 380 continue 381 } 382 image.Digest = desc.Digest.String() 383 384 manifest, err := getManifestForHost(ctx, store, fullTag) 385 if err != nil { 386 log.Debugf("Getting manifest for %q: %v", fullTag, err) 387 continue 388 } 389 390 image.Created = getTimeFromAnnotations(manifest.Annotations) 391 392 images = append(images, image) 393 } 394 return nil 395 }) 396 397 return images, err 398 } 399 400 // ListGadgetImages lists all the gadget images. 401 func ListGadgetImages(ctx context.Context) ([]*GadgetImageDesc, error) { 402 ociStore, err := getLocalOciStore() 403 if err != nil { 404 return nil, fmt.Errorf("getting oci store: %w", err) 405 } 406 407 images, err := listGadgetImages(ctx, ociStore) 408 if err != nil { 409 return nil, fmt.Errorf("listing all tags: %w", err) 410 } 411 412 for _, image := range images { 413 image.Repository = strings.TrimPrefix(image.Repository, defaultDomain+"/"+officialRepoPrefix) 414 } 415 416 return images, nil 417 } 418 419 // DeleteGadgetImage removes the given image. 420 func DeleteGadgetImage(ctx context.Context, image string) error { 421 ociStore, err := getLocalOciStore() 422 if err != nil { 423 return fmt.Errorf("getting oci store: %w", err) 424 } 425 426 targetImage, err := normalizeImageName(image) 427 if err != nil { 428 return fmt.Errorf("normalizing image: %w", err) 429 } 430 431 fullName := targetImage.String() 432 descriptor, err := ociStore.Resolve(ctx, fullName) 433 if err != nil { 434 return fmt.Errorf("resolving image: %w", err) 435 } 436 437 images, err := listGadgetImages(ctx, ociStore) 438 if err != nil { 439 return fmt.Errorf("listing images: %w", err) 440 } 441 442 digest := descriptor.Digest.String() 443 for _, img := range images { 444 imgFullName := fmt.Sprintf("%s:%s", img.Repository, img.Tag) 445 if img.Digest == digest && imgFullName != fullName { 446 // We cannot blindly delete a whole image tree. 447 // Indeed, it is possible for several image names to point to the same 448 // underlying image, like: 449 // REPOSITORY TAG DIGEST 450 // docker.io/library/bar latest f959f580ba01 451 // docker.io/library/foo latest f959f580ba01 452 // Where foo and bar are different names referencing the same image, as 453 // the digest shows. 454 // In this case, we just untag the image name given by the user. 455 return ociStore.Untag(ctx, fullName) 456 } 457 } 458 459 err = ociStore.Delete(ctx, descriptor) 460 if err != nil { 461 return err 462 } 463 464 return ociStore.GC(ctx) 465 } 466 467 // splitIGDomain splits a repository name to domain and remote-name. 468 // If no valid domain is found, the default domain is used. Repository name 469 // needs to be already validated before. 470 // Inspired on https://github.com/distribution/reference/blob/v0.5.0/normalize.go#L126 471 // TODO: Ideally we should use the upstream function but docker.io is harcoded there 472 // https://github.com/distribution/reference/blob/v0.5.0/normalize.go#L31 473 func splitIGDomain(name string) (domain, remainder string) { 474 i := strings.IndexRune(name, '/') 475 if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != localhost && strings.ToLower(name[:i]) == name[:i]) { 476 domain, remainder = defaultDomain, name 477 } else { 478 domain, remainder = name[:i], name[i+1:] 479 } 480 if domain == defaultDomain && !strings.ContainsRune(remainder, '/') { 481 remainder = officialRepoPrefix + remainder 482 } 483 return 484 } 485 486 func normalizeImageName(image string) (reference.Named, error) { 487 // Use the default gadget's registry if no domain is specified. 488 domain, remainer := splitIGDomain(image) 489 490 name, err := reference.ParseNormalizedNamed(domain + "/" + remainer) 491 if err != nil { 492 return nil, fmt.Errorf("parsing normalized image %q: %w", image, err) 493 } 494 return reference.TagNameOnly(name), nil 495 } 496 497 func getHostString(repository string) (string, error) { 498 repo, err := reference.Parse(repository) 499 if err != nil { 500 return "", fmt.Errorf("parsing repository %q: %w", repository, err) 501 } 502 if named, ok := repo.(reference.Named); ok { 503 return reference.Domain(named), nil 504 } 505 return "", fmt.Errorf("image has to be a named reference") 506 } 507 508 func newAuthClient(repository string, authOptions *AuthOptions) (*oras_auth.Client, error) { 509 log.Debugf("Using auth file %q", authOptions.AuthFile) 510 511 var cfg *configfile.ConfigFile 512 var err error 513 514 if authOptions.SecretBytes != nil && len(authOptions.SecretBytes) != 0 { 515 cfg, err = config.LoadFromReader(bytes.NewReader(authOptions.SecretBytes)) 516 if err != nil { 517 return nil, fmt.Errorf("loading auth config: %w", err) 518 } 519 } else if authFileReader, err := os.Open(authOptions.AuthFile); err != nil { 520 // If the AuthFile was not set explicitly, we allow to fall back to the docker auth, 521 // otherwise we fail to avoid masking an error from the user 522 if !errors.Is(err, os.ErrNotExist) || authOptions.AuthFile != DefaultAuthFile { 523 return nil, fmt.Errorf("opening auth file %q: %w", authOptions.AuthFile, err) 524 } 525 526 log.Debugf("Couldn't find default auth file %q...", authOptions.AuthFile) 527 log.Debugf("Using default docker auth file instead") 528 log.Debugf("$HOME: %q", os.Getenv("HOME")) 529 530 cfg, err = config.Load("") 531 if err != nil { 532 return nil, fmt.Errorf("loading auth config: %w", err) 533 } 534 535 } else { 536 defer authFileReader.Close() 537 cfg, err = config.LoadFromReader(authFileReader) 538 if err != nil { 539 return nil, fmt.Errorf("loading auth config: %w", err) 540 } 541 } 542 543 hostString, err := getHostString(repository) 544 if err != nil { 545 return nil, fmt.Errorf("getting host string: %w", err) 546 } 547 authConfig, err := cfg.GetAuthConfig(hostString) 548 if err != nil { 549 return nil, fmt.Errorf("getting auth config: %w", err) 550 } 551 552 return &oras_auth.Client{ 553 Credential: oras_auth.StaticCredential(hostString, oras_auth.Credential{ 554 Username: authConfig.Username, 555 Password: authConfig.Password, 556 AccessToken: authConfig.Auth, 557 RefreshToken: authConfig.IdentityToken, 558 }), 559 }, nil 560 } 561 562 func craftSignatureTag(digest string) (string, error) { 563 // WARNING: cosign is considering changing the scheme for 564 // publishing/retrieving sigstore bundles to/from an OCI registry, see: 565 // https://sigstore.slack.com/archives/C0440BFT43H/p1712253122721879?thread_ts=1712238666.552719&cid=C0440BFT43H 566 // https://github.com/sigstore/cosign/pull/3622 567 parts := strings.Split(digest, ":") 568 if len(parts) != 2 { 569 return "", fmt.Errorf("wrong digest, expected two parts, got %d", len(parts)) 570 } 571 572 return fmt.Sprintf("%s-%s.sig", parts[0], parts[1]), nil 573 } 574 575 func getSignature(ctx context.Context, repo *remote.Repository, signatureTag string) ([]byte, string, error) { 576 _, signatureManifestBytes, err := oras.FetchBytes(ctx, repo, signatureTag, oras.DefaultFetchBytesOptions) 577 if err != nil { 578 return nil, "", fmt.Errorf("getting signature bytes: %w", err) 579 } 580 581 signatureManifest := &ocispec.Manifest{} 582 err = json.Unmarshal(signatureManifestBytes, signatureManifest) 583 if err != nil { 584 return nil, "", fmt.Errorf("decoding signature manifest: %w", err) 585 } 586 587 layers := signatureManifest.Layers 588 expectedLen := 1 589 layersLen := len(layers) 590 if layersLen != expectedLen { 591 return nil, "", fmt.Errorf("wrong number of signature manifest layers: expected %d, got %d", expectedLen, layersLen) 592 } 593 594 layer := layers[0] 595 // Taken from: 596 // https://github.com/sigstore/cosign/blob/e23dcd11f24b729f6ff9300ab7a61b09d71da12a/pkg/types/media.go#L28 597 expectedMediaType := "application/vnd.dev.cosign.simplesigning.v1+json" 598 if layer.MediaType != expectedMediaType { 599 return nil, "", fmt.Errorf("wrong layer media type: expected %s, got %s", expectedMediaType, layer.MediaType) 600 } 601 602 signature, ok := layer.Annotations["dev.cosignproject.cosign/signature"] 603 if !ok { 604 return nil, "", fmt.Errorf("no signature in layer") 605 } 606 607 signatureBytes, err := base64.StdEncoding.DecodeString(signature) 608 if err != nil { 609 return nil, "", fmt.Errorf("decoding signature: %w", err) 610 } 611 612 payloadTag := layer.Digest.String() 613 614 return signatureBytes, payloadTag, nil 615 } 616 617 func getPayload(ctx context.Context, repo *remote.Repository, payloadTag string) ([]byte, error) { 618 // The payload is stored as a blob, so we fetch bytes from the blob store and 619 // not the manifest one. 620 _, payloadBytes, err := oras.FetchBytes(ctx, repo.Blobs(), payloadTag, oras.DefaultFetchBytesOptions) 621 if err != nil { 622 return nil, fmt.Errorf("getting payload bytes: %w", err) 623 } 624 625 return payloadBytes, nil 626 } 627 628 func getImageDigest(ctx context.Context, store *oci.Store, imageRef string) (string, error) { 629 desc, err := store.Resolve(ctx, imageRef) 630 if err != nil { 631 return "", fmt.Errorf("resolving image %q: %w", imageRef, err) 632 } 633 634 return desc.Digest.String(), nil 635 } 636 637 func getSigningInformation(ctx context.Context, repo *remote.Repository, imageDigest string, authOpts *AuthOptions) ([]byte, []byte, error) { 638 signatureTag, err := craftSignatureTag(imageDigest) 639 if err != nil { 640 return nil, nil, fmt.Errorf("crafting signature tag: %w", err) 641 } 642 643 signature, payloadTag, err := getSignature(ctx, repo, signatureTag) 644 if err != nil { 645 return nil, nil, fmt.Errorf("getting signature: %w", err) 646 } 647 648 payload, err := getPayload(ctx, repo, payloadTag) 649 if err != nil { 650 return nil, nil, fmt.Errorf("getting payload: %w", err) 651 } 652 653 return signature, payload, nil 654 } 655 656 func newVerifier(publicKey []byte) (signature.Verifier, error) { 657 block, _ := pem.Decode(publicKey) 658 if block == nil { 659 return nil, fmt.Errorf("decoding public key to PEM blocks") 660 } 661 662 pub, err := x509.ParsePKIXPublicKey(block.Bytes) 663 if err != nil { 664 return nil, fmt.Errorf("parsing public key: %w", err) 665 } 666 667 verifier, err := signature.LoadVerifier(pub, crypto.SHA256) 668 if err != nil { 669 return nil, fmt.Errorf("loading verifier: %w", err) 670 } 671 672 return verifier, nil 673 } 674 675 func checkPayloadImage(payloadBytes []byte, imageDigest string) error { 676 payloadImage := &payload.SimpleContainerImage{} 677 err := json.Unmarshal(payloadBytes, payloadImage) 678 if err != nil { 679 return fmt.Errorf("unmarshalling payload: %w", err) 680 } 681 682 if payloadImage.Critical.Image.DockerManifestDigest != imageDigest { 683 return fmt.Errorf("payload digest does not correspond to image: expected %s, got %s", imageDigest, payloadImage.Critical.Image.DockerManifestDigest) 684 } 685 686 return nil 687 } 688 689 func verifyImage(ctx context.Context, image string, imgOpts *ImageOptions) error { 690 imageStore, err := getLocalOciStore() 691 if err != nil { 692 return fmt.Errorf("getting local oci store: %w", err) 693 } 694 695 imageRef, err := normalizeImageName(image) 696 if err != nil { 697 return fmt.Errorf("normalizing image name: %w", err) 698 } 699 700 imageDigest, err := getImageDigest(ctx, imageStore, imageRef.String()) 701 if err != nil { 702 return fmt.Errorf("getting image digest: %w", err) 703 } 704 705 verifier, err := newVerifier([]byte(imgOpts.PublicKey)) 706 if err != nil { 707 return fmt.Errorf("creating verifier: %w", err) 708 } 709 710 repo, err := newRepository(imageRef, &imgOpts.AuthOptions) 711 if err != nil { 712 return fmt.Errorf("creating repository: %w", err) 713 } 714 715 signatureBytes, payloadBytes, err := getSigningInformation(ctx, repo, imageDigest, &imgOpts.AuthOptions) 716 if err != nil { 717 return fmt.Errorf("getting signing information: %w", err) 718 } 719 720 err = verifier.VerifySignature(bytes.NewReader(signatureBytes), bytes.NewReader(payloadBytes)) 721 if err != nil { 722 return fmt.Errorf("verifying signature: %w", err) 723 } 724 725 // We should not read the payload before confirming it was signed, so let's 726 // do this check once it was confirmed to be signed: 727 // https://github.com/containers/image/blob/main/docs/containers-signature.5.md#the-cryptographic-signature 728 err = checkPayloadImage(payloadBytes, imageDigest) 729 if err != nil { 730 return fmt.Errorf("checking payload image: %w", err) 731 } 732 733 return nil 734 } 735 736 // newRepository creates a client to the remote repository identified by 737 // image using the given auth options. 738 func newRepository(image reference.Named, authOpts *AuthOptions) (*remote.Repository, error) { 739 repo, err := remote.NewRepository(image.Name()) 740 if err != nil { 741 return nil, fmt.Errorf("creating remote repository: %w", err) 742 } 743 repo.PlainHTTP = authOpts.Insecure 744 if !authOpts.Insecure { 745 client, err := newAuthClient(image.Name(), authOpts) 746 if err != nil { 747 return nil, fmt.Errorf("creating auth client: %w", err) 748 } 749 repo.Client = client 750 } 751 752 return repo, nil 753 } 754 755 func getImageListDescriptor(ctx context.Context, target oras.ReadOnlyTarget, reference string) (ocispec.Index, error) { 756 imageListDescriptor, err := target.Resolve(ctx, reference) 757 if err != nil { 758 return ocispec.Index{}, fmt.Errorf("resolving image %q: %w", reference, err) 759 } 760 if imageListDescriptor.MediaType != ocispec.MediaTypeImageIndex { 761 return ocispec.Index{}, fmt.Errorf("image %q is not an image index", reference) 762 } 763 764 reader, err := target.Fetch(ctx, imageListDescriptor) 765 if err != nil { 766 return ocispec.Index{}, fmt.Errorf("fetching image index: %w", err) 767 } 768 defer reader.Close() 769 770 var index ocispec.Index 771 if err = json.NewDecoder(reader).Decode(&index); err != nil { 772 return ocispec.Index{}, fmt.Errorf("unmarshalling image index: %w", err) 773 } 774 return index, nil 775 } 776 777 func getContentBytesFromDescriptor(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]byte, error) { 778 reader, err := fetcher.Fetch(ctx, desc) 779 if err != nil { 780 return nil, fmt.Errorf("fetching descriptor: %w", err) 781 } 782 defer reader.Close() 783 bytes, err := io.ReadAll(reader) 784 if err != nil { 785 return nil, fmt.Errorf("reading descriptor: %w", err) 786 } 787 return bytes, nil 788 } 789 790 func ensureImage(ctx context.Context, imageStore oras.Target, image string, imgOpts *ImageOptions, pullPolicy string) error { 791 switch pullPolicy { 792 case PullImageAlways: 793 _, err := pullGadgetImageToStore(ctx, imageStore, image, &imgOpts.AuthOptions) 794 if err != nil { 795 return fmt.Errorf("pulling image (always) %q: %w", image, err) 796 } 797 case PullImageMissing: 798 if err := pullIfNotExist(ctx, imageStore, &imgOpts.AuthOptions, image); err != nil { 799 return fmt.Errorf("pulling image (if missing) %q: %w", image, err) 800 } 801 case PullImageNever: 802 // Just check if the image exists to report a better error message 803 targetImage, err := normalizeImageName(image) 804 if err != nil { 805 return fmt.Errorf("normalizing image: %w", err) 806 } 807 if _, err := imageStore.Resolve(ctx, targetImage.String()); err != nil { 808 return fmt.Errorf("resolving image %q on local registry: %w", targetImage.String(), err) 809 } 810 } 811 812 if !imgOpts.VerifyPublicKey { 813 log.Warnf("you set --verify-image=false, image will not be verified") 814 815 return nil 816 } 817 818 err := verifyImage(ctx, image, imgOpts) 819 if err != nil { 820 return fmt.Errorf("verifying image %q: %w", image, err) 821 } 822 823 return nil 824 } 825 826 // EnsureImage ensures the image is present in the local store 827 func EnsureImage(ctx context.Context, image string, imgOpts *ImageOptions, pullPolicy string) error { 828 imageStore, err := getLocalOciStore() 829 if err != nil { 830 return fmt.Errorf("getting local oci store: %w", err) 831 } 832 833 return ensureImage(ctx, imageStore, image, imgOpts, pullPolicy) 834 } 835 836 func getManifestForHost(ctx context.Context, target oras.ReadOnlyTarget, image string) (*ocispec.Manifest, error) { 837 index, err := getIndex(ctx, target, image) 838 if err != nil { 839 return nil, fmt.Errorf("getting index: %w", err) 840 } 841 842 var manifestDesc *ocispec.Descriptor 843 for _, indexManifest := range index.Manifests { 844 // TODO: Check docker code 845 if indexManifest.Platform.Architecture == runtime.GOARCH { 846 manifestDesc = &indexManifest 847 break 848 } 849 } 850 if manifestDesc == nil { 851 return nil, fmt.Errorf("no manifest found for architecture %q", runtime.GOARCH) 852 } 853 854 manifestBytes, err := getContentBytesFromDescriptor(ctx, target, *manifestDesc) 855 if err != nil { 856 return nil, fmt.Errorf("getting content from descriptor: %w", err) 857 } 858 859 manifest := &ocispec.Manifest{} 860 err = json.Unmarshal(manifestBytes, manifest) 861 if err != nil { 862 return nil, fmt.Errorf("decoding manifest: %w", err) 863 } 864 return manifest, nil 865 } 866 867 func GetManifestForHost(ctx context.Context, image string) (*ocispec.Manifest, error) { 868 imageStore, err := getLocalOciStore() 869 if err != nil { 870 return nil, fmt.Errorf("getting local oci store: %w", err) 871 } 872 return getManifestForHost(ctx, imageStore, image) 873 } 874 875 // getIndex gets an index for the given image 876 func getIndex(ctx context.Context, target oras.ReadOnlyTarget, image string) (*ocispec.Index, error) { 877 imageRef, err := normalizeImageName(image) 878 if err != nil { 879 return nil, fmt.Errorf("normalizing image: %w", err) 880 } 881 882 index, err := getImageListDescriptor(ctx, target, imageRef.String()) 883 if err != nil { 884 return nil, fmt.Errorf("getting image list descriptor: %w", err) 885 } 886 887 return &index, nil 888 } 889 890 func GetContentFromDescriptor(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) { 891 imageStore, err := getLocalOciStore() 892 if err != nil { 893 return nil, fmt.Errorf("getting local oci store: %w", err) 894 } 895 896 reader, err := imageStore.Fetch(ctx, desc) 897 if err != nil { 898 return nil, fmt.Errorf("fetching descriptor: %w", err) 899 } 900 return reader, nil 901 }