also looking at this
fix(release): prevent the standalone macOS CLI sandbox crash
#291
5 files
+120
-31
| @@ -0,0 +1,40 @@ | ||
| 1 | import { resolve } from 'node:path' | |
| 2 | ||
| 3 | function command(args: string[]): string { | |
| 4 | const result = Bun.spawnSync(args, { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }) | |
| 5 | if (result.exitCode !== 0) | |
| 6 | throw new Error(`${args.join(' ')} failed (${result.signalCode ?? result.exitCode})\n${result.stdout}\n${result.stderr}`) | |
| 7 | return `${result.stdout}\n${result.stderr}` | |
| 8 | } | |
| 9 | ||
| 10 | export function verifyMacosRelease(binary: string, version: string, execute = true): void { | |
| 11 | if (process.platform !== 'darwin') | |
| 12 | throw new Error('macOS release verification requires macOS trust services') | |
| 13 | command(['codesign', '--verify', '--deep', '--strict', '--verbose=4', binary]) | |
| 14 | const metadata = command(['codesign', '--display', '--verbose=4', binary]) | |
| 15 | if (!metadata.includes('Signature=adhoc')) { | |
| 16 | if (!metadata.includes('Authority=Developer ID Application:') || !metadata.includes('Timestamp=')) | |
| 17 | throw new Error('Developer ID release must have a certificate chain and secure timestamp') | |
| 18 | if (!/flags=.*\bruntime\b/.test(metadata)) | |
| 19 | throw new Error('Developer ID release must enable hardened runtime') | |
| 20 | } | |
| 21 | const entitlements = command(['codesign', '--display', '--entitlements', '-', '--xml', binary]) | |
| 22 | for (const key of ['com.apple.security.app-sandbox', 'com.apple.security.cs.disable-library-validation', 'com.apple.security.cs.allow-unsigned-executable-memory']) { | |
| 23 | const escaped = key.replaceAll('.', '\\.') | |
| 24 | if (new RegExp(`<key>${escaped}</key>\\s*<true\\s*/>`).test(entitlements)) | |
| 25 | throw new Error(`Standalone release must not enable ${key}`) | |
| 26 | } | |
| 27 | if (execute) { | |
| 28 | const output = command([binary, '--version']) | |
| 29 | if (output.trim().split('\n')[0] !== `craft version ${version}`) | |
| 30 | throw new Error(`Expected craft version ${version}, received: ${output}`) | |
| 31 | } | |
| 32 | console.log(`Verified ${binary}${execute ? ` launches as ${version}` : ' signature and entitlements'}`) | |
| 33 | } | |
| 34 | ||
| 35 | if (import.meta.main) { | |
| 36 | const [binary, version, option] = Bun.argv.slice(2) | |
| 37 | if (!binary || !version || (option !== undefined && option !== '--no-execute')) | |
| 38 | throw new Error('Usage: bun scripts/verify-macos-release.ts <binary> <version> [--no-execute]') | |
| 39 | verifyMacosRelease(resolve(binary), version, option !== '--no-execute') | |
| 40 | } | |