首页
/ Cypress 二进制代码签名实战:macOS 与 Windows 证书轮换及 electron-builder 签名链路解析

Cypress 二进制代码签名实战:macOS 与 Windows 证书轮换及 electron-builder 签名链路解析

2026-09-05 16:06:39作者:董斯意

Cypress 在 CI 中构建 Windows 与 Mac 发行版时会执行代码签名,签名的具体工作由 electron-buildercreate-build-artifacts 构建任务中完成。本文基于仓库内的 code-signing 指南 展开,完整覆盖 Mac(Apple Developer ID 证书)与 Windows(SSL.com 证书)两条签名密钥的轮换流程,并结合 electron-builder.jsonwindows-sign.js 等仓库源码,深入讲解签名配置、公证(notarize)与远程签名委托的实际实现,读完后可理解 Cypress 发布产物"签名 → 公证 → 校验"的完整链路及其安全设计。

何时进行代码签名:CI 构建流程中的定位

code-signing 指南 开宗明义:Cypress 的 Windows 和 Mac 发行版在 CI 构建时执行代码签名,签名动作由 electron-buildercreate-build-artifacts 任务中承担。指南同时声明了一个适用前提:读者应已熟悉 electron-builder 官方 Code Signing 文档中关于 CSC_LINKCSC_KEY_PASSWORD 等环境变量的一般约定——下文正是在这个前提下,把 Cypress 仓库中"证书如何申请、密钥如何存放、构建时如何消费"三件事讲透。

从源码结构看,这条签名链路的调用入口是 scripts/binary/build.ts 中的 electronBuilder.build(...) 调用:

await electronBuilder.build({
  publish: 'never',
  config: {
    electronVersion,
    directories: {
      app: appFolder,
      output: outputFolder,
    },
    icon: iconFilename,
    // for now we cannot pack source files in asar file
    // because electron-builder does not copy nested folders
    // from packages/*/node_modules
    asar: false,
  },
})

根目录 package.json 中声明的依赖为 "electron-builder": "^25.1.8",因此本文所述的签名行为均针对该版本的 electron-builder API。构建流程中签名并非孤立步骤,而是嵌在如下顺序里:

  1. lerna run build / lerna run build-prod 构建各 package(见 build.ts);
  2. 把各 package 的产物复制到 dist 目录;
  3. electronBuilder.build(...) 打包并签名(Windows 走委托脚本,macOS 走 CSC_LINK 证书自动签名);
  4. 触发 electron-builder.json 中声明的 afterPack / afterSign 钩子完成后续加工与公证。

buildCypressApp 的选项接口中还暴露了 skipSigning?: boolean(见 build.ts),允许本地构建时跳过签名——签名失败时若设置了该选项会吞掉异常(build.ts),这为"本地打包调试"与"CI 正式发布"两种场景做了区分。

electron-builder.json:签名相关配置一览

仓库根目录的 electron-builder.json 是签名行为的配置中枢,关键项如下:

{
  "productName": "Cypress",
  "appId": "com.electron.cypress",
  "mac": {
    "target": "zip",
    "forceCodeSigning": true,
    "hardenedRuntime": true,
    "entitlements": "./scripts/entitlements.mac.inherit.plist",
    "entitlementsInherit": "./scripts/entitlements.mac.inherit.plist",
    "type": "distribution"
  },
  "win": {
    "signingHashAlgorithms": ["sha256"],
    "sign": "./scripts/windows-sign.js",
    "target": "dir"
  },
  "afterPack": "./scripts/after-pack-hook.js",
  "afterSign": "./scripts/after-sign-hook.js"
}

逐项说明:

  • mac.forceCodeSigning: true:强制要求提供签名证书(即上文 CSC_LINK / CSC_KEY_PASSWORD),否则打包直接失败,保证 CI 产物不会静默产出未签名包。
  • mac.hardenedRuntime: true:启用 macOS 强化运行时,这是通过 Apple 公证的前提。
  • mac.entitlements / mac.entitlementsInherit:都指向 scripts/entitlements.mac.inherit.plist。该文件授予三项能力:
    • com.apple.security.cs.allow-jit —— 允许 JIT,Electron/V8 运行时所需;
    • com.apple.security.cs.allow-unsigned-executable-memory —— 允许未签名可执行内存;
    • com.apple.security.cs.allow-dyld-environment-variables —— 允许 dyld 环境变量,Cypress 二进制依赖环境变量注入(如云协议相关配置),从源码中大量的 process.env 消费可以推断其必要性。
  • win.signingHashAlgorithms: ["sha256"]:仅使用 SHA-256 摘要算法签名,符合当前 Windows Authenticode 要求。
  • win.sign: "./scripts/windows-sign.js":把 Windows 签名委托给自定义脚本(详见后文远程签名一节),而不是由 electron-builder 用本地证书直接签。
  • afterPack / afterSign:分别指向 after-pack-hook.jsafter-sign-hook.js。前者负责把各 package 的 node_modules 拷入产物、翻转 Electron Fuses、生成 V8 快照等打包后加工(与签名无直接关系,但会改变最终进签/进公证的对象);后者负责 macOS 公证。

