Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
268 changes: 267 additions & 1 deletion go.work.sum

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions image/docker/docker_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,28 @@ func (c *dockerClient) fetchManifest(ctx context.Context, ref dockerReference, t
return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil
}

// getManifestSize returns the size of a manifest using a HEAD request.
// This is used when we need the size for OCI descriptor fields but don't need the manifest content.
func (c *dockerClient) getManifestSize(ctx context.Context, ref dockerReference, manifestDigest digest.Digest) (int64, error) {
path := fmt.Sprintf(manifestPath, reference.Path(ref.ref), manifestDigest.String())
headers := map[string][]string{
"Accept": manifest.DefaultRequestedManifestMIMETypes,
}
res, err := c.makeRequest(ctx, http.MethodHead, path, headers, nil, v2Auth, nil)
if err != nil {
return 0, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return 0, fmt.Errorf("getting manifest size %s in %s: %w", manifestDigest.String(), ref.ref.Name(), registryHTTPResponseToError(res))
}
contentLength := res.ContentLength
if contentLength < 0 {
return 0, fmt.Errorf("manifest HEAD response missing Content-Length for %s", manifestDigest.String())
}
return contentLength, nil
}

// getExternalBlob returns the reader of the first available blob URL from urls, which must not be empty.
// This function can return nil reader when no url is supported by this function. In this case, the caller
// should fallback to fetch the non-external blob (i.e. pull from the registry).
Expand Down
198 changes: 198 additions & 0 deletions image/docker/docker_client_referrers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package docker

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"

digest "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/internal/iolimits"
"go.podman.io/image/v5/manifest"
)

const (
// OCI 1.1 referrers API path for fetching artifacts that reference a manifest
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
referrersPath = "/v2/%s/referrers/%s"
)

// getReferrers fetches the referrers index for a manifest using the OCI 1.1 referrers API.
// If artifactType is non-empty, it filters the results to only include referrers of that type.
// Returns (nil, nil) if the registry does not support the referrers API or no referrers exist.
func (c *dockerClient) getReferrers(ctx context.Context, ref dockerReference, manifestDigest digest.Digest, artifactType string) (*manifest.OCI1Index, error) {
if err := manifestDigest.Validate(); err != nil {
return nil, err
}

path := fmt.Sprintf(referrersPath, reference.Path(ref.ref), manifestDigest.String())
if artifactType != "" {
path += "?artifactType=" + url.QueryEscape(artifactType)
}

headers := map[string][]string{
"Accept": {imgspecv1.MediaTypeImageIndex},
}

logrus.Debugf("Fetching referrers for %s via %s", manifestDigest.String(), path)
res, err := c.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()

// 404 means the API is not supported or no referrers exist via the API
// Fall back to the tag-based referrers scheme (OCI 1.1 fallback)
if res.StatusCode == http.StatusNotFound {
logrus.Debugf("Referrers API returned 404 for %s, trying tag-based fallback", manifestDigest.String())
return c.getReferrersFromTag(ctx, ref, manifestDigest)
}

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetching referrers for %s: %w", manifestDigest.String(), registryHTTPResponseToError(res))
}

body, err := iolimits.ReadAtMost(res.Body, iolimits.MaxManifestBodySize)
if err != nil {
return nil, err
}

index, err := manifest.OCI1IndexFromManifest(body)
if err != nil {
return nil, fmt.Errorf("parsing referrers index for %s: %w", manifestDigest.String(), err)
}

logrus.Debugf("Found %d referrers for %s", len(index.Manifests), manifestDigest.String())
return index, nil
}

