---
key: "slsa-provenance"
title: "Read SLSA provenance and verify the claim you need"
url: "https://softwaresecurity.com/domains/supply-chain/provenance/read-and-verify-slsa/"
api: "https://softwaresecurity.com/api/v1/articles/slsa-provenance.json"
domain: "supply-chain"
topic: "Software supply chain"
type: "Guide"
identifiers: ["CWE-494","A03:2025","A08:2025","CAPEC-444"]
tags: ["Build integrity and provenance"]
stages: ["release"]
roles: ["developer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Read SLSA provenance and verify the claim you need

Connect an artifact to its build evidence while distinguishing provenance, build assurance and source controls.

Canonical URL: https://softwaresecurity.com/domains/supply-chain/provenance/read-and-verify-slsa/

## In short

Authenticate the provenance envelope, then require every predicate: `predicateType` is `https://slsa.dev/provenance/v1`, a subject with your artifact name has its exact digest, `builder.id` is on your allowlist, `buildType` is the type you expect, and `externalParameters` equal your expected values with no extra keys. Then check the resolved source commit against the approval. A trusted builder with unexpected parameters can still build something you never approved.

- SLSA v1.2 says verifiers should check `buildType` and `externalParameters` against expected values, not only the builder and subject.
- `externalParameters` must be complete at Build L3 and are untrusted; verifiers should reject unrecognized fields.
- `resolvedDependencies` has no completeness requirement; use it for the resolved commit only when your build type defines that entry.
- Valid provenance is evidence about production, not about review, tests or vulnerabilities.
- `gh attestation verify` and `slsa-verifier` check signer and source identity; compare the remaining predicate fields in your own policy.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| `predicateType` | `https://slsa.dev/provenance/v1` | — | SLSA 1.2: verifying artifacts |
| `externalParameters` | Exact match with the expected object; reject unknown keys | Stricter than SLSA's minimum, which lets an ecosystem ignore an approved list of parameters. | SLSA 1.2: build provenance v1 fields |
| GitHub Actions `buildType` | `https://actions.github.io/buildtypes/workflow/v1` | Its `externalParameters.workflow` holds `ref`, `repository` and `path`. | GitHub Actions workflow build type v1 |
| `gh attestation verify` identity flags | `--repo` plus `--signer-workflow`, `--source-ref` and `--deny-self-hosted-runners` | — | GitHub CLI manual: gh attestation verify |

## Verify it

1. Run the PR4 policy against S1 to S4 and confirm that only S1 passes and S4 fails on `externalParameters`.
2. Run `gh attestation verify <file> --repo <owner>/<repo> --signer-workflow <owner>/<repo>/.github/workflows/<file>.yml --source-ref <ref>` and expect exit code 0; change the workflow path and expect exit code 1.
3. Print `buildType` and `externalParameters` with `--format json --jq` and compare both with the values your release expects.
4. For SLSA GitHub generator provenance, run `slsa-verifier verify-artifact` with `--source-uri` and `--source-tag` and expect PASSED.
5. Retain the bundle, verifier output and policy revision under the release record.

## What you will learn

- Interpret subject, predicate, builder, source and invocation without conflating them.
- Apply independent provenance predicates to near-miss statements.
- State the limits of an authenticated production claim.

Audience: Engineers who generate or verify build provenance and write the policies that consume it

Before you start:

- Distinguish an artifact digest, build invocation, source revision and trusted builder identity.

Time: About 28 minutes: 6 to read and 22 for the exercise

## Ask a question that provenance can answer

Provenance is evidence about production. It can help answer whether the artifact came from an allowed builder, source and set of declared parameters. It cannot by itself show that the source was reviewed, the tests were meaningful, or the software has no vulnerabilities. Those are separate claims that need appropriate evidence. SLSA 1.2 has a Build track and a Source track; the Source track levels (L1 version controlled, L2 history and provenance, L3 continuous technical controls, L4 two-party review) describe guarantees that build provenance does not prove. This guide examines the build provenance v1 predicate rather than treating every attestation as interchangeable. Keep SolarWinds in mind (/domains/supply-chain/builds/case-study-solarwinds/): the release was signed and the repository clean, but the build server compiled altered source, which only provenance from a hardened builder or an independent rebuild can expose.

Start with a consumer decision: Atlas accepts a release only if its authenticated build provenance names an allowed builder, the approved source commit and the requested artifact digest. The build platform, rather than arbitrary build steps, should supply security-relevant provenance values according to its claimed guarantees. If an attacker can edit the statement after building and sign with an accepted identity, the statement describes an attacker-controlled story. Trust in the producer and the mechanism is part of the verification argument.

## Read the subject, build definition and run details

The in-toto statement's `subject` identifies output artifacts by digest. The `predicateType` selects the meaning of the predicate. Within build provenance v1, `buildDefinition` records the build type, parameters and resolved dependencies, while `runDetails` identifies the builder and execution. A URI or version label in resolved dependencies is not enough when policy requires an exact source revision; record and compare the digest using the appropriate identifier type.

The example below uses deliberately fictional but syntactically complete values. It is a minimal statement, not an authenticated envelope. The `buildType` URI denotes Atlas’s own contract: the caller chooses a repository and ref, which the builder reports as `externalParameters`, and the builder records the commit it resolved under `resolvedDependencies`. GitHub’s workflow build type follows the same pattern with `externalParameters.workflow` (`ref`, `repository`, `path`). SLSA treats external parameters as untrusted input that must be complete at Build L3 and verified downstream, so a verifier compares them with expected values and should reject unrecognized keys; the policy here requires an exact match.

```json
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "atlas.tar",
      "digest": {
        "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
      }
    }
  ],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://build.example/atlas/v1",
      "externalParameters": {
        "repository": "https://code.example/atlas",
        "ref": "refs/heads/main"
      },
      "resolvedDependencies": [
        {
          "uri": "git+https://code.example/atlas@refs/heads/main",
          "digest": {
            "gitCommit": "1111111111111111111111111111111111111111"
          }
        }
      ]
    },
    "runDetails": {
      "builder": {
        "id": "https://build.example/workers/atlas-v1"
      },
      "metadata": {
        "invocationId": "https://build.example/runs/82"
      }
    }
  }
}
```

## Evaluate the trusted statement against separate predicates

A policy first verifies the statement’s authentication and expected predicate type. It then compares the subject digest to the bytes being promoted, the builder ID to an allowlist, `buildType` to the one type whose parameters it knows how to interpret, and `externalParameters` to the exact expected values, rejecting extra keys. Only then does it compare the resolved source commit to the approval record. A known builder used with unexpected parameters, such as another ref, workflow path or injected build flag, can still produce an unauthorized artifact; SLSA's verification guidance calls for these checks for that reason. Extra subjects or dependencies require defined handling; a policy that checks only the first array element may ignore the one actually being deployed. The lab at /labs/artifact-policy/ repairs a gate that accepts a valid signature from the wrong producer.

The exercise supplies four statements that each differ from S1 in one place. A second statement with the right source and wrong subject cannot authorize the requested artifact. A third with the right bytes but a different builder fails the expected production process. A fourth adds one external parameter and fails even though builder, source and bytes match, because its build was influenced by an input no one approved. These checks are not redundant: a rebuild may yield the same bytes through a less trusted system, while a trusted builder may legitimately build an unapproved change. Treat missing required evidence as unresolved or denied according to policy, never infer an approved value from a familiar filename.

```python
"""Policy PR4 for Atlas. Input: the statement from an envelope a trusted verifier authenticated."""

EXTERNAL = {"repository": "https://code.example/atlas", "ref": "refs/heads/main"}
SOURCE_URI = "git+https://code.example/atlas@refs/heads/main"


def obj(value):
    return value if isinstance(value, dict) else {}


def arr(value):
    return value if isinstance(value, list) else []


def failures(statement, name, sha256):
    """Return every failed predicate; an empty list means the statement satisfies PR4."""
    statement, reasons = obj(statement), []
    if statement.get("_type") != "https://in-toto.io/Statement/v1":
        reasons.append("statement type")
    if statement.get("predicateType") != "https://slsa.dev/provenance/v1":
        reasons.append("predicateType")
    named = [obj(s) for s in arr(statement.get("subject")) if obj(s).get("name") == name]
    if len(named) != 1 or obj(named[0].get("digest")).get("sha256") != sha256:
        reasons.append("subject digest")
    build = obj(obj(statement.get("predicate")).get("buildDefinition"))
    run = obj(obj(statement.get("predicate")).get("runDetails"))
    if obj(run.get("builder")).get("id") != "https://build.example/workers/atlas-v1":
        reasons.append("builder.id")
    if build.get("buildType") != "https://build.example/atlas/v1":
        reasons.append("buildType")
    # Exact equality also rejects missing, changed and unrecognized external parameters.
    if build.get("externalParameters") != EXTERNAL:
        reasons.append("externalParameters")
    deps = arr(build.get("resolvedDependencies"))
    source = [obj(d) for d in deps if obj(d).get("uri") == SOURCE_URI]
    if len(source) != 1 or obj(source[0].get("digest")).get("gitCommit") != "1" * 40:
        reasons.append("source commit")
    return reasons
```

## Run the same checks with real verifiers

For artifacts attested with `actions/attest` on GitHub, `gh attestation verify` authenticates the Sigstore bundle and checks the certificate identity: `--repo`, `--signer-workflow`, `--source-ref` and `--deny-self-hosted-runners` correspond to source, builder and runner predicates. The certificate fields come from the GitHub OIDC token and cannot be changed by the workflow, but the predicate can, so print `buildType` and `externalParameters` and compare them in your own policy. For provenance from the SLSA GitHub generator, `slsa-verifier` checks the builder, source repository and tag. The commands below run against public releases of the GitHub CLI and slsa-verifier itself.

```bash
# GitHub artifact attestations: pin the signer workflow, source ref and runner type,
# then print buildType and externalParameters for your own exact comparison.
gh release download v2.94.0 -R cli/cli -p 'gh_2.94.0_linux_amd64.tar.gz'
gh attestation verify gh_2.94.0_linux_amd64.tar.gz --repo cli/cli \
  --signer-workflow cli/cli/.github/workflows/deployment.yml \
  --source-ref refs/heads/trunk --deny-self-hosted-runners --format json \
  --jq '.[].verificationResult.statement.predicate.buildDefinition | {buildType, externalParameters}'

# SLSA GitHub generator provenance: slsa-verifier checks builder, source and tag.
gh release download v2.7.1 -R slsa-framework/slsa-verifier \
  -p 'slsa-verifier-linux-amd64' -p 'slsa-verifier-linux-amd64.intoto.jsonl'
slsa-verifier verify-artifact slsa-verifier-linux-amd64 \
  --provenance-path slsa-verifier-linux-amd64.intoto.jsonl \
  --source-uri github.com/slsa-framework/slsa-verifier --source-tag v2.7.1
```

## Keep the decision reproducible and its claim bounded

Retain the authenticated envelope or bundle, the artifact digest, full parsed statement, verification output, policy revision, approval reference and rejection reasons. Store these together under a stable release record. A link to a build UI may expire or change access; record the actual invocation identifier and preserve relevant evidence under your retention policy. Sensitive parameters or private repository URLs may need controlled access rather than omission from all internal evidence.

Provenance generation and verification introduce storage, compatibility and outage costs. Decide which predicates are hard release requirements and which are investigative metadata. Additional fields are useful only if their producer and semantics are understood. In particular, do not label a self-generated statement SLSA level 3 without assessing the required build-platform properties. The release-verification project on this page applies the same checks, including `buildType` and `externalParameters`, to a signed release; this desk exercise teaches the narrower skill of reading and applying a provenance claim without awarding a maturity level.

## Guided practice

Atlas has approved one source commit for one artifact. Treat cryptographic authentication as a supplied fact, then apply the consumer policy to four statements.

### Supplied material

```text
Policy PR4: authenticated statement; predicateType https://slsa.dev/provenance/v1; subject sha256 = 64 a characters; builder https://build.example/workers/atlas-v1; buildType https://build.example/atlas/v1; externalParameters exactly {repository: https://code.example/atlas, ref: refs/heads/main} with no other keys; resolved source git+https://code.example/atlas@refs/heads/main with gitCommit = 40 ones.
S1: exactly the statement printed above; authentication accepted by trusted verifier V2.
S2: S1 with subject sha256 = 64 b characters; authentication accepted.
S3: S1 with builder.id = https://build.example/workers/general; authentication accepted.
S4: S1 with one more externalParameters key, buildFlags = --allow-unsigned-plugins; authentication accepted.
Approval A82 binds the expected source and subject. Production request uses atlas.tar with sha256 = 64 a characters.
No source review attestation, application test results or builder-level assessment are supplied.
Fictional decision owner: release engineer Dev.
```

### 1. Read the four identities

Locate artifact, requested source, resolved source, builder and invocation in S1 and state their separate roles.

Worked answer: `subject.digest.sha256` identifies output bytes; `externalParameters.repository` and `ref` name the source the caller requested; the `resolvedDependencies` `gitCommit` identifies the revision actually built; `builder.id` identifies the production system; `invocationId` identifies one run. The filename `atlas.tar` is not the artifact integrity check.

### 2. Apply all predicates

Decide S1, S2, S3 and S4 under PR4.

Worked answer: S1 satisfies supplied authentication and all policy fields. S2 fails subject: its statement concerns different bytes. S3 fails builder even though source and output match. S4 fails `externalParameters`: the extra key is an unapproved external input, so reject it even though builder, source and bytes match.

### 3. Test a missing-source variation

Remove `resolvedDependencies` from S1 and decide whether the matching filename permits promotion.

Worked answer: Deny or hold for missing required source evidence under PR4. A familiar filename does not establish the approved commit, and absence is not permission to skip the condition.

### 4. Constrain the conclusion

Write two claims that S1 supports in the fixture and two it cannot establish.

Worked answer: It supports the allowed builder/source assertion and subject binding after stipulated authentication. It does not establish application test success or a SLSA build level, because no such evidence is supplied.

### 5. Prepare the retention record

Name exact fields for decision PROV-82 and identify what must remain pending.

Worked answer: PROV-82 joins A82, S1 envelope/reference, V2 result, PR4, full subject digest, builder, invocation run 82, owner Dev. Real authentication and artifact hashing remain pending outside this desk model; source review and platform-level assurance are separate questions.

Deliverable: A provenance field map, four policy outcomes and a bounded evidence-retention record.

Scope: The statement is unsigned teaching data. Matching its fields cannot prove its truth, a SLSA level or application safety.

## Executable practice

- [Build and verify a signed software release: project ZIP](https://softwaresecurity.com/downloads/projects/release-verification-bd914fcbede61c32.zip) — Node.js 22+ · no dependencies

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [SLSA 1.2 specification](https://slsa.dev/spec/v1.2/). Approved, November 2025.
- [SLSA 1.2 provenance specification](https://slsa.dev/spec/v1.2/provenance)
- [Sigstore: verifying signatures and attestations](https://docs.sigstore.dev/cosign/verifying/verify/)
- [SLSA 1.2: build provenance v1 fields](https://slsa.dev/spec/v1.2/build-provenance)
- [SLSA 1.2: verifying artifacts](https://slsa.dev/spec/v1.2/verifying-artifacts)
- [SLSA 1.2: source track requirements](https://slsa.dev/spec/v1.2/source-requirements)
- [GitHub Actions workflow build type v1](https://github.com/actions/buildtypes/tree/main/workflow/v1)
- [GitHub CLI manual: gh attestation verify](https://cli.github.com/manual/gh_attestation_verify)
- [slsa-verifier](https://github.com/slsa-framework/slsa-verifier)
- [OWASP Top 10 CI/CD Security Risks: CICD-SEC-9 Improper Artifact Integrity Validation](https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-09-Improper-Artifact-Integrity-Validation). v1.0 (2022).