轮换 macOS 签名密钥:从 Apple 证书到 CircleCI 上下文

这部分完整继承 code-signing 指南 中 "Rotating the Mac code signing key" 的 4 步流程:

  1. 在 Mac 上用 Cypress 的 Apple 开发者计划身份登录 Xcode。
  2. 按 Apple 官方"Create, export, and delete signing certificates"文档操作:
    1. 先执行 "View signing certificates" 查看现有证书;
    2. 执行 "Create a signing certificate",提示选择类型时选择 Developer ID Application(注意:用于应用分发签名的是 Developer ID Application 证书,而非 App Store 证书);
    3. 执行 "Export a signing certificate",导出时设置一个强口令,该口令之后会成为环境变量 CSC_KEY_PASSWORD
  3. 把导出的、已加密的 .p12 文件上传到 Google Drive 的 Code Signing 文件夹,并生成一个公共的直接下载链接。
  4. 在 CircleCI 的 test-runner:sign-mac-binary 上下文中,把 CSC_LINK 设为该直接下载 URL,把 CSC_KEY_PASSWORD 设为加密 p12 时使用的口令。

几点值得强调的工程细节:

  • 凭据存放采用"外部密文文件 + 下载链接"模式:私钥从不落进代码仓库或 CI 明文环境变量,CSC_LINK 只指向加密后的 .p12CSC_KEY_PASSWORD 单独存放。electron-builder 构建时会自动从 CSC_LINK 下载证书文件并用口令解密后签名。
  • 上下文按平台隔离:macOS 与 Windows 分别使用 test-runner:sign-mac-binarytest-runner:sign-windows-binary 两个独立上下文,密钥轮换互不影响,也便于按平台最小化授权。
  • 轮换时口令变更必须同步CSC_KEY_PASSWORD.p12 的加密口令是绑定的,二者不同步会导致 CI 签名阶段解密失败。

源码纵深:签名之后还要"公证"(notarize)

macOS 上仅签名并不够,还需向 Apple 提交公证。Cypress 通过 after-sign-hook.js 在 electron-builder 的 afterSign 阶段完成,逻辑如下:

  1. 平台守卫process.platform !== 'darwin' 时直接跳过(日志提示 not Mac, skipping after sign hook);
  2. 逃生开关:设置了 SKIP_NOTARIZATION 环境变量时跳过公证,便于调试构建;
  3. 定位应用:在 params.appOutDir 下查找 <productFilename>.app,找不到则抛错;appId 硬编码为 com.electron.cypress,与 electron-builder.json 中的 appId 一致;
  4. 三项必需凭据NOTARIZE_APP_APPLE_IDNOTARIZE_APP_PASSWORDNOTARIZE_APP_TEAM_ID,任一缺失立即抛错(注意公证用的是 Apple ID + App 专用密码 + Team ID,与签名用的 Developer ID 证书是两套凭据);
  5. 调用 @electron/notarizenotarize(...) 提交公证,失败会打印 could not notarize application 并向上抛出,使构建失败。

此外,build.ts 在 darwin 平台且未 skipSigning 时,会用 macOS 自带的 Gatekeeper 校验工具对产物做一次自证:

const args = ['-a', '-vvvv', appFolder]
console.log(`cmd: spctl ${args.join(' ')}`)
const sp = spawn('spctl', args, { stdio: 'inherit' })

即执行 spctl -a -vvvv <产物路径>,退出码非 0 则构建失败——相当于在 CI 中模拟了"用户机器上 Gatekeeper 是否放行"的终检。

轮换 Windows 签名密钥:CSR、SSL.com 证书与 PFX 转换

这部分完整继承 code-signing 指南 中 "Rotating the Windows code signing key" 的 6 步流程。

第 1 步:用 openssl 生成私钥与 CSR

# generate a new private key
openssl genrsa -out win-code-signing.key 4096
# create a CSR using the private key
openssl req -new -key win-code-signing.key -out win-code-signing.csr

第 2 步:把 CSR 提交给 SSL.com(使用 Cypress 的 SSL.com 账户)换取证书

  • 若是续期(renewing),按 SSL.com 官方的 Renewing EV/OV and IV Certificates 指引操作;
  • 若是轮换(rotating),需联系 SSL.com 支持申请重新签发。

第 3 步:从 SSL.com 控制台获取完整证书链,以 ASCII-armored PEM 格式保存为 win-code-signing.crt(内容形如 -----BEGIN CERTIFICATE----- 等块)。

第 4 步:用 openssl 把明文 PEM 的私钥与证书转为二进制的 PKCS#12/PFX 并加密,口令同样是强口令,之后成为 CSC_KEY_PASSWORD

