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