Skip to content

Commit ea9d5e1

Browse files
committed
fix(security): make missing SHA-256 checksums a hard error
- Add requirePythonChecksum() and requireSocketPatchChecksum() functions that throw hard errors when checksums are missing in production builds - Update spawn.mts and resolve-binary.mts to use the new require functions - Update SEA build downloads.mjs to require checksums for all external tools - Add scripts/validate-checksums.mjs for build-time checksum validation In development mode (checksums not inlined), downloads proceed without verification. In production builds (checksums inlined), missing checksums cause immediate failures to prevent supply chain attacks. This is a security hardening change to ensure all external tool downloads are verified against known-good SHA-256 checksums.
1 parent f1bf2f6 commit ea9d5e1

File tree

6 files changed

+236
-10
lines changed

6 files changed

+236
-10
lines changed

packages/cli/scripts/sea-build-utils/downloads.mjs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,16 +327,25 @@ export async function downloadExternalTools(platform, arch, isMusl = false) {
327327
const tag = config.version
328328
const url = `https://github.com/${config.owner}/${config.repo}/releases/download/${tag}/${assetName}`
329329

330-
// Get SHA256 checksum if available in external-tools.json.
330+
// Get SHA256 checksum from external-tools.json.
331+
// SECURITY: Checksum verification is REQUIRED for all external tool downloads.
332+
// If checksum is missing, the build MUST fail.
331333
const toolConfig = externalTools[toolName]
332334
const sha256 = toolConfig?.checksums?.[assetName]
333335

336+
if (!sha256) {
337+
throw new Error(
338+
`Missing SHA-256 checksum for ${toolName} asset: ${assetName}. ` +
339+
'This is a security requirement. Please update external-tools.json with the correct checksum.',
340+
)
341+
}
342+
334343
await httpDownload(url, archivePath, {
335344
logger,
336345
progressInterval: 10,
337346
retries: 2,
338347
retryDelay: 5000,
339-
...(sha256 && { sha256 }),
348+
sha256,
340349
})
341350

342351
// Extract binary (or handle standalone binaries).

packages/cli/src/env/python-checksums.mts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,51 @@ import type { PythonChecksums } from '../types.mjs'
1313
/**
1414
* Get Python checksums from inlined environment variable.
1515
* Returns a map of asset filename to SHA-256 hex checksum.
16+
*
17+
* @throws Error if checksums are missing in production builds.
1618
*/
1719
export function getPythonChecksums(): PythonChecksums {
1820
const checksums = process.env['INLINED_SOCKET_CLI_PYTHON_CHECKSUMS']
1921
if (!checksums) {
22+
// In development mode (not inlined), return empty object.
23+
// Build validation will catch missing checksums at build time.
2024
return {}
2125
}
2226
try {
2327
return JSON.parse(checksums) as PythonChecksums
2428
} catch {
25-
return {}
29+
throw new Error(
30+
'Failed to parse Python checksums. This indicates a build configuration error.',
31+
)
32+
}
33+
}
34+
35+
/**
36+
* Lookup a Python checksum by asset name.
37+
* In production builds (checksums inlined), throws a hard error if asset is missing.
38+
* In dev mode (checksums not inlined), returns undefined to allow development.
39+
*
40+
* @param assetName - The asset filename to look up.
41+
* @returns The SHA-256 hex checksum, or undefined in dev mode.
42+
* @throws Error if checksum is not found in production builds.
43+
*/
44+
export function requirePythonChecksum(assetName: string): string | undefined {
45+
const checksums = getPythonChecksums()
46+
47+
// In dev mode, checksums are not inlined so the object is empty.
48+
// Allow downloads without verification during development.
49+
if (Object.keys(checksums).length === 0) {
50+
return undefined
51+
}
52+
53+
// In production mode, checksums are inlined.
54+
// Require checksum for every asset - missing checksum is a HARD ERROR.
55+
const sha256 = checksums[assetName]
56+
if (!sha256) {
57+
throw new Error(
58+
`Missing SHA-256 checksum for Python asset: ${assetName}. ` +
59+
'This is a security requirement. Please update external-tools.json with the correct checksum.',
60+
)
2661
}
62+
return sha256
2763
}

packages/cli/src/env/socket-patch-checksums.mts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,51 @@ import type { SocketPatchChecksums } from '../types.mjs'
1313
/**
1414
* Get Socket Patch checksums from inlined environment variable.
1515
* Returns a map of asset filename to SHA-256 hex checksum.
16+
*
17+
* @throws Error if checksums are missing in production builds.
1618
*/
1719
export function getSocketPatchChecksums(): SocketPatchChecksums {
1820
const checksums = process.env['INLINED_SOCKET_CLI_SOCKET_PATCH_CHECKSUMS']
1921
if (!checksums) {
22+
// In development mode (not inlined), return empty object.
23+
// Build validation will catch missing checksums at build time.
2024
return {}
2125
}
2226
try {
2327
return JSON.parse(checksums) as SocketPatchChecksums
2428
} catch {
25-
return {}
29+
throw new Error(
30+
'Failed to parse Socket Patch checksums. This indicates a build configuration error.',
31+
)
32+
}
33+
}
34+
35+
/**
36+
* Lookup a Socket Patch checksum by asset name.
37+
* In production builds (checksums inlined), throws a hard error if asset is missing.
38+
* In dev mode (checksums not inlined), returns undefined to allow development.
39+
*
40+
* @param assetName - The asset filename to look up.
41+
* @returns The SHA-256 hex checksum, or undefined in dev mode.
42+
* @throws Error if checksum is not found in production builds.
43+
*/
44+
export function requireSocketPatchChecksum(assetName: string): string | undefined {
45+
const checksums = getSocketPatchChecksums()
46+
47+
// In dev mode, checksums are not inlined so the object is empty.
48+
// Allow downloads without verification during development.
49+
if (Object.keys(checksums).length === 0) {
50+
return undefined
51+
}
52+
53+
// In production mode, checksums are inlined.
54+
// Require checksum for every asset - missing checksum is a HARD ERROR.
55+
const sha256 = checksums[assetName]
56+
if (!sha256) {
57+
throw new Error(
58+
`Missing SHA-256 checksum for Socket Patch asset: ${assetName}. ` +
59+
'This is a security requirement. Please update external-tools.json with the correct checksum.',
60+
)
2661
}
62+
return sha256
2763
}

packages/cli/src/utils/dlx/resolve-binary.mts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { SOCKET_CLI_PYCLI_LOCAL_PATH } from '../../env/socket-cli-pycli-local-pa
1313
import { SOCKET_CLI_SFW_LOCAL_PATH } from '../../env/socket-cli-sfw-local-path.mts'
1414
import { SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH } from '../../env/socket-cli-socket-patch-local-path.mts'
1515
import { getSfwNpmVersion } from '../../env/sfw-version.mts'
16-
import { getSocketPatchChecksums } from '../../env/socket-patch-checksums.mts'
16+
import { requireSocketPatchChecksum } from '../../env/socket-patch-checksums.mts'
1717
import { getSocketPatchVersion } from '../../env/socket-patch-version.mts'
1818
import { getSynpVersion } from '../../env/synp-version.mts'
1919

@@ -167,8 +167,9 @@ export function resolveSocketPatch(): BinaryResolution {
167167
}
168168

169169
// Get SHA-256 checksum for integrity verification.
170-
const checksums = getSocketPatchChecksums()
171-
const sha256 = checksums[assetName]
170+
// In dev mode (checksums not inlined), returns undefined to allow development.
171+
// In production builds, missing checksums throw a HARD ERROR.
172+
const sha256 = requireSocketPatchChecksum(assetName)
172173

173174
return {
174175
type: 'github-release',

packages/cli/src/utils/dlx/spawn.mts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { getDefaultOrgSlug } from '../../commands/ci/fetch-default-org-slug.mjs'
5252
import { getCliVersion } from '../../env/cli-version.mts'
5353
import { getPyCliVersion } from '../../env/pycli-version.mts'
5454
import { getPythonBuildTag } from '../../env/python-build-tag.mts'
55-
import { getPythonChecksums } from '../../env/python-checksums.mts'
55+
import { requirePythonChecksum } from '../../env/python-checksums.mts'
5656
import { getPythonVersion } from '../../env/python-version.mts'
5757
import { SOCKET_CLI_PYTHON_PATH } from '../../env/socket-cli-python-path.mts'
5858
import { getSynpVersion } from '../../env/synp-version.mts'
@@ -947,8 +947,9 @@ async function downloadPython(pythonDir: string): Promise<void> {
947947
const tarballName = 'python-standalone.tar.gz'
948948

949949
// Get SHA-256 checksum for integrity verification.
950-
const checksums = getPythonChecksums()
951-
const sha256 = checksums[assetName]
950+
// In dev mode (checksums not inlined), returns undefined to allow development.
951+
// In production builds, missing checksums throw a HARD ERROR.
952+
const sha256 = requirePythonChecksum(assetName)
952953

953954
await safeMkdir(pythonDir, { recursive: true })
954955

scripts/validate-checksums.mjs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* @fileoverview Build-time validation for SHA-256 checksums.
5+
* Ensures all required platform-specific tool assets have checksums defined
6+
* in external-tools.json before building SEA binaries.
7+
*
8+
* This script is a security requirement - builds MUST NOT proceed if any
9+
* checksums are missing for downloadable binaries.
10+
*
11+
* Exit codes:
12+
* - 0: All required checksums are present.
13+
* - 1: One or more checksums are missing.
14+
*/
15+
16+
import { readFileSync } from 'node:fs'
17+
import path from 'node:path'
18+
import { fileURLToPath } from 'node:url'
19+
20+
import { getDefaultLogger } from '@socketsecurity/lib/logger'
21+
22+
import { PLATFORM_MAP_TOOLS } from '../packages/cli/scripts/constants/external-tools-platforms.mjs'
23+
24+
const logger = getDefaultLogger()
25+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
26+
const rootPath = path.join(__dirname, '..')
27+
28+
// Load external tools configuration.
29+
const externalToolsPath = path.join(
30+
rootPath,
31+
'packages/cli/external-tools.json',
32+
)
33+
const externalTools = JSON.parse(readFileSync(externalToolsPath, 'utf8'))
34+
35+
/**
36+
* Validate that all required checksums exist for external tools.
37+
* @returns {boolean} True if all checksums are valid, false otherwise.
38+
*/
39+
function validateChecksums() {
40+
const errors = []
41+
const warnings = []
42+
43+
logger.info('Validating SHA-256 checksums for external tools...\n')
44+
45+
// Track all unique assets that need checksums.
46+
const requiredAssets = new Map() // Map<toolName, Set<assetName>>
47+
48+
// Collect all assets needed across all platforms.
49+
for (const [platform, tools] of Object.entries(PLATFORM_MAP_TOOLS)) {
50+
if (!tools) continue
51+
52+
for (const [toolName, assetName] of Object.entries(tools)) {
53+
if (!assetName) continue
54+
55+
if (!requiredAssets.has(toolName)) {
56+
requiredAssets.set(toolName, new Set())
57+
}
58+
requiredAssets.get(toolName).add(assetName)
59+
}
60+
}
61+
62+
// Validate each tool's checksums.
63+
for (const [toolName, assets] of requiredAssets) {
64+
const toolConfig = externalTools[toolName]
65+
66+
if (!toolConfig) {
67+
errors.push(`Tool "${toolName}" not found in external-tools.json`)
68+
continue
69+
}
70+
71+
// Only GitHub release tools need checksums.
72+
if (toolConfig.type !== 'github-release') {
73+
continue
74+
}
75+
76+
const checksums = toolConfig.checksums || {}
77+
const missingAssets = []
78+
79+
for (const assetName of assets) {
80+
if (!checksums[assetName]) {
81+
missingAssets.push(assetName)
82+
}
83+
}
84+
85+
if (missingAssets.length > 0) {
86+
errors.push(
87+
`Missing checksums for ${toolName}:\n` +
88+
missingAssets.map(a => ` - ${a}`).join('\n'),
89+
)
90+
} else {
91+
logger.success(`${toolName}: ${assets.size} asset checksum(s) verified`)
92+
}
93+
}
94+
95+
// Check for extra checksums that aren't used (informational).
96+
for (const [toolName, toolConfig] of Object.entries(externalTools)) {
97+
if (toolConfig.type !== 'github-release' || !toolConfig.checksums) {
98+
continue
99+
}
100+
101+
const usedAssets = requiredAssets.get(toolName) || new Set()
102+
const extraAssets = Object.keys(toolConfig.checksums).filter(
103+
asset => !usedAssets.has(asset),
104+
)
105+
106+
if (extraAssets.length > 0) {
107+
warnings.push(
108+
`${toolName} has ${extraAssets.length} unused checksum(s) (may be for unsupported platforms)`,
109+
)
110+
}
111+
}
112+
113+
// Print summary.
114+
console.log('')
115+
if (warnings.length > 0) {
116+
logger.warn('Warnings:')
117+
for (const warning of warnings) {
118+
logger.warn(` ${warning}`)
119+
}
120+
console.log('')
121+
}
122+
123+
if (errors.length > 0) {
124+
logger.error('CHECKSUM VALIDATION FAILED')
125+
console.log('')
126+
for (const error of errors) {
127+
logger.error(error)
128+
}
129+
console.log('')
130+
logger.error(
131+
'All external tool assets MUST have SHA-256 checksums defined in external-tools.json.',
132+
)
133+
logger.error('This is a security requirement to prevent supply chain attacks.')
134+
return false
135+
}
136+
137+
logger.success('\nAll required checksums are present.')
138+
return true
139+
}
140+
141+
// Run validation.
142+
const valid = validateChecksums()
143+
process.exit(valid ? 0 : 1)

0 commit comments

Comments
 (0)