From 9d4d94e3be2e4b77f489cdb7730d5d4ff13c1b6b Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 27 Jun 2026 11:18:25 -0400 Subject: [PATCH 1/2] feat(release): sign release artifacts and add build provenance Signed-off-by: Joshua Temple --- .github/workflows/release.yaml | 20 +++++ .goreleaser.yaml | 45 +++++++++++- docs/cascade-release-public-key.asc | 29 ++++++++ docs/release-verification.md | 109 ++++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 docs/cascade-release-public-key.asc create mode 100644 docs/release-verification.md diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7638b8c9..6ac42ec0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -60,6 +60,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write + attestations: write steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -70,6 +72,17 @@ jobs: with: go-version: "1.25" + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + - name: Import GPG key + env: + GPG_KEY_MATERIAL: ${{ secrets.CASCADE_RELEASE_GPG_KEY }} + run: | + mkdir -p ~/.gnupg + chmod 700 ~/.gnupg + echo "$GPG_KEY_MATERIAL" | gpg --batch --import + - name: Run GoReleaser uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: @@ -78,3 +91,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_CURRENT_TAG: ${{ github.ref_name }} + GPG_FINGERPRINT: ${{ secrets.CASCADE_RELEASE_GPG_FINGERPRINT }} + + - name: Attest build provenance + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: "dist/*" + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3a1e16ed..bc04fd91 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -19,20 +19,61 @@ builds: goarch: - amd64 - arm64 + flags: + - -trimpath ldflags: - -s -w - -X main.version={{.Version}} - -X main.commit={{.Commit}} - - -X main.date={{.Date}} + - -X main.date={{.CommitDate}} + mod_timestamp: '{{ .CommitTimestamp }}' archives: - id: archives - format: tar.gz name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" checksum: name_template: "checksums.txt" +sboms: + - artifacts: archive + documents: + - "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom.spdx.json" + +signs: + # cosign keyless signing (OIDC + sigstore) + - id: cosign + cmd: cosign + args: + - sign-blob + - --output-certificate=${certificate} + - --output-signature=${signature} + - ${artifact} + - --yes + artifacts: checksum + signature: "${artifact}.sig" + certificate: "${artifact}.pem" + output: true + + # GPG signing. The release key has no passphrase (it lives only as a repo + # secret, the same trust boundary a passphrase would), so no passphrase + # handling is needed. + - id: gpg + cmd: gpg + args: + - --batch + - --no-tty + - --local-user + - "{{ .Env.GPG_FINGERPRINT }}" + - --armor + - --output + - "${signature}" + - --detach-sign + - "${artifact}" + artifacts: checksum + signature: "${artifact}.asc" + output: true + changelog: use: github sort: asc diff --git a/docs/cascade-release-public-key.asc b/docs/cascade-release-public-key.asc new file mode 100644 index 00000000..f5624962 --- /dev/null +++ b/docs/cascade-release-public-key.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGo/+XEBEADESe0XSTtG3VcsjrD7y8yXNlDYltHdkTlNDaS4hs2aNMoGYCN2 +3+RGNDLOwVBD/mprWcXwZUauKYDnnrFF6wjLqg1jsEoWKLYOmCwJa/QhRFtof1+b +Xj9Vku5E83YGij9u+6TQdJd8dz5xT4yx/dd5dshbi1LXEj7REla/lxLBdqk4bz7O +zfilr6oBVVSsvmLhZBXpXgXIRO1ROs6Qc3r5W/qM6lCCMrpBA+7Mw+sCiOj2PIvJ +xEkN8qUeYF2lPvD2jdullh6Zp8klGlnZjPGhpzrSf1MTjlK0G5zs8EVCvwyvEUFe +935dwMkpJ3DcYTypyDgBDXZPXB7KX3Z/zWWMrlWknR4I4AarF+Jk9tMjcA6L2B0G +uGyZisVxylJdUlj2LesVBfgU6xCri96+hmCESIWnHnl/KYIVm/towrXUXxgs6TfZ +572nMI3kXZR9wCXRENh9PcBsNZl7YMYufSRbqKWL20XhoxqwdbqCTAZPdX9F+RWG +34ioUwS3ZXbOR6alRsHYyGdvsqzyIYf63P/6Mpz1ye3Byjq4IYgOoJwKkjWa1McW +ZSnqVB8TskGFi0LfCHrSJBgeDUHhoFSDhIztyT4B5WcpGKl6fAp9uq65t6BvII7e +eyZyAA3iN2mTma0T6bPUIw8KB3dgY2CG789bbNdzBruv6Ufk258RS8vCRwARAQAB +tC9DYXNjYWRlIFJlbGVhc2UgU2lnbmluZyA8aW5mb0BzdGFibGVrZXJuZWwuY29t +PokCUQQTAQgAOxYhBOdFYjn1Sn92xmIfvaqvEgknHbHaBQJqP/lxAhsDBQsJCAcC +AiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEKqvEgknHbHaBSUQAIHKg+wrfTfU7mfK +uwuPtjC6aYvhdW9vSxeSOfoT8BH2KKAU0W9oC8y5ixzeuVG4NG/4JSVRz5zwmOr8 +EDy4d3f+P/qTR3l6hNR6iNyKPMtGqrnAr6w03fO1KG9jJwcelTTe158Az0PQWcx3 +pBW7cL0s8Tci1JBo+942ILpf3aO5AIz8gN9mULsW6ZlX1lS7eTA2jUpEVvIZw4qJ ++PNr1z/c4eRUsseBCCwcqvSUbGp7Y11sVSjsA64s2Ysh8jBdeU8HxsYizWNrLdYN +qdbnXcc+Stlo0QreeJO0/uhz6Z/1omqJPGXtydZrp5XAArm/eUDY9xiHvnyihprL +WDWeB9A0WIcQDJm69GwhMhopOUYM3rySZfapPG/ycqXaHfLp6jZfcGZCWBUWpEmg +oRE/GhQ757bwrZyj9CTz0DWJtdMpaY8eRwC2w6O3jbDVNgFUHgO/c8xa7dGsvMZF +dPf/uAYEPC5V5SfIoa6bxBjvaMo1YlWMXRCYqvu5N+nCFNtiABTr8sLKEkn/AZiY +522BFyj/1zsvFBt3V5Q2m6jo59mIEab8E4Vu49s9qlGRVPK09TkqbZvgxuDwUR3U +cQOMJCFjXCa2AuAm/9KMuTlsEqOVqCbR2W9NULjXgHaFJMhGx2oY2lB9CLQif0cN +pQg9BtXEuAHx3zRLgAn+nDJr09uI +=SHi3 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/docs/release-verification.md b/docs/release-verification.md new file mode 100644 index 00000000..6f3175d1 --- /dev/null +++ b/docs/release-verification.md @@ -0,0 +1,109 @@ +# Release Verification Guide + +cascade releases are signed using two cryptographic mechanisms and include SLSA build provenance to ensure authenticity and integrity. + +## Verifying cosign signatures (recommended) + +Cascade uses keyless cosign signing via Sigstore, which does not require key management. To verify a release: + +1. Install cosign (https://github.com/sigstore/cosign/releases). + +2. Download the release artifacts and signatures from the GitHub release page (checksums.txt, checksums.txt.sig, checksums.txt.pem, and the archives). + +3. Verify the checksums file signature: + +```bash +cosign verify-blob \ + --certificate=checksums.txt.pem \ + --signature=checksums.txt.sig \ + --certificate-identity-regexp='^https://github.com/stablekernel/cascade' \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ + checksums.txt +``` + +The certificate is issued by Sigstore's public certificate authority. cosign automatically verifies the certificate chain and confirms the signature was created by GitHub Actions during the release workflow run. + +4. Verify the checksums match the downloaded binaries: + +```bash +sha256sum -c checksums.txt +``` + +## Verifying GPG signatures + +For users who prefer traditional GPG verification: + +1. Obtain the cascade maintainer's public key from `docs/cascade-release-public-key.asc` in this repository and import it: + +```bash +gpg --import docs/cascade-release-public-key.asc +``` + +2. Download the release artifacts and `.asc` signature files from the GitHub release page. + +3. Verify the checksums file signature: + +```bash +gpg --verify checksums.txt.asc checksums.txt +``` + +4. If verification succeeds, verify the checksums match the downloaded binaries: + +```bash +sha256sum -c checksums.txt +``` + +## Verifying SLSA provenance + +Cascade releases include SLSA build provenance that provides cryptographic evidence about how the artifacts were built. The provenance is stored in GitHub's attestation store and verified with the GitHub CLI. To verify: + +1. Install the GitHub CLI (https://cli.github.com) if not already installed. + +2. Verify the provenance for a release artifact: + +```bash +gh attestation verify cascade_VERSION_linux_amd64.tar.gz \ + --repo stablekernel/cascade \ + --certificate-identity https://github.com/stablekernel/cascade/.github/workflows/release.yaml@refs/tags/vVERSION +``` + +This verifies that the artifact was built by the release workflow for the specified tag and that the provenance is signed by GitHub. + +## Reproducing the build + +Cascade builds are designed to be bit-for-bit reproducible using GoReleaser. To reproduce: + +1. Check out the specific release tag: + +```bash +git clone https://github.com/stablekernel/cascade.git +cd cascade +git checkout v0.X.Y +``` + +2. Ensure Go 1.25 is installed (the version used for official releases). + +3. Build with the same flags used in the release workflow: + +```bash +goreleaser build --single-target --clean --skip-post-hooks \ + --id cascade +``` + +4. Compare the output with the official release binary: + +```bash +sha256sum dist/cascade_linux_amd64/cascade +``` + +If the checksum matches the official checksums.txt, the build is reproducible. + +## Trust model + +cosign keyless signing uses an OIDC token issued by GitHub Actions during the release workflow. The token proves the signature was created during a specific GitHub Actions run in the cascade repository on the specified tag. Verification automatically confirms: + +- The signature was created by the GitHub Actions runner (not a local machine). +- It was created during a release workflow run in the cascade repository. +- It is bound to the release workflow ref (the release tag) recorded in the certificate. + +This model provides strong authenticity guarantees without requiring separate key distribution or management. From 642277ed4c44b6890b075159acda9e492a8f1303 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Tue, 30 Jun 2026 16:18:21 -0400 Subject: [PATCH 2/2] fix(release): install syft before GoReleaser for SBOM generation Signed-off-by: Joshua Temple --- .github/workflows/release.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6ac42ec0..d4e8266e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -83,6 +83,9 @@ jobs: chmod 700 ~/.gnupg echo "$GPG_KEY_MATERIAL" | gpg --batch --import + - name: Install syft + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + - name: Run GoReleaser uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: