Skip to content

Commit c8481f3

Browse files
committed
feat: add support for referrers attestations
While the backwards compatible tag-based attestation method works, it can result in a lot of additional tags being pushed to OCI registries. In order to maintain fewer tags, Chains should be able to use the referrer's API when pushing signatures and attestations. Co-Authored-By: Claude <[email protected]> Signed-off-by: arewm <[email protected]> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED
1 parent d4c7d19 commit c8481f3

File tree

6 files changed

+376
-4
lines changed

6 files changed

+376
-4
lines changed

docs/config.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Supported keys include:
6666
|:-------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|
6767
| `storage.gcs.bucket` | The GCS bucket for storage | | |
6868
| `storage.oci.repository` | The OCI repo to store OCI signatures and attestation in | If left undefined _and_ one of `artifacts.{oci,taskrun}.storage` includes `oci` storage, attestations will be stored alongside the stored OCI artifact itself. ([example on GCP](../images/attestations-in-artifact-registry.png)) Defining this value results in the OCI bundle stored in the designated location _instead of_ alongside the image. See [cosign documentation](https://github.com/sigstore/cosign#specifying-registry) for additional information. | |
69+
| `storage.oci.referrers-api` | Enable OCI 1.1 referrers API for storing signatures and attestations. When enabled, uses the referrers API instead of tag-based storage, reducing the number of additional tags pushed to OCI registries. **Note:** This feature is experimental as it relies on an experimental feature in cosign. | `true`, `false` | `false` |
6970
| `storage.docdb.url` | The go-cloud URI reference to a docstore collection | `firestore://projects/[PROJECT]/databases/(default)/documents/[COLLECTION]?name_field=name` | |
7071
| `storage.docdb.mongo-server-url` (optional) | The value of MONGO_SERVER_URL env var with the MongoDB connection URI | Example: `mongodb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]` | |
7172
| `storage.docdb.mongo-server-url-dir` (optional) | The path of the directory that contains the file named MONGO_SERVER_URL that stores the value of MONGO_SERVER_URL env var | If the file `/mnt/mongo-creds-secret/MONGO_SERVER_URL` has the value of MONGO_SERVER_URL, then set `storage.docdb.mongo-server-url-dir: /mnt/mongo-creds-secret` | |
@@ -189,4 +190,4 @@ To restrict the controller to the dev and test namespaces, you would start the c
189190
```shell
190191
--namespace=dev,test
191192
```
192-
In this example, the controller will only monitor resources (pipelinesruns and taskruns) within the dev and test namespaces.
193+
In this example, the controller will only monitor resources (pipelinesruns and taskruns) within the dev and test namespaces.

pkg/chains/storage/oci/legacy.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import (
3434
"github.com/google/go-containerregistry/pkg/v1/remote"
3535
"github.com/pkg/errors"
3636
"github.com/sigstore/cosign/v2/pkg/oci"
37+
"github.com/sigstore/cosign/v2/pkg/oci/mutate"
3738
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
39+
"github.com/sigstore/cosign/v2/pkg/oci/static"
3840
"github.com/tektoncd/chains/pkg/artifacts"
3941
"github.com/tektoncd/chains/pkg/chains/formats/simple"
4042
"github.com/tektoncd/chains/pkg/config"
@@ -105,6 +107,13 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra
105107
return nil
106108
}
107109

110+
// Use new AttestationStorer with potential referrers API support
111+
logger.Infof("ReferrersAPI config value: %v", b.cfg.Storage.OCI.ReferrersAPI)
112+
if b.cfg.Storage.OCI.ReferrersAPI {
113+
logger.Info("Using referrers API for attestation upload")
114+
return b.uploadAttestationWithReferrers(ctx, &attestation, signature, storageOpts, auth)
115+
}
116+
logger.Info("Using traditional tag-based attestation upload")
108117
return b.uploadAttestation(ctx, &attestation, signature, storageOpts, auth)
109118
}
110119

@@ -115,6 +124,13 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra
115124

