Chapter 25Lesson 02~215 minutes

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.

Public labSHA-256actions/attestgh attestationSPDX

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.
Mandatory path: GitHub.com + GitHub Free + one disposable public personal repository + standard GitHub-hosted Ubuntu runner. The workflow uses no secrets, packages, cloud accounts, signing keys or paid features. Artifact-attestation write access is confined to the attestation job; checkout persistence is disabled.

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
Why two attestation steps? The first uses the default SLSA provenance predicate. The second uses the same subject but an SPDX SBOM predicate. Consumers can require one or both. The SBOM document is also uploaded as a human/machine-readable file, but the attestation is what cryptographically binds that predicate to the subject and workflow identity.
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, and attestations: 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
Do not delete attestations or the repository as the default cleanup. Deletion destroys evidence and has different lifecycle semantics. Archiving the disposable repository is reversible and preserves the learning record. If your organization has a formal evidence-retention policy, follow that policy instead.

Knowledge check

Why does the workflow compute both a Git commit SHA and an artifact SHA-256?

Why is --signer-workflow valuable when verification already uses --repo?

Why does the SBOM verification use --predicate-type?

What does the REST attestation listing prove by itself?

Why is there no signing secret in the workflow?

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.

Next lesson

Artifact Attestations, SBOMs, Build Provenance, Verification, and SLSA-Oriented Workflows: Configuration, Design Choices, and Tradeoffs

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.