macOS 签名与公证:Gatekeeper 拦截的六类原因与一次通过清单

在自己的 Mac 上双击安装包一切正常,用户下载后却看到「无法验证开发者」。签名与公证是两道不同的关卡,任何一层漏签、缺时间戳或 entitlements 冲突都会被拦。本文给出六类常见拦截原因、一次通过的检查顺序,以及三台干净机器的验收方法。

你的机器能打开,不代表用户的机器能打开。

一、六封一模一样的邮件

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

许澄的 Mac 工具上线第一周,收到六封内容几乎相同的邮件:「下载后提示无法打开,因为 Apple 无法验证。」

他让用户截图,看到的都是同一个对话框。可他自己在开发机上试了三次,双击就能开。

问题在于开发机会记住「仍要打开」的选择,也会对本地构建放行;干净机器不会。签名与公证针对的从来不是开发者,而是第一次拿到文件的陌生人。

他补了签名,仍有两位用户被拦。最后发现是安装包里嵌了一个没签的辅助程序——主程序签了,嵌在里面的二进制没签,整包就不算通过。

二、签名与公证是两道关卡

两者经常被混为一谈,实际是先后关系。

关卡做什么失败表现谁检查
代码签名证明文件来自你,且签名后未被改动「无法验证开发者」或「已损坏」Gatekeeper、系统完整性检查
公证把产物交给 Apple 自动检查,接受后取得票据日志未获 Accepted,或 Gatekeeper 评估拒绝notarytool 日志、Gatekeeper
装订票据把公证结果附在产物上离线或断网时仍被拦Gatekeeper 离线校验

签名解决「是不是你」,公证解决「Apple 有没有看过」。 只签名不公证,用户仍会看到拦截;只公证不装订,断网环境下票据取不到。

签名对象要列清单。 主程序、框架、插件、命令行工具、安装器,逐个登记;清单之外的二进制,就是下一次拦截的来源。

证书类型决定用途。 直装分发用 Developer ID 系列证书;走 Mac App Store 用另一套分发证书。两者不能互换,混用会在提交或运行时失败。

公证是异步的。 提交后要等结果,失败会返回日志。把「提交完成」当成「通过」是常见误判。

票据要能离线验证。 装订之后再断网启动一次,确认系统不依赖网络也能放行;只测联网状态,会漏掉一部分用户。

三、六类高频拦截

以下六类覆盖了大多数一次不通过的情形。

#原因典型表现处理方向
1嵌套二进制未签主程序正常,辅助工具被隔离由内向外逐个签名
2缺安全时间戳证书过期后旧版本突然打不开签名时加时间戳服务
3Hardened Runtime 未开公证直接拒绝开启并补齐例外
4entitlements 冲突能签过但运行崩溃或权限被拒最小权限,逐项复核
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。

上述两条只用于说明公开文档入口,不构成对审核结果的承诺。证书类型与签名要求可能随系统版本变化,发布前请以开发者账号内的最新说明为准。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元