116125
func (b *Backend) uploadSignature(ctx context.Context, format simple.SimpleContainerImage, rawPayload []byte, signature string, storageOpts config.StorageOpts, remoteOpts ...remote.Option) error {
117126
logger := logging.FromContext(ctx)
127+
// Use new SimpleStorer with potential referrers API support
128+
logger.Infof("ReferrersAPI config value: %v", b.cfg.Storage.OCI.ReferrersAPI)
129+
if b.cfg.Storage.OCI.ReferrersAPI {
130+
logger.Info("Using referrers API for signature upload")
131+
return b.uploadSignatureWithReferrers(ctx, format, rawPayload, signature, storageOpts, remoteOpts...)
132+
}
133+
logger.Info("Using traditional tag-based signature upload")
118134

119135
imageName := format.ImageName()
120136
logger.Infof("Uploading %s signature", imageName)
@@ -299,3 +315,90 @@ func newRepo(cfg config.Config, imageName name.Digest) (name.Repository, error)
299315
}
300316
return name.NewRepository(imageName.Repository.Name(), opts...)
301317
}
318+
319+
// uploadSignatureWithReferrers uses cosign's OCI 1.1 experimental API for signatures
320+
func (b *Backend) uploadSignatureWithReferrers(ctx context.Context, format simple.SimpleContainerImage, rawPayload []byte, signature string, storageOpts config.StorageOpts, remoteOpts ...remote.Option) error {
321+
logger := logging.FromContext(ctx)
322+
imageName := format.ImageName()
323+
ref, err := name.NewDigest(imageName)
324+
if err != nil {
325+
return errors.Wrap(err, "getting digest")
326+
}
327+
328+
// Get or create the SignedEntity (same as SimpleStorer.Store)
329+
se, err := ociremote.SignedEntity(ref, ociremote.WithRemoteOptions(remoteOpts...))
330+
var entityNotFoundError *ociremote.EntityNotFoundError
331+
if errors.As(err, &entityNotFoundError) {
332+
se = ociremote.SignedUnknown(ref)
333+
} else if err != nil {
334+
return errors.Wrap(err, "getting signed entity")
335+
}
336+
337+
// Create signature options (same as SimpleStorer.Store)
338+
sigOpts := []static.Option{}
339+
if storageOpts.Cert != "" {
340+
sigOpts = append(sigOpts, static.WithCertChain([]byte(storageOpts.Cert), []byte(storageOpts.Chain)))
341+
}
342+
343+
// Create the new signature for this entity
344+
b64sig := base64.StdEncoding.EncodeToString([]byte(signature))
345+
sig, err := static.NewSignature(rawPayload, b64sig, sigOpts...)
346+
if err != nil {
347+
return errors.Wrap(err, "creating signature")
348+
}
349+
350+
// Attach the signature to the entity
351+
newSE, err := mutate.AttachSignatureToEntity(se, sig)
352+
if err != nil {
353+
return errors.Wrap(err, "attaching signature to entity")
354+
}
355+
356+
// Use cosign's OCI 1.1 experimental function instead of WriteSignatures
357+
logger.Info("Using OCI 1.1 referrers API for signature upload")
358+
return ociremote.WriteSignaturesExperimentalOCI(ref, newSE, ociremote.WithRemoteOptions(remoteOpts...))
359+
}
360+
361+
// uploadAttestationWithReferrers uses cosign's OCI 1.1 experimental API for attestations
362+
func (b *Backend) uploadAttestationWithReferrers(ctx context.Context, attestation *intoto.Statement, signature string, storageOpts config.StorageOpts, remoteOpts ...remote.Option) error {
363+
logger := logging.FromContext(ctx)
364+
if len(attestation.Subject) == 0 {
365+
return errors.New("no subjects in attestation")
366+
}
367+
368+
// Get the first subject (main image)
369+
subject := attestation.Subject[0]
370+
imageName := fmt.Sprintf("%s@sha256:%s", subject.Name, subject.Digest["sha256"])
371+
ref, err := name.NewDigest(imageName)
372+
if err != nil {
373+
return errors.Wrap(err, "parsing subject digest")
374+
}
375+
376+
// Create DSSE envelope with the attestation
377+
payload, err := json.Marshal(attestation)
378+
if err != nil {
379+
return errors.Wrap(err, "marshaling attestation")
380+
}
381+
382+
envelope := dsse.Envelope{
383+
PayloadType: "application/vnd.in-toto+json",
384+
Payload: base64.StdEncoding.EncodeToString(payload),
385+
Signatures: []dsse.Signature{
386+
{
387+
Sig: signature,
388+
},
389+
},
390+
}
391+
392+
// Marshal the envelope to create bundle bytes
393+
bundleBytes, err := json.Marshal(envelope)
394+
if err != nil {
395+
return errors.Wrap(err, "marshaling DSSE envelope")
396+
}
397+
398+
// Extract predicate type from attestation
399+
predicateType := attestation.PredicateType
400+
401+
// Use cosign's OCI 1.1 experimental function for attestations
402+
logger.Info("Using OCI 1.1 referrers API for attestation upload")
403+
return ociremote.WriteAttestationNewBundleFormat(ref, bundleBytes, predicateType, ociremote.WithRemoteOptions(remoteOpts...))
404+
}