// getReferrersFromTag implements the OCI 1.1 referrers tag-based fallback scheme.
// When the referrers API is not supported, referrers are stored as an image index
// at the tag "sha256-<digest>" (replacing ":" with "-").
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
func (c *dockerClient) getReferrersFromTag(ctx context.Context, ref dockerReference, manifestDigest digest.Digest) (*manifest.OCI1Index, error) {
// Convert digest to tag format: sha256:abc123 -> sha256-abc123
tagName := strings.ReplaceAll(manifestDigest.String(), ":", "-")

logrus.Debugf("Trying referrers tag fallback: %s", tagName)

// Fetch the manifest at this tag
path := fmt.Sprintf(manifestPath, reference.Path(ref.ref), tagName)
headers := map[string][]string{
"Accept": {imgspecv1.MediaTypeImageIndex, imgspecv1.MediaTypeImageManifest},
}

res, err := c.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()

// 404 means no referrers tag exists
if res.StatusCode == http.StatusNotFound {
logrus.Debugf("No referrers tag found for %s", manifestDigest.String())
return nil, nil
}

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetching referrers tag for %s: %w", manifestDigest.String(), registryHTTPResponseToError(res))
}

body, err := iolimits.ReadAtMost(res.Body, iolimits.MaxManifestBodySize)
if err != nil {
return nil, err
}

contentType := res.Header.Get("Content-Type")

// The tag might point to an image index (containing multiple referrers)
// or a single image manifest (if there's only one referrer)
if contentType == imgspecv1.MediaTypeImageIndex {
index, err := manifest.OCI1IndexFromManifest(body)
if err != nil {
return nil, fmt.Errorf("parsing referrers index from tag for %s: %w", manifestDigest.String(), err)
}
logrus.Debugf("Found %d referrers via tag fallback for %s", len(index.Manifests), manifestDigest.String())
return index, nil
}

// If it's a single manifest, check if it's a referrer (has subject field)
if contentType == imgspecv1.MediaTypeImageManifest {
// Parse as OCI manifest to check for subject
ociMan, err := manifest.OCI1FromManifest(body)
if err != nil {
return nil, fmt.Errorf("parsing manifest from referrers tag for %s: %w", manifestDigest.String(), err)
}

// Check if this manifest references our target
if ociMan.Subject != nil && ociMan.Subject.Digest == manifestDigest {
// Convert to an index with one entry
desc := imgspecv1.Descriptor{
MediaType: contentType,
Digest: digest.FromBytes(body),
Size: int64(len(body)),
ArtifactType: ociMan.ArtifactType,
}
syntheticIndex := manifest.OCI1IndexFromComponents([]imgspecv1.Descriptor{desc}, nil)
logrus.Debugf("Found 1 referrer via tag fallback (single manifest) for %s", manifestDigest.String())
return syntheticIndex, nil
}
}

logrus.Debugf("Referrers tag for %s does not contain valid referrers", manifestDigest.String())
return nil, nil
}

// getSigstoreReferrers fetches sigstore signature referrers using the OCI 1.1 referrers API.
// It filters for common sigstore artifact types and returns the matching descriptors.
// Returns nil if the referrers API is not supported or no sigstore signatures exist.
func (c *dockerClient) getSigstoreReferrers(ctx context.Context, ref dockerReference, manifestDigest digest.Digest) ([]imgspecv1.Descriptor, error) {
// First try without artifact type filter to get all referrers
index, err := c.getReferrers(ctx, ref, manifestDigest, "")
if err != nil {
return nil, err
}
if index == nil {
return nil, nil
}

// Filter for sigstore-related artifact types
var sigstoreReferrers []imgspecv1.Descriptor
for _, desc := range index.Manifests {
if isSigstoreReferrer(desc) {
sigstoreReferrers = append(sigstoreReferrers, desc)
}
}

return sigstoreReferrers, nil
}

// isSigstoreReferrer returns true if the descriptor represents a sigstore signature or bundle.
func isSigstoreReferrer(desc imgspecv1.Descriptor) bool {
// Check artifact type (OCI 1.1 style)
if desc.ArtifactType != "" {
if strings.HasPrefix(desc.ArtifactType, "application/vnd.dev.sigstore") ||
strings.HasPrefix(desc.ArtifactType, "application/vnd.dev.cosign") {
return true
}
}

// Check media type for legacy compatibility
if strings.HasPrefix(desc.MediaType, "application/vnd.dev.sigstore") ||
strings.HasPrefix(desc.MediaType, "application/vnd.dev.cosign") {
return true
}

// For OCI referrers fallback tag scheme, the descriptor in the index might have
// artifactType set to "application/vnd.oci.empty.v1+json" even when the actual
// manifest is a sigstore bundle. Include these and verify the actual content later.
if desc.ArtifactType == "application/vnd.oci.empty.v1+json" {
return true
}

return false
}
106 changes: 103 additions & 3 deletions image/docker/docker_image_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"sync"

