Skip to content

Provenance: each release's tarball attested and held to the bytes hex.pm serves - #13

Merged
HackTuah merged 2 commits into
mainfrom
ci/provenance
Oct 7, 2026
Merged

HackTuah merged 2 commits into
mainfrom
ci/provenance

Conversation

@HackTuah

Copy link
Copy Markdown
Member

beam_mcp's provenance workflow, for the signer: on a release tag CI builds the tarball the canonical way, attests it (SLSA build provenance), and verifies the attestation against what hex.pm serves. A manual run takes a published tag, so once this merges I will run it on v0.2.1 to attest the release that went out today. Docs and CHANGELOG say how to verify.

…tes hex.pm serves

.github/workflows/provenance.yml, after beam_mcp's: on a v* tag CI builds the tarball with
tools/release_tarball.sh, attests its digest with actions/attest-build-provenance (pinned by
commit), verifies it with --signer-workflow, and holds it to the bytes hex.pm serves (a
fetch or a mismatch fails the tag's run). A manual run takes `ref`, a published tag, and is
held to hex.pm the same way, so 0.2.1, released before this workflow, can be attested now.
docs/verifying-releases.md gives the verify command; CHANGELOG entry under Unreleased.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah requested a review from minitru September 26, 2026 19:32
Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit 491d29d into main Oct 7, 2026
5 checks passed

@minitru minitru left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge review. It works: the 0.2.1 tarball hex.pm serves has an attestation in the repo's store. The follow-ups below are about what else the workflow will attest and how tightly the provenance is bound; none blocks.

What I checked

  • All four action pins match their version tags (checkout v7.0.1, setup-beam v1.24.1, attest-build-provenance v4.2.2, upload-artifact v7.0.1).
  • The one run so far is the manual one on 7 Oct, and it succeeded.
  • hex.pm's checksum for 0.2.1 (df44fba2…) has one attestation.
  • I did not download the tarball or run gh attestation verify myself.

Findings, most important first

  1. It attests before it knows the tarball is the release. The step order is build, attest, then compare with hex.pm. A tag run whose bytes don't match hex.pm fails, but the attestation is already in the store. A manual run with no ref attests the branch head under the release's file name by design. Consumers who verify the hex.pm tarball are safe, since the digest must match. But "signed by this workflow" then doesn't mean "a release". Fix: fetch and compare first, attest only on a match, and drop the branch-head mode or have it skip the attest step.

  2. Tag-controlled code runs in the job that can sign. release_tarball.sh runs mix deps.get and mix hex.build on the tag's tree, so that tree's mix.exs executes in a job holding id-token: write and attestations: write. The usual hardening is two jobs: build with contents: read only and upload the tarball, then a second job that attests the artifact.

  3. No tag ruleset. The only ruleset is "main protection" on branches. Anyone with write access can push, move or delete a v* tag, and a push triggers an attestation for whatever commit it points at, on main or not. A tag ruleset on v* closes that, and an "is this commit on main" check in the workflow would too.

  4. The 0.2.1 provenance names main, not the tag. A manual run executes at the branch head, so the attestation's source ref and commit are main's; only the log ties the bytes to v0.2.1. This is unavoidable for 0.2.1, because the workflow didn't exist at that tag. The doc says it was a manual run, but it should also say that --source-ref refs/tags/v0.2.1 won't verify for that release.

  5. The toolchain isn't the pinned pair. The comment says "a copy of .tool-versions' line", but the workflow asks for "28" and "1.18" where .tool-versions has 28.1.1 and 1.18.4, and Hex itself is unpinned. If a patch release changes the tarball bytes, a correct release fails the hex.pm comparison after it's already published. version-file: .tool-versions with version-type: strict removes the copy.

  6. Version string goes straight into shell. ${{ steps.build.outputs.version }} is interpolated into run: blocks, and it comes from a loose sed over the tag's mix.exs. Pass it through env: and reject anything that isn't a version number. This is low risk given who can tag, but it's the pattern the INPUT_REF handling already gets right.

  7. GOVERNANCE.md list. The new ruleset paragraph sits between steps 3 and 4, so step 4 renders as a separate list. Move it below step 4.

Good as written: the input ref is passed through env, restricted to v* and checked to be a real tag. A missing or mismatched release fails loudly on a tag run. The workflow never publishes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants