...

Source file src/github.com/sigstore/cosign/v2/pkg/oci/remote/remote.go

Documentation: github.com/sigstore/cosign/v2/pkg/oci/remote

     1  //
     2  // Copyright 2021 The Sigstore Authors.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //     http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package remote
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  
    24  	"github.com/google/go-containerregistry/pkg/name"
    25  	v1 "github.com/google/go-containerregistry/pkg/v1"
    26  	"github.com/google/go-containerregistry/pkg/v1/remote"
    27  	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
    28  	"github.com/google/go-containerregistry/pkg/v1/types"
    29  	payloadsize "github.com/sigstore/cosign/v2/internal/pkg/cosign/payload/size"
    30  	ociexperimental "github.com/sigstore/cosign/v2/internal/pkg/oci/remote"
    31  	"github.com/sigstore/cosign/v2/pkg/oci"
    32  )
    33  
    34  // These enable mocking for unit testing without faking an entire registry.
    35  var (
    36  	remoteImage = remote.Image
    37  	remoteIndex = remote.Index
    38  	remoteGet   = remote.Get
    39  	remoteWrite = remote.Write
    40  )
    41  
    42  // EntityNotFoundError is the error that SignedEntity returns when the
    43  // provided ref does not exist.
    44  type EntityNotFoundError struct {
    45  	baseErr error
    46  }
    47  
    48  func (e *EntityNotFoundError) Error() string {
    49  	return fmt.Sprintf("entity not found in registry, error: %v", e.baseErr)
    50  }
    51  
    52  func NewEntityNotFoundError(err error) error {
    53  	return &EntityNotFoundError{
    54  		baseErr: err,
    55  	}
    56  }
    57  
    58  // SignedEntity provides access to a remote reference, and its signatures.
    59  // The SignedEntity will be one of SignedImage or SignedImageIndex.
    60  func SignedEntity(ref name.Reference, options ...Option) (oci.SignedEntity, error) {
    61  	o := makeOptions(ref.Context(), options...)
    62  
    63  	got, err := remoteGet(ref, o.ROpt...)
    64  	var te *transport.Error
    65  	if errors.As(err, &te) && te.StatusCode == http.StatusNotFound {
    66  		return nil, NewEntityNotFoundError(err)
    67  	} else if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	switch got.MediaType {
    72  	case types.OCIImageIndex, types.DockerManifestList:
    73  		ii, err := got.ImageIndex()
    74  		if err != nil {
    75  			return nil, err
    76  		}
    77  		return &index{
    78  			v1Index: ii,
    79  			ref:     ref.Context().Digest(got.Digest.String()),
    80  			opt:     o,
    81  		}, nil
    82  
    83  	case types.OCIManifestSchema1, types.DockerManifestSchema2:
    84  		i, err := got.Image()
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  		return &image{
    89  			Image: i,
    90  			opt:   o,
    91  		}, nil
    92  
    93  	default:
    94  		return nil, fmt.Errorf("unknown mime type: %v", got.MediaType)
    95  	}
    96  }
    97  
    98  // normalize turns image digests into tags with optional prefix & suffix:
    99  // sha256:d34db33f -> [prefix]sha256-d34db33f[.suffix]
   100  func normalize(h v1.Hash, prefix string, suffix string) string {
   101  	return normalizeWithSeparator(h, prefix, suffix, "-")
   102  }
   103  
   104  // normalizeWithSeparator turns image digests into tags with optional prefix & suffix:
   105  // sha256:d34db33f -> [prefix]sha256[algorithmSeparator]d34db33f[.suffix]
   106  func normalizeWithSeparator(h v1.Hash, prefix string, suffix string, algorithmSeparator string) string {
   107  	if suffix == "" {
   108  		return fmt.Sprint(prefix, h.Algorithm, algorithmSeparator, h.Hex)
   109  	}
   110  	return fmt.Sprint(prefix, h.Algorithm, algorithmSeparator, h.Hex, ".", suffix)
   111  }
   112  
   113  // SignatureTag returns the name.Tag that associated signatures with a particular digest.
   114  func SignatureTag(ref name.Reference, opts ...Option) (name.Tag, error) {
   115  	o := makeOptions(ref.Context(), opts...)
   116  	return suffixTag(ref, o.SignatureSuffix, "-", o)
   117  }
   118  
   119  // AttestationTag returns the name.Tag that associated attestations with a particular digest.
   120  func AttestationTag(ref name.Reference, opts ...Option) (name.Tag, error) {
   121  	o := makeOptions(ref.Context(), opts...)
   122  	return suffixTag(ref, o.AttestationSuffix, "-", o)
   123  }
   124  
   125  // SBOMTag returns the name.Tag that associated SBOMs with a particular digest.
   126  func SBOMTag(ref name.Reference, opts ...Option) (name.Tag, error) {
   127  	o := makeOptions(ref.Context(), opts...)
   128  	return suffixTag(ref, o.SBOMSuffix, "-", o)
   129  }
   130  
   131  // DigestTag returns the name.Tag that associated SBOMs with a particular digest.
   132  func DigestTag(ref name.Reference, opts ...Option) (name.Tag, error) {
   133  	o := makeOptions(ref.Context(), opts...)
   134  	return suffixTag(ref, "", ":", o)
   135  }
   136  
   137  // DockerContentDigest fetches the Docker-Content-Digest header for the referenced tag,
   138  // which is required to delete the object in registry API v2.3 and greater.
   139  // See https://github.com/distribution/distribution/blob/main/docs/content/spec/api.md#deleting-an-image
   140  // and https://github.com/distribution/distribution/issues/1579
   141  func DockerContentDigest(ref name.Tag, opts ...Option) (name.Tag, error) {
   142  	o := makeOptions(ref.Context(), opts...)
   143  	desc, err := remoteGet(ref, o.ROpt...)
   144  	if err != nil {
   145  		return name.Tag{}, err
   146  	}
   147  	h := desc.Digest
   148  	return o.TargetRepository.Tag(normalizeWithSeparator(h, o.TagPrefix, "", ":")), nil
   149  }
   150  
   151  func suffixTag(ref name.Reference, suffix string, algorithmSeparator string, o *options) (name.Tag, error) {
   152  	var h v1.Hash
   153  	if digest, ok := ref.(name.Digest); ok {
   154  		var err error
   155  		h, err = v1.NewHash(digest.DigestStr())
   156  		if err != nil { // This is effectively impossible.
   157  			return name.Tag{}, err
   158  		}
   159  	} else {
   160  		desc, err := remoteGet(ref, o.ROpt...)
   161  		if err != nil {
   162  			return name.Tag{}, err
   163  		}
   164  		h = desc.Digest
   165  	}
   166  	return o.TargetRepository.Tag(normalizeWithSeparator(h, o.TagPrefix, suffix, algorithmSeparator)), nil
   167  }
   168  
   169  // signatures is a shared implementation of the oci.Signed* Signatures method.
   170  func signatures(digestable oci.SignedEntity, o *options) (oci.Signatures, error) {
   171  	h, err := digestable.Digest()
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  	return Signatures(o.TargetRepository.Tag(normalize(h, o.TagPrefix, o.SignatureSuffix)), o.OriginalOptions...)
   176  }
   177  
   178  // attestations is a shared implementation of the oci.Signed* Attestations method.
   179  func attestations(digestable oci.SignedEntity, o *options) (oci.Signatures, error) {
   180  	h, err := digestable.Digest()
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	return Signatures(o.TargetRepository.Tag(normalize(h, o.TagPrefix, o.AttestationSuffix)), o.OriginalOptions...)
   185  }
   186  
   187  // attachment is a shared implementation of the oci.Signed* Attachment method.
   188  func attachment(digestable oci.SignedEntity, attName string, o *options) (oci.File, error) {
   189  	// Try using OCI 1.1 behavior
   190  	if file, err := attachmentExperimentalOCI(digestable, attName, o); err == nil {
   191  		return file, nil
   192  	}
   193  
   194  	h, err := digestable.Digest()
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	img, err := SignedImage(o.TargetRepository.Tag(normalize(h, o.TagPrefix, attName)), o.OriginalOptions...)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	ls, err := img.Layers()
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  	if len(ls) != 1 {
   207  		return nil, fmt.Errorf("expected exactly one layer in attachment, got %d", len(ls))
   208  	}
   209  
   210  	return &attached{
   211  		SignedImage: img,
   212  		layer:       ls[0],
   213  	}, nil
   214  }
   215  
   216  type attached struct {
   217  	oci.SignedImage
   218  	layer v1.Layer
   219  }
   220  
   221  var _ oci.File = (*attached)(nil)
   222  
   223  // FileMediaType implements oci.File
   224  func (f *attached) FileMediaType() (types.MediaType, error) {
   225  	return f.layer.MediaType()
   226  }
   227  
   228  // Payload implements oci.File
   229  func (f *attached) Payload() ([]byte, error) {
   230  	size, err := f.layer.Size()
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	err = payloadsize.CheckSize(uint64(size))
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	// remote layers are believed to be stored
   240  	// compressed, but we don't compress attachments
   241  	// so use "Compressed" to access the raw byte
   242  	// stream.
   243  	rc, err := f.layer.Compressed()
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	defer rc.Close()
   248  	return io.ReadAll(rc)
   249  }
   250  
   251  // attachmentExperimentalOCI is a shared implementation of the oci.Signed* Attachment method (for OCI 1.1+ behavior).
   252  func attachmentExperimentalOCI(digestable oci.SignedEntity, attName string, o *options) (oci.File, error) {
   253  	h, err := digestable.Digest()
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	d := o.TargetRepository.Digest(h.String())
   258  
   259  	artifactType := ociexperimental.ArtifactType(attName)
   260  	index, err := Referrers(d, artifactType, o.OriginalOptions...)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  	results := index.Manifests
   265  
   266  	numResults := len(results)
   267  	if numResults == 0 {
   268  		return nil, fmt.Errorf("unable to locate reference with artifactType %s", artifactType)
   269  	} else if numResults > 1 {
   270  		// TODO: if there is more than 1 result.. what does that even mean?
   271  		// TODO: use ui.Warn
   272  		fmt.Printf("WARNING: there were a total of %d references with artifactType %s\n", numResults, artifactType)
   273  	}
   274  	// TODO: do this smarter using "created" annotations
   275  	lastResult := results[numResults-1]
   276  
   277  	img, err := SignedImage(o.TargetRepository.Digest(lastResult.Digest.String()), o.OriginalOptions...)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	ls, err := img.Layers()
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	if len(ls) != 1 {
   286  		return nil, fmt.Errorf("expected exactly one layer in attachment, got %d", len(ls))
   287  	}
   288  	return &attached{
   289  		SignedImage: img,
   290  		layer:       ls[0],
   291  	}, nil
   292  }
   293  

View as plain text