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
buildTypeandexternalParametersagainst expected values, not only the builder and subject. externalParametersmust be complete at Build L3 and are untrusted; verifiers should reject unrecognized fields.resolvedDependencieshas 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 verifyandslsa-verifiercheck signer and source identity; compare the remaining predicate fields in your own policy.
| 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 |
- Run the PR4 policy against S1 to S4 and confirm that only S1 passes and S4 fails on
externalParameters. - 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. - Print
buildTypeandexternalParameterswith--format json --jqand compare both with the values your release expects. - For SLSA GitHub generator provenance, run
slsa-verifier verify-artifactwith--source-uriand--source-tagand expect PASSED. - 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 prerequisites
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 practiceFrom build to a verified release decision
- Approved sourceThe build checks out the approved commit; provenance records its digest where the build type says, usually in
resolvedDependencies. - Build platformThe platform, not the build steps, writes and signs provenance: subject digest, build type, parameters and builder ID.
- DistributionThe artifact and its signed in-toto statement reach the consumer through registries and channels it does not trust.
- 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 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.
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.
{
"_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"
}
}
}
}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 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 reasonsFor 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.
# 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.1Retain 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
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.
Expected result and reasoning
subject.digest.sha256identifies output bytes;externalParameters.repositoryandrefname the source the caller requested; theresolvedDependenciesgitCommitidentifies the revision actually built;builder.ididentifies the production system;invocationIdidentifies one run. The filenameatlas.taris not the artifact integrity check.2. Apply all predicates
Decide S1, S2, S3 and S4 under PR4.
Decide each statement under PR4
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. Test a missing-source variation
Remove
resolvedDependenciesfrom 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. 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. 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.
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.
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:
node build.mjs
node --test exercise.test.mjs20 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.
- SLSA 1.2 specification. Approved, November 2025.
- SLSA 1.2 provenance specification
- Sigstore: verifying signatures and attestations
- SLSA 1.2: build provenance v1 fields
- SLSA 1.2: verifying artifacts
- SLSA 1.2: source track requirements
- GitHub Actions workflow build type v1
- GitHub CLI manual: gh attestation verify
- slsa-verifier
- OWASP Top 10 CI/CD Security Risks: CICD-SEC-9 Improper Artifact Integrity Validation. v1.0 (2022).
Reference information
- SDLC phase
- Release
- Role
- Developers
- CAPEC attack patterns