Skip to content

fix(security): fail closed when checksums unavailable or mismatched#269

Open
mitre88 wants to merge 1 commit intoGentleman-Programming:mainfrom
mitre88:fix/checksum-fail-closed
Open

fix(security): fail closed when checksums unavailable or mismatched#269
mitre88 wants to merge 1 commit intoGentleman-Programming:mainfrom
mitre88:fix/checksum-fail-closed

Conversation

@mitre88
Copy link
Copy Markdown

@mitre88 mitre88 commented Apr 9, 2026

Summary

Closes #245

Checksum verification in the binary install path was fail-open: if checksums.txt could not be downloaded, the archive name was absent from it, or no SHA256 tool was available, the installer logged a warning and continued. This allows installation of an unverified (potentially compromised) binary.

This PR hardens all three paths to fail closed by default, with an explicit escape hatch for environments where skipping is intentional.

Changes

scripts/install.sh

  • Could not download checksums.txtfatal (was warn)
  • Archive not found in checksums.txtfatal (was warn)
  • No sha256sum/shasum foundfatal (was warn + fake match)
  • New --insecure flag to explicitly opt out of verification

scripts/install.ps1

  • Could not download checksums.txt (catch block) → Stop-WithError (was Write-Warn)
  • Archive not found in checksums.txtStop-WithError (was Write-Warn)
  • New -Insecure switch parameter

internal/update/upgrade/download.go

The self-upgrade path (Download) had zero checksum verification. New functions added:

  • downloadToFile — downloads to a temp file and returns SHA256 hex digest (via io.MultiWriter)
  • fetchChecksums — fetches checksums.txt, returns error on HTTP non-200
  • expectedChecksumFor — parses BSD-style checksums.txt, returns error if filename not listed
  • resolveArchiveName / resolveChecksumURL — URL helpers (also improve testability)
  • Download now downloads to a temp file, verifies checksum, then extracts — fail closed at every step

internal/update/upgrade/download_test.go

New table-driven tests covering all four failure modes from the issue:

  • TestExpectedChecksumFor — pure unit test for the checksum parser
  • TestFetchChecksums — success + HTTP 404 → error
  • TestDownloadToFile — verifies SHA256 computed correctly while streaming
  • TestDownload_ChecksumVerification — four sub-cases: match (success), mismatch, missing checksums.txt, archive not listed

Test plan

  • go test ./internal/update/upgrade/... — all existing + new tests pass
  • go vet ./... — no issues
  • ./scripts/install.sh --insecure — installs with warning, no fatal
  • ./scripts/install.sh — fails when checksums.txt unreachable (e.g. no network)

@mitre88
Copy link
Copy Markdown
Author

mitre88 commented Apr 10, 2026

Note: I can't add labels from a fork. This PR needs the type:bug label for CI to pass.

Checksum verification was fail-open: if checksums.txt was missing, the
archive name was absent from the file, or no sha256 tool was found, the
installer warned and continued. This weakens the supply-chain trust
boundary at install time.

Changes:
- install.sh: convert three warn+continue paths to fatal; add --insecure
  escape hatch for manual use in air-gapped or trusted environments
- install.ps1: same for two warn+continue paths; add -Insecure switch
- download.go: add mandatory checksum verification to the self-upgrade
  path (Download); introduces downloadToFile (SHA256 while streaming),
  fetchChecksums, expectedChecksumFor, resolveArchiveName, and
  resolveChecksumURL; existing downloadBinary kept for extraction
- download_test.go: add TestExpectedChecksumFor, TestFetchChecksums,
  TestDownloadToFile, and TestDownload_ChecksumVerification covering all
  four failure modes from the issue

Closes Gentleman-Programming#245
@mitre88 mitre88 force-pushed the fix/checksum-fail-closed branch from 97d359b to b70661e Compare April 10, 2026 02:53
Copy link
Copy Markdown
Contributor

@Alan-TheGentleman Alan-TheGentleman left a comment

Choose a reason for hiding this comment

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

Solid security hardening — the fail-open to fail-closed transition is exactly right, and the test coverage for all four failure modes is thorough. The Go implementation correctly downloads to a temp dir, verifies the checksum, and only then extracts. LGTM.

A few minor suggestions (none blocking):

  1. fetchChecksums: add io.LimitReader — the project already uses this pattern in strategy.go. Without it, a malicious server could force unbounded memory allocation. Something like io.LimitReader(resp.Body, 1<<20) would be sufficient.

  2. downloadToFile: explicit f.Close() before return — the deferred close means write errors from Close() are silently dropped. Minor, but good practice for data-integrity code.

  3. Constant-time comparison (crypto/subtle.ConstantTimeCompare) for the digest check. Negligible real-world risk for a CLI tool, but it's the standard practice for checksum verification.

  4. downloadBinary is now orphaned from Download() — consider a // Deprecated note or removal in a follow-up to avoid confusion about which path is checksum-verified.

Nice work overall.

@Alan-TheGentleman Alan-TheGentleman added the type:bug Bug fix label Apr 12, 2026
@mitre88
Copy link
Copy Markdown
Author

mitre88 commented Apr 17, 2026

Gentle ping — this PR is ready for review. All CI checks pass. Would appreciate a look when possible. Thanks!

@mitre88
Copy link
Copy Markdown
Author

mitre88 commented Apr 18, 2026

Bump — all CI checks pass (Unit Tests, E2E Tests, lint). This PR has been approved. Ready to merge. Could a maintainer hit the merge button? Thanks!

@mitre88
Copy link
Copy Markdown
Author

mitre88 commented Apr 18, 2026

@Gentleman-Programming — this security fix has been approved and all CI passes (unit + E2E). It makes checksum validation fail-closed instead of fail-open. Could you merge? Thanks!

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

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(installer): fail closed when checksums are unavailable or mismatched

2 participants