Chapter 25Lesson 05~225 minutes

Checkpoint Lab — Artifact Attestations, SBOMs, Build Provenance, Verification, and SLSA-Oriented Workflows

The checkpoint produces one deterministic artifact, provenance attestation and SPDX SBOM attestation, verifies repository/workflow/ref identity, deliberately mutates the artifact to force verification failure, compares provenance with inventory evidence, and writes a deployment gate that treats attestations as evidence—not as a blanket claim that software is secure.

Checkpoint labTamper testDeployment gateVerificationChapter 26 bridge

Learning objectives

  • Produce and identify a deterministic artifact by digest, then generate and verify real GitHub provenance and SBOM attestations in a public disposable repository.
  • Predict repository/workflow/evidence state before the run and independently verify run SHA, artifact digest, subject and signer/ref policy afterward.
  • Mutate a copy of the artifact to force a verification failure and preserve the original evidence while diagnosing the cause.
  • Compare provenance claims with SBOM inventory and write a deployment policy that requires both where appropriate.
  • Define a versioned, reviewable SLSA-oriented operating statement without overstating certification or application security.
Checkpoint assumptions: GitHub.com, GitHub Free, a disposable public personal repository, standard GitHub-hosted Ubuntu runner, current GitHub CLI with gh attestation, and no paid cloud/registry. The lab creates cryptographic attestations through GitHub’s OIDC/Sigstore service but creates no signing key or long-lived credential.

1. Scenario and predictions before changing state

Atlas Relay is preparing a deployment gate. The team no longer accepts “download from the release page” as provenance. It wants exact bytes, authenticated build identity, component inventory, and a controlled response when any of those differ.

Prediction Expected result Independent verification
P1: workflow commit and artifact digest are different identities Run headSha is a Git SHA; tar has SHA-256 bytes digest Compare gh run view with sha256sum.
P2: one artifact can have more than one typed attestation Same tar digest has SLSA provenance + SPDX predicate Run two gh attestation verify commands with different predicate types.
P3: changing one byte invalidates the original subject match Tampered copy gets a new digest and fails verification Preserve verifier stderr/exit code.
P4: a broad owner-only trust policy can be weaker than exact workflow/ref policy Both may cryptographically verify, but exact policy has narrower authorization Compare verifier constraints and document acceptance rule.

Record P1–P4 in CHECKPOINT.md before running the workflow. The checkpoint is an experiment: prediction first, state change second, independent evidence third.

2. Setup and preflight

gh --version
gh auth status
gh attestation verify --help | sed -n '1,100p'

OWNER=$(gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq .login)
REPO="$OWNER/gh-attestation-ch25-checkpoint"

gh repo create "$REPO" --public --add-readme \
  --description "Disposable Chapter 25 checkpoint"
gh repo clone "$REPO" ch25-checkpoint
cd ch25-checkpoint
DEFAULT_BRANCH=$(gh repo view "$REPO" --json defaultBranchRef --jq .defaultBranchRef.name)
BASE_SHA=$(git rev-parse HEAD)
printf 'repo=%s branch=%s base_sha=%s\n' "$REPO" "$DEFAULT_BRANCH" "$BASE_SHA"

Inspect the repository UI and confirm it is the disposable public repository. If it is private, stop: the free path intentionally depends on public-repository attestation availability.

3. Install the governed workflow

Use the exact workflow from Lesson 2 as .github/workflows/ch25-attest.yml. Its permissions are deliberately limited to reading source, requesting OIDC identity and writing attestations. It does not publish a package, mutate Issues, write repository contents, or receive secrets.

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 to .github/workflows/ch25-attest.yml
cat > CHECKPOINT.md <<'EOF'
# Chapter 25 predictions
- P1: source Git SHA and artifact SHA-256 will differ.
- P2: the same artifact digest will have provenance and SPDX attestations.
- P3: a one-byte mutation will fail attestation verification.
- P4: production policy will require repository + signer workflow + source ref, not owner-only trust.
EOF

