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  }