Skip to content

Commit 0dd4ea1

Browse files
feat: add OCI 1.1 Referrers API support with configurable distribution
Signed-off-by: Anitha Natarajan <anataraj@redhat.com> Co-authored-by: Copilot <claude-sonnet@users.noreply.github.com>
1 parent 2a68e6a commit 0dd4ea1

16 files changed

Lines changed: 1422 additions & 40 deletions

docs/config.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Supported keys include:
6969
| `storage.gcs.bucket` | The GCS bucket for storage | | |
7070
| `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. | |
7171
| `storage.oci.repository.insecure` | Whether to use insecure connection when connecting to the OCI repository | `true`, `false` | `false` |
72+
| `storage.oci.distribution-method` | Controls how OCI signatures and attestations are attached to images in the registry, and implicitly the payload encoding: `legacy` uses tag-based storage with DSSE payloads, `referrers-api` uses the OCI 1.1 Referrers API with Sigstore protobuf-bundle attestations. See [OCI Artifact Distribution (Referrers)](oci-artifact-distribution-format-referrers-schema.md) for details. | `legacy`, `referrers-api` | `legacy` |
7273
| `storage.docdb.url` | The go-cloud URI reference to a docstore collection | `firestore://projects/[PROJECT]/databases/(default)/documents/[COLLECTION]?name_field=name` | |
7374
| `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]` | |
7475
| `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` | |
@@ -90,6 +91,8 @@ Supported keys include:
9091
>
9192
> **Recommendation**: Only use `storage.oci.repository.insecure: true` in development or test environments. For production deployments, always use secure HTTPS connections with valid TLS certificates (`storage.oci.repository.insecure: false`, which is the default).
9293
94+
For a full description of each format and registry compatibility see [OCI Artifact Distribution (Referrers)](oci-artifact-distribution-format-referrers-schema.md).
95+
9396
#### docstore
9497

