Skip to content

Commit fa3f8b2

Browse files
committed
ROX-34488: discover cosign signatures via OCI 1.1 Referrers API
Extract signature payload fetching into cosign_payload_fetcher.go and add referrer-based discovery alongside the existing tag-based method. FetchSignatures now queries both paths and merges results. Referrer errors are swallowed so registries without OCI 1.1 support keep working. This enables discovery of signatures created by cosign v3+, which defaults to storing signatures as OCI referrers instead of legacy tags. Partially generated by AI.
1 parent 3459544 commit fa3f8b2

6 files changed

Lines changed: 563 additions & 119 deletions

File tree

pkg/images/enricher/testing_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type fakeSigFetcher struct {
5656
}
5757

5858
func (f *fakeSigFetcher) FetchSignatures(_ context.Context, _ *storage.Image, _ string,
59-
_ types.Registry) ([]*storage.Signature, error) {
59+
_ types.Registry, _ ...retry.OptionsModifier) ([]*storage.Signature, error) {
6060
if f.fail {
6161
err := errors.New("some error")
6262
if f.retryable {
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package signatures
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"strings"
9+
10+
"github.com/google/go-containerregistry/pkg/name"
11+
v1 "github.com/google/go-containerregistry/pkg/v1"
12+
"github.com/sigstore/cosign/v3/pkg/cosign"
13+
"github.com/sigstore/cosign/v3/pkg/oci"
14+
ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
15+
"github.com/stackrox/rox/generated/storage"
16+
imgUtils "github.com/stackrox/rox/pkg/images/utils"
17+
)
18+
19+
const (
20+
bundleSigArtifactTypePrefix = "application/vnd.dev.sigstore.bundle"
21+
22+
// maxReferrerManifests limits the number of referrer signature manifests processed per image
23+
// to bound latency and prevent pathological cases from consuming the caller's timeout budget.
24+
maxReferrerManifests = 50
25+
)
26+
27+
// signaturePayload is the internal representation of a fetched cosign signature before
28+
// it is persisted into the CosignSignature proto. Two formats are supported:
29+
//
30+
// - SimpleSigning: the legacy cosign format. The signature, payload, certificate, and
31+
// rekor bundle are stored as individual fields in cosign.SignedPayload.
32+
// - Sigstore bundle: the OCI 1.1 bundle format. The raw bundle JSON is stored as-is
33+
// in sigstoreBundle and verified directly via sigstore-go at verification time.
34+
//
35+
// The format is determined by sigstoreBundle: non-empty means sigstore bundle,
36+
// empty means SimpleSigning.
37+
type signaturePayload struct {
38+
cosign.SignedPayload
39+
sigstoreBundle []byte
40+
}
41+
42+
var _ oci.SignedEntity = (*tagSignedEntity)(nil)
43+
44+
// tagSignedEntity adapts an image reference for tag-based signature discovery.
45+
// Signatures() constructs the cosign tag reference (<algo>-<hex>.sig) and fetches
46+
// the signature manifest from the registry. Digest() returns the image digest so
47+
// cosign can build the tag. Only these two methods are called by cosign.FetchSignatures.
48+
// Attestations and Attachment return safe defaults to prevent panics if cosign evolves.
49+
type tagSignedEntity struct {
50+
opts []ociremote.Option
51+
imgRef name.Reference
52+
imgSHA string
53+
}
54+
55+
func newTagSignedEntity(img *storage.Image, imgRef name.Reference, opts ...ociremote.Option) *tagSignedEntity {
56+
return &tagSignedEntity{
57+
opts: opts,
58+
imgRef: imgRef,
59+
imgSHA: imgUtils.GetSHA(img),
60+
}
61+
}
62+
63+
func (s *tagSignedEntity) Digest() (v1.Hash, error) {
64+
return v1.NewHash(s.imgSHA)
65+
}
66+
67+
func (s *tagSignedEntity) Signatures() (oci.Signatures, error) {
68+
h, err := s.Digest()
69+
if err != nil {
70+
return nil, err
71+
}
72+
// Cosign ref: https://github.com/sigstore/cosign/blob/main/pkg/oci/remote/remote.go
73+
return ociremote.Signatures(s.imgRef.Context().Tag(fmt.Sprint(h.Algorithm, "-", h.Hex, ".sig")), s.opts...)
74+
}
75+
76+
func (s *tagSignedEntity) Attestations() (oci.Signatures, error) { return nil, nil }
77+
78+
func (s *tagSignedEntity) Attachment(_ string) (oci.File, error) {
79+
return nil, errors.New("attachments not supported on tag-based signature entity")
80+
}
81+
82+
// fetchSignaturesByTag discovers SimpleSigning signatures via the cosign tag-based method.
83+
// Discovery: looks up the tag <algo>-<hex>.sig in the same repository as the image.
84+
// Format: always SimpleSigning (signature and payload stored in OCI layer annotations).
85+
func fetchSignaturesByTag(image *storage.Image, imgRef name.Reference, opts []ociremote.Option) ([]signaturePayload, error) {
86+
se := newTagSignedEntity(image, imgRef, opts...)
87+
payloads, err := cosign.FetchSignatures(se)
88+
if err != nil && (isMissingSignatureError(err) || isUnknownMimeTypeError(err)) {
89+
return nil, nil
90+
}
91+
wrapped := make([]signaturePayload, len(payloads))
92+
for i, p := range payloads {
93+
wrapped[i] = signaturePayload{SignedPayload: p}
94+
}
95+
return wrapped, err
96+
}
97+
98+
// fetchSignaturesByReferrer discovers sigstore bundle signatures via the OCI 1.1 Referrers API.
99+
// Discovery: queries the referrers index for the image digest and filters for sigstore
100+
// bundle artifact types. Only the sigstore bundle format is supported for referrer-based
101+
// discovery — cosign v3 exclusively produces bundles for this path.
102+
func fetchSignaturesByReferrer(ctx context.Context, digestRef name.Digest, repo name.Repository,
103+
opts []ociremote.Option,
104+
) ([]signaturePayload, error) {
105+
index, err := ociremote.Referrers(digestRef, "", opts...)
106+
if err != nil {
107+
if checkIfErrorContainsCode(err, http.StatusNotFound) {
108+
log.Warnf("OCI referrers API not supported for %s (404)", digestRef.String())
109+
return nil, nil
110+
}
111+
return nil, err
112+
}
113+
if index == nil {
114+
return nil, nil
115+
}
116+
117+
manifests := index.Manifests
118+
if len(manifests) > maxReferrerManifests {
119+
log.Warnf("Image %s has %d referrer manifests, processing only first %d",
120+
digestRef.String(), len(manifests), maxReferrerManifests)
121+
manifests = manifests[:maxReferrerManifests]
122+
}
123+
124+
var payloads []signaturePayload
125+
for _, desc := range manifests {
126+
if ctx.Err() != nil {
127+
break
128+
}
129+
if !strings.HasPrefix(desc.ArtifactType, bundleSigArtifactTypePrefix) {
130+
continue
131+
}
132+
p, err := fetchSigstoreBundle(repo.Digest(desc.Digest.String()), opts)
133+
if err != nil {
134+
log.Warnf("Failed to fetch sigstore bundle from referrer %s: %v", desc.Digest, err)
135+
continue
136+
}
137+
payloads = append(payloads, p...)
138+
}
139+
return payloads, nil
140+
}
141+
142+
// fetchSigstoreBundle fetches a sigstore bundle referrer and stores the raw JSON.
143+
// Format: sigstore bundle (DSSE envelope + verification material + tlog entries in one blob).
144+
// Storage: the raw bundle JSON is stored in SigstoreBundle; no decomposition into individual
145+
// fields. Verification is deferred to verifySigstoreBundle which uses sigstore-go directly.
146+
func fetchSigstoreBundle(bundleRef name.Reference, opts []ociremote.Option) ([]signaturePayload, error) {
147+
b, err := ociremote.Bundle(bundleRef, opts...)
148+
if err != nil {
149+
return nil, err
150+
}
151+
152+
bundleJSON, err := b.MarshalJSON()
153+
if err != nil {
154+
return nil, fmt.Errorf("marshalling sigstore bundle: %w", err)
155+
}
156+
157+
return []signaturePayload{{
158+
sigstoreBundle: bundleJSON,
159+
}}, nil
160+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package signatures
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
"testing"
8+
9+
"github.com/google/go-containerregistry/pkg/name"
10+
"github.com/sigstore/cosign/v3/pkg/oci/mutate"
11+
ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
12+
"github.com/sigstore/cosign/v3/pkg/oci/static"
13+
"github.com/stackrox/rox/pkg/images/types"
14+
imgUtils "github.com/stackrox/rox/pkg/images/utils"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
// uploadSignatureAsReferrer stores a cosign signature as an OCI 1.1 referrer for the given image.
20+
func uploadSignatureAsReferrer(imgRef string, b64Sig string, sigPayload, certPEM, chainPEM []byte) error {
21+
sigOpts := []static.Option{static.WithCertChain(certPEM, chainPEM)}
22+
23+
sig, err := static.NewSignature(sigPayload, b64Sig, sigOpts...)
24+
if err != nil {
25+
return err
26+
}
27+
28+
ref, err := name.ParseReference(imgRef)
29+
if err != nil {
30+
return err
31+
}
32+
d, ok := ref.(name.Digest)
33+
if !ok {
34+
return fmt.Errorf("could not cast reference %q to name.Digest", ref.String())
35+
}
36+
37+
se, err := ociremote.SignedEntity(ref)
38+
if err != nil {
39+
return err
40+
}
41+
42+
newSE, err := mutate.AttachSignatureToEntity(se, sig)
43+
if err != nil {
44+
return err
45+
}
46+
47+
return ociremote.WriteSignaturesExperimentalOCI(d, newSE)
48+
}
49+
50+
func TestFetchTagPayloads_WithSignatures(t *testing.T) {
51+
registryServer, imgRef, err := registryServerWithImage("nginx")
52+
require.NoError(t, err, "setting up registry")
53+
defer registryServer.Close()
54+
55+
sigPayload, err := base64.StdEncoding.DecodeString(payload1)
56+
require.NoError(t, err)
57+
58+
require.NoError(t, uploadSignatureForImage(imgRef, sig1, sigPayload, nil, nil, nil),
59+
"uploading tag-based signature")
60+
61+
ref, err := name.ParseReference(imgRef)
62+
require.NoError(t, err)
63+
img, err := imgUtils.GenerateImageFromString(imgRef)
64+
require.NoError(t, err)
65+
66+
payloads, err := fetchSignaturesByTag(types.ToImage(img), ref, nil)
67+
require.NoError(t, err)
68+
assert.Len(t, payloads, 1)
69+
assert.Equal(t, sigPayload, payloads[0].Payload)
70+
assert.Equal(t, sig1, payloads[0].Base64Signature)
71+
}
72+
73+
func TestFetchTagPayloads_NoSignatures(t *testing.T) {
74+
registryServer, imgRef, err := registryServerWithImage("nginx")
75+
require.NoError(t, err, "setting up registry")
76+
defer registryServer.Close()
77+
78+
ref, err := name.ParseReference(imgRef)
79+
require.NoError(t, err)
80+
img, err := imgUtils.GenerateImageFromString(imgRef)
81+
require.NoError(t, err)
82+
83+
payloads, err := fetchSignaturesByTag(types.ToImage(img), ref, nil)
84+
require.NoError(t, err)
85+
assert.Empty(t, payloads)
86+
}
87+
88+
func TestFetchReferrerPayloads_NoSignatures(t *testing.T) {
89+
registryServer, imgRef, err := registryServerWithImage("nginx")
90+
require.NoError(t, err, "setting up registry")
91+
defer registryServer.Close()
92+
93+
ref, err := name.ParseReference(imgRef)
94+
require.NoError(t, err)
95+
d, ok := ref.(name.Digest)
96+
require.True(t, ok)
97+
98+
payloads, err := fetchSignaturesByReferrer(context.Background(), d, ref.Context(), nil)
99+
require.NoError(t, err)
100+
assert.Empty(t, payloads)
101+
}

0 commit comments

Comments
 (0)