From 2ae8bce3d6d2c2e6897b0e8b23d0ab95c5440c77 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2026 15:35:08 -0700 Subject: [PATCH] [eric] test: add a check that says whether the build is really signed or honestly admits it isnt --- scripts/ci/verify-signature.js | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 scripts/ci/verify-signature.js diff --git a/scripts/ci/verify-signature.js b/scripts/ci/verify-signature.js new file mode 100644 index 00000000..8e2fb4ce --- /dev/null +++ b/scripts/ci/verify-signature.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +// Reports — and, with --require-signed, ENFORCES — the code-signing state of a +// built artifact. Local dev builds are intentionally unsigned +// (CSC_IDENTITY_AUTO_DISCOVERY=false), so by default this only REPORTS so you +// always know what you're holding. The release workflows pass --require-signed +// AFTER Azure/Apple have signed, turning it into the SmartScreen/Gatekeeper gate: +// +// Windows: Get-AuthenticodeSignature .Status == Valid (Authenticode -> no SmartScreen block) +// macOS: codesign --verify --deep --strict AND spctl --assess == accepted (Gatekeeper) +// plus a stapled notarization ticket (stapler validate) +// +// node scripts/ci/verify-signature.js [--target ] [--require-signed] +// +// Exit 0 = reported ok (or signed when required). Exit 1 = required but not signed. + +'use strict'; +const { execSync } = require('child_process'); +const h = require('./lib/app-harness'); + +function parseArgs(argv) { + const out = { target: null, requireSigned: false }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--target') out.target = argv[++i]; + else if (argv[i] === '--require-signed') out.requireSigned = true; + } + return out; +} + +function sh(cmd) { + try { return { ok: true, out: execSync(cmd, { encoding: 'utf8' }).trim() }; } + catch (e) { return { ok: false, out: ((e.stdout || '') + (e.stderr || '')).toString().trim(), code: e.status }; } +} + +// Windows: Authenticode status + signer subject via Get-AuthenticodeSignature. +function inspectWindows(target) { + const ps = `$ErrorActionPreference='SilentlyContinue'; $s = Get-AuthenticodeSignature -LiteralPath '${target}'; '{0}|{1}' -f $s.Status, ($s.SignerCertificate.Subject)`; + const r = sh(`powershell -NoProfile -Command "${ps.replace(/"/g, '\\"')}"`); + const [status, subject] = (r.out || '').split('|'); + const signed = (status || '').trim() === 'Valid'; + return { signed, status: (status || 'Unknown').trim(), signer: (subject || '').trim() }; +} + +// macOS: codesign validity + Gatekeeper assessment + notarization staple. +function inspectMac(target) { + const cs = sh(`codesign --verify --deep --strict --verbose=2 "${target}"`); + const assess = sh(`spctl --assess --type execute --verbose=4 "${target}"`); + const staple = sh(`xcrun stapler validate "${target}"`); + const signed = cs.ok && assess.ok; + const status = `codesign=${cs.ok ? 'valid' : 'invalid'} gatekeeper=${assess.ok ? 'accepted' : 'rejected'} staple=${staple.ok ? 'present' : 'absent'}`; + return { signed, status, signer: (cs.out.match(/Authority=(.+)/) || [])[1] || '' }; +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const target = h.signableTarget(args.target || h.packagedAppPath()); + process.stdout.write(`Inspecting signature: ${target}\n`); + + let info; + if (process.platform === 'win32') info = inspectWindows(target); + else if (process.platform === 'darwin') info = inspectMac(target); + else { process.stdout.write(' (linux: no code-signing model checked)\n'); process.exit(0); } + + process.stdout.write(` signed = ${info.signed}\n status = ${info.status}\n`); + if (info.signer) process.stdout.write(` signer = ${info.signer}\n`); + + if (args.requireSigned && !info.signed) { + process.stderr.write(`\nSIGNATURE FAIL: --require-signed but artifact is not validly signed (${info.status}).\n`); + process.exit(1); + } + if (!info.signed) process.stdout.write('\nNOTE: artifact is UNSIGNED (expected for a local dev build; CI signs on v* tags).\n'); + else process.stdout.write('\nSIGNATURE OK: artifact is validly signed.\n'); + process.exit(0); +} + +main();