Commit 96c981b
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
49 | 53 | | |
| 54 | + | |
| 55 | + | |
50 | 56 | | |
51 | 57 | | |
52 | 58 | | |
53 | | - | |
| 59 | + | |
54 | 60 | | |
55 | 61 | | |
56 | 62 | | |
57 | 63 | | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
58 | 69 | | |
59 | | - | |
| 70 | + | |
60 | 71 | | |
61 | 72 | | |
62 | 73 | | |
| |||
0 commit comments