mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
fix: ad-hoc sign packaged macOS key helpers and app bundle
electron-builder 26 skips macOS signing entirely when no Developer ID identity is configured, so the packaged key helpers can ship unsigned or with a broken signature and macOS kills them even with SIP disabled. afterPack now verifies and ad-hoc re-signs the packaged xkey helpers and the outer app bundle, failing the build when a signature cannot be repaired.
This commit is contained in:
Binary file not shown.
Binary file not shown.
+76
-2
@@ -16,6 +16,15 @@ const REQUIRED_RUNTIME_PACKAGES = [
|
||||
'koffi'
|
||||
]
|
||||
|
||||
// electron-builder 26 skips macOS signing entirely when no Developer ID
|
||||
// identity is configured, so an unpacked bundle can ship without a usable
|
||||
// signature. macOS kills a helper whose code or signature is missing or
|
||||
// modified even when SIP is disabled, which is what customers hit on newer
|
||||
// macOS releases. Ad-hoc re-sign the runtime helpers and the outer bundle so
|
||||
// every Mach-O verifies strictly; spctl still rejects ad-hoc code, which is
|
||||
// acceptable for the SIP-disabled customer workflow.
|
||||
const MACOS_HELPER_NAMES = ['xkey_helper', 'xkey_helper_4_1_13']
|
||||
|
||||
function getRuntimeResources(context) {
|
||||
const productName = context.packager.appInfo.productFilename
|
||||
return context.electronPlatformName === 'darwin'
|
||||
@@ -121,6 +130,63 @@ function normalizeBuilderArch(arch) {
|
||||
return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch)
|
||||
}
|
||||
|
||||
function runCodesign(args) {
|
||||
execFileSync('/usr/bin/codesign', args, { stdio: 'ignore' })
|
||||
}
|
||||
|
||||
function isMacosCodeValid(targetPath, run = runCodesign) {
|
||||
try {
|
||||
run(['--verify', '--strict', targetPath])
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function findMacosHelperPaths(runtimeResources) {
|
||||
return MACOS_HELPER_NAMES.map((name) => path.join(runtimeResources, 'resources', name)).filter(
|
||||
(helperPath) => existsSync(helperPath)
|
||||
)
|
||||
}
|
||||
|
||||
function signMacosHelpers(runtimeResources, run = runCodesign) {
|
||||
const helperPaths = findMacosHelperPaths(runtimeResources)
|
||||
for (const helperPath of helperPaths) {
|
||||
chmodSync(helperPath, 0o755)
|
||||
if (!isMacosCodeValid(helperPath, run)) {
|
||||
run(['--force', '--sign', '-', helperPath])
|
||||
}
|
||||
for (const arch of ['arm64', 'x86_64']) {
|
||||
try {
|
||||
run(['--verify', '--strict', '--arch', arch, helperPath])
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'macOS helper signature verification failed: ' +
|
||||
path.basename(helperPath) +
|
||||
' (' +
|
||||
arch +
|
||||
')',
|
||||
{ cause: error }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return helperPaths
|
||||
}
|
||||
|
||||
function signMacosAppBundle(appBundlePath, run = runCodesign) {
|
||||
if (isMacosCodeValid(appBundlePath, run)) return appBundlePath
|
||||
run(['--force', '--sign', '-', appBundlePath])
|
||||
try {
|
||||
run(['--verify', '--strict', appBundlePath])
|
||||
} catch (error) {
|
||||
throw new Error('macOS app bundle signature verification failed: ' + appBundlePath, {
|
||||
cause: error
|
||||
})
|
||||
}
|
||||
return appBundlePath
|
||||
}
|
||||
|
||||
/**
|
||||
* A foreign-architecture binary only fails once the user touches the feature
|
||||
* that needs it, so verify the ones whose filename is shared across
|
||||
@@ -226,7 +292,9 @@ function pruneForeignArchNativeRuntimes(runtimeResources, platform, arch) {
|
||||
for (const entry of readdirSync(modulesRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || entry.name === expected || !foreign.test(entry.name)) continue
|
||||
rmSync(path.join(modulesRoot, entry.name), { recursive: true, force: true })
|
||||
removed.push(runtime.modules.length ? `${runtime.modules.join('/')}/${entry.name}` : entry.name)
|
||||
removed.push(
|
||||
runtime.modules.length ? `${runtime.modules.join('/')}/${entry.name}` : entry.name
|
||||
)
|
||||
}
|
||||
}
|
||||
return removed
|
||||
@@ -280,6 +348,9 @@ exports.default = async function afterPack(context) {
|
||||
execFileSync('/usr/bin/codesign', ['--force', '--sign', '-', ffmpegPath], {
|
||||
stdio: 'ignore'
|
||||
})
|
||||
signMacosHelpers(runtimeResources)
|
||||
const productName = context.packager.appInfo.productFilename
|
||||
signMacosAppBundle(path.join(context.appOutDir, productName + '.app'))
|
||||
}
|
||||
|
||||
if (context.electronPlatformName === 'win32') {
|
||||
@@ -298,7 +369,6 @@ exports.default = async function afterPack(context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.getRuntimeResources = getRuntimeResources
|
||||
@@ -312,3 +382,7 @@ exports.pruneIntelMacKeyTool = pruneIntelMacKeyTool
|
||||
exports.pruneForeignArchConnectors = pruneForeignArchConnectors
|
||||
exports.pruneForeignArchNativeRuntimes = pruneForeignArchNativeRuntimes
|
||||
exports.validateRuntimeBinaryArchitecture = validateRuntimeBinaryArchitecture
|
||||
exports.findMacosHelperPaths = findMacosHelperPaths
|
||||
exports.isMacosCodeValid = isMacosCodeValid
|
||||
exports.signMacosHelpers = signMacosHelpers
|
||||
exports.signMacosAppBundle = signMacosAppBundle
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { createRequire } from 'module'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync
|
||||
} from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { dirname, join, resolve } from 'path'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
@@ -16,13 +24,37 @@ const {
|
||||
validateFfmpegRuntime,
|
||||
validateReaderSkillRuntime,
|
||||
validateSherpaRuntime,
|
||||
validateSilkWasmRuntime
|
||||
validateSilkWasmRuntime,
|
||||
findMacosHelperPaths,
|
||||
isMacosCodeValid,
|
||||
signMacosHelpers,
|
||||
signMacosAppBundle
|
||||
} = nodeRequire('../../scripts/after-pack.cjs') as {
|
||||
validateAsarRuntimeDependencies: (runtimeResources: string) => void
|
||||
validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void
|
||||
validateReaderSkillRuntime: (runtimeResources: string) => string
|
||||
validateSherpaRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void
|
||||
validateSilkWasmRuntime: (runtimeResources: string) => void
|
||||
findMacosHelperPaths: (runtimeResources: string) => string[]
|
||||
isMacosCodeValid: (targetPath: string, run?: CodesignRunner) => boolean
|
||||
signMacosHelpers: (runtimeResources: string, run?: CodesignRunner) => string[]
|
||||
signMacosAppBundle: (appBundlePath: string, run?: CodesignRunner) => string
|
||||
}
|
||||
|
||||
type CodesignRunner = (args: string[]) => void
|
||||
|
||||
function createCodesignStub(options: { verifyFails?: (args: string[]) => boolean } = {}): {
|
||||
calls: string[][]
|
||||
run: CodesignRunner
|
||||
} {
|
||||
const calls: string[][] = []
|
||||
const run: CodesignRunner = (args) => {
|
||||
calls.push(args)
|
||||
if (options.verifyFails && args[0] === '--verify' && options.verifyFails(args)) {
|
||||
throw new Error('code object is not signed at all')
|
||||
}
|
||||
}
|
||||
return { calls, run }
|
||||
}
|
||||
const root = mkdtempSync(join(tmpdir(), 'wxe-runtime-package-'))
|
||||
|
||||
@@ -217,6 +249,96 @@ describe('production runtime packaging', () => {
|
||||
expect(config).toContain('node_modules/sherpa-onnx-node/**')
|
||||
expect(config).toContain('node_modules/sherpa-onnx-*/**')
|
||||
})
|
||||
|
||||
it('finds only the macOS helpers that exist in packaged resources', () => {
|
||||
const resources = join(root, 'helper-detect-resources', 'resources')
|
||||
mkdirSync(resources, { recursive: true })
|
||||
expect(findMacosHelperPaths(join(root, 'helper-detect-resources'))).toEqual([])
|
||||
|
||||
const helperPath = join(resources, 'xkey_helper')
|
||||
writeFileSync(helperPath, 'fixture')
|
||||
const versionedPath = join(resources, 'xkey_helper_4_1_13')
|
||||
writeFileSync(versionedPath, 'fixture')
|
||||
expect(findMacosHelperPaths(join(root, 'helper-detect-resources'))).toEqual([
|
||||
helperPath,
|
||||
versionedPath
|
||||
])
|
||||
})
|
||||
|
||||
it('ad-hoc signs packaged helpers whose signature is missing or modified', () => {
|
||||
const resources = join(root, 'helper-sign-resources', 'resources')
|
||||
mkdirSync(resources, { recursive: true })
|
||||
const helperPath = join(resources, 'xkey_helper')
|
||||
writeFileSync(helperPath, 'fixture')
|
||||
const stub = createCodesignStub({ verifyFails: (args) => !args.includes('--arch') })
|
||||
|
||||
expect(signMacosHelpers(join(root, 'helper-sign-resources'), stub.run)).toEqual([helperPath])
|
||||
expect(stub.calls).toContainEqual(['--force', '--sign', '-', helperPath])
|
||||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'arm64', helperPath])
|
||||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'x86_64', helperPath])
|
||||
expect(statSync(helperPath).mode & 0o777).toBe(0o755)
|
||||
})
|
||||
|
||||
it('keeps helpers that already verify strictly without re-signing them', () => {
|
||||
const resources = join(root, 'helper-valid-resources', 'resources')
|
||||
mkdirSync(resources, { recursive: true })
|
||||
const helperPath = join(resources, 'xkey_helper')
|
||||
writeFileSync(helperPath, 'fixture')
|
||||
const stub = createCodesignStub()
|
||||
|
||||
expect(signMacosHelpers(join(root, 'helper-valid-resources'), stub.run)).toEqual([helperPath])
|
||||
expect(stub.calls.filter((args) => args[0] === '--force')).toEqual([])
|
||||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'arm64', helperPath])
|
||||
expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'x86_64', helperPath])
|
||||
})
|
||||
|
||||
it('fails packaging when a helper signature cannot be repaired', () => {
|
||||
const resources = join(root, 'helper-broken-resources', 'resources')
|
||||
mkdirSync(resources, { recursive: true })
|
||||
writeFileSync(join(resources, 'xkey_helper'), 'fixture')
|
||||
const stub = createCodesignStub({ verifyFails: () => true })
|
||||
|
||||
expect(() => signMacosHelpers(join(root, 'helper-broken-resources'), stub.run)).toThrow(
|
||||
/xkey_helper \(arm64\)/
|
||||
)
|
||||
})
|
||||
|
||||
it('ad-hoc signs an invalid app bundle and verifies it strictly', () => {
|
||||
const appBundle = join(root, 'TraceMemo.app')
|
||||
const calls: string[][] = []
|
||||
let verifyCount = 0
|
||||
const run: CodesignRunner = (args) => {
|
||||
calls.push(args)
|
||||
if (args[0] === '--verify') {
|
||||
verifyCount += 1
|
||||
if (verifyCount === 1) throw new Error('bundle is not signed')
|
||||
}
|
||||
}
|
||||
|
||||
expect(signMacosAppBundle(appBundle, run)).toBe(appBundle)
|
||||
expect(calls).toEqual([
|
||||
['--verify', '--strict', appBundle],
|
||||
['--force', '--sign', '-', appBundle],
|
||||
['--verify', '--strict', appBundle]
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps an app bundle that already verifies strictly', () => {
|
||||
const appBundle = join(root, 'Valid.app')
|
||||
const stub = createCodesignStub()
|
||||
|
||||
expect(signMacosAppBundle(appBundle, stub.run)).toBe(appBundle)
|
||||
expect(stub.calls).toEqual([['--verify', '--strict', appBundle]])
|
||||
})
|
||||
|
||||
it('fails packaging when the app bundle cannot be made strictly valid', () => {
|
||||
const appBundle = join(root, 'Broken.app')
|
||||
const stub = createCodesignStub({ verifyFails: () => true })
|
||||
|
||||
expect(() => signMacosAppBundle(appBundle, stub.run)).toThrow(
|
||||
/app bundle signature verification failed/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('per-architecture macOS packaging', () => {
|
||||
|
||||
Reference in New Issue
Block a user