Repository navigation
Conversation
…led verification The gh path probed only `gh attestation --help`, but its tag-bound check also needs `gh attestation verify --signer-workflow` (gh 2.51.0) and `--source-ref` (gh 2.68.0), with gh's errors discarded. So a host with an older gh and no cosign got either "requires cosign or GitHub CLI (gh)" (gh < 2.49, although gh is installed) or "GitHub attestation verification FAILED" (gh 2.49-2.67), which reads like a tampered release. When cosign is absent and gh is present, ask gh's own help whether it has the attestation command and both flags (capability, not version parsing) and, if not, refuse naming what it lacks and the remedy (gh >= 2.68.0 or cosign). Still fail-closed; cosign is still preferred whenever present; a real verification failure still says FAILED; the cosign identity, the --source-ref value, checksum, license-bundle and sudo logic are unchanged. The tag guard matched line by line, so a BPFCOMPAT_VERSION carrying a valid tag on one line of a multi-line value passed it and was stopped only by curl. Reject any character a tag cannot hold before the regex, so the whole value is checked. install_test.sh: the gh shim can play gh < 2.49 / < 2.51 / < 2.68; new cases for each (and an unparsable version), for cosign still winning over an old gh, and for newline-bearing tags. Signed-off-by: ErenAri <erenari27@gmail.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ll is made Review follow-ups for the gh capability probe: - A gh that cannot run at all (every command fails, e.g. an unreadable ~/.config/gh/config.yml) was told it "lacks the 'gh attestation' command" and to upgrade. When `gh attestation --help` fails, the installer now also runs `gh --help`; if that fails too it refuses with "installed gh could not run 'gh --help' (<gh's first error line>); fix gh or install cosign". Still fail-closed; nothing else changes. - Comment: `gh attestation` exists, hidden, from gh 2.47.0 (public from 2.49.0). Behaviour is unchanged: on 2.47/2.48 the flag probe already names --signer-workflow and --source-ref as missing. install_test.sh: removing or neutralising one gh verify call used to go unnoticed (only the whole gh block and the bundle call were guarded). The gh stub now records every verify call; the gh-path install cases assert that exactly the installed assets (CLI, validator, and the license bundle when listed) were verified, each with the release workflow and --source-ref refs/tags/<tag>, and the cosign cases that gh verified nothing. New cases swap only the CLI, or only the validator, for bytes attested by another tag's run (SHA256SUMS regenerated to match), so each call must refuse on its own. Also: a gh that cannot run, with and without cosign, and a root `--help` in the stub as real gh has. Mutation check (scratch copies of install.sh): replacing a verify call with `true`, turning its `die` into a warning, or dropping its --source-ref, for each of the three calls, fails this suite (9/9); the previous suite caught 2/9. Signed-off-by: ErenAri <erenari27@gmail.com>
…fication On the gh path every `gh attestation verify` failure was reported as "GitHub attestation verification FAILED for <asset>", with gh's output sent to /dev/null. A capable gh that is not logged in, holds a rejected token, or cannot reach the GitHub API got the same tampering-like words as a release whose attestation does not match (review S2). The installer now keeps four outcomes apart, and every one still refuses (exit 1, nothing installed, temp dir removed, no checksum-only fallback, no retry): 1. gh lacks the attestation command or flags: the existing help probe, "installed gh <ver> lacks ...", unchanged. 2. No usable credentials: "GitHub authentication needed for attestation verification: gh is not logged in (<gh's first line>)", or "GitHub rejected gh's credentials (HTTP 401)". 3. GitHub API out of reach: "GitHub API unavailable for attestation verification: no HTTP response | HTTP 5xx | HTTP 429, rate limited | HTTP 403, rate limited (<gh's first line>)". 4. Anything else, including no attestation for the digest (404), another signer workflow or source ref, or a digest mismatch: the existing "GitHub attestation verification FAILED for <asset>", unchanged. Classes 2 and 3 rest only on positive evidence from one read-only request made before the first verify call: `gh api --method GET --include repos/<repo>/attestations/sha256:<CLI digest>?per_page=1`, the endpoint that call queries, through the same gh, host, token and proxy. gh exits 4 only from its root auth check (internal/ghcmd/cmd.go, pkg/cmd/root/root.go at v2.68.0 and v2.102.0), which also guards `attestation verify` (only --bundle lifts it). With --include the first stdout line is the status line gh builds from the response, and a transport failure exits 1 with empty stdout. A 403 counts only with GitHub's X-Ratelimit-Remaining: 0 or Retry-After header. Any other answer leaves the decision to the verify calls, unchanged (same assets, --repo, --signer-workflow, --source-ref refs/tags/<tag>), whose failures other than exit code 4 always read "verification FAILED". gh's verify text is never parsed: with stdout not a terminal it prints only its final error, worded differently by 2.68 and 2.102 for the same 401 or 404, and on 2.102 a dead proxy fails before the API fetch with a Sigstore message. Probing before verifying, not after a failure, means a refusal can never be relabelled as an auth or network problem when the verify itself spends a nearly exhausted rate limit or a token expires in between. Cost: one extra GET on the gh path (about 240 KB for v0.3.7's CLI, next to about 22 MB of downloads). The cosign path is unchanged and still runs no gh command. gh's words are quoted as one line with control characters removed, also in the "could not run 'gh --help'" refusal (review R2-N2), and the gh_err and gh_ver captures tolerate an imported pipefail (R2-N1). install_test.sh: the gh stub answers the probe from the attestation DB and can play a gh that is not logged in (exit 4), holds a rejected token (401), gets no response, a 503, a 429, a rate-limited 403 (also with coloured headers) or a plain 403, a verify that exits 4, and a refusing verify whose stderr claims a network or auth error. 35 new cases check the class named, the absence of the other classes' wording, nothing installed, the installer's temp dir removed, no control bytes, exactly one probe made before any verify call, the cosign path untouched in every mode, and imported pipefail. The 45 existing cases pass unchanged; against f8809a9's install.sh the new suite fails 24 cases. All 34 mutants of the change are caught (a refusal reworded as network or auth, a die removed on the auth or network path, a 404 taken for an outage, the probe ignored or removed, S1 regressions on the new calls). Real gh 2.68.0 and 2.102.0 (official tarballs) with the real v0.3.7 assets: no token and an invalid token give class 2, a dead HTTPS proxy class 3, a modified CLI or rc.4's assets served as v0.3.7 class 4, and the real release installs. f8809a9 said "FAILED" for the first three. Signed-off-by: ErenAri <erenari27@gmail.com>
The RC canary installs through the gh path, where one transient 5xx or dropped connection at the read-only API probe ended the install, although gh's own verify retries a 5xx. The probe now makes up to three attempts, 3 s apart (6 s of waiting at most), for a 5xx or no HTTP response only, and the last attempt decides. A 429 or rate-limited 403 is not retried: GitHub asks the client to wait longer than an installer should, and asking again sooner spends more of the limit. 401, 404 and 200 are answers. Every outcome still refuses or verifies as before. Text quoted from gh is reduced to printable ASCII by an allow-list (octal, because busybox tr has no [:print:] class), so C1 controls such as a raw or UTF-8 encoded CSI no longer pass. Tests: retry sequences (5xx then 200; 502, 503, 200; no response then 200; 5xx then 401, 429 or 404; no response then a rate-limited 403 or a refusal) with exact attempt counts and waits; C1 bytes in both quoting places; and the defensive branches no case pinned: a probe exit other than 1 or 4 with no output, exit 1 with output that is no status line, HTTP/1.1 status lines, and a verify exit other than 0, 1 or 4. Signed-off-by: ErenAri <erenari27@gmail.com>
On the gh path SHA256SUMS is unsigned, and gh attestation verify matches an artifact by digest only. So a party able to change release assets could delete the validator or license-bundle line (a CLI-only install with a false "not published" note), or serve one genuine asset of the tag under another asset's name, and the install still passed. The CLI's verify call now also prints the subjects of its verified provenance (--format json --jq, as "<sha256> <name>" lines), which name every asset the tag's release run attested. The installer refuses, as a failed verification naming the mismatch, when SHA256SUMS and those subjects disagree on whether the validator or the license bundle exists, or when a file to be installed is not attested under its own name. That call's stderr is still kept for the auth classification. The cosign path is unchanged. Tests: the gh stub prints the subjects of every statement that holds the verified digest. Refused: the validator line deleted (v0.3.7 and rc.5 shapes), the bundle line deleted, the arm64 CLI served as the amd64 CLI, the CLI served as the validator, an arm64 validator line added on an aarch64 host, and bytes from another statement of the run served as the validator or as the CLI; each also refused by cosign. Installed: a CLI-only install, an aarch64 host and a re-run release with two statements. Signed-off-by: ErenAri <erenari27@gmail.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft. Merge only on an exact-head operator decision. This changes the live installer:
scripts/install.shonmainis what everycurl … /main/scripts/install.sh | shuser runs. Every refusal stays fail-closed, and cosign is still preferred whenever it is installed.Final head:
69cebeae6791d3c8f016cdb0a10ed3b68bce0778(5 commits onmain402d7c9). All five commits are pushed (723b5e2,6705bb1and69cebeaon 2026-10-19 after the earlierf8809a9); see the hosted-CI note above.Why
A LOCAL smoke of main's installer at
402d7c9against the real v0.3.7 and v0.4.0-rc.4 releases found:SHA256SUMSare refused.gh attestation; it is hidden in 2.47–2.48 and public from 2.49) and no cosign, every install is refused with "requires cosign or GitHub CLI (gh)", although gh is installed. Ubuntu noble's packaged gh is 2.45.0.gh attestationexists, but its verify call also needs--signer-workflow(gh ≥ 2.51.0) and--source-ref(gh ≥ 2.68.0). These minimums were checked in gh'sverify.goat those tags. On gh 2.47–2.67 the user saw "GitHub attestation verification FAILED", which reads like tampering. The 2.51–2.67 part is new since install.sh: bind signatures to the requested tag, validate the version, install atomically #236; rc.4's installer did not pass--source-ref.BPFCOMPAT_VERSIONmatched, so a value with an embedded newline got through to curl.Later reviews and triage added:
S2 (review of
f8809a9): a gh that is new enough but cannot complete the check still ended in "GitHub attestation verification FAILED", with gh's output sent to/dev/null: a gh that is not logged in, holds a rejected token, or cannot reach the GitHub API. Reproduced LOCAL with real gh 2.102.0 andf8809a9's installer.N7 (review of
723b5e2): the RC canary's clean-install job runs the candidate commit'sinstall.shon the gh path withgithub.token. After723b5e2, one transient 5xx or dropped connection at the new API probe failed that time-windowed job, although gh's own verify retries a 5xx.T4 / F4 (triage): on the gh path
SHA256SUMSis unsigned, andgh attestation verifymatches an artifact by digest only. A party able to change release assets (a TLS interceptor against a separately obtained installer, an asset-write credential against pinned-version users, or an honest bad re-upload) could, with every verify call passing:bpfcompat-linux-amd64;Reproduced LOCAL with real gh 2.68.0 and 2.102.0 and
723b5e2's installer: T4-S1, T4-S3 and T4-S4 all exit 0. The cosign path refuses all of them.Changes
cc36c70: name the missing gh capability (pushed)gh attestation --help,gh attestation verify --help) whether the command and both flags exist. No version parsing.installed gh 2.67.0 lacks --source-ref in 'gh attestation verify', needed for tag-bound attestation verification; upgrade to gh >= 2.68.0 or install cosign - refusing checksum-only installation.casenow runs before the existing regex, so any newline or other character a tag cannot contain is refused before any download.f8809a9: review follow-ups (S1, N1, N2) (pushed)~/.config/gh/config.yml) is reported asinstalled gh could not run 'gh --help' (<gh's first error line>); fix gh or install cosign - refusing checksum-only installation, not as lacking a command.gh attestationexists, hidden, from gh 2.47.0 (public from 2.49.0).--source-ref refs/tags/<tag>; new cases swap only the CLI, or only the validator, for another tag's attested bytes.723b5e2: separate auth and API failures from a refused verification (S2)On the gh path the installer reports four distinct outcomes. Every one refuses: exit 1, nothing installed, temp dir removed, no checksum-only fallback.
installed gh <ver> lacks …, orinstalled gh could not run 'gh --help' (…)GitHub authentication needed for attestation verification: gh is not logged in (<gh's first line>), or… GitHub rejected gh's credentials (HTTP 401) (…); thenrun 'gh auth login' or set a valid GH_TOKEN, or install cosign - refusing checksum-only installationGitHub API unavailable for attestation verification: no HTTP response | HTTP 5xx | HTTP 429, rate limited | HTTP 403, rate limited (<gh's first line>); check the network or proxy and https://www.githubstatus.com, then retry, or install cosign - refusing checksum-only installation69cebeaGitHub attestation verification FAILED for <asset>, followed for69cebea's checks by the mismatchgh api --method GET --include repos/<repo>/attestations/sha256:<CLI digest>?per_page=1. That is the endpoint the first verify queries, through the same gh, host, token and proxy.attestation verifyreads the asset or touches the network (only--bundle, never passed, lifts it).--include, the first stdout line is the status line gh builds from the response (HTTP/x NNN; HTTP/1.1 and HTTP/2.0 both parse).X-Ratelimit-Remaining: 0or aRetry-Afterheader.|| :so an importedpipefailcannot turn a refusal into a silent exit.6705bb1: retry the probe on a 5xx or no response only (N7); printable ASCII only (N2); pin the untested branches (N1)Retry-After,X-Ratelimit-Reset, typically a minute or more), longer than an installer should; asking again within seconds would only spend more of the limit (and of a secondary limit's patience).LC_ALL=C tr -cd '\040-\176'. C1 controls (a raw CSI byte0x9b, or its UTF-8 formc2 9b) no longer pass. The octal range is used rather than[:print:]because busyboxtr1.36.1 does not honour[:print:](observed:tr -cd '[:print:]'keeps only the[of a test line, while[:alnum:]works), which would blank gh's words on busybox systems such as Alpine.69cebea: bind gh-path installs to the release's attested names (T4 remedy R-B)--format json --jq '.[].verificationResult.statement.subject[] | "\(.digest.sha256) \(.name)"', to a private file in the installer's 0700 temp dir. These lines name, with digests, every asset the tag's release run attested in that statement. The call's stderr is still captured separately for the exit-4 classification.SHA256SUMSand those subjects disagree on whether the validator or the license bundle exists (it decides what is downloaded):GitHub attestation verification FAILED for bpfcompat-validator-static-linux-amd64: SHA256SUMS does not list it, but the v0.3.7 release provenance of bpfcompat-linux-amd64 does… FAILED for bpfcompat-validator-static-linux-arm64: SHA256SUMS lists it, but the v0.3.7 release provenance of bpfcompat-linux-arm64 does not<sha256> <name>pair among the subjects:GitHub attestation verification FAILED for bpfcompat-linux-amd64: the v0.3.7 release provenance of bpfcompat-linux-amd64 does not list these bytes under that name (it lists them as bpfcompat-linux-arm64)release-artifacts.ymlattests the CLIs and the validator (and, from rc.5, the bundle) in oneattest-build-provenancestep (release-artifacts.yml:254-268@402d7c9), andSHA256SUMSlists the validator and the bundle from a fixed list (:229-247). Re-run releases pass: membership is checked against the union of every verified statement holding the CLI's digest.Validation
All of this is LOCAL evidence on the writer's host (2026-10-19). Hosted CI for this head has not run (account billing lock; see the note at the top).
Stub suite (
scripts/install_test.sh):shshsh6705bb169cebeauname,sha256sum,tar) instead of the PATH shims: the 4 known cases (aarch64 mapping, unsupported arch, missing sha tools, license bundle without tar) plus, at69cebea, the 2 aarch64-host R-B cases. busybox's ownsleeplikewise bypasses the sleep shim, so under busybox the retry cases check the attempt counts but not the waits (the suite prints a note).723b5e2are all present and pass; their assertions are unchanged or strengthened (the outage cases now expect exactly three probe attempts and the waits between them; the unattested-bundle case now requires the bundle's own verify call to refuse it).69cebea's suite against723b5e2's script: 27 failures; against6705bb1's script: 13 failures, all R-B cases (the 12 new ones and the probe case's subjects request).6705bb1's suite against723b5e2's script: 14 failures. The 7 N1 cases that pass there pin behaviour that was already correct; their RED is the round-1 surviving mutants, now caught (below).pipefailrefusal. Every refusal asserts: nothing installed, temp dir removed, the exact class-4 line, no auth or network wording, every verify call made and passed, and only the CLI's call requesting its subjects.--format json --jq <that exact expression>with the<sha256> <name>subjects of every attested statement holding the verified digest (it refuses any other expression), and takes one probe attempt at a time from a sequence; more than 10 probe attempts in one run stop with exit 4, so an unbounded retry fails fast.Mutations (scratch copies of
install.sh, never committed; exact-text replacements with asserted match counts; each run under dash with ok + FAIL = 110, so every catch is a named assertion): 65 of 66 caught.--format/--jqdropped; retry unbounded; retry applied to 404; allow-list reverted totr -d '[:cntrl:]'; and 20 more (retry on 429 or rate-limited 403, retry dropped for 5xx or no response, 2 or 4 attempts, no wait or a 30 s wait, first answer kept, presence checked one way only or for one asset only, names checked for the CLI only or not for the CLI, the "lists them as" clause dropped, presence read fromSHA256SUMStwice, high bytes kept, stderr merged into the subjects, and[:print:]in place of the octal range).[:print:]instead of the octal range, is equivalent with GNUtr; under busybox it is caught (19 failures beyond the 6 harness-only ones).Real gh (LOCAL): official linux_amd64 tarballs of gh 2.68.0 and 2.102.0, checked against their releases' checksums files; isolated HOME, XDG dirs and
GH_CONFIG_DIR; no cosign on PATH; a sudo guard; the committedinstall.sh(69cebea, sha256e75646b8…c5e0) fed on stdin underenv -i; real release bytes served from a localfile://mirror so only gh used the network; the token taken from the host's existing login, passed only through the environment, never printed or stored.SHA256SUMS… (it lists them as bpfcompat-linux-arm64)… (it lists them as bpfcompat-linux-amd64)SHA256SUMS lists it, but …gh is not logged in (…)no HTTP response (…), 3 attemptsGitHub rejected gh's credentials (HTTP 401) (gh: Bad credentials (HTTP 401))723b5e2's installer with the same real gh exits 0 on T4-S1, T4-S3 and T4-S4 at both versions.--jqoutput shape: real--format jsonoutput at both versions is one result per verified statement withverificationResult.statement.subject[].{name,digest.sha256}; the--jqstdout is exactly the three v0.3.7 subject lines, byte-identical withCLICOLOR_FORCE=1and withGH_DEBUG=api. From source, at both versions: the exporter writes only the verified results (pkg/cmd/attestation/verify/verify.go:258-264@v2.68.0,:309-315@v2.102.0); sigstore-go v0.7.0 / v1.3.0 marshal the statement with protojson understatement; in-toto v1.1.1 / v1.2.0 name the fieldssubject,name,digest; and go-gh v2.11.2 / v2.16.1 print jq string results raw, never coloured.Also LOCAL:
69cebea): v0.3.7 installs with "cosign signature over SHA256SUMS verified", identity bound torefs/tags/v0.3.7; gh is never run.f8809a9; the capability check is unchanged since): with a valid config, the precise "lacks the 'gh attestation' command" refusal; with a malformedconfig.yml, "could not run 'gh --help' (…)". The API probe never runs on 2.45, because the capability check refuses first.-S warningand at default severity, and oninstall.shwith-s dash.install.shparses underdash -n,bash --posix -n,bash -n,busybox sh -nandbusybox ash -n.ci.yml's test and build jobs at69cebea: 40 steps ran, 0 failed; 1 skipped (go mod download, vendored offline replay).scripts/check-experimental-freeze.sh origin/main: PASS.Residuals (not changed here)
release-artifacts.yml:254-268@402d7c9). Splitting them later makes every gh-path install refuse (fail-closed). A comment or check there belongs in a later, non-frozen change.SHA256SUMSfor bundle presence and the validator pin (action.yml:207,:233@402d7c9) andverify-release-assets.shmatches by digest; README's manual prebuilt instructions also verify by digest only (README:386-408@402d7c9,:390-415@2269647). R-B covers neither.GH_HOSTset, or a single GHES host configured, the probe and the verify both go to that host. A reachable GHES answers 404, so the verify fails withErrUnsupportedHostand the install reads class 4 "FAILED"; or a bad GHES token gives class 2 without naming the host. This predates the change and is fail-closed, but it misdirects like S2 did. Option, not implemented because it is a behaviour choice: pinGH_HOST=github.comon the probe and the verify calls (it changes their environment, not their argv, and overrides a user's deliberateGH_HOST).Retry-AfterwhileX-Ratelimit-Remainingis non-zero.BPFCOMPAT_NO_VALIDATOR=1does not skip the comparison: aSHA256SUMSthat disagrees with the provenance about the validator is refused even when the validator is not wanted.docs/verifying-releases.mdstate the gh ≥ 2.68.0 minimum in docs: bind manual release verification to the exact tag and mark rc-only behaviour #267 (head2269647), not here.