From e0d6f4c24965770425235e7b74fb208b74a1ff1c Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Fri, 27 Mar 2026 18:53:22 -0400 Subject: [PATCH 1/2] feat(security): verify binary SHA-256 checksum on download (B5) - Fetch checksums.txt from release assets after binary download - Compute SHA-256 of downloaded binary using Node crypto - On mismatch: delete binary and throw Error with clear message - Graceful fallback: warn if checksums.txt not available (older releases) Addresses: design-partner-eval B5 (P1 security) --- src/utils/binary-manager.ts | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/utils/binary-manager.ts b/src/utils/binary-manager.ts index 353faea..552182f 100644 --- a/src/utils/binary-manager.ts +++ b/src/utils/binary-manager.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; +import crypto from 'crypto'; import axios from 'axios'; import { promisify } from 'util'; import stream from 'stream'; @@ -124,6 +125,9 @@ export class BinaryManager { const writer = fs.createWriteStream(tempFilePath); await pipeline(response.data, writer); + // Verify checksum integrity + await this.verifyChecksum(tempFilePath, assetName); + // Move to install dir // We rename it to capiscio-core (or .exe) for internal consistency fs.copyFileSync(tempFilePath, this.binaryPath); @@ -156,6 +160,44 @@ export class BinaryManager { } } + private async verifyChecksum(filePath: string, assetName: string): Promise { + const checksumsUrl = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${VERSION}/checksums.txt`; + + let expectedHash: string | null = null; + try { + const resp = await axios.get(checksumsUrl, { timeout: 30000 }); + const lines = (resp.data as string).trim().split('\n'); + for (const line of lines) { + const parts = line.trim().split(/\s+/); + if (parts.length === 2 && parts[1] === assetName) { + expectedHash = parts[0]; + break; + } + } + } catch { + console.warn('Warning: Could not fetch checksums.txt. Skipping integrity verification.'); + return; + } + + if (!expectedHash) { + console.warn(`Warning: Asset ${assetName} not found in checksums.txt. Skipping verification.`); + return; + } + + const fileBuffer = fs.readFileSync(filePath); + const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + if (actualHash !== expectedHash) { + // Remove the tampered file + fs.unlinkSync(filePath); + throw new Error( + `Binary integrity check failed for ${assetName}. ` + + `Expected SHA-256: ${expectedHash}, got: ${actualHash}. ` + + 'The downloaded file does not match the published checksum.' + ); + } + } + private getPlatform(): string { const platform = os.platform(); switch (platform) { From 5e649f6367d58e0ecdc1ed7bd3bf88946d3958f6 Mon Sep 17 00:00:00 2001 From: Beon de Nood Date: Fri, 27 Mar 2026 19:03:02 -0400 Subject: [PATCH 2/2] fix: resolve TS2322 undefined-to-null type mismatch --- src/utils/binary-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/binary-manager.ts b/src/utils/binary-manager.ts index 552182f..a1c6a8a 100644 --- a/src/utils/binary-manager.ts +++ b/src/utils/binary-manager.ts @@ -170,7 +170,7 @@ export class BinaryManager { for (const line of lines) { const parts = line.trim().split(/\s+/); if (parts.length === 2 && parts[1] === assetName) { - expectedHash = parts[0]; + expectedHash = parts[0] ?? null; break; } }