From 5369ee9ad3d18f89ab11c078517b2f88e74fc3d4 Mon Sep 17 00:00:00 2001 From: BryanFRD Date: Sun, 24 May 2026 14:33:17 +0200 Subject: [PATCH] security: cosign sign release tarballs, Docker image, and CycloneDX SBOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #510. ## What's signed - The 5 platform tarballs (linux-x64/arm64, darwin-x64/arm64, win32-x64) → .sig + .crt sidecars attached to the GitHub Release - The Docker image (ghcr.io/ferrlabs/ferrflow:latest + :vX.Y.Z) → Sigstore signature recorded in GHCR + Rekor - CycloneDX SBOM (sbom.cdx.json) → also signed via cosign ## How All keyless. The signing identity is the GitHub Actions OIDC workload identity: https://github.com/FerrLabs/FerrFlow/.github/workflows/publish.yml@refs/tags/vX.Y.Z recorded in the public Rekor transparency log. No private keys to manage or rotate. ## Verification Documented in docs/verifying-releases.md with copy-paste commands for cosign verify-blob (tarballs + SBOM) and cosign verify (Docker). ## Cost - +30s on the release workflow for the signing step - +1.5 min for cargo install cargo-cyclonedx (one-shot; could be cached via taiki-e/install-action if it becomes annoying) - SBOM size ~50-200 KB depending on dep count ## Why this matters Several compliance-focused customers asked for either SLSA provenance (already there via attest-build-provenance) or Sigstore signatures. This closes the second leg cheaply. SBOMs are increasingly a hard intake gate for vulnerability scanners (Grype, Trivy, JFrog Xray). --- .github/workflows/publish.yml | 73 ++++++++++++++++++++++ docs/verifying-releases.md | 110 ++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 docs/verifying-releases.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98455b4..9adc368 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -129,6 +129,58 @@ jobs: uses: actions/attest-build-provenance@v4 with: subject-path: artifacts/* + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + with: + cosign-release: v2.4.1 + + - name: Sign release artifacts with cosign (keyless) + # Keyless Sigstore signing — no private key to manage. Identity + # is the workflow's OIDC token, recorded in the Rekor transparency + # log. Verify with: + # cosign verify-blob \ + # --certificate ferrflow-linux-x64.tar.gz.crt \ + # --signature ferrflow-linux-x64.tar.gz.sig \ + # --certificate-identity-regexp "https://github.com/FerrLabs/FerrFlow/.*" \ + # --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + # ferrflow-linux-x64.tar.gz + env: + COSIGN_EXPERIMENTAL: "1" + run: | + set -euo pipefail + shopt -s nullglob + for artifact in artifacts/ferrflow-*.tar.gz artifacts/ferrflow-*.zip; do + [ -f "$artifact" ] || continue + echo "Signing $artifact" + cosign sign-blob --yes \ + --output-signature "${artifact}.sig" \ + --output-certificate "${artifact}.crt" \ + "$artifact" + done + + - name: Install cyclonedx-cli for SBOM + run: cargo install --locked cargo-cyclonedx + + - name: Generate SBOM (CycloneDX) + # One SBOM per build (the same dependency tree ships in every + # platform binary). Attached to the GitHub Release as a sidecar + # so downstream supply-chain scanners (grype, trivy, JFrog Xray, + # Anchore) can ingest it at intake. + run: | + set -euo pipefail + cargo cyclonedx --format json --override-filename sbom + mv sbom.cdx.json artifacts/sbom.cdx.json + echo "SBOM size: $(wc -c < artifacts/sbom.cdx.json) bytes" + + - name: Sign SBOM with cosign (keyless) + env: + COSIGN_EXPERIMENTAL: "1" + run: | + cosign sign-blob --yes \ + --output-signature artifacts/sbom.cdx.json.sig \ + --output-certificate artifacts/sbom.cdx.json.crt \ + artifacts/sbom.cdx.json - name: Wait for draft release to be visible # Defense against the race where this workflow (push:tags) fires # before `ferrflow release --draft` (running in the CI workflow @@ -326,3 +378,24 @@ jobs: subject-digest: ${{ steps.docker-push.outputs.digest }} push-to-registry: true continue-on-error: true + + - name: Install cosign for Docker signing + uses: sigstore/cosign-installer@v3 + with: + cosign-release: v2.4.1 + + - name: Sign Docker image with cosign (keyless) + # Sigstore signature stored alongside the image in GHCR. Verify + # with: + # cosign verify ghcr.io/ferrlabs/ferrflow:vX.Y.Z \ + # --certificate-identity-regexp "https://github.com/FerrLabs/FerrFlow/.*" \ + # --certificate-oidc-issuer https://token.actions.githubusercontent.com + env: + COSIGN_EXPERIMENTAL: "1" + DIGEST: ${{ steps.docker-push.outputs.digest }} + VERSION: ${{ steps.version.outputs.value }} + run: | + set -euo pipefail + cosign sign --yes "ghcr.io/ferrlabs/ferrflow@${DIGEST}" + cosign sign --yes "ghcr.io/ferrlabs/ferrflow:${VERSION}" + cosign sign --yes "ghcr.io/ferrlabs/ferrflow:latest" diff --git a/docs/verifying-releases.md b/docs/verifying-releases.md new file mode 100644 index 0000000..aa0fc45 --- /dev/null +++ b/docs/verifying-releases.md @@ -0,0 +1,110 @@ +# Verifying FerrFlow releases + +Every release tarball, the Docker image, and the SBOM that ships +alongside them are signed via [Sigstore](https://www.sigstore.dev/) +keyless signing. No public keys to track — the signing identity is the +GitHub Actions workload identity, anchored in the Rekor transparency +log. + +## What ships per release + +| Artifact | Sidecars | +|---|---| +| `ferrflow-linux-x64.tar.gz` | `.sig`, `.crt` | +| `ferrflow-linux-arm64.tar.gz` | `.sig`, `.crt` | +| `ferrflow-darwin-x64.tar.gz` | `.sig`, `.crt` | +| `ferrflow-darwin-arm64.tar.gz` | `.sig`, `.crt` | +| `ferrflow-windows-x64.zip` | `.sig`, `.crt` | +| `sbom.cdx.json` | `.sig`, `.crt` | +| `ghcr.io/ferrlabs/ferrflow:vX.Y.Z` | Cosign signature recorded in +GHCR + Rekor | + +All sidecars are downloadable from the GitHub Release page next to the +binary. + +## Verifying a tarball + +```bash +# install cosign (one-time) +curl -L https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \ + -o /usr/local/bin/cosign && chmod +x /usr/local/bin/cosign + +# download the artifact + sidecars from the release page +TAG=v5.0.1 +gh release download "$TAG" --repo FerrLabs/FerrFlow \ + -p 'ferrflow-linux-x64.tar.gz*' + +# verify +cosign verify-blob \ + --certificate ferrflow-linux-x64.tar.gz.crt \ + --signature ferrflow-linux-x64.tar.gz.sig \ + --certificate-identity-regexp "https://github.com/FerrLabs/FerrFlow/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + ferrflow-linux-x64.tar.gz +# → Verified OK +``` + +A passing verification means: + +- The tarball bytes haven't been tampered with since the release + workflow signed them. +- The signing identity was a workflow running in `FerrLabs/FerrFlow` + triggered by GitHub Actions' OIDC issuer. +- The signature is recorded in the public Rekor log ( + https://search.sigstore.dev/ — search for the `.sig` value). + +## Verifying the Docker image + +```bash +cosign verify ghcr.io/ferrlabs/ferrflow:v5.0.1 \ + --certificate-identity-regexp "https://github.com/FerrLabs/FerrFlow/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +``` + +## Verifying the SBOM + +The SBOM (`sbom.cdx.json`) is a [CycloneDX](https://cyclonedx.org/) +document listing every transitive dependency of the published binary. +It's signed the same way as the tarballs: + +```bash +cosign verify-blob \ + --certificate sbom.cdx.json.crt \ + --signature sbom.cdx.json.sig \ + --certificate-identity-regexp "https://github.com/FerrLabs/FerrFlow/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + sbom.cdx.json +``` + +Feed the verified SBOM into your scanner of choice (Grype, Trivy, +Snyk, JFrog Xray, Anchore — all CycloneDX-aware). + +## Why this matters + +- **Supply chain attacks**: an attacker who compromises a CDN, a + mirror, or a malicious typosquat package can't forge the signature + because the signing identity is anchored to the GitHub Actions OIDC + flow. +- **Compliance**: SOC2/ISO27001 customers can attest that the binary + they pulled is what their auditor approved. +- **No key management**: nobody at FerrLabs has a private signing key + to lose or rotate. The workflow proves its identity at the moment of + signing. + +## What's NOT signed + +- Source tarballs from `git archive` — these come from GitHub, not from + the release workflow. If you need an attestation for source, use + `gh attestation verify` on the build provenance. + +## Provenance (SLSA) + +In addition to Sigstore signatures, every release also ships a +[SLSA build provenance attestation](https://slsa.dev/) via +`actions/attest-build-provenance`. Verify with: + +```bash +gh attestation verify ferrflow-linux-x64.tar.gz --repo FerrLabs/FerrFlow +``` + +Tracks the workflow run, the source commit SHA, and the build inputs.