Skip to content

fix(tray): verify self-update artifacts against release checksums - #1343

Open
Dumbris wants to merge 1 commit into
mainfrom
fix/verify-update-artifact-checksum
Open

Dumbris wants to merge 1 commit into
mainfrom
fix/verify-update-artifact-checksum

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 22, 2026

Copy link
Copy Markdown
Member

What

The tray's GitHub self-update downloaded a release asset over http.Get and passed it straight to update.Apply with empty update.Options — no Checksum, no Signature, no PublicKey. Whatever came back on that connection was written over the running binary.

internal/tray/tray.go is behind !nogui && !headless && !linux, so the exposed surface is macOS non-bundle, non-Homebrew installs and all Windows installs. (macOS .app bundles are refused earlier and update through Sparkle, which does verify EdDSA; Homebrew installs are refused in findAssetURL.)

The tray path was effectively the un-hardened duplicate of an already-hardened flow: mcpproxy update --self (cmd/mcpproxy/update_cmd.go) has verified artifacts against checksums.txt all along.

How

Every release publishes checksums.txt, a sha256sum manifest over all assets (.github/workflows/release.yml, and prerelease.yml for RCs). The tray now:

  1. resolves checksums.txt from the same release,
  2. looks up the exact asset name it selected, and
  3. refuses to apply unless the SHA-256 of the downloaded archive matches.

It fails closed — no manifest asset, an unreachable or unparseable manifest, an unlisted asset, or any mismatch aborts and installs nothing.

Two details that decide correctness:

  • The hash is over the downloaded archive, not the extracted member, because that is what the manifest covers. update.Options.Checksum would have hashed the extracted bytes on both archive paths — the wrong thing.
  • findAsset prefers the mcpproxy-latest-<os>-<arch> alias, and in a real release its digest differs from the versioned archive's (notarization). So the asset name is now carried from selection into the lookup instead of being rederived.

ParseChecksums / VerifyFileSHA256 move from package main to internal/updatecheck so both self-update paths share one parser instead of a second copy of security-critical parsing; cmd/mcpproxy keeps thin wrappers. The parser follows the coreutils grammar strictly (digest, space, mode byte, name verbatim) and refuses ambiguity: conflicting duplicate names, an over-cap manifest that may have been cut mid-line, and escape sequences coreutils does not emit.

Verified

  • New test fails against the pre-fix code: every refusal case installed the artifact anyway.
  • go test ./internal/tray/... ./internal/updatecheck/... ./cmd/mcpproxy/... -race green; golangci-lint clean in both the bare and --build-tags server passes.
  • Against the live v0.68.0 release: the real mcpproxy-latest-darwin-arm64.tar.gz verifies and applies, and serving a different real asset under that name is refused with a digest mismatch. All 24 manifest entries parse.

Follow-ups (not here)

  • Cosign verification of checksums.txt itself. The release publishes the bundle and update --self verifies it by shelling out to cosign; doing that in the tray would abort on the many machines without cosign, so it needs its own call. No new dependency either way.
  • applyTarGzUpdate matches the archive member by suffix mcpproxy/mcpproxy.exe and writes it over os.Executable() — for the tray that is mcpproxy-tray, so a successful tray self-update installs the core binary over the tray binary. applyZipUpdate applies the first non-dir zip entry regardless of name. Pre-existing, untouched here; cmd/mcpproxy gets this right via coreBinaryName().

🤖 Generated with Claude Code

The tray's GitHub self-update path downloaded a release asset over
http.Get and handed it straight to update.Apply with empty
update.Options - no Checksum, no Signature, no PublicKey. Any response
that reached that code was installed over the running binary. The file
is behind `!nogui && !headless && !linux`, so this shipped on macOS
non-bundle installs and on Windows (macOS .app bundles are refused
earlier and use Sparkle, which does verify).

Every release already publishes checksums.txt, a sha256sum manifest over
all assets. The tray now fetches it from the same release, looks up the
exact asset name it selected, and refuses to apply unless the SHA-256 of
the downloaded archive matches. It fails closed: a release with no
manifest, an unreachable or unparseable manifest, an asset the manifest
does not list, and any digest mismatch all abort with a clear error and
install nothing.

The digest is taken over the downloaded archive, not over the extracted
member, because that is what the manifest covers - update.Options.Checksum
would hash the wrong bytes on both archive paths. The lookup uses the
asset name actually downloaded: findAsset prefers the
`mcpproxy-latest-<os>-<arch>` alias, whose digest differs from the
versioned archive's in a real release, so rederiving a name would compare
against the wrong entry.

ParseChecksums/VerifyFileSHA256 move from package main to
internal/updatecheck so both self-update paths share one parser rather
than keeping a second copy of security-critical parsing;
cmd/mcpproxy keeps thin wrappers. The parser follows the coreutils
grammar strictly (digest, space, mode byte, then the name verbatim) and
rejects ambiguity - conflicting duplicate names, an over-cap manifest
that may have been truncated mid-line, escape sequences coreutils does
not emit.

Follow-up, not in this change: the release also publishes a cosign
bundle over checksums.txt, which `mcpproxy update --self` verifies by
shelling out to the cosign binary. Doing the same in the tray would
abort on the many machines without cosign installed, so it needs its own
decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: d210af1
Status: ✅  Deploy successful!
Preview URL: https://4c05b70e.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-verify-update-artifact-c.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 73.23944% with 19 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/updatecheck/checksums.go 72.46% 16 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/verify-update-artifact-checksum

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (22 MB)
  • smart-mcp-proxymcpproxy-goIDTCVL.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 35747409072 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants