From 8deaed98f96e25a8f228525321193149f95aa9a1 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Wed, 22 Jul 2026 21:21:07 +0100 Subject: [PATCH 1/2] Harden CLI distribution trust --- .github/SECURITY.md | 29 ++++++++++++++ INSTALLATION.md | 17 ++++++++- README.md | 16 +++++++- docs/github-actions.md | 12 +++--- docs/quick-start.md | 4 +- docs/tool-guides.md | 2 +- install-web/index.html | 2 +- install-web/install.sh | 86 ++++++++++++++++++++++++++++-------------- install.sh | 86 ++++++++++++++++++++++++++++-------------- 9 files changed, 187 insertions(+), 67 deletions(-) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..23076dec --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,29 @@ +# Security Policy + +## Reporting a Vulnerability + +Please do not open a public issue or pull request for a potential security +vulnerability. Use GitHub's private vulnerability reporting for the public CLI +distribution repository: + +https://github.com/boringcache/cli/security/advisories/new + +Include the CLI version, platform, relevant release or workflow link, expected +impact, and enough reproduction detail for us to verify the report. Do not +include live credentials, customer data, or secrets. + +## Supported Versions + +The latest published CLI release is supported. Security fixes are normally +shipped as a new release rather than backported to older binaries. + +## Scope + +This public repository is the CLI distribution channel. It owns the installer, +release binaries, checksums, signed checksum bundles, release notes, and public +documentation. Product source is maintained separately in the private +BoringCache monorepo. + +Reports about the CLI, installer, release pipeline, artifact integrity, or the +authorization boundary between the CLI and BoringCache service are all welcome +through the private reporting link above. diff --git a/INSTALLATION.md b/INSTALLATION.md index ccbea6ff..efdb386a 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -7,6 +7,14 @@ BoringCache CLI. The normal install command is: curl -sSL https://install.boringcache.com/install.sh | sh ``` +The installer always verifies `SHA256SUMS`. When `cosign` is available it also +verifies `SHA256SUMS.bundle` automatically. To make Sigstore verification +mandatory, install `cosign` first and run: + +```bash +curl -sSL https://install.boringcache.com/install.sh | BORINGCACHE_VERIFY_SIGNATURE=1 sh +``` + When testing the installer itself, use: ```bash @@ -59,6 +67,7 @@ The release workflow publishes: - `boringcache-windows-amd64.exe` - `boringcache-windows-arm64.exe` - `SHA256SUMS` +- `SHA256SUMS.bundle` ## Installation Locations @@ -71,7 +80,13 @@ The script installs to the first writable location: ## Security Notes - Downloads use HTTPS. -- Release assets include `SHA256SUMS`. +- The installer always verifies the binary against `SHA256SUMS`. +- When `cosign` is available, the installer verifies `SHA256SUMS.bundle` + automatically; `BORINGCACHE_VERIFY_SIGNATURE=1` makes this fail closed. +- Sigstore verification accepts only the monorepo CLI release workflow on a + semantic-version tag or the checksum-repair workflow on `main`. - The installer verifies the downloaded binary can run before finishing. - Product source is maintained in the BoringCache monorepo; this public repo is the distribution channel. +- Potential vulnerabilities should be reported through the private process in + [the security policy](.github/SECURITY.md), not a public issue. diff --git a/README.md b/README.md index 7f6a7808..71babeb7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ cd your-project boringcache onboard ``` +The installer always verifies the release checksum. If `cosign` is installed, +it also verifies the signed checksum bundle automatically. To require the +signature and fail closed, run: + +```bash +curl -sSL https://install.boringcache.com/install.sh | BORINGCACHE_VERIFY_SIGNATURE=1 sh +``` + `boringcache onboard` authenticates the CLI, chooses a workspace, writes `.boringcache.toml` when it can, and lines up the same cache names across local runs, Docker builds, and GitHub Actions. If you want to start sign-in from the terminal by email, use `boringcache onboard --email you@example.com`. For a brand-new account, pass `--name` and `--username` too. @@ -38,7 +46,13 @@ Use adapter commands when the build tool already speaks a remote-cache protocol Use `cache-registry` when the repo already has a checked-in local endpoint setup or another process should keep the proxy alive. `cache-registry` is the proxy. `run --proxy` and adapter commands temporarily start that same proxy for one command. When `.boringcache.toml` stores the Docker command, `boringcache docker` is the short form. Use the longer version when you want to pass the Docker command inline. -If you are wiring GitHub Actions, use [`boringcache/one@v1`](https://github.com/boringcache/one) after onboard so CI can reuse the same repo config and trust model. +If you are wiring GitHub Actions, pin the verified `boringcache/one` v1.13.99 +distribution commit after onboard so CI can reuse the same repo config and +trust model: + +```yaml +- uses: boringcache/one@b55458ec8a4165e3fd70b1a1645f518a2095ed02 # v1.13.99 +``` ## Docs diff --git a/docs/github-actions.md b/docs/github-actions.md index 3fff4281..e2c42609 100644 --- a/docs/github-actions.md +++ b/docs/github-actions.md @@ -5,14 +5,14 @@ The preferred path is: 1. install the CLI locally 2. run `boringcache onboard` 3. commit `.boringcache.toml` when it helps -4. use [`boringcache/one@v1`](https://github.com/boringcache/one) in GitHub Actions +4. use the verified, immutable [`boringcache/one`](https://github.com/boringcache/one) release commit in GitHub Actions That keeps CI and local runs on the same workspace, entries, and cache profiles. Example: ```yaml -- uses: boringcache/one@v1 +- uses: boringcache/one@b55458ec8a4165e3fd70b1a1645f518a2095ed02 # v1.13.99 with: workspace: my-org/my-project cache-profiles: bundle-install @@ -21,10 +21,10 @@ Example: BORINGCACHE_SAVE_TOKEN: ${{ secrets.BORINGCACHE_SAVE_TOKEN }} ``` -For proxy-backed modes, `boringcache/one@v1` also accepts first-class `metadata-hints` so sessions and misses stay grouped by stable labels instead of per-run noise: +For proxy-backed modes, `boringcache/one` also accepts first-class `metadata-hints` so sessions and misses stay grouped by stable labels instead of per-run noise: ```yaml -- uses: boringcache/one@v1 +- uses: boringcache/one@b55458ec8a4165e3fd70b1a1645f518a2095ed02 # v1.13.99 with: mode: bazel workspace: my-org/my-project @@ -39,7 +39,7 @@ For proxy-backed modes, `boringcache/one@v1` also accepts first-class `metadata- Keep those hints low-cardinality. Good values are `project=web`, `benchmark=grpc-bazel`, `tool=gradle`, `lane=ci`, or `workflow=build`. Avoid commit SHAs, run ids, timestamps, and cold/warm labels for normal sessions; BoringCache classifies new and recurring misses from cache target and lifecycle data. -If the repo already defines `[proxy]` or adapter `metadata-hints` in `.boringcache.toml`, `boringcache/one@v1` inherits them through the CLI dry-run plan. Prefer repo config for durable defaults and use the action input only when the workflow needs an explicit override. +If the repo already defines `[proxy]` or adapter `metadata-hints` in `.boringcache.toml`, `boringcache/one` inherits them through the CLI dry-run plan. Prefer repo config for durable defaults and use the action input only when the workflow needs an explicit override. The canonical repo-config starting points in [Tool guides](tool-guides.md) are meant to be shared between local CLI runs and GitHub Actions for exactly this reason. @@ -81,7 +81,7 @@ You can still override a configured adapter from the workflow when needed: BORINGCACHE_SAVE_TOKEN: ${{ secrets.BORINGCACHE_SAVE_TOKEN }} ``` -Use `boringcache/one@v1` when you want the action to keep owning tool setup such as Bazel rc files, Maven or Gradle cache config, buildx setup, or container networking. +Use `boringcache/one` when you want the action to keep owning tool setup such as Bazel rc files, Maven or Gradle cache config, buildx setup, or container networking. Keep the full commit pin and update it deliberately after verifying a newer public release. When you run `boringcache docker` directly in GitHub Actions, the CLI derives the same branch/default/PR human tags from GitHub metadata that archive and proxy flows use. The action path passes provider-neutral metadata so Docker cache artifacts report the resolved human import/export tags and CI context. For Docker and BuildKit registry caches on pull requests, restore-only is the default. diff --git a/docs/quick-start.md b/docs/quick-start.md index b9b655c9..9d539743 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -52,5 +52,7 @@ boringcache docker The next docs to read are usually [Adapter commands](adapter-commands.md) and [Tool guides](tool-guides.md). -If the repo uses GitHub Actions, the next step is usually [`boringcache/one@v1`](https://github.com/boringcache/one). +If the repo uses GitHub Actions, the next step is usually the immutable +[`boringcache/one`](https://github.com/boringcache/one) v1.13.99 distribution +commit `b55458ec8a4165e3fd70b1a1645f518a2095ed02`. See [GitHub Actions](github-actions.md). diff --git a/docs/tool-guides.md b/docs/tool-guides.md index 41b16319..46c4033f 100644 --- a/docs/tool-guides.md +++ b/docs/tool-guides.md @@ -14,7 +14,7 @@ The pattern is simple: `cache-registry` itself is warm by default. Use `--on-demand` only for advanced shared-proxy setups that prefer immediate startup over warmed first reads. The snippets below are intended to be copy-pasteable `.boringcache.toml` -starting points. They work for local CLI runs and for `boringcache/one@v1` +starting points. They work for local CLI runs and for `boringcache/one` because the action asks the CLI for the same repo plan. Shared defaults for the examples: diff --git a/install-web/index.html b/install-web/index.html index 4a341e5f..8422cd6c 100644 --- a/install-web/index.html +++ b/install-web/index.html @@ -188,7 +188,7 @@