digest "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/internal/imagesource/impl"
Expand Down Expand Up @@ -617,8 +618,9 @@ func (s *dockerImageSource) appendSignaturesFromAPIExtension(ctx context.Context
return nil
}

// appendSignaturesFromSigstoreAttachments implements GetSignaturesWithFormat() using the sigstore tag convention,
// storing the signatures to *dest.
// appendSignaturesFromSigstoreAttachments implements GetSignaturesWithFormat() using sigstore conventions,
// storing the signatures to *dest. It first tries the OCI 1.1 referrers API (Cosign v3 style),
// then falls back to the tag-based attachment scheme.
// On error, the contents of *dest are undefined.
func (s *dockerImageSource) appendSignaturesFromSigstoreAttachments(ctx context.Context, dest *[]signature.Signature, instanceDigest *digest.Digest) error {
if !s.c.useSigstoreAttachments {
Expand All @@ -631,6 +633,104 @@ func (s *dockerImageSource) appendSignaturesFromSigstoreAttachments(ctx context.
return err
}

// Try OCI 1.1 referrers API first (Cosign v3 / sigstore bundle format)
foundViaReferrers, err := s.appendSignaturesFromReferrers(ctx, dest, manifestDigest)
if err != nil {
logrus.Debugf("Referrers API failed: %v, falling back to tag-based scheme", err)
}
if foundViaReferrers {
return nil
}

// Fall back to tag-based attachment scheme (legacy Cosign style)
return s.appendSignaturesFromTagAttachments(ctx, dest, manifestDigest)
}

// appendSignaturesFromReferrers fetches sigstore signatures using the OCI 1.1 referrers API.
// Returns true if any signatures were found, false otherwise.
func (s *dockerImageSource) appendSignaturesFromReferrers(ctx context.Context, dest *[]signature.Signature, manifestDigest digest.Digest) (bool, error) {
referrers, err := s.c.getSigstoreReferrers(ctx, s.physicalRef, manifestDigest)
if err != nil {
return false, err
}
if len(referrers) == 0 {
return false, nil
}

logrus.Debugf("Found %d sigstore referrers via OCI 1.1 referrers API", len(referrers))

for i, desc := range referrers {
logrus.Debugf("Fetching sigstore referrer %d/%d: %s (type: %s)", i+1, len(referrers), desc.Digest.String(), desc.ArtifactType)

// For referrers, we need to fetch the manifest first to get the actual signature content
// The referrer descriptor points to a manifest, which contains layers with the signature
payload, artifactType, annotations, err := s.fetchReferrerPayloadAndType(ctx, desc)
if err != nil {
return false, err
}

// Skip if this isn't actually a sigstore bundle (the index might have wrong artifact type)
if !signature.IsSigstoreBundleMediaType(artifactType) &&
!signature.IsSigstoreSignatureMediaType(artifactType) {
logrus.Debugf("Skipping referrer %s: artifact type %s is not a sigstore type", desc.Digest.String(), artifactType)
continue
}

*dest = append(*dest, signature.SigstoreFromComponents(artifactType, payload, annotations))
}

return true, nil
}

// fetchReferrerPayloadAndType fetches the actual signature payload, artifact type, and annotations from a referrer descriptor.
// For sigstore bundles, the referrer is typically an OCI manifest with a single layer containing the bundle.
// It returns (payload, artifactType, annotations, error).
func (s *dockerImageSource) fetchReferrerPayloadAndType(ctx context.Context, desc imgspecv1.Descriptor) ([]byte, string, map[string]string, error) {
// Referrers point to manifests, not blobs. Fetch via manifest API.
manifestBlob, _, err := s.c.fetchManifest(ctx, s.physicalRef, desc.Digest.String())
if err != nil {
return nil, "", nil, fmt.Errorf("fetching referrer manifest %s: %w", desc.Digest.String(), err)
}

// Parse the manifest to get layers and artifact type
ociManifest, err := manifest.OCI1FromManifest(manifestBlob)
if err != nil {
// If it's not an OCI manifest, the payload might be the manifest itself (for simple bundles)
logrus.Debugf("Referrer %s is not an OCI manifest, using raw content as payload", desc.Digest.String())
return manifestBlob, desc.ArtifactType, desc.Annotations, nil
}

// Get the actual artifact type from the manifest (more reliable than the index entry)
artifactType := ociManifest.ArtifactType
// "application/vnd.oci.empty.v1+json" is a placeholder used when artifact type is unset
if artifactType == "" || artifactType == "application/vnd.oci.empty.v1+json" {
// Check the layer's media type first (most reliable for actual content type)
if len(ociManifest.Layers) > 0 && ociManifest.Layers[0].MediaType != "" {
artifactType = ociManifest.Layers[0].MediaType
} else if desc.ArtifactType != "" && desc.ArtifactType != "application/vnd.oci.empty.v1+json" {
// Fallback to descriptor's artifact type if meaningful
artifactType = desc.ArtifactType
}
}

// For OCI manifests, the signature/bundle is typically in the first layer
if len(ociManifest.Layers) == 0 {
return nil, "", nil, fmt.Errorf("referrer manifest %s has no layers", desc.Digest.String())
}

// Fetch the first layer (the signature payload)
// Annotations are stored on the layer descriptor, not the referrer index descriptor
layer := ociManifest.Layers[0]
payload, err := s.c.getOCIDescriptorContents(ctx, s.physicalRef, layer, iolimits.MaxSignatureBodySize, none.NoCache)
if err != nil {
return nil, "", nil, fmt.Errorf("fetching referrer payload %s: %w", layer.Digest.String(), err)
}

return payload, artifactType, layer.Annotations, nil
}

// appendSignaturesFromTagAttachments implements the legacy tag-based sigstore attachment scheme.
func (s *dockerImageSource) appendSignaturesFromTagAttachments(ctx context.Context, dest *[]signature.Signature, manifestDigest digest.Digest) error {
ociManifest, err := s.c.getSigstoreAttachmentManifest(ctx, s.physicalRef, manifestDigest)
if err != nil {
return err
Expand All @@ -644,7 +744,7 @@ func (s *dockerImageSource) appendSignaturesFromSigstoreAttachments(ctx context.
// Note that this copies all kinds of attachments: attestations, and whatever else is there,
// not just signatures. We leave the signature consumers to decide based on the MIME type.
logrus.Debugf("Fetching sigstore attachment %d/%d: %s", layerIndex+1, len(ociManifest.Layers), layer.Digest.String())
// We dont benefit from a real BlobInfoCache here because we never try to reuse/mount attachment payloads.
// We don't benefit from a real BlobInfoCache here because we never try to reuse/mount attachment payloads.
// That might eventually need to change if payloads grow to be not just signatures, but something
// significantly large.
payload, err := s.c.getOCIDescriptorContents(ctx, s.physicalRef, layer, iolimits.MaxSignatureBodySize,
Expand Down
7 changes: 4 additions & 3 deletions image/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/secure-systems-lab/go-securesystemslib v0.9.1
github.com/sigstore/fulcio v1.8.1
github.com/sigstore/sigstore v1.9.6-0.20251111174640-d8ab8afb1326
github.com/sigstore/sigstore v1.10.0
github.com/sirupsen/logrus v1.9.4-0.20251023124752-b61f268f75b6
github.com/stretchr/testify v1.11.1
github.com/sylabs/sif/v2 v2.22.0
Expand Down Expand Up @@ -65,7 +65,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-containerregistry v0.20.6 // indirect
github.com/google/go-containerregistry v0.20.7 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
Expand All @@ -87,6 +87,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sigstore/protobuf-specs v0.5.0 // indirect
github.com/sigstore/sigstore-go v1.1.4 // indirect
github.com/smallstep/pkcs7 v0.1.1 // indirect
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
Expand All @@ -100,7 +101,7 @@ require (
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
Loading
Loading