-
Notifications
You must be signed in to change notification settings - Fork 91
feat(install): verify release archive checksums in both installers #942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/windows-powershell-installer
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -77,6 +77,36 @@ public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wP | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| # Verify a downloaded archive against the release's checksums.txt. | ||||||
| # Hard-fails (throws) on a real mismatch. Soft-skips when checksums.txt can't be | ||||||
| # fetched (older release, network blip) or has no entry for this file, so pinned | ||||||
| # installs of pre-checksums releases keep working. | ||||||
| function Test-Checksum { | ||||||
| param([string]$Path, [string]$Name, [string]$ChecksumsUrl) | ||||||
|
|
||||||
| $sums = $null | ||||||
| try { | ||||||
| $sums = (Invoke-WebRequest -Uri $ChecksumsUrl -UseBasicParsing).Content | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 — verification is effectively dead on Windows PowerShell 5.1. GitHub release-assets are served with Downstream effect:
So on the dominant Windows shell, every install soft-skips verification and the user sees a misleading "no checksum entry" notice instead of either a real check or a clear "unsupported" error. Fix — decode bytes explicitly: $resp = Invoke-WebRequest -Uri $ChecksumsUrl -UseBasicParsing
$sums = if ($resp.Content -is [byte[]]) {
[System.Text.Encoding]::UTF8.GetString($resp.Content)
} else {
$resp.Content
}Worth a Pester test that feeds a known-good fixture through |
||||||
| } catch { | ||||||
| Write-Muted "Skipping integrity check — checksums.txt not published for this release" | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| # checksums.txt is sha256sum format: "<hash> <filename>" (one entry per line). | ||||||
| $line = ($sums -split "`n") | Where-Object { $_ -match "\s\*?$([regex]::Escape($Name))\s*$" } | Select-Object -First 1 | ||||||
| if (-not $line) { | ||||||
| Write-Muted "Skipping integrity check — no checksum entry for $Name" | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| $expected = (($line -split '\s+')[0]).ToLower() | ||||||
| $actual = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLower() | ||||||
| if ($actual -ne $expected) { | ||||||
| throw "Checksum mismatch for $Name (expected $expected, got $actual)" | ||||||
| } | ||||||
| Write-Muted "Verified $Name (sha256)" | ||||||
| } | ||||||
|
|
||||||
| # --------------------------------------------------------------------------- | ||||||
| # Architecture / baseline detection | ||||||
| # --------------------------------------------------------------------------- | ||||||
|
|
@@ -171,10 +201,12 @@ function Install-Target { | |||||
| $filename = "$App-$target.zip" | ||||||
|
|
||||||
| if ($useLatest) { | ||||||
| $url = "https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename" | ||||||
| $base = "https://github.com/AltimateAI/altimate-code/releases/latest/download" | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Using the mutable Prompt for AI agents
Suggested change
|
||||||
| } else { | ||||||
| $url = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion/$filename" | ||||||
| $base = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion" | ||||||
| } | ||||||
| $url = "$base/$filename" | ||||||
| $checksumsUrl = "$base/checksums.txt" | ||||||
|
|
||||||
| Write-Host "" | ||||||
| Write-Host "Installing $App version: $specificVersion" | ||||||
|
|
@@ -184,12 +216,6 @@ function Install-Target { | |||||
| $zipPath = Join-Path $tmpDir $filename | ||||||
|
|
||||||
| try { | ||||||
| # NOTE: integrity verification (SHA256/signature) of the archive is | ||||||
| # intentionally deferred to match the bash installer's posture — both rely | ||||||
| # on HTTPS from github.com release assets. Releases do not currently publish | ||||||
| # a checksums file; adding one + verifying it in both installers is tracked | ||||||
| # as a follow-up. See PR #930 discussion. | ||||||
| # | ||||||
| # Prefer curl.exe (ships with Windows 10 1803+) for a fast download with | ||||||
| # --fail so HTTP errors don't write an error page to disk; fall back to | ||||||
| # Invoke-WebRequest where curl.exe is unavailable. | ||||||
|
|
@@ -201,6 +227,10 @@ function Install-Target { | |||||
| Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing | ||||||
| } | ||||||
|
|
||||||
| # Integrity check: hard-fail on mismatch; skip (with notice) when the release | ||||||
| # predates checksums.txt or the fetch fails, so older pinned installs still work. | ||||||
| Test-Checksum -Path $zipPath -Name $filename -ChecksumsUrl $checksumsUrl | ||||||
|
|
||||||
| Expand-Archive -Path $zipPath -DestinationPath $tmpDir -Force | ||||||
| $extracted = Join-Path $tmpDir $BinaryName | ||||||
| if (-not (Test-Path $extracted)) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| /** | ||
| * Release-archive integrity verification across the install surface. | ||
| * | ||
| * The release publishes a checksums.txt asset; both installers fetch it and | ||
| * verify the downloaded archive (sha256) before extracting — hard-fail on | ||
| * mismatch, soft-skip when the file is absent (older pinned releases). | ||
| */ | ||
| import { describe, test, expect } from "bun:test" | ||
| import { readFileSync } from "node:fs" | ||
| import { join } from "node:path" | ||
|
|
||
| const REPO_ROOT = join(import.meta.dir, "../../../..") | ||
| const BASH_INSTALL = readFileSync(join(REPO_ROOT, "install"), "utf-8") | ||
| const PS1 = readFileSync(join(REPO_ROOT, "install.ps1"), "utf-8") | ||
| const RELEASE_YML = readFileSync(join(REPO_ROOT, ".github/workflows/release.yml"), "utf-8") | ||
|
|
||
| describe("release publishes checksums", () => { | ||
| test("release.yml generates checksums.txt and uploads it", () => { | ||
| expect(RELEASE_YML).toContain("sha256sum *.tar.gz *.zip > checksums.txt") | ||
| expect(RELEASE_YML).toContain("packages/opencode/dist/checksums.txt") | ||
| }) | ||
| }) | ||
|
|
||
| describe("bash installer verifies checksums", () => { | ||
| test("fetches checksums.txt and compares sha256", () => { | ||
| expect(BASH_INSTALL).toContain("checksums.txt") | ||
| expect(BASH_INSTALL).toMatch(/sha256sum|shasum -a 256/) | ||
| }) | ||
|
|
||
| test("hard-fails on mismatch", () => { | ||
| expect(BASH_INSTALL).toContain("Checksum mismatch") | ||
| expect(BASH_INSTALL).toContain("verify_checksum") | ||
| }) | ||
| }) | ||
|
|
||
| describe("PowerShell installer verifies checksums", () => { | ||
| test("fetches checksums.txt and compares sha256", () => { | ||
| expect(PS1).toContain("checksums.txt") | ||
| expect(PS1).toContain("Get-FileHash") | ||
| expect(PS1).toContain("Test-Checksum") | ||
| }) | ||
|
|
||
| test("hard-fails on mismatch before extracting", () => { | ||
| expect(PS1).toContain("Checksum mismatch") | ||
| // The verify call must precede the actual extraction call (not the | ||
| // Expand-Archive mention in the top-of-file ProgressPreference comment). | ||
| expect(PS1.indexOf("Test-Checksum -Path")).toBeLessThan(PS1.indexOf("Expand-Archive -Path")) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Checksum mismatch path references undefined
tmp_dirunderset -u. This triggers an unbound-variable error and breaks intended error-handling cleanup logic.Prompt for AI agents