➜ openssl pkcs12 -export -inkey win-code-signing.key -in win-code-signing.crt -out encrypted-win-code-signing.pfx
Enter Export Password: <password>
Verifying - Enter Export Password: <password>

第 5 步:把 encrypted-win-code-signing.pfx 上传到 Google Drive 的 Code Signing 文件夹,获取公共直接下载链接。

第 6 步:在 CircleCI 的 test-runner:sign-windows-binary 上下文中,把 CSC_LINK 设为该直接下载 URL,把 CSC_KEY_PASSWORD 设为加密 pfx 的口令。

源码纵深:为什么 Windows 不直接用 pfx 本地签,而是远程签名

按 electron-builder 的一般约定,CSC_LINK 指向的 pfx 会被拉取后在构建机上执行 Authenticode 签名。但 Cypress 的 windows-sign.js 文件头注释说明了偏离这一默认做法的原因:

This signing procedure only runs on windows binary builds to leverage remote signing in order to fullfil new requirements around OV and IV code signing.

即为了满足 SSL.com 自 2023 年 6 月起对 OV/IV 证书密钥存储的新要求(私钥不能长期离开受控环境),Cypress 改为把签名委托给 SSL.com 的远程签名服务。这与 electron-builder.json"sign": "./scripts/windows-sign.js" 的委托配置相呼应,完整链路见 windows-sign.jssign(configuration) 函数:

  1. 读取四个远程签名凭据(均来自 CI 环境变量,任一缺失则打印缺失项并 process.exit(1)):

    • WINDOWS_SIGN_USER_NAMEWINDOWS_SIGN_USER_PASSWORD —— SSL.com 账户凭据;
    • WINDOWS_SIGN_CREDENTIAL_ID —— SSL.com 侧证书/凭据标识;
    • WINDOWS_SIGN_USER_TOTP —— TOTP 二次验证密钥。
  2. 下载并校验 SSL.com 的 CodeSignTool:脚本把工具版本钉死为 v1.3.2,并预置了下载包的 SHA-256 校验和:

    const CODE_SIGN_TOOL_VERSION = 'v1.3.2'
    const CODE_SIGN_TOOL_SHA256 = '4afc32e8b7f79bbe1de7e4e7049aaad4e0f754357613b9bbec0e3052f06fd36b'
    

    下载后先计算实际 SHA-256,与预期值不符立即抛错("Downloaded CodeSignTool archive checksum ... does not match expected ...")。注释还贴心地给出升级工具时的校验和再生成方法:curl -fSL <CODE_SIGN_TOOL_URL> | shasum -a 256。这保证了"能签我们二进制的工具"只能经受控的版本提升才会变化,防止供应链篡改。

  3. 调用 CodeSignTool 完成远程签名

    childProcess.execSync(`CodeSignTool.bat sign -input_file_path="${configuration.path}" -output_dir_path="${TEMP_DIR}" -credential_id="${CREDENTIAL_ID}" -username="${USER_NAME}" -password="${USER_PASSWORD}" -totp_secret="${USER_TOTP}"`)
    
  4. 回写产物:由于 CodeSignTool 无法在无交互确认的情况下原地覆盖文件,脚本先签出到临时目录 os.tmpdir()/release/tmp,再把签名后的文件移回原位置覆盖(mv "${tempFile}" "${dir}"),对 electron-builder 而言签名对象路径不变。

从源码结构看,configuration.path 由 electron-builder 在逐个签包文件时传入,因此该委托脚本只对 Windows 可执行文件生效,与 electron-builder.jsonwin.target: "dir"(产出目录而非压缩包)相配合。

小结:两套密钥、两条链路、统一的 CI 凭据模型

维度 macOS Windows
证书来源 Apple Developer ID Application 证书 SSL.com 签发的 OV/IV 证书
密钥形态 加密 .p12 加密 .pfx(PKCS#12)
凭据上下文 test-runner:sign-mac-binary test-runner:sign-windows-binary
核心环境变量 CSC_LINKCSC_KEY_PASSWORD CSC_LINKCSC_KEY_PASSWORD,另加 WINDOWS_SIGN_USER_NAME/_PASSWORD/_CREDENTIAL_ID/_USER_TOTP
签名方式 electron-builder 本地签名(forceCodeSigning 委托脚本走 SSL.com 远程签名(win.sign
签名后动作 afterSign 钩子公证(NOTARIZE_APP_*)+ spctl 自证

两个平台共享同一套"加密密钥文件存 Google Drive、CSC_LINK 指下载链接、CSC_KEY_PASSWORD 存口令"的凭据模型,差异在于 Windows 因密钥托管合规要求额外走了一条版本钉死、校验和校验的远程签名通道。对需要维护自签发布流程的 Electron 项目而言,这套"证书轮换 SOP + 委托签名 + 公证钩子 + Gatekeeper 自证"的组合是一个可以直接对照仓库文件复现参考的完整范本。

登录后查看全文
热门项目推荐
相关项目推荐