A build provenance attestation is a signed, machine-readable statement, produced by the build pipeline itself, that records exactly how and from what an artifact was built — which builder ran, from which source at which commit, via which build definition, and with which parameters. It answers “where did these bytes come from?” with cryptographic evidence rather than trust. The dominant format is an in-toto attestation: a JSON Statement that binds a subject (the artifact, identified by its digest) to a predicate (the provenance claims), wrapped in a signed DSSE envelope (in-toto attestation spec). The predicate schema that CI systems overwhelmingly use is SLSA Provenance, predicate type https://slsa.dev/provenance/v1, whose buildDefinition captures the source and parameters and whose runDetails captures the builder identity and timing (SLSA v1.2 build provenance). Once produced, the attestation is attached to the artifact in an OCI registry as a referrer — a separate manifest whose subject field points at the image’s digest — so anyone pulling the image can discover and retrieve its provenance (OCI Distribution Spec).
Boundary of this note
This note owns the pipeline mechanism of producing a provenance attestation and attaching it to the registry. The verification and enforcement side — checking provenance at admission, what each SLSA Build Level requires, gating deploys on it — is owned by Build Provenance and Verifiable Builds and The SLSA Framework in DevSecOps and Supply Chain Security MOC. Those are forward-links (ghost notes) here; this note deliberately does not teach SLSA levels or verification policy in depth.
Mental Model — Provenance as a Signed Referrer Hanging Off the Artifact
Think of the artifact and its provenance as two objects in the registry, joined by a one-way arrow. The image is addressed by its content digest. The attestation is a separate manifest that carries a subject pointing back at that digest, so the registry can answer “what refers to this image?” and hand you the provenance. The attestation is signed, so the arrow is not merely a claim of association but a verifiable one.
flowchart TD
subgraph REG["OCI registry"]
IMG["Image manifest<br/>sha256:abc123…<br/>(the artifact)"]
ATT["Attestation manifest<br/>subject → sha256:abc123…<br/>artifactType: provenance"]
ATT -->|"subject digest<br/>(referrer relationship)"| IMG
end
BUILD["CI build job"] -->|"1. build + push"| IMG
BUILD -->|"2. generate provenance"| PRED["SLSA predicate<br/>builder / source / params"]
PRED --> STMT["in-toto Statement<br/>subject + predicateType + predicate"]
STMT --> ENV["DSSE envelope<br/>base64 payload + signature"]
ENV -->|"3. push as referrer"| ATT
CONSUMER["Consumer / verifier"] -->|"GET /v2/app/referrers/sha256:abc…"| REG
REG -->|"returns index of referrers"| CONSUMER
style IMG fill:#2d5,stroke:#083,color:#000
style ATT fill:#6cf,stroke:#059,color:#000
What it shows and the insight to take: the pipeline (left) builds the image, generates a provenance predicate, wraps it in a signed in-toto/DSSE envelope, and pushes it as an attestation manifest whose subject is the image digest (blue node). The registry now knows the attestation refers to the image. A consumer later queries the referrers API with the image digest and gets back the attestation. The insight: provenance is not embedded inside the image — it is a first-class, independently-signed sibling object linked by digest, which is exactly why it can be added, rotated, or accompanied by other referrers (signatures, SBOMs) without changing the image’s bytes.
The Three Layers of an in-toto Attestation
An in-toto attestation is a layered structure. Getting the layers straight is the whole battle, because “attestation,” “statement,” “predicate,” and “envelope” are constantly used loosely.
Caption: the envelope (outermost) authenticates; the statement binds an artifact to a predicate type; the predicate carries the actual claims. Read outside-in for verification, inside-out for production.
1. The Predicate layer — the type-specific claims. For build provenance this is the SLSA Provenance object (detailed below). The predicate is where “how was it built” actually lives.
2. The Statement layer — binds the predicate to a subject. Its schema is exact (in-toto Statement v1):
_type is always https://in-toto.io/Statement/v1. subject is an array of ResourceDescriptors, each of which must carry a digest — the spec is explicit that “subject artifacts are matched purely by digest, regardless of content type.” So the statement is bound to the exact bytes by their hash. predicateType is a URI naming the schema; predicate holds the data (and may be omitted if the type fully describes it).
3. The Envelope layer — a DSSE (Dead Simple Signing Envelope) that authenticates and serializes the statement (in-toto envelope):
The Statement is base64-encoded into payload; payloadType is application/vnd.in-toto+json; signatures is a required array. Crucially, DSSE signs over a PAE (Pre-Authentication Encoding) — a canonical concatenation of the payloadType and payload — so the signature covers both the content and its declared type, defeating a class of type-confusion attacks. (The in-toto envelope doc references DSSE v1.0 for PAE mechanics rather than restating them.) A verifier should not trust the envelope’s media type as a faithful indicator of the predicate type — it must decode the payload and check the Statement’s predicateType after verifying the signature.
What a SLSA Provenance Predicate Records
The SLSA Provenance predicate (https://slsa.dev/provenance/v1, which “always resolves to the latest minor version”) has two top-level halves (SLSA v1.2 build provenance):
buildDefinition — all the inputs that defined the build:
buildType — a URI naming the template by which the build was performed (e.g. an identifier for “GitHub Actions workflow” or a Google Cloud Build type). It tells a verifier how to interpret the parameters below.
externalParameters — the user/tenant-controlled inputs: the source, the entrypoint, and the build parameters. This is where the source repository and ref live. A canonical example:
The spec labels these the “external interface to the build” and — importantly — untrusted: a verifier must check them against policy, because a tenant controls them.
internalParameters — platform-set values (trusted, because the platform is trusted), retained for “debugging, incident response, and vulnerability management.”
resolvedDependencies — the artifacts actually fetched during the build, each with a URI and digest, e.g. the exact source commit resolved:
This pins the entrypoint source at a specific commit hash, so “built from main” becomes “built from commit 7fd1a60….”
runDetails — how this particular run executed:
builder.id — a URI identifying “the transitive closure of the trusted build platform.” This single field establishes builder identity and, per the spec, alone determines the SLSA Build level; it SHOULD resolve to documentation of that builder’s security guarantees. It is the answer to “who built this,” and it is the anchor of trust — a verifier trusts a provenance because it trusts this builder id and can confirm the envelope was signed by that builder’s identity.
builder.version / builder.builderDependencies — component versions and orchestrator dependencies not affecting the output.
metadata — invocationId (unique per build run), startedOn, finishedOn timestamps.
So a complete provenance answers, verifiably: who built it (builder.id), from what (externalParameters.repository + resolvedDependencies commit digest), via what recipe (buildType), with what inputs (externalParameters), when (metadata) — bound to which output (the Statement subject digest).
Uncertain
Verify: the exact set and nesting of SLSA v1.2 provenance fields (e.g. whether v1.2 renamed or added any field versus v1.0). Reason: the slsa.dev/spec/v1.2/provenance and .../about pages rendered largely as navigation hubs when fetched, so the field-level detail here is reconciled from the v1.2 build-provenance page plus the (now “Retired”) v1.0 provenance schema page, which share the .../provenance/v1 predicate type. To resolve: fetch the full rendered v1.2 build-provenance JSON schema section and diff it against v1.0. The predicate type URI, the two-part buildDefinition/runDetails split, and the external/internal parameter distinction are directly confirmed from v1.2 sources. #uncertain
Attaching the Attestation to the Registry — the OCI Referrers Model
Producing the signed envelope is half the job; the pipeline must attach it so consumers can find it. The OCI Distribution Spec referrers API (added in distribution-spec v1.1) is the mechanism (OCI Distribution Spec).
The attestation is pushed as its own manifest that includes a subject field pointing at the image’s digest, and an artifactType identifying it as provenance. The registry indexes this relationship. On a successful push, the registry acknowledges with an OCI-Subject: <digest> response header, confirming it recorded the referrer link.
To discover provenance later, a client issues:
GET /v2/<name>/referrers/<image-digest>
which returns “a JSON body with an image index containing a list of descriptors” — every manifest that declared that image as its subject. Each descriptor carries its mediaType, digest, and artifactType. A client can filter:
GET /v2/<name>/referrers/<image-digest>?artifactType=<provenance-type>
and the registry signals filtering via an OCI-Filters-Applied header. Registries that predate v1.1 return 404 for the referrers endpoint; clients then fall back to the referrers tag schema (a predictable tag derived from the subject digest) to locate the same artifacts.
sequenceDiagram
participant CI as CI build job
participant REG as OCI registry
participant V as Verifier
CI->>REG: PUT image manifest → sha256:abc…
CI->>CI: build SLSA provenance + sign (DSSE)
CI->>REG: PUT attestation manifest (subject=sha256:abc…, artifactType=provenance)
REG-->>CI: 201 + OCI-Subject: sha256:abc…
Note over REG: registry indexes the referrer link
V->>REG: GET /v2/app/referrers/sha256:abc…?artifactType=provenance
REG-->>V: 200 image index → [attestation descriptor]
V->>REG: GET attestation blob
REG-->>V: DSSE envelope (verify signature + subject)
Caption: the produce-and-attach flow (top) and the discover flow (bottom). The insight: the image and its provenance are pushed independently but linked by the subject digest at the registry, so provenance is co-located with the artifact it describes and travels with it across registry copies.
Worked Example — Producing Provenance in Two Common Pipelines
GitHub Actions: actions/attest-build-provenance
permissions: id-token: write # mint an OIDC token = the builder identity contents: read attestations: write # write to the GH attestations APIjobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build and push image id: push run: | docker build -t $REG/app:$GITHUB_SHA . docker push $REG/app:$GITHUB_SHA - name: Attest build provenance uses: actions/attest-build-provenance@v1 with: subject-name: ${{ env.REG }}/app subject-digest: sha256:abc123… # the pushed image digest push-to-registry: true # attach as an OCI referrer
Commentary.id-token: write is load-bearing: it lets the job mint an OIDC token that is the builder identity — no stored signing key. The attest-build-provenance action generates a SLSA build-provenance predicate in in-toto format, then signs it keyless via Sigstore: it exchanges the OIDC token for a short-lived Sigstore-issued (Fulcio) certificate, signs the DSSE envelope with the ephemeral key, and records the signing event in the Rekor transparency log (actions/attest-build-provenance). subject-name + subject-digest bind the statement to the pushed image; push-to-registry: true attaches the attestation to the image as an OCI referrer (otherwise it lands only in GitHub’s attestations API). Public repos use the public-good Sigstore; private repos use GitHub’s private Sigstore instance. The provenance records the workflow, repo, and commit as its source/externalParameters and the GitHub-hosted runner as the builder.id.
Cosign: cosign attest
# Produce a provenance predicate however (e.g. from the build system), then:cosign attest --predicate provenance.json \ --type slsaprovenance \ --yes \ $REG/app@sha256:abc123…
Commentary.cosign attest wraps the predicate in an in-toto Statement, signs it in a DSSE envelope, and pushes it to the registry as an attached artifact referring to the image digest (Sigstore, Verifying attestations). With no --key, cosign signs keyless — Fulcio cert from an OIDC identity, Rekor for transparency. --type slsaprovenance sets the predicate type. The counterpart cosign verify-attestation (verification) belongs to Build Provenance and Verifiable Builds, not here.
Common Misunderstandings and Failure Modes
“The attestation is inside the image.” It is not — it is a separate referrer manifest linked by subject digest. Pulling the image does not automatically pull provenance; you must query the referrers API. Symptom: “I pulled the image, where’s the provenance?” Fix:GET /v2/<name>/referrers/<digest>.
Trusting the envelope’s media type as the predicate type. The DSSE payloadType (application/vnd.in-toto+json) does not tell you it is SLSA provenance; you must decode the payload after verifying the signature and read the Statement’s predicateType. Trusting the outer type is a type-confusion foot-gun the spec explicitly warns against.
Subject digest mismatch. If the provenance’s subject.digest does not equal the digest you actually deployed, the attestation is about some other artifact. This is the single most important check and the reason promotion must carry the digest faithfully — provenance is bound to bytes, not tags.
push-to-registry: false / registry lacks referrers support. The attestation exists (in GitHub’s API) but is not discoverable from the registry, so a cluster admission check querying the registry finds nothing. Fix: push to registry, and ensure the registry supports distribution-spec v1.1 (or the client falls back to the tag schema).
Confusing provenance with a signature or SBOM. All three are referrers attached the same way, but they answer different questions: a signature (Image Signing with Sigstore) says “someone vouched for these bytes,” an SBOM lists what is inside, and provenance says how it was built. A hardened supply chain attaches all three.
The unifying idea: they are all in-toto attestations attached as OCI referrers; only the predicate schema differs. This note’s machinery — Statement + DSSE + referrer push — is identical across the whole family; the SLSA-provenance predicate is simply the one that records the build.
Production Notes
The reference production pattern is now well-established: a hosted builder (GitHub Actions with attest-build-provenance, Google Cloud Build, GitLab) generates SLSA provenance automatically, signs it keyless through Sigstore (Fulcio short-lived certs anchored in the build’s OIDC identity, transparency in Rekor), and attaches it to the pushed image as an OCI referrer. Because signing is keyless and identity-based, there is no long-lived signing key to leak — the builder’s OIDC identity is the root of trust, which is exactly why id-token: write and OIDC federation (OIDC and Secretless Pipeline Authentication) underpin the whole scheme. The consumption side — gh attestation verify, cosign verify-attestation, admission controllers, SLSA level policy — is deliberately out of scope here and lives in DevSecOps and Supply Chain Security MOC (Build Provenance and Verifiable Builds, The SLSA Framework).
The framework itself is moving: SLSA v1.2 is the current, “Approved” version, organized into a Build track and a Source track (SLSA v1.2 about); the older v1.0 provenance page is now marked “Retired,” though the predicate type URI .../provenance/v1 is intentionally stable across minor versions so existing attestations keep validating. Point-in-time note: these facts are as of 2026-07 — SLSA version status and the exact provenance schema should be re-pinned when this note is next revised.