9598
You can read about the go-cloud docstore URI format [here](https://gocloud.dev/howto/docstore/). Tekton Chains supports the following docstore services:
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
<!--
2+
---
3+
linkTitle: "OCI Artifact Distribution (Referrers)"
4+
weight: 35
5+
---
6+
-->
7+
8+
# OCI Artifact Distribution: the Referrers Schema
9+
10+
When Chains is configured with `oci` as the artifact storage backend, it signs the resulting image and uploads the signature (`.sig`) and SLSA Provenance (`.att`) for the TaskRun or PipelineRun to the configured OCI repository. As OCI registries adopt the [OCI Distribution Spec v1.1.1](https://specs.opencontainers.org/distribution-spec/?v=v1.1.1), [PR#1691](https://github.com/tektoncd/chains/pull/1691) introduced support for uploading these blobs in the Referrers API format, when the registry supports it. This page explains the additional configuration required to adopt the new approach.
11+
12+
If you don't change anything, Chains uses the older tag-based layout and just works. Proceed reading further, if you want to use the OCI 1.1 Referrers API instead.
13+
14+
## The problem with the old layout
15+
16+
cosign, which Chains uses under the hood, has traditionally stored a signature
17+
or attestation by pushing an extra tag next to the image. For an image at digest
18+
`sha256:abc...`, it creates tags like `sha256-abc....sig` and `sha256-abc....att`.
19+
20+
This works on every registry, but it has drawbacks:
21+
22+
- Tags are meant to name top-level artifacts you look up and pull. Reusing them
23+
for supporting metadata (signatures, attestations) puts that metadata in the
24+
same namespace as the real artifacts, where it is easy to confuse the two.
25+
- The `.att` tag points to a single manifest that holds *all* of an image's
26+
attestations, and that manifest has no stable digest. When another process
27+
adds an attestation, they are folded back into the same manifest and its
28+
digest changes, so there is no stable, individually addressable reference to a
29+
single attestation.
30+
- No OCI standard describes this layout, so every tool has to special-case it.
31+
32+
## What the Referrers schema is
33+
34+
The [OCI 1.1 distribution spec](https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers)
35+
added a standard way to attach one artifact to another. Instead of inventing a
36+
tag, you push the signature or attestation as its own manifest that records the
37+
image it belongs to in a `subject` field. The registry can then answer a simple
38+
question: "what artifacts refer to this image?"
39+
40+
This is the **Referrers schema**. The payoff is no extra tags, a clean registry,
41+
and a standard that registries and policy tools already understand.
42+
43+
## How cosign enables it
44+
45+
cosign and the `go-containerregistry` library it uses both understand the
46+
Referrers schema, and Chains drives them directly through that library rather
47+
than shelling out to the cosign CLI. When Chains writes in referrers mode, the
48+
library prefers the registry's native Referrers API if it is available. If the
49+
registry does not expose it, the library falls back to the spec's **referrers
50+
tag schema**: it maintains a single `sha256-<digest>` index tag that lists the
51+
referrers.
52+
53+
Either way it is still "referrers mode" — the stored content is the same, no
54+
`.sig` or `.att` tags are created, and `cosign verify` and `oras discover` both
55+
work. This fallback is automatic and needs no configuration, so it also covers
56+
registries that don't yet have native support.
57+
58+
## How Chains enables it
59+
60+
Chains exposes this through a single config flag in the `chains-config`
61+
ConfigMap:
62+
63+
```yaml
64+
apiVersion: v1
65+
kind: ConfigMap
66+
metadata:
67+
name: chains-config
68+
namespace: tekton-chains
69+
data:
70+
storage.oci.distribution-method: "referrers-api"
71+
```
72+
73+
| Value | What Chains does |
74+
|---|---|
75+
| `legacy` (default) | Old tag-based layout (`.sig` / `.att` tags), DSSE-encoded. Works everywhere. |
76+
| `referrers-api` | OCI 1.1 Referrers schema. No extra tags, with automatic fallback on registries that lack native support. |
77+
78+
That's the only knob. You pick *where* artifacts go, and Chains picks the right
79+
encoding to match (explained next).
80+
81+
> [!NOTE]
82+
> This flag only takes effect when the OCI storage backend is in use — that is,
83+
> when `artifacts.oci.storage`, `artifacts.taskrun.storage`, or
84+
> `artifacts.pipelinerun.storage` includes `oci`. If you store signatures and
85+
> attestations somewhere else (docstore, mongo, Grafeas or other options supported in chains), `storage.oci.distribution-method` has no effect.
86+
87+
> [!TIP]
88+
> Chains vendors the Sigstore libraries it needs, so referrers mode works out of
89+
> the box — there is nothing extra to install. You only need a separate `cosign`
90+
> or `oras` CLI if you want to verify or inspect the stored artifacts yourself,
91+
> as shown below.
92+
93+
## Why the encoding is tied to the layout
94+
95+
The two choices line up with cosign's own defaults as they have evolved:
96+
97+
- **legacy** matches cosign's original default — tag-based storage with the
98+
attestation written as a **DSSE** envelope. This is what existing tooling has
99+
always verified.
100+
- **referrers-api** writes the attestation over the OCI 1.1 Referrers API as a
101+
**Sigstore protobuf bundle**, the format newer cosign versions write and
102+
verify.
103+
104+
Chains deliberately ties the encoding to the layout instead of exposing it as a
105+
separate switch. The reason is compatibility: cosign's verification for
106+
referrer-stored attestations expects the protobuf bundle, so a
107+
referrers-plus-DSSE attestation cannot be reliably verified with
108+
`cosign verify-attestation`. Pairing referrers with protobuf keeps Chains in
109+
step with cosign and avoids a matrix of combinations that no tool can verify.
110+
111+
Image **signatures** are handled differently from attestations. A cosign image
112+
signature is a plain signature over the payload, not a DSSE envelope, so in
113+
referrers mode Chains writes it using cosign's native signature manifest — the
114+
exact shape `cosign verify` looks for — rather than a protobuf bundle. Signature
115+
verification therefore works with no extra flags.
116+
117+
## Verifying
118+
119+
Verification is the same in both modes — point cosign at your key:
120+
121+
```shell
122+
# Verify a signature
123+
cosign verify \
124+
--key k8s://tekton-chains/signing-secrets \
125+
<image>@sha256:<digest>
126+
127+
# Verify an attestation
128+
cosign verify-attestation \
129+
--key k8s://tekton-chains/signing-secrets \
130+
--type slsaprovenance \
131+
<image>@sha256:<digest>
132+
```
133+
134+
> [!IMPORTANT]
135+
> Starting with **cosign v2.0**, and continuing through the v3.x series,
136+
> `cosign verify` and `cosign verify-attestation` check for transparency-log
137+
> (Rekor) inclusion **by default**. Whether that check can pass depends on
138+
> Chains' transparency setting:
139+
>
140+
> - **Transparency disabled** (`transparency.enabled: "false"`): Chains does not
141+
> record signatures in a transparency log, so there are no Rekor entries and
142+
> the default cosign check fails with an error such as:
143+
>
144+
> ```text
145+
> Error: no matching signatures: ... not enough verified log entries from
146+
> transparency log: 0 < 1
147+
> ```
148+
>
149+
> This is not a signature problem. Add `--insecure-ignore-tlog=true` to the
150+
> commands above to verify against the public key alone.
151+
>
152+
> - **Transparency enabled** (`transparency.enabled: "true"`): signatures are
153+
> recorded in Rekor, so the default tlog check passes and no extra flag is
154+
> needed.
155+
156+
To see what was stored, use [`oras`](https://oras.land/):
157+
158+
```shell
159+
oras discover <image>@sha256:<digest>
160+
```
161+
162+
In `referrers-api` mode you will see two referrers. The signature manifest does not
163+
have an `artifactType` set (cosign uses the layer `mediaType` instead), so `oras`
164+
displays it as `<unknown>`. The attestation manifest carries
165+
`application/vnd.dev.sigstore.bundle.v0.3+json` as its `artifactType`, which `oras`
166+
displays directly:
167+
168+
```text
169+
<image>@sha256:<digest>
170+
├── <unknown> ← signature
171+
│ └── sha256:<sig-manifest-digest>
172+
└── application/vnd.dev.sigstore.bundle.v0.3+json ← attestation
173+
└── sha256:<att-manifest-digest>
174+
```
175+
176+
In `legacy` mode, `oras discover` returns no referrers because the signature and
177+
attestation are stored as ordinary tags (`.sig` / `.att`) rather than referrer
178+
manifests.
179+
180+
## Things to keep in mind in referrers mode
181+
182+
These are interoperability notes, not bugs in Chains.
183+
184+
1. **`storage.oci.repository` is ignored.** This setting normally redirects where
185+
OCI signatures and attestations are stored, letting you keep them in a
186+
different repository from the image. A referrer, by contrast, must live in the
187+
same repository as the image it points at, because the referrer manifest
188+
references its subject by digest within that repository. In referrers mode
189+
Chains logs a warning and stores the referrer next to the image. The override
190+
still works in `legacy` mode.
191+
192+
2. **Older cosign discovery paths may not surface the attestation.** Chains
193+
stores attestations as a protobuf bundle, which is the default for current
194+
cosign versions. Older cosign releases that default to the tag-based layout
195+
discover attestations by a different type and may not list it. The attestation
196+
is still present — `oras discover` shows it and policy engines can consume it —
197+
and `cosign verify` of the signature is unaffected.
198+
199+
3. **Some registries accept a write but don't return it on read.** If a registry
200+
reports success but you can't read the referrer back, it isn't fully OCI 1.1
201+
compliant. Switch that registry to `legacy`.
202+
203+
4. **`oras discover` shows the signature as `<unknown>`.** cosign does not set
204+
`artifactType` on signature manifests — it identifies signatures by the layer
205+
`mediaType` (`application/vnd.dev.cosign.simplesigning.v1+json`) instead. As a
206+
result, `oras` cannot identify the artifact type and falls back to `<unknown>`.
207+
This is expected and consistent across all registries. The attestation always
208+
shows its `artifactType` (`application/vnd.dev.sigstore.bundle.v0.3+json`)
209+
because that is set on the manifest itself. Verification with `cosign verify`
210+
and `cosign verify-attestation` is unaffected.
211+
212+
5. **Concurrent writes can race on the tag-schema fallback.** On registries
213+
without a native Referrers API, the index tag is updated with a
214+
read-append-write cycle, so simultaneous writes to the same image can drop an
215+
entry. Registries with the native Referrers API are not affected. This does
216+
not apply to `legacy` mode.
217+
218+
## Registry compatibility
219+
220+
cosign works with a wide range of registries, including Amazon ECR, Azure
221+
Container Registry, Docker Hub, GitHub Container Registry, GitLab Container
222+
Registry, Google Artifact Registry, Harbor, JFrog Artifactory, and Quay. See the
223+
[cosign registry support page](https://docs.sigstore.dev/cosign/system_config/registry_support/)
224+
for the current list.
225+
226+
Both `legacy` and `referrers-api` work against any OCI-compliant registry.
227+
In `referrers-api` mode, registries with a native Referrers API use it directly;
228+
the rest fall back automatically to the referrers tag schema, as described above.
229+
You do not need to know a registry's level of OCI 1.1 support in advance.
230+
231+
## See also
232+
233+
- [Chains configuration reference](config.md) — all `storage.oci.*` keys.
234+
- [Signing](signing.md) — how signing keys and secrets are configured.
235+
- [cosign registry support](https://docs.sigstore.dev/cosign/system_config/registry_support/)
236+
- [OCI distribution spec — Listing Referrers](https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers)

pkg/chains/signing.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
186186
}
187187
measureMetrics(ctx, metrics.SignedMessagesCount, o.Recorder)
188188

189+
// Attempt to extract the public key so storage backends that need it
190+
// (e.g. protobuf-bundle OCI format) can use it without re-fetching.
191+
// This is intentionally non-fatal: for the default legacy format the
192+
// key is never used, so a transient KMS error here must not prevent
193+
// signatures from being stored.
194+
pubKey, pubKeyErr := signer.PublicKey()
195+
if pubKeyErr != nil {
196+
logger.Warnf("Could not extract public key from signer (will be unavailable to storage backends): %v", pubKeyErr)
197+
}
198+
189199
// Now store those!
190200
for _, backend := range sets.List[string](signableType.StorageBackend(cfg)) {
191201
b, ok := o.Backends[backend]
@@ -202,6 +212,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
202212
FullKey: signableType.FullKey(obj),
203213
Cert: signer.Cert(),
204214
Chain: signer.Chain(),
215+
PublicKey: pubKey,
205216
PayloadFormat: payloadFormat,
206217
}
207218
if err := b.StorePayload(ctx, tektonObj, rawPayload, string(signature), storageOpts); err != nil {

pkg/chains/signing/iface.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ limitations under the License.
1414
package signing
1515

1616
import (
17+
"crypto"
18+
1719
"github.com/sigstore/sigstore/pkg/signature"
1820
)
1921

@@ -41,4 +43,8 @@ type Bundle struct {
4143
Cert []byte
4244
// Cert is an optional PEM encoded x509 certificate chain, if one was used for signing.
4345
Chain []byte
46+
// PublicKey is the public key from the signer.
47+
// Available for storage backends that need direct access to the key material
48+
// (e.g. to create a cosign protobuf bundle without a certificate).
49+
PublicKey crypto.PublicKey
4450
}

0 commit comments

Comments
 (0)