Electron macOS 打包签名公证踩坑实录
从"文件已损坏"到成功上架,记录我在 Electron 应用 macOS 签名公证过程中遇到的所有坑。
背景
最近开发了一个 Electron 桌面应用(AgentOS/Kooky),打包 macOS 版本时遇到了一系列签名和公证问题。从最初的"安装包提示文件已损坏",到最终成功完成公证,折腾了整整两天。把这些坑记录下来,希望能帮到遇到类似问题的同学。
一、什么是签名和公证?
在 macOS 上分发应用(非 App Store),需要完成两步:
| 步骤 | 作用 | 谁执行 |
|---|---|---|
| 签名 (Code Signing) | 证明应用来源可信,未被篡改 | 开发者用 Developer ID 证书签名 |
| 公证 (Notarization) | Apple 扫描应用确认无恶意代码 | Apple 公证服务 |
未签名或未公证的应用,用户打开时会提示"文件已损坏"或"无法验证开发者"。
二、配置流程
1. 获取 Developer ID 证书
登录 Apple Developer,申请 Developer ID Application 证书。下载后导入到钥匙串:
# 检查证书是否可用
security find-identity -v -p codesigning
期望输出:
1) XXXXXXXX... "Developer ID Application: Your Name (TEAM_ID)"
1 valid identities found
2. electron-builder 配置
{
"mac": {
"target": [{ "target": "dmg", "arch": ["arm64", "x64"] }],
"identity": "Developer ID Application: Your Name (TEAM_ID)",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"notarize": {
"teamId": "TEAM_ID"
}
}
}
3. Entitlements 配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
4. 公证环境变量
export APPLE_ID="your@email.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx" # appleid.apple.com 生成
export APPLE_TEAM_ID="YOUR_TEAM_ID"
三、踩坑实录
坑 1:安装包提示"文件已损坏"
现象:打包后的 dmg 安装到 macOS 上,打开时提示"文件已损坏,您应该将它移到废纸篓"。
排查:
codesign -dv --verbose=4 dist-electron/mac/YourApp.app
发现:
Signature=adhoc
TeamIdentifier=not set
原因:应用没有被开发者证书签名,只有系统默认的 ad-hoc 签名。electron-builder 在找不到有效证书时会静默跳过签名,不报错!
解决:检查证书有效性:
security find-identity -v -p codesigning
如果显示 0 valid identities found,说明证书无效。
坑 2:证书不受信任 (CSSMERR_TP_NOT_TRUSTED)
现象:
1) XXXX... "Developer ID Application: Your Name (TEAM_ID)" (CSSMERR_TP_NOT_TRUSTED)
0 valid identities found
证书存在但状态为不受信任。
原因:钥匙串中缺少 Apple 中间证书,证书链不完整。
解决:安装 Apple 中间证书:
# 下载 Developer ID G2 中间证书
curl -O https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
# 安装到系统钥匙串
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain DeveloperIDG2CA.cer
坑 3:签名报错 errSecInternalComponent
现象:
Warning: unable to build chain to self-signed root for signer
"Developer ID Application: Your Name (TEAM_ID)"
xxx: errSecInternalComponent
任何文件用 Developer ID 证书签名都失败,但 ad-hoc 签名正常。
排查过程:
- 证书存在且有效 ✅
- 中间证书已安装 ✅
- Apple Root CA 存在 ✅
- Keychain 已解锁 ✅
- 重新导入证书 ❌ 无效
- 新建 keychain 测试 ❌ 无效
根因:macOS trustd 服务的信任评估缓存损坏!
某些软件(如深信服 EasyConnect VPN、ESET 杀毒软件)会修改系统证书信任设置,污染了 trustd 的缓存。
解决:
# 清除 trustd 缓存
sudo rm -rf /private/var/protected/trustd
# 重启 trustd 服务
sudo killall trustd
# 等待几秒后重试
sleep 3
codesign --force --options runtime --sign "Your Name (TEAM_ID)" your_binary
坑 4:公证失败 — JSON 解析错误
现象:
⨯ Unexpected token 'E', "Error: HTT"... is not valid JSON
at JSON.parse (<anonymous>)
原因:Apple 公证服务返回了 HTTP 错误页面,@electron/notarize 尝试解析 JSON 失败。
解决:检查网络连通性,重试即可:
# 测试公证服务是否可用
xcrun notarytool history \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
坑 5:文件权限错误 EACCES
现象:
EACCES: permission denied, unlink 'dist/assets/xxx.js'
原因:之前用 sudo 打包,导致输出目录文件属主为 root。
解决:
# 修复权限
sudo chown -R $(whoami) dist dist-electron
# 之后永远不要用 sudo 打包
yarn build:mac
坑 6:node-pty 原生模块签名失败
现象:打包后包含原生模块(如 node-pty)的应用,公证失败。
原因:原生模块的二进制文件也需要签名,electron-builder 默认不处理。
解决:使用 afterPack 钩子手动签名:
// build/afterPack.js
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
exports.default = async function afterPack(context) {
if (process.platform !== 'darwin') return
const appName = context.packager.appInfo.productFilename
const appOutDir = context.appOutDir
const appPath = path.join(appOutDir, `${appName}.app`)
const resourcesDir = path.join(appPath, 'Contents', 'Resources')
const identity = context.packager.platformSpecificBuildOptions.identity
const entitlements = path.resolve(__dirname, 'entitlements.mac.plist')
// 需要签名的原生模块二进制
const binaries = [
path.join(resourcesDir, 'app.asar.unpacked', 'node_modules', 'node-pty', 'prebuilds', 'darwin-arm64', 'spawn-helper'),
path.join(resourcesDir, 'app.asar.unpacked', 'node_modules', 'node-pty', 'prebuilds', 'darwin-arm64', 'pty.node'),
]
for (const binary of binaries) {
if (!fs.existsSync(binary)) continue
fs.chmodSync(binary, 0o755)
const cmd = `codesign --force --options runtime --sign "${identity}" --entitlements "${entitlements}" "${binary}"`
execSync(cmd, { stdio: 'inherit' })
}
}
四、验证命令
打包完成后,用这些命令验证:
# 1. 检查签名
codesign -dv --verbose=4 YourApp.app
# 期望:Signature 不是 adhoc,TeamIdentifier 正确
# 2. Gatekeeper 验证
spctl -a -vv YourApp.app
# 期望:accepted, source=Notarized Developer ID
# 3. 公证票据验证
xcrun stapler validate YourApp.app
# 期望:The validate action worked!
# 4. DMG 签名验证
codesign -dv YourApp.dmg
五、常见问题速查表
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 文件已损坏 | 未签名/未公证 | 检查证书 + 公证环境变量 |
Signature=adhoc | 证书无效 | 修复证书信任链 |
CSSMERR_TP_NOT_TRUSTED | 缺中间证书 | 安装 DeveloperIDG2CA.cer |
errSecInternalComponent | trustd 缓存损坏 | sudo rm -rf /private/var/protected/trustd |
| 公证 JSON 错误 | 网络问题 | 检查网络,重试 |
| EACCES 权限错误 | 曾用 sudo 打包 | sudo chown -R $(whoami) dist |
| 原生模块公证失败 | 未签名原生二进制 | afterPack 钩子手动签名 |
六、用户临时绕过方案
如果发布前无法完成公证,可告知用户:
xattr -cr /Applications/YourApp.app
这会移除 macOS 隔离属性,允许打开未公证应用。仅临时方案,正式发布必须公证。
总结
Electron macOS 签名公证的坑主要集中在:
- 证书链问题:缺少中间证书导致信任链断裂
- 系统缓存损坏:VPN/安全软件污染 trustd 缓存
- 权限问题:错误使用 sudo
- 原生模块:需要手动签名
把这些坑踩完后,打包流程就顺畅了。希望这篇文章能帮你少走弯路!