Artifact Attestations, SBOM Provenance, Verification, and SLSA-Oriented Builds: Guided Hands-On Workflow
Build a tiny artifact, generate provenance and SBOM attestations in a disposable public repository, verify them, tamper with the bytes, and preserve failure evidence.
Learning objectives
- Create a disposable public repository whose mandatory path uses only free GitHub-hosted resources and fake data.
- Build one deterministic text artifact, calculate its digest, create provenance and SPDX SBOM evidence, and record run identity.
- Generate live GitHub attestations with full-SHA-pinned first-party actions when public-repository attestations are available.
- Verify the exact artifact, then modify one byte and preserve the expected verification failure.
- Complete an offline/local simulation when live attestation generation is unavailable.
1. Lab scenario: a tiny releasable artifact with two claims
Create a disposable public repository named
gha-attestation-lab. The “application” is intentionally
tiny: a source text file is converted into
dist/hello.txt. The workflow records SHA-256, generates
a minimal SPDX 2.3 JSON SBOM tied to that digest, creates one
provenance attestation and one SBOM attestation, and prints only
attestation identifiers/URLs—not OIDC tokens or credentials.
Public repositories are the mandatory live path because artifact attestations are available there on current GitHub plans. If your lab must remain private and you do not have GitHub Enterprise Cloud, use the faithful local simulation in Section 11; it teaches digest binding and policy evaluation without pretending to create a GitHub/Sigstore signature.
2. Preflight and exact assumptions (verified 2026-09-10)
| Item | Lab assumption | Evidence to retain |
|---|---|---|
| Repository | Disposable public repository | URL + visibility screenshot/text note. |
| Runner | ubuntu-24.04 |
Set-up-job image metadata. |
| Checkout |
actions/checkout v7.0.1 @
3d3c42e5aac5ba805825da76410c181273ba90b1
|
Workflow manifest. |
| Attest |
actions/attest v4.2.2 @
1e69f48acb82d1966a394da916b4c1698aa569d6
|
Workflow manifest + release mapping. |
| Permissions | contents read; id-token/attestations/artifact-metadata write | Workflow YAML; no write-all. |
| Secrets | None | No secret references. |
| CLI |
Record installed gh --version before
verification
|
Evidence packet; syntax verified against current CLI manual. |
3. Create deterministic source and build script
The build script converts source to a normalized artifact. Keeping the transformation simple makes the digest relationship easy to reason about. The build also writes a checksum file as independent byte-identity evidence.
mkdir -p src scripts
printf 'hello from attested build
' > src/message.txt
cat > scripts/build.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
rm -rf dist
mkdir -p dist
tr '[:lower:]' '[:upper:]' < src/message.txt > dist/hello.txt
sha256sum dist/hello.txt > dist/hello.txt.sha256
EOF
chmod +x scripts/build.sh
4. Workflow: build first, then attest the exact bytes
The job requests only the scopes needed for source checkout and
attestation generation. It does not publish a package or deploy
anything. Two invocations of actions/attest create two
distinct predicate types for the same subject: default SLSA
provenance and SPDX SBOM.
name: Attested tiny build
on:
workflow_dispatch:
permissions:
contents: read
id-token: write
attestations: write
artifact-metadata: write
jobs:
build-attest:
runs-on: ubuntu-24.04
steps:
- name: Checkout exact workflow source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build and record identity
shell: bash
run: |
./scripts/build.sh
printf 'run_id=%s\nrun_attempt=%s\nsource_sha=%s\n' \
"$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$GITHUB_SHA"
cat dist/hello.txt.sha256
python3 --version
gh --version | head -1
- name: Generate minimal SPDX SBOM for the exact file
shell: bash
run: |
python3 - <<'PY'
import hashlib, json, os, datetime
p='dist/hello.txt'
digest=hashlib.sha256(open(p,'rb').read()).hexdigest()
doc={
'spdxVersion':'SPDX-2.3',
'dataLicense':'CC0-1.0',
'SPDXID':'SPDXRef-DOCUMENT',
'name':'gha-attestation-lab',
'documentNamespace':f'https://example.invalid/spdx/{os.environ["GITHUB_SHA"]}',
'creationInfo':{
'created':datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
'creators':['Tool: gha-attestation-course-lab']
},
'files':[{
'fileName':'dist/hello.txt',
'SPDXID':'SPDXRef-File-hello',
'checksums':[{'algorithm':'SHA256','checksumValue':digest}],
'licenseConcluded':'NOASSERTION',
'copyrightText':'NOASSERTION'
}]
}
open('dist/sbom.spdx.json','w').write(json.dumps(doc,indent=2)+'\n')
PY
- name: Attest build provenance
id: provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/hello.txt
- name: Attest SPDX SBOM
id: sbom
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/hello.txt
sbom-path: dist/sbom.spdx.json
- name: Record non-secret attestation evidence
shell: bash
env:
PROV_ID: ${{ steps.provenance.outputs.attestation-id }}
PROV_URL: ${{ steps.provenance.outputs.attestation-url }}
SBOM_ID: ${{ steps.sbom.outputs.attestation-id }}
SBOM_URL: ${{ steps.sbom.outputs.attestation-url }}
run: |
printf 'provenance_id=%s\nprovenance_url=%s\n' "$PROV_ID" "$PROV_URL"
printf 'sbom_id=%s\nsbom_url=%s\n' "$SBOM_ID" "$SBOM_URL"
5. Run once and freeze the evidence before testing failures
Dispatch the workflow. Before doing any tamper experiment, save the
original run ID, run attempt, source SHA, workflow YAML, runner
image metadata, checksum, provenance attestation ID/URL and SBOM
attestation ID/URL. Download/copy dist/hello.txt from
the same source revision locally by running
./scripts/build.sh; compare its checksum with the build
log.
The critical state is the digest. The attestation subject must match the exact bytes you verify. A same-name file from another build is not automatically the same subject.
6. Verify provenance with an explicit expected identity
Authenticate the GitHub CLI to your disposable repository, record
gh --version, then verify the artifact. Replace the
placeholders with the actual owner/repository. The signer-workflow
restriction makes the trust decision stronger than owner-only
verification.
gh --version
gh attestation verify dist/hello.txt --repo YOUR_OWNER/gha-attestation-lab --signer-workflow YOUR_OWNER/gha-attestation-lab/.github/workflows/attested-build.yml
7. Verify that an SPDX predicate also exists for the same subject
The default verifier expects SLSA provenance. To select the SPDX SBOM predicate, specify its predicate type. Verification proves that the signed SBOM claim is attached to the exact subject digest under the required identity; it does not independently prove the inventory is complete.
gh attestation verify dist/hello.txt --repo YOUR_OWNER/gha-attestation-lab --predicate-type https://spdx.dev/Document/v2.3
8. Negative test A: alter one byte and preserve the failure
Copy the valid artifact before changing it. Then append one byte to the test copy. Keep the original verification output and the tampered verification output side by side. Do not overwrite the original evidence.
cp dist/hello.txt dist/hello.valid.txt
cp dist/hello.txt dist/hello.tampered.txt
printf 'X' >> dist/hello.tampered.txt
sha256sum dist/hello.valid.txt dist/hello.tampered.txt
gh attestation verify dist/hello.tampered.txt --repo YOUR_OWNER/gha-attestation-lab || true
Expected observation: the digest changes and no matching valid attestation verifies for the modified subject. The failure is evidence that verification is bound to bytes rather than file name.
9. Negative test B: require the wrong signer identity
Use the original valid bytes but deliberately require a signer workflow that did not create the attestation. This proves that “artifact bytes are unchanged” and “builder identity is approved” are independent checks.
gh attestation verify dist/hello.valid.txt --repo YOUR_OWNER/gha-attestation-lab --signer-workflow YOUR_OWNER/gha-attestation-lab/.github/workflows/not-the-builder.yml || true
Expected observation: identity-constrained verification fails even though the artifact digest itself has not changed.
10. Evidence packet
| Evidence | Capture |
|---|---|
| Event/source |
workflow_dispatch, source ref and exact
GITHUB_SHA.
|
| Run | Run ID + attempt + job/step conclusions. |
| Runner/tooling |
Runner image details, Python version, actual
gh --version.
|
| Action manifest | Checkout and attest full SHAs plus release mappings. |
| Subject | Artifact filename and SHA-256. |
| Provenance | Attestation ID/URL + successful identity-constrained verify output. |
| SBOM | SPDX file + SBOM attestation ID/URL + predicate-type verify output. |
| Negative evidence | Tampered digest + failure; wrong-signer failure. |
| Limitations | No release/package/deployment; public disposable repo; toy SBOM. |
11. Faithful local fallback when live attestations are unavailable
If repository visibility/plan or connectivity prevents live attestation generation, do not fabricate a Sigstore success. Instead simulate the data model locally: compute the subject digest, create a JSON statement with source/workflow identity, and write a policy checker that requires exact digest/repository/workflow/ref. Mark the result UNSIGNED SIMULATION. This teaches policy mechanics but does not claim cryptographic provenance.
import hashlib, json
artifact = open('dist/hello.txt','rb').read()
digest = hashlib.sha256(artifact).hexdigest()
statement = {
"simulation": True,
"subject": {"name":"hello.txt", "sha256":digest},
"builder": {
"repository":"YOUR_OWNER/gha-attestation-lab",
"workflow":".github/workflows/attested-build.yml",
"ref":"refs/heads/main"
}
}
open('dist/provenance.simulation.json','w').write(json.dumps(statement,indent=2))
assert statement['subject']['sha256'] == hashlib.sha256(artifact).hexdigest()
assert statement['builder']['workflow'] == '.github/workflows/attested-build.yml'
print('UNSIGNED SIMULATION: digest + policy matched')
12. Cleanup and rollback
Keep the evidence packet long enough to finish the lesson, then delete the disposable repository if desired. Deleting a workflow run, artifact or attestation is a separate GitHub state mutation and should not be used merely to make a failed experiment disappear. Preserve first-failure evidence first.
No cloud credential, registry login, package publication or production infrastructure is created by this lab.
13. Challenge: choose the missing layer
A team verifies SHA-256 but cannot prove which workflow produced the release. Another team verifies provenance but cannot answer which components are inside the artifact. A third team verifies both but accepts any workflow from the organization. Identify the missing control for each: provenance, SBOM, or identity-constrained verification policy.
14. Lesson summary
You have created two signed claims for the same subject, verified positive identity/digest cases, forced two independent negative cases, and preserved enough evidence to explain each result. The next lesson turns these mechanics into design choices.
Knowledge check
Why does the lab create the artifact before calling
actions/attest?
The attestation must bind the exact final subject bytes. Building again afterward can create a different digest.
What does the tamper test prove?
That verification is digest-bound: changing the bytes breaks the subject match even if the filename stays the same.
Why run a wrong-signer test against the original valid file?
It isolates identity policy from byte integrity and proves both checks are necessary.
If the local fallback passes, have you created GitHub artifact provenance?
No. It is explicitly unsigned simulation of digest/policy mechanics, not a Sigstore/GitHub attestation.
Why does the SBOM verification not prove the SBOM is complete?
The signature authenticates the claim and its association with the subject; workflow logic can still generate incomplete or incorrect predicate content.
Official references and version notes
- Artifact attestations concept — GitHub model for Sigstore-backed attestations, public/private signing roots and verification policy.
- Use artifact attestations — Current generation, permissions, binary/container and verification guidance.
- actions/attest v4.2.2 — Current consolidated GitHub action for provenance, SBOM and custom attestations.
- GitHub CLI attestation verify — Current identity, predicate, source-ref and signer-workflow verification controls.
- Verify attestations offline — Bundle and trusted-root workflow for disconnected verification.
- SLSA-oriented GitHub guidance — GitHub guidance connecting attestations and reusable workflows to stronger SLSA-oriented build controls.
- Workflow permissions — Current id-token, attestations, artifact-metadata and contents permission syntax.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.