From d282c4a87bcbb33d3b343e655a5a33dc01a2d631 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 17 May 2026 06:49:53 +0200 Subject: [PATCH] ci(release): sigstore keyless signing for SHA256SUMS + RELEASING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supply-Chain-Pentester finding (v0.10.0 adversarial review): SHA256SUMS shipped unsigned, so anyone who could replace a release asset could also replace the checksum file. The dossier sold defect detection (true) but quietly implied tamper detection (false). Closes 80% of that gap with sigstore keyless OIDC — no long-lived signing key, no KMS provisioning, no rotation. The trust anchor is the GitHub-Actions workflow identity (issuer `token.actions.githubusercontent.com`, subject `.github/workflows/release.yml@refs/tags/vX.Y.Z`). Workflow changes: - `permissions.id-token: write` so the runner can request its OIDC token (required by cosign keyless flow). - New `Install cosign` step (sigstore/cosign-installer@v3, v2.4.1). - New `Sign SHA256SUMS with cosign (keyless OIDC)` step between checksum generation and release creation. Emits three artifacts: - `SHA256SUMS.txt.cosign.bundle` (verifier-friendly bundle) - `SHA256SUMS.txt.sig` (detached signature) - `SHA256SUMS.txt.pem` (Fulcio-issued short-lived cert) - The existing `Collect assets` step's permissive `find` already picks up the new files; the release page will include them automatically. New `RELEASING.md` documents: - Why signed git tags matter + how to verify (`git tag -v`). - What CI signs (and why sigstore keyless was chosen). - How a consumer verifies a downloaded binary (two-step flow: cosign verify-blob on the bundle, then sha256sum -c). - What is explicitly NOT signed at v0.10.0+ (binary archives transitively only, VSIX, compliance tarball, the maintainer's GPG keylist — for parity with the dossier §0 honest scope). - The manual-republish procedure used for v0.10.0 (#294 context). This addresses the Supply-Chain-Pentester's "one minimum primitive that closes 80% of the gaps" recommendation. The remaining 20% (per-archive signatures, VSIX signing, attestation in-toto bundle) are separate workstreams. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/release.yml | 37 ++++++++++ RELEASING.md | 130 ++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 RELEASING.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c5dc0b..6f4f50f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,12 @@ on: permissions: contents: write + # Required for sigstore keyless signing (cosign): GitHub OIDC token + # is the trust anchor — Fulcio mints a short-lived signing cert from + # the workflow's identity, the signature is recorded in Rekor, and + # consumers verify via the issuer + identity (no long-lived keys to + # rotate). See `Sign release assets with cosign` step below. + id-token: write env: CARGO_TERM_COLOR: always @@ -345,6 +351,37 @@ jobs: sha256sum * > SHA256SUMS.txt cat SHA256SUMS.txt + # ── Sigstore keyless signing (Supply-Chain-Pentester finding) ── + # Closes the gap called out in the v0.10.0 adversarial review: + # SHA256SUMS shipped unsigned, so an attacker who could replace + # the release-page asset could also replace the checksum file. + # Sigstore keyless flow binds the signature to the GitHub-Actions + # OIDC identity (workflow ref + commit SHA + actor); no long-lived + # keys to rotate. Verification: + # cosign verify-blob \ + # --certificate-identity-regexp "https://github.com/pulseengine/rivet/.github/workflows/release.yml@.*" \ + # --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + # --signature SHA256SUMS.txt.sig \ + # --bundle SHA256SUMS.txt.cosign.bundle \ + # SHA256SUMS.txt + - name: Install cosign + uses: sigstore/cosign-installer@v3 + with: + cosign-release: 'v2.4.1' + + - name: Sign SHA256SUMS with cosign (keyless OIDC) + run: | + cd release + cosign sign-blob \ + --yes \ + --bundle SHA256SUMS.txt.cosign.bundle \ + --output-signature SHA256SUMS.txt.sig \ + --output-certificate SHA256SUMS.txt.pem \ + SHA256SUMS.txt + echo "::notice::SHA256SUMS signed via sigstore keyless flow." + echo "::notice::Bundle: SHA256SUMS.txt.cosign.bundle (verifier-friendly)." + echo "::notice::Detached: SHA256SUMS.txt.sig + SHA256SUMS.txt.pem." + - name: Create or update Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..aaadb53 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,130 @@ +# Releasing rivet + +This document covers the steps a maintainer takes to cut a new rivet +release. Most of the work happens in CI; the maintainer's job is to +land a release commit and push a signed tag. + +## TL;DR + +```bash +# 1. Land a release-prep PR with the version bump + CHANGELOG update. +# (Workspace version lives in Cargo.toml [workspace.package].) +gh pr merge --squash + +# 2. From the merge commit, create and push a GPG-signed tag. +git fetch origin main && git checkout main && git reset --hard origin/main +git tag -s vX.Y.Z -m "rivet vX.Y.Z" +git push origin vX.Y.Z + +# 3. CI does the rest: cross-platform binaries, VSIX, compliance report, +# cosign-signed SHA256SUMS, GitHub Release page, VS Code Marketplace. +``` + +## Why signed tags + +The release workflow keys publication off the tag push. An attacker +with write access to the `main` ref (or someone tricking a maintainer +into running `git tag` without `-s`) can otherwise create a release on +behalf of the rivet project without leaving cryptographic evidence. + +`git tag -s` binds the tag to a GPG key. Verification: + +```bash +git tag -v vX.Y.Z +``` + +CI does **not** enforce that the tag is signed — branch protection on +the tag namespace would be the right place to add that gate, but that's +deployment-specific. Maintainers should sign tags as a matter of policy. + +## What CI signs + +Per the supply-chain finding in the v0.10.0 adversarial review, the +release workflow now signs `SHA256SUMS.txt` using **sigstore keyless +OIDC** (no long-lived keys, no rotation, no KMS provisioning needed): + +- `SHA256SUMS.txt.cosign.bundle` — verifier-friendly bundle (single + file containing signature + certificate + Rekor inclusion proof). +- `SHA256SUMS.txt.sig` — detached signature, useful for tools that + don't speak the bundle format. +- `SHA256SUMS.txt.pem` — Fulcio-issued certificate (short-lived, + expires within minutes — verification relies on Rekor's tamper- + evident log, not on the cert validity window). + +Trust anchor: the certificate's identity claim is the workflow's +GitHub-OIDC identity (issuer `https://token.actions.githubusercontent.com`, +subject pattern `https://github.com/pulseengine/rivet/.github/workflows/release.yml@refs/tags/vX.Y.Z`). +A signature that doesn't match this identity is not a rivet release. + +## How a consumer verifies + +```bash +# Download from the release page: +# SHA256SUMS.txt +# SHA256SUMS.txt.cosign.bundle +# + +# 1. Verify the signature on SHA256SUMS.txt. +cosign verify-blob \ + --bundle SHA256SUMS.txt.cosign.bundle \ + --certificate-identity-regexp 'https://github.com/pulseengine/rivet/.github/workflows/release.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + SHA256SUMS.txt + +# 2. Verify your binary's checksum against the signed list. +sha256sum -c SHA256SUMS.txt --ignore-missing +``` + +If the first step fails the binary is not from this release pipeline — +do not run it. If the second step fails the binary has been modified +since the release was cut — do not run it. + +## What is NOT signed (current scope) + +Per the v0.10.0 dossier §0 honest scope statement, the following are +**not** signed and should not be treated as cryptographically attested: + +- Individual binary archives (`rivet-vX.Y.Z-*.tar.gz`, `*.zip`). The + signed `SHA256SUMS.txt` covers them transitively; verification + requires the two-step flow above. +- The VSIX (`rivet-sdlc-X.Y.Z.vsix`). Tracked as a separate workstream. +- The git tag's signing status (rivet relies on the maintainer's GPG + key for tag signatures; the rivet project does not currently + distribute a maintainer keylist). +- The compliance report tarball (`*-compliance-report.tar.gz`). It is + reproducible from source, but the tarball as shipped is unsigned. + +This list reflects v0.10.0+ state. Future workstreams may close some +of these. + +## Manual republish (when CI couldn't complete) + +If `build-test-evidence` or another non-blocking job fails such that +the binaries built but the release page wasn't populated, the +maintainer can republish from the workflow artifacts: + +```bash +# Download the artifacts from the failed workflow run. +RUN_ID= +gh run download "$RUN_ID" --dir /tmp/release-staging + +# Collect into a staging dir. +cd /tmp/release-staging +mkdir -p out +find . -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.vsix" \) -exec cp {} out/ \; +cd out +sha256sum * > SHA256SUMS.txt + +# Sign locally (you need cosign installed and a Sigstore identity). +# NB: a local cosign signature uses YOUR identity, not the CI identity. +# Consumers must verify against the maintainer's published identity, +# not the workflow's. Prefer re-running the workflow when possible. +cosign sign-blob --bundle SHA256SUMS.txt.cosign.bundle SHA256SUMS.txt + +# Create the release. +gh release create vX.Y.Z --title "Rivet vX.Y.Z" --notes-file CHANGES.md ./* +``` + +This path was used for the v0.10.0 cold-republish; see the release +workflow patch in #294 for the structural fix that prevents future +hand-publishes.