Gatekeeper reported the Bare server binary as damaged because it only had a linker ad-hoc stamp after embed. Seal darwin server binaries with the same sign-macos path used for Electron apps (codesign / rcodesign).
This commit is contained in:
@@ -202,6 +202,15 @@ async function buildOne(host, outRoot) {
|
||||
binary = flat
|
||||
}
|
||||
console.log(`[bare-standalone] wrote ${binary}`)
|
||||
|
||||
// Re-seal darwin binaries so Gatekeeper does not report "damaged".
|
||||
// bare-runtime ships linker ad-hoc stamps; embedding invalidates them.
|
||||
// Linux CI uses rcodesign (same path as Electron postPackage).
|
||||
if (host.startsWith('darwin') && process.env.PEARDOCK_SKIP_MACOS_SIGN !== '1') {
|
||||
console.log(`[bare-standalone] codesigning ${binary} for ${host}…`)
|
||||
const { signBinary } = require('./sign-macos-app.cjs')
|
||||
await signBinary(binary)
|
||||
}
|
||||
} else {
|
||||
console.warn(`[bare-standalone] WARN: expected binary not found under ${outDir}`)
|
||||
console.warn(
|
||||
@@ -221,6 +230,8 @@ async function buildOne(host, outRoot) {
|
||||
builtAt: new Date().toISOString(),
|
||||
entry: 'bin/peardock-server.mjs',
|
||||
bundleId: entry.id,
|
||||
macosSigned:
|
||||
host.startsWith('darwin') && process.env.PEARDOCK_SKIP_MACOS_SIGN !== '1',
|
||||
},
|
||||
null,
|
||||
2
|
||||
|
||||
@@ -173,7 +173,10 @@ open peardock-darwin-arm64/peardock.app # macOS
|
||||
# Windows: peardock-win32-x64\\\\peardock-client.exe
|
||||
\`\`\`
|
||||
|
||||
macOS builds from Linux CI are **unsigned** (Gatekeeper may require right-click → Open).
|
||||
macOS **client** (`.app`) and **server** (`peardock-server`) binaries are codesigned in CI
|
||||
(ad-hoc / self-signed via \`rcodesign\` on Linux, or \`codesign\` on macOS). That avoids the
|
||||
Gatekeeper "**damaged** and can't be opened" false positive. First open may still need
|
||||
right-click → Open (not notarized unless \`MAC_CODESIGN_IDENTITY\` + Apple Developer ID).
|
||||
|
||||
## Checksums
|
||||
|
||||
|
||||
+149
-18
@@ -1,23 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Deep-sign a peardock .app so macOS Gatekeeper does not report
|
||||
* Codesign peardock macOS artifacts so Gatekeeper does not report
|
||||
* "is damaged and can't be opened. You should move it to the Trash."
|
||||
*
|
||||
* That message almost always means an *invalid* signature (Electron binary
|
||||
* still has a partial linker-signed ad-hoc stamp after packager rewrites the
|
||||
* bundle, with no sealed resources).
|
||||
* Targets:
|
||||
* - Electron .app bundles (deep sign / nested helpers)
|
||||
* - Standalone peardock-server Mach-O binaries (Bare)
|
||||
*
|
||||
* That message almost always means an *invalid* signature (linker ad-hoc
|
||||
* stamp after packager/embed rewrites the binary, or unsigned download).
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/sign-macos-app.cjs path/to/peardock.app
|
||||
* node scripts/sign-macos-app.cjs path/to/out/peardock-darwin-arm64
|
||||
* node scripts/sign-macos-app.cjs path/to/out/peardock-server-darwin-arm64
|
||||
* node scripts/sign-macos-app.cjs path/to/peardock-server
|
||||
*
|
||||
* Identity (first match wins):
|
||||
* MAC_CODESIGN_IDENTITY / CSC_NAME — "Developer ID Application: …" or team identity
|
||||
* otherwise ad-hoc (`-`) which is enough to make the app *valid* (not "damaged")
|
||||
* otherwise ad-hoc (`-`) which is enough to make the artifact *valid* (not "damaged")
|
||||
*
|
||||
* Tools:
|
||||
* macOS: /usr/bin/codesign (required for production identities)
|
||||
* Linux CI: rcodesign (apple-codesign) for ad-hoc when present
|
||||
* Linux CI: rcodesign (apple-codesign) for self-signed seal when present
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
@@ -26,6 +31,7 @@ const path = require('path')
|
||||
const { spawnSync, execFileSync } = require('child_process')
|
||||
|
||||
const ENTITLEMENTS = path.join(__dirname, 'entitlements.mac.plist')
|
||||
const SERVER_BIN_NAMES = new Set(['peardock-server', 'peardock-server.exe'])
|
||||
|
||||
function log(...a) {
|
||||
console.log('[sign-macos]', ...a)
|
||||
@@ -44,6 +50,50 @@ function findApps(input) {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Find standalone peardock-server Mach-O binaries under a path.
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function findServerBinaries(input) {
|
||||
if (!fs.existsSync(input)) return []
|
||||
const st = fs.statSync(input)
|
||||
if (st.isFile()) {
|
||||
const base = path.basename(input)
|
||||
if (SERVER_BIN_NAMES.has(base) || base === 'peardock-server') return [input]
|
||||
// Allow explicit path to any file named like our server binary
|
||||
if (!base.endsWith('.app') && !base.endsWith('.dmg') && !base.endsWith('.pkg')) {
|
||||
// Heuristic: treat as binary if caller passed a file path that looks executable
|
||||
if (base.includes('peardock-server')) return [input]
|
||||
}
|
||||
return []
|
||||
}
|
||||
if (!st.isDirectory()) return []
|
||||
const found = []
|
||||
const stack = [input]
|
||||
while (stack.length) {
|
||||
const dir = stack.pop()
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const ent of entries) {
|
||||
const p = path.join(dir, ent.name)
|
||||
if (ent.isDirectory()) {
|
||||
if (ent.name === 'node_modules' || ent.name.endsWith('.app') || ent.name.startsWith('.')) {
|
||||
continue
|
||||
}
|
||||
stack.push(p)
|
||||
} else if (ent.isFile() && SERVER_BIN_NAMES.has(ent.name)) {
|
||||
found.push(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function which(cmd) {
|
||||
try {
|
||||
const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], {
|
||||
@@ -281,12 +331,16 @@ function ensureRcodesignSelfSignedP12(bin) {
|
||||
return { p12Path, password }
|
||||
}
|
||||
|
||||
function codesignRcodesign(appPath, id) {
|
||||
/**
|
||||
* Sign a path (`.app` or standalone Mach-O) with rcodesign on Linux CI.
|
||||
* Uses a self-signed p12 (not Developer ID) so the signature is *valid/sealed*.
|
||||
*/
|
||||
function codesignRcodesign(targetPath, id) {
|
||||
const bin = which('rcodesign')
|
||||
if (!bin) {
|
||||
throw new Error(
|
||||
'rcodesign not found (needed to sign macOS .app on Linux). ' +
|
||||
'CI installs tools/rcodesign/<host>/rcodesign, or package darwin clients on macOS.'
|
||||
'rcodesign not found (needed to sign macOS artifacts on Linux). ' +
|
||||
'CI installs tools/rcodesign/<host>/rcodesign, or build/sign on macOS.'
|
||||
)
|
||||
}
|
||||
if (id !== '-') {
|
||||
@@ -299,7 +353,7 @@ function codesignRcodesign(appPath, id) {
|
||||
const entitlements = ensureEntitlements()
|
||||
const { p12Path, password } = ensureRcodesignSelfSignedP12(bin)
|
||||
|
||||
// Prefer full Electron-friendly sign: p12 + runtime + entitlements
|
||||
// Prefer full Electron/Bare-friendly sign: p12 + runtime + entitlements
|
||||
const attempts = [
|
||||
[
|
||||
'sign',
|
||||
@@ -311,7 +365,7 @@ function codesignRcodesign(appPath, id) {
|
||||
'runtime',
|
||||
'--entitlements-xml-file',
|
||||
entitlements,
|
||||
appPath,
|
||||
targetPath,
|
||||
],
|
||||
[
|
||||
'sign',
|
||||
@@ -321,9 +375,9 @@ function codesignRcodesign(appPath, id) {
|
||||
password,
|
||||
'--code-signature-flags',
|
||||
'runtime',
|
||||
appPath,
|
||||
targetPath,
|
||||
],
|
||||
['sign', '--p12-file', p12Path, '--p12-password', password, appPath],
|
||||
['sign', '--p12-file', p12Path, '--p12-password', password, targetPath],
|
||||
]
|
||||
|
||||
let lastErr = ''
|
||||
@@ -343,6 +397,68 @@ function codesignRcodesign(appPath, id) {
|
||||
throw new Error(`rcodesign failed:\n${lastErr}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a standalone Mach-O binary (peardock-server) with Apple codesign.
|
||||
*/
|
||||
function codesignDarwinBinary(binPath, id) {
|
||||
const entitlements = ensureEntitlements()
|
||||
const hardened = id !== '-'
|
||||
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', binPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
log(`codesign binary identity=${id === '-' ? 'ad-hoc' : id}`)
|
||||
const args = ['--force', '--sign', id, '--entitlements', entitlements]
|
||||
if (hardened) args.push('--options', 'runtime', '--timestamp')
|
||||
else args.push('--timestamp=none')
|
||||
// Stable identifier (bare-runtime defaults to peardock-server-<hash>)
|
||||
args.push('--identifier', 'com.peardock.server', binPath)
|
||||
|
||||
const r = spawnSync('codesign', args, { encoding: 'utf8' })
|
||||
if (r.status !== 0) {
|
||||
throw new Error(`codesign failed for binary:\n${r.stderr || r.stdout}`)
|
||||
}
|
||||
|
||||
const v = spawnSync('codesign', ['--verify', '--strict', '--verbose=2', binPath], {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (v.status !== 0) {
|
||||
throw new Error(`codesign verify failed for binary:\n${v.stderr || v.stdout}`)
|
||||
}
|
||||
log('verify ok:', (v.stderr || v.stdout || '').trim().split('\n').slice(0, 3).join(' | '))
|
||||
}
|
||||
|
||||
/**
|
||||
* Codesign a standalone peardock-server binary (Bare).
|
||||
* Call after bare-standalone embeds the runtime for each darwin host.
|
||||
*
|
||||
* @param {string} binPath
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function signBinary(binPath) {
|
||||
if (!fs.existsSync(binPath)) throw new Error(`Binary not found: ${binPath}`)
|
||||
const id = identity()
|
||||
log('binary:', binPath)
|
||||
log('identity:', id === '-' ? 'ad-hoc (-)' : id)
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
codesignDarwinBinary(binPath, id)
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', binPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// Linux CI cross-compile of darwin server binaries
|
||||
codesignRcodesign(binPath, id)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function signApp(appPath) {
|
||||
if (!fs.existsSync(appPath)) throw new Error(`App not found: ${appPath}`)
|
||||
const id = identity()
|
||||
@@ -412,22 +528,37 @@ function signApp(appPath) {
|
||||
async function main() {
|
||||
const input = process.argv[2]
|
||||
if (!input) {
|
||||
console.error('Usage: node scripts/sign-macos-app.cjs <path-to.app|dir>')
|
||||
console.error(
|
||||
'Usage: node scripts/sign-macos-app.cjs <path-to.app|peardock-server|dir>'
|
||||
)
|
||||
process.exit(2)
|
||||
}
|
||||
const resolved = path.resolve(input)
|
||||
const apps = findApps(resolved)
|
||||
if (!apps.length) {
|
||||
console.error('No .app found at', resolved)
|
||||
if (!fs.existsSync(resolved)) {
|
||||
console.error('Path not found:', resolved)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const apps = findApps(resolved)
|
||||
const bins = findServerBinaries(resolved)
|
||||
|
||||
if (!apps.length && !bins.length) {
|
||||
console.error('No .app or peardock-server binary found at', resolved)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
for (const app of apps) {
|
||||
await signApp(app)
|
||||
}
|
||||
for (const bin of bins) {
|
||||
// Avoid double-signing if someone nested a binary inside an .app (unlikely)
|
||||
if (apps.some((a) => bin === a || bin.startsWith(a + path.sep))) continue
|
||||
await signBinary(bin)
|
||||
}
|
||||
log('done')
|
||||
}
|
||||
|
||||
module.exports = { signApp, findApps, identity }
|
||||
module.exports = { signApp, signBinary, findApps, findServerBinaries, identity }
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((err) => {
|
||||
|
||||
Reference in New Issue
Block a user