fix: the checksum check proves the archive it downloaded was checked - #50
Merged
Merged
Conversation
An adversarial review found the one input that exited 0 with the download unverified, and it is the worst possible one: `shasum -c --ignore-missing` answers for the files the checksums file NAMES. Absent names are skipped; names that are present and irrelevant are verified and reported as a pass. A checksums file naming any readable file with known content — /dev/null will do — therefore passed, and the unverified archive went on to be unpacked, executed to read its version, and installed. Reproduced against this Mac's own /usr/bin/shasum. Nothing in the invocation asserted that the artefact it protects was in scope. The file is now narrowed to the archive's own line before shasum sees it, and --ignore-missing goes with the narrowing: one name, the file just downloaded, so a pass is a statement about that file. A line naming a PATH is refused rather than matched, because "/somewhere/Gropius.app.zip" would verify something never downloaded and make shasum print a success line that contains the archive's own — so the pass is read line by line, not as a substring. Narrowing the input is not making the verdict. The digest is still computed and compared by shasum, which stays the only thing in this product that decides whether bytes match. Red first: both new rows passed verification before the change. One existing row moved cause — a checksums file naming only the client bundle is now reported as not covering the archive, which is the thing that matters about it. Assisted-by: Claude:claude-opus-5 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo (cherry picked from commit bf01e58224c45dd293348d83524a6e7e558d04be)
The serving version was rendered straight from whatever holds the loopback port. The challenge that lets a responder in proves a shared DATA ROOT, not an identity — deliberately, at mode 0640, so a peer account can answer it under the shared-cache mode this product documents — so that string is a string a peer account chose. A newline in it forged report lines: the review predicted it and the test reproduced it, printing "The firewall grant was re-made for this build." as the report's own second line. Escape sequences and carriage returns reached the terminal too. It is checked where it becomes a fact rather than at the seam that read it, so every path into the report goes through one switch. What this cannot check is whether a plausible version is a true one: the report's provenance for that line is the control plane, and a report that asks can be told something false. The comment says so rather than implying more. Also: stagedVersion runs the downloaded binary inside the directory that was verified (the rule the step is written under, and cmd.Dir was never set), with an empty environment and a capped read — the timeout bounded how long a hostile build could take and said nothing about how much it could send. The control-plane read refuses redirects, because loopback is a property of the request and a redirect is how a request stops being the one that was made. The one quit warning that skipped redact now applies it, by the rule this repository adopted after the last leak. And one claim is corrected rather than hardened, which is what the record said to expect. "No environment variable can point the download" was false: -q suppresses .curlrc only, and curl still honours https_proxy and CURL_CA_BUNDLE — both measured. The claim is now about what Gropius reads, with the limit written down. Neutralising them was rejected: the bootstrap honours the same variables, so the verb would fail where the documented install works, and it buys nothing against another account on this Mac, which cannot set this account's environment. Assisted-by: Claude:claude-opus-5 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo (cherry picked from commit 4b5b00c0b0d527d98e78e63fc8f5880ef51ec7c9)
Four decisions: the scoped verification and why narrowing the input is not making the verdict; validating the control plane's version where it becomes a fact, and what validation cannot establish; the curl-environment claim corrected rather than hardened, with the reason neutralising was rejected; and the branch deletion and recovery. iss-2609120417422598 carries the finding that is NOT this branch's to fix: install.sh runs the same shasum invocation with the same missing scope assertion, and it is the command the README tells people to pipe into bash. The issue carries the measured reproduction and the three moves the Go verb took, and says plainly that a verb with "update" in its name has not closed it. Assisted-by: Claude:claude-opus-5 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo (cherry picked from commit 4cfba4a786740298fb4b0faa62e28bb231bb0dcf)
install.sh verifies the download with `shasum -c --ignore-missing`, which answers for the files the checksums file NAMES: absent names are skipped, and names that are present and irrelevant are verified and reported as a pass. Nothing asserts that the archive it protects was in scope (iss-2609120417422598). Five rows against the real script, a real /usr/bin/shasum and a real archive. Red before the fix, and red in the two ways that matter: a checksums file naming /dev/null with its own digest — the capture's own reproduction — exits 0 and goes on to unpack the archive it never looked at, and so does a line naming the archive by an absolute PATH, which verifies a file that was never downloaded and prints a success line CONTAINING the one the script looks for. A checksums file naming only the other bundle already failed, but as "corrupt or tampered", which sends a user to look at their network for a file that is intact and answers about something else. The two rows that must not move are here too: the honest bare-name line still passes through to the unpacking, and a wrong digest still fails as a mismatch. The pass is read as a LINE. The script's own CI-seam warning quotes the words "Checksum OK." in prose, so a substring match reported every refused run as a pass — which is the same shape of mistake as the defect under test. Assisted-by: Claude:claude-opus-5 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo
`shasum -c --ignore-missing SHA256SUMS.txt` answers for the files the checksums file NAMES. Absent names are skipped; names that are present and irrelevant are verified and reported as a pass. So a checksums file naming any other readable file with a correct digest — /dev/null did it — exited 0 with the download never looked at, and install.sh went on to print "Checksum OK.", unpack the archive, clear its quarantine and hand the binary inside it the install, as root, on the command the README tells people to pipe into bash (iss-2609120417422598). The same three moves the update verb took (d7e31ab, internal/lifecycle/ updatefetch.go), for the one asset each invocation downloads: - the checksums file is narrowed to the archive's OWN line before shasum sees it — an exact bare name, a 64-character hex digest, either separator; - --ignore-missing goes with the narrowing, because there is then exactly one name and it is the file just downloaded; - a line naming a PATH is refused rather than matched, and the pass is read as a LINE, because "/somewhere/Gropius.app.zip: OK" ends in the same characters as the line this script looks for. Narrowing the input is not making the verdict: the digest is still computed and compared by /usr/bin/shasum, which stays the only thing here that decides whether bytes match. An empty file, a page that is not a checksums file, and a file that answers about a different asset now fail with a message that says the checksums carry no line for this archive, instead of "corrupt or tampered". No new tool to name by absolute path: the scan is bash's own `read`, fed by a redirect from the file, so the standard input `curl … | bash` feeds the script from is untouched. TestTheInstallerVerifiesBeforeItHandsOver anchored on the flag that is gone; it now anchors on the invocation and refuses --ignore-missing coming back in any line that is not a comment. Assisted-by: Claude:claude-opus-5 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo
iss-2609120417422598 resolved against 9ecd1ad, with the decision line the bootstrap's half of the fix was missing: the same three moves as the Go verb, the scan written with bash's own `read` rather than a new PATH-resolved tool, and a refusal of its own for a checksums file that is intact and answers about something else. Assisted-by: Claude:claude-opus-5 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo
The changelog carries a Security entry for each of the two user-facing changes on this branch: the bootstrap and the update verb both proved only that shasum had checked something, not that it had checked the archive; and the update report printed the serving version as the control plane handed it back. Written under [Unreleased] on the same terms as the 0.5.0 entries. Assisted-by: Claude:claude-fable-5-1 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo
…ect refusal are held The security review of this branch found three hardening moves with no test that would go red without them: the staged build's version query runs with an empty environment and inside the verified directory, its answer is read only up to a cap, and the control-plane read stops at a redirect. Each now has a row that was watched to fail against the line removed: a build that echoes a variable from this process's environment, one that reports its working directory, one that pads its answer past the cap, and a server that answers 302 towards a second server that counts whether it was asked. The quit warning's comment claimed the redaction rule was applied everywhere; the install verb's warnings still print error text as it comes. The comment now says what this line does, and the install verb's five lines are captured as iss-2609120438396694. Assisted-by: Claude:claude-fable-5-1 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo
REPPL
enabled auto-merge
September 12, 2026 04:39
…of its listing In the merge queue, the tripwire that asserts install.sh changed nothing in the real /Applications reported a .doctor-*.tmp file that was there for one snapshot and not the other. That is the lifecycle package's writability probe: the test that proves the live-environment guard can be opened builds the live install environment, which asks whether this account can write /Applications by creating and removing that file, and go test runs the two packages side by side. It is not something the bootstrap writes, and the bundle fingerprints beside the listing are what catch an install, so the one name is left out of the comparison — exactly that name, held by a test that was watched to fail with the exemption widened to nothing. The race itself is recorded as iss-2609120444017291: a unit test still creates a file in the machine's /Applications. Assisted-by: Claude:claude-fable-5-1 Claude-Session: https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo
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.
One page that says what is on (itd-2609081718534201, spc-2609081750377336 closed → intent shipped).
What this adds
app.Advertises, read bycmd/gropiusat start and by the control plane.docs/posture-reference.md, pointers from the mesh-VPN, bind and statistics pages, the README feature line and the changelog entry.Reviews
Ruthless review and adversarial security review, each run twice; every finding either fixed or captured. Second-pass verdicts: APPROVE.
Gates
make test(race),gofmt -l .,go vet ./...,abcd docs lintgreen; branch scanned clean with gitleaks.https://claude.ai/code/session_01RooZgUESQnVnievtn48pHo