Artifact Attestations, SBOMs, Build Provenance, Verification, and SLSA-Oriented Workflows: Guided Hands-On Workflow and Core Operations
You will create a disposable public repository, build a deterministic artifact, compute its digest, generate both build-provenance and SPDX SBOM attestations, download the exact artifact, and verify it with the GitHub CLI. Every verification step constrains the expected repository, signer workflow, and source ref so cryptographic success is connected to an explicit trust policy.
Learning objectives
- Create a disposable public repository and prove repository/ref/tool state before enabling an attestation-producing workflow.
- Build deterministic bytes, calculate SHA-256, and generate build-provenance plus SPDX SBOM attestations with explicit least-privilege permissions.
- Download the exact Actions artifact and independently verify its digest and GitHub attestation with repository/workflow/ref constraints.
- Inspect provenance and SBOM predicates without confusing user-controlled predicate data with certificate-backed workflow identity.
- Use the repository attestations REST API as metadata evidence while understanding that API listing is not a substitute for cryptographic verification.
1. Preflight: inspect before creating state
gh --version
gh auth status
gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq '{login,id}'
gh attestation verify --help | sed -n '1,80p'
OWNER=$(gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq .login)
REPO="$OWNER/gh-attestation-ch25-lab"
printf 'Target repository: %s\n' "$REPO"
If gh attestation is missing, update GitHub CLI using
its official installation channel before continuing. Do not
substitute a random signing extension: this chapter is specifically
about the GitHub attestation trust model.
2. Create the disposable repository and capture source identity
gh repo create "$REPO" --public --add-readme \
--description "Disposable Chapter 25 provenance lab"
gh repo clone "$REPO" ch25-lab
cd ch25-lab
DEFAULT_BRANCH=$(gh repo view "$REPO" --json defaultBranchRef --jq .defaultBranchRef.name)
SOURCE_SHA=$(git rev-parse HEAD)
printf 'default_branch=%s source_sha=%s\n' "$DEFAULT_BRANCH" "$SOURCE_SHA"
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO" \
--jq '{id,full_name,visibility,default_branch}'
The repository object is hosted GitHub state;
SOURCE_SHA is a Git object identifier. Neither is an
artifact attestation yet. Keep both because your later verification
policy will constrain provenance back to this source context.
3. Add the deterministic build + two attestations
Create .github/workflows/ch25-attest.yml with the
workflow below. It deliberately creates a tiny tar archive with
normalized ordering, timestamp and ownership so two runs from the
same source produce the same subject bytes. The SBOM is SPDX 2.3 and
is generated after the artifact digest is known, tying its
package record to the shipped subject rather than merely copying a
source manifest.
name: Chapter 25 provenance lab
on:
workflow_dispatch:
permissions:
contents: read
id-token: write
attestations: write
jobs:
build-attest:
runs-on: ubuntu-24.04
steps:
- name: Check out exact source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build deterministic artifact and SPDX SBOM
shell: bash
run: |
set -euo pipefail
rm -rf build dist
mkdir -p build/atlas-lab dist
printf 'atlas-provenance-lab\nversion=1\n' > build/atlas-lab/README.txt
printf 'runtime-dependencies=none\n' > build/atlas-lab/components.txt
tar --sort=name \
--mtime='UTC 1970-01-01' \
--owner=0 --group=0 --numeric-owner \
-C build -cf dist/atlas-lab.tar atlas-lab
(cd dist && sha256sum atlas-lab.tar > SHA256SUMS)
ARTIFACT_SHA=$(cut -d' ' -f1 dist/SHA256SUMS)
export ARTIFACT_SHA
python - <<'PY'
import json, os
sha = os.environ['ARTIFACT_SHA']
doc = {
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "atlas-lab-sbom",
"documentNamespace": "https://example.invalid/spdx/atlas-lab/" + sha,
"creationInfo": {
"created": "1970-01-01T00:00:00Z",
"creators": ["Tool: chapter-25-lab-generator/1.0"]
},
"packages": [{
"name": "atlas-lab",
"SPDXID": "SPDXRef-Package-atlas-lab",
"versionInfo": "1.0.0",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": False,
"checksums": [{"algorithm": "SHA256", "checksumValue": sha}]
}],
"relationships": [{
"spdxElementId": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": "SPDXRef-Package-atlas-lab"
}]
}
with open('dist/atlas-lab.spdx.json', 'w', encoding='utf-8') as f:
json.dump(doc, f, indent=2, sort_keys=True)
f.write('\n')
PY
cat dist/SHA256SUMS
- name: Attest build provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: dist/atlas-lab.tar
- name: Attest SPDX SBOM
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: dist/atlas-lab.tar
sbom-path: dist/atlas-lab.spdx.json
- name: Upload exact lab evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ch25-evidence
path: |
dist/atlas-lab.tar
dist/SHA256SUMS
dist/atlas-lab.spdx.json
if-no-files-found: error
retention-days: 7
mkdir -p .github/workflows
# Save the YAML above as .github/workflows/ch25-attest.yml, then:
git add .github/workflows/ch25-attest.yml
git commit -m "Add Chapter 25 provenance lab"
git push origin HEAD
SOURCE_SHA=$(git rev-parse HEAD)
printf 'workflow_source_sha=%s
' "$SOURCE_SHA"
4. Run the workflow and preserve the causal evidence
gh workflow run ch25-attest.yml -R "$REPO" --ref "$DEFAULT_BRANCH"
RUN_ID=$(gh run list -R "$REPO" --workflow ch25-attest.yml --limit 1 --json databaseId,headSha,status,conclusion,event,createdAt --jq '.[0].databaseId')
gh run watch "$RUN_ID" -R "$REPO" --exit-status
gh run view "$RUN_ID" -R "$REPO" --json databaseId,headSha,event,status,conclusion,jobs --jq '{databaseId,headSha,event,status,conclusion,jobs:[.jobs[]|{name,conclusion}]}'
Expected state: one successful job, one uploaded Actions artifact
named ch25-evidence, and two attestations associated
with the same tar digest—one provenance predicate and one SBOM
predicate. The run’s headSha is source evidence; it is
not the artifact SHA-256.
5. Download exact bytes and verify the producer-published digest
cd ..
rm -rf ch25-verify
mkdir ch25-verify
gh run download "$RUN_ID" -R "$REPO" -n ch25-evidence -D ch25-verify
cd ch25-verify
ls -la
sha256sum -c SHA256SUMS
ARTIFACT_SHA=$(sha256sum atlas-lab.tar | cut -d' ' -f1)
printf 'artifact_sha256=%s
' "$ARTIFACT_SHA"
The checksum file is useful operational evidence, but a producer could publish a malicious artifact and matching checksum. The next step adds authenticated provenance: it asks whether GitHub/Sigstore can verify a signed claim for these exact bytes and whether the signer identity matches your policy.
6. Verify repository, signer workflow and source ref
gh attestation verify atlas-lab.tar -R "$REPO" --signer-workflow "$REPO/.github/workflows/ch25-attest.yml" --source-ref "refs/heads/$DEFAULT_BRANCH"
# Save machine-readable verification evidence for policy inspection.
gh attestation verify atlas-lab.tar -R "$REPO" --signer-workflow "$REPO/.github/workflows/ch25-attest.yml" --source-ref "refs/heads/$DEFAULT_BRANCH" --format json > provenance-verification.json
jq '.[0].verificationResult.statement | {subject,predicateType}' provenance-verification.json
jq '.[0].verificationResult.signature.certificate | keys' provenance-verification.json
Successful verification proves the artifact digest has a valid
provenance attestation and that authenticated signer/source
constraints supplied to the CLI match. GitHub CLI documentation
warns that fields in statement.predicate can include
workflow-controlled data; when building high-assurance policy,
prefer certificate/timestamp identity fields and trusted-builder
constraints over blindly trusting arbitrary predicate text.
7. Verify and inspect the SBOM predicate separately
gh attestation verify atlas-lab.tar -R "$REPO" --signer-workflow "$REPO/.github/workflows/ch25-attest.yml" --source-ref "refs/heads/$DEFAULT_BRANCH" --predicate-type https://spdx.dev/Document/v2.3 --format json --jq '.[].verificationResult.statement.predicate' > verified-sbom.json
jq '{spdxVersion,name,packages,relationships}' verified-sbom.json
Expected observation: the same tar subject has a second attestation whose predicate type is SPDX 2.3. This lab intentionally has no third-party runtime dependencies so the inventory stays small. In production, use an SBOM generator that examines the final package/image/filesystem or otherwise proves it captures resolved shipped components. The attestation protects the claim’s origin; it does not make an incomplete inventory complete.
8. Inspect repository attestation metadata through REST
The REST endpoint can list bundles by subject digest. This is useful for inventory and automation, but GitHub explicitly states that meaningful security requires cryptographic verification of the signature/timestamps and signer identity. Treat API listing as discovery, not acceptance.
SUBJECT="sha256:$ARTIFACT_SHA"
gh api --paginate \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"repos/$REPO/attestations/$SUBJECT?per_page=100" \
--jq '{count:(.attestations|length),initiators:[.attestations[].initiator]}'
For public resources, current GitHub attestation listing can be read
without authentication; private-resource visibility and token
permissions differ. The lab stays authenticated through
gh because it keeps host/repository intent explicit.
9. Challenge: choose the correct evidence/control
| Situation | Best next control |
|---|---|
| You downloaded a release asset and want to know whether its bytes came from this repository |
gh attestation verify against the file and
expected repository; add signer/ref constraints.
|
| A provenance attestation verifies, but you need component inventory | Require/verify an SBOM predicate tied to the same subject; inspect its completeness. |
| A registry tag now points to a different image | Verify the immutable image digest/attestation, not the mutable tag alone. |
| REST says an attestation exists | Run cryptographic verification; API presence alone is insufficient. |
| The workflow moved to a central reusable builder | Verify the signer reusable workflow/repository explicitly, not only the caller repository. |
10. Verification checklist and reversible cleanup
-
The repository is public/disposable and the workflow permissions
are exactly
contents: read,id-token: write, andattestations: write. - The tar artifact passes its published SHA-256 check and its provenance attestation under repository/workflow/ref constraints.
- The SPDX SBOM predicate verifies separately and names the same subject digest.
- The Actions run head SHA and artifact SHA-256 are recorded as different identities serving different purposes.
- No private signing key, PAT, cloud credential or package token was created for the lab.
cd ../ch25-lab
git status --short
gh run view "$RUN_ID" -R "$REPO" --json databaseId,headSha,conclusion
# Reversible end state after reviewing evidence.
gh repo archive "$REPO" --yes
Knowledge check
Why does the workflow compute both a Git commit SHA and an artifact SHA-256?
The commit SHA identifies source Git state; the artifact SHA-256 identifies the produced bytes. Provenance links them but they are not interchangeable.
Why is --signer-workflow valuable when
verification already uses --repo?
The repository may contain multiple workflows with different trust properties. Constraining the signer workflow narrows the authenticated builder identity.
Why does the SBOM verification use
--predicate-type?
The default verifier expects SLSA provenance. SPDX is a different typed claim and must be requested explicitly.
What does the REST attestation listing prove by itself?
Only that GitHub returned attestation metadata for the digest under your visibility. It does not replace cryptographic signature/timestamp and identity verification.
Why is there no signing secret in the workflow?
GitHub establishes the Actions workflow identity through OIDC and Sigstore infrastructure; the job does not manage a long-lived private signing key.
Summary
You produced deterministic bytes, recorded their digest, generated two typed attestations, downloaded the exact artifact, verified provenance under repository/workflow/ref policy, inspected an SPDX SBOM predicate, and used REST only as discovery evidence. Next you will choose which of these controls belongs at build, publication, distribution and deployment boundaries.
Official references
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.