Skip to content

Commit 53ff875

Browse files
authored
feat(security): verify binary SHA-256 checksums after download (B5)
* 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) * fix: resolve TS2322 undefined-to-null type mismatch
1 parent 1709037 commit 53ff875

1 file changed

Lines changed: 42 additions & 0 deletions

File tree

src/utils/binary-manager.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'fs';
22
import path from 'path';
33
import os from 'os';
4+
import crypto from 'crypto';
45
import axios from 'axios';
56
import { promisify } from 'util';
67
import stream from 'stream';
@@ -124,6 +125,9 @@ export class BinaryManager {
124125
const writer = fs.createWriteStream(tempFilePath);
125126
await pipeline(response.data, writer);
126127

128+
// Verify checksum integrity
129+
await this.verifyChecksum(tempFilePath, assetName);
130+
127131
// Move to install dir
128132
// We rename it to capiscio-core (or .exe) for internal consistency
129133
fs.copyFileSync(tempFilePath, this.binaryPath);
@@ -156,6 +160,44 @@ export class BinaryManager {
156160
}
157161
}
158162

163+
private async verifyChecksum(filePath: string, assetName: string): Promise<void> {
164+
const checksumsUrl = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${VERSION}/checksums.txt`;
165+
166+
let expectedHash: string | null = null;
167+
try {
168+
const resp = await axios.get(checksumsUrl, { timeout: 30000 });
169+
const lines = (resp.data as string).trim().split('\n');
170+
for (const line of lines) {
171+
const parts = line.trim().split(/\s+/);
172+
if (parts.length === 2 && parts[1] === assetName) {
173+
expectedHash = parts[0] ?? null;
174+
break;
175+
}
176+
}
177+
} catch {
178+
console.warn('Warning: Could not fetch checksums.txt. Skipping integrity verification.');
179+
return;
180+
}
181+
182+
if (!expectedHash) {
183+
console.warn(`Warning: Asset ${assetName} not found in checksums.txt. Skipping verification.`);
184+
return;
185+
}
186+
187+
const fileBuffer = fs.readFileSync(filePath);
188+
const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
189+
190+
if (actualHash !== expectedHash) {
191+
// Remove the tampered file
192+
fs.unlinkSync(filePath);
193+
throw new Error(
194+
`Binary integrity check failed for ${assetName}. ` +
195+
`Expected SHA-256: ${expectedHash}, got: ${actualHash}. ` +
196+
'The downloaded file does not match the published checksum.'
197+
);
198+
}
199+
}
200+
159201
private getPlatform(): string {
160202
const platform = os.platform();
161203
switch (platform) {

0 commit comments

Comments
 (0)