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:
@@ -122,9 +122,9 @@ jobs:
|
||||
echo "==> npm ci done (electron binary deferred to forge package step)"
|
||||
node -e "console.log('node', process.version); console.log('esbuild', require('esbuild').version)"
|
||||
|
||||
# Ad-hoc codesign for darwin .app bundles produced on Linux (fixes Gatekeeper "damaged").
|
||||
# Codesign for darwin .app + peardock-server binaries on Linux (fixes Gatekeeper "damaged").
|
||||
# Binary is vendored under tools/rcodesign/ (no GitHub download — runners often stall there).
|
||||
- name: Install rcodesign (macOS ad-hoc signing on Linux)
|
||||
- name: Install rcodesign (macOS codesign on Linux)
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -227,7 +227,7 @@ Asset patterns:
|
||||
- `peardock-server-VERSION-HOST.tar.gz` (+ `.sha256`)
|
||||
- `peardock-client-VERSION-HOST.tar.gz` (+ `.sha256`; macOS includes a `.app`)
|
||||
|
||||
macOS clients from Linux CI are ad-hoc signed. If Gatekeeper blocks: right-click → Open, or `xattr -cr ~/Applications/peardock.app`. Re-sign: `npm run sign:macos -- out/peardock-darwin-arm64/peardock.app`.
|
||||
macOS client (`.app`) and server (`peardock-server`) binaries from CI are codesigned (avoids Gatekeeper “damaged”). If Gatekeeper still blocks: right-click → Open, or `xattr -cr <path>`. Re-sign: `npm run sign:macos -- out/peardock-darwin-arm64/peardock.app` or `…/peardock-server-darwin-arm64/peardock-server`.
|
||||
|
||||
---
|
||||
|
||||
@@ -243,7 +243,7 @@ macOS clients from Linux CI are ad-hoc signed. If Gatekeeper blocks: right-click
|
||||
| `npm run soak` / `soak:24h` | Certification soak against Docker |
|
||||
| `npm run make` | Cross-build all server + client hosts |
|
||||
| `npm run make:server` / `make:client` | Server-only or client-only builds |
|
||||
| `npm run sign:macos` | Ad-hoc / identity re-sign for `.app` |
|
||||
| `npm run sign:macos` | Ad-hoc / identity re-sign for `.app` or `peardock-server` |
|
||||
| `npm run release:checksums` | Tarball + SHA-256 (+ optional GPG) |
|
||||
| `npm run release:rolling` | Local dry-run helper for rolling publish |
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ v1 used Hyperswarm topics + ad-hoc JSON streams and is **not** wire-compatible.
|
||||
| Rate limit errors | Middleware | `server/utils/rateLimiter.js` |
|
||||
| Tunnel create fails | Holesail flag / SSRF | `ENABLE_HOLESAIL`; `PEARDOCK_TUNNEL_HOSTS` |
|
||||
| Swarm empty / error | Swarm mode / flag | Engine swarm state; `ENABLE_SWARM` |
|
||||
| macOS “damaged” app | Gatekeeper | `xattr -cr`; ad-hoc sign docs |
|
||||
| macOS “damaged” app/binary | Gatekeeper | codesign in CI (`sign-macos-app.cjs`); `xattr -cr` / right-click Open |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ Build: `npm run build:client-bundle` → `scripts/build-client-bundle.cjs`.
|
||||
| `build-client-bundle.cjs` | esbuild client bundle |
|
||||
| `gitea-rolling-release.sh` | CI publish to `rolling` |
|
||||
| `hosts.cjs` | Canonical 64-bit host list |
|
||||
| `sign-macos-app.cjs` | Ad-hoc / identity sign |
|
||||
| `sign-macos-app.cjs` | Ad-hoc / identity sign for `.app` + `peardock-server` |
|
||||
| `healthcheck.js` | Docker socket probe |
|
||||
| `soak.js` | Long-run certification |
|
||||
| `release-checksums.sh` | Checksums / optional GPG |
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ Admin-defined recurring maintenance (e.g. prune) persisted and restored at boot.
|
||||
| 6×64-bit client hosts | Electron forge |
|
||||
| rolling tag | Continuous release channel |
|
||||
| install.sh | Server systemd + client install |
|
||||
| macOS ad-hoc sign | Gatekeeper-friendly CI builds |
|
||||
| macOS codesign (client + server) | Gatekeeper-friendly CI builds (avoids “damaged”) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+7
-5
@@ -8,8 +8,8 @@ No 32-bit (`ia32` / `armv7`) builds.
|
||||
|------|---------------|-------------------|
|
||||
| `linux-x64` | yes (cross) | yes (cross via electron-forge) |
|
||||
| `linux-arm64` | yes | yes |
|
||||
| `darwin-x64` | yes | yes (unsigned / ad-hoc from Linux CI) |
|
||||
| `darwin-arm64` | yes | yes (unsigned / ad-hoc from Linux CI) |
|
||||
| `darwin-x64` | yes (codesigned in CI) | yes (codesigned in CI) |
|
||||
| `darwin-arm64` | yes (codesigned in CI) | yes (codesigned in CI) |
|
||||
| `win32-x64` | yes | yes |
|
||||
| `win32-arm64` | yes | yes |
|
||||
|
||||
@@ -96,9 +96,11 @@ Published assets are consumed by:
|
||||
|
||||
### Notes
|
||||
|
||||
- **macOS clients** are **ad-hoc codesigned** after package (`scripts/sign-macos-app.cjs`) so Gatekeeper does not show “damaged / move to Trash”. CI installs `rcodesign` to ad-hoc sign darwin `.app`s built on Linux. For Developer ID + notarization, set `MAC_CODESIGN_IDENTITY` / `CSC_NAME` on a Mac (or import certs in CI).
|
||||
- Re-sign a local/downloaded build: `npm run sign:macos -- out/peardock-darwin-arm64/peardock.app`
|
||||
- If the app is still blocked: `xattr -cr ~/Applications/peardock.app` or right-click → Open.
|
||||
- **macOS clients** (`.app`) and **peardock-server** binaries are **codesigned** after build (`scripts/sign-macos-app.cjs`) so Gatekeeper does not show “damaged / move to Trash”. Server signing runs inside `bare-standalone.cjs` for every `darwin-*` host; client signing runs in Electron Forge `postPackage`. CI installs vendored `rcodesign` to seal darwin artifacts built on Linux. For Developer ID + notarization, set `MAC_CODESIGN_IDENTITY` / `CSC_NAME` on a Mac (or import certs in CI).
|
||||
- Re-sign a local/downloaded build:
|
||||
- Client: `npm run sign:macos -- out/peardock-darwin-arm64/peardock.app`
|
||||
- Server: `npm run sign:macos -- out/peardock-server-darwin-arm64/peardock-server`
|
||||
- If still blocked: `xattr -cr <path>` or right-click → Open. Skip signing with `PEARDOCK_SKIP_MACOS_SIGN=1` (not recommended for release).
|
||||
- **Native modules** must ship prebuilds for each target (Holepunch stack does). Rebuild-from-source is disabled for cross packages (`npm_config_build_from_source=false`).
|
||||
- **Server** cross-compile uses bare-runtime platform prebuilds (no Docker-in-Docker required to *build*; runtime still needs a Docker socket).
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# Vendored `rcodesign` (apple-codesign)
|
||||
|
||||
Used by CI on Linux to **ad-hoc sign** macOS `.app` bundles produced by
|
||||
electron-forge cross-packaging (avoids Gatekeeper “damaged / move to Trash”).
|
||||
Used by CI on Linux to **seal-sign** macOS artifacts produced on Linux:
|
||||
|
||||
- Electron `.app` bundles (forge `postPackage` → `scripts/sign-macos-app.cjs`)
|
||||
- Bare `peardock-server` binaries (`scripts/bare-standalone.cjs` for every `darwin-*` host)
|
||||
|
||||
so Gatekeeper does not report them as “damaged / move to Trash”.
|
||||
|
||||
| Path | Binary |
|
||||
|------|--------|
|
||||
|
||||
Reference in New Issue
Block a user