diff --git a/src/commands/bar/install-subcommand.ts b/src/commands/bar/install-subcommand.ts index f2d20790..a1fc58f8 100644 --- a/src/commands/bar/install-subcommand.ts +++ b/src/commands/bar/install-subcommand.ts @@ -36,11 +36,8 @@ const BAR_GITHUB_REPO = 'kaitranntt/ccs'; /** * Allowlist of hostnames from which we will accept asset downloads. * GitHub releases redirect from github.com to objects.githubusercontent.com. - * - * TODO(checksum-v2): once release assets ship a checksums.txt/.sha256 file, - * wire SHA-256 verification here. The download URL is already validated for - * host+HTTPS as a v1 minimum guard. The verifier hook below is the intended - * extension point. + * Artifact authenticity is enforced separately with the GitHub release asset + * SHA-256 digest before extraction. */ const DOWNLOAD_HOST_ALLOWLIST: ReadonlyArray = [ 'github.com', @@ -53,6 +50,7 @@ const DOWNLOAD_HOST_ALLOWLIST: ReadonlyArray = [ export interface ReleaseAssetResult { downloadUrl: string; + sha256: string; } export interface CompatResult { @@ -68,10 +66,10 @@ export interface InstallDeps { */ fetchReleaseAsset: (tag: string, asset: string) => Promise; /** - * Download the zip archive and extract the .app bundle into dest/. - * Production: uses undici to stream + extract (with redirect + status check). + * Download the zip archive, verify its SHA-256 digest, and extract the .app bundle into dest/. + * Production: uses undici to stream + verify + extract (with redirect + status check). */ - downloadAndExtract: (url: string, dest: string) => Promise; + downloadAndExtract: (url: string, dest: string, expectedSha256: string) => Promise; /** * GET {baseUrl}/api/bar/summary — capability handshake. * 200 → compatible; 404 → no-bar-api; else/unreachable → unreachable. @@ -190,7 +188,16 @@ async function defaultFetchReleaseAsset(tag: string, asset: string): Promise { +async function defaultDownloadAndExtract( + url: string, + dest: string, + expectedSha256: string +): Promise { const { request } = await import('undici'); - const { createWriteStream, mkdirSync } = fs; + const { createHash } = await import('crypto'); + const { createReadStream, createWriteStream, mkdirSync } = fs; const { promisify } = await import('util'); const { pipeline } = await import('stream'); const streamPipeline = promisify(pipeline); const { execFile } = await import('child_process'); const execFileAsync = promisify(execFile); - // Validate initial URL (Finding #9) + // Validate initial URL (Finding #9) and require an integrity pin. validateDownloadUrl(url); + if (!/^[a-fA-F0-9]{64}$/.test(expectedSha256)) { + throw new Error('Missing or invalid SHA-256 digest for CCS Bar archive. Refusing to install.'); + } mkdirSync(dest, { recursive: true }); @@ -263,10 +280,25 @@ async function defaultDownloadAndExtract(url: string, dest: string): Promise