From 868951fc61d69f52756dedaf0fa6c6a047689588 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 27 Jun 2026 11:18:25 -0400 Subject: [PATCH] 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 aa3ecd4..a732bbd 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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 3a1e16e..bc04fd9 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 0000000..f562496 --- /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 0000000..6f3175d --- /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.