From af1c215a9e303a7fddd85cd4440f16f65320eb2a Mon Sep 17 00:00:00 2001 From: z33b0t Date: Wed, 29 Jul 2026 01:13:53 +0100 Subject: [PATCH] ci: attest build provenance for the release artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release path produced a notarized macOS zip, a Windows zip and a sha256 in the Homebrew cask, none of it tied back to the run or commit that built it. A checksum proves the file didn't change in transit; anyone who can write to the release (or the tap) can publish a consistent zip-plus-checksum pair. Notarization proves Apple saw the binary and that it carries our Developer ID — it says nothing about which source tree it was built from. Adds actions/attest-build-provenance (pinned to a SHA, per #4) to both release jobs, publishing a signed SLSA statement that binds the artifact digest to the repo, workflow and commit: gh attestation verify Blurt-macOS.zip --repo lightware-dev/blurt Scoped to release tags — attestation is about what we publish, and a PR build isn't. The version-less Blurt-.zip copies need no separate statement: they're byte-identical and attestations are keyed by digest, not filename. Per-job permissions widen to id-token: write + attestations: write. This matters more on the Windows side, where releases are unsigned (#9), so the attestation is currently the only cryptographic link between a download and this repository. It is not a substitute for signing, and neither defends against repo write access: an attacker who can push a tag gets CI to attest their payload. What both defend is a release asset replaced *without* a code push. Also fixes a README claim while adding the verification instructions: the Windows section said "grab the latest signed build", which has never been true — exactly the misreading #9 is about, and it was in the docs as well as the workflow. Closes #24 --- .github/workflows/mac.yml | 27 +++++++++++++++++++++++++++ .github/workflows/windows.yml | 22 ++++++++++++++++++++++ README.md | 21 ++++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 3697351..ffb46a4 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -30,6 +30,8 @@ jobs: name: Build & package Blurt (release) permissions: contents: write # needed to publish a GitHub Release on tag pushes + id-token: write # OIDC token the attestation is signed against + attestations: write # write the SLSA provenance statement steps: - name: checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -150,6 +152,31 @@ jobs: echo "zip=$ZIP" >> "$GITHUB_OUTPUT" echo "name=$(basename "${ZIP%.zip}")-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + # Binds this artifact's digest to the repo, workflow and commit that + # produced it. Notarization proves Apple saw the binary and that it + # carries our Developer ID; it says nothing about which source tree it + # was built from, and a checksum only proves the file didn't change in + # transit. Consumers verify with: + # gh attestation verify Blurt-macOS.zip --repo lightware-dev/blurt + # + # Only on release tags: attestation is about what we publish, and a PR + # build isn't published. The version-less Blurt-macOS.zip copy needs no + # separate attestation — it's byte-identical, and attestations are keyed + # by digest rather than by filename. + # + # Caveat worth knowing: this attests the bytes *this run* built. On a + # re-run where the release already carries an earlier attempt's asset, + # the upload step deliberately leaves the published file untouched (see + # below) and a macOS rebuild is never byte-identical — fresh codesign + # timestamp, new notarization ticket. The attestation would then cover a + # digest that isn't the published one. Same failure mode the cask bump + # already guards against; if it bites, delete the asset and re-run clean. + - name: attest build provenance + if: steps.cfg.outputs.release == 'true' + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: ${{ steps.artifact.outputs.zip }} + - name: clean up signing keychain if: always() && steps.cfg.outputs.signed == 'true' run: security delete-keychain "$RUNNER_TEMP/signing.keychain-db" || true diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 6690459..0a19268 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -30,6 +30,8 @@ jobs: name: Build & package Blurt (release) permissions: contents: write # needed to publish a GitHub Release on tag pushes + id-token: write # OIDC token the attestation is signed against + attestations: write # write the SLSA provenance statement defaults: run: shell: pwsh @@ -120,6 +122,26 @@ jobs: $sha = "$env:GITHUB_SHA".Substring(0,7) "name=Blurt-$ver-Windows-$sha" >> $env:GITHUB_OUTPUT + # Binds this artifact's digest to the repo, workflow and commit that + # produced it: + # gh attestation verify Blurt-Windows.zip --repo lightware-dev/blurt + # + # Carries more weight here than on macOS, because Windows releases are + # unsigned (#9) — this is currently the only cryptographic link between + # a downloaded zip and this repository. It is not a substitute for + # signing: it defends a release asset replaced *without* a code push, + # not an attacker with repo write access, who can push a tag and have CI + # attest their payload just as readily. + # + # Release tags only; a PR build isn't published. The version-less + # Blurt-Windows.zip copy is byte-identical and attestations are keyed by + # digest, so one statement covers both. + - name: attest build provenance + if: steps.cfg.outputs.release == 'true' + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: ${{ steps.artifact.outputs.zip }} + - name: upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/README.md b/README.md index 829fe74..9c713ad 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,15 @@ https://github.com/lightware-dev/blurt/releases/latest/download/Blurt-macOS.zip Unzip, drag **Blurt.app** to `/Applications`, and open it — it's a universal (Apple Silicon + Intel) menu-bar app that launches without Gatekeeper warnings. +Releases carry a signed [build provenance +attestation](https://docs.github.com/actions/security-guides/using-artifact-attestations) +binding the download to the workflow run and commit that built it — notarization +proves Apple saw the binary, not which source tree it came from. To check: + +```bash +gh attestation verify Blurt-macOS.zip --repo lightware-dev/blurt +``` + ### Build from source Built on the Mac (needs Xcode command-line tools): @@ -239,7 +248,7 @@ Settings). See `clients/mac/README.md` for details ## Windows client A native .NET 8 / WPF tray app — the Windows twin of the Mac client, same server -protocol and behaviour. Grab the latest signed build from the +protocol and behaviour. Grab the latest build from the [**Releases page**](https://github.com/lightware-dev/blurt/releases/latest), or the stable URL: @@ -247,6 +256,16 @@ stable URL: https://github.com/lightware-dev/blurt/releases/latest/download/Blurt-Windows.zip ``` +Windows builds are **not** Authenticode-signed, so SmartScreen warns on first +launch — choose *More info* → *Run anyway*. They do carry a signed [build +provenance attestation](https://docs.github.com/actions/security-guides/using-artifact-attestations), +which is currently the only cryptographic link between the zip and this +repository: + +```bash +gh attestation verify Blurt-Windows.zip --repo lightware-dev/blurt +``` + Build from source (needs the .NET 8 SDK): ```bash