Electron macOS 打包签名公证踩坑实录

7 阅读5分钟

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
errSecInternalComponenttrustd 缓存损坏sudo rm -rf /private/var/protected/trustd
公证 JSON 错误网络问题检查网络,重试
EACCES 权限错误曾用 sudo 打包sudo chown -R $(whoami) dist
原生模块公证失败未签名原生二进制afterPack 钩子手动签名

六、用户临时绕过方案

如果发布前无法完成公证,可告知用户:

xattr -cr /Applications/YourApp.app

这会移除 macOS 隔离属性,允许打开未公证应用。仅临时方案,正式发布必须公证。


总结

Electron macOS 签名公证的坑主要集中在:

  1. 证书链问题:缺少中间证书导致信任链断裂
  2. 系统缓存损坏:VPN/安全软件污染 trustd 缓存
  3. 权限问题:错误使用 sudo
  4. 原生模块:需要手动签名

把这些坑踩完后,打包流程就顺畅了。希望这篇文章能帮你少走弯路!


参考资料