diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d1603e..98dc1ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,6 +118,7 @@ jobs: attestations: write # store the attestation env: TAG: ${{ needs.release-please.outputs.tag_name }} + VERSION: ${{ needs.release-please.outputs.version }} steps: - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: @@ -129,21 +130,49 @@ jobs: run: | set -euo pipefail (cd dist && sha256sum --strict -c SHA256SUMS) + # The version names the bundle file. An empty or wrong version + # would go out in a release that nobody can change. + if [ "$TAG" != "v${VERSION}" ]; then + echo "::error::The tag ${TAG} does not match the version ${VERSION}. See docs/releasing.md." + exit 1 + fi tagged="$(gh api "repos/${GITHUB_REPOSITORY}/commits/refs/tags/${TAG}" --jq .sha)" if [ "$tagged" != "$GITHUB_SHA" ]; then echo "::error::The tag ${TAG} points to ${tagged}, not to ${GITHUB_SHA}. See docs/releasing.md." exit 1 fi - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + id: attest with: subject-path: dist/*.tar.gz + # The attestation is also a file of the release. With this file, + # gh attestation verify --bundle needs no GitHub login. One bundle + # covers both tarballs. + - name: Save the attestation bundle + env: + BUNDLE: ${{ steps.attest.outputs.bundle-path }} + run: cp -- "$BUNDLE" "dist/nodkey_${VERSION}.sigstore.json" + # The same check as in docs/releasing.md, with no login. A bundle that + # fails it never goes out: the release stays a draft. + - name: Check the tarballs with the bundle + run: | + set -euo pipefail + gh --version + for f in dist/*.tar.gz; do + gh attestation verify "$f" \ + --bundle "dist/nodkey_${VERSION}.sigstore.json" \ + --repo "$GITHUB_REPOSITORY" \ + --cert-identity "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@refs/heads/main" \ + --deny-self-hosted-runners + echo "$f: OK" + done # release-please made a draft release (see release-please-config.json). # Add the files first and publish after that, so the release is never # public without its files. Immutable releases need this order. - name: Upload the files to the draft release env: GH_TOKEN: ${{ github.token }} - run: gh release upload "$TAG" dist/*.tar.gz dist/SHA256SUMS --clobber --repo "$GITHUB_REPOSITORY" + run: gh release upload "$TAG" dist/*.tar.gz dist/SHA256SUMS "dist/nodkey_${VERSION}.sigstore.json" --clobber --repo "$GITHUB_REPOSITORY" - name: Publish the release env: GH_TOKEN: ${{ github.token }} diff --git a/DESIGN.md b/DESIGN.md index d7dcb51..a744140 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2625,7 +2625,9 @@ bws 2.1.0 and bw 2026.9.1 on 2026-10-14 - release-please keeps a release pull request open. Merging it tags the version and creates the GitHub Release. A job then builds static Linux binaries (`CGO_ENABLED=0`, amd64 and arm64, `-trimpath`), a - `SHA256SUMS` file, and a build provenance attestation. + `SHA256SUMS` file, and a build provenance attestation. The attestation + is also a file of the release (`nodkey_.sigstore.json`), so it + can be checked with no GitHub login. ## 19. How the work is done diff --git a/docs/releasing.md b/docs/releasing.md index a3ae329..e924ff0 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -64,8 +64,11 @@ After every push to `main`, the **Release** workflow runs release-please. `nodkey__linux_.tar.gz`, and writes `SHA256SUMS`. - `publish` gets these files from `build`. It checks them against `SHA256SUMS` and checks the tag again. Then it makes a signed build - provenance attestation for the tarballs, uploads the files to the draft - release and publishes it. + provenance attestation for the tarballs. It saves the attestation as + one more file, `nodkey_.sigstore.json` (a Sigstore bundle + for both tarballs), and checks both tarballs with it, with no login, + as in [Verify a download](#verify-a-download). Then it uploads the + files to the draft release and publishes it. The release is never public without its files. Immutable releases are on for this repository: after a release is published, nobody can change its @@ -269,33 +272,107 @@ deleted it can still work for up to one hour. ## Verify a download -Download the tarball for your machine and `SHA256SUMS` from the release -page, or use `gh`: +A release has these files: + +- `nodkey__linux_amd64.tar.gz` and + `nodkey__linux_arm64.tar.gz`: the binary, `LICENSE` and + `README.md`; +- `SHA256SUMS`: the SHA-256 of both tarballs; +- `nodkey_.sigstore.json`: the build provenance attestation of + both tarballs, as a Sigstore bundle. 0.1.0 does not have this file. + +Set the version you want, and your machine's arch: `amd64`, or `arm64` +on 64-bit ARM. + +```sh +VERSION=0.1.0 +ARCH=amd64 +``` + +Then download the tarball, `SHA256SUMS` and the bundle file, with `curl` +or with `gh`. 0.1.0 has no bundle file, so skip that download for 0.1.0: ```sh -gh release download v0.1.0 --repo code6x/nodkey \ - --pattern 'nodkey_0.1.0_linux_amd64.tar.gz' --pattern SHA256SUMS +U="https://github.com/code6x/nodkey/releases/download/v$VERSION" +curl -fLO "$U/nodkey_${VERSION}_linux_${ARCH}.tar.gz" +curl -fLO "$U/SHA256SUMS" +curl -fLO "$U/nodkey_${VERSION}.sigstore.json" ``` +```sh +gh release download "v$VERSION" --repo code6x/nodkey \ + --pattern "nodkey_${VERSION}_linux_${ARCH}.tar.gz" --pattern SHA256SUMS \ + --pattern "nodkey_${VERSION}.sigstore.json" +``` + +`curl` needs no login. For `gh release download`, gh older than 2.96.0 +needs a login (`gh auth login`), even for this public repository. + 1. Check the checksum: ```sh sha256sum -c --ignore-missing SHA256SUMS ``` - It must print `nodkey_0.1.0_linux_amd64.tar.gz: OK`. `--ignore-missing` - skips the tarballs that you did not download. + It must print `nodkey__linux_.tar.gz: OK`. + `--ignore-missing` skips the tarballs that you did not download. The checksum alone is not enough. Whoever can publish a release can also - publish a matching `SHA256SUMS`. Always do step 2 too. + publish a matching `SHA256SUMS`. Always do steps 2 and 3 too. 2. Check the build provenance attestation. It proves that the Release - workflow on `main` of `code6x/nodkey` built this exact file, on a runner - that GitHub hosts: + workflow on `main` of `code6x/nodkey` built this exact file, on a + runner that GitHub hosts. With the bundle file of the release, gh needs + no login. This is the way for a host that must have no GitHub login, + such as the broker host: + + ```sh + gh attestation verify "nodkey_${VERSION}_linux_${ARCH}.tar.gz" \ + --bundle "nodkey_${VERSION}.sigstore.json" \ + --repo code6x/nodkey \ + --cert-identity https://github.com/code6x/nodkey/.github/workflows/release.yml@refs/heads/main \ + --deny-self-hosted-runners + ``` + + Without `--bundle`, gh gets the attestation from GitHub. Then it needs a + login, even for this public repository. Use this form for 0.1.0, which + has no bundle file: ```sh - gh attestation verify nodkey_0.1.0_linux_amd64.tar.gz \ + gh attestation verify "nodkey_${VERSION}_linux_${ARCH}.tar.gz" \ --repo code6x/nodkey \ --cert-identity https://github.com/code6x/nodkey/.github/workflows/release.yml@refs/heads/main \ --deny-self-hosted-runners ``` + + Both forms check the same thing. Both still go online to get the + Sigstore trusted root. Use gh 2.93.0 or newer: older versions have + known bugs in `gh attestation verify` + ([GHSA-fgw4-v983-mgp8](https://github.com/cli/cli/security/advisories/GHSA-fgw4-v983-mgp8), + [GHSA-8xvp-7hj6-mcj9](https://github.com/cli/cli/security/advisories/GHSA-8xvp-7hj6-mcj9)). + When no terminal is attached, gh prints nothing on success. Then check + its exit code: 0 means the check passed. + +3. Check the version. The attestation proves which workflow built the + file, but not which version it is: the tarball of an older release + passes step 2 under a newer name too. The attestation holds the file + names that the workflow gave the tarballs. Run step 2 again with + `--format json`, and check that the name of your file is in it. Add + the same `--format` and `--jq` lines to the form without `--bundle` + for 0.1.0. + + ```sh + gh attestation verify "nodkey_${VERSION}_linux_${ARCH}.tar.gz" \ + --bundle "nodkey_${VERSION}.sigstore.json" \ + --repo code6x/nodkey \ + --cert-identity https://github.com/code6x/nodkey/.github/workflows/release.yml@refs/heads/main \ + --deny-self-hosted-runners \ + --format json --jq '.[].verificationResult.statement.subject[].name' | + grep -Fx "nodkey_${VERSION}_linux_${ARCH}.tar.gz" + ``` + + It must print the name of your file. If it prints nothing, stop: the + file is not the version that its name says. On the machine that the + binary is for, `nodkey version` must also print the version, after you + take it out of the tarball (`tar -xzf nodkey`, then + `./nodkey version`).