Skip to content
Merged
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
31 changes: 30 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}
Expand Down
4 changes: 3 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<version>.sigstore.json`), so it
can be checked with no GitHub login.

## 19. How the work is done

Expand Down
101 changes: 89 additions & 12 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ After every push to `main`, the **Release** workflow runs release-please.
`nodkey_<version>_linux_<arch>.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_<version>.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
Expand Down Expand Up @@ -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_<version>_linux_amd64.tar.gz` and
`nodkey_<version>_linux_arm64.tar.gz`: the binary, `LICENSE` and
`README.md`;
- `SHA256SUMS`: the SHA-256 of both tarballs;
- `nodkey_<version>.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_<version>_linux_<arch>.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 <tarball> nodkey`, then
`./nodkey version`).
Loading