From d28f32b4a8c9a54afcd77fa7ec62c632df28418e Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 28 Oct 2026 11:10:46 +1300 Subject: [PATCH 1/2] feat(cli): release builds for macOS and Linux, an install script and rack-gateway update Releases now build the CLI on three runners: darwin-arm64 (security keys, libfido2 linked statically), linux-amd64 and linux-arm64 (nofido). Each archive holds a single rack-gateway binary and ships with its checksum and its build provenance attestation bundle (.sigstore.json). scripts/install-release.sh is published as the release's install.sh: curl -fsSL https://github.com/DocSpring/rack-gateway/releases/latest/download/install.sh | sh rack-gateway update downloads the release for this platform, checks the checksum and the Sigstore bundle (signed by .github/workflows/release.yml at that release's tag), runs the new binary's build-info to check its version and that it keeps security key support, then renames it over the running binary. Release builds check GitHub once a day in the background and print a notice after the command when a newer release exists. Closes #197 --- .github/workflows/release.yml | 115 +++++--- AGENTS.md | 11 +- cmd/rack-gateway/main.go | 6 +- .../configuration/environment-variables.mdx | 1 + docs/src/content/docs/index.mdx | 4 +- .../deploy-approvals/circleci.mdx | 6 +- docs/src/content/docs/operations/upgrades.mdx | 2 +- .../docs/user-guide/cli/installation.mdx | 72 ++++- docs/src/content/docs/user-guide/index.mdx | 2 +- go.mod | 41 ++- go.sum | 178 +++++++++++- internal/cli/AGENTS.md | 35 +++ internal/cli/cli_update.go | 148 ++++++++++ internal/cli/cli_update_check.go | 83 ++++++ internal/cli/cli_update_test.go | 135 +++++++++ internal/cli/root.go | 7 +- internal/cli/update/check.go | 103 +++++++ internal/cli/update/check_test.go | 139 +++++++++ internal/cli/update/install.go | 210 ++++++++++++++ internal/cli/update/install_test.go | 273 ++++++++++++++++++ internal/cli/update/release.go | 184 ++++++++++++ internal/cli/update/release_test.go | 128 ++++++++ .../testdata/public-good-trusted-root.json | 126 ++++++++ ...-gateway-linux-amd64-v0.1.28.sigstore.json | 67 +++++ internal/cli/update/verify.go | 112 +++++++ internal/cli/update/verify_test.go | 161 +++++++++++ internal/cli/webauthn/webauthn.go | 3 + internal/cli/webauthn/webauthn_stub.go | 3 + scripts/build-cli-release.sh | 66 +++++ scripts/install-release.sh | 120 ++++++++ 30 files changed, 2470 insertions(+), 71 deletions(-) create mode 100644 internal/cli/cli_update.go create mode 100644 internal/cli/cli_update_check.go create mode 100644 internal/cli/cli_update_test.go create mode 100644 internal/cli/update/check.go create mode 100644 internal/cli/update/check_test.go create mode 100644 internal/cli/update/install.go create mode 100644 internal/cli/update/install_test.go create mode 100644 internal/cli/update/release.go create mode 100644 internal/cli/update/release_test.go create mode 100644 internal/cli/update/testdata/public-good-trusted-root.json create mode 100644 internal/cli/update/testdata/rack-gateway-linux-amd64-v0.1.28.sigstore.json create mode 100644 internal/cli/update/verify.go create mode 100644 internal/cli/update/verify_test.go create mode 100755 scripts/build-cli-release.sh create mode 100755 scripts/install-release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91c90a49..425228f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,9 +129,24 @@ jobs: subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true + # One CLI archive per platform, each built and smoke-tested on its own runner. macOS builds link libfido2 + # statically and support security keys; Linux builds use the nofido tag (go-libfido2 links libfido2 dynamically + # there). Intel Macs build from source. `rack-gateway update` and the install script download these archives, so + # each one is published with its checksum and its build provenance attestation bundle. release_build: - runs-on: ubuntu-24.04 + name: release_build (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} needs: verify_tag + strategy: + fail-fast: true + matrix: + include: + - platform: linux-amd64 + runner: ubuntu-24.04 + - platform: linux-arm64 + runner: ubuntu-24.04-arm + - platform: darwin-arm64 + runner: macos-15 permissions: contents: read checks: read # wait-for-checks.js reads check runs for the tagged commit @@ -139,6 +154,7 @@ jobs: attestations: write env: VERSION: ${{ needs.verify_tag.outputs.version }} + ASSET: rack-gateway-${{ matrix.platform }}.tar.gz steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -152,30 +168,22 @@ jobs: go-version: "1.27.3" cache: false - - name: Build binary + # The formulae come from the runner image's Homebrew snapshot (no auto-update). + - name: Install libfido2 + if: startsWith(matrix.platform, 'darwin-') + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALL_CLEANUP: "1" + run: brew install libfido2 openssl@3 + + - name: Build archive and checksum env: - CGO_ENABLED: 0 - GOOS: linux - GOARCH: amd64 GOFLAGS: -mod=readonly - run: | - set -euo pipefail - BUILD_TIME="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" - go build \ - -tags nofido \ - -buildvcs=false \ - -trimpath \ - -ldflags "-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \ - -o rack-gateway-linux-amd64 \ - ./cmd/rack-gateway/ - ./rack-gateway-linux-amd64 version | grep -F "client: ${VERSION}" - - - name: Create archive and checksum - run: | - set -euo pipefail - mkdir -p dist - tar czf dist/rack-gateway-linux-amd64.tar.gz rack-gateway-linux-amd64 - (cd dist && sha256sum rack-gateway-linux-amd64.tar.gz > rack-gateway-linux-amd64.tar.gz.sha256) + REPOSITORY: ${{ github.repository }} + run: ./scripts/build-cli-release.sh "$VERSION" "$REPOSITORY" + + - name: Check the archive is for this platform + run: test -f "dist/${ASSET}" - name: Wait for CI and E2E checks on the tagged commit uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -185,14 +193,24 @@ jobs: await script({ github, context, core }); - name: Attest binary build provenance + id: attest uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: - subject-path: dist/rack-gateway-linux-amd64.tar.gz + subject-path: dist/${{ env.ASSET }} + + - name: Save the attestation bundle + env: + BUNDLE: ${{ steps.attest.outputs.bundle-path }} + run: cp "$BUNDLE" "dist/${ASSET}.sigstore.json" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: rack-gateway-linux-amd64 - path: dist/ + name: rack-gateway-${{ matrix.platform }} + path: | + dist/${{ env.ASSET }} + dist/${{ env.ASSET }}.sha256 + dist/${{ env.ASSET }}.sigstore.json + if-no-files-found: error release_create: runs-on: ubuntu-24.04 @@ -208,33 +226,50 @@ jobs: REPOSITORY: ${{ github.repository }} IMAGE_DIGEST: ${{ needs.docker_build.outputs.digest }} steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - name: rack-gateway-linux-amd64 + pattern: rack-gateway-* path: dist + merge-multiple: true - - name: Verify checksum + - name: Verify checksums run: | set -euo pipefail cd dist - sha256sum --check --strict rack-gateway-linux-amd64.tar.gz.sha256 + for platform in linux-amd64 linux-arm64 darwin-arm64; do + sha256sum --check --strict "rack-gateway-${platform}.tar.gz.sha256" + test -s "rack-gateway-${platform}.tar.gz.sigstore.json" + done + + # Published as the release's install.sh, so .../releases/latest/download/install.sh is the latest installer. + - name: Add the install script + run: cp scripts/install-release.sh dist/install.sh - name: Generate changelog run: | set -euo pipefail { - echo "## Installation" + echo "## Install the CLI" + echo + echo "macOS (Apple Silicon) and Linux (x86_64 and arm64):" + echo + echo '```sh' + echo "curl -fsSL https://github.com/${REPOSITORY}/releases/download/${RELEASE_TAG}/install.sh | RACK_GATEWAY_VERSION=${RELEASE_TAG} sh" + echo '```' + echo + echo "Already installed? Run \`rack-gateway update\`." echo - echo "### Linux (x86_64)" + echo "The macOS build supports security keys. The Linux builds don't: build the CLI from source for that." + echo "Each archive has a SHA-256 checksum and a build provenance attestation (\`.sigstore.json\`), which" + echo "the install script and \`rack-gateway update\` check. To check one by hand:" echo echo '```sh' - echo "curl -fsSLO https://github.com/${REPOSITORY}/releases/download/${RELEASE_TAG}/rack-gateway-linux-amd64.tar.gz" - echo "curl -fsSLO https://github.com/${REPOSITORY}/releases/download/${RELEASE_TAG}/rack-gateway-linux-amd64.tar.gz.sha256" - echo "sha256sum --check rack-gateway-linux-amd64.tar.gz.sha256" echo "gh attestation verify rack-gateway-linux-amd64.tar.gz --repo ${REPOSITORY}" - echo "tar xzf rack-gateway-linux-amd64.tar.gz" - echo "sudo install -m 0755 rack-gateway-linux-amd64 /usr/local/bin/rack-gateway" - echo "rack-gateway version" echo '```' echo echo "### Docker image" @@ -249,8 +284,10 @@ jobs: - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: files: | - dist/rack-gateway-linux-amd64.tar.gz - dist/rack-gateway-linux-amd64.tar.gz.sha256 + dist/rack-gateway-*.tar.gz + dist/rack-gateway-*.tar.gz.sha256 + dist/rack-gateway-*.tar.gz.sigstore.json + dist/install.sh prerelease: ${{ contains(env.RELEASE_TAG, '-') }} body_path: changelog.md name: rack-gateway v${{ env.RELEASE_VERSION }} diff --git a/AGENTS.md b/AGENTS.md index 76c00f35..1a377f82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -617,9 +617,14 @@ a PR with green CI (admins can bypass). The Release workflow only runs on `v*` t The Release workflow (`.github/workflows/release.yml`): - Checks the tag matches `web/package.json` and waits for CI + E2E to pass on the tagged commit - Builds the Docker image without any build cache and pushes it by digest, without a tag -- Builds the CLI (`rack-gateway version` reports the release version) with checksums -- Publishes signed build provenance attestations for the image and the CLI archive -- Creates a draft GitHub release with the CLI archive and checksum +- Builds the CLI archives with `scripts/build-cli-release.sh`, each on its own runner: `darwin-arm64` (with security + key support: libfido2 linked statically from Homebrew), `linux-amd64` and `linux-arm64` (`nofido`). The script + checks the binary's `build-info` reports the release version +- Publishes signed build provenance attestations for the image and each CLI archive +- Creates a draft GitHub release with each CLI archive, its checksum and its attestation bundle (`.sigstore.json`), + and `scripts/install-release.sh` as `install.sh`. The install script and `rack-gateway update` download these, so + keep the asset names (`rack-gateway--.tar.gz`, holding one `rack-gateway` binary) and the workflow's + path (`.github/workflows/release.yml`, the signer identity the CLI checks) as they are - Only then tags the image `:vX.Y.Z` and `:` (plus `:latest` when this is the highest stable version, whatever order releases finish in) and publishes the release. If that last job fails, re-run it: nothing public points at a release that didn't finish diff --git a/cmd/rack-gateway/main.go b/cmd/rack-gateway/main.go index 841ee4ae..116d8773 100644 --- a/cmd/rack-gateway/main.go +++ b/cmd/rack-gateway/main.go @@ -18,9 +18,13 @@ func main() { cli.BuildTime = buildTime // Execute CLI - if err := cli.Execute(); err != nil { + err := cli.Execute() + if err != nil { // Errors can quote gateway responses; DisplayError keeps them from driving the terminal. fmt.Fprintf(os.Stderr, "Error: %s\n", cli.DisplayError(err)) + } + cli.PrintUpdateNotice() + if err != nil { os.Exit(1) } } diff --git a/docs/src/content/docs/configuration/environment-variables.mdx b/docs/src/content/docs/configuration/environment-variables.mdx index dd4eb57e..dd4baa9c 100644 --- a/docs/src/content/docs/configuration/environment-variables.mdx +++ b/docs/src/content/docs/configuration/environment-variables.mdx @@ -308,6 +308,7 @@ monitoring service's key, so set them as secrets. An invalid value stops the gat | `RACK_GATEWAY_URL` | Gateway URL for `RACK_GATEWAY_API_TOKEN`; without one it must match the rack's configured URL. Not allowed with several racks | | `RACK_GATEWAY_API_TOKEN` | API token for automation | | `GATEWAY_CLI_CONFIG_DIR` | Override CLI config dir (default `~/.config/rack-gateway`) | +| `RACK_GATEWAY_NO_UPDATE_CHECK` | Set to turn off the daily check for a newer CLI release (see [Updating](/user-guide/cli/installation/#updating)) | | `CONVOX_APP` | Default app for commands that take `--app` (after `--app` and before `.convox/app`) | ## Redaction diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 3bf9e1b2..ff32da6b 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -70,8 +70,8 @@ Users install the `rack-gateway` CLI, which handles authentication and wraps Con ## Quick Example ```bash -# Install the rack-gateway CLI -# (download from releases or build from source) +# Install the rack-gateway CLI (macOS on Apple Silicon, Linux) +curl -fsSL https://github.com/DocSpring/rack-gateway/releases/latest/download/install.sh | sh # Login with Google OAuth rack-gateway login staging https://gateway.example.com diff --git a/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx b/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx index 4f0b8e59..b02a0b8d 100644 --- a/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx +++ b/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx @@ -165,13 +165,13 @@ commands: - run: name: Install the rack-gateway CLI command: | - VERSION=v0.1.10 - BASE=https://github.com/docspring/rack-gateway/releases/download/$VERSION + VERSION=v0.1.29 + BASE=https://github.com/DocSpring/rack-gateway/releases/download/$VERSION curl -fsSLO $BASE/rack-gateway-linux-amd64.tar.gz curl -fsSLO $BASE/rack-gateway-linux-amd64.tar.gz.sha256 sha256sum --check rack-gateway-linux-amd64.tar.gz.sha256 tar xzf rack-gateway-linux-amd64.tar.gz - sudo install -m 0755 rack-gateway-linux-amd64 /usr/local/bin/rack-gateway + sudo install -m 0755 rack-gateway /usr/local/bin/rack-gateway ``` Set `RACK_GATEWAY_API_TOKEN` (a token with the `cicd` role) and `RACK_GATEWAY_URL` in a CircleCI context or the diff --git a/docs/src/content/docs/operations/upgrades.mdx b/docs/src/content/docs/operations/upgrades.mdx index 7d3ab873..ddf82601 100644 --- a/docs/src/content/docs/operations/upgrades.mdx +++ b/docs/src/content/docs/operations/upgrades.mdx @@ -516,7 +516,7 @@ After upgrading: - [ ] Test critical functionality (login, deploy) - [ ] Run `verify-audit`, or check the next hourly verification on the **Audit Logs** page - [ ] Monitor error rates for 24 hours -- [ ] Upgrade the CLI to the same release +- [ ] Upgrade the CLI to the same release (`rack-gateway update --version vX.Y.Z`) ## Troubleshooting Upgrades diff --git a/docs/src/content/docs/user-guide/cli/installation.mdx b/docs/src/content/docs/user-guide/cli/installation.mdx index b3fda27b..b7d74ffe 100644 --- a/docs/src/content/docs/user-guide/cli/installation.mdx +++ b/docs/src/content/docs/user-guide/cli/installation.mdx @@ -1,28 +1,50 @@ --- title: "CLI Installation" -description: "How to install the rack-gateway CLI on macOS, Linux, and Windows." +description: "How to install and update the rack-gateway CLI on macOS and Linux." --- import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components'; The `rack-gateway` CLI is distributed as a single binary. Choose the installation method that works best for your platform. -## Download from GitHub Releases (Linux x86_64) +## Install a Release -Each release publishes a Linux x86_64 build with a checksum and a signed build provenance attestation: +Releases include builds for macOS on Apple Silicon and for Linux (x86_64 and arm64). Install the latest one with: ```bash -VERSION=v0.1.10 # see https://github.com/docspring/rack-gateway/releases -curl -fsSLO https://github.com/docspring/rack-gateway/releases/download/$VERSION/rack-gateway-linux-amd64.tar.gz -curl -fsSLO https://github.com/docspring/rack-gateway/releases/download/$VERSION/rack-gateway-linux-amd64.tar.gz.sha256 -sha256sum --check rack-gateway-linux-amd64.tar.gz.sha256 -gh attestation verify rack-gateway-linux-amd64.tar.gz --repo DocSpring/rack-gateway -tar xzf rack-gateway-linux-amd64.tar.gz -sudo install -m 0755 rack-gateway-linux-amd64 /usr/local/bin/rack-gateway +curl -fsSL https://github.com/DocSpring/rack-gateway/releases/latest/download/install.sh | sh ``` -The release build has no WebAuthn support (no CGO), so it verifies MFA with TOTP, YubiKey OTP or backup codes. To -use a security key from the CLI, or on macOS and other platforms, [build from source](#building-from-source). +The script downloads the build for your machine and checks its SHA-256 checksum. When the GitHub CLI (`gh`) is +installed, it also checks the build provenance attestation: the archive must have been built by the repository's +Release workflow. It installs `rack-gateway` into `/usr/local/bin` when you can write to it, or `~/.local/bin`. + +| Variable | Description | +|----------|-------------| +| `RACK_GATEWAY_VERSION` | The release to install, such as `v0.1.29` (default: the latest release) | +| `RACK_GATEWAY_INSTALL_DIR` | Where to install the binary | +| `RACK_GATEWAY_REPOSITORY` | The GitHub repository to install from (default: `DocSpring/rack-gateway`) | + +The macOS build supports security keys (WebAuthn). The Linux builds don't: they verify MFA with TOTP, YubiKey OTP or +backup codes. To use a security key from Linux, or to run the CLI on an Intel Mac, +[build from source](#building-from-source). + +### Installing by hand + +Each release has, for each platform, an archive holding the `rack-gateway` binary, its checksum and its attestation +bundle: + +```bash +VERSION=v0.1.29 # see https://github.com/DocSpring/rack-gateway/releases +PLATFORM=linux-amd64 # or linux-arm64, darwin-arm64 +BASE=https://github.com/DocSpring/rack-gateway/releases/download/$VERSION +curl -fsSLO $BASE/rack-gateway-$PLATFORM.tar.gz +curl -fsSLO $BASE/rack-gateway-$PLATFORM.tar.gz.sha256 +shasum -a 256 --check rack-gateway-$PLATFORM.tar.gz.sha256 +gh attestation verify rack-gateway-$PLATFORM.tar.gz --repo DocSpring/rack-gateway +tar xzf rack-gateway-$PLATFORM.tar.gz +sudo install -m 0755 rack-gateway /usr/local/bin/rack-gateway +``` ## Verify Installation @@ -33,7 +55,7 @@ rack-gateway version You should see output like: ``` -client: v0.1.10 +client: 0.1.29 gateway: (not logged in) server: (not logged in) ``` @@ -121,7 +143,7 @@ Security keys (WebAuthn/FIDO2) in the CLI need a build with libfido2. Install it @@ -168,7 +190,24 @@ task go:build:cli:nofido ## Updating -To update, download the new release (Linux x86_64), or rebuild from source: +```bash +rack-gateway update +``` + +`rack-gateway update` downloads the latest release for your platform, checks its checksum and its build provenance +attestation (signed by the Release workflow for that release's tag), runs the new binary to check it works, and then +replaces the installed one. `rack-gateway update --check` only reports whether there's a newer release, and +`rack-gateway update --version v0.1.29` installs that release, even an older one. + +If you can't write to the directory the CLI is installed in, run `sudo rack-gateway update`. A Linux build from +source with security key support isn't replaced by a release, which has none: rebuild it from source instead. + +Once a day, the CLI checks GitHub for a newer release in the background, and after a command, prints a line +suggesting `rack-gateway update` when there is one. Only release builds check, and only when stderr is a terminal: +never when `CI` is set, or for `version`, `update` and `completion`. Set `RACK_GATEWAY_NO_UPDATE_CHECK=1` to turn it +off. The last result is kept in `update-check.json` in the CLI's config directory. + +If you built the CLI from source, rebuild it: ```bash cd rack-gateway @@ -204,7 +243,8 @@ chmod +x /usr/local/bin/rack-gateway ### macOS security warning -On macOS, you may see a security warning the first time you run the CLI. Either: +The install script and `rack-gateway update` download with `curl` and Go, which don't mark the binary as downloaded +from the internet. If you downloaded the archive with a browser, macOS may refuse to run it the first time. Either: 1. Right-click the binary in Finder and select "Open", then click "Open" in the dialog 2. Or run: `xattr -d com.apple.quarantine /usr/local/bin/rack-gateway` diff --git a/docs/src/content/docs/user-guide/index.mdx b/docs/src/content/docs/user-guide/index.mdx index 19482f4b..67649a2d 100644 --- a/docs/src/content/docs/user-guide/index.mdx +++ b/docs/src/content/docs/user-guide/index.mdx @@ -22,7 +22,7 @@ If you're new to Rack Gateway, start with these guides: The `rack-gateway` CLI is your primary interface for running Convox commands through the gateway. - [Overview](/user-guide/cli/) - Introduction to the CLI - - [Installation](/user-guide/cli/installation/) - The Linux release, or build from source + - [Installation](/user-guide/cli/installation/) - Install and update the CLI, or build it from source - [Authentication](/user-guide/cli/authentication/) - Login and session management - [Multi-Rack](/user-guide/cli/multi-rack/) - Work with multiple environments - [Commands](/user-guide/cli/commands/) - Complete command reference diff --git a/go.mod b/go.mod index ac90b2eb..88acbe64 100644 --- a/go.mod +++ b/go.mod @@ -33,11 +33,13 @@ require ( github.com/riverqueue/river v0.49.0 github.com/riverqueue/river/riverdriver/riverpgxv5 v0.49.0 github.com/riverqueue/river/rivertype v0.49.0 + github.com/sigstore/sigstore-go v1.3.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.12.1 github.com/swaggo/swag v1.16.6 github.com/tkrajina/typescriptify-golang-structs v0.2.0 + golang.org/x/mod v0.42.0 golang.org/x/oauth2 v0.37.0 golang.org/x/sync v0.24.0 golang.org/x/sys v0.49.0 @@ -74,6 +76,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/adrg/xdg v0.5.3 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.55.8 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect @@ -95,6 +98,7 @@ require ( github.com/aws/smithy-go v1.28.5 // indirect github.com/bearsh/hid v1.6.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bmatcuk/doublestar/v4 v4.10.2 // indirect github.com/boombuler/barcode v1.1.0 // indirect @@ -102,6 +106,7 @@ require ( github.com/bytedance/sonic v1.15.4 // indirect github.com/bytedance/sonic/loader v0.5.2 // indirect github.com/casbin/govaluate v1.10.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cert-manager/cert-manager v1.10.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.3 // indirect @@ -117,7 +122,10 @@ require ( github.com/convox/version v0.0.0-20160822184233-ffefa0d565d2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/creack/pty v1.1.24 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dlclark/regexp2/v2 v2.2.1 // indirect github.com/docker/engine v1.4.2-0.20190717161051-705d9623b7c1 // indirect @@ -138,9 +146,15 @@ require ( github.com/go-jose/go-jose/v4 v4.1.5 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.25.5 // indirect + github.com/go-openapi/errors v0.22.8 // indirect github.com/go-openapi/jsonpointer v1.0.2 // indirect github.com/go-openapi/jsonreference v1.0.3 // indirect + github.com/go-openapi/loads v0.25.0 // indirect + github.com/go-openapi/runtime v0.33.0 // indirect + github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect github.com/go-openapi/spec v1.0.1 // indirect + github.com/go-openapi/strfmt v0.27.0 // indirect github.com/go-openapi/swag v0.29.2 // indirect github.com/go-openapi/swag/cmdutils v0.29.2 // indirect github.com/go-openapi/swag/conv v0.29.2 // indirect @@ -153,6 +167,7 @@ require ( github.com/go-openapi/swag/stringutils v0.29.2 // indirect github.com/go-openapi/swag/typeutils v0.29.2 // indirect github.com/go-openapi/swag/yamlutils v0.29.2 // indirect + github.com/go-openapi/validate v0.26.1 // indirect github.com/go-playground/locales v0.14.2 // indirect github.com/go-playground/universal-translator v0.18.2 // indirect github.com/go-playground/validator/v10 v10.30.5 // indirect @@ -165,24 +180,31 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/certificate-transparency-go v1.3.3 // indirect github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/go-containerregistry v0.21.7 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/s2a-go v0.1.11 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.23 // indirect github.com/googleapis/gax-go/v2 v2.26.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/in-toto/attestation v1.2.0 // indirect + github.com/in-toto/in-toto-golang v0.11.0 // indirect github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.3 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kedacore/keda/v2 v2.21.0 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/klauspost/cpuid/v2 v2.4.1 // indirect github.com/leodido/go-urn v1.5.0 // indirect + github.com/letsencrypt/boulder v0.20260309.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mattn/go-ieproxy v0.0.12 // indirect github.com/mattn/go-isatty v0.0.24 // indirect @@ -200,6 +222,8 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/philhofer/fwd v1.2.0 // indirect @@ -216,24 +240,38 @@ require ( github.com/riverqueue/river/riverdriver v0.49.0 // indirect github.com/riverqueue/river/rivershared v0.49.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/satori/go.uuid v1.2.0 // indirect github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/sigstore/protobuf-specs v0.5.1 // indirect + github.com/sigstore/rekor v1.5.3 // indirect + github.com/sigstore/rekor-tiles/v2 v2.3.0 // indirect + github.com/sigstore/sigstore v1.10.8 // indirect + github.com/sigstore/timestamp-authority/v2 v2.1.3 // indirect github.com/sirupsen/logrus v1.10.2 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spiffe/go-spiffe/v2 v2.9.0 // indirect github.com/stretchr/objx v0.5.3 // indirect + github.com/theupdateframework/go-tuf v0.7.0 // indirect + github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect github.com/tidwall/gjson v1.20.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.2 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/tinylib/msgp v1.6.5 // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/transparency-dev/formats v0.1.1 // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.2 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.mongodb.org/mongo-driver/v2 v2.9.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.47.0 // indirect @@ -249,7 +287,6 @@ require ( go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/arch v0.32.0 // indirect golang.org/x/crypto v0.58.0 // indirect - golang.org/x/mod v0.42.0 // indirect golang.org/x/net v0.61.0 // indirect golang.org/x/text v0.43.0 // indirect golang.org/x/tools v0.52.0 // indirect diff --git a/go.sum b/go.sum index dca13fc9..59b70c09 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ cloud.google.com/go/compute/metadata v0.10.0 h1:pyKMUQSwchgkIBBJGdILqQbs/BNJXqwS cloud.google.com/go/compute/metadata v0.10.0/go.mod h1:rGFHRrIif570kSibjFTMbt6/4/tzgJWFGI/HVol4GIk= cloud.google.com/go/iam v1.14.0 h1:YRIB1/4xxwJ+NfI6RQr1WaOoHHeV0MizJEj940nBXJA= cloud.google.com/go/iam v1.14.0/go.mod h1:JTPZfbHdGXJrOYREtkRoFzCDGaBdbA7ILLKO/DDuav0= +cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE= +cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= cloud.google.com/go/logging v1.20.0 h1:l7yzCB8/JSY5PHT9RsaqdT1jaBGbM6KlSJDX210HlXM= cloud.google.com/go/logging v1.20.0/go.mod h1:ALUaJQZM691Eeujy9kFx0xREq3AMPuGd+pTRZuTYv+w= cloud.google.com/go/longrunning v1.3.0 h1:lZ6c8SJJBmILp/Wnr3vGnBLa0q+cYfSHRCcUfVI2hpI= @@ -20,13 +22,29 @@ cloud.google.com/go/storage v1.69.0 h1:jAAMC1411HEh78nKsU0Zns+eFj3TnhjAWIhg5Ud/X cloud.google.com/go/storage v1.69.0/go.mod h1:PELYsxTYm2peE4mwLEC1+mS1dA/kUSRUxNv56rOy44g= cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e h1:VsUbObBMxXlc23Eb9VeeJYE4jvTs87qa5RqSN2U5FJU= +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e/go.mod h1:32qQ5yj3R24Eu03iWFWchdC3OB653wPvoepWejkefbY= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-sdk-for-go v66.0.0+incompatible h1:bmmC38SlE8/E81nNADlgmVGurPWMHDX2YNXVQMrBpEE= github.com/Azure/azure-sdk-for-go v66.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 h1:u93s+zU2JD62im61Bm5CZIc1ZrOJaIAWEg0WOrMVkEo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1/go.mod h1:oXtinPO4OLj9d1DOTrqrL1oRwGhcqadvAmrl6wTeGlk= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 h1:MaKvxE6D0KkjOg6Wd9M00iqP5PR0kUxCfiezes4JweM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0/go.mod h1:i2h9fsTFKZorh8RdV2IcSUf/Qj98GlTkrTvUbX/s8as= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/Azure/azure-storage-file-go v0.6.0 h1:C8DY6l1s1c0mfQXC9ijI1ddDwHdIbvwoDH8agIT9ryk= github.com/Azure/azure-storage-file-go v0.6.0/go.mod h1:/En0UPyBtnVgniO08kDwCLL8letVdjIbjIeGmJeziaA= github.com/Azure/go-ansiterm v0.0.0-20261008234906-119c4d577770 h1:n3LUIJHwvkpfvfkt0dD7at4iYfQD24ML2jflWi3r2ew= @@ -61,6 +79,8 @@ github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= +github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0 h1:MDT4FxAPve5FnYn6vOL1r7RCRDG+l9cI7a5LlCuHsqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e h1:Bqtt5C+uVk+vH/t5dmB47uDCTwxw16EYHqvJnmY2aQc= github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e/go.mod h1:njRCDrl+1RQ/A/+KVU8Ho2EWAxUSkohOWczdW3dzDG0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.62.0 h1:5y2LR2pripbccOte6BpZ+guv5O5W0OQIfKvUYZrGaAQ= @@ -73,6 +93,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ= @@ -86,10 +107,14 @@ github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= @@ -120,6 +145,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 h1:QNtg+Mtj1zmepk568+UKBD5DFfqh+ESTUUqQT27JkQc= +github.com/aws/aws-sdk-go-v2/service/kms v1.52.0/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= github.com/aws/aws-sdk-go-v2/service/rds v1.130.3 h1:/r/iLO6C3fZt8h10+YT4ID2R//JD6q7k9P/7f8AsN+A= github.com/aws/aws-sdk-go-v2/service/rds v1.130.3/go.mod h1:kINuLzcAGKKdpwsN4A24D3y0gAVJDrN4/dGfkJeq9JE= github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= @@ -138,6 +165,8 @@ github.com/bearsh/hid v1.6.0 h1:eOBSuF2pg+SCytKGuGjOzZx73xQ72gevJ5IlFvzgfGE= github.com/bearsh/hid v1.6.0/go.mod h1:7JhM3r/tm4ALu4WWFqshda+Q6aIcnGRpUR08sx/dHdc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= @@ -157,6 +186,10 @@ github.com/casbin/casbin/v3 v3.11.0/go.mod h1:5rJbQr2e6AuuDDNxnPc5lQlC9nIgg6nS1z github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cert-manager/cert-manager v1.10.2 h1:2/QH9C8ffeB+t8xHYsITkY2d9ulye9a5mi1F7o+MmC0= github.com/cert-manager/cert-manager v1.10.2/go.mod h1:v3T3yAt5ASv4/9cbO42EilLsoZDlybQrh8o6RhTw/vo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -167,6 +200,8 @@ github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2 github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= github.com/convox/changes v0.0.0-20191105034405-8c0df759a3b3 h1:S0AiQtEXMb5jBw4N6TaIPMcnCLV91Se+HEOpMmYyt+o= @@ -196,10 +231,19 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= @@ -252,6 +296,8 @@ github.com/gin-contrib/sse v1.1.2 h1:MU2fgl1RrdYTMcgJLtz2kJF+vPg3xrqaaKfUUU18tCo github.com/gin-contrib/sse v1.1.2/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= +github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA= @@ -264,12 +310,24 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/analysis v0.25.5 h1:xPYEvTb90o1y0epuiOPAoG4QqahjP3cdp5xNlHeKJRI= +github.com/go-openapi/analysis v0.25.5/go.mod h1:d3UGtQC5uq5Kqqqis2VH09Km/v3vwsWrYkbp4gdm+Rc= +github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= +github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= github.com/go-openapi/jsonpointer v1.0.2 h1:NbW7xT2b+l7x+JefiPGDsZWsrLk1ogrz4AHK1TTzMt8= github.com/go-openapi/jsonpointer v1.0.2/go.mod h1:N0rI4MbNKD7xAFTiHtB+5Nhrg2j/wwYg7m7Fmb4/jk8= github.com/go-openapi/jsonreference v1.0.3 h1:5rDbq+JsjCv6/nUDDJt83nLZoADI4w03uvHXer+4aYQ= github.com/go-openapi/jsonreference v1.0.3/go.mod h1:OJSlMXHysgH272D9z2tiy2MGe5A9vQLHaCPb6KbYdE4= +github.com/go-openapi/loads v0.25.0 h1:74Bc2snfaVlsHzwdQj/3gsA9XJz3daXTJVs+4ZaK7jI= +github.com/go-openapi/loads v0.25.0/go.mod h1:JFBw4SIB9+PTIFHDfcXuSSy5h6aWzjtUCrPYyx3qWU8= +github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU= +github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw= +github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= +github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk= github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8= +github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= +github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4= github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo= github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro= @@ -300,6 +358,8 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8 github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs= github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho= github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak= +github.com/go-openapi/validate v0.26.1 h1:pZSbvtRO8G2R2FpWTYRn3w8LrsNwbtaVhP2dWiBa0Us= +github.com/go-openapi/validate v0.26.1/go.mod h1:B8UMgXiQiwwQWIbmuROlwJZDPGlikPuh7iHV1vPX9Oo= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.2 h1:d8UmcrM6Nip0hfGZKLGpAvZH37XB4TS0xzK9B56YNCY= @@ -311,6 +371,8 @@ github.com/go-playground/validator/v10 v10.30.5/go.mod h1:wEqiaov48pXX1kjhc3Da8y github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-webauthn/webauthn v0.18.2 h1:0BeftmEHU7i3Dv0VFwBtidy/ba37Vcdjvqst9EYu8Sk= @@ -323,6 +385,8 @@ github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA= github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -336,10 +400,14 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= +github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= +github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc= @@ -353,6 +421,8 @@ github.com/google/pprof v0.0.0-20260903180319-d6c3cb2f37ec h1:ZhR4jlKRUmzPEPd2RL github.com/google/pprof v0.0.0-20260903180319-d6c3cb2f37ec/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/s2a-go v0.1.11 h1:7v1V1TraNGP63WptsAkNqr2w8jgm4vM6A6qx6Lsw9iM= github.com/google/s2a-go v0.1.11/go.mod h1:pz4tyvwXvJLLbyrkh6FW1eS2zPUXMaTmyNhYtyP2tNw= +github.com/google/trillian v1.7.3 h1:hziW+vo4czis48tzx2GK5xRBl/ZxBA9B0/UR5avXOro= +github.com/google/trillian v1.7.3/go.mod h1:qh8iy4x/GvnVXUBd5pK4oncuT1Y9vVYfibQVsR/WpKg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.23 h1:J+VGF1wjAM2dU2kKUXZxJdDppRLReWP7KWcclfngQhc= @@ -372,11 +442,40 @@ github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2e github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk= +github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk= +github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= +github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -391,10 +490,16 @@ github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.3 h1:f8HoREISb3l5eO9coPgK7U3hKLhWGokOFVcThl5jrao= github.com/jackc/puddle/v2 v2.2.3/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -424,6 +529,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= +github.com/letsencrypt/boulder v0.20260309.0 h1:kZynrxK3QfqLGx6hhoz+Rfs3hgltJs1p9Mp+4+VwnY0= +github.com/letsencrypt/boulder v0.20260309.0/go.mod h1:yG8lj8pNPZ8taq3oNdTpfBS+eC74IaEuiewqzVpXiWE= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= @@ -440,6 +547,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME= github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= @@ -472,10 +581,17 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.33.0 h1:C8gBA6Uc2ZEubiV+SXiu5tZnMTwEmXHgkJwGozKtZf8= github.com/onsi/ginkgo/v2 v2.33.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.43.1 h1:vGIPFuYrIO6/0Z09s0I0QQQgFchiX4+tb1re3MScJYo= github.com/onsi/gomega v1.43.1/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -484,6 +600,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -532,13 +650,43 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= +github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= +github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+uQBDeAhHhwdzB5fSlVdhGcM= github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= +github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= +github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U= +github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= +github.com/sigstore/rekor v1.5.3 h1:0Tyolw3zreRgm7PUW8dccFLXGBThi08278jI8EXNSr4= +github.com/sigstore/rekor v1.5.3/go.mod h1:h3GK5dDqCcWJJZUJwdpKGSSmEV2GEjPUjJy3WTjBwzA= +github.com/sigstore/rekor-tiles/v2 v2.3.0 h1:HhMgH61UP0t899V8Fjt7pz1YdgOBptbaQdnCF+79cdc= +github.com/sigstore/rekor-tiles/v2 v2.3.0/go.mod h1:DEFiKSyQ4nF75QRVNdOPaIH3cmvMkO2B6xDZjNYngPc= +github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4= +github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M= +github.com/sigstore/sigstore-go v1.3.0 h1:hnIMHREyCNTYFtOE1o7ae3Axa9B5W5EjUSBJICP2NBE= +github.com/sigstore/sigstore-go v1.3.0/go.mod h1:AyRQXfpH89py1twjE3kEZxlRersng90GSYqQV9zGJE8= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8 h1:tofVQ+UWJgad/69I5zbqxdFCN5gpIn9tRQP7iBzIpBw= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8/go.mod h1:73AfJE8H6w5KGCFPBu4x/OG+i1Yxgmh0L/FtV7prd88= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8 h1:8Mt7J36GcUEmbiJaiFhz2tud5ZIgkfVVCe2H/WJCHmw= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8/go.mod h1:YiTpAsxoWXhF9KlLOVWCh7BckN5cYO8X01WufDq1ido= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8 h1:MxpAIMZVzn0Tpbarc9ax1I498oQBp7oYSMgoMSsOmKI= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8/go.mod h1:bnAUEkFNam6STvkVZhptVwWzWR5pS24CEtQ+lhxu7S0= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8 h1:1DGe4/clcdOnkz5MINEczWlmEvjUtZd+AjPPT/cBhQ8= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8/go.mod h1:6IDFhpgxtzqbnzrFkyegbj7RfWwKeRrb3/+xAD1Wp+Y= +github.com/sigstore/timestamp-authority/v2 v2.1.3 h1:Fc+LjCTfik1lh3YLkaosENfkXa3R2Y1nswiUKutBdFA= +github.com/sigstore/timestamp-authority/v2 v2.1.3/go.mod h1:myoFOKJB/u5vNTFwvBBJVkG3NnOBeIJevbfjNeasLjo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= @@ -573,6 +721,10 @@ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWD github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.4.2 h1:w7976/W8uTwlsegP5nRymlpjPgrwSh+AXUf85is6nJk= +github.com/theupdateframework/go-tuf/v2 v2.4.2/go.mod h1:JqBrIUnNLAaNq/8GmBcEMFWfAFBbqp/MkJEJseXKbks= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= @@ -584,13 +736,27 @@ github.com/tidwall/pretty v1.2.2 h1:dz1jrRuE7or/74V490B4/GP1pZm5WKlt2bgCP5A83w8= github.com/tidwall/pretty v1.2.2/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tink-crypto/tink-go-awskms/v3 v3.0.0 h1:XSohRhCkXAVI0iaCnWB/GS05TEmpnKurQmzaY1jzt3Y= +github.com/tink-crypto/tink-go-awskms/v3 v3.0.0/go.mod h1:+7MXsShLzVbSQ6dI0Pe4JuZM52jD1jQ1itAygd/MDsA= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.3.0 h1:3s6YMgMOBZRU8qG6ybpKSF2Sau+y3sMvxR911M59SwA= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.3.0/go.mod h1:X8UNvbQu2wanAGa8ixRUU/DWt1V2hUBfvPGy6s9nE2s= +github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0 h1:eXuNqgrcYelxU1MVikOJDP3wTS5lvihM4ntoAbAMfvs= +github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0/go.mod h1:3RhcxAqek6xUlRFmJifvU4CYLZN60KMQdIKqpZAZJG0= +github.com/tink-crypto/tink-go/v2 v2.7.0 h1:k7QnUXJ1cRDpvoy/5l1FimZqMAArRff8vjUqzi5N04o= +github.com/tink-crypto/tink-go/v2 v2.7.0/go.mod h1:cWNpQ/yAT/QHzAV0kBGMOSJzzYTKofDZdJaUqOPPWCI= github.com/tinylib/msgp v1.6.5 h1:iAH6XTP7BpzErjBZlujJQXxA+GmRi2YWs6M4KdLtNbE= github.com/tinylib/msgp v1.6.5/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/tkrajina/typescriptify-golang-structs v0.2.0 h1:ZedWk82egydDspGTryAatbX0/1NZDQbdiZLoCbOk4f8= github.com/tkrajina/typescriptify-golang-structs v0.2.0/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= +github.com/transparency-dev/formats v0.1.1 h1:4bVHJc+KdBgpA1OJD1yjI+g0i5Z1graCppTMH8lWKJI= +github.com/transparency-dev/formats v0.1.1/go.mod h1:qtZ8goRuJ8FTBG9c9+Bj0rn2rUG7eG/AUTkr+Aw3jFw= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc= @@ -603,9 +769,13 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= go.mongodb.org/mongo-driver/v2 v2.9.2 h1:OCAdjI37pC/SVxucz/9CM8zD4jCdegtsY5QcKzYFCgE= @@ -634,6 +804,8 @@ go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.step.sm/crypto v0.77.7 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA= +go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -815,3 +987,5 @@ sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3C sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/cli/AGENTS.md b/internal/cli/AGENTS.md index a488f783..3bb20620 100644 --- a/internal/cli/AGENTS.md +++ b/internal/cli/AGENTS.md @@ -153,6 +153,41 @@ rack-gateway convox Proxies command to Convox API through the gateway with authentication. +### Update (`internal/cli/update`) + +```bash +rack-gateway update [--check] [--version vX.Y.Z] +``` + +Replaces the running binary (symlinks resolved) with a GitHub release, in this order, so a failed step leaves the +installed binary untouched: + +1. A temporary file in the binary's directory: an unwritable directory fails before anything is downloaded, with a + `sudo` hint, and the final rename stays on one filesystem. +2. The archive for this platform (`update.AssetName`: darwin/arm64, linux/amd64, linux/arm64), its `.sha256` and + its `.sigstore.json`, over HTTPS from GitHub only (`isGitHubURL` also checks every redirect), with size limits. +3. The checksum, then the Sigstore bundle (`ProvenanceVerifier`): a SLSA provenance attestation for the archive's + digest, signed by `.github/workflows/release.yml@refs/tags/` in `update.Repository` with GitHub Actions' + OIDC issuer. The trusted root comes from the public Sigstore instance through TUF, with no local cache. +4. The `rack-gateway` file from the archive (a regular file only), then the new binary's hidden `build-info` + command: it must report the release's version, and keep security key support (`webauthn.Supported`) when this + build has it. Then the rename. + +Development builds (`Version` isn't a release) refuse to update. Without `--version`, it never downgrades. +`update.Repository` defaults to `DocSpring/rack-gateway`; `scripts/build-cli-release.sh` sets it to the repository +that builds the release. + +The daily check (`cli_update_check.go`): the root command's `PersistentPreRun` starts `update.Checker` in the +background when the saved result (`update-check.json` in the config dir) is a day old, and `main` prints the notice +after the command and its error, waiting at most a second for a check still running. A canceled check isn't saved, +a failed one is (so an offline machine doesn't retry on every command). It's skipped for development builds, when +stderr isn't a terminal, when `CI` or `RACK_GATEWAY_NO_UPDATE_CHECK` is set, and for the commands in +`skipUpdateCheckCommands` (including cobra's completion requests). + +`update/testdata` holds the v0.1.28 Linux archive's real attestation bundle and the public Sigstore trusted root +from October 2026, so a test checks the production verifier options against what `actions/attest-build-provenance` +creates. The other verifier tests sign with sigstore-go's `VirtualSigstore`. + ## Testing ### E2E Tests diff --git a/internal/cli/cli_update.go b/internal/cli/cli_update.go new file mode 100644 index 00000000..6986899f --- /dev/null +++ b/internal/cli/cli_update.go @@ -0,0 +1,148 @@ +package cli + +import ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/spf13/cobra" + + "github.com/DocSpring/rack-gateway/internal/cli/update" + "github.com/DocSpring/rack-gateway/internal/cli/webauthn" +) + +const updateTimeout = 10 * time.Minute + +// UpdateCommand returns the update command, which replaces this binary with a verified release from GitHub. +func UpdateCommand() *cobra.Command { + var checkOnly bool + var version string + cmd := &cobra.Command{ + Use: "update", + Short: "Update the rack-gateway CLI to the latest release", + Long: `Update the rack-gateway CLI to the latest release on GitHub. + +The update checks the release archive's SHA-256 checksum and its build provenance attestation, which must be +signed by the repository's Release workflow for that release's tag, before it replaces this binary. + +Releases include builds for macOS on Apple Silicon and for Linux (x86_64 and arm64). The Linux builds have no +security key support, so a Linux build with it isn't replaced: build that one from source.`, + Args: cobra.NoArgs, + RunE: SilenceOnError(func(cmd *cobra.Command, _ []string) error { + ctx, cancel := context.WithTimeout(cmd.Context(), updateTimeout) + defer cancel() + return runUpdate(ctx, cmd, update.NewClient(Version, time.Minute), checkOnly, version) + }), + } + cmd.Flags().BoolVar(&checkOnly, "check", false, "Only check whether a newer release is available") + cmd.Flags().StringVar(&version, "version", "", + "Install this release (for example v0.1.29), even when it's older than this one") + return cmd +} + +func runUpdate(ctx context.Context, cmd *cobra.Command, client *update.Client, checkOnly bool, version string) error { + if !update.IsReleaseVersion(Version) { + return fmt.Errorf("this is a development build (%s): install a release with the install script instead", + Version) + } + rel, err := targetRelease(ctx, client, version) + if err != nil { + return err + } + out := cmd.OutOrStdout() + switch { + case rel.Version() == Version: + _, _ = fmt.Fprintf(out, "rack-gateway %s is the latest release.\n", Version) + return nil + case version == "" && update.IsNewer(Version, rel.Version()): + _, _ = fmt.Fprintf(out, "rack-gateway %s is newer than the latest release (%s).\n", Version, rel.Version()) + return nil + case checkOnly: + _, _ = fmt.Fprintf(out, "rack-gateway %s is available (this is %s): %s\n", + rel.Version(), Version, client.ReleaseURL(rel.Tag)) + _, _ = fmt.Fprintln(out, `Run "rack-gateway update" to install it.`) + return nil + } + return installRelease(ctx, cmd, client, rel) +} + +// targetRelease returns the release named by --version, or the latest release. +func targetRelease(ctx context.Context, client *update.Client, version string) (*update.Release, error) { + if version == "" { + rel, err := client.LatestRelease(ctx) + if err != nil { + return nil, fmt.Errorf("failed to find the latest release: %w", err) + } + return rel, nil + } + if !update.IsReleaseVersion(version) { + return nil, fmt.Errorf("invalid --version %q (expected a release such as v0.1.29)", version) + } + tag := update.TagFor(version) + rel, err := client.ReleaseByTag(ctx, tag) + if err != nil { + return nil, fmt.Errorf("failed to find release %s: %w", tag, err) + } + return rel, nil +} + +func installRelease(ctx context.Context, cmd *cobra.Command, client *update.Client, rel *update.Release) error { + executable, err := currentExecutable() + if err != nil { + return err + } + verifier, err := update.NewProvenanceVerifier(client.Repository) + if err != nil { + return err + } + installer := &update.Installer{ + Client: client, + Verify: verifier.Verify, + BuildInfo: update.RunBuildInfo, + Executable: executable, + Current: currentBuildInfo(), + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + Out: cmd.OutOrStdout(), + } + if err := installer.Install(ctx, rel); err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Updated %s from %s to %s.\n", executable, Version, rel.Version()) + return nil +} + +// currentExecutable returns the path of the running binary, with symlinks resolved. +func currentExecutable() (string, error) { + executable, err := os.Executable() + if err != nil { + return "", fmt.Errorf("can't find the running rack-gateway binary: %w", err) + } + resolved, err := filepath.EvalSymlinks(executable) + if err != nil { + return "", fmt.Errorf("can't find the running rack-gateway binary: %w", err) + } + return resolved, nil +} + +func currentBuildInfo() update.BuildInfo { + return update.BuildInfo{Version: Version, SecurityKeys: webauthn.Supported} +} + +// BuildInfoCommand returns the hidden build-info command. `rack-gateway update` runs it on a downloaded binary to +// check that it runs on this machine, is the release it asked for and keeps security key support. +func BuildInfoCommand() *cobra.Command { + return &cobra.Command{ + Use: "build-info", + Short: "Show the CLI version and whether it supports security keys", + Hidden: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + _, err := fmt.Fprint(cmd.OutOrStdout(), currentBuildInfo().String()) + return err + }, + } +} diff --git a/internal/cli/cli_update_check.go b/internal/cli/cli_update_check.go new file mode 100644 index 00000000..e7eb03fc --- /dev/null +++ b/internal/cli/cli_update_check.go @@ -0,0 +1,83 @@ +package cli + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "time" + + "github.com/spf13/cobra" + + "github.com/DocSpring/rack-gateway/internal/cli/terminal" + "github.com/DocSpring/rack-gateway/internal/cli/update" +) + +const ( + updateCheckFile = "update-check.json" + updateCheckTimeout = 5 * time.Second + // updateNoticeWait is how long the CLI waits, after a command, for an update check that is still running. + updateNoticeWait = time.Second +) + +// skipUpdateCheckCommands never show the update notice: they're about updates and versions, or shells and scripts +// read their output. +var skipUpdateCheckCommands = map[string]bool{ + "update": true, + "version": true, + "build-info": true, + "completion": true, + "help": true, + cobra.ShellCompRequestCmd: true, + cobra.ShellCompNoDescRequestCmd: true, +} + +// pendingUpdateCheck is the update check the current command started, if any. +var pendingUpdateCheck *update.Check + +// startUpdateCheck starts a background check for a newer release, at most once a day (see update.Checker). +func startUpdateCheck(cmd *cobra.Command) { + if !updateCheckEnabled(cmd) || !terminal.StderrIsTerminal() { + return + } + checker := &update.Checker{ + Client: update.NewClient(Version, updateCheckTimeout), + Current: Version, + Load: func() ([]byte, error) { + return fs.ReadFile(os.DirFS(ConfigPath), updateCheckFile) + }, + Save: func(data []byte) error { + if err := ensurePrivateDir(ConfigPath); err != nil { + return err + } + return writePrivateFileAtomically(filepath.Join(ConfigPath, updateCheckFile), data) + }, + Now: time.Now, + } + pendingUpdateCheck = checker.Start() +} + +// updateCheckEnabled reports whether this command checks for updates: only release builds outside CI, unless +// RACK_GATEWAY_NO_UPDATE_CHECK is set. startUpdateCheck also needs stderr to be a terminal. +func updateCheckEnabled(cmd *cobra.Command) bool { + if !update.IsReleaseVersion(Version) || os.Getenv("RACK_GATEWAY_NO_UPDATE_CHECK") != "" || os.Getenv("CI") != "" { + return false + } + for c := cmd; c != nil; c = c.Parent() { + if skipUpdateCheckCommands[c.Name()] { + return false + } + } + return true +} + +// PrintUpdateNotice prints a notice to stderr when a newer release is available. main calls it after the command, +// and after its error. +func PrintUpdateNotice() { + if pendingUpdateCheck == nil { + return + } + if notice := pendingUpdateCheck.Notice(updateNoticeWait); notice != "" { + _, _ = fmt.Fprintf(os.Stderr, "\n%s\n", displayText(notice)) + } +} diff --git a/internal/cli/cli_update_test.go b/internal/cli/cli_update_test.go new file mode 100644 index 00000000..cf36a404 --- /dev/null +++ b/internal/cli/cli_update_test.go @@ -0,0 +1,135 @@ +package cli + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/DocSpring/rack-gateway/internal/cli/update" + "github.com/DocSpring/rack-gateway/internal/cli/webauthn" +) + +func setVersion(t *testing.T, version string) { + t.Helper() + previous := Version + Version = version + t.Cleanup(func() { Version = previous }) +} + +// releaseClient returns a release client whose GitHub API serves the latest release and release v0.1.27. +func releaseClient(t *testing.T, latest string) *update.Client { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/repos/DocSpring/rack-gateway/releases/latest": + _, _ = w.Write([]byte(`{"tag_name":"` + latest + `"}`)) + case "/repos/DocSpring/rack-gateway/releases/tags/v0.1.27": + _, _ = w.Write([]byte(`{"tag_name":"v0.1.27"}`)) + default: + http.NotFound(w, r) + } + })) + t.Cleanup(server.Close) + client := update.NewClient("test", 5*time.Second) + client.APIBaseURL = server.URL + client.DownloadBaseURL = "https://github.com" + client.Repository = "DocSpring/rack-gateway" + return client +} + +func runUpdateForTest(t *testing.T, client *update.Client, checkOnly bool, version string) (string, error) { + t.Helper() + var out bytes.Buffer + cmd := &cobra.Command{} + cmd.SetOut(&out) + err := runUpdate(context.Background(), cmd, client, checkOnly, version) + return out.String(), err +} + +func TestUpdateWhenAlreadyCurrentOrNewer(t *testing.T) { + setVersion(t, "0.1.28") + + out, err := runUpdateForTest(t, releaseClient(t, "v0.1.28"), false, "") + require.NoError(t, err) + require.Equal(t, "rack-gateway 0.1.28 is the latest release.\n", out) + + out, err = runUpdateForTest(t, releaseClient(t, "v0.1.27"), false, "") + require.NoError(t, err) + require.Equal(t, "rack-gateway 0.1.28 is newer than the latest release (0.1.27).\n", out) +} + +func TestUpdateCheckOnlyReportsTheRelease(t *testing.T) { + setVersion(t, "0.1.28") + + out, err := runUpdateForTest(t, releaseClient(t, "v0.1.29"), true, "") + require.NoError(t, err) + require.Equal(t, "rack-gateway 0.1.29 is available (this is 0.1.28): "+ + "https://github.com/DocSpring/rack-gateway/releases/tag/v0.1.29\n"+ + "Run \"rack-gateway update\" to install it.\n", out) + + // --version names a release even when it's older than this one. + out, err = runUpdateForTest(t, releaseClient(t, "v0.1.29"), true, "0.1.27") + require.NoError(t, err) + require.Contains(t, out, "rack-gateway 0.1.27 is available (this is 0.1.28)") +} + +func TestUpdateRejectsDevBuildsAndBadVersions(t *testing.T) { + setVersion(t, "dev") + _, err := runUpdateForTest(t, releaseClient(t, "v0.1.29"), false, "") + require.ErrorContains(t, err, "this is a development build (dev)") + + setVersion(t, "0.1.28") + _, err = runUpdateForTest(t, releaseClient(t, "v0.1.29"), false, "latest") + require.ErrorContains(t, err, `invalid --version "latest"`) + _, err = runUpdateForTest(t, releaseClient(t, "v0.1.29"), false, "v0.1.99") + require.ErrorContains(t, err, "failed to find release v0.1.99") +} + +func TestUpdateCheckEnabled(t *testing.T) { + root := &cobra.Command{Use: "rack-gateway"} + apps := &cobra.Command{Use: "apps"} + completion := &cobra.Command{Use: "completion"} + zsh := &cobra.Command{Use: "zsh"} + completion.AddCommand(zsh) + root.AddCommand(apps, completion, UpdateCommand(), VersionCommand(), BuildInfoCommand()) + t.Setenv("CI", "") + t.Setenv("RACK_GATEWAY_NO_UPDATE_CHECK", "") + + setVersion(t, "0.1.28") + require.True(t, updateCheckEnabled(apps)) + require.True(t, updateCheckEnabled(root)) + for _, name := range []string{"update", "version", "build-info"} { + cmd, _, err := root.Find([]string{name}) + require.NoError(t, err) + require.False(t, updateCheckEnabled(cmd), name) + } + require.False(t, updateCheckEnabled(zsh)) + + t.Setenv("CI", "true") + require.False(t, updateCheckEnabled(apps)) + t.Setenv("CI", "") + t.Setenv("RACK_GATEWAY_NO_UPDATE_CHECK", "1") + require.False(t, updateCheckEnabled(apps)) + t.Setenv("RACK_GATEWAY_NO_UPDATE_CHECK", "") + setVersion(t, "dev") + require.False(t, updateCheckEnabled(apps)) +} + +func TestBuildInfoCommand(t *testing.T) { + setVersion(t, "0.1.28") + var out bytes.Buffer + cmd := BuildInfoCommand() + cmd.SetOut(&out) + cmd.SetArgs([]string{}) + require.NoError(t, cmd.Execute()) + + info, err := update.ParseBuildInfo(out.Bytes()) + require.NoError(t, err) + require.Equal(t, update.BuildInfo{Version: "0.1.28", SecurityKeys: webauthn.Supported}, info) +} diff --git a/internal/cli/root.go b/internal/cli/root.go index f891f9d6..834a5275 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -27,7 +27,10 @@ Rack management: rack-gateway switch # Switch to a different rack rack-gateway login # Login to a new rack`, // Every step of a command uses the rack that was current when it started. - PersistentPreRun: func(*cobra.Command, []string) { pinCurrentRack() }, + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + pinCurrentRack() + startUpdateCheck(cmd) + }, RunE: func(cmd *cobra.Command, _ []string) error { // If no subcommand is specified, show help return cmd.Help() @@ -53,6 +56,8 @@ Rack management: SwitchCommand(), RacksCommand(), VersionCommand(), + UpdateCommand(), + BuildInfoCommand(), GatewayCommand(), WebCommand(), CompletionCommand(), diff --git a/internal/cli/update/check.go b/internal/cli/update/check.go new file mode 100644 index 00000000..9b0dc436 --- /dev/null +++ b/internal/cli/update/check.go @@ -0,0 +1,103 @@ +package update + +import ( + "context" + "encoding/json" + "fmt" + "runtime" + "time" +) + +// CheckInterval is how long the CLI waits after checking GitHub before it checks again. +const CheckInterval = 24 * time.Hour + +// checkState is what the last update check found. The CLI saves it in its config directory. +type checkState struct { + CheckedAt time.Time `json:"checked_at"` + LatestTag string `json:"latest_tag,omitempty"` +} + +// Checker checks GitHub for a newer release at most once per CheckInterval. +type Checker struct { + Client *Client + Current string + // Load returns the saved state, or an error when there is none. Save replaces it. + Load func() ([]byte, error) + Save func([]byte) error + Now func() time.Time +} + +// Check is an update check that may still be running in the background. +type Check struct { + client *Client + current string + saved checkState + done chan struct{} + cancel context.CancelFunc + fresh *checkState +} + +// Start begins a background check when the last one is more than CheckInterval old. It never blocks. +func (c *Checker) Start() *Check { + check := &Check{client: c.Client, current: c.Current} + if data, err := c.Load(); err == nil { + _ = json.Unmarshal(data, &check.saved) + } + if c.Now().Sub(check.saved.CheckedAt) < CheckInterval { + return check + } + ctx, cancel := context.WithCancel(context.Background()) + check.done = make(chan struct{}) + check.cancel = cancel + go func() { + defer close(check.done) + check.fresh = c.fetch(ctx, check.saved) + }() + return check +} + +// fetch asks GitHub for the latest release and saves what it found. A failed check is saved too, so an offline +// machine doesn't try on every command, but one canceled because the command finished first is not. +func (c *Checker) fetch(ctx context.Context, saved checkState) *checkState { + state := checkState{CheckedAt: c.Now(), LatestTag: saved.LatestTag} + rel, err := c.Client.LatestRelease(ctx) + if err != nil && ctx.Err() != nil { + return nil + } + if err == nil { + state.LatestTag = rel.Tag + } + if data, err := json.Marshal(state); err == nil { + _ = c.Save(data) + } + return &state +} + +// Notice returns the message to show when a newer release is available, or "". It waits at most wait for a check +// still running, then cancels it and uses the last saved result. +func (k *Check) Notice(wait time.Duration) string { + state := k.saved + if k.done != nil { + timer := time.NewTimer(wait) + select { + case <-k.done: + if k.fresh != nil { + state = *k.fresh + } + case <-timer.C: + } + timer.Stop() + k.cancel() + } + if state.LatestTag == "" || !IsNewer(state.LatestTag, k.current) { + return "" + } + latest := Release{Tag: state.LatestTag} + if _, ok := AssetName(runtime.GOOS, runtime.GOARCH); !ok { + return fmt.Sprintf("A new release of rack-gateway is available: %s → %s. Build it from source: %s", + k.current, latest.Version(), k.client.ReleaseURL(latest.Tag)) + } + return fmt.Sprintf( + "A new release of rack-gateway is available: %s → %s. Run \"rack-gateway update\" to install it.", + k.current, latest.Version()) +} diff --git a/internal/cli/update/check_test.go b/internal/cli/update/check_test.go new file mode 100644 index 00000000..2ae3a2e5 --- /dev/null +++ b/internal/cli/update/check_test.go @@ -0,0 +1,139 @@ +package update + +import ( + "encoding/json" + "errors" + "net/http" + "runtime" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var testNow = time.Date(2026, 10, 28, 12, 0, 0, 0, time.UTC) + +type memoryState struct { + data []byte + saves atomic.Int32 +} + +func (m *memoryState) checker(client *Client, current string) *Checker { + return &Checker{ + Client: client, + Current: current, + Load: func() ([]byte, error) { + if m.data == nil { + return nil, errors.New("no state") + } + return m.data, nil + }, + Save: func(data []byte) error { + m.data = data + m.saves.Add(1) + return nil + }, + Now: func() time.Time { return testNow }, + } +} + +func (m *memoryState) set(t *testing.T, state checkState) { + t.Helper() + data, err := json.Marshal(state) + require.NoError(t, err) + m.data = data +} + +func (m *memoryState) get(t *testing.T) checkState { + t.Helper() + var state checkState + require.NoError(t, json.Unmarshal(m.data, &state)) + return state +} + +func latestHandler(t *testing.T, requests *atomic.Int32, tag string) http.Handler { + t.Helper() + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requests.Add(1) + _, _ = w.Write([]byte(`{"tag_name":"` + tag + `"}`)) + }) +} + +// requireNotice checks notice suggests updating from 0.1.28 to 0.1.29. +func requireNotice(t *testing.T, notice string) { + t.Helper() + if _, ok := AssetName(runtime.GOOS, runtime.GOARCH); !ok { + require.Contains(t, notice, + "A new release of rack-gateway is available: 0.1.28 → 0.1.29. Build it from source: ") + return + } + require.Equal(t, `A new release of rack-gateway is available: 0.1.28 → 0.1.29. `+ + `Run "rack-gateway update" to install it.`, notice) +} + +func TestCheckAsksGitHubOnceADay(t *testing.T) { + var requests atomic.Int32 + client := testClient(t, latestHandler(t, &requests, "v0.1.29")) + state := &memoryState{} + + notice := state.checker(client, "0.1.28").Start().Notice(5 * time.Second) + requireNotice(t, notice) + require.Equal(t, int32(1), requests.Load()) + require.Equal(t, checkState{CheckedAt: testNow, LatestTag: "v0.1.29"}, state.get(t)) + + // Within a day, the saved result is used without asking GitHub. + notice = state.checker(client, "0.1.28").Start().Notice(5 * time.Second) + requireNotice(t, notice) + require.Equal(t, int32(1), requests.Load()) + + // After a day, it asks again. + state.set(t, checkState{CheckedAt: testNow.Add(-CheckInterval), LatestTag: "v0.1.29"}) + require.Empty(t, state.checker(client, "0.1.29").Start().Notice(5*time.Second)) + require.Equal(t, int32(2), requests.Load()) +} + +func TestCheckShowsNothingWhenUpToDate(t *testing.T) { + var requests atomic.Int32 + client := testClient(t, latestHandler(t, &requests, "v0.1.28")) + + require.Empty(t, (&memoryState{}).checker(client, "0.1.28").Start().Notice(5*time.Second)) + require.Empty(t, (&memoryState{}).checker(client, "0.1.29").Start().Notice(5*time.Second)) + + // A saved tag that isn't a release is ignored. + state := &memoryState{} + state.set(t, checkState{CheckedAt: testNow, LatestTag: "v9.9.9\u001b[2J"}) + require.Empty(t, state.checker(client, "0.1.28").Start().Notice(5*time.Second)) +} + +func TestFailedCheckIsSavedAndKeepsTheLastResult(t *testing.T) { + client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "unavailable", http.StatusServiceUnavailable) + })) + state := &memoryState{} + state.set(t, checkState{CheckedAt: testNow.Add(-2 * CheckInterval), LatestTag: "v0.1.29"}) + + notice := state.checker(client, "0.1.28").Start().Notice(5 * time.Second) + requireNotice(t, notice) + require.Equal(t, checkState{CheckedAt: testNow, LatestTag: "v0.1.29"}, state.get(t)) +} + +func TestCheckStillRunningAfterTheCommandIsCanceled(t *testing.T) { + release := make(chan struct{}) + client := testClient(t, http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + select { + case <-release: + case <-r.Context().Done(): + } + })) + t.Cleanup(func() { close(release) }) + state := &memoryState{} + saved := checkState{CheckedAt: testNow.Add(-2 * CheckInterval), LatestTag: "v0.1.29"} + state.set(t, saved) + + check := state.checker(client, "0.1.28").Start() + requireNotice(t, check.Notice(10*time.Millisecond)) + <-check.done + require.Zero(t, state.saves.Load(), "a canceled check isn't saved, so the next command checks again") + require.Equal(t, saved, state.get(t)) +} diff --git a/internal/cli/update/install.go b/internal/cli/update/install.go new file mode 100644 index 00000000..be697c08 --- /dev/null +++ b/internal/cli/update/install.go @@ -0,0 +1,210 @@ +package update + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +const ( + binaryName = "rack-gateway" + maxArchiveBytes = 256 << 20 + maxBinaryBytes = 512 << 20 + maxSmallAssetBytes = 1 << 20 + buildInfoTimeout = time.Minute + + securityKeysSupported = "supported" + securityKeysUnsupported = "not supported" +) + +// BuildInfo is what `rack-gateway build-info` reports about a CLI binary. +type BuildInfo struct { + Version string + SecurityKeys bool +} + +// String formats the build info as `rack-gateway build-info` prints it. +func (b BuildInfo) String() string { + keys := securityKeysUnsupported + if b.SecurityKeys { + keys = securityKeysSupported + } + return fmt.Sprintf("version: %s\nsecurity keys: %s\n", b.Version, keys) +} + +// ParseBuildInfo parses the output of `rack-gateway build-info`. +func ParseBuildInfo(out []byte) (BuildInfo, error) { + var info BuildInfo + var keys string + for _, line := range strings.Split(string(out), "\n") { + if value, ok := strings.CutPrefix(line, "version: "); ok { + info.Version = value + } else if value, ok := strings.CutPrefix(line, "security keys: "); ok { + keys = value + } + } + if info.Version == "" || (keys != securityKeysSupported && keys != securityKeysUnsupported) { + return BuildInfo{}, fmt.Errorf("unexpected build-info output: %q", out) + } + info.SecurityKeys = keys == securityKeysSupported + return info, nil +} + +// Installer replaces the running CLI binary with a release. +type Installer struct { + Client *Client + // Verify checks a release archive's provenance attestation bundle (see ProvenanceVerifier.Verify). + Verify func(bundleJSON, digest []byte, tag string) error + // BuildInfo runs `build-info` on a downloaded binary (see RunBuildInfo). + BuildInfo func(ctx context.Context, path string) ([]byte, error) + Executable string + Current BuildInfo + GOOS string + GOARCH string + Out io.Writer +} + +// Install downloads the release's archive for this platform, verifies its checksum and build provenance, checks +// that the binary inside runs and is a release this build can be replaced with, and then renames it over the +// running binary. Until that rename, the installed binary is untouched. +func (i *Installer) Install(ctx context.Context, rel *Release) error { + asset, ok := AssetName(i.GOOS, i.GOARCH) + if !ok { + return fmt.Errorf("releases have no build for %s/%s: build the CLI from source", i.GOOS, i.GOARCH) + } + dir := filepath.Dir(i.Executable) + tmp, err := os.CreateTemp(dir, "."+binaryName+".update-*") + if err != nil { + return fmt.Errorf( + "can't write to %s (%w). Run \"sudo rack-gateway update\", or reinstall the CLI into a directory you own", + dir, err) + } + installed := false + defer func() { + if !installed { + _ = tmp.Close() + _ = os.Remove(tmp.Name()) + } + }() + + archive, err := i.download(ctx, rel.Tag, asset) + if err != nil { + return err + } + if err := extractBinary(archive, tmp); err != nil { + return err + } + if err := i.check(ctx, tmp.Name(), rel); err != nil { + return err + } + if err := os.Rename(tmp.Name(), i.Executable); err != nil { + return err + } + installed = true + return nil +} + +// download returns the release archive once its checksum and its build provenance attestation check out. +func (i *Installer) download(ctx context.Context, tag, asset string) ([]byte, error) { + _, _ = fmt.Fprintf(i.Out, "Downloading %s from %s\n", asset, i.Client.ReleaseURL(tag)) + archive, err := i.Client.Download(ctx, tag, asset, maxArchiveBytes) + if err != nil { + return nil, err + } + checksum, err := i.Client.Download(ctx, tag, asset+".sha256", maxSmallAssetBytes) + if err != nil { + return nil, err + } + digest, err := VerifyChecksum(archive, checksum, asset) + if err != nil { + return nil, err + } + bundleJSON, err := i.Client.Download(ctx, tag, asset+".sigstore.json", maxSmallAssetBytes) + if err != nil { + return nil, err + } + if err := i.Verify(bundleJSON, digest, tag); err != nil { + return nil, err + } + _, _ = fmt.Fprintf(i.Out, "Verified its checksum and build provenance (signed by %s)\n", + SignerIdentity(i.Client.Repository, tag)) + return archive, nil +} + +// check makes the new binary executable like the one it replaces, runs it, and checks it is the release we asked +// for and keeps security key support when this build has it. +func (i *Installer) check(ctx context.Context, path string, rel *Release) error { + current, err := os.Stat(i.Executable) + if err != nil { + return err + } + if err := os.Chmod(path, current.Mode().Perm()); err != nil { + return err + } + out, err := i.BuildInfo(ctx, path) + if err != nil { + return fmt.Errorf("the downloaded binary doesn't run on this machine: %w", err) + } + next, err := ParseBuildInfo(out) + if err != nil { + return err + } + if TagFor(next.Version) != rel.Tag { + return fmt.Errorf("the downloaded binary reports version %s, not %s", next.Version, rel.Version()) + } + if i.Current.SecurityKeys && !next.SecurityKeys { + return fmt.Errorf("the %s/%s release has no security key support, and this build has it: "+ + "build the CLI from source to keep it", i.GOOS, i.GOARCH) + } + return nil +} + +// extractBinary writes the archive's rack-gateway binary to dst and closes it. +func extractBinary(archive []byte, dst *os.File) error { + gz, err := gzip.NewReader(bytes.NewReader(archive)) + if err != nil { + return fmt.Errorf("invalid release archive: %w", err) + } + tr := tar.NewReader(gz) + for { + hdr, err := tr.Next() + if errors.Is(err, io.EOF) { + return fmt.Errorf("the release archive has no %s binary", binaryName) + } + if err != nil { + return fmt.Errorf("invalid release archive: %w", err) + } + if hdr.Name != binaryName { + continue + } + if hdr.Typeflag != tar.TypeReg || hdr.Size <= 0 || hdr.Size > maxBinaryBytes { + return fmt.Errorf("the release archive's %s is not a regular file of a sensible size", binaryName) + } + if _, err := io.CopyN(dst, tr, hdr.Size); err != nil { + return fmt.Errorf("invalid release archive: %w", err) + } + if err := dst.Sync(); err != nil { + return err + } + return dst.Close() + } +} + +// RunBuildInfo runs `build-info` on the binary at path and returns its output. +func RunBuildInfo(ctx context.Context, path string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, buildInfoTimeout) + defer cancel() + // path is the binary Install just extracted from a release archive whose provenance it verified. + cmd := exec.CommandContext(ctx, path, "build-info") + cmd.Env = append(os.Environ(), "RACK_GATEWAY_NO_UPDATE_CHECK=1") + return cmd.Output() +} diff --git a/internal/cli/update/install_test.go b/internal/cli/update/install_test.go new file mode 100644 index 00000000..006119ef --- /dev/null +++ b/internal/cli/update/install_test.go @@ -0,0 +1,273 @@ +package update + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +const testAsset = "rack-gateway-linux-amd64.tar.gz" + +type tarEntry struct { + name string + body string + typeflag byte +} + +func tarGz(t *testing.T, entries ...tarEntry) []byte { + t.Helper() + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + tw := tar.NewWriter(gz) + for _, e := range entries { + hdr := &tar.Header{Name: e.name, Mode: 0o755, Size: int64(len(e.body)), Typeflag: e.typeflag} + if e.typeflag == tar.TypeSymlink { + hdr.Size = 0 + hdr.Linkname = "/etc/passwd" + } + require.NoError(t, tw.WriteHeader(hdr)) + if hdr.Size > 0 { + _, err := tw.Write([]byte(e.body)) + require.NoError(t, err) + } + } + require.NoError(t, tw.Close()) + require.NoError(t, gz.Close()) + return buf.Bytes() +} + +type releaseFixture struct { + archive []byte + checksum []byte + verified [][]byte + verify error + info BuildInfo + exe string +} + +func newReleaseFixture(t *testing.T) *releaseFixture { + t.Helper() + archive := tarGz(t, tarEntry{name: "rack-gateway", body: "new binary", typeflag: tar.TypeReg}) + sum := sha256.Sum256(archive) + exe := filepath.Join(t.TempDir(), "rack-gateway") + require.NoError(t, os.WriteFile(exe, []byte("old binary"), 0o600)) + binaryMode := os.FileMode(0o751) + require.NoError(t, os.Chmod(exe, binaryMode)) + return &releaseFixture{ + archive: archive, + checksum: []byte(hex.EncodeToString(sum[:]) + " " + testAsset + "\n"), + info: BuildInfo{Version: "0.1.29", SecurityKeys: true}, + exe: exe, + } +} + +func (f *releaseFixture) installer(t *testing.T, current BuildInfo) *Installer { + t.Helper() + assets := map[string][]byte{ + testAsset: f.archive, + testAsset + ".sha256": f.checksum, + testAsset + ".sigstore.json": []byte(`{"bundle":true}`), + } + client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, ok := assets[filepath.Base(r.URL.Path)] + if !ok || filepath.Dir(r.URL.Path) != "/DocSpring/rack-gateway/releases/download/v0.1.29" { + http.NotFound(w, r) + return + } + _, _ = w.Write(body) + })) + return &Installer{ + Client: client, + Verify: func(bundleJSON, digest []byte, tag string) error { + require.Equal(t, `{"bundle":true}`, string(bundleJSON)) + require.Equal(t, "v0.1.29", tag) + f.verified = append(f.verified, digest) + return f.verify + }, + BuildInfo: func(_ context.Context, path string) ([]byte, error) { + body, err := os.ReadFile(filepath.Clean(path)) + require.NoError(t, err) + require.Equal(t, "new binary", string(body)) + return []byte(f.info.String()), nil + }, + Executable: f.exe, + Current: current, + GOOS: "linux", + GOARCH: "amd64", + Out: &bytes.Buffer{}, + } +} + +func (f *releaseFixture) requireUnchanged(t *testing.T) { + t.Helper() + body, err := os.ReadFile(f.exe) + require.NoError(t, err) + require.Equal(t, "old binary", string(body)) + entries, err := os.ReadDir(filepath.Dir(f.exe)) + require.NoError(t, err) + require.Len(t, entries, 1, "the temporary file is removed") +} + +var release0129 = &Release{Tag: "v0.1.29"} + +func TestInstallReplacesTheBinary(t *testing.T) { + f := newReleaseFixture(t) + installer := f.installer(t, BuildInfo{Version: "0.1.28", SecurityKeys: true}) + var out bytes.Buffer + installer.Out = &out + + require.NoError(t, installer.Install(context.Background(), release0129)) + + body, err := os.ReadFile(f.exe) + require.NoError(t, err) + require.Equal(t, "new binary", string(body)) + info, err := os.Stat(f.exe) + require.NoError(t, err) + require.Equal(t, os.FileMode(0o751), info.Mode().Perm(), "keeps the old binary's permissions") + sum := sha256.Sum256(f.archive) + require.Equal(t, [][]byte{sum[:]}, f.verified) + entries, err := os.ReadDir(filepath.Dir(f.exe)) + require.NoError(t, err) + require.Len(t, entries, 1) + require.Contains(t, out.String(), + "signed by https://github.com/DocSpring/rack-gateway/.github/workflows/release.yml@refs/tags/v0.1.29") +} + +func TestInstallLeavesTheBinaryAloneWhenAStepFails(t *testing.T) { + tests := []struct { + name string + modify func(f *releaseFixture) + current BuildInfo + wantErr string + }{ + { + name: "checksum mismatch", + modify: func(f *releaseFixture) { f.checksum = []byte(strings.Repeat("0", 64) + " " + testAsset + "\n") }, + wantErr: "checksum mismatch for " + testAsset, + }, + { + name: "attestation rejected", + modify: func(f *releaseFixture) { f.verify = errors.New("attestation verification failed: wrong signer") }, + wantErr: "wrong signer", + }, + { + name: "binary reports another version", + modify: func(f *releaseFixture) { f.info.Version = "0.1.30" }, + wantErr: "reports version 0.1.30, not 0.1.29", + }, + { + name: "release would drop security key support", + modify: func(f *releaseFixture) { f.info.SecurityKeys = false }, + current: BuildInfo{Version: "0.1.28", SecurityKeys: true}, + wantErr: "has no security key support, and this build has it", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := newReleaseFixture(t) + tt.modify(f) + err := f.installer(t, tt.current).Install(context.Background(), release0129) + require.ErrorContains(t, err, tt.wantErr) + f.requireUnchanged(t) + }) + } +} + +func TestInstallWithoutSecurityKeysAcceptsAReleaseWithout(t *testing.T) { + f := newReleaseFixture(t) + f.info.SecurityKeys = false + require.NoError(t, f.installer(t, BuildInfo{Version: "0.1.28"}).Install(context.Background(), release0129)) +} + +func TestInstallNeedsAWritableDirectory(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip("root can write to any directory") + } + f := newReleaseFixture(t) + dir := filepath.Dir(f.exe) + readOnly, writable := os.FileMode(0o555), os.FileMode(0o755) + require.NoError(t, os.Chmod(dir, readOnly)) + t.Cleanup(func() { _ = os.Chmod(dir, writable) }) + + err := f.installer(t, BuildInfo{Version: "0.1.28"}).Install(context.Background(), release0129) + require.ErrorContains(t, err, `Run "sudo rack-gateway update"`) + require.Empty(t, f.verified, "nothing is downloaded") +} + +func TestInstallRefusesUnsupportedPlatforms(t *testing.T) { + f := newReleaseFixture(t) + installer := f.installer(t, BuildInfo{Version: "0.1.28"}) + installer.GOOS, installer.GOARCH = "darwin", "amd64" + require.ErrorContains(t, installer.Install(context.Background(), release0129), "no build for darwin/amd64") + f.requireUnchanged(t) +} + +func TestExtractBinaryOnlyTakesTheRegularRackGatewayFile(t *testing.T) { + tests := []struct { + name string + archive []byte + wantErr string + }{ + { + name: "no binary", + archive: tarGz(t, tarEntry{name: "README.md", body: "hi", typeflag: tar.TypeReg}), + wantErr: "has no rack-gateway binary", + }, + { + name: "binary in a subdirectory", + archive: tarGz(t, tarEntry{name: "bin/rack-gateway", body: "x", typeflag: tar.TypeReg}), + wantErr: "has no rack-gateway binary", + }, + { + name: "symlink", + archive: tarGz(t, tarEntry{name: "rack-gateway", typeflag: tar.TypeSymlink}), + wantErr: "not a regular file", + }, + { + name: "not gzip", + archive: []byte("plain text"), + wantErr: "invalid release archive", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dst, err := os.CreateTemp(t.TempDir(), "binary") + require.NoError(t, err) + defer func() { _ = dst.Close() }() + require.ErrorContains(t, extractBinary(tt.archive, dst), tt.wantErr) + }) + } + + dst, err := os.CreateTemp(t.TempDir(), "binary") + require.NoError(t, err) + archive := tarGz(t, + tarEntry{name: "LICENSE", body: "MIT", typeflag: tar.TypeReg}, + tarEntry{name: "rack-gateway", body: "binary", typeflag: tar.TypeReg}) + require.NoError(t, extractBinary(archive, dst)) + body, err := os.ReadFile(dst.Name()) + require.NoError(t, err) + require.Equal(t, "binary", string(body)) +} + +func TestBuildInfoRoundTrip(t *testing.T) { + for _, info := range []BuildInfo{{Version: "0.1.29", SecurityKeys: true}, {Version: "0.1.29"}} { + parsed, err := ParseBuildInfo([]byte(info.String())) + require.NoError(t, err) + require.Equal(t, info, parsed) + } + _, err := ParseBuildInfo([]byte("version: 0.1.29\n")) + require.ErrorContains(t, err, "unexpected build-info output") + _, err = ParseBuildInfo([]byte("Error: unknown command \"build-info\"\n")) + require.ErrorContains(t, err, "unexpected build-info output") +} diff --git a/internal/cli/update/release.go b/internal/cli/update/release.go new file mode 100644 index 00000000..8ef350aa --- /dev/null +++ b/internal/cli/update/release.go @@ -0,0 +1,184 @@ +// Package update finds new rack-gateway CLI releases on GitHub, verifies them and installs them. +package update + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "golang.org/x/mod/semver" +) + +// Repository is the GitHub repository the CLI's releases come from. The Release workflow sets it to the repository +// that builds the release, so a fork's CLI updates from the fork. +var Repository = "DocSpring/rack-gateway" + +const ( + githubAPIBaseURL = "https://api.github.com" + githubDownloadBaseURL = "https://github.com" + maxRedirects = 5 + maxAPIResponseBytes = 1 << 20 +) + +// releaseTagPattern is the tag format the Release workflow accepts. +var releaseTagPattern = regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$`) + +// Release is a published GitHub release of the CLI. +type Release struct { + Tag string `json:"tag_name"` +} + +// Version is the release's version without the "v" prefix, as `rack-gateway version` prints it. +func (r Release) Version() string { + return strings.TrimPrefix(r.Tag, "v") +} + +// Client reads releases from GitHub and downloads their assets. +type Client struct { + HTTP *http.Client + APIBaseURL string + DownloadBaseURL string + Repository string + UserAgent string +} + +// NewClient returns a client for this CLI's GitHub releases. Requests and redirects may only go to GitHub over +// HTTPS. +func NewClient(currentVersion string, timeout time.Duration) *Client { + return &Client{ + HTTP: &http.Client{ + Timeout: timeout, + CheckRedirect: checkRedirect(isGitHubURL), + }, + APIBaseURL: githubAPIBaseURL, + DownloadBaseURL: githubDownloadBaseURL, + Repository: Repository, + UserAgent: "rack-gateway/" + currentVersion, + } +} + +// isGitHubURL reports whether u is an HTTPS URL on GitHub or the hosts GitHub serves release assets from. +func isGitHubURL(u *url.URL) bool { + host := u.Hostname() + return u.Scheme == "https" && + (host == "github.com" || host == "api.github.com" || strings.HasSuffix(host, ".githubusercontent.com")) +} + +func checkRedirect(allowed func(*url.URL) bool) func(*http.Request, []*http.Request) error { + return func(req *http.Request, via []*http.Request) error { + if len(via) >= maxRedirects { + return errors.New("too many redirects") + } + if !allowed(req.URL) { + return fmt.Errorf("refusing to follow a redirect to %s", req.URL.Redacted()) + } + return nil + } +} + +// LatestRelease returns the newest stable release. +func (c *Client) LatestRelease(ctx context.Context) (*Release, error) { + return c.release(ctx, "releases/latest") +} + +// ReleaseByTag returns the published release with this tag. +func (c *Client) ReleaseByTag(ctx context.Context, tag string) (*Release, error) { + if !releaseTagPattern.MatchString(tag) { + return nil, fmt.Errorf("invalid release tag %q (expected vMAJOR.MINOR.PATCH)", tag) + } + return c.release(ctx, "releases/tags/"+tag) +} + +func (c *Client) release(ctx context.Context, path string) (*Release, error) { + endpoint := fmt.Sprintf("%s/repos/%s/%s", c.APIBaseURL, c.Repository, path) + body, err := c.get(ctx, endpoint, maxAPIResponseBytes, map[string]string{ + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }) + if err != nil { + return nil, err + } + var rel Release + if err := json.Unmarshal(body, &rel); err != nil { + return nil, fmt.Errorf("failed to parse the GitHub release: %w", err) + } + if !releaseTagPattern.MatchString(rel.Tag) { + return nil, fmt.Errorf("GitHub returned an invalid release tag %q", rel.Tag) + } + return &rel, nil +} + +// ReleaseURL returns the web page of the release with this tag. +func (c *Client) ReleaseURL(tag string) string { + return fmt.Sprintf("%s/%s/releases/tag/%s", c.DownloadBaseURL, c.Repository, url.PathEscape(tag)) +} + +// Download returns one of a release's assets, refusing anything larger than limit bytes. +func (c *Client) Download(ctx context.Context, tag, asset string, limit int64) ([]byte, error) { + endpoint := fmt.Sprintf("%s/%s/releases/download/%s/%s", + c.DownloadBaseURL, c.Repository, url.PathEscape(tag), url.PathEscape(asset)) + body, err := c.get(ctx, endpoint, limit, nil) + if err != nil { + return nil, fmt.Errorf("failed to download %s: %w", asset, err) + } + return body, nil +} + +func (c *Client) get(ctx context.Context, endpoint string, limit int64, headers map[string]string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", c.UserAgent) + for name, value := range headers { + req.Header.Set(name, value) + } + resp, err := c.HTTP.Do(req) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GET %s: %s", endpoint, resp.Status) + } + body, err := io.ReadAll(io.LimitReader(resp.Body, limit+1)) + if err != nil { + return nil, err + } + if int64(len(body)) > limit { + return nil, fmt.Errorf("GET %s: response is larger than %d bytes", endpoint, limit) + } + return body, nil +} + +// IsReleaseVersion reports whether version is a release version ("0.1.29" or "v0.1.29"), not a development build. +func IsReleaseVersion(version string) bool { + return releaseTagPattern.MatchString(TagFor(version)) +} + +// IsNewer reports whether release version a is newer than release version b. +func IsNewer(a, b string) bool { + return semver.Compare(TagFor(a), TagFor(b)) > 0 +} + +// TagFor returns the release tag for a version, with or without its "v" prefix. +func TagFor(version string) string { + return "v" + strings.TrimPrefix(version, "v") +} + +// AssetName returns the name of the release archive for a platform, and whether releases include one. +func AssetName(goos, goarch string) (string, bool) { + switch goos + "/" + goarch { + case "darwin/arm64", "linux/amd64", "linux/arm64": + return fmt.Sprintf("rack-gateway-%s-%s.tar.gz", goos, goarch), true + default: + return "", false + } +} diff --git a/internal/cli/update/release_test.go b/internal/cli/update/release_test.go new file mode 100644 index 00000000..53e51378 --- /dev/null +++ b/internal/cli/update/release_test.go @@ -0,0 +1,128 @@ +package update + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// testClient returns a client whose API and downloads are served by handler over plain HTTP. +func testClient(t *testing.T, handler http.Handler) *Client { + t.Helper() + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + client := NewClient("0.1.28", 5*time.Second) + client.HTTP.CheckRedirect = nil + client.APIBaseURL = server.URL + client.DownloadBaseURL = server.URL + client.Repository = testRepository + return client +} + +func TestLatestRelease(t *testing.T) { + var gotPath, gotAgent string + client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath, gotAgent = r.URL.Path, r.Header.Get("User-Agent") + _, _ = w.Write([]byte(`{"tag_name":"v0.1.29","html_url":"https://example.com"}`)) + })) + + rel, err := client.LatestRelease(context.Background()) + require.NoError(t, err) + require.Equal(t, "v0.1.29", rel.Tag) + require.Equal(t, "0.1.29", rel.Version()) + require.Equal(t, "/repos/DocSpring/rack-gateway/releases/latest", gotPath) + require.Equal(t, "rack-gateway/0.1.28", gotAgent) +} + +func TestReleaseByTag(t *testing.T) { + client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/repos/DocSpring/rack-gateway/releases/tags/v0.1.27", r.URL.Path) + _, _ = w.Write([]byte(`{"tag_name":"v0.1.27"}`)) + })) + + rel, err := client.ReleaseByTag(context.Background(), "v0.1.27") + require.NoError(t, err) + require.Equal(t, &Release{Tag: "v0.1.27"}, rel) +} + +func TestReleaseRejectsBadTagsAndErrors(t *testing.T) { + client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/repos/DocSpring/rack-gateway/releases/tags/v0.1.29": + _, _ = w.Write([]byte(`{"tag_name":"v0.1.29\u001b[2J"}`)) + default: + http.Error(w, "rate limited", http.StatusForbidden) + } + })) + + _, err := client.ReleaseByTag(context.Background(), "v0.1.29") + require.ErrorContains(t, err, "invalid release tag") + _, err = client.ReleaseByTag(context.Background(), "../../evil") + require.ErrorContains(t, err, `invalid release tag "../../evil"`) + _, err = client.LatestRelease(context.Background()) + require.ErrorContains(t, err, "403 Forbidden") +} + +func TestDownloadEnforcesTheSizeLimit(t *testing.T) { + client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/DocSpring/rack-gateway/releases/download/v0.1.29/asset.tar.gz", r.URL.Path) + _, _ = w.Write([]byte(strings.Repeat("x", 10))) + })) + + body, err := client.Download(context.Background(), "v0.1.29", "asset.tar.gz", 10) + require.NoError(t, err) + require.Len(t, body, 10) + _, err = client.Download(context.Background(), "v0.1.29", "asset.tar.gz", 9) + require.ErrorContains(t, err, "larger than 9 bytes") +} + +func TestRedirectsStayOnGitHub(t *testing.T) { + check := NewClient("0.1.28", time.Second).HTTP.CheckRedirect + redirect := func(rawURL string, hops int) error { + u, err := url.Parse(rawURL) + require.NoError(t, err) + return check(&http.Request{URL: u}, make([]*http.Request, hops)) + } + + require.NoError(t, redirect("https://release-assets.githubusercontent.com/github-production-release-asset/1", 1)) + require.NoError(t, redirect("https://objects.githubusercontent.com/github-production-release-asset-2e65be/1", 1)) + require.NoError(t, redirect("https://github.com/DocSpring/rack-gateway/releases/download/v0.1.29/a", 1)) + require.ErrorContains(t, redirect("http://objects.githubusercontent.com/a", 1), "refusing to follow a redirect") + require.ErrorContains(t, redirect("https://example.com/a", 1), "refusing to follow a redirect") + require.ErrorContains(t, redirect("https://githubusercontent.com.example.com/a", 1), "refusing to follow") + require.ErrorContains(t, redirect("https://github.com/a", maxRedirects), "too many redirects") +} + +func TestVersions(t *testing.T) { + require.True(t, IsReleaseVersion("0.1.29")) + require.True(t, IsReleaseVersion("v0.1.29")) + require.True(t, IsReleaseVersion("0.2.0-rc.1")) + require.False(t, IsReleaseVersion("dev")) + require.False(t, IsReleaseVersion("0.1")) + require.False(t, IsReleaseVersion("0.1.29 ")) + + require.True(t, IsNewer("v0.1.29", "0.1.28")) + require.True(t, IsNewer("0.1.10", "0.1.9")) + require.True(t, IsNewer("0.2.0", "0.2.0-rc.1")) + require.False(t, IsNewer("0.1.28", "0.1.28")) + require.False(t, IsNewer("0.1.27", "0.1.28")) + require.False(t, IsNewer("garbage", "0.1.28")) +} + +func TestAssetName(t *testing.T) { + for _, platform := range [][2]string{{"darwin", "arm64"}, {"linux", "amd64"}, {"linux", "arm64"}} { + name, ok := AssetName(platform[0], platform[1]) + require.True(t, ok) + require.Equal(t, "rack-gateway-"+platform[0]+"-"+platform[1]+".tar.gz", name) + } + for _, platform := range [][2]string{{"darwin", "amd64"}, {"windows", "amd64"}, {"freebsd", "arm64"}} { + _, ok := AssetName(platform[0], platform[1]) + require.False(t, ok) + } +} diff --git a/internal/cli/update/testdata/public-good-trusted-root.json b/internal/cli/update/testdata/public-good-trusted-root.json new file mode 100644 index 00000000..effb0a19 --- /dev/null +++ b/internal/cli/update/testdata/public-good-trusted-root.json @@ -0,0 +1,126 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + }, + { + "baseUrl": "https://log2025-1.rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=", + "keyDetails": "PKIX_ED25519", + "validFor": { + "start": "2025-09-23T00:00:00Z" + } + }, + "logId": { + "keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" + } + ] + }, + "validFor": { + "start": "2021-03-07T03:20:29Z", + "end": "2022-12-31T23:59:59.999Z" + } + }, + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + }, + "validFor": { + "start": "2022-04-13T20:06:15Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstore.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-03-14T00:00:00Z", + "end": "2022-10-31T23:59:59.999Z" + } + }, + "logId": { + "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" + } + }, + { + "baseUrl": "https://ctfe.sigstore.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-10-20T00:00:00Z" + } + }, + "logId": { + "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" + } + } + ], + "timestampAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore-tsa-selfsigned" + }, + "uri": "https://timestamp.sigstore.dev/api/v1/timestamp", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw==" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E" + } + ] + }, + "validFor": { + "start": "2025-07-04T00:00:00Z" + } + } + ] +} diff --git a/internal/cli/update/testdata/rack-gateway-linux-amd64-v0.1.28.sigstore.json b/internal/cli/update/testdata/rack-gateway-linux-amd64-v0.1.28.sigstore.json new file mode 100644 index 00000000..1051c5cc --- /dev/null +++ b/internal/cli/update/testdata/rack-gateway-linux-amd64-v0.1.28.sigstore.json @@ -0,0 +1,67 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "3510272490", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "integratedTime": "1793088509", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIESzxTzPz3I9b7BlLB8ay/8XPpfSmlZraK7RLi2/+iwEAiEA8w0yfA5Y1I4a0gvCc3QPbAG5+bnKVYGf3BxXxnCBIic=" + }, + "inclusionProof": { + "logIndex": "3388368228", + "rootHash": "KSH+Iue88jnT4Jq6Xs/tifbZ6YG8JTTVpitIctuAGI8=", + "treeSize": "3388368353", + "hashes": [ + "M0RYmn9uxANgjdcoggOzFUVo0yTlnMdD4Ld3YCB93YA=", + "p6C0TSqWsGpe/NVxfmkDBgExjHpbc+1rci+bVujqfOQ=", + "hDI8h3bODIpQux6O++VYQvhJ8pULodYAsyN7AfWlzXA=", + "CgQ1Qc6O7Bz2rsFY/bDy8BHnTz2QNLywO+i0ewtlCbE=", + "dd9/RLTLDuy88a4QwD1GlfKAw8mjaIoA29aYNI2/eCw=", + "c6eiaMH9wD0DDrehkrka04jeSsKbigeSCVcs29niCGI=", + "P7CK4qHVr1ctk0bQXgowYln5dPbclCmw+Iwfag33rOc=", + "lG6JZUlgtDZH2CcSzaif/OhayGbdExQqwkdO4PrfNOc=", + "OjqB36aNmF+6ZceO0noSXEv/48bfFrsN9ypRSv1b3FI=", + "66PrIq+385ODDXrybzsuO/301ZCH/XXoDW1RMh7yiuA=", + "oF/gr1KVj9uBV2WFqaRjFQNHqG3Sl/W2DUeyiuXWrPU=", + "JuGMU2qm7qd/OEVn5AHk5Fn/JmhLHs+Ef6RQ/fUSx+o=", + "pPATjZdl+MpovsMbIiboU8szzW7+YQ5IGfEjyEMF00A=", + "Nu/SV+GdG112GpdFh3/R17f0PcB9JfDP5cIQeRi365I=", + "tNzrpii8umtU6X7IxV8azjdBCJ3IrRgX7zjLxSEBY3c=", + "lCtYatXrabcYsSSO5wvYOJoWFpgT7QB/6sXgVUVax8M=", + "4HDK+JeJ/1S/3wpx4w2CsNcq7sqO+rlci3EEWF/p3Dc=", + "DGHKRV9Hj1e3Z4G8WPXuCUHHumJYK14vN1lAd3RRZqc=", + "6isO7d5SZuUjk8FoC2N/4lrs2iljQ9nlCzDM90al4Mw=", + "zGfeoWKmrZlXMOvyi3R2GDbGpmI1skJW/wLTXCCgF0o=", + "1DaDKzrCNNBhmSdY5vFjz+NpAmxmYfYEgBCdPLrMP1A=", + "xH/DCseLHr9eKoYT8qsORZK7zVdEGYWHuVtsVrD95wY=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 1193050959916656506\n3388368353\nKSH+Iue88jnT4Jq6Xs/tifbZ6YG8JTTVpitIctuAGI8=\n\n\u2014 rekor.sigstore.dev wNI9ajBFAiBbdPqTER0tcliW2HY8VxY4cah/Ixm3P00LrRbh/598VwIhAOG9qIxm4JcnNQY7naUa3q/S3uiQYeoUglIo2ysyz4nZ\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiOTI4ODdlYzViOTA5MGYwNTczMGY0NWYwMTNmODA0ODI5Yjc5YjI1NjFhN2IwYzEwZjExZDYyOTg1ZTI4NTdkNCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjBmY2ZkMmVkNjRjNzVjNDE1YzdmMzk5Mzc5ZjQ4ODhhODFmOTdkZjlmNDdjY2RkNjYyNWE2MzQxNGJmYjM0OTcifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVRQ0lGTkhSY1djRGh6dmVZT2ZiK0FTVTNzbHE3TWlaMWxJL01pWXBuWTRoZUxRQWlCalRUdHJHOTJWV2FZODNrclVlenk4L0RmN05kTk50VmljdVNWUUIvYW1EUT09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VjNGFrTkRRbTV0WjBGM1NVSkJaMGxWU2xaNVRteHZjVE5CY2tKV1RUUktMMGhLVkdweWFrTnBWMWxqZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwWmVFMUVTVE5OUkdkM1QwUkpOVmRvWTA1TmFsbDRUVVJKTTAxRVozaFBSRWsxVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVXJRbEl2VFU1cmNTdHFTVmsyYmk4NE5tVmpkMlpaWVVKbFNVaHhha0ZwTkVGTFV6Z0tMMEZ3YlVkQlNXWlhLMjkzT1RSUFdsaHZiVkZPUlVsUWVXeEdkVVozYWxKMll6bHBjRE0xU0c1NVEyUlNjMUZRYlV0UFEwSmFaM2RuWjFkVlRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdaQ1owNVdTRk5OUlVkRVFWZG5RbFJtQ2pBcmJsQldhVkZTYkhadGJ6SlBhMjlXWVV4SFRHaG9hMUI2UW01Q1owNVdTRkpGUWtGbU9FVllWRUppYUd4c2IyUklVbmRqZW05MlRESmtjR1JIYURFS1dXazFhbUl5TUhaU1J6bHFWVE5DZVdGWE5XNU1NMHBvV1RKemRGb3lSakJhV0dSb1pWTTRkVm95YkRCaFNGWnBURE5rZG1OdGRHMWlSemt6WTNrNWVRcGFWM2hzV1ZoT2JFeHViSFJpUlVKNVdsZGFla3d6VW1oYU0wMTJaR3BCZFUxVE5IbFBSRUUxUW1kdmNrSm5SVVZCV1U4dlRVRkZRa0pEZEc5a1NGSjNDbU42YjNaTU0xSjJZVEpXZFV4dFJtcGtSMngyWW01TmRWb3liREJoU0ZacFpGaE9iR050VG5aaWJsSnNZbTVSZFZreU9YUk5Ra2xIUTJselIwRlJVVUlLWnpjNGQwRlJTVVZDU0VJeFl6Sm5kMDVuV1V0TGQxbENRa0ZIUkhaNlFVSkJkMUZ2VG1wWk1FMVVTWHBhVjA1c1RtcGpNazlFU1RWUFZHY3pUbnBHYUFwYVJHY3pXbGRaZWxwVVJUTk9WRmsxVG1wSk5FNXFUbXROUkVGV1FtZHZja0puUlVWQldVOHZUVUZGUlVKQlpGTmFWM2hzV1ZoT2JFMURVVWREYVhOSENrRlJVVUpuTnpoM1FWRlZSVVpyVW5aWk1VNTNZMjFzZFZwNU9YbFpWMDV5VEZka2FHUkhWak5aV0d0M1NIZFpTMHQzV1VKQ1FVZEVkbnBCUWtKblVWSUtZMjFXYldONU9UQlpWMlI2VEROWmQweHFSWFZOYW1kM1QzZFpTMHQzV1VKQ1FVZEVkbnBCUWtOQlVYUkVRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkUXBNYlVacVpFZHNkbUp1VFhWYU1td3dZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVZHJSME5wYzBkQlVWRkNaemM0ZDBGUmEwVlhkM2hhQ21GSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkRXd3VW5aWk1VNTNZMjFzZFZwNU9YbFpWMDV5VEZka2FHUkhWak5aV0d0MlRHMWtjR1JIYURFS1dXazVNMkl6U25KYWJYaDJaRE5OZG1OdFZuTmFWMFo2V2xNMU5XSlhlRUZqYlZadFkzazVNRmxYWkhwTU0xbDNUR3BGZFUxcVozZFBRVmxMUzNkWlFncENRVWRFZG5wQlFrTm5VWEZFUTJjeVRtcFJlRTFxVG14Wk1sVXlUbnBaTkUxcWF6VlBSR016VFZkR2EwOUVaR3hhYWs1c1RWUmpNVTVxYXpKTmFtY3lDazB5VVhkTlFqQkhRMmx6UjBGUlVVSm5OemgzUVZGelJVUjNkMDVhTW13d1lVaFdhVXhYYUhaak0xSnNXa1JCTlVKbmIzSkNaMFZGUVZsUEwwMUJSVTBLUWtOelRVdFhhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9VVmlNazVVWTBoS2NHSnRZM1pqYlVacVlYa3hibGxZVW14a01rWTFUVVJuUndwRGFYTkhRVkZSUW1jM09IZEJVVEJGUzJkM2IwNXFXVEJOVkVsNldsZE9iRTVxWXpKUFJFazFUMVJuTTA1NlJtaGFSR2N6V2xkWmVscFVSVE5PVkZrMUNrNXFTVFJPYWs1clRVUkJhRUpuYjNKQ1owVkZRVmxQTDAxQlJVOUNRazFOUlZoS2JGcHVUWFprUjBadVkzazVNazFETkhoTWFrazBUVUp2UjBOcGMwY0tRVkZSUW1jM09IZEJVVGhGUkVGM1MwMVVRWHBPUkdkNVQwUm5kMDVxUVhOQ1oyOXlRbWRGUlVGWlR5OU5RVVZSUWtJMFRVaEhhREJrU0VKNlQyazRkZ3BhTW13d1lVaFdhVXh0VG5aaVV6bEZZakpPVkdOSVNuQmliV04zUjBGWlMwdDNXVUpDUVVkRWRucEJRa1ZSVVV0RVFXZDZUV3BCTWsxRVVUQk5WRUp3Q2tKbmIzSkNaMFZGUVZsUEwwMUJSVk5DUm5OTlYxZG9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNVJXSXlUbFJqU0Vwd1ltMWpkbU50Um1vS1lYa3hibGxZVW14a01rWTFUSGsxYm1GWVVtOWtWMGwyWkRJNWVXRXlXbk5pTTJSNlRETktiR0pIVm1oak1sVjFaVmN4YzFGSVNteGFiazEyWkVkR2JncGplVGt5VFVNMGVFeHFTVFJOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU9hbGt3VFZSSmVscFhUbXhPYW1NeVQwUkpOVTlVWnpOT2VrWm9DbHBFWnpOYVYxbDZXbFJGTTA1VVdUVk9ha2swVG1wT2EwMUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRmpNbWQzV0ZGWlMwdDNXVUlLUWtGSFJIWjZRVUpHVVZKUVJFVXhiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RsSkhPV3BWTTBKNVlWYzFia3d6U21oWk1uTjBXakpHTUFwYVdHUm9aVk01YUZrelVuQmlNalY2VEROS01XSnVUWFpPUkVFd1RucEJNRTlVV1RCT2FsbDJXVmhTTUZwWE1YZGtTRTEyVFZSQlYwSm5iM0pDWjBWRkNrRlpUeTlOUVVWWFFrRm5UVUp1UWpGWmJYaHdXWHBDUWtKbmIzSkNaMFZGUVZsUEwwMUJSVmxDUkUxTlRWaEtiR05IT0RaU1J6bHFWVE5DZVdGWE5XNEtURE5LYUZreWMzUmFNa1l3V2xoa2FHVlVjSGxhVjFrMlkyMVdiV041T1RCWlYyUjZURE5aZDB4cVJYVk5hbWQzWjFselIwTnBjMGRCVVZGQ01XNXJRd3BDUVVsRlpsRlNOMEZJYTBGa2QwUmtVRlJDY1hoelkxSk5iVTFhU0doNVdscDZZME52YTNCbGRVNDBPSEptSzBocGJrdEJUSGx1ZFdwblFVRkJZVVk0Q2xveFZFNUJRVUZGUVhkQ1NVMUZXVU5KVVVOR1JrUnhUR1pvYXk5c1UxUlFZbTF5VkhKRVkxUm5OWE54V1V4Tlp6VlViblJTYXpKbFVrRjFkamhCU1dnS1FVazJkR0ZPUmtWS1NrSTJiUzkxZDNBM2JuTnVSV1E1Y3k4eVQwcFZObmQ2UmtGVFlVNUlhSFowV2tsTlFXOUhRME54UjFOTk5EbENRVTFFUVRKalFRcE5SMUZEVFVaaVZ5OVJSbFU0TnpKcFRrbzJhREZKUldaTU1WZFpNbTVJUkRORVRYSjBZVk13VXpKb1VXWkxlSFYzWjJWb1VIcDRhek5wV1RKbWRYRlBDa0ZqSzFwWFVVbDNVRU5uWWxaQ1JqVm1haTluSzFvd1p6VXdNMlpyT1ZSSmFGZzRPR0Z6YVM5cWRubHRSbE5aWm1wM1NYRjVNMFpST1dwWVUybExSRllLWVc1NFozUXdhM01LTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfX0=" + } + ], + "timestampVerificationData": {}, + "certificate": { + "rawBytes": "MIIG8jCCBnmgAwIBAgIUJVyNloq3ArBVM4J/HJTjrjCiWYcwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMDI3MDgwODI5WhcNMjYxMDI3MDgxODI5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+BR/MNkq+jIY6n/86ecwfYaBeIHqjAi4AKS8/ApmGAIfW+ow94OZXomQNEIPylFuFwjRvc9ip35HnyCdRsQPmKOCBZgwggWUMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBTf0+nPViQRlvmo2OkoVaLGLhhkPzBnBgNVHREBAf8EXTBbhllodHRwczovL2dpdGh1Yi5jb20vRG9jU3ByaW5nL3JhY2stZ2F0ZXdheS8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbEByZWZzL3RhZ3MvdjAuMS4yODA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzABAwQoNjY0MTIzZWNlNjc2ODI5OTg3NzFhZDg3ZWYzZTE3NTY5NjI4NjNkMDAVBgorBgEEAYO/MAEEBAdSZWxlYXNlMCQGCisGAQQBg78wAQUEFkRvY1NwcmluZy9yYWNrLWdhdGV3YXkwHwYKKwYBBAGDvzABBgQRcmVmcy90YWdzL3YwLjEuMjgwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMGkGCisGAQQBg78wAQkEWwxZaHR0cHM6Ly9naXRodWIuY29tL0RvY1NwcmluZy9yYWNrLWdhdGV3YXkvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy90YWdzL3YwLjEuMjgwOAYKKwYBBAGDvzABCgQqDCg2NjQxMjNlY2U2NzY4Mjk5ODc3MWFkODdlZjNlMTc1Njk2Mjg2M2QwMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDA5BgorBgEEAYO/MAEMBCsMKWh0dHBzOi8vZ2l0aHViLmNvbS9Eb2NTcHJpbmcvcmFjay1nYXRld2F5MDgGCisGAQQBg78wAQ0EKgwoNjY0MTIzZWNlNjc2ODI5OTg3NzFhZDg3ZWYzZTE3NTY5NjI4NjNkMDAhBgorBgEEAYO/MAEOBBMMEXJlZnMvdGFncy92MC4xLjI4MBoGCisGAQQBg78wAQ8EDAwKMTAzNDgyODgwNjAsBgorBgEEAYO/MAEQBB4MHGh0dHBzOi8vZ2l0aHViLmNvbS9Eb2NTcHJpbmcwGAYKKwYBBAGDvzABEQQKDAgzMjA2MDQ0MTBpBgorBgEEAYO/MAESBFsMWWh0dHBzOi8vZ2l0aHViLmNvbS9Eb2NTcHJpbmcvcmFjay1nYXRld2F5Ly5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvdGFncy92MC4xLjI4MDgGCisGAQQBg78wARMEKgwoNjY0MTIzZWNlNjc2ODI5OTg3NzFhZDg3ZWYzZTE3NTY5NjI4NjNkMDAUBgorBgEEAYO/MAEUBAYMBHB1c2gwXQYKKwYBBAGDvzABFQRPDE1odHRwczovL2dpdGh1Yi5jb20vRG9jU3ByaW5nL3JhY2stZ2F0ZXdheS9hY3Rpb25zL3J1bnMvNDA0NzA0OTY0NjYvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzBBBgorBgEEAYO/MAEYBDMMMXJlcG86RG9jU3ByaW5nL3JhY2stZ2F0ZXdheTpyZWY6cmVmcy90YWdzL3YwLjEuMjgwgYsGCisGAQQB1nkCBAIEfQR7AHkAdwDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAaF8Z1TNAAAEAwBIMEYCIQCFFDqLfhk/lSTPbmrTrDcTg5sqYLMg5TntRk2eRAuv8AIhAI6taNFEJJB6m/uwp7nsnEd9s/2OJU6wzFASaNHhvtZIMAoGCCqGSM49BAMDA2cAMGQCMFbW/QFU872iNJ6h1IEfL1WY2nHD3DMrtaS0S2hQfKxuwgehPzxk3iY2fuqOAc+ZWQIwPCgbVBF5fj/g+Z0g503fk9TIhX88asi/jvymFSYfjwIqy3FQ9jXSiKDVanxgt0ks" + } + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicmFjay1nYXRld2F5LWxpbnV4LWFtZDY0LnRhci5neiIsImRpZ2VzdCI6eyJzaGEyNTYiOiIxMzY3OTU1M2NmN2UxYjQ1MzNmNmIyZjE1MWI1MjFjZGQwYjM1YjcwYzgxNDg4YzY4M2IzMmFmNDEyNzBkOTFlIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MSIsInByZWRpY2F0ZSI6eyJidWlsZERlZmluaXRpb24iOnsiYnVpbGRUeXBlIjoiaHR0cHM6Ly9hY3Rpb25zLmdpdGh1Yi5pby9idWlsZHR5cGVzL3dvcmtmbG93L3YxIiwiZXh0ZXJuYWxQYXJhbWV0ZXJzIjp7IndvcmtmbG93Ijp7InJlZiI6InJlZnMvdGFncy92MC4xLjI4IiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9Eb2NTcHJpbmcvcmFjay1nYXRld2F5IiwicGF0aCI6Ii5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sIn19LCJpbnRlcm5hbFBhcmFtZXRlcnMiOnsiZ2l0aHViIjp7ImV2ZW50X25hbWUiOiJwdXNoIiwicmVwb3NpdG9yeV9pZCI6IjEwMzQ4Mjg4MDYiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiMzIwNjA0NDEiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9Eb2NTcHJpbmcvcmFjay1nYXRld2F5QHJlZnMvdGFncy92MC4xLjI4IiwiZGlnZXN0Ijp7ImdpdENvbW1pdCI6IjY2NDEyM2VjZTY3NjgyOTk4NzcxYWQ4N2VmM2UxNzU2OTYyODYzZDAifX1dfSwicnVuRGV0YWlscyI6eyJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly9naXRodWIuY29tL0RvY1NwcmluZy9yYWNrLWdhdGV3YXkvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy90YWdzL3YwLjEuMjgifSwibWV0YWRhdGEiOnsiaW52b2NhdGlvbklkIjoiaHR0cHM6Ly9naXRodWIuY29tL0RvY1NwcmluZy9yYWNrLWdhdGV3YXkvYWN0aW9ucy9ydW5zLzQwNDcwNDk2NDY2L2F0dGVtcHRzLzEifX19fQ==", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEQCIFNHRcWcDhzveYOfb+ASU3slq7MiZ1lI/MiYpnY4heLQAiBjTTtrG92VWaY83krUezy8/Df7NdNNtVicuSVQB/amDQ==" + } + ] + } +} diff --git a/internal/cli/update/verify.go b/internal/cli/update/verify.go new file mode 100644 index 00000000..828d79f3 --- /dev/null +++ b/internal/cli/update/verify.go @@ -0,0 +1,112 @@ +package update + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + "github.com/sigstore/sigstore-go/pkg/verify" +) + +const ( + // githubActionsIssuer issues the OIDC tokens GitHub Actions jobs sign attestations with. + githubActionsIssuer = "https://token.actions.githubusercontent.com" + // slsaProvenanceV1 is the predicate type of the attestations actions/attest-build-provenance creates. + slsaProvenanceV1 = "https://slsa.dev/provenance/v1" +) + +// Release workflows sign with the public Sigstore instance's certificates, which are only valid for a few minutes, +// so a bundle must carry a transparency log entry and a signed certificate timestamp, as gh attestation verify +// requires. +var publicGoodVerifierOptions = []verify.VerifierOption{ + verify.WithSignedCertificateTimestamps(1), + verify.WithTransparencyLog(1), + verify.WithObserverTimestamps(1), +} + +// SignerIdentity is the certificate identity of the Release workflow run that built the release with this tag. +func SignerIdentity(repository, tag string) string { + return fmt.Sprintf("https://github.com/%s/.github/workflows/release.yml@refs/tags/%s", repository, tag) +} + +// ProvenanceVerifier checks a release archive's build provenance attestation. +type ProvenanceVerifier struct { + verifier *verify.Verifier + repository string +} + +// NewProvenanceVerifier fetches the public Sigstore instance's trusted root through TUF (the root of trust is built +// into sigstore-go) and returns a verifier for this repository's releases. +func NewProvenanceVerifier(repository string) (*ProvenanceVerifier, error) { + opts := tuf.DefaultOptions().WithDisableLocalCache() + trustedRoot, err := root.FetchTrustedRootWithOptions(opts) + if err != nil { + return nil, fmt.Errorf("failed to fetch the Sigstore trusted root: %w", err) + } + return provenanceVerifierWith(trustedRoot, repository, publicGoodVerifierOptions...) +} + +func provenanceVerifierWith( + material root.TrustedMaterial, + repository string, + options ...verify.VerifierOption, +) (*ProvenanceVerifier, error) { + verifier, err := verify.NewVerifier(material, options...) + if err != nil { + return nil, err + } + return &ProvenanceVerifier{verifier: verifier, repository: repository}, nil +} + +// Verify checks that bundleJSON is a SLSA provenance attestation for an artifact with this SHA-256 digest, signed by +// the repository's Release workflow running for this tag. +func (p *ProvenanceVerifier) Verify(bundleJSON, digest []byte, tag string) error { + var b bundle.Bundle + if err := b.UnmarshalJSON(bundleJSON); err != nil { + return fmt.Errorf("invalid attestation bundle: %w", err) + } + return p.verifyEntity(&b, digest, tag) +} + +func (p *ProvenanceVerifier) verifyEntity(entity verify.SignedEntity, digest []byte, tag string) error { + identity, err := verify.NewShortCertificateIdentity(githubActionsIssuer, "", SignerIdentity(p.repository, tag), "") + if err != nil { + return err + } + policy := verify.NewPolicy(verify.WithArtifactDigest("sha256", digest), verify.WithCertificateIdentity(identity)) + result, err := p.verifier.Verify(entity, policy) + if err != nil { + return fmt.Errorf("attestation verification failed: %w", err) + } + if result.Statement == nil || result.Statement.GetPredicateType() != slsaProvenanceV1 { + return errors.New("attestation verification failed: not a SLSA build provenance attestation") + } + return nil +} + +// VerifyChecksum checks data against a checksum file in sha256sum format (" "), which must have +// a line for name. It returns the data's SHA-256 digest. +func VerifyChecksum(data, checksums []byte, name string) ([]byte, error) { + sum := sha256.Sum256(data) + for _, line := range strings.Split(string(checksums), "\n") { + fields := strings.Fields(line) + if len(fields) != 2 || strings.TrimPrefix(fields[1], "*") != name { + continue + } + expected, err := hex.DecodeString(fields[0]) + if err != nil { + return nil, fmt.Errorf("invalid checksum for %s", name) + } + if !bytes.Equal(expected, sum[:]) { + return nil, fmt.Errorf("checksum mismatch for %s", name) + } + return sum[:], nil + } + return nil, fmt.Errorf("no checksum for %s", name) +} diff --git a/internal/cli/update/verify_test.go b/internal/cli/update/verify_test.go new file mode 100644 index 00000000..66e26367 --- /dev/null +++ b/internal/cli/update/verify_test.go @@ -0,0 +1,161 @@ +package update + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "testing" + + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/testing/ca" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/stretchr/testify/require" +) + +const ( + testRepository = "DocSpring/rack-gateway" + testTag = "v0.1.29" + // The SHA-256 digest of the v0.1.28 rack-gateway-linux-amd64.tar.gz release archive. + v0128LinuxAMD64Digest = "13679553cf7e1b4533f6b2f151b521cdd0b35b70c81488c683b32af41270d91e" +) + +// The v0.1.28 release's real attestation, checked with the public Sigstore instance's trusted root as TUF served +// it in October 2026: the verifier options and policy accept what actions/attest-build-provenance creates. +func TestProvenanceVerifierAcceptsARealReleaseAttestation(t *testing.T) { + bundleJSON, err := os.ReadFile("testdata/rack-gateway-linux-amd64-v0.1.28.sigstore.json") + require.NoError(t, err) + trustedRoot, err := root.NewTrustedRootFromPath("testdata/public-good-trusted-root.json") + require.NoError(t, err) + verifier, err := provenanceVerifierWith(trustedRoot, testRepository, publicGoodVerifierOptions...) + require.NoError(t, err) + digest, err := hex.DecodeString(v0128LinuxAMD64Digest) + require.NoError(t, err) + + require.NoError(t, verifier.Verify(bundleJSON, digest, "v0.1.28")) + + require.ErrorContains(t, verifier.Verify(bundleJSON, digest, "v0.1.27"), "attestation verification failed") + otherDigest := sha256.Sum256([]byte("another archive")) + require.ErrorContains(t, verifier.Verify(bundleJSON, otherDigest[:], "v0.1.28"), "attestation verification failed") + fork, err := provenanceVerifierWith(trustedRoot, "someone/rack-gateway", publicGoodVerifierOptions...) + require.NoError(t, err) + require.ErrorContains(t, fork.Verify(bundleJSON, digest, "v0.1.28"), "attestation verification failed") + require.ErrorContains(t, verifier.Verify([]byte(`{}`), digest, "v0.1.28"), "invalid attestation bundle") +} + +func TestProvenanceVerifierChecksSignerDigestAndPredicate(t *testing.T) { + sigstore, err := ca.NewVirtualSigstore() + require.NoError(t, err) + verifier, err := provenanceVerifierWith(sigstore, testRepository, + verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + require.NoError(t, err) + digest := sha256.Sum256([]byte("release archive")) + + tests := []struct { + name string + identity string + issuer string + predicate string + wantErr string + }{ + { + name: "the release workflow for this tag", + identity: SignerIdentity(testRepository, testTag), + issuer: githubActionsIssuer, + predicate: slsaProvenanceV1, + }, + { + name: "the release workflow for another tag", + identity: SignerIdentity(testRepository, "v0.1.28"), + issuer: githubActionsIssuer, + predicate: slsaProvenanceV1, + wantErr: "attestation verification failed", + }, + { + name: "the release workflow from a branch", + identity: "https://github.com/DocSpring/rack-gateway/.github/workflows/release.yml@refs/heads/main", + issuer: githubActionsIssuer, + predicate: slsaProvenanceV1, + wantErr: "attestation verification failed", + }, + { + name: "another workflow", + identity: "https://github.com/DocSpring/rack-gateway/.github/workflows/ci.yml@refs/tags/" + testTag, + issuer: githubActionsIssuer, + predicate: slsaProvenanceV1, + wantErr: "attestation verification failed", + }, + { + name: "another repository", + identity: SignerIdentity("someone/rack-gateway", testTag), + issuer: githubActionsIssuer, + predicate: slsaProvenanceV1, + wantErr: "attestation verification failed", + }, + { + name: "another OIDC issuer", + identity: SignerIdentity(testRepository, testTag), + issuer: "https://accounts.google.com", + predicate: slsaProvenanceV1, + wantErr: "attestation verification failed", + }, + { + name: "not build provenance", + identity: SignerIdentity(testRepository, testTag), + issuer: githubActionsIssuer, + predicate: "https://in-toto.io/attestation/release/v0.1", + wantErr: "not a SLSA build provenance attestation", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entity, err := sigstore.Attest(tt.identity, tt.issuer, statement(tt.predicate, digest[:])) + require.NoError(t, err) + err = verifier.verifyEntity(entity, digest[:], testTag) + if tt.wantErr == "" { + require.NoError(t, err) + return + } + require.ErrorContains(t, err, tt.wantErr) + }) + } + + t.Run("another artifact", func(t *testing.T) { + entity, err := sigstore.Attest(SignerIdentity(testRepository, testTag), githubActionsIssuer, + statement(slsaProvenanceV1, digest[:])) + require.NoError(t, err) + other := sha256.Sum256([]byte("another archive")) + require.ErrorContains(t, verifier.verifyEntity(entity, other[:], testTag), "attestation verification failed") + }) +} + +func statement(predicateType string, digest []byte) []byte { + return fmt.Appendf(nil, + `{"_type":"https://in-toto.io/Statement/v1","predicateType":%q,`+ + `"subject":[{"name":"rack-gateway-linux-amd64.tar.gz","digest":{"sha256":%q}}],"predicate":{}}`, + predicateType, hex.EncodeToString(digest)) +} + +func TestVerifyChecksum(t *testing.T) { + data := []byte("release archive") + sum := sha256.Sum256(data) + hexSum := hex.EncodeToString(sum[:]) + + digest, err := VerifyChecksum(data, []byte(hexSum+" a.tar.gz\n"), "a.tar.gz") + require.NoError(t, err) + require.Equal(t, sum[:], digest) + + // sha256sum's binary mode marks the name with "*". + _, err = VerifyChecksum(data, []byte(hexSum+" *a.tar.gz\n"), "a.tar.gz") + require.NoError(t, err) + + other := sha256.Sum256([]byte("tampered")) + _, err = VerifyChecksum(data, []byte(hex.EncodeToString(other[:])+" a.tar.gz\n"), "a.tar.gz") + require.ErrorContains(t, err, "checksum mismatch for a.tar.gz") + + _, err = VerifyChecksum(data, []byte(hexSum+" b.tar.gz\n"), "a.tar.gz") + require.ErrorContains(t, err, "no checksum for a.tar.gz") + + _, err = VerifyChecksum(data, []byte("not-hex a.tar.gz\n"), "a.tar.gz") + require.ErrorContains(t, err, "invalid checksum for a.tar.gz") +} diff --git a/internal/cli/webauthn/webauthn.go b/internal/cli/webauthn/webauthn.go index 0ee6e9b5..641514e4 100644 --- a/internal/cli/webauthn/webauthn.go +++ b/internal/cli/webauthn/webauthn.go @@ -16,6 +16,9 @@ import ( "github.com/DocSpring/rack-gateway/internal/cli/touchnotify" ) +// Supported reports whether this build can use security keys (it was built with libfido2). +const Supported = true + // GetAssertion prompts the user to authenticate with their FIDO2 device func GetAssertion(options AssertionOptions) (*AssertionResponse, error) { resp, _, err := GetAssertionWithCachedPIN(options, "") diff --git a/internal/cli/webauthn/webauthn_stub.go b/internal/cli/webauthn/webauthn_stub.go index 2c97118f..bb141684 100644 --- a/internal/cli/webauthn/webauthn_stub.go +++ b/internal/cli/webauthn/webauthn_stub.go @@ -4,6 +4,9 @@ package webauthn import "fmt" +// Supported reports whether this build can use security keys. This one was built with the nofido tag. +const Supported = false + // GetAssertion stub - WebAuthn not available in this build func GetAssertion(options AssertionOptions) (*AssertionResponse, error) { return nil, fmt.Errorf("WebAuthn support not compiled in this build (use -tags without nofido)") diff --git a/scripts/build-cli-release.sh b/scripts/build-cli-release.sh new file mode 100755 index 00000000..bbf4985f --- /dev/null +++ b/scripts/build-cli-release.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Builds the CLI release archive for the platform it runs on: dist/rack-gateway--.tar.gz, holding a +# single `rack-gateway` binary, and its .sha256 checksum file. The Release workflow runs it on each platform's +# runner, so it can run the binary it built. +# +# macOS builds link libfido2 and OpenSSL statically from Homebrew (brew install libfido2 openssl@3), so they +# support security keys. Linux builds use the nofido tag: go-libfido2 links libfido2 dynamically there. +# +# Usage: scripts/build-cli-release.sh +set -euo pipefail + +VERSION="${1:?usage: build-cli-release.sh }" +REPOSITORY="${2:?usage: build-cli-release.sh }" + +OS="$(go env GOOS)" +ARCH="$(go env GOARCH)" +NAME="rack-gateway-${OS}-${ARCH}" + +case "$OS" in + darwin) + export CGO_ENABLED=1 CGO_CFLAGS=-Wno-deprecated-declarations + TAGS="" + SECURITY_KEYS="supported" + ;; + linux) + export CGO_ENABLED=0 + TAGS="nofido" + SECURITY_KEYS="not supported" + ;; + *) + echo "Error: releases have no build for $OS" >&2 + exit 1 + ;; +esac + +BUILD_TIME="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +LDFLAGS="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" +LDFLAGS+=" -X github.com/DocSpring/rack-gateway/internal/cli/update.Repository=${REPOSITORY}" + +rm -rf dist/build +mkdir -p dist/build +go build -tags "$TAGS" -buildvcs=false -trimpath -ldflags "$LDFLAGS" -o dist/build/rack-gateway ./cmd/rack-gateway/ + +# rack-gateway update runs build-info on the binary it downloads, so check it here too. +info="$(dist/build/rack-gateway build-info)" +echo "$info" +grep -qxF "version: ${VERSION}" <<<"$info" +grep -qxF "security keys: ${SECURITY_KEYS}" <<<"$info" + +# The macOS binary must only link libraries every Mac has, not Homebrew's. +if [ "$OS" = "darwin" ]; then + otool -L dist/build/rack-gateway + if otool -L dist/build/rack-gateway | tail -n +2 | grep -vE '^[[:space:]]+(/usr/lib/|/System/Library/)'; then + echo "Error: the binary links libraries outside macOS" >&2 + exit 1 + fi +fi + +# COPYFILE_DISABLE keeps macOS tar from adding AppleDouble (._*) files. +COPYFILE_DISABLE=1 tar -C dist/build -czf "dist/${NAME}.tar.gz" rack-gateway +if command -v sha256sum >/dev/null; then + (cd dist && sha256sum "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256") +else + (cd dist && shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256") +fi +cat "dist/${NAME}.tar.gz.sha256" diff --git a/scripts/install-release.sh b/scripts/install-release.sh new file mode 100755 index 00000000..cf9da358 --- /dev/null +++ b/scripts/install-release.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# Installs the rack-gateway CLI from a GitHub release: +# +# curl -fsSL https://github.com/DocSpring/rack-gateway/releases/latest/download/install.sh | sh +# +# Releases have builds for macOS on Apple Silicon and Linux (x86_64 and arm64). The script checks the archive's +# SHA-256 checksum, and its build provenance attestation with `gh attestation verify` when the GitHub CLI is +# installed, before it installs anything. +# +# Environment: +# RACK_GATEWAY_VERSION The release to install, such as v0.1.29 (default: the latest release) +# RACK_GATEWAY_INSTALL_DIR Where to install (default: /usr/local/bin when you can write to it, or ~/.local/bin) +# RACK_GATEWAY_REPOSITORY The GitHub repository to install from (default: DocSpring/rack-gateway) +set -eu + +REPOSITORY="${RACK_GATEWAY_REPOSITORY:-DocSpring/rack-gateway}" +SOURCE_DOCS="https://github.com/${REPOSITORY}#readme" + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +platform() { + os="$(uname -s)" + arch="$(uname -m)" + case "$os" in + Darwin) os=darwin ;; + Linux) os=linux ;; + *) fail "releases have no build for $os: build the CLI from source ($SOURCE_DOCS)" ;; + esac + case "$arch" in + x86_64 | amd64) arch=amd64 ;; + arm64 | aarch64) arch=arm64 ;; + *) fail "releases have no build for $arch: build the CLI from source ($SOURCE_DOCS)" ;; + esac + # A shell running under Rosetta reports x86_64 on an Apple Silicon Mac. + if [ "$os" = darwin ] && [ "$arch" = amd64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then + arch=arm64 + fi + if [ "$os" = darwin ] && [ "$arch" = amd64 ]; then + fail "releases have no build for Intel Macs: build the CLI from source ($SOURCE_DOCS)" + fi + echo "$os-$arch" +} + +download() { + if command -v curl >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -fsSL -o "$2" "$1" + elif command -v wget >/dev/null 2>&1; then + wget --https-only -q -O "$2" "$1" + else + fail "curl or wget is needed to download the CLI" + fi +} + +verify_checksum() { + if command -v sha256sum >/dev/null 2>&1; then + (cd "$1" && sha256sum -c "$2.sha256" >/dev/null) || fail "checksum mismatch for $2" + else + (cd "$1" && shasum -a 256 -c "$2.sha256" >/dev/null) || fail "checksum mismatch for $2" + fi +} + +verify_attestation() { + if ! command -v gh >/dev/null 2>&1; then + echo "Checked the SHA-256 checksum. Install the GitHub CLI (gh) to also check the build provenance attestation." + return + fi + gh attestation verify "$1/$2" --bundle "$1/$2.sigstore.json" --repo "$REPOSITORY" \ + --signer-workflow "$REPOSITORY/.github/workflows/release.yml" >/dev/null || + fail "the build provenance attestation for $2 doesn't check out" + echo "Checked the SHA-256 checksum and the build provenance attestation." +} + +install_dir() { + if [ -n "${RACK_GATEWAY_INSTALL_DIR:-}" ]; then + echo "$RACK_GATEWAY_INSTALL_DIR" + elif [ -w /usr/local/bin ]; then + echo /usr/local/bin + else + echo "$HOME/.local/bin" + fi +} + +main() { + asset="rack-gateway-$(platform).tar.gz" + if [ -n "${RACK_GATEWAY_VERSION:-}" ]; then + base="https://github.com/$REPOSITORY/releases/download/v${RACK_GATEWAY_VERSION#v}" + else + base="https://github.com/$REPOSITORY/releases/latest/download" + fi + + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + echo "Downloading $base/$asset" + for file in "$asset" "$asset.sha256" "$asset.sigstore.json"; do + download "$base/$file" "$tmp/$file" + done + verify_checksum "$tmp" "$asset" + verify_attestation "$tmp" "$asset" + tar -xzf "$tmp/$asset" -C "$tmp" rack-gateway + + dir="$(install_dir)" + mkdir -p "$dir" + [ -w "$dir" ] || fail "can't write to $dir: set RACK_GATEWAY_INSTALL_DIR, or pipe the script to sudo sh" + # Rename over the old binary rather than writing into it, so a running rack-gateway keeps working. + cp "$tmp/rack-gateway" "$dir/.rack-gateway.install.$$" + chmod 0755 "$dir/.rack-gateway.install.$$" + mv -f "$dir/.rack-gateway.install.$$" "$dir/rack-gateway" + + version="$("$dir/rack-gateway" build-info | sed -n 's/^version: //p')" + echo "Installed rack-gateway $version to $dir/rack-gateway" + case ":$PATH:" in + *":$dir:"*) ;; + *) echo "$dir isn't on your PATH: add it to run rack-gateway" ;; + esac +} + +main "$@" From 2fb0988e1174d3f4db806ffd99a316560d3dc3ed Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 28 Oct 2026 11:30:42 +1300 Subject: [PATCH 2/2] fix(cli): tell builds no release can replace to build from source update.Replaceable decides which builds a release can replace: a platform without a release build, or a Linux build with security keys (only the macOS release has them), can't be. The update notice and update --check now tell those to build the new release from source, and update refuses them before it downloads anything (the build-info check after the download stays). update --version naming the installed release now says it's already installed, not that it's the latest. --- internal/cli/AGENTS.md | 3 ++ internal/cli/cli_update.go | 23 +++++++++-- internal/cli/cli_update_check.go | 5 ++- internal/cli/cli_update_test.go | 15 ++++++- internal/cli/update/check.go | 31 ++++++++------ internal/cli/update/check_test.go | 33 +++++++++++---- internal/cli/update/install.go | 8 ++-- internal/cli/update/install_test.go | 64 ++++++++++++++++++++--------- internal/cli/update/release.go | 14 +++++++ 9 files changed, 148 insertions(+), 48 deletions(-) diff --git a/internal/cli/AGENTS.md b/internal/cli/AGENTS.md index 3bb20620..5e818a68 100644 --- a/internal/cli/AGENTS.md +++ b/internal/cli/AGENTS.md @@ -174,6 +174,9 @@ installed binary untouched: build has it. Then the rename. Development builds (`Version` isn't a release) refuse to update. Without `--version`, it never downgrades. +`update.Replaceable` says which builds no release can replace: a platform without a release build, or a Linux +build with security keys (only the macOS release has them). `update` refuses those before downloading anything, and +the notice and `update --check` tell them to build the new release from source. `update.Repository` defaults to `DocSpring/rack-gateway`; `scripts/build-cli-release.sh` sets it to the repository that builds the release. diff --git a/internal/cli/cli_update.go b/internal/cli/cli_update.go index 6986899f..0d9f8dfc 100644 --- a/internal/cli/cli_update.go +++ b/internal/cli/cli_update.go @@ -3,6 +3,7 @@ package cli import ( "context" "fmt" + "io" "os" "path/filepath" "runtime" @@ -53,7 +54,11 @@ func runUpdate(ctx context.Context, cmd *cobra.Command, client *update.Client, c return err } out := cmd.OutOrStdout() + replaceable := update.Replaceable(runtime.GOOS, runtime.GOARCH, webauthn.Supported) switch { + case rel.Version() == Version && version != "": + _, _ = fmt.Fprintf(out, "rack-gateway %s is already installed.\n", Version) + return nil case rel.Version() == Version: _, _ = fmt.Fprintf(out, "rack-gateway %s is the latest release.\n", Version) return nil @@ -61,14 +66,26 @@ func runUpdate(ctx context.Context, cmd *cobra.Command, client *update.Client, c _, _ = fmt.Fprintf(out, "rack-gateway %s is newer than the latest release (%s).\n", Version, rel.Version()) return nil case checkOnly: - _, _ = fmt.Fprintf(out, "rack-gateway %s is available (this is %s): %s\n", - rel.Version(), Version, client.ReleaseURL(rel.Tag)) - _, _ = fmt.Fprintln(out, `Run "rack-gateway update" to install it.`) + printAvailableRelease(out, client, rel, replaceable) return nil + case replaceable != nil: + return replaceable } return installRelease(ctx, cmd, client, rel) } +// printAvailableRelease says a release is available, and how to install it: `rack-gateway update`, unless no +// release can replace this build (replaceable). +func printAvailableRelease(out io.Writer, client *update.Client, rel *update.Release, replaceable error) { + _, _ = fmt.Fprintf(out, "rack-gateway %s is available (this is %s): %s\n", + rel.Version(), Version, client.ReleaseURL(rel.Tag)) + if replaceable != nil { + _, _ = fmt.Fprintf(out, "rack-gateway update can't install it: %s.\n", replaceable) + return + } + _, _ = fmt.Fprintln(out, `Run "rack-gateway update" to install it.`) +} + // targetRelease returns the release named by --version, or the latest release. func targetRelease(ctx context.Context, client *update.Client, version string) (*update.Release, error) { if version == "" { diff --git a/internal/cli/cli_update_check.go b/internal/cli/cli_update_check.go index e7eb03fc..a58034a7 100644 --- a/internal/cli/cli_update_check.go +++ b/internal/cli/cli_update_check.go @@ -5,6 +5,7 @@ import ( "io/fs" "os" "path/filepath" + "runtime" "time" "github.com/spf13/cobra" @@ -42,7 +43,9 @@ func startUpdateCheck(cmd *cobra.Command) { } checker := &update.Checker{ Client: update.NewClient(Version, updateCheckTimeout), - Current: Version, + Current: currentBuildInfo(), + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, Load: func() ([]byte, error) { return fs.ReadFile(os.DirFS(ConfigPath), updateCheckFile) }, diff --git a/internal/cli/cli_update_test.go b/internal/cli/cli_update_test.go index cf36a404..c805f734 100644 --- a/internal/cli/cli_update_test.go +++ b/internal/cli/cli_update_test.go @@ -5,6 +5,7 @@ import ( "context" "net/http" "net/http/httptest" + "runtime" "testing" "time" @@ -62,6 +63,12 @@ func TestUpdateWhenAlreadyCurrentOrNewer(t *testing.T) { out, err = runUpdateForTest(t, releaseClient(t, "v0.1.27"), false, "") require.NoError(t, err) require.Equal(t, "rack-gateway 0.1.28 is newer than the latest release (0.1.27).\n", out) + + // --version naming this build's own release doesn't claim it's the latest. + setVersion(t, "0.1.27") + out, err = runUpdateForTest(t, releaseClient(t, "v0.1.29"), false, "v0.1.27") + require.NoError(t, err) + require.Equal(t, "rack-gateway 0.1.27 is already installed.\n", out) } func TestUpdateCheckOnlyReportsTheRelease(t *testing.T) { @@ -69,9 +76,13 @@ func TestUpdateCheckOnlyReportsTheRelease(t *testing.T) { out, err := runUpdateForTest(t, releaseClient(t, "v0.1.29"), true, "") require.NoError(t, err) + howTo := "Run \"rack-gateway update\" to install it.\n" + // A Linux test build with security keys, or a platform without releases, can't be replaced by a release. + if replaceable := update.Replaceable(runtime.GOOS, runtime.GOARCH, webauthn.Supported); replaceable != nil { + howTo = "rack-gateway update can't install it: " + replaceable.Error() + ".\n" + } require.Equal(t, "rack-gateway 0.1.29 is available (this is 0.1.28): "+ - "https://github.com/DocSpring/rack-gateway/releases/tag/v0.1.29\n"+ - "Run \"rack-gateway update\" to install it.\n", out) + "https://github.com/DocSpring/rack-gateway/releases/tag/v0.1.29\n"+howTo, out) // --version names a release even when it's older than this one. out, err = runUpdateForTest(t, releaseClient(t, "v0.1.29"), true, "0.1.27") diff --git a/internal/cli/update/check.go b/internal/cli/update/check.go index 9b0dc436..757e0670 100644 --- a/internal/cli/update/check.go +++ b/internal/cli/update/check.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "runtime" "time" ) @@ -20,7 +19,9 @@ type checkState struct { // Checker checks GitHub for a newer release at most once per CheckInterval. type Checker struct { Client *Client - Current string + Current BuildInfo + GOOS string + GOARCH string // Load returns the saved state, or an error when there is none. Save replaces it. Load func() ([]byte, error) Save func([]byte) error @@ -30,16 +31,22 @@ type Checker struct { // Check is an update check that may still be running in the background. type Check struct { client *Client - current string - saved checkState - done chan struct{} - cancel context.CancelFunc - fresh *checkState + current BuildInfo + // replaceable is why `rack-gateway update` can't replace this build, or nil (see Replaceable). + replaceable error + saved checkState + done chan struct{} + cancel context.CancelFunc + fresh *checkState } // Start begins a background check when the last one is more than CheckInterval old. It never blocks. func (c *Checker) Start() *Check { - check := &Check{client: c.Client, current: c.Current} + check := &Check{ + client: c.Client, + current: c.Current, + replaceable: Replaceable(c.GOOS, c.GOARCH, c.Current.SecurityKeys), + } if data, err := c.Load(); err == nil { _ = json.Unmarshal(data, &check.saved) } @@ -89,15 +96,15 @@ func (k *Check) Notice(wait time.Duration) string { timer.Stop() k.cancel() } - if state.LatestTag == "" || !IsNewer(state.LatestTag, k.current) { + if state.LatestTag == "" || !IsNewer(state.LatestTag, k.current.Version) { return "" } latest := Release{Tag: state.LatestTag} - if _, ok := AssetName(runtime.GOOS, runtime.GOARCH); !ok { + if k.replaceable != nil { return fmt.Sprintf("A new release of rack-gateway is available: %s → %s. Build it from source: %s", - k.current, latest.Version(), k.client.ReleaseURL(latest.Tag)) + k.current.Version, latest.Version(), k.client.ReleaseURL(latest.Tag)) } return fmt.Sprintf( "A new release of rack-gateway is available: %s → %s. Run \"rack-gateway update\" to install it.", - k.current, latest.Version()) + k.current.Version, latest.Version()) } diff --git a/internal/cli/update/check_test.go b/internal/cli/update/check_test.go index 2ae3a2e5..8f27a229 100644 --- a/internal/cli/update/check_test.go +++ b/internal/cli/update/check_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "net/http" - "runtime" "sync/atomic" "testing" "time" @@ -22,7 +21,9 @@ type memoryState struct { func (m *memoryState) checker(client *Client, current string) *Checker { return &Checker{ Client: client, - Current: current, + Current: BuildInfo{Version: current}, + GOOS: "linux", + GOARCH: "amd64", Load: func() ([]byte, error) { if m.data == nil { return nil, errors.New("no state") @@ -63,15 +64,33 @@ func latestHandler(t *testing.T, requests *atomic.Int32, tag string) http.Handle // requireNotice checks notice suggests updating from 0.1.28 to 0.1.29. func requireNotice(t *testing.T, notice string) { t.Helper() - if _, ok := AssetName(runtime.GOOS, runtime.GOARCH); !ok { - require.Contains(t, notice, - "A new release of rack-gateway is available: 0.1.28 → 0.1.29. Build it from source: ") - return - } require.Equal(t, `A new release of rack-gateway is available: 0.1.28 → 0.1.29. `+ `Run "rack-gateway update" to install it.`, notice) } +// A build no release can replace is told to build the new release from source instead. +func TestNoticeForBuildsNoReleaseReplaces(t *testing.T) { + var requests atomic.Int32 + client := testClient(t, latestHandler(t, &requests, "v0.1.29")) + sourceBuild := "A new release of rack-gateway is available: 0.1.28 → 0.1.29. Build it from source: " + + client.DownloadBaseURL + "/DocSpring/rack-gateway/releases/tag/v0.1.29" + + for name, modify := range map[string]func(c *Checker){ + "linux with security keys": func(c *Checker) { c.Current.SecurityKeys = true }, + "intel mac": func(c *Checker) { c.GOOS = "darwin" }, + } { + t.Run(name, func(t *testing.T) { + checker := (&memoryState{}).checker(client, "0.1.28") + modify(checker) + require.Equal(t, sourceBuild, checker.Start().Notice(5*time.Second)) + }) + } + + macWithKeys := (&memoryState{}).checker(client, "0.1.28") + macWithKeys.GOOS, macWithKeys.GOARCH, macWithKeys.Current.SecurityKeys = "darwin", "arm64", true + requireNotice(t, macWithKeys.Start().Notice(5*time.Second)) +} + func TestCheckAsksGitHubOnceADay(t *testing.T) { var requests atomic.Int32 client := testClient(t, latestHandler(t, &requests, "v0.1.29")) diff --git a/internal/cli/update/install.go b/internal/cli/update/install.go index be697c08..2ea2e626 100644 --- a/internal/cli/update/install.go +++ b/internal/cli/update/install.go @@ -77,10 +77,10 @@ type Installer struct { // that the binary inside runs and is a release this build can be replaced with, and then renames it over the // running binary. Until that rename, the installed binary is untouched. func (i *Installer) Install(ctx context.Context, rel *Release) error { - asset, ok := AssetName(i.GOOS, i.GOARCH) - if !ok { - return fmt.Errorf("releases have no build for %s/%s: build the CLI from source", i.GOOS, i.GOARCH) + if err := Replaceable(i.GOOS, i.GOARCH, i.Current.SecurityKeys); err != nil { + return err } + asset, _ := AssetName(i.GOOS, i.GOARCH) dir := filepath.Dir(i.Executable) tmp, err := os.CreateTemp(dir, "."+binaryName+".update-*") if err != nil { @@ -141,7 +141,7 @@ func (i *Installer) download(ctx context.Context, tag, asset string) ([]byte, er } // check makes the new binary executable like the one it replaces, runs it, and checks it is the release we asked -// for and keeps security key support when this build has it. +// for and keeps security key support when this build has it (Replaceable says which releases should). func (i *Installer) check(ctx context.Context, path string, rel *Release) error { current, err := os.Stat(i.Executable) if err != nil { diff --git a/internal/cli/update/install_test.go b/internal/cli/update/install_test.go index 006119ef..54d689d3 100644 --- a/internal/cli/update/install_test.go +++ b/internal/cli/update/install_test.go @@ -48,36 +48,45 @@ func tarGz(t *testing.T, entries ...tarEntry) []byte { } type releaseFixture struct { - archive []byte + archive []byte + // checksum is the .sha256 file; nil means the archive's correct one. checksum []byte verified [][]byte verify error info BuildInfo exe string + goos string + goarch string } func newReleaseFixture(t *testing.T) *releaseFixture { t.Helper() archive := tarGz(t, tarEntry{name: "rack-gateway", body: "new binary", typeflag: tar.TypeReg}) - sum := sha256.Sum256(archive) exe := filepath.Join(t.TempDir(), "rack-gateway") require.NoError(t, os.WriteFile(exe, []byte("old binary"), 0o600)) binaryMode := os.FileMode(0o751) require.NoError(t, os.Chmod(exe, binaryMode)) return &releaseFixture{ - archive: archive, - checksum: []byte(hex.EncodeToString(sum[:]) + " " + testAsset + "\n"), - info: BuildInfo{Version: "0.1.29", SecurityKeys: true}, - exe: exe, + archive: archive, + info: BuildInfo{Version: "0.1.29", SecurityKeys: true}, + exe: exe, + goos: "linux", + goarch: "amd64", } } func (f *releaseFixture) installer(t *testing.T, current BuildInfo) *Installer { t.Helper() + asset, _ := AssetName(f.goos, f.goarch) + checksum := f.checksum + if checksum == nil { + sum := sha256.Sum256(f.archive) + checksum = []byte(hex.EncodeToString(sum[:]) + " " + asset + "\n") + } assets := map[string][]byte{ - testAsset: f.archive, - testAsset + ".sha256": f.checksum, - testAsset + ".sigstore.json": []byte(`{"bundle":true}`), + asset: f.archive, + asset + ".sha256": checksum, + asset + ".sigstore.json": []byte(`{"bundle":true}`), } client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, ok := assets[filepath.Base(r.URL.Path)] @@ -103,8 +112,8 @@ func (f *releaseFixture) installer(t *testing.T, current BuildInfo) *Installer { }, Executable: f.exe, Current: current, - GOOS: "linux", - GOARCH: "amd64", + GOOS: f.goos, + GOARCH: f.goarch, Out: &bytes.Buffer{}, } } @@ -123,7 +132,8 @@ var release0129 = &Release{Tag: "v0.1.29"} func TestInstallReplacesTheBinary(t *testing.T) { f := newReleaseFixture(t) - installer := f.installer(t, BuildInfo{Version: "0.1.28", SecurityKeys: true}) + f.info.SecurityKeys = false + installer := f.installer(t, BuildInfo{Version: "0.1.28"}) var out bytes.Buffer installer.Out = &out @@ -167,10 +177,14 @@ func TestInstallLeavesTheBinaryAloneWhenAStepFails(t *testing.T) { wantErr: "reports version 0.1.30, not 0.1.29", }, { - name: "release would drop security key support", - modify: func(f *releaseFixture) { f.info.SecurityKeys = false }, + // Only the macOS release should have security keys: this one is broken. + name: "release would drop security key support", + modify: func(f *releaseFixture) { + f.goos, f.goarch = "darwin", "arm64" + f.info.SecurityKeys = false + }, current: BuildInfo{Version: "0.1.28", SecurityKeys: true}, - wantErr: "has no security key support, and this build has it", + wantErr: "the darwin/arm64 release has no security key support, and this build has it", }, } for _, tt := range tests { @@ -184,10 +198,21 @@ func TestInstallLeavesTheBinaryAloneWhenAStepFails(t *testing.T) { } } -func TestInstallWithoutSecurityKeysAcceptsAReleaseWithout(t *testing.T) { +func TestInstallOnMacKeepsSecurityKeys(t *testing.T) { f := newReleaseFixture(t) - f.info.SecurityKeys = false - require.NoError(t, f.installer(t, BuildInfo{Version: "0.1.28"}).Install(context.Background(), release0129)) + f.goos, f.goarch = "darwin", "arm64" + installer := f.installer(t, BuildInfo{Version: "0.1.28", SecurityKeys: true}) + require.NoError(t, installer.Install(context.Background(), release0129)) +} + +// A Linux build with security keys (built from source) is refused before anything is downloaded: the Linux +// releases have none. +func TestInstallRefusesToReplaceALinuxBuildWithSecurityKeys(t *testing.T) { + f := newReleaseFixture(t) + err := f.installer(t, BuildInfo{Version: "0.1.28", SecurityKeys: true}).Install(context.Background(), release0129) + require.ErrorContains(t, err, "the linux/amd64 release has no security key support, and this build has it") + require.Empty(t, f.verified) + f.requireUnchanged(t) } func TestInstallNeedsAWritableDirectory(t *testing.T) { @@ -209,7 +234,8 @@ func TestInstallRefusesUnsupportedPlatforms(t *testing.T) { f := newReleaseFixture(t) installer := f.installer(t, BuildInfo{Version: "0.1.28"}) installer.GOOS, installer.GOARCH = "darwin", "amd64" - require.ErrorContains(t, installer.Install(context.Background(), release0129), "no build for darwin/amd64") + require.ErrorContains(t, installer.Install(context.Background(), release0129), + "releases have no build for darwin/amd64") f.requireUnchanged(t) } diff --git a/internal/cli/update/release.go b/internal/cli/update/release.go index 8ef350aa..23f442f8 100644 --- a/internal/cli/update/release.go +++ b/internal/cli/update/release.go @@ -173,6 +173,20 @@ func TagFor(version string) string { return "v" + strings.TrimPrefix(version, "v") } +// Replaceable returns why no release can replace a build for this platform, or nil when one can. Releases have +// builds for darwin/arm64, linux/amd64 and linux/arm64, and only the macOS one supports security keys, which a +// build that supports them must keep. +func Replaceable(goos, goarch string, securityKeys bool) error { + if _, ok := AssetName(goos, goarch); !ok { + return fmt.Errorf("releases have no build for %s/%s: build the CLI from source", goos, goarch) + } + if securityKeys && goos != "darwin" { + return fmt.Errorf("the %s/%s release has no security key support, and this build has it: "+ + "build the CLI from source to keep it", goos, goarch) + } + return nil +} + // AssetName returns the name of the release archive for a platform, and whether releases include one. func AssetName(goos, goarch string) (string, bool) { switch goos + "/" + goarch {