Skip to content

Verify release checksum before replacing lstk binary - #410

Open
skyrpex wants to merge 4 commits into
mainfrom
claude/lstk-release-integrity-verify-00adda
Open

Verify release checksum before replacing lstk binary#410
skyrpex wants to merge 4 commits into
mainfrom
claude/lstk-release-integrity-verify-00adda

Conversation

@skyrpex

@skyrpex skyrpex commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What

The self-updater's direct-binary path (lstk update when not installed via Homebrew/npm) downloaded the release archive from GitHub and replaced the running executable without any integrity check. It now verifies the archive's SHA-256 against the release's published checksums.txt before extraction:

  • New parseChecksums (internal/update/checksum.go) parses the goreleaser manifest (tolerates CRLF, blank lines, * binary-mode marker; rejects malformed lines with line numbers).
  • updateBinary is now a binaryUpdater method with injectable download base URL and executable resolver, enabling unit tests against httptest servers. It fetches checksums.txt first (fail fast, 1 MiB read cap), requires an entry for the platform asset, stream-hashes the archive during download (io.MultiWriter), and aborts before extractAndReplace on mismatch.
  • Hard fail on missing checksums.txt (404), missing asset entry, malformed manifest, or hash mismatch — warn-and-continue would make the control bypassable by deleting one release asset.
  • Spinner text is now "Downloading and verifying update".

Why

Flagged on Slack (see DEVX-999): the downloaded binary was trusted without verification. goreleaser already publishes a SHA-256 checksums.txt on every release, so this needs no release-pipeline changes. Homebrew and npm update paths are unchanged — those package managers verify integrity themselves.

Notes for reviewers

  • Error surfacing reuses ErrInternal per the error-code policy in internal/output/error_code.go; the message text carries the security signal ("may be corrupted or tampered with; update aborted"). A dedicated integrity code would be an additive follow-up.
  • Signed releases (cosign keyless: goreleaser signs: block, id-token: write in the release job, sigstore verification in the updater) are deliberately out of scope — worth a separate ticket.
  • Tests: parser table tests plus an end-to-end binaryUpdater matrix (happy path, mismatch leaves the binary untouched with no temp leftovers, 404, missing entry, malformed manifest). The existing integration test TestUpdateBinaryInPlace does a real GitHub download and now exercises verification end-to-end (its spinner-text assertion was updated).

Review: security-relevant change to the self-update path — human review advisable.

Closes DEVX-999

Co-Authored-By: Claude noreply@anthropic.com

Co-Authored-By: Claude <noreply@anthropic.com>
@skyrpex skyrpex added semver: patch docs: skip Pull request does not require documentation changes labels Jul 24, 2026
@skyrpex
skyrpex marked this pull request as ready for review July 24, 2026 14:34
@skyrpex
skyrpex requested a review from a team as a code owner July 24, 2026 14:34

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardens the self-update binary path by verifying the downloaded archive's SHA-256 against the release's checksums.txt before replacing the running executable — a clean, fail-closed change.

  1. thought(non-blocking): on internal/update/github.gochecksums.txt is fetched from the same releases/download endpoint as the archive, so this protects against archive corruption or partial/CDN-level tampering, not a fully compromised release (an attacker who could swap the archive could swap the manifest too). That matches what the PR claims and the CLAUDE.md wording is accurate — flagging only so it isn't later read as artifact signing.
  2. praise: the design fails closed across every path — missing checksums.txt, missing asset entry, malformed manifest, and digest mismatch all abort before the executable is touched, each with an actionable error. I confirmed the real v0.18.0 release ships checksums.txt and .goreleaser.yaml emits exactly that name, so the assumed asset name holds; unit + integration coverage is thorough. @skyrpex

Automated review on behalf of @gtsiolis.


Generated by Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only change since my last review is a doc-comment clarification on internal/update/github.go, which resolves the threat-model note I raised — the manifest-shares-origin caveat is now explicit in the code. Nothing else changed and the fail-closed verification path across every failure mode still stands. LGTM 🎉 @skyrpex

Automated review on behalf of @gtsiolis.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs: skip Pull request does not require documentation changes semver: patch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant