fix(security): verify Windows updater asset digests #1792
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1792
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/verify-updater-assets"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The Windows updater currently checks download length but can launch a same-length altered setup or extract a same-length altered ZIP. This verifies the selected release asset against GitHub's SHA-256 digest before either handoff.
The updater rejects missing, malformed and unsupported digests before requesting the payload, hashes streamed bytes on both the browser and API routes, and promotes a unique owned partial file only after the length and digest checks pass. Digest failures are terminal; cleanup preserves the previous cached file. Only the chosen asset requires a valid digest, so an unused legacy ZIP does not block a valid setup.
Related to #1679; signing, setup-list authentication and release-tooling lockfiles remain separate work. GitHub's digest checks bytes against GitHub's release metadata and does not authenticate a compromised publishing account.
Validation:
Updater.csis compiled in a game-free fixture with authored HTTP responses, process/UI spies and temporary directories. No executable is launched or downloaded.main.RUN_ALL_ARGSin its own file to match upstream's runner format. The runner is unchanged frommain; the strict selection runs all 20 cases and passes without skips.SelfContained=false,UseAppHost=false), not a publish/package test.Local gaps: the full source suite, Linux execution, self-contained publishing, a real self-update or the complete delayed-retry schedule. The full suite was not run locally because an existing owner-imposed stop excludes a shader fixture; no assertions or skip policy were changed. Hosted Windows/Linux source suites and Windows Builder packaging now pass. No installed player, game files, saves or runtime settings are changed by this PR.
Current head
848175cenormally mergesmainat117eae79, including #1858's presentation definitions and #1793's shared source-check repairs. The updater, its regression fixtures and the source-check runner are unchanged by those merges. All eight hosted checks pass on this head: both OS source suites, Windows Builder packaging, Windows Vulkan, Linux AppImage, both native lowering checks and workflow lint. Release-upload jobs are correctly skipped for a pull request. No real self-update was executed.Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
Looks good, and the conflict I flagged earlier is gone (merge state is clean, all hosted checks pass).
Checked the fail-closed paths in
Updater.cs:ParseDigestthrows on a missing, non-string, non-sha256:, wrong-length or non-hex digest, and it runs at the top ofDownloadWithRetries, before any payload request.InvalidDataExceptionis notHttpRequestException, soIsTransientis false and neither the retry loop nor the API-URL fallback swallows it. Both callers end up inDownloadFailedwith nothing launched or extracted.FixedTimeEqualsover the streamed bytes, checked after the length check and beforeFile.Move, so the final path only ever holds a verified file. The hash is over exactly the bytes written, on both the browser and API routes.CreateNewwith a GUID name,ownedis set only once the stream is open, and thefinallydeletes only that file, so a previously cached good file is never clobbered or deleted on failure.On where the expected digest comes from: it's the
digestfield of the samereleases/latestAPI response over HTTPS, so it stops a same-length swapped or corrupted asset but not a compromised publisher account or API response. The PR description says exactly that and leaves signing to separate work, which seems the right scope.Nit, no change needed: a hard kill mid-download leaves a
.part-<guid>file in the update cache, since nothing sweeps old ones. It's small and the cache is per-user, so fine to leave.