feat(update): verify SHA-256 sidecar before self-replace #33

Merged
David merged 1 commit from feat/update-checksum-verify into main 2026-05-14 19:39:33 +02:00
Owner

Summary

Closes the last actionable item from the post-audit backlog: yt update now verifies a SHA-256 sidecar before replacing the running binary, and both Forgejo workflows emit that sidecar at publish time. This is supply-chain hardening on top of the existing TLS hop and the permission pre-check.

CI side

.forgejo/workflows/build-linux.yml and build-windows.yml each gain a Compute SHA-256 sidecar step right after the binary is extracted. The step runs sha256sum <binary> and saves the GNU-format result to <binary>.sha256. The publish step then uploads the sidecar to the same Generic Package path (/latest/<binary>.sha256, plus every tagged path) and deletes the old sidecar alongside the old binary on overwrite, so the pair never drifts.

CLI side

After yt update finishes downloading the binary it issues a second GET for <url>.sha256:

  • 200 with parseable digest, hashes match -> proceed to write + self-replace.
  • 200, hash mismatch (or body unparseable) -> hard error, refuse the swap. The bytes never touch disk because verification runs against the in-memory buffer.
  • 404 or any other non-2xx, or network failure -> log a tracing::warn! and continue. This keeps installs working against pre-checksum releases until every tag has a sidecar.

The sidecar parser accepts GNU <hash> <file>, BSD SHA256 (file) = <hash>, and a bare 64-char hex digest. Verification fires after the freshness short-circuit, so an already-up-to-date binary still skips both the download and the sidecar fetch. --force still bypasses freshness; it does NOT bypass checksum verification because a mismatch is a security signal, not user preference.

Nine new tests: parser cases (GNU, BSD, bare, rejected garbage), a known SHA-256 vector for "abc", and four wiremock round-trips (match-allows-install, mismatch-blocks-install, missing-sidecar-is-not-fatal, unparseable-sidecar-errors). The existing non_2xx_response_errors test now also asserts the error message points at the binary URL, not the sidecar URL.

Test plan

  • cargo fmt --all
  • cargo clippy --all-targets --all-features -- --deny warnings
  • cargo test --all-targets (202 passed; was 193)
  • Local Nu smoke-test of the workflow's sha256sum | save --raw + open --raw | decode utf-8 pattern
  • CI: after merge, confirm build-linux.yml publishes both yt-linux-x86_64 AND yt-linux-x86_64.sha256 under the Generic Package
  • CI: same for the Windows workflow
  • Manual: yt update against a tag that now has a sidecar verifies and installs cleanly
  • Manual: tampering with the published sidecar (or the binary) produces "checksum mismatch ... Refusing to install" and leaves the running binary intact
## Summary Closes the last actionable item from the post-audit backlog: `yt update` now verifies a SHA-256 sidecar before replacing the running binary, and both Forgejo workflows emit that sidecar at publish time. This is supply-chain hardening on top of the existing TLS hop and the permission pre-check. ### CI side `.forgejo/workflows/build-linux.yml` and `build-windows.yml` each gain a `Compute SHA-256 sidecar` step right after the binary is extracted. The step runs `sha256sum <binary>` and saves the GNU-format result to `<binary>.sha256`. The publish step then uploads the sidecar to the same Generic Package path (`/latest/<binary>.sha256`, plus every tagged path) and deletes the old sidecar alongside the old binary on overwrite, so the pair never drifts. ### CLI side After `yt update` finishes downloading the binary it issues a second GET for `<url>.sha256`: - **200 with parseable digest, hashes match** -> proceed to write + self-replace. - **200, hash mismatch (or body unparseable)** -> hard error, refuse the swap. The bytes never touch disk because verification runs against the in-memory buffer. - **404 or any other non-2xx, or network failure** -> log a `tracing::warn!` and continue. This keeps installs working against pre-checksum releases until every tag has a sidecar. The sidecar parser accepts GNU `<hash> <file>`, BSD `SHA256 (file) = <hash>`, and a bare 64-char hex digest. Verification fires after the freshness short-circuit, so an already-up-to-date binary still skips both the download and the sidecar fetch. `--force` still bypasses freshness; it does NOT bypass checksum verification because a mismatch is a security signal, not user preference. Nine new tests: parser cases (GNU, BSD, bare, rejected garbage), a known SHA-256 vector for `"abc"`, and four wiremock round-trips (match-allows-install, mismatch-blocks-install, missing-sidecar-is-not-fatal, unparseable-sidecar-errors). The existing `non_2xx_response_errors` test now also asserts the error message points at the binary URL, not the sidecar URL. ## Test plan - [x] `cargo fmt --all` - [x] `cargo clippy --all-targets --all-features -- --deny warnings` - [x] `cargo test --all-targets` (202 passed; was 193) - [x] Local Nu smoke-test of the workflow's `sha256sum | save --raw` + `open --raw | decode utf-8` pattern - [ ] CI: after merge, confirm `build-linux.yml` publishes both `yt-linux-x86_64` AND `yt-linux-x86_64.sha256` under the Generic Package - [ ] CI: same for the Windows workflow - [ ] Manual: `yt update` against a tag that now has a sidecar verifies and installs cleanly - [ ] Manual: tampering with the published sidecar (or the binary) produces "checksum mismatch ... Refusing to install" and leaves the running binary intact
feat(update): verify SHA-256 sidecar before self-replace
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 17s
Create release / Create release from merged PR (pull_request) Has been skipped
099f25c8ed
CI side: both `build-linux.yml` and `build-windows.yml` now run `sha256sum` after producing the binary and publish the resulting `<binary>.sha256` to the same Generic Package path next to the artifact. The sidecar is deleted on overwrite alongside the binary so they stay paired.

CLI side: after `yt update` downloads the binary it issues a second GET for `<url>.sha256`. A 200 with a parseable digest triggers a verification against the downloaded bytes; on mismatch the command refuses the swap with a hard error and the user keeps their existing binary. The sidecar parser accepts GNU `sha256sum` output (`<hash>  <file>`), BSD `SHA256 (file) = <hash>`, and a bare 64-char hex digest. A 404 (or any other non-2xx, or a network failure) logs a tracing warning and continues so older releases without a sidecar still install. The verify step runs after the freshness short-circuit and before writing to disk, so mismatched bytes never touch the filesystem.

Nine new wiremock and unit tests lock the parser surface, the SHA-256 vector for "abc", the match / mismatch / 404 / non-digest sidecar cases, and the existing-error test now also asserts the failure message points at the binary URL (not the sidecar URL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
David merged commit b0b6de3b84 into main 2026-05-14 19:39:33 +02:00
David deleted branch feat/update-checksum-verify 2026-05-14 19:39:33 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/youtrack-cli!33
No description provided.