不买每年 99 刀的苹果开发者证书,macOS 软件自动更新实践
Auto Email Sender 是一款基于 Electron + Python 混合架构的开源桌面客户端。
在 Windows 平台上,自动更新体验极其丝滑: 系统检测到新版本后,后台默默下载增量补丁,弹个轻量提示,点一下重启就完成了无缝升级。
但一换到 macOS 平台,画风突变: 每次点检查更新,只能弹个弹窗把用户踹去 GitHub Releases 页面,让用户对着几百兆的 DMG 苦苦等待下载,挂载磁盘映像,再痛苦地把应用图标拖进 Applications 文件夹覆盖旧版本……
用户在群里疯狂吐槽:“都 2026 年了,怎么苹果电脑更新个软件还像在用 Windows 98?!”
我们当然想做应用内一键更新,但横亘在面前的有一座大山: 项目是个非盈利开源项目,没有每年给苹果上贡 99 美元购买 Apple Developer Program(开发者计划),更没有所谓的 Developer ID 证书和官方 Notarization(公证)。
苹果生态是出了名的“高墙深筑”。没有苹果爸爸的证书加持,macOS 应用真的注定只能当二等公民,无法享受优雅的应用内静默更新吗?
什么是 Apple Developer ID 和公证(Notarization)?苹果从 macOS Catalina 开始强制推行公证机制:任何从非 App Store 渠道分发的应用,必须使用年费 99 刀的开发者证书签名,并上传到苹果服务器进行自动化恶意软件扫描(公证)。通过后才会被 Gatekeeper 信任,否则双击直接弹“无法验证开发者,已移动到废纸篓”。
为什么直接抄 Windows 作业会当场暴毙?
我们在 Windows 上用的是业界通用的 electron-updater。那直接把 macOS 上的拦截判断删掉,让它走同一套逻辑行不行?
不行,代码一放开,在 macOS 上不仅更新不了,甚至会直接把旧应用搞崩溃。
因为 electron-updater 在 macOS 底层重度依赖 Squirrel.Mac,而 Squirrel.Mac 的设计哲学是**“把苹果系统的代码签名(Code Signing)当成唯一的身份证”**。
它在替换新应用之前,会强制校验新旧 App 的 Apple Team ID 和 Bundle Signature。
而我们这种白嫖选手,在 macOS 打包时用的是 Ad-hoc 签名(即本地自签名,codesign -s -):
- 它能让应用在本地拥有基本的完整性结构,防止二进制段被篡改;
- 但它的 Team ID 是空的,根本不被系统级信任链认可;
- Squirrel.Mac 看到这种“黑户”,安全检查当场报错,把下载好的更新包直接丢弃。
通俗比喻:官方办护照 vs 两人之间的暗号私印为什么不花钱买证书就不能用常规更新器? 想象两种信任模式:
- 官方公证体系(苹果开发者证书):就像国家公安局颁发的正式护照。你每到一个关卡,系统都要联网扫描护照防伪,必须每年交年费给苹果盖章,没有护照直接被当成非法入境;
- Sparkle 自建体系(Ed25519 密码学公私钥):就像你和老朋友之间约定的一套专属火漆印章。第一天安装时用户手动信任你一次(认领你的印章),以后每次新版本只要盖着开发者独有的火漆印,软件自己验一下就能确认是正品。
纯靠密码学数学原理,0 元成本,照样能把安全防篡改做得固若金汤。
核心矛盾所在不是 Electron 不支持,而是操作系统层面的“信任链”断裂了。如果不买证书,就必须在应用层另辟蹊径,寻找一套不依赖苹果 CA 根证书的第二信任体系。
老牌神器 Sparkle 与现代 EdDSA 密码学
深入调研 macOS 桌面开发生态后,一个历史比 macOS 还悠久的老牌更新框架浮出了水面——Sparkle。
绝大部分顶级 Mac 独占软件(比如 IINA、Keka、Sublime Text)用的都是 Sparkle。更令人振奋的是,Sparkle 的架构设计远比 Squirrel.Mac 灵活且务实:
它不仅支持 Apple 官方签名,还原生支持一套基于 Ed25519 (EdDSA) 的非对称加密签名链!
| 信任体系维度 | 苹果官方体系 (Developer ID + 公证) | Sparkle 自建信任体系 (EdDSA 密钥对) |
|---|---|---|
| 解决的核心问题 | 电脑初次拿到 App 时,操作系统是否信任该软件 | 用户安装旧版本后,新版本是否由原作者亲手发布 |
| 每年开销 | 99 美元/年(断费即废) | 0 元(纯数学与密码学保障) |
| 防篡改能力 | 依赖苹果在线公证比对 | 依赖 Ed25519 高强度数字签名,物理无法伪造 |
| 适用范围 | 首次双击打开绕过 Gatekeeper 拦截 | 应用后续的所有自动化迭代与热更新 |
巧妙的“信任接力”我们承认并接受苹果的底线:首次安装时,用户必须手动右键打开一次 DMG,允许这个未公证的软件运行; 但只要用户在第一天信任了我们,从第二天开始,Sparkle 就会用内置的 EdDSA 公钥接管后续的一切更新! 只要新下载的更新包带有效私钥签名,就证明是开发者本人发布的正品,系统即可在应用内部安全替换!
在 Electron 里插一根“原生撬棍”
Sparkle 是用纯 Objective-C / Swift 写成的原生 macOS 动态库,而我们的前端是 TypeScript / React。
我们没有选择去用臃肿复杂的第三方封装库,而是自己写了一层极薄的 Objective-C++ 原生 Node 扩展(Native Bridge):
[ React 前端界面 ]
│ (IPC: "check-for-updates")
▼
[ Electron 主进程 ]
│ (调用原生动态库)
▼
[ Sparkle Bridge (Obj-C++) ]
│ (直接驱动)
▼
[ Sparkle.framework ] ──> 自动解析 appcast.xml ──> EdDSA 验签 ──> 优雅弹出原生更新窗口 ──> 替换重启这样设计的妙处在于: 我们彻底卸下了所有负担!什么下载断点续传、文件解压、权限提升(Privilege Escalation)、进程杀灭与重启,全部交给经过十几年风雨考验的原生 Sparkle 去做,主进程代码仅仅只有几十行胶水代码,稳如老狗。
全量 DMG 与差分包(Delta Update)的艺术
在分发资产上,我们原本打算为自动更新单独打包一套 ZIP,但很快发现这是在给自己找麻烦。
Sparkle 强大到令人发指:它可以直接从 DMG 镜像里无缝提取应用并执行替换!
这意味着我们不需要为“首次安装”和“自动更新”割裂地维护两套产物。每次 CI/CD 发布时,流程极为统一:
- 构建生成完整的未公证 DMG(既作为 GitHub 官网下载件,也作为全量更新包);
- 调取 CI 里的私钥,生成 Ed25519 签名与
appcast.xml更新索引; - 如果上一个版本的二进制可用,CI 顺带跑一条命令,自动计算生成一个只有几兆大小的差分包(.delta)!
什么是差分更新(Delta Updates)?假设你的 Electron 应用整包有 120MB,新版本仅仅修改了几张图片和前端 JS 代码。Sparkle 的
generate_appcast工具会自动对比新旧两个版本的二进制文件,仅把差异字节切片打包成一个 5MB 的 delta 文件。 用户更新时几秒钟下载完成,体验快到飞起。
更新元数据必须“最后登场”
在打磨 GitHub Actions 自动化发布流水线时,我们踩过一个极其难受的坑:
有一段时间,用户频繁抱怨:“软件提示有新版本,但点下载时总是报 404 错误!”
排查后发现,CI 流水线是并发上传文件的:appcast.xml 比较小,先上传到了 GitHub Releases;而几百兆的 DMG 和 Delta 包还在慢吞吞地传输。
结果客户端里的 Sparkle 刚好看到了新的 appcast.xml,兴奋地去拉里面的下载链接,直接撞上未就绪的 404!
我们立即重构了发布次序,立下了铁律:
必须等所有平台的安装包、差分补丁全部落盘上传完毕后,才能把作为“总开关”的 appcast.xml 推向公网!
总结
谁说不给苹果交保护费,就只能忍受野蛮的手动覆盖?
- 认清信任的边界: Gatekeeper 防的是来源未知的木马,而更新防的是传输篡改与冒名顶替。搞清楚每一层防御针对的是什么,才能合理选择武器;
- 密码学面前人人平等: 苹果的证书归根到底也是数字签名,利用成熟的开源密码学框架(Sparkle + Ed25519),我们完全可以用零成本搭建起一条牢不可破的自主更新隧道;
- 原生事情交给原生做: 在跨平台桌面开发中,不要试图用 Node.js 去手写跨平台的安装器状态机。拥抱 macOS 平台最好的原生基础设施,往往能换来最顶级的人机交互体验。