Cypress 二进制代码签名实战:macOS 与 Windows 证书轮换及 electron-builder 签名链路解析
【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypress
Cypress 在 CI 中构建 Windows 与 Mac 发行版时会执行代码签名,签名的具体工作由electron-builder在create-build-artifacts构建任务中完成。本文基于仓库内的 code-signing 指南 展开,完整覆盖 Mac(Apple Developer ID 证书)与 Windows(SSL.com 证书)两条签名密钥的轮换流程,并结合 electron-builder.json、windows-sign.js 等仓库源码,深入讲解签名配置、公证(notarize)与远程签名委托的实际实现,读完后可理解 Cypress 发布产物"签名 → 公证 → 校验"的完整链路及其安全设计。
何时进行代码签名:CI 构建流程中的定位
code-signing 指南 开宗明义:Cypress 的Windows 和 Mac 发行版在 CI 构建时执行代码签名,签名动作由electron-builder在create-build-artifacts任务中承担。指南同时声明了一个适用前提:读者应已熟悉 electron-builder 官方 Code Signing 文档中关于CSC_LINK、CSC_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。构建流程中签名并非孤立步骤,而是嵌在如下顺序里:
lerna run build/lerna run build-prod构建各 package(见 build.ts);- 把各 package 的产物复制到 dist 目录;
electronBuilder.build(...)打包并签名(Windows 走委托脚本,macOS 走CSC_LINK证书自动签名);- 触发 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.js 与 after-sign-hook.js。前者负责把各 package 的node_modules拷入产物、翻转 Electron Fuses、生成 V8 快照等打包后加工(与签名无直接关系,但会改变最终进签/进公证的对象);后者负责 macOS 公证。
轮换 macOS 签名密钥:从 Apple 证书到 CircleCI 上下文
这部分完整继承 code-signing 指南 中 "Rotating the Mac code signing key" 的 4 步流程:
- 在 Mac 上用 Cypress 的 Apple 开发者计划身份登录 Xcode。
- 按 Apple 官方"Create, export, and delete signing certificates"文档操作:
- 先执行 "View signing certificates" 查看现有证书;
- 执行 "Create a signing certificate",提示选择类型时选择Developer ID Application(注意:用于应用分发签名的是 Developer ID Application 证书,而非 App Store 证书);
- 执行 "Export a signing certificate",导出时设置一个强口令,该口令之后会成为环境变量
CSC_KEY_PASSWORD。
- 把导出的、已加密的
.p12文件上传到 Google Drive 的 Code Signing 文件夹,并生成一个公共的直接下载链接。 - 在 CircleCI 的
test-runner:sign-mac-binary上下文中,把CSC_LINK设为该直接下载 URL,把CSC_KEY_PASSWORD设为加密 p12 时使用的口令。
几点值得强调的工程细节:
- 凭据存放采用"外部密文文件 + 下载链接"模式:私钥从不落进代码仓库或 CI 明文环境变量,
CSC_LINK只指向加密后的.p12,CSC_KEY_PASSWORD单独存放。electron-builder 构建时会自动从CSC_LINK下载证书文件并用口令解密后签名。 - 上下文按平台隔离:macOS 与 Windows 分别使用
test-runner:sign-mac-binary与test-runner:sign-windows-binary两个独立上下文,密钥轮换互不影响,也便于按平台最小化授权。 - 轮换时口令变更必须同步:
CSC_KEY_PASSWORD与.p12的加密口令是绑定的,二者不同步会导致 CI 签名阶段解密失败。
源码纵深:签名之后还要"公证"(notarize)
macOS 上仅签名并不够,还需向 Apple 提交公证。Cypress 通过 after-sign-hook.js 在 electron-builder 的afterSign阶段完成,逻辑如下:
- 平台守卫:
process.platform !== 'darwin'时直接跳过(日志提示not Mac, skipping after sign hook); - 逃生开关:设置了
SKIP_NOTARIZATION环境变量时跳过公证,便于调试构建; - 定位应用:在
params.appOutDir下查找<productFilename>.app,找不到则抛错;appId硬编码为com.electron.cypress,与 electron-builder.json 中的appId一致; - 三项必需凭据:
NOTARIZE_APP_APPLE_ID、NOTARIZE_APP_PASSWORD、NOTARIZE_APP_TEAM_ID,任一缺失立即抛错(注意公证用的是 Apple ID + App 专用密码 + Team ID,与签名用的 Developer ID 证书是两套凭据); - 调用
@electron/notarize的notarize(...)提交公证,失败会打印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.js 的sign(configuration)函数:
读取四个远程签名凭据(均来自 CI 环境变量,任一缺失则打印缺失项并
process.exit(1)):WINDOWS_SIGN_USER_NAME、WINDOWS_SIGN_USER_PASSWORD—— SSL.com 账户凭据;WINDOWS_SIGN_CREDENTIAL_ID—— SSL.com 侧证书/凭据标识;WINDOWS_SIGN_USER_TOTP—— TOTP 二次验证密钥。
下载并校验 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。这保证了"能签我们二进制的工具"只能经受控的版本提升才会变化,防止供应链篡改。调用 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}"`)回写产物:由于 CodeSignTool 无法在无交互确认的情况下原地覆盖文件,脚本先签出到临时目录
os.tmpdir()/release/tmp,再把签名后的文件移回原位置覆盖(mv "${tempFile}" "${dir}"),对 electron-builder 而言签名对象路径不变。
从源码结构看,configuration.path由 electron-builder 在逐个签包文件时传入,因此该委托脚本只对 Windows 可执行文件生效,与electron-builder.json中win.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_LINK、CSC_KEY_PASSWORD | CSC_LINK、CSC_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 自证"的组合是一个可以直接对照仓库文件复现参考的完整范本。
【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypress
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考