你的机器能打开,不代表用户的机器能打开。
一、六封一模一样的邮件
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
许澄的 Mac 工具上线第一周,收到六封内容几乎相同的邮件:「下载后提示无法打开,因为 Apple 无法验证。」
他让用户截图,看到的都是同一个对话框。可他自己在开发机上试了三次,双击就能开。
问题在于开发机会记住「仍要打开」的选择,也会对本地构建放行;干净机器不会。签名与公证针对的从来不是开发者,而是第一次拿到文件的陌生人。
他补了签名,仍有两位用户被拦。最后发现是安装包里嵌了一个没签的辅助程序——主程序签了,嵌在里面的二进制没签,整包就不算通过。
二、签名与公证是两道关卡
两者经常被混为一谈,实际是先后关系。
| 关卡 | 做什么 | 失败表现 | 谁检查 |
|---|---|---|---|
| 代码签名 | 证明文件来自你,且签名后未被改动 | 「无法验证开发者」或「已损坏」 | Gatekeeper、系统完整性检查 |
| 公证 | 把产物交给 Apple 自动检查,接受后取得票据 | 日志未获 Accepted,或 Gatekeeper 评估拒绝 | notarytool 日志、Gatekeeper |
| 装订票据 | 把公证结果附在产物上 | 离线或断网时仍被拦 | Gatekeeper 离线校验 |
签名解决「是不是你」,公证解决「Apple 有没有看过」。 只签名不公证,用户仍会看到拦截;只公证不装订,断网环境下票据取不到。
签名对象要列清单。 主程序、框架、插件、命令行工具、安装器,逐个登记;清单之外的二进制,就是下一次拦截的来源。
证书类型决定用途。 直装分发用 Developer ID 系列证书;走 Mac App Store 用另一套分发证书。两者不能互换,混用会在提交或运行时失败。
公证是异步的。 提交后要等结果,失败会返回日志。把「提交完成」当成「通过」是常见误判。
票据要能离线验证。 装订之后再断网启动一次,确认系统不依赖网络也能放行;只测联网状态,会漏掉一部分用户。
三、六类高频拦截
以下六类覆盖了大多数一次不通过的情形。
| # | 原因 | 典型表现 | 处理方向 |
|---|---|---|---|
| 1 | 嵌套二进制未签 | 主程序正常,辅助工具被隔离 | 由内向外逐个签名 |
| 2 | 缺安全时间戳 | 证书过期后旧版本突然打不开 | 签名时加时间戳服务 |
| 3 | Hardened Runtime 未开 | 公证直接拒绝 | 开启并补齐例外 |
| 4 | entitlements 冲突 | 能签过但运行崩溃或权限被拒 | 最小权限,逐项复核 |
| 5 | 公证票据未装订 | 联网可用,离线被拦 | 对产物执行装订 |
| 6 | 产物被二次修改 | 显示「已损坏」 | 签名后不再改动文件 |
嵌套项必须由内向外签。 安装包里常带命令行工具、帮助程序、框架与插件;先完成内层签名,再签外层封装。外层封装会记录嵌套代码的签名信息,之后改签内层可能使外层验证失败,并不是“外层签名覆盖内层”。
配置要先于最终签名确定。 在构建配置中启用 Hardened Runtime,并复核各目标的 entitlements;自定义签名流程通过 codesign --options runtime 将该标志写入适用代码的签名。不能先完成签名,再把它当作一个独立的运行开关补上。
时间戳不是可选项。 证书有有效期,没有时间戳的签名在证书到期后会失效,用户手里的旧版本会突然打不开。
证书到期的影响是延迟的。 到期当天未必出问题,用户重装或系统重新校验时才暴露;把到期日写进日历,而不是等报警。
entitlements 要按最小权限写。 为了省事打开一堆权限,轻则公证被问询,重则运行时行为与预期不符。每一项都应有对应的功能理由。
权限说明要写给用户看。 系统弹窗里的用途描述,要能让人明白「为什么需要」,而不是重复权限名称。
签名之后不要再改文件。 改图标、改配置文件、改权限都会让签名失效;需要改就回到构建,而不是在产物上打补丁。
产物校验要和签名一起做。 用系统工具验证签名链完整、票据存在、架构正确;三项都过,才算可发布。
四、一次通过清单与三台机器验收
把顺序固定下来,比记住每个参数更可靠。
确定 Hardened Runtime 与 entitlements → 构建
→ 由内向外签名(适用代码含 runtime 标志、安全时间戳)
→ 校验签名 → 提交公证 → 确认 Accepted
→ 对支持的分发产物装订票据 → 校验票据与 Gatekeeper 评估
→ 在三台干净机器上安装并启动
三台机器要覆盖三种状态。 一台从未装过该产品,一台装过旧版本,一台断网。三种状态分别验证首次信任、覆盖安装与离线校验。
覆盖安装要验证配置迁移。 旧版本的偏好、缓存与许可证要能平滑过渡;迁移失败的表现往往是「设置全丢了」。
验收要保存工具结果,不靠弹窗推断。 分别记录签名验证、notarytool 的 Accepted 状态、stapler 验证及 Gatekeeper 评估结果。已公证软件首次启动也可能出现确认弹窗;“有提示”不能直接推断公证失败,“能打开”也可能来自本机历史放行。
下载路径要模拟真实链路。 从浏览器下载会附加隔离属性,直接从构建目录复制不会。用真实下载链接测试,才会复现用户看到的行为。
隔离属性要用真实浏览器下载。 从构建目录直接复制不会带隔离标记,这种测试会给出错误的安心感。
密钥要放在可撤销的位置。 证书与私钥不要散落在个人目录;CI 中使用加密变量或专用密钥服务,并确保离职或换机时可以吊销。
首次通过率要记录。 每次提交公证的结果、失败原因与修复动作写进发布记录。连续三个版本一次通过,才说明流程稳定。
失败日志要归档。 公证拒绝的日志包含具体文件与原因,是下一次通过的依据;只记「失败了」等于白跑。
五、行动清单与复算验收
- 确认使用 Developer ID 证书,而非商店分发证书
- 列出安装包内所有可执行文件与嵌套组件
- 在最终签名前配置 Hardened Runtime,逐项复核各目标的 entitlements
- 由内向外签名,为适用代码写入 runtime 标志及安全时间戳
- 提交公证并等待明确结果,失败时读取日志
- 对最终产物装订票据,并校验签名完整性
- 在三台干净机器(全新、覆盖安装、断网)上验收
- 把证书与私钥放入可撤销的存储位置,记录到期日
验收标准:保存 Accepted 日志、签名及票据验证结果;在首次下载、旧版升级、离线三种状态下通过 Gatekeeper 检查。 离线测试使用可装订的分发产物,不能把裸命令行二进制等不支持直接装订的对象强套同一步骤。正常的首次启动确认不算失败,安全拦截必须定位原因。
每版本签名成本 = 年费 ÷ 年发版数 + 公证等待工时 × 时薪
示例:年费 99 美元,年发 12 版,每版等待 20 分钟,时薪 40 美元
每版本成本 ≈ 8.3 + 13.3 = 21.6 美元
首次通过率 = 一次通过的版本数 ÷ 提交版本数
低于 80% 时,优先修流程,而不是加快重试
使用边界与实操验收
本文只讲 macOS 直装分发的签名与公证,不涉及分发形态选择,那属于第 01 篇;安装包格式与更新器属于第 04 篇;商店分发证书与 Mac App Store 审核属于第 08 篇。
证书与账号的所有权、团队角色与找回路径,属于商店开发者账号:Apple、Google、Microsoft 与 Chrome 的团队与找回 ;证书到期的季度巡检属于维护负债账本:依赖、证书与平台政策的季度巡检 。
本文也不主张「所有产物都必须公证」。内部测试版可以只签名;面向外部用户的版本,尤其是通过浏览器下载的版本,应当完成签名、公证与装订三件事。
验证以用户侧为准:发布后一周,统计因「无法打开」产生的工单数量。目标是不超过下载量的千分之一;超过这个比例,说明干净机器的验收没有覆盖到位。
验收记录要能复现。写清系统版本、芯片架构与安装来源;换一台机器复测时,条件不同会得到不同结论。
关键参考与核验范围
本文的流程与阈值为教学演示,实际命令与要求请以 Apple 当期文档为准。核验日期:2026-10-25。
- Notarizing macOS software before distribution :Apple 官方公证流程与要求。
- Hardened Runtime :Hardened Runtime 的能力与例外配置说明。
上述两条只用于说明公开文档入口,不构成对审核结果的承诺。证书类型与签名要求可能随系统版本变化,发布前请以开发者账号内的最新说明为准。
系列导航
- 上一篇:分发形态选择:直装、原生商店、包管理器与 Web 版的四条路
- 下一篇:Windows 签名与 SmartScreen:OV、EV 与信誉积累怎么选
- 回到目录:一个人把软件交付到用户手里:签名、安装、更新与审核
- 相关阅读:商店开发者账号:Apple、Google、Microsoft 与 Chrome 的团队与找回 、维护负债账本:依赖、证书与平台政策的季度巡检
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。