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.

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.
Applies to
  • SLSA v1.2 (approved), build provenance predicate https://slsa.dev/provenance/v1
  • GitHub CLI 2.94.0 (gh attestation verify flags shown)
  • slsa-verifier v2.7.1

Reading and enforcing provenance claims. Provenance format validity is not a SLSA level assessment.

Part of
Learning path: From source to trusted release (step 11 of 12). Workflow: Build an AppSec program
Learning goals and prerequisitesAbout 28 minutes: 6 to read and 22 for the exercise

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.

Before you start

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

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

Refresh first: Verify artifact signatures against an explicit trust policy

Go to the guided practice

From build to a verified release decision

ProducerDistributionConsumerSource repositoryBuild platformPackage registryRelease policyApproved commit,recorded by digest1Build, then signthe provenance2atlas.tar and itssigned statement3Artifact and statement3Verify thesignature andpredicate type4Compare digest,builder ID andbuild type4Match parametersand source committo approval4Trust boundaryUntrusted input1Step, described below
  1. Approved sourceThe build checks out the approved commit; provenance records its digest where the build type says, usually in resolvedDependencies.
  2. Build platformThe platform, not the build steps, writes and signs provenance: subject digest, build type, parameters and builder ID.
  3. DistributionThe artifact and its signed in-toto statement reach the consumer through registries and channels it does not trust.
  4. Consumer policyVerify the signature and predicate type, then compare the subject digest, builder ID, build type, external parameters and resolved source commit with the expected values and the approval record.
Provenance answers where and how an artifact was built. It does not show that the source was reviewed or that the software is free of vulnerabilities; those claims need separate evidence.

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.

policy_pr4.pyPython
"""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.

Testverify-provenance.shShell
# 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 scenario material
Plain 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. 1. Read the four identities

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

    Expected result and reasoning

    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. 2. Apply all predicates

    Decide S1, S2, S3 and S4 under PR4.

    Decide each statement under PR4

    S1
    S2
    S3
    S4

    Expected result and reasoning

    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. 3. Test a missing-source variation

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

    Expected result and reasoning

    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. 4. Constrain the conclusion

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

    Expected result and reasoning

    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. 5. Prepare the retention record

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

    Expected result and reasoning

    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.

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

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

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Run a local project

Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.

Node.js 22+ · no dependencies · 60–90 min

Build and verify a signed software release

Build a real executable, generate an SBOM and DSSE-signed in-toto provenance, then repair the consuming gate so a valid signature cannot approve the wrong source, builder or release.

In the extracted folder, run:

Shell
node build.mjs
node --test exercise.test.mjs

20 regression tests · The starter intentionally fails some cases.

Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.

Actual Ed25519 signatures over the DSSE pre-authentication encoding, checked against the DSSE test vector, and executable output; there is no certificate or transparency log, the local producer and consumer are not isolated, and this does not establish a SLSA level.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. SLSA 1.2 specification. Approved, November 2025.
  2. SLSA 1.2 provenance specification
  3. Sigstore: verifying signatures and attestations
  4. SLSA 1.2: build provenance v1 fields
  5. SLSA 1.2: verifying artifacts
  6. SLSA 1.2: source track requirements
  7. GitHub Actions workflow build type v1
  8. GitHub CLI manual: gh attestation verify
  9. slsa-verifier
  10. OWASP Top 10 CI/CD Security Risks: CICD-SEC-9 Improper Artifact Integrity Validation. v1.0 (2022).

Reference information

SDLC phase
Release
CAPEC attack patterns

Download this guide as Markdown