Skip to content

Commit e29377d

Browse files
committed
sigstore-bundle: add oci v1.1 referrers API support for fetching signatures
Signed-off-by: Robert Sturla <[email protected]>
1 parent 621dfbc commit e29377d

File tree

3 files changed

+323
-3
lines changed

3 files changed

+323
-3
lines changed

image/docker/docker_client.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,28 @@ func (c *dockerClient) fetchManifest(ctx context.Context, ref dockerReference, t
10351035
return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil
10361036
}
10371037

1038+
// getManifestSize returns the size of a manifest using a HEAD request.
1039+
// This is used when we need the size for OCI descriptor fields but don't need the manifest content.
1040+
func (c *dockerClient) getManifestSize(ctx context.Context, ref dockerReference, manifestDigest digest.Digest) (int64, error) {
1041+
path := fmt.Sprintf(manifestPath, reference.Path(ref.ref), manifestDigest.String())
1042+
headers := map[string][]string{
1043+
"Accept": manifest.DefaultRequestedManifestMIMETypes,
1044+
}
1045+
res, err := c.makeRequest(ctx, http.MethodHead, path, headers, nil, v2Auth, nil)
1046+
if err != nil {
1047+
return 0, err
1048+
}
1049+
defer res.Body.Close()
1050+
if res.StatusCode != http.StatusOK {
1051+
return 0, fmt.Errorf("getting manifest size %s in %s: %w", manifestDigest.String(), ref.ref.Name(), registryHTTPResponseToError(res))
1052+
}
1053+
contentLength := res.ContentLength
1054+
if contentLength < 0 {
1055+
return 0, fmt.Errorf("manifest HEAD response missing Content-Length for %s", manifestDigest.String())
1056+
}
1057+
return contentLength, nil
1058+
}
1059+
10381060
// getExternalBlob returns the reader of the first available blob URL from urls, which must not be empty.
10391061
// This function can return nil reader when no url is supported by this function. In this case, the caller
10401062
// should fallback to fetch the non-external blob (i.e. pull from the registry).
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"strings"
9+
10+
digest "github.com/opencontainers/go-digest"
11+
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
12+
"github.com/sirupsen/logrus"
13+
"go.podman.io/image/v5/docker/reference"
14+
"go.podman.io/image/v5/internal/iolimits"
15+
"go.podman.io/image/v5/manifest"
16+
)
17+
18+
const (
19+
// OCI 1.1 referrers API path for fetching artifacts that reference a manifest
20+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
21+
referrersPath = "/v2/%s/referrers/%s"
22+
)
23+
24+
// getReferrers fetches the referrers index for a manifest using the OCI 1.1 referrers API.
25+
// If artifactType is non-empty, it filters the results to only include referrers of that type.
26+
// Returns (nil, nil) if the registry does not support the referrers API or no referrers exist.
27+
func (c *dockerClient) getReferrers(ctx context.Context, ref dockerReference, manifestDigest digest.Digest, artifactType string) (*manifest.OCI1Index, error) {
28+
if err := manifestDigest.Validate(); err != nil {
29+
return nil, err
30+
}
31+
32+
path := fmt.Sprintf(referrersPath, reference.Path(ref.ref), manifestDigest.String())
33+
if artifactType != "" {
34+
path += "?artifactType=" + url.QueryEscape(artifactType)
35+
}
36+
37+
headers := map[string][]string{
38+
"Accept": {imgspecv1.MediaTypeImageIndex},
39+
}
40+
41+
logrus.Debugf("Fetching referrers for %s via %s", manifestDigest.String(), path)
42+
res, err := c.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
43+
if err != nil {
44+
return nil, err
45+
}
46+
defer res.Body.Close()
47+
48+
// 404 means the API is not supported or no referrers exist via the API
49+
// Fall back to the tag-based referrers scheme (OCI 1.1 fallback)
50+
if res.StatusCode == http.StatusNotFound {
51+
logrus.Debugf("Referrers API returned 404 for %s, trying tag-based fallback", manifestDigest.String())
52+
return c.getReferrersFromTag(ctx, ref, manifestDigest)
53+
}
54+
55+
if res.StatusCode != http.StatusOK {
56+
return nil, fmt.Errorf("fetching referrers for %s: %w", manifestDigest.String(), registryHTTPResponseToError(res))
57+
}
58+
59+
body, err := iolimits.ReadAtMost(res.Body, iolimits.MaxManifestBodySize)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
index, err := manifest.OCI1IndexFromManifest(body)
65+
if err != nil {
66+
return nil, fmt.Errorf("parsing referrers index for %s: %w", manifestDigest.String(), err)
67+
}
68+
69+
logrus.Debugf("Found %d referrers for %s", len(index.Manifests), manifestDigest.String())
70+
return index, nil
71+
}
72+
73+
// getReferrersFromTag implements the OCI 1.1 referrers tag-based fallback scheme.
74+
// When the referrers API is not supported, referrers are stored as an image index
75+
// at the tag "sha256-<digest>" (replacing ":" with "-").
76+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
77+
func (c *dockerClient) getReferrersFromTag(ctx context.Context, ref dockerReference, manifestDigest digest.Digest) (*manifest.OCI1Index, error) {
78+
// Convert digest to tag format: sha256:abc123 -> sha256-abc123
79+
tagName := strings.ReplaceAll(manifestDigest.String(), ":", "-")
80+
81+
logrus.Debugf("Trying referrers tag fallback: %s", tagName)
82+
83+
// Fetch the manifest at this tag
84+
path := fmt.Sprintf(manifestPath, reference.Path(ref.ref), tagName)
85+
headers := map[string][]string{
86+
"Accept": {imgspecv1.MediaTypeImageIndex, imgspecv1.MediaTypeImageManifest},
87+
}
88+
89+
res, err := c.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
90+
if err != nil {
91+
return nil, err
92+
}
93+
defer res.Body.Close()
94+
95+
// 404 means no referrers tag exists
96+
if res.StatusCode == http.StatusNotFound {
97+
logrus.Debugf("No referrers tag found for %s", manifestDigest.String())
98+
return nil, nil
99+
}
100+
101+
if res.StatusCode != http.StatusOK {
102+
return nil, fmt.Errorf("fetching referrers tag for %s: %w", manifestDigest.String(), registryHTTPResponseToError(res))
103+
}
104+
105+
body, err := iolimits.ReadAtMost(res.Body, iolimits.MaxManifestBodySize)
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
contentType := res.Header.Get("Content-Type")
111+
112+
// The tag might point to an image index (containing multiple referrers)
113+
// or a single image manifest (if there's only one referrer)
114+
if contentType == imgspecv1.MediaTypeImageIndex {
115+
index, err := manifest.OCI1IndexFromManifest(body)
116+
if err != nil {
117+
return nil, fmt.Errorf("parsing referrers index from tag for %s: %w", manifestDigest.String(), err)
118+
}
119+
logrus.Debugf("Found %d referrers via tag fallback for %s", len(index.Manifests), manifestDigest.String())
120+
return index, nil
121+
}
122+
123+
// If it's a single manifest, check if it's a referrer (has subject field)
124+
if contentType == imgspecv1.MediaTypeImageManifest {
125+
// Parse as OCI manifest to check for subject
126+
ociMan, err := manifest.OCI1FromManifest(body)
127+
if err != nil {
128+
return nil, fmt.Errorf("parsing manifest from referrers tag for %s: %w", manifestDigest.String(), err)
129+
}
130+
131+
// Check if this manifest references our target
132+
if ociMan.Subject != nil && ociMan.Subject.Digest == manifestDigest {
133+
// Convert to an index with one entry
134+
desc := imgspecv1.Descriptor{
135+
MediaType: contentType,
136+
Digest: digest.FromBytes(body),
137+
Size: int64(len(body)),
138+
ArtifactType: ociMan.ArtifactType,
139+
}
140+
syntheticIndex := manifest.OCI1IndexFromComponents([]imgspecv1.Descriptor{desc}, nil)
141+
logrus.Debugf("Found 1 referrer via tag fallback (single manifest) for %s", manifestDigest.String())
142+
return syntheticIndex, nil
143+
}
144+
}
145+
146+
logrus.Debugf("Referrers tag for %s does not contain valid referrers", manifestDigest.String())
147+
return nil, nil
148+
}
149+
150+
// getSigstoreReferrers fetches sigstore signature referrers using the OCI 1.1 referrers API.
151+
// It filters for common sigstore artifact types and returns the matching descriptors.
152+
// Returns nil if the referrers API is not supported or no sigstore signatures exist.
153+
func (c *dockerClient) getSigstoreReferrers(ctx context.Context, ref dockerReference, manifestDigest digest.Digest) ([]imgspecv1.Descriptor, error) {
154+
// First try without artifact type filter to get all referrers
155+
index, err := c.getReferrers(ctx, ref, manifestDigest, "")
156+
if err != nil {
157+
return nil, err
158+
}
159+
if index == nil {
160+
return nil, nil
161+
}
162+
163+
// Filter for sigstore-related artifact types
164+
var sigstoreReferrers []imgspecv1.Descriptor
165+
for _, desc := range index.Manifests {
166+
if isSigstoreReferrer(desc) {
167+
sigstoreReferrers = append(sigstoreReferrers, desc)
168+
}
169+
}
170+
171+
return sigstoreReferrers, nil
172+
}
173+
174+
// isSigstoreReferrer returns true if the descriptor represents a sigstore signature or bundle.
175+
func isSigstoreReferrer(desc imgspecv1.Descriptor) bool {
176+
// Check artifact type (OCI 1.1 style)
177+
if desc.ArtifactType != "" {
178+
if strings.HasPrefix(desc.ArtifactType, "application/vnd.dev.sigstore") ||
179+
strings.HasPrefix(desc.ArtifactType, "application/vnd.dev.cosign") {
180+
return true
181+
}
182+
}
183+
184+
// Check media type for legacy compatibility
185+
if strings.HasPrefix(desc.MediaType, "application/vnd.dev.sigstore") ||
186+
strings.HasPrefix(desc.MediaType, "application/vnd.dev.cosign") {
187+
return true
188+
}
189+
190+
// For OCI referrers fallback tag scheme, the descriptor in the index might have
191+
// artifactType set to "application/vnd.oci.empty.v1+json" even when the actual
192+
// manifest is a sigstore bundle. Include these and verify the actual content later.
193+
if desc.ArtifactType == "application/vnd.oci.empty.v1+json" {
194+
return true
195+
}
196+
197+
return false
198+
}

