← GitHub articles

Software supply chain

Ship verifiable releases with GitHub artifact attestations

How to add signed build provenance and SBOM attestations to a release, so anyone downstream can check what was built, from which commit, by which workflow.

Shubham Anandani··9–11 min read

Problem

A release asset is a promise: this file came from this project. Most consumers have no way to check it. If a binary is rebuilt outside CI, swapped in transit, or built from a different commit than the tag suggests, the file name looks identical and nothing flags the difference. Checksums help only if you trust the place you got the checksum from. Build-pipeline compromises such as the 2020 SolarWinds attack are the reason this problem is taken seriously.

GitHub artifact attestations address this with a signed statement that binds an artifact's digest to its build provenance: the repository, workflow, commit and runner that produced it. The statements follow the in-toto and Sigstore formats. You can verify them with the GitHub CLI, online or offline. An SBOM (software bill of materials) can be attached in the same way, so a consumer can see what is inside the artifact as well as where it came from.

Options and trade-offs

  • Do nothing: no friction, and no way for anyone to verify anything.
  • Checksums only: catches accidental corruption, but proves nothing about origin if the checksum file sits next to the artifact.
  • SBOM only: shows contents, but without a signed link to a build it is just another unverified file.
  • Build provenance attestation: a signed statement tying the artifact digest to the workflow run. This is the strongest single step for most projects.
  • Provenance plus an SBOM attestation: adds contents transparency on top of origin. Verifying the SBOM requires naming its predicate type explicitly, because verification defaults to provenance.
  • Custom predicates: record your own assertions (test results, policy checks) in the in-toto format when provenance and SBOM are not enough.

My recommendation

Attest provenance for every released binary and container image, and add an SBOM attestation when consumers care about contents. Treat verification as part of the release process, not an afterthought: run gh attestation verify in the release workflow's final step so a broken attestation fails the release. Be honest in the README about what this proves. A valid attestation says the artifact came from a particular workflow run. It does not say the workflow was uncompromised or the build reproducible.

Minimal working example

A release workflow for a Java project. It builds a jar, creates an SBOM, attests both, builds and attests a container image by digest, and publishes the release:

name: release
on:
  push:
    tags: ["v*"]

permissions:
  contents: write        # create the release
  id-token: write        # sign with the workflow's OIDC identity
  attestations: write    # store the attestation
  packages: write        # push the image

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"

      - name: Build
        run: ./gradlew --no-daemon build

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          path: .
          format: spdx-json
          output-file: sbom.spdx.json

      - name: Attest build provenance (jar)
        uses: actions/attest-build-provenance@v2
        with:
          subject-path: build/libs/*.jar

      - name: Attest SBOM
        uses: actions/attest-sbom@v2
        with:
          subject-path: build/libs/*.jar
          sbom-path: sbom.spdx.json

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        id: push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}

      - name: Attest image provenance (by digest)
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ghcr.io/${{ github.repository }}
          subject-digest: ${{ steps.push.outputs.digest }}
          push-to-registry: true

      - name: Publish release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh release create "${{ github.ref_name }}" build/libs/*.jar sbom.spdx.json --generate-notes

Verify as a consumer:

# Provenance (the default)
gh attestation verify build/libs/app-1.0.0.jar --repo OWNER/REPO

# SBOM: name the predicate type explicitly
gh attestation verify build/libs/app-1.0.0.jar --repo OWNER/REPO \
  --predicate-type https://spdx.dev/Document/v2.3

# Container image
gh attestation verify oci://ghcr.io/OWNER/REPO:v1.0.0 --repo OWNER/REPO

Test it: release a tag, run the three commands against the published assets, then change one byte of the jar and confirm verification fails.

Checklist

  • Grant the job id-token: write and attestations: write. Add packages: write when pushing images.
  • Attest container images by digest, not by tag, and leave the tag out of subject-name.
  • Pin third-party actions to a full commit SHA and let Dependabot update them. The attest actions move between major versions, so check the current release before copying versions from any article.
  • Verify the artifact in CI before publishing, and document the verification command in the README.
  • Publish the SBOM next to the artifact, and tell users to pass the SBOM predicate type when verifying it.
  • Practice offline verification (download the attestation bundle and the trusted root with the CLI) for environments without internet access.
  • State plainly in the README what an attestation does and does not prove.

Where this doesn't apply

If you only ship source and no built artifacts, there is little to attest. Attestations for private and internal repositories need GitHub Enterprise Cloud, and the feature is not available on GitHub Enterprise Server, so check your plan first. Fully air-gapped environments need their own trust anchors.

One honest limitation

Provenance is necessary but not sufficient. A signed statement that a workflow built a file does not make the build hermetic or reproducible, and a compromised workflow produces a perfectly valid attestation. Higher assurance depends on build isolation and organizational policy, which the SLSA levels describe.

Sources

  • Artifact attestations — https://docs.github.com/en/actions/concepts/security/artifact-attestations
  • actions/attest-build-provenance — https://github.com/actions/attest-build-provenance
  • actions/attest-sbom — https://github.com/actions/attest-sbom
  • Verifying attestations offline — https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/verifying-attestations-offline
  • SLSA specification, build levels — https://slsa.dev/spec/v1.0/levels
  • ---