pkg/chains/storage/oci/options.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515
package oci
1616

17-
import "github.com/google/go-containerregistry/pkg/name"
17+
import (
18+
"os"
19+
20+
"github.com/google/go-containerregistry/pkg/name"
21+
"github.com/google/go-containerregistry/pkg/v1/remote"
22+
)
1823

1924
// Option provides a config option compatible with all OCI storers.
2025
type Option interface {
@@ -52,3 +57,32 @@ func (o *targetRepoOption) applySimpleStorer(s *SimpleStorer) error {
5257
s.repo = &o.repo
5358
return nil
5459
}
60+
61+
// WithReferrersAPI configures the storers to use OCI 1.1 referrers API.
62+
func WithReferrersAPI(enabled bool) Option {
63+
return &referrersAPIOption{
64+
enabled: enabled,
65+
}
66+
}
67+
68+
type referrersAPIOption struct {
69+
enabled bool
70+
}
71+
72+
func (o *referrersAPIOption) applyAttestationStorer(s *AttestationStorer) error {
73+
if o.enabled {
74+
// Enable cosign experimental mode to activate OCI 1.1 referrers API
75+
os.Setenv("COSIGN_EXPERIMENTAL", "1")
76+
s.remoteOpts = append(s.remoteOpts, remote.WithUserAgent("chains/referrers-api"))
77+
}
78+
return nil
79+
}
80+
81+
func (o *referrersAPIOption) applySimpleStorer(s *SimpleStorer) error {
82+
if o.enabled {
83+
// Enable cosign experimental mode to activate OCI 1.1 referrers API
84+
os.Setenv("COSIGN_EXPERIMENTAL", "1")
85+
s.remoteOpts = append(s.remoteOpts, remote.WithUserAgent("chains/referrers-api"))
86+
}
87+
return nil
88+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
Copyright 2023 The Tekton Authors
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+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package oci
15+
16+
import (
17+
"os"
18+
"testing"
19+
20+
"github.com/google/go-containerregistry/pkg/name"
21+
"github.com/tektoncd/chains/pkg/config"
22+
)
23+
24+
func TestReferrersAPIOption(t *testing.T) {
25+
// Test that WithReferrersAPI option sets COSIGN_EXPERIMENTAL
26+
27+
// Clear any existing env var
28+
originalValue := os.Getenv("COSIGN_EXPERIMENTAL")
29+
defer func() {
30+
if originalValue != "" {
31+
os.Setenv("COSIGN_EXPERIMENTAL", originalValue)
32+
} else {
33+
os.Unsetenv("COSIGN_EXPERIMENTAL")
34+
}
35+
}()
36+
os.Unsetenv("COSIGN_EXPERIMENTAL")
37+
38+
// Create storer with referrers API enabled
39+
repo, err := name.NewRepository("example.com/test")
40+
if err != nil {
41+
t.Fatalf("Failed to create repository: %v", err)
42+
}
43+
44+
opts := []AttestationStorerOption{
45+
WithTargetRepository(repo),
46+
WithReferrersAPI(true),
47+
}
48+
49+
storer, err := NewAttestationStorer(opts...)
50+
if err != nil {
51+
t.Fatalf("Failed to create attestation storer: %v", err)
52+
}
53+
54+
// Check that COSIGN_EXPERIMENTAL was set
55+
if os.Getenv("COSIGN_EXPERIMENTAL") != "1" {
56+
t.Errorf("Expected COSIGN_EXPERIMENTAL to be set to '1', got '%s'", os.Getenv("COSIGN_EXPERIMENTAL"))
57+
}
58+
59+
// Check that the storer was configured correctly
60+
if storer.repo == nil {
61+
t.Errorf("Expected storer.repo to be set")
62+
}
63+
64+
if storer.repo.Name() != "example.com/test" {
65+
t.Errorf("Expected repo name to be 'example.com/test', got '%s'", storer.repo.Name())
66+
}
67+
}
68+
69+
func TestReferrersAPIDisabled(t *testing.T) {
70+
// Test that WithReferrersAPI(false) doesn't set COSIGN_EXPERIMENTAL
71+
72+
// Clear any existing env var
73+
originalValue := os.Getenv("COSIGN_EXPERIMENTAL")
74+
defer func() {
75+
if originalValue != "" {
76+
os.Setenv("COSIGN_EXPERIMENTAL", originalValue)
77+
} else {
78+
os.Unsetenv("COSIGN_EXPERIMENTAL")
79+
}
80+
}()
81+
os.Unsetenv("COSIGN_EXPERIMENTAL")
82+
83+
// Create storer with referrers API disabled
84+
repo, err := name.NewRepository("example.com/test")
85+
if err != nil {
86+
t.Fatalf("Failed to create repository: %v", err)
87+
}
88+
89+
opts := []SimpleStorerOption{
90+
WithTargetRepository(repo),
91+
WithReferrersAPI(false),
92+
}
93+
94+
storer, err := NewSimpleStorerFromConfig(opts...)
95+
if err != nil {
96+
t.Fatalf("Failed to create simple storer: %v", err)
97+
}
98+
99+
// Check that COSIGN_EXPERIMENTAL was not set
100+
if os.Getenv("COSIGN_EXPERIMENTAL") != "" {
101+
t.Errorf("Expected COSIGN_EXPERIMENTAL to be unset, got '%s'", os.Getenv("COSIGN_EXPERIMENTAL"))
102+
}
103+
104+
// Check that the storer was configured correctly
105+
if storer.repo == nil {
106+
t.Errorf("Expected storer.repo to be set")
107+
}
108+
}
109+
110+
func TestOCIBackendReferrersConfig(t *testing.T) {
111+
// Test that the OCI backend respects the referrers API configuration
112+
cfg := config.Config{
113+
Storage: config.StorageConfigs{
114+
OCI: config.OCIStorageConfig{
115+
Repository: "example.com/repo",
116+
ReferrersAPI: true,
117+
},
118+
},
119+
}
120+
121+
backend := &Backend{
122+
cfg: cfg,
123+
}
124+
125+
// Verify that the config is accessible
126+
if !backend.cfg.Storage.OCI.ReferrersAPI {
127+
t.Errorf("Expected ReferrersAPI to be enabled in backend config")
128+
}
129+
130+
if backend.cfg.Storage.OCI.Repository != "example.com/repo" {
131+
t.Errorf("Expected repository to be 'example.com/repo', got '%s'", backend.cfg.Storage.OCI.Repository)
132+
}
133+
}

pkg/config/config.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,9 @@ type GCSStorageConfig struct {
116116
}
117117

118118
type OCIStorageConfig struct {
119-
Repository string
120-
Insecure bool
119+
Repository string
120+
Insecure bool
121+
ReferrersAPI bool
121122
}
122123

123124
type TektonStorageConfig struct {
@@ -179,6 +180,7 @@ const (
179180
gcsBucketKey = "storage.gcs.bucket"
180181
ociRepositoryKey = "storage.oci.repository"
181182
ociRepositoryInsecureKey = "storage.oci.repository.insecure"
183+
ociReferrersAPIKey = "storage.oci.referrers-api"
182184
docDBUrlKey = "storage.docdb.url"
183185
docDBMongoServerURLKey = "storage.docdb.mongo-server-url"
184186
docDBMongoServerURLDirKey = "storage.docdb.mongo-server-url-dir"
@@ -310,6 +312,7 @@ func NewConfigFromMap(data map[string]string) (*Config, error) {
310312
asString(gcsBucketKey, &cfg.Storage.GCS.Bucket),
311313
asString(ociRepositoryKey, &cfg.Storage.OCI.Repository),
312314
asBool(ociRepositoryInsecureKey, &cfg.Storage.OCI.Insecure),
315+
asBool(ociReferrersAPIKey, &cfg.Storage.OCI.ReferrersAPI),
313316
asString(docDBUrlKey, &cfg.Storage.DocDB.URL),
314317
asString(docDBMongoServerURLKey, &cfg.Storage.DocDB.MongoServerURL),
315318
asString(docDBMongoServerURLDirKey, &cfg.Storage.DocDB.MongoServerURLDir),

0 commit comments

Comments
 (0)