diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 043df80..1a109ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, development] pull_request: permissions: @@ -15,9 +15,11 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The newest Go release, so that binaries get its security fixes. - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version-file: go.mod + go-version: stable + check-latest: true - run: make vet test race # Builds every release target with the release config, without signing. @@ -27,10 +29,26 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The newest Go release, so that binaries get its security fixes. - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version-file: go.mod + go-version: stable + check-latest: true + # GoReleaser makes each binary's SBOM with Syft. + - uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + syft-version: v1.54.1 - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: version: v2.18.3 args: release --snapshot --clean + + # Fails when the code calls a function with a known vulnerability, in a + # module or in the Go standard library. + vulncheck: + runs-on: ubuntu-latest + steps: + - uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0 + with: + go-version-input: stable + check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91c8204..6982e65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write # to sign the attestation with Sigstore + attestations: write # to store the attestation with GitHub steps: # GoReleaser skips signing when MACOS_SIGN_P12 is empty, and a missing # secret arrives as an empty string. Fail rather than publish unsigned @@ -54,12 +56,18 @@ jobs: with: fetch-depth: 0 # the changelog needs the previous tag persist-credentials: false + # The newest Go release, so that binaries get its security fixes. - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version-file: go.mod + go-version: stable + check-latest: true cache: false # build release binaries from a clean module cache - run: make vet test race + # GoReleaser makes each binary's SBOM with Syft. + - uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + syft-version: v1.54.1 - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: version: v2.18.3 @@ -72,3 +80,13 @@ jobs: MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + + # Sign a SLSA build provenance attestation for every file in + # SHA256SUMS, each binary and its SBOM: it records that this workflow + # built the file from the tagged commit. "gh attestation verify" + # checks it; see the README. The release is already published by now, + # so if this step fails, delete the GitHub release (not the tag) and + # re-run the workflow. + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-checksums: dist/SHA256SUMS diff --git a/.goreleaser.yaml b/.goreleaser.yaml index db4a763..6e6aaa8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -24,6 +24,17 @@ archives: - formats: [binary] name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" +# A CycloneDX SBOM for each binary, named like it: rypt-linux-amd64.cdx.json +# and so on. Syft reads the Go version and the modules from the binary's +# build information, so the SBOM lists exactly what was compiled in. +sboms: + - id: binaries + artifacts: binary + cmd: syft + args: ["$artifact", "--output", "cyclonedx-json=$document", "--quiet"] + documents: + - "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}.cdx.json" + checksum: name_template: SHA256SUMS algorithm: sha256 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3daa2ba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +## Branches + +- `main` is what has been released. It changes only through pull requests, + and every release is a tag on it. +- `development` collects the work for the next release. +- Work happens on a short-lived branch from `development`, named for what it + does: `feature/...`, `fix/...`, `docs/...`, `ci/...`. Open a pull request + into `development`, and delete the branch once it is merged. +- A release is a pull request from `development` into `main`, followed by a + tag on `main`; see [docs/releasing.md](docs/releasing.md). If `main` then + has commits `development` lacks, merge `main` back into `development`. +- An urgent fix branches from `main` as `hotfix/...`, merges into `main` and + is released as a patch, and is then merged into `development`. + +Both `main` and `development` are protected: a change needs a pull request +whose `test` and `build` checks pass, and nobody, admins included, can push +to them directly or force-push. + +Keep commits focused, with messages in the imperative. Rebase a branch onto +`development` rather than merging `development` into it. + +## Tests + +See [Development](README.md#development) in the README. diff --git a/README.md b/README.md index c439e05..eb467b8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,24 @@ that `go install` records. To check a release or `make dist` output, run (`shasum -a 256 -c --ignore-missing SHA256SUMS` on macOS) in the directory that holds it. +### Verify a release + +From v0.2.0 on, every release file has a signed build provenance +attestation, which shows that this repository's release workflow built it +from the tagged commit. A checksum only shows that a file matches +`SHA256SUMS`, which is published beside it; the attestation shows where both +came from. With the [GitHub CLI](https://cli.github.com): + +```sh +gh attestation verify rypt-linux-amd64 \ + --repo x6c-co/rypt-cli \ + --signer-workflow x6c-co/rypt-cli/.github/workflows/release.yml +``` + +The same command checks the `rypt` that Homebrew installed: give it +`"$(command -v rypt)"` instead of a file name. Each binary's SBOM +(`rypt-linux-amd64.cdx.json` and so on) is attested the same way. + ## Usage ``` diff --git a/docs/releasing.md b/docs/releasing.md index 8fe4523..d132d69 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,5 +1,15 @@ # Releasing rypt +A release is a pull request from `development` into `main` (see +[CONTRIBUTING.md](../CONTRIBUTING.md)). Once it is merged, tag the merge +commit on `main`: + +```sh +git switch main && git pull +git tag -a v1.0.0 -m v1.0.0 +git push origin v1.0.0 +``` + Pushing a tag that starts with `v` runs [.github/workflows/release.yml](../.github/workflows/release.yml). It runs `make vet test race`, builds the six binaries with GoReleaser @@ -8,11 +18,6 @@ binaries, publishes a GitHub release with the binaries, `SHA256SUMS`, and a changelog of the commits since the previous tag, and then updates the Homebrew cask in [x6c-co/homebrew-tap](https://github.com/x6c-co/homebrew-tap). -```sh -git tag -a v1.0.0 -m v1.0.0 -git push origin v1.0.0 -``` - A tag with a pre-release suffix, such as `v1.1.0-rc.1`, is published as a pre-release and leaves the Homebrew cask alone. `rypt version` prints the tag. @@ -20,8 +25,18 @@ The release binaries are named and checksummed the same way as `make dist` output: `rypt-linux-amd64`, `rypt-darwin-arm64`, `rypt-windows-amd64.exe`, and so on. +Each binary has a CycloneDX SBOM beside it, named like it +(`rypt-linux-amd64.cdx.json`), which GoReleaser makes with Syft from the +binary's own build information: the Go version and every module compiled in. +The SBOMs are listed in `SHA256SUMS` too, and the workflow's last step signs a +build provenance attestation for every file in `SHA256SUMS` with +[actions/attest](https://github.com/actions/attest); the README shows how to +verify one. The release workflow builds with +the newest Go release, whatever go.mod's `go` line says, so that binaries +get the standard library's security fixes. + [.github/workflows/ci.yml](../.github/workflows/ci.yml) runs the same tests on -every push to `main` and every pull request, and builds all six targets with +every push to `main` and `development` and every pull request, and builds all six targets with the release configuration, unsigned. To build locally with the release configuration, unsigned, install diff --git a/go.mod b/go.mod index a5172f8..4b89f14 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/x6c-co/rypt-cli -go 1.27.0 +go 1.27.2 require ( github.com/x6c-co/rypt v0.1.0