Skip to content

Commit 96c981b

Browse files
basvandijkclaude
andauthored
fix(ci): verify the gh CLI download against a pinned sha256 (#11311)
## What The scheduled `update-mainnet-canister-revisions` job downloaded the `gh` CLI tarball from `github.com/cli/cli` release assets, untarred it, installed it into `/usr/local/bin` with `sudo` and ran it — trusting nothing but the URL. This pins the sha256 of the release asset in this repo and checks the download **before** unpacking or executing any of it, following the pattern already used for bazelisk in [`ci/container/Dockerfile`](https://github.com/dfinity/ic/blob/master/ci/container/Dockerfile#L34-L36). Companion to #11310, which does the same for `execlog2csv`. ## Why GitHub release assets are mutable under an existing tag — the API reports `"immutable": false` for the pinned `v2.74.0` — so anyone with release access to `cli/cli` got code execution inside this job. The blast radius is unusually direct: the job runs on a `cron: "0 */2 * * *"` schedule with the `PR_CREATION_BOT` app token exported as `GH_TOKEN`, and **the unverified binary is itself the consumer of that token** — [`ci/src/mainnet_revisions/mainnet_revisions.py`](https://github.com/dfinity/ic/blob/master/ci/src/mainnet_revisions/mainnet_revisions.py#L119-L142) runs `gh pr list / create / view / merge --auto` with it. A trojaned `gh` reads the PR-bot token straight out of its own environment and can create and auto-merge PRs into `dfinity/ic`. The hash comes from the release's `gh_2.98.0_checksums.txt` asset. Verifying against that asset *at runtime* would be worthless: it is another mutable asset in the same release, so whoever can swap the tarball can swap the sums file alongside it. The hash has to live here, in a reviewed tree. ## Also in this change - **`linux_386` → `linux_amd64`.** The job ran the 32-bit build on an x86_64 runner, relying on the kernel's IA-32 emulation for no reason. The asset name appears twice (download URL and the `mv` of the extracted directory); both are updated. - **2.74.0 → 2.98.0**, the latest release at time of writing. - **`set -eEuxo pipefail`**, matching the sibling step in the same file. GitHub's `shell: bash` default is already `-eo pipefail`, so the step fails on a mismatch regardless; the explicit `set` makes that independent of the default rather than inherited. ## Verification Ran the step's script, extracted verbatim from the YAML, against the live upstream release: - **Positive** — end-to-end: logs `gh_cli.tar.gz: OK`, extracts to `gh_2.98.0_linux_amd64/bin/gh`, installs, exit 0. The pinned digest also matches upstream's own `gh_2.98.0_checksums.txt`. - **Negative** — one flipped hex char → `gh_cli.tar.gz: FAILED`, `WARNING: 1 computed checksum did NOT match`, exit 1, and **zero** of `tar`/`mv`/`chmod`/`cp` executed, confirming the check precedes unpacking. - The 2.98.0 `linux_amd64` binary is **statically linked**, so it has no glibc dependency on the `ic-build` container it is copied into. On the version bump specifically: - `gh 2.98.0` still supports every invocation in `mainnet_revisions.py`: `pr list -H/--head -R/--repo`, `pr create --head --repo --body --title`, `pr view --json --jq/-q`, `pr merge --auto`. - Read all 35 release notes between 2.74.0 and 2.98.0 for breaking changes on those commands: none. The one adjacent change is v2.93.0 removing a `numberFieldOnly` optimisation, so `gh pr view --json number` now performs a real API validation call instead of a shortcut — same output, one extra round trip. CI exercises this on the PR itself: the workflow's `pull_request` `paths` filter covers this file, so the job runs here with `DRY_RUN=--dry-run` (no PR creation, no auto-merge). ## Scope / residual risk - A pin authenticates bytes, not intent — it protects the scheduled and protected-branch runs, where the tree is reviewed. - `v2.98.0` happens to be an **immutable** release upstream, which is defence in depth, but that is an upstream-controlled property that may not hold for a future bump — the in-repo hash is what makes each bump verifiable at review time. - This closes one of the seven sites in the underlying finding. Still open: `crane` in `container-mirror.yml`, the `internetcomputer.org/install.sh` pipe in `api-bn-recovery-test.yml`, `moc` in `ci/container/Dockerfile`, and the tag-pinned `namespacelabs/*` actions in `ci-rbe-evaluation.yml`. - Defence-in-depth follow-up: `cli/cli` publishes build-provenance attestations, so a future version of this step could add `gh attestation verify` — though bootstrapping that needs a trusted `gh` first, which is exactly what the hash pin provides. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a41da9e commit 96c981b

1 file changed

Lines changed: 14 additions & 3 deletions

File tree

.github/workflows/update-mainnet-canister-revisions.yaml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,29 @@ jobs:
4545

4646
- name: Setup gh cli
4747
env:
48-
GH_CLI_VERSION: 2.74.0
48+
GH_CLI_VERSION: 2.98.0
49+
# sha256 of gh_${GH_CLI_VERSION}_linux_amd64.tar.gz. When bumping the
50+
# version, take the new value from that release's
51+
# gh_${GH_CLI_VERSION}_checksums.txt asset.
52+
GH_CLI_SHA256: 3b8ac6b30336802fc1a858d7c084e11cdf24ac1a761ca90b68022d7d729208de
4953
run: |
54+
set -eEuxo pipefail
55+
5056
# Without --fail, 4xx and 5xx are considered "success", because UNIX.
5157
# If this keeps failing, consider applying other --retry-??? flags.
5258
curl \
53-
--location https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_386.tar.gz \
59+
--location https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_amd64.tar.gz \
5460
--output gh_cli.tar.gz \
5561
--fail \
5662
--retry 7
5763
64+
# Release assets are mutable under an existing tag, so the URL alone is
65+
# not a trust anchor: verify against the checksum pinned above before
66+
# unpacking or running anything.
67+
echo "${GH_CLI_SHA256} gh_cli.tar.gz" | sha256sum --check
68+
5869
tar -xvf gh_cli.tar.gz
59-
mv "gh_${GH_CLI_VERSION}_linux_386" gh_cli
70+
mv "gh_${GH_CLI_VERSION}_linux_amd64" gh_cli
6071
chmod +x ./gh_cli/bin/gh
6172
sudo cp ./gh_cli/bin/gh /usr/local/bin/
6273
rm -rf gh_cli*

0 commit comments

Comments
 (0)