image/docker/docker_image_src.go

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"sync"
1919

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

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

636+
// Try OCI 1.1 referrers API first (Cosign v3 / sigstore bundle format)
637+
foundViaReferrers, err := s.appendSignaturesFromReferrers(ctx, dest, manifestDigest)
638+
if err != nil {
639+
logrus.Debugf("Referrers API failed: %v, falling back to tag-based scheme", err)
640+
}
641+
if foundViaReferrers {
642+
return nil
643+
}
644+
645+
// Fall back to tag-based attachment scheme (legacy Cosign style)
646+
return s.appendSignaturesFromTagAttachments(ctx, dest, manifestDigest)
647+
}
648+
649+
// appendSignaturesFromReferrers fetches sigstore signatures using the OCI 1.1 referrers API.
650+
// Returns true if any signatures were found, false otherwise.
651+
func (s *dockerImageSource) appendSignaturesFromReferrers(ctx context.Context, dest *[]signature.Signature, manifestDigest digest.Digest) (bool, error) {
652+
referrers, err := s.c.getSigstoreReferrers(ctx, s.physicalRef, manifestDigest)
653+
if err != nil {
654+
return false, err
655+
}
656+
if len(referrers) == 0 {
657+
return false, nil
658+
}
659+
660+
logrus.Debugf("Found %d sigstore referrers via OCI 1.1 referrers API", len(referrers))
661+
662+
for i, desc := range referrers {
663+
logrus.Debugf("Fetching sigstore referrer %d/%d: %s (type: %s)", i+1, len(referrers), desc.Digest.String(), desc.ArtifactType)
664+
665+
// For referrers, we need to fetch the manifest first to get the actual signature content
666+
// The referrer descriptor points to a manifest, which contains layers with the signature
667+
payload, artifactType, annotations, err := s.fetchReferrerPayloadAndType(ctx, desc)
668+
if err != nil {
669+
return false, err
670+
}
671+
672+
// Skip if this isn't actually a sigstore bundle (the index might have wrong artifact type)
673+
if !signature.IsSigstoreBundleMediaType(artifactType) &&
674+
!signature.IsSigstoreSignatureMediaType(artifactType) {
675+
logrus.Debugf("Skipping referrer %s: artifact type %s is not a sigstore type", desc.Digest.String(), artifactType)
676+
continue
677+
}
678+
679+
*dest = append(*dest, signature.SigstoreFromComponents(artifactType, payload, annotations))
680+
}
681+
682+
return true, nil
683+
}
684+
685+
// fetchReferrerPayloadAndType fetches the actual signature payload, artifact type, and annotations from a referrer descriptor.
686+
// For sigstore bundles, the referrer is typically an OCI manifest with a single layer containing the bundle.
687+
// It returns (payload, artifactType, annotations, error).
688+
func (s *dockerImageSource) fetchReferrerPayloadAndType(ctx context.Context, desc imgspecv1.Descriptor) ([]byte, string, map[string]string, error) {
689+
// Referrers point to manifests, not blobs. Fetch via manifest API.
690+
manifestBlob, _, err := s.c.fetchManifest(ctx, s.physicalRef, desc.Digest.String())
691+
if err != nil {
692+
return nil, "", nil, fmt.Errorf("fetching referrer manifest %s: %w", desc.Digest.String(), err)
693+
}
694+
695+
// Parse the manifest to get layers and artifact type
696+
ociManifest, err := manifest.OCI1FromManifest(manifestBlob)
697+
if err != nil {
698+
// If it's not an OCI manifest, the payload might be the manifest itself (for simple bundles)
699+
logrus.Debugf("Referrer %s is not an OCI manifest, using raw content as payload", desc.Digest.String())
700+
return manifestBlob, desc.ArtifactType, desc.Annotations, nil
701+
}
702+
703+
// Get the actual artifact type from the manifest (more reliable than the index entry)
704+
artifactType := ociManifest.ArtifactType
705+
// "application/vnd.oci.empty.v1+json" is a placeholder used when artifact type is unset
706+
if artifactType == "" || artifactType == "application/vnd.oci.empty.v1+json" {
707+
// Check the layer's media type first (most reliable for actual content type)
708+
if len(ociManifest.Layers) > 0 && ociManifest.Layers[0].MediaType != "" {
709+
artifactType = ociManifest.Layers[0].MediaType
710+
} else if desc.ArtifactType != "" && desc.ArtifactType != "application/vnd.oci.empty.v1+json" {
711+
// Fallback to descriptor's artifact type if meaningful
712+
artifactType = desc.ArtifactType
713+
}
714+
}
715+
716+
// For OCI manifests, the signature/bundle is typically in the first layer
717+
if len(ociManifest.Layers) == 0 {
718+
return nil, "", nil, fmt.Errorf("referrer manifest %s has no layers", desc.Digest.String())
719+
}
720+
721+
// Fetch the first layer (the signature payload)
722+
// Annotations are stored on the layer descriptor, not the referrer index descriptor
723+
layer := ociManifest.Layers[0]
724+
payload, err := s.c.getOCIDescriptorContents(ctx, s.physicalRef, layer, iolimits.MaxSignatureBodySize, none.NoCache)
725+
if err != nil {
726+
return nil, "", nil, fmt.Errorf("fetching referrer payload %s: %w", layer.Digest.String(), err)
727+
}
728+
729+
return payload, artifactType, layer.Annotations, nil
730+
}
731+
732+
// appendSignaturesFromTagAttachments implements the legacy tag-based sigstore attachment scheme.
733+
func (s *dockerImageSource) appendSignaturesFromTagAttachments(ctx context.Context, dest *[]signature.Signature, manifestDigest digest.Digest) error {
634734
ociManifest, err := s.c.getSigstoreAttachmentManifest(ctx, s.physicalRef, manifestDigest)
635735
if err != nil {
636736
return err
@@ -644,7 +744,7 @@ func (s *dockerImageSource) appendSignaturesFromSigstoreAttachments(ctx context.
644744
// Note that this copies all kinds of attachments: attestations, and whatever else is there,
645745
// not just signatures. We leave the signature consumers to decide based on the MIME type.
646746
logrus.Debugf("Fetching sigstore attachment %d/%d: %s", layerIndex+1, len(ociManifest.Layers), layer.Digest.String())
647-
// We dont benefit from a real BlobInfoCache here because we never try to reuse/mount attachment payloads.
747+
// We don't benefit from a real BlobInfoCache here because we never try to reuse/mount attachment payloads.
648748
// That might eventually need to change if payloads grow to be not just signatures, but something
649749
// significantly large.
650750
payload, err := s.c.getOCIDescriptorContents(ctx, s.physicalRef, layer, iolimits.MaxSignatureBodySize,

0 commit comments

Comments
 (0)