Skip to content

Commit 02cb633

Browse files
TheMaxMurclaude
andcommitted
ci(release): use GitHub attest-build-provenance (immutable-release safe)
The draft-then-publish approach still failed: GitHub immutable releases reject asset uploads even to a DRAFT, so the slsa-github-generator "append provenance to the release" model cannot work here at all. Switch to actions/attest-build- provenance, which records keyless build provenance for every .uf2 in the GitHub attestation API + Rekor (not as a release asset), so it is immutable-safe. Drops the separate provenance/publish jobs; verify with `gh attestation verify`. Docs + CHANGELOG 0.2.1 updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ca7ba39 commit 02cb633

3 files changed

Lines changed: 33 additions & 78 deletions

File tree

.github/workflows/release.yml

Lines changed: 17 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
77
# on a bit-identical rebuild of all eight (a non-reproducible image fails the job
88
# before anything is published), generates a CycloneDX SBOM, hashes everything
99
# into SHA256SUMS, signs that with keyless cosign (sigstore/Fulcio via OIDC — no
10-
# private key), and creates the GitHub Release as a DRAFT.
10+
# private key), attests GitHub build provenance for every .uf2, and publishes the
11+
# GitHub Release.
1112
#
12-
# `provenance` emits SLSA build provenance for every released artifact via the
13-
# slsa-github-generator reusable workflow (also keyless) and uploads it to the
14-
# draft: a consumer can verify which workflow, at which commit, on which runner
15-
# built each .uf2.
16-
#
17-
# `publish` un-drafts the release once the provenance is attached — a release
18-
# becomes immutable on publish, so the provenance must land while it's a draft.
13+
# The provenance is a GitHub attestation (Sigstore-signed, in the attestation API
14+
# + the public Rekor log), NOT a release asset — so it stays compatible with
15+
# immutable releases. A consumer verifies which workflow / commit / runner built
16+
# each .uf2 with `gh attestation verify` (docs/supply-chain.md).
1917
#
2018
# The .uf2 images are UNSIGNED for secure boot — cosign + the SLSA provenance
2119
# attest the BUILD, not the boot seal. On a secure-boot device, seal an image
@@ -43,10 +41,8 @@ jobs:
4341
timeout-minutes: 150
4442
permissions:
4543
contents: write # create the release + upload assets
46-
id-token: write # keyless cosign (OIDC token from Fulcio)
47-
outputs:
48-
hashes: ${{ steps.hashes.outputs.hashes }}
49-
tag: ${{ steps.tag.outputs.tag }}
44+
id-token: write # keyless cosign + the attestation's Fulcio OIDC token
45+
attestations: write # GitHub build-provenance attestation (immutable-safe)
5046
steps:
5147
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
5248
with:
@@ -129,13 +125,14 @@ jobs:
129125
sha256sum ./*.uf2 ./*.cdx.json > SHA256SUMS
130126
cat SHA256SUMS
131127
132-
- name: subjects for SLSA provenance
133-
id: hashes
134-
run: |
135-
# base64(sha256sum) over the released build artifacts (.uf2 + SBOM);
136-
# clean names (no ./) so `slsa-verifier verify-artifact <file>` matches.
137-
cd dist
138-
echo "hashes=$(sha256sum ./*.uf2 ./*.cdx.json | sed 's, \./, ,' | base64 -w0)" >> "$GITHUB_OUTPUT"
128+
- name: attest build provenance
129+
# GitHub-native build provenance for every .uf2 — signed keyless via
130+
# Sigstore/Fulcio against this run's OIDC identity, recorded in the GitHub
131+
# attestation API + the public Rekor log (NOT a release asset, so it is
132+
# compatible with immutable releases). Verify with `gh attestation verify`.
133+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
134+
with:
135+
subject-path: dist/*.uf2
139136

140137
- name: sign SHA256SUMS (keyless cosign)
141138
env:
@@ -163,53 +160,12 @@ jobs:
163160
} >> release-notes.md
164161
cat release-notes.md
165162
166-
- name: create the GitHub Release (draft — published after provenance)
163+
- name: create the GitHub Release
167164
env:
168165
GH_TOKEN: ${{ github.token }}
169166
run: |
170167
tag="${{ steps.tag.outputs.tag }}"
171168
gh release create "$tag" \
172169
--title "RS-Key $tag" \
173170
--notes-file release-notes.md \
174-
--draft \
175171
dist/*
176-
177-
# SLSA build provenance for every released artifact, generated + signed in the
178-
# SLSA project's trusted reusable workflow (keyless, no private key). The .uf2
179-
# subjects come from the build job; the provenance is uploaded to the same
180-
# release. Verify with `slsa-verifier` (docs/supply-chain.md).
181-
#
182-
# NB: pinned by version TAG, not a commit SHA — the one `uses:` here that is
183-
# intentionally not SHA-pinned. The generator verifies its own ref, and
184-
# slsa-verifier checks the builder id
185-
# `…/generator_generic_slsa3.yml@refs/tags/v2.1.0`; a SHA pin breaks both.
186-
provenance:
187-
needs: [build]
188-
permissions:
189-
actions: read # read the workflow run that built the artifacts
190-
id-token: write # keyless signing of the provenance (Fulcio OIDC)
191-
contents: write # attach the provenance to the release
192-
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
193-
with:
194-
base64-subjects: "${{ needs.build.outputs.hashes }}"
195-
upload-assets: true
196-
upload-tag-name: "${{ needs.build.outputs.tag }}"
197-
provenance-name: "rs-key-${{ needs.build.outputs.tag }}.intoto.jsonl"
198-
199-
# Publish (un-draft) the release only after the provenance asset is attached.
200-
# A release becomes immutable on publish, so the draft must carry everything —
201-
# artifacts (build) and provenance (provenance) — before this flips it live.
202-
publish:
203-
needs: [build, provenance]
204-
runs-on: ubuntu-latest
205-
timeout-minutes: 10
206-
permissions:
207-
contents: write # publish (un-draft) the release
208-
steps:
209-
- name: publish the draft release
210-
env:
211-
GH_TOKEN: ${{ github.token }}
212-
run: |
213-
gh release edit "${{ needs.build.outputs.tag }}" \
214-
--repo "${{ github.repository }}" \
215-
--draft=false

CHANGELOG.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ tag: the USB `bcdDevice` build counter (bumped on every behavior change), and
1717

1818
No firmware change — `bcdDevice` stays `0x0760` and the eight `.uf2` images are
1919
bit-identical to 0.2.0. This release ships the fixed release pipeline: 0.2.0
20-
published its GitHub Release before the SLSA provenance was attached, and
21-
GitHub's immutable releases rejected the late upload.
22-
23-
### Fixed
24-
25-
- The release workflow now creates the GitHub Release as a draft, uploads the
26-
SLSA provenance to it, then publishes last — so the provenance lands before the
27-
release turns immutable.
20+
published its GitHub Release without provenance, because the SLSA generator's
21+
"append the provenance to the release" model is incompatible with GitHub's
22+
immutable releases (the late asset upload is rejected — even on a draft).
2823

2924
### Changed
3025

26+
- Build provenance now uses GitHub's native `attest-build-provenance`: each
27+
`.uf2` is attested keyless (Sigstore/Fulcio + the Rekor log) into the
28+
**attestation API** instead of being uploaded as a release asset, so it is
29+
compatible with immutable releases. Verify with `gh attestation verify`
30+
(`docs/supply-chain.md`).
3131
- All GitHub Actions bumped to their current major versions (off the deprecated
3232
Node 20 runtime).
3333

docs/supply-chain.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ See [production.md](production.md) for that.
2121
| Reproducible build | the 8 `.uf2` flavors | the binary is a pure function of the source at the tag — anyone can rebuild it |
2222
| Repro **gate** | (CI, blocking) | the release job *fails* if any flavor doesn't rebuild bit-identical, so a non-reproducible image is never published |
2323
| Checksums + signature | `SHA256SUMS` + `SHA256SUMS.cosign.bundle` | the hashes were signed by this repo's release workflow (keyless cosign) |
24-
| SLSA provenance | `rs-key-<tag>.intoto.jsonl` | which workflow, at which commit, on which runner built each `.uf2` (SLSA build provenance, keyless) |
24+
| Build provenance | a GitHub **attestation** (not a release file) | which workflow, at which commit, on which runner built each `.uf2` (keyless, via `attest-build-provenance`) |
2525
| SBOM | `rs-key-<tag>-sbom.cdx.json` | the CycloneDX bill of materials for the firmware crate |
2626
| Dependency audit | `supply-chain/` (in-repo) | every dependency is covered by an imported audit or a recorded exemption (cargo-vet) |
2727

@@ -52,18 +52,17 @@ cosign verify-blob \
5252
sha256sum -c SHA256SUMS # then check the artifacts against it
5353
```
5454

55-
### 3. SLSA build provenance
55+
### 3. Build provenance (GitHub attestation)
5656

5757
```sh
58-
slsa-verifier verify-artifact rs-key-<tag>-default.uf2 \
59-
--provenance-path rs-key-<tag>.intoto.jsonl \
60-
--source-uri github.com/TheMaxMur/RS-Key \
61-
--source-tag <tag>
58+
gh attestation verify rs-key-<tag>-default.uf2 --repo TheMaxMur/RS-Key
6259
```
6360

64-
This confirms the `.uf2` was produced by `release.yml` in this repo at `<tag>`,
65-
not hand-built and uploaded. The provenance is generated and signed inside the
66-
SLSA project's trusted reusable workflow, so the build job can't forge its own.
61+
This confirms the `.uf2` was built by `release.yml` in this repo — the attestation
62+
records the workflow, commit and runner, so a hand-built upload won't verify. The
63+
provenance is a GitHub attestation (Sigstore-signed, logged in Rekor) kept in the
64+
attestation API rather than as a release asset, so it stays available even though
65+
the published release is immutable.
6766

6867
## Dependency review — cargo-vet
6968

0 commit comments

Comments
 (0)