In masutaka/actions , which I introduced in [2026-02-23-1] , CLI binaries such as jactionlint are downloaded from each tool’s GitHub Releases.

The downloaded binaries are verified with both a SHA256 checksum and gh attestation verify. In this post, I’ll explain how and why.

Verifying with SHA256 Checksums and Attestations

Here’s how masutaka/actions verifies jactionlint:

🔗 .github/workflows/gh_action_lint.yml

- name: Install jactionlint
  env:
    GH_TOKEN: ${{ github.token }}
    REPO: jdx/jactionlint
    ASSET: "*_linux_amd64.tar.gz"
    TAG: v2.0.2
    ASSET_SHA256: 026bce6d0ef196f5b22bf43acb1348d47c5d0a5ae61d6ec3bdba1229a823161b
    SIGNER_WORKFLOW: jdx/jactionlint/.github/workflows/release.yaml
    EXECUTABLE: jactionlint
  run: |
    gh release download "$TAG" \
      --repo "$REPO" \
      --pattern "$ASSET" \
      --output asset.tar.gz
    # 正規の release workflow がそのタグからビルドした成果物であることを検証する
    gh attestation verify asset.tar.gz \
      --repo "$REPO" \
      --signer-workflow "$SIGNER_WORKFLOW" \
      --source-ref "refs/tags/$TAG"
    # ピンした時点から中身が変わっていないことを検証する
    echo "${ASSET_SHA256}  asset.tar.gz" | sha256sum -c -
    tar -xzf asset.tar.gz "$EXECUTABLE"

It downloads the asset from GitHub Releases and uses it only if it passes both gh attestation verify and sha256sum -c.

--signer-workflow only checks the workflow path; it doesn’t pin the ref or commit. So I use --source-ref to restrict it to artifacts built from the recorded tag.

The attestations checked by gh attestation verify have to be generated by the publisher in their release workflow. For the publisher’s side of the story, Songmu’s post goes into detail:

🔗 GitHub Actions上で証明書を付与するサプライチェーン構築 | おそらくはそれさえも平凡な日々

After reading it, I also migrated my own github-nippou to actions/attest .

By the way, --signer-workflow can point to a different repository than --repo. For example, ghalint is signed by suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml, a reusable workflow called from its release.yaml .

Why Verify with Both?

Because SHA256 checksums and gh attestation verify verify different things.

  • SHA256 checksum
    • Verifies that “the content hasn’t changed since the checksum was recorded”
    • Even a binary built locally without going through CI will match if you record its value
  • gh attestation verify
    • Verifies that “the artifact was built from that tag by the legitimate release workflow”
    • However, unless it’s an immutable release, tags can be moved. If an attacker gains write access, moves the tag to a tampered commit, and rebuilds, it still passes
    • It also passes if the artifact is rebuilt under the same tag and the asset is overwritten. For example, when a build-time dependency gets compromised later on

I couldn’t find any official recommendation to use both, but I did find some supporting information.

The GitHub Docs page on Artifact attestations says that “artifact attestations are not a guarantee that an artifact is secure.”

There’s also SLSA (Supply-chain Levels for Software Artifacts), an OpenSSF framework that defines levels of software supply chain security. The build provenance checked by gh attestation verify is also in the SLSA format.

SLSA’s Threats & mitigations states that the threat of compromised build-time dependencies is out of SLSA’s scope. The mitigation it suggests is pinning dependencies by digest:

You may be able to mitigate this threat by pinning your build dependencies, preferably by digest rather than version number.

This is aimed at publishers, but I think users pinning by checksum follows the same idea.

That said, checksums only protect against replacement after you’ve recorded them. If a dependency was already compromised at the time of the first release, both the checksum and the attestation will pass.

Personally, I’d be happy if gh attestation verify could also check that the artifact matches a recorded checksum, but for now I’m verifying with two separate methods.

If There’s No Build Provenance, Release Attestations May Help

What follows is something I learned while writing this post. I’ve only tried it locally and don’t use it in masutaka/actions.

gh attestation verify verifies build provenance, so it can’t be used unless the publisher generates it.

For example, tfupdate , a tool that bulk-updates version constraints for Terraform core and providers, doesn’t generate build provenance, as you can see from the repository’s Attestations page.

So gh attestation verify fails with a 404:

$ gh attestation verify asset.tar.gz --repo minamijoyo/tfupdate
Error: HTTP 404: Not Found (https://api.github.com/repos/minamijoyo/tfupdate/attestations/sha256:559bc7c6c15496682a3207cbdc4e6327de252f5f1a4348f5098027d6bcc4ebe2?per_page=30&predicate_type=https%3A%2F%2Fslsa.dev%2Fprovenance%2Fv1)

Even so, if the repository has immutable releases enabled, you can use release attestations. GitHub generates them automatically for releases published after the setting is turned on.

A release attestation lets you verify that “this asset was included in the release for this tag.” It doesn’t show up on the Attestations page, so check the release page to see whether it’s an immutable release. tfupdate v0.10.2 is an immutable release, so it can be verified with gh release verify-asset:

$ gh release verify-asset v0.10.2 asset.tar.gz --repo minamijoyo/tfupdate
Calculated digest for asset.tar.gz: sha256:559bc7c6c15496682a3207cbdc4e6327de252f5f1a4348f5098027d6bcc4ebe2
Resolved tag v0.10.2 to sha1:9d61d78d73e1325629e46c9e92d713ab258b50cd
Loaded attestation from GitHub API

✓ Verification succeeded! asset.tar.gz is present in release v0.10.2

This method is described in the GitHub Docs page Verifying the integrity of a release .

A release attestation doesn’t tell you whether the artifact was built by the legitimate workflow. Still, it does confirm that the downloaded file is the same as the asset in that release.

With an immutable release, tags can’t be moved and assets can’t be overwritten, so it also closes the tag-moving loophole I mentioned in “Why Verify with Both?”

Conclusion

It seems like a good idea to verify CLI binaries used in GitHub Actions and elsewhere with both SHA256 checksums and gh attestation verify. The checksum confirms the content hasn’t changed, and the attestation confirms it was built by the legitimate release workflow.

Even if the publisher doesn’t generate build provenance, gh release verify-asset looks usable for immutable releases. I’ve only tried this one locally.

Here’s a comparison of the three methods:

Verification What it confirms What the publisher needs
SHA256 checksum The content hasn’t changed since the checksum was recorded Nothing (a checksums.txt lets you cross-check the value you record)
gh attestation verify It was built from that tag by the legitimate release workflow Generating build provenance
gh release verify-asset It was included in the release for that tag Enabling immutable releases

Another option is to install via mise-action . For tools using the aqua backend, checksums are recorded in mise.lock, along with attestation information if available.

In the AI era, this kind of defense in depth should matter more than ever. I hope this helps someone.

References