Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
110 changes: 110 additions & 0 deletions docs/verifying-releases.md
Original file line number Diff line number Diff line change
@@ -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.
Loading