🎯 Next Steps

  • Move into your repo: cd your-project
  • Connect the CLI: boringcache onboard
  • Wrap one repeated step: boringcache run -- bundle install
  • -
  • Wire CI when ready: boringcache/one@v1
  • +
  • Wire CI when ready: boringcache/one@b55458ec8a4165e3fd70b1a1645f518a2095ed02 (v1.13.99)
  • 📖 Documentation: Visit boringcache.com/docs for setup paths and CLI docs.

    diff --git a/install-web/install.sh b/install-web/install.sh index 3247cf1b..b7e04e6d 100644 --- a/install-web/install.sh +++ b/install-web/install.sh @@ -1,6 +1,7 @@ #!/bin/sh # BoringCache CLI Installation Script # Usage: curl -sSL -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://install.boringcache.com/install.sh | sh +# Strict: curl -sSL https://install.boringcache.com/install.sh | BORINGCACHE_VERIFY_SIGNATURE=1 sh set -e @@ -14,6 +15,9 @@ NC='\033[0m' # No Color # GitHub repository REPO="boringcache/cli" BINARY_NAME="boringcache" +CHECKSUM_CERTIFICATE_IDENTITY_REGEXP='^https://github\.com/boringcache/monorepo/\.github/workflows/(cli-release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+|cli-release-checksums\.yml@refs/heads/main)$' +CHECKSUM_CERTIFICATE_OIDC_ISSUER='https://token.actions.githubusercontent.com' +VERIFY_CHECKSUM_SIGNATURE=0 # Function to print colored output print_status() { @@ -110,33 +114,52 @@ verify_checksum() { fi } +prepare_checksum_signature_verification() { + VERIFY_CHECKSUM_SIGNATURE=0 + + case "${BORINGCACHE_VERIFY_SIGNATURE:-auto}" in + auto) + if command -v cosign >/dev/null 2>&1; then + VERIFY_CHECKSUM_SIGNATURE=1 + print_status "cosign found; release signature verification is enabled." + else + print_warning "cosign not found; continuing with SHA-256 checksum verification." + print_warning "Install cosign and set BORINGCACHE_VERIFY_SIGNATURE=1 for fail-closed signature verification." + fi + ;; + 1|true|required) + if ! command -v cosign >/dev/null 2>&1; then + print_error "BORINGCACHE_VERIFY_SIGNATURE=1 requires cosign in PATH." + return 1 + fi + VERIFY_CHECKSUM_SIGNATURE=1 + ;; + 0|false|off) + ;; + *) + print_error "BORINGCACHE_VERIFY_SIGNATURE must be auto, 1, or 0." + return 1 + ;; + esac +} + verify_checksum_signature() { local temp_dir="$1" - if [ "${BORINGCACHE_VERIFY_SIGNATURE:-0}" != "1" ]; then + if [ "${VERIFY_CHECKSUM_SIGNATURE}" != "1" ]; then return 0 fi - if ! command -v cosign >/dev/null 2>&1; then - print_error "BORINGCACHE_VERIFY_SIGNATURE=1 requires cosign in PATH." - print_error "Install cosign or unset BORINGCACHE_VERIFY_SIGNATURE to use checksum-only verification." - exit 1 + if [ ! -s "${temp_dir}/SHA256SUMS.bundle" ]; then + print_error "Signed checksum bundle is missing or empty." + return 1 fi - if [ -f "${temp_dir}/SHA256SUMS.bundle" ]; then - cosign verify-blob \ - --bundle "${temp_dir}/SHA256SUMS.bundle" \ - --certificate-identity-regexp '^https://github.com/boringcache/.+/.github/workflows/.+@refs/(heads/main|tags/v.+)$' \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - "${temp_dir}/SHA256SUMS" >/dev/null - else - cosign verify-blob \ - --certificate "${temp_dir}/SHA256SUMS.pem" \ - --signature "${temp_dir}/SHA256SUMS.sig" \ - --certificate-identity-regexp '^https://github.com/boringcache/.+/.github/workflows/.+@refs/(heads/main|tags/v.+)$' \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - "${temp_dir}/SHA256SUMS" >/dev/null - fi + cosign verify-blob \ + --bundle "${temp_dir}/SHA256SUMS.bundle" \ + --certificate-identity-regexp "${CHECKSUM_CERTIFICATE_IDENTITY_REGEXP}" \ + --certificate-oidc-issuer "${CHECKSUM_CERTIFICATE_OIDC_ISSUER}" \ + "${temp_dir}/SHA256SUMS" >/dev/null } # Function to download and install binary @@ -187,14 +210,20 @@ install_binary() { # Create temporary directory local temp_dir=$(mktemp -d) local temp_file="${temp_dir}/${binary_name}" + + if ! prepare_checksum_signature_verification; then + rm -rf "${temp_dir}" + exit 1 + fi download_file "${download_url}" "${temp_file}" download_file "${release_url}/SHA256SUMS" "${temp_dir}/SHA256SUMS" - if [ "${BORINGCACHE_VERIFY_SIGNATURE:-0}" = "1" ]; then + if [ "${VERIFY_CHECKSUM_SIGNATURE}" = "1" ]; then if ! download_file "${release_url}/SHA256SUMS.bundle" "${temp_dir}/SHA256SUMS.bundle"; then rm -f "${temp_dir}/SHA256SUMS.bundle" - download_file "${release_url}/SHA256SUMS.sig" "${temp_dir}/SHA256SUMS.sig" - download_file "${release_url}/SHA256SUMS.pem" "${temp_dir}/SHA256SUMS.pem" + print_error "Signed checksum bundle is unavailable for ${version}." + rm -rf "${temp_dir}" + exit 1 fi fi @@ -205,13 +234,13 @@ install_binary() { exit 1 fi - if ! verify_checksum "${temp_dir}" "${binary_name}"; then - print_error "Checksum verification failed for ${binary_name}" + if ! verify_checksum_signature "${temp_dir}"; then + print_error "Checksum signature verification failed" exit 1 fi - if ! verify_checksum_signature "${temp_dir}"; then - print_error "Checksum signature verification failed" + if ! verify_checksum "${temp_dir}" "${binary_name}"; then + print_error "Checksum verification failed for ${binary_name}" exit 1 fi @@ -329,5 +358,6 @@ main() { print_status "📖 Docs: https://boringcache.com/docs" } -# Run main function -main "$@" +if [ "${BORINGCACHE_INSTALLER_SOURCE_ONLY:-0}" != "1" ]; then + main "$@" +fi diff --git a/install.sh b/install.sh index 65b5ac05..ae42a9ff 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,7 @@ #!/bin/sh # BoringCache CLI Installation Script # Usage: curl -sSL -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://install.boringcache.com/install.sh | sh +# Strict: curl -sSL https://install.boringcache.com/install.sh | BORINGCACHE_VERIFY_SIGNATURE=1 sh set -e @@ -14,6 +15,9 @@ NC='\033[0m' # No Color # GitHub repository REPO="boringcache/cli" BINARY_NAME="boringcache" +CHECKSUM_CERTIFICATE_IDENTITY_REGEXP='^https://github\.com/boringcache/monorepo/\.github/workflows/(cli-release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+|cli-release-checksums\.yml@refs/heads/main)$' +CHECKSUM_CERTIFICATE_OIDC_ISSUER='https://token.actions.githubusercontent.com' +VERIFY_CHECKSUM_SIGNATURE=0 # Function to print colored output print_status() { @@ -111,33 +115,52 @@ verify_checksum() { fi } +prepare_checksum_signature_verification() { + VERIFY_CHECKSUM_SIGNATURE=0 + + case "${BORINGCACHE_VERIFY_SIGNATURE:-auto}" in + auto) + if command -v cosign >/dev/null 2>&1; then + VERIFY_CHECKSUM_SIGNATURE=1 + print_status "cosign found; release signature verification is enabled." + else + print_warning "cosign not found; continuing with SHA-256 checksum verification." + print_warning "Install cosign and set BORINGCACHE_VERIFY_SIGNATURE=1 for fail-closed signature verification." + fi + ;; + 1|true|required) + if ! command -v cosign >/dev/null 2>&1; then + print_error "BORINGCACHE_VERIFY_SIGNATURE=1 requires cosign in PATH." + return 1 + fi + VERIFY_CHECKSUM_SIGNATURE=1 + ;; + 0|false|off) + ;; + *) + print_error "BORINGCACHE_VERIFY_SIGNATURE must be auto, 1, or 0." + return 1 + ;; + esac +} + verify_checksum_signature() { local temp_dir="$1" - if [ "${BORINGCACHE_VERIFY_SIGNATURE:-0}" != "1" ]; then + if [ "${VERIFY_CHECKSUM_SIGNATURE}" != "1" ]; then return 0 fi - if ! command -v cosign >/dev/null 2>&1; then - print_error "BORINGCACHE_VERIFY_SIGNATURE=1 requires cosign in PATH." - print_error "Install cosign or unset BORINGCACHE_VERIFY_SIGNATURE to use checksum-only verification." - exit 1 + if [ ! -s "${temp_dir}/SHA256SUMS.bundle" ]; then + print_error "Signed checksum bundle is missing or empty." + return 1 fi - if [ -f "${temp_dir}/SHA256SUMS.bundle" ]; then - cosign verify-blob \ - --bundle "${temp_dir}/SHA256SUMS.bundle" \ - --certificate-identity-regexp '^https://github.com/boringcache/.+/.github/workflows/.+@refs/(heads/main|tags/v.+)$' \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - "${temp_dir}/SHA256SUMS" >/dev/null - else - cosign verify-blob \ - --certificate "${temp_dir}/SHA256SUMS.pem" \ - --signature "${temp_dir}/SHA256SUMS.sig" \ - --certificate-identity-regexp '^https://github.com/boringcache/.+/.github/workflows/.+@refs/(heads/main|tags/v.+)$' \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - "${temp_dir}/SHA256SUMS" >/dev/null - fi + cosign verify-blob \ + --bundle "${temp_dir}/SHA256SUMS.bundle" \ + --certificate-identity-regexp "${CHECKSUM_CERTIFICATE_IDENTITY_REGEXP}" \ + --certificate-oidc-issuer "${CHECKSUM_CERTIFICATE_OIDC_ISSUER}" \ + "${temp_dir}/SHA256SUMS" >/dev/null } # Function to download and install binary @@ -188,14 +211,20 @@ install_binary() { # Create temporary directory local temp_dir=$(mktemp -d) local temp_file="${temp_dir}/${binary_name}" + + if ! prepare_checksum_signature_verification; then + rm -rf "${temp_dir}" + exit 1 + fi download_file "${download_url}" "${temp_file}" download_file "${release_url}/SHA256SUMS" "${temp_dir}/SHA256SUMS" - if [ "${BORINGCACHE_VERIFY_SIGNATURE:-0}" = "1" ]; then + if [ "${VERIFY_CHECKSUM_SIGNATURE}" = "1" ]; then if ! download_file "${release_url}/SHA256SUMS.bundle" "${temp_dir}/SHA256SUMS.bundle"; then rm -f "${temp_dir}/SHA256SUMS.bundle" - download_file "${release_url}/SHA256SUMS.sig" "${temp_dir}/SHA256SUMS.sig" - download_file "${release_url}/SHA256SUMS.pem" "${temp_dir}/SHA256SUMS.pem" + print_error "Signed checksum bundle is unavailable for ${version}." + rm -rf "${temp_dir}" + exit 1 fi fi @@ -206,13 +235,13 @@ install_binary() { exit 1 fi - if ! verify_checksum "${temp_dir}" "${binary_name}"; then - print_error "Checksum verification failed for ${binary_name}" + if ! verify_checksum_signature "${temp_dir}"; then + print_error "Checksum signature verification failed" exit 1 fi - if ! verify_checksum_signature "${temp_dir}"; then - print_error "Checksum signature verification failed" + if ! verify_checksum "${temp_dir}" "${binary_name}"; then + print_error "Checksum verification failed for ${binary_name}" exit 1 fi @@ -323,5 +352,6 @@ main() { print_status " ${BINARY_NAME} onboard" } -# Run main function -main "$@" +if [ "${BORINGCACHE_INSTALLER_SOURCE_ONLY:-0}" != "1" ]; then + main "$@" +fi From ab6032cf4da39dbf886d70fb96a810dbb52b2f74 Mon Sep 17 00:00:00 2001 From: Gaurav Tiwari Date: Wed, 22 Jul 2026 21:24:34 +0100 Subject: [PATCH 2/2] Verify the public CLI distribution --- .github/dependabot.yml | 11 ++++ .github/workflows/verify.yml | 42 ++++++++++++ scripts/test-installer-trust.sh | 110 ++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/verify.yml create mode 100755 scripts/test-installer-trust.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2664eb0d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 00000000..6f5eb84d --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,42 @@ +name: Verify Distribution + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: verify-distribution-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Verify distribution + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + steps: + - name: Checkout distribution + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check installer syntax + run: | + sh -n install.sh + sh -n install-web/install.sh + sh -n scripts/test-installer-trust.sh + + - name: Test installer trust contract + run: scripts/test-installer-trust.sh + + - name: Check public trust files + run: | + test -s .github/SECURITY.md + if grep -R -n --exclude-dir=.git 'boringcache/one@v1' README.md INSTALLATION.md docs install-web; then + echo 'Mutable boringcache/one references are not allowed in public guidance.' >&2 + exit 1 + fi diff --git a/scripts/test-installer-trust.sh b/scripts/test-installer-trust.sh new file mode 100755 index 00000000..26ee8fa2 --- /dev/null +++ b/scripts/test-installer-trust.sh @@ -0,0 +1,110 @@ +#!/bin/sh + +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +CLI_ROOT=$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd) +TEST_ROOT=$(mktemp -d) +ORIGINAL_PATH=${PATH} +EXPECTED_IDENTITY_REGEXP='^https://github\.com/boringcache/monorepo/\.github/workflows/(cli-release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+|cli-release-checksums\.yml@refs/heads/main)$' + +cleanup() { + rm -rf "${TEST_ROOT}" +} + +fail() { + printf 'installer trust test failed: %s\n' "$1" >&2 + exit 1 +} + +write_checksum() { + directory="$1" + binary_name="$2" + + if command -v sha256sum >/dev/null 2>&1; then + (cd "${directory}" && sha256sum "${binary_name}" > SHA256SUMS) + else + checksum=$(shasum -a 256 "${directory}/${binary_name}" | awk '{print $1}') + printf '%s %s\n' "${checksum}" "${binary_name}" > "${directory}/SHA256SUMS" + fi +} + +test_installer() { + installer="$1" + fixture_name="$2" + fixture_dir="${TEST_ROOT}/${fixture_name}" + fake_bin="${fixture_dir}/bin" + cosign_args="${fixture_dir}/cosign-args" + binary_name="boringcache-linux-amd64" + + mkdir -p "${fake_bin}" + BORINGCACHE_INSTALLER_SOURCE_ONLY=1 + export BORINGCACHE_INSTALLER_SOURCE_ONLY + # shellcheck source=/dev/null + . "${installer}" + + [ "${CHECKSUM_CERTIFICATE_IDENTITY_REGEXP}" = "${EXPECTED_IDENTITY_REGEXP}" ] || + fail "${fixture_name} trusts an unexpected workflow identity" + + printf 'release binary\n' > "${fixture_dir}/${binary_name}" + write_checksum "${fixture_dir}" "${binary_name}" + verify_checksum "${fixture_dir}" "${binary_name}" || + fail "${fixture_name} rejected a matching checksum" + + printf 'tampered release binary\n' > "${fixture_dir}/${binary_name}" + if verify_checksum "${fixture_dir}" "${binary_name}" >/dev/null 2>&1; then + fail "${fixture_name} accepted a checksum mismatch" + fi + + printf '404: Not Found\n' > "${fixture_dir}/${binary_name}" + if verify_checksum "${fixture_dir}" "${binary_name}" >/dev/null 2>&1; then + fail "${fixture_name} accepted an HTTP error body as a release binary" + fi + + VERIFY_CHECKSUM_SIGNATURE=1 + rm -f "${fixture_dir}/SHA256SUMS.bundle" + if verify_checksum_signature "${fixture_dir}" >/dev/null 2>&1; then + fail "${fixture_name} accepted a missing signature bundle" + fi + + if ( + PATH=/usr/bin:/bin + BORINGCACHE_VERIFY_SIGNATURE=1 + VERIFY_CHECKSUM_SIGNATURE=0 + prepare_checksum_signature_verification >/dev/null 2>&1 + ); then + fail "${fixture_name} allowed strict verification without cosign" + fi + + printf '%s\n' '#!/bin/sh' 'printf "%s\n" "$@" > "${COSIGN_ARGS_FILE}"' > "${fake_bin}/cosign" + chmod +x "${fake_bin}/cosign" + PATH="${fake_bin}:${ORIGINAL_PATH}" + COSIGN_ARGS_FILE="${cosign_args}" + BORINGCACHE_VERIFY_SIGNATURE=1 + VERIFY_CHECKSUM_SIGNATURE=0 + export PATH COSIGN_ARGS_FILE BORINGCACHE_VERIFY_SIGNATURE VERIFY_CHECKSUM_SIGNATURE + + prepare_checksum_signature_verification || + fail "${fixture_name} did not enable strict verification with cosign present" + [ "${VERIFY_CHECKSUM_SIGNATURE}" = "1" ] || + fail "${fixture_name} did not record strict verification state" + + printf 'signed bundle fixture\n' > "${fixture_dir}/SHA256SUMS.bundle" + verify_checksum_signature "${fixture_dir}" || + fail "${fixture_name} rejected the strict signature fixture" + grep -Fx -- "${EXPECTED_IDENTITY_REGEXP}" "${cosign_args}" >/dev/null || + fail "${fixture_name} did not pass the exact signer allowlist to cosign" + grep -Fx -- "${CHECKSUM_CERTIFICATE_OIDC_ISSUER}" "${cosign_args}" >/dev/null || + fail "${fixture_name} did not pin the GitHub Actions OIDC issuer" + + PATH=${ORIGINAL_PATH} + unset COSIGN_ARGS_FILE BORINGCACHE_VERIFY_SIGNATURE + export PATH +} + +trap cleanup EXIT HUP INT TERM + +test_installer "${CLI_ROOT}/install.sh" "root-installer" +test_installer "${CLI_ROOT}/install-web/install.sh" "web-installer" + +printf 'installer trust tests passed\n'