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.