git add .github/workflows/ch25-attest.yml CHECKPOINT.md
git commit -m "Add Chapter 25 attestation checkpoint"
git push origin HEAD
CHECKPOINT_SOURCE_SHA=$(git rev-parse HEAD)

4. Build, attest and capture run identity

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 --jq '.[0].databaseId')
gh run watch "$RUN_ID" -R "$REPO" --exit-status

gh run view "$RUN_ID" -R "$REPO"   --json databaseId,headSha,event,conclusion,createdAt,jobs   --jq '{databaseId,headSha,event,conclusion,createdAt,jobs:[.jobs[]|{name,conclusion}]}'

Prediction P1 starts here: save headSha. It identifies the source/workflow run, not the output bytes.

5. Download exact evidence and prove the original artifact

cd ..
rm -rf ch25-checkpoint-verify
mkdir ch25-checkpoint-verify
gh run download "$RUN_ID" -R "$REPO" -n ch25-evidence -D ch25-checkpoint-verify
cd ch25-checkpoint-verify

sha256sum -c SHA256SUMS
ARTIFACT_SHA=$(sha256sum atlas-lab.tar | cut -d' ' -f1)
printf 'artifact_sha256=%s
' "$ARTIFACT_SHA"

# Provenance gate: repository + exact signer workflow + source ref + hosted-runner policy.
gh attestation verify atlas-lab.tar   -R "$REPO"   --signer-workflow "$REPO/.github/workflows/ch25-attest.yml"   --source-ref "refs/heads/$DEFAULT_BRANCH"   --deny-self-hosted-runners   --format json > provenance.json

jq '.[0].verificationResult.statement | {subject,predicateType}' provenance.json

P1 is confirmed when the Git run SHA and artifact SHA-256 are visibly different. The provenance subject should contain the artifact’s digest, while the certificate/verification result authenticates the GitHub Actions builder identity.

6. Prove the second predicate and compare it with provenance

gh attestation verify atlas-lab.tar   -R "$REPO"   --signer-workflow "$REPO/.github/workflows/ch25-attest.yml"   --source-ref "refs/heads/$DEFAULT_BRANCH"   --deny-self-hosted-runners   --predicate-type https://spdx.dev/Document/v2.3   --format json > sbom-verification.json

jq '.[0].verificationResult.statement | {subject,predicateType}' sbom-verification.json
jq '.[0].verificationResult.statement.predicate | {spdxVersion,name,packages}' sbom-verification.json

P2 is confirmed when the same subject digest has two verified predicate types. Provenance tells the build story; SPDX tells the inventory story. The checkpoint’s component inventory is intentionally trivial; production SBOM generation must examine the final shipped object or otherwise prove resolved-component coverage.

7. Inject one-byte substitution and diagnose the failure

cp atlas-lab.tar atlas-lab-tampered.tar
printf 'X' >> atlas-lab-tampered.tar

sha256sum atlas-lab.tar atlas-lab-tampered.tar

set +e
gh attestation verify atlas-lab-tampered.tar   -R "$REPO"   --signer-workflow "$REPO/.github/workflows/ch25-attest.yml"   --source-ref "refs/heads/$DEFAULT_BRANCH"   --deny-self-hosted-runners   2>tamper.stderr
TAMPER_STATUS=$?
set -e

cat tamper.stderr
printf 'tamper_status=%s
' "$TAMPER_STATUS"
test "$TAMPER_STATUS" -ne 0

P3 is confirmed only if the tampered copy has a different digest and verification fails. Keep the original file and failure evidence. Do not “repair” the tampered copy by signing it; the correct response is to retrieve/rebuild the expected artifact and investigate the distribution change.

8. Cross-check attestation inventory through the versioned REST API

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 '{subject:"'"'$SUBJECT'"'",attestation_count:(.attestations|length),initiators:[.attestations[].initiator]}'

Use this as independent metadata/inventory evidence. Do not replace the two gh attestation verify checks with this response; REST presence is not cryptographic acceptance.

9. Write the deployment gate

deployment_gate:
  artifact_identity:
    algorithm: sha256
    immutable_digest_required: true
  attestations:
    provenance:
      predicate: https://slsa.dev/provenance/v1
      required: true
    sbom:
      predicate: https://spdx.dev/Document/v2.3
      required: true
  authenticated_builder:
    repository: "OWNER/gh-attestation-ch25-checkpoint"
    signer_workflow: ".github/workflows/ch25-attest.yml"
    source_refs:
      - "refs/heads/main"   # replace with the actual protected release ref policy
    self_hosted_runners: deny
  consumer_behavior:
    verify_before_deploy: true
    fail_closed_on_digest_or_identity_mismatch: true
  exceptions:
    approver: "supply-chain-owner"
    evidence_required: true
    expiry_required: true

P4 is now a production rule: the gate does not trust every attestation from the owner. It requires the expected repository, signer workflow and source-ref policy. Adapt the branch to the repository’s actual protected release model; for high-value releases, an exact source digest or governed release tag may be better than a broad branch constraint.

10. Write the SLSA-oriented statement without overclaiming

Record: “This checkpoint generates GitHub artifact attestations and verifies them under an explicit consumer policy. GitHub documentation currently describes artifact attestations alone as SLSA v1.0 Build Level 2 and reusable-workflow designs as helping achieve v1 Build Level 3. The current SLSA specification is v1.2; this lab does not claim independent certification or evaluate every Build L3 requirement.”

That wording is intentionally narrower than “SLSA compliant.” Production teams should name the specification version/track, assess the actual builder and producer process, and retain evidence for the claim they make.

11. Final verification and cleanup

  • Original atlas-lab.tar passes SHA-256 checksum and provenance verification.
  • SPDX predicate verifies separately for the same subject digest.
  • Tampered copy has a different digest and a non-zero verification result.
  • Verification constrains repository, signer workflow, source ref and hosted-runner policy.
  • REST inventory and workflow run/head SHA are recorded as supporting metadata, not substituted for cryptographic verification.
  • Deployment gate and SLSA-oriented statement are documented with exception/expiry ownership.
# Preserve local evidence; then archive the disposable repository.
ls -la
jq '.[0].verificationResult.statement | {subject,predicateType}' provenance.json
jq '.[0].verificationResult.statement | {subject,predicateType}' sbom-verification.json

cd ../ch25-checkpoint
git status --short
gh repo archive "$REPO" --yes
No destructive cleanup is required. Do not delete attestations or rewrite Git history to make the checkpoint disappear. Archive the disposable repository after evidence review. If organizational evidence retention requires longer preservation, follow that policy.

Knowledge check

The tar verifies under --repo but fails under --signer-workflow. What does that diagnose?

Why does the tampered file fail even though its filename is almost the same?

What relationship should provenance and SBOM have in this checkpoint?

Why is REST attestation count not enough for deployment?

What does --deny-self-hosted-runners express?

Can this checkpoint claim “software is secure” because all checks pass?

12. Production operating model and Chapter 26 bridge

Chapter 25 adds a consumer-verifiable supply-chain evidence layer to the GitHub operating model: immutable subject digests, authenticated workflow provenance, attested SBOM predicates, explicit repository/workflow/ref/runner trust, tamper rejection, and a cautious versioned SLSA-oriented claim. The central rule is simple: producers generate evidence, but consumers receive the security benefit only when they verify the exact artifact under an authorization policy.

Chapter 26 now generalizes the machine-readable techniques used throughout this course. You will move from individual gh api calls into systematic REST/GraphQL clients with pagination, rate-limit handling, safe mutation semantics, and automation design.

Next chapter

REST API, GraphQL API, gh api, Pagination, Rate Limits, and Automation Clients: Concepts, Architecture, and Mental Model

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.