Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Supported keys include:
| `storage.gcs.bucket` | The GCS bucket for storage | | |
| `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. | |
| `storage.oci.repository.insecure` | Whether to use insecure connection when connecting to the OCI repository | `true`, `false` | `false` |
| `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` |
| `storage.docdb.url` | The go-cloud URI reference to a docstore collection | `firestore://projects/[PROJECT]/databases/(default)/documents/[COLLECTION]?name_field=name` | |
| `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]` | |
| `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` | |
Expand All @@ -90,6 +91,8 @@ Supported keys include:
>
> **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).

For a full description of each format and registry compatibility see [OCI Artifact Distribution (Referrers)](oci-artifact-distribution-format-referrers-schema.md).

#### docstore

You can read about the go-cloud docstore URI format [here](https://gocloud.dev/howto/docstore/). Tekton Chains supports the following docstore services:
Expand Down
212 changes: 212 additions & 0 deletions docs/oci-artifact-distribution-format-referrers-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<!--
---
linkTitle: "OCI Artifact Distribution (Referrers)"
weight: 35
---
-->

# OCI Artifact Distribution: the Referrers Schema

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.

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.

## The problem with the old layout

cosign, which Chains uses under the hood, has traditionally stored a signature
or attestation by pushing an extra tag next to the image. For an image at digest
`sha256:abc...`, it creates tags like `sha256-abc....sig` and `sha256-abc....att`.

This works on every registry, but it has drawbacks:

- Tags are meant to name top-level artifacts you look up and pull. Reusing them
for supporting metadata (signatures, attestations) puts that metadata in the
same namespace as the real artifacts, where it is easy to confuse the two.
- The `.att` tag points to a single manifest that holds *all* of an image's
attestations, and that manifest has no stable digest. When another process
adds an attestation, they are folded back into the same manifest and its
digest changes, so there is no stable, individually addressable reference to a
single attestation.
- No OCI standard describes this layout, so every tool has to special-case it.

## What the Referrers schema is

The [OCI 1.1 distribution spec](https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers)
added a standard way to attach one artifact to another. Instead of inventing a
tag, you push the signature or attestation as its own manifest that records the
image it belongs to in a `subject` field. The registry can then answer a simple
question: "what artifacts refer to this image?"

This is the **Referrers schema**. The payoff is no extra tags, a clean registry,
and a standard that registries and policy tools already understand.

## How cosign enables it

cosign and the `go-containerregistry` library it uses both understand the
Referrers schema, and Chains drives them directly through that library rather
than shelling out to the cosign CLI. When Chains writes in referrers mode, the
library prefers the registry's native Referrers API if it is available. If the
registry does not expose it, the library falls back to the spec's **referrers
tag schema**: it maintains a single `sha256-<digest>` index tag that lists the
referrers.

Either way it is still "referrers mode" — the stored content is the same, no
`.sig` or `.att` tags are created, and `cosign verify` and `oras discover` both
work. This fallback is automatic and needs no configuration, so it also covers
registries that don't yet have native support.

## How Chains enables it

Chains exposes this through a single config flag in the `chains-config`
ConfigMap:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: chains-config
namespace: tekton-chains
data:
storage.oci.distribution-method: "referrers-api"
```

| Value | What Chains does |
|---|---|
| `legacy` (default) | Old tag-based layout (`.sig` / `.att` tags), DSSE-encoded. Works everywhere. |
| `referrers-api` | OCI 1.1 Referrers schema. No extra tags, with automatic fallback on registries that lack native support. |

That's the only knob. You pick *where* artifacts go, and Chains picks the right
encoding to match (explained next).

> [!NOTE]
> This flag only takes effect when the OCI storage backend is in use — that is,
> when `artifacts.oci.storage`, `artifacts.taskrun.storage`, or
> `artifacts.pipelinerun.storage` includes `oci`. If you store signatures and
> attestations somewhere else (docstore, mongo, Grafeas or other options supported in chains), `storage.oci.distribution-method` has no effect.

> [!TIP]
> Chains vendors the Sigstore libraries it needs, so referrers mode works out of
> the box — there is nothing extra to install. You only need a separate `cosign`
> or `oras` CLI if you want to verify or inspect the stored artifacts yourself,
> as shown below.

## Why the encoding is tied to the layout

The two choices line up with cosign's own defaults as they have evolved:

- **legacy** matches cosign's original default — tag-based storage with the
attestation written as a **DSSE** envelope. This is what existing tooling has
always verified.
- **referrers-api** writes the attestation over the OCI 1.1 Referrers API as a
**Sigstore protobuf bundle**, the format newer cosign versions write and
verify.

Chains deliberately ties the encoding to the layout instead of exposing it as a
separate switch. The reason is compatibility: cosign's verification for
referrer-stored attestations expects the protobuf bundle, so a
referrers-plus-DSSE attestation cannot be reliably verified with
`cosign verify-attestation`. Pairing referrers with protobuf keeps Chains in
step with cosign and avoids a matrix of combinations that no tool can verify.

Image **signatures** are handled differently from attestations. A cosign image
signature is a plain signature over the payload, not a DSSE envelope, so in
referrers mode Chains writes it using cosign's native signature manifest — the
exact shape `cosign verify` looks for — rather than a protobuf bundle. Signature
verification therefore works with no extra flags.

## Verifying

Verification is the same in both modes — point cosign at your key:

```shell
# Verify a signature
cosign verify \
--key k8s://tekton-chains/signing-secrets \
<image>@sha256:<digest>

# Verify an attestation
cosign verify-attestation \
--key k8s://tekton-chains/signing-secrets \
--type slsaprovenance \
<image>@sha256:<digest>
```

> [!IMPORTANT]
> Starting with **cosign v2.0**, and continuing through the v3.x series,
> `cosign verify` and `cosign verify-attestation` check for transparency-log
> (Rekor) inclusion **by default**. Whether that check can pass depends on
> Chains' transparency setting:
>
> - **Transparency disabled** (`transparency.enabled: "false"`): Chains does not
> record signatures in a transparency log, so there are no Rekor entries and
> the default cosign check fails with an error such as:
>
> ```text
> Error: no matching signatures: ... not enough verified log entries from
> transparency log: 0 < 1
> ```
>
> This is not a signature problem. Add `--insecure-ignore-tlog=true` to the
> commands above to verify against the public key alone.
>
> - **Transparency enabled** (`transparency.enabled: "true"`): signatures are
> recorded in Rekor, so the default tlog check passes and no extra flag is
> needed.

To see what was stored, use [`oras`](https://oras.land/):

```shell
oras discover <image>@sha256:<digest>
```

## Things to keep in mind in referrers mode

These are interoperability notes, not bugs in Chains.

1. **`storage.oci.repository` is ignored.** This setting normally redirects where
OCI signatures and attestations are stored, letting you keep them in a
different repository from the image. A referrer, by contrast, must live in the
same repository as the image it points at, because the referrer manifest
references its subject by digest within that repository. In referrers mode
Chains logs a warning and stores the referrer next to the image. The override
still works in `legacy` mode.

2. **Older cosign discovery paths may not surface the attestation.** Chains
stores attestations as a protobuf bundle, which is the default for current
cosign versions. Older cosign releases that default to the tag-based layout
discover attestations by a different type and may not list it. The attestation
is still present — `oras discover` shows it and policy engines can consume it —
and `cosign verify` of the signature is unaffected.

3. **Some registries accept a write but don't return it on read.** If a registry
reports success but you can't read the referrer back, it isn't fully OCI 1.1
compliant. Switch that registry to `legacy`.

4. **`oras discover` may show a different `artifactType` per registry.** This is
display only; the stored content and verification don't change.

5. **Concurrent writes can race on the tag-schema fallback.** On registries
without a native Referrers API, the index tag is updated with a
read-append-write cycle, so simultaneous writes to the same image can drop an
entry. Registries with the native Referrers API are not affected. This does
not apply to `legacy` mode.

## Registry compatibility

cosign works with a wide range of registries, including Amazon ECR, Azure
Container Registry, Docker Hub, GitHub Container Registry, GitLab Container
Registry, Google Artifact Registry, Harbor, JFrog Artifactory, and Quay. See the
[cosign registry support page](https://docs.sigstore.dev/cosign/system_config/registry_support/)
for the current list.

Both `legacy` and `referrers-api` work against any OCI-compliant registry.
In `referrers-api` mode, registries with a native Referrers API use it directly;
the rest fall back automatically to the referrers tag schema, as described above.
You do not need to know a registry's level of OCI 1.1 support in advance.

## See also

- [Chains configuration reference](config.md) — all `storage.oci.*` keys.
- [Signing](signing.md) — how signing keys and secrets are configured.
- [cosign registry support](https://docs.sigstore.dev/cosign/system_config/registry_support/)
- [OCI distribution spec — Listing Referrers](https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers)
11 changes: 11 additions & 0 deletions pkg/chains/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
}
measureMetrics(ctx, metrics.SignedMessagesCount, o.Recorder)

// Attempt to extract the public key so storage backends that need it
// (e.g. protobuf-bundle OCI format) can use it without re-fetching.
// This is intentionally non-fatal: for the default legacy format the
// key is never used, so a transient KMS error here must not prevent
// signatures from being stored.
pubKey, pubKeyErr := signer.PublicKey()
if pubKeyErr != nil {
logger.Warnf("Could not extract public key from signer (will be unavailable to storage backends): %v", pubKeyErr)
}

// Now store those!
for _, backend := range sets.List[string](signableType.StorageBackend(cfg)) {
b, ok := o.Backends[backend]
Expand All @@ -202,6 +212,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject)
FullKey: signableType.FullKey(obj),
Cert: signer.Cert(),
Chain: signer.Chain(),
PublicKey: pubKey,
PayloadFormat: payloadFormat,
}
if err := b.StorePayload(ctx, tektonObj, rawPayload, string(signature), storageOpts); err != nil {
Expand Down
6 changes: 6 additions & 0 deletions pkg/chains/signing/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ limitations under the License.
package signing

import (
"crypto"

"github.com/sigstore/sigstore/pkg/signature"
)

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