notarytool 公证失败:2026 排查清单

notarytool 公证失败后,不要立刻重新上传同一个包。先保存 Submission ID,再读取公证状态和 JSON 日志;随后按 Developer ID 签名、Hardened Runtime、entitlements、嵌套代码、封装格式与 stapler 逐层定位。只有确认失败层级后,重建并重签才有意义。

这篇内容适合三类人:通过官网直接分发 DMG、PKG 或 ZIP 的 macOS 独立开发者;维护自动签名、公证脚本,但流水线偶发返回 Invalid 或长时间处理中状态的人;准备把发布任务迁移到远程 Mac、需要核验证书、Keychain 与工具链是否完整的小团队。

01 先把“公证失败”拆成不同故障层

公证、代码签名、票据附加和 Gatekeeper 验证不是同一个动作。Apple 将站外分发定义为一条由 Developer ID 签名、公证扫描和 Gatekeeper 检查共同组成的链路;公证也不是 App Review,而是自动扫描软件、检查代码签名问题并返回结果的服务。(Apple Developer:Developer ID 与站外分发)

你应先记录下面这些结果:

  • notarytool submit 是否成功上传。
  • 返回的是 AcceptedInvalid,还是仍在处理中。
  • notarytool log 是否能读取完整 JSON。
  • stapler staple 是否成功把票据附加到目标。
  • spctl 是否在最终分发文件上通过 Gatekeeper 验证。

Apple 官方示例显示,上传成功后会返回一个 Submission ID;这个 ID 必须保留,后续读取日志和追踪状态都要用到。Invalid 的具体原因通常位于日志中的 issues 数组,而不是命令行最后一行的概括信息。(Apple Developer:自定义公证工作流)

xcrun notarytool submit "<发布包路径>" \
  --keychain-profile "<Keychain Profile>" \
  --output-format json

xcrun notarytool log "<Submission ID>" \
  --keychain-profile "<Keychain Profile>" \
  "<脱敏日志路径>.json"

先检查日志里的 statusstatusSummarypathmessageseverity。如果没有 Submission ID,优先处理上传或认证问题;如果状态是 Invalid,不要把时间花在 stapler 上;如果状态是 Accepted 但 stapler 失败,重点转向网络、目标文件可写性和票据附加条件。

Apple 文档指出,公证扫描通常在较短时间内完成,但不应把某个固定耗时当成保证。官方自动化示例还提供了 --wait--timeout 和日志输出方式,适合在脚本中保存每次发布结果。

02 Developer ID 身份先于一切权限配置

应用、辅助工具和安装包不能只检查最外层

公证要求你使用与站外分发匹配的 Developer ID 身份。Apple 明确区分 Developer ID Application 和 Developer ID Installer:前者用于应用及可执行代码,后者用于安装包。Mac App Distribution、Apple Developer、临时开发签名或 ad hoc 签名不能直接替代站外分发所需身份。(Apple Developer:macOS 软件公证要求)

不要只运行一次:

codesign --verify --deep --strict --verbose=2 "<应用路径>"

--deep 可以帮助你快速发现问题,但不能代替对嵌套对象逐个检查。你还要分别查看主 App、Framework、Plug-in、XPC Service、命令行工具和第三方二进制的签名身份:

codesign -dvvv "<应用路径>"
codesign -dvvv "<嵌套对象路径>"
codesign -vvv --strict --verbose=4 "<目标路径>"

重点观察以下日志线索:

  • 私钥缺失:钥匙串中可能看得到证书名称,但签名动作无法使用对应私钥。
  • 证书链异常codesign 验证可能失败,或日志显示签名身份无法建立有效链。
  • 安全时间戳缺失:自定义签名脚本未使用安全时间戳时,公证日志可能把它列为签名完整性问题。
  • 身份选错:使用开发证书或 Mac App Store 相关身份时,日志通常会指向证书类型、签名要求或分发方式不匹配。

Apple 要求发布代码使用有效签名、Developer ID 身份和安全时间戳;Xcode 的标准分发流程通常会自动处理时间戳,但自定义脚本必须主动验证。不要自行推断证书“还能用多久”或“肯定能通过审核”,以公证日志和 codesign 输出为准。

03 Hardened Runtime 与 entitlements 要按实际能力收敛

启用 Hardened Runtime 只是公证的起点,不代表所有运行时行为都会自动被允许。Apple 要求提交公证的 macOS App 启用 Hardened Runtime;如果应用依赖 JIT、动态加载第三方库或未签名可执行内存,则必须声明对应例外 entitlement。(Apple Developer:Hardened Runtime)

先查看签名后真正嵌入的 entitlement,而不是只看工程文件:

codesign -d --entitlements :- "<已签名应用或工具路径>"
plutil -lint "<entitlements 文件路径>"
plutil -convert xml1 "<entitlements 文件路径>"

排查时,把问题分成三类:

格式错误:entitlements 文件不是正确的 XML,含有不被接受的 BOM,或手工编辑后产生非法结构。Apple 文档建议用 plutil -lint 检查;如果签名后的输出出现 bplist00,说明嵌入内容不是公证要求的格式。(Apple Developer:解决常见公证问题)

⚠️ 配置过多:为了“先通过”,把 JIT、关闭库验证、未签名可执行内存等权限全部打开。这样可能掩盖真实问题,也扩大运行时权限。Apple 建议只使用应用实际需要的 entitlement,并且布尔型例外在为 false 时不要嵌入。

签名后内容被修改:签名完成后替换 Framework、修改资源、重新打包或注入第三方二进制,都会破坏签名完整性。此时重新运行公证通常没有意义,必须回到“修改完成后再签名”的顺序。

特别检查 com.apple.security.get-task-allow。Apple 的公证文档明确要求,发布版本不能把它设置为 true;调试插件时的特殊例外不能直接复制到生产包。

04 嵌套代码通过才算真正通过

外层 App 的签名验证成功,不等于内部所有代码都合格。一个典型 macOS 发布包可能同时包含 Framework、Plug-in、XPC Service、命令行工具和第三方动态库。任何一个对象在签名后被替换,最终都可能在公证日志中暴露出来。

建议按由内到外的顺序检查:

codesign --verify --strict --verbose=4 "<Framework 路径>"
codesign --verify --strict --verbose=4 "<Plug-in 路径>"
codesign --verify --strict --verbose=4 "<XPC Service 路径>"
codesign --verify --strict --verbose=4 "<主 App 路径>"

然后再检查 Gatekeeper 视角:

spctl --assess --type execute --verbose=4 "<应用路径>"
spctl --assess --type open --context context:primary-signature \
  --verbose=4 "<发布包路径>"

这里要保持证据链:codesign 说明签名结构是否完整,公证日志说明 Apple 扫描阶段发现了什么,spctl 说明本机 Gatekeeper 如何评估最终文件。三者结果不一致时,不要直接把问题归类为“公证服务异常”。

如果 App 中有插件,Hardened Runtime 的影响还会传递到宿主进程。Apple 文档说明,插件不会独立声明自己的 entitlement,而是继承宿主可执行文件的相关能力;因此,插件需要的权限必须在宿主签名时正确声明。

05 这几个长尾故障要分开处理

Invalid 后怎样查看具体错误日志

使用原始 Submission ID 读取 JSON 日志:

xcrun notarytool log "<Submission ID>" \
  --keychain-profile "<Keychain Profile>" \
  "<日志文件路径>.json"

不要只复制命令行中的 statusSummary。日志中的 path 能告诉你具体是主程序、嵌套 Framework 还是安装包内部对象;message 则用于决定回到签名、权限还是封装步骤。Apple 官方示例中的 Invalid 日志包含问题路径、严重程度和错误信息。

已签名为什么仍无法通过公证

“已签名”只说明某个签名动作完成,不代表签名身份正确、Hardened Runtime 已启用、entitlements 格式正确,也不代表签名后没有修改内容。优先检查 Developer ID 类型、安全时间戳、get-task-allow、嵌套代码和最终提交的归档是否就是刚刚验证过的那个文件。

公证成功但 stapler 无法附加票据

先确认你操作的目标类型。Apple 支持给 App、Bundle、DMG 和 flat PKG 附加票据,但 ZIP 本身不能直接 stapler;你需要先对 ZIP 中的 App 或其他可附加对象执行 stapler,再重新创建 ZIP。(Apple Developer:自定义公证工作流)

如果目标是 DMG 或 PKG,检查文件是否可写,并确认远程 Mac 能访问票据下载所需网络资源。Accepted 代表公证服务接受并生成票据,不等于本地 stapler 已经成功。

远程 Mac 如何保存公证凭据

不要把 Apple ID、Team ID 和应用专用密码直接写进脚本。Apple 官方推荐使用 notarytool store-credentials 把凭据保存到 Keychain,再通过 --keychain-profile 引用。

xcrun notarytool store-credentials "<Keychain Profile>" \
  --apple-id "<Apple ID 占位符>" \
  --team-id "<Team ID 占位符>"

在远程 Mac 上验收时,至少确认:

  • 执行发布脚本的用户能访问对应 Keychain。
  • 非交互式 SSH 会话不会因钥匙串锁定而失败。
  • CI 或定时任务使用的 DEVELOPER_DIR 与人工测试一致。
  • 脚本只输出脱敏日志,不打印密码、私钥或完整认证令牌。
  • 更换机器后能重新导入证书、私钥和 Keychain Profile,而不是依赖旧机器残留状态。

Apple 还提供 Notary API,用于把上传和状态查询从 notarytool 中拆出;但如果你的签名、打包和 stapler 仍在 Mac 上执行,API 不能消除 Mac 工具链本身的依赖。(Apple Developer:通过网页提交软件进行公证)

DMG、PKG 和 ZIP 是否用同一套排查方式

共同部分是签名身份、嵌套代码、公证日志和最终 Gatekeeper 验证。不同之处在于容器的签名与票据处理方式,不能把三种格式当成完全相同。

分发格式 重点检查 票据处理 常见误判
ZIP ZIP 内 App 的签名和内容是否在归档前已固定 不能直接对 ZIP stapler;处理内部对象后重新压缩 看到上传成功就认为最终 ZIP 已附加票据
DMG 镜像内容、镜像完整性、内部 App 或 PKG 可对 DMG 及其中适用对象处理 只验证 DMG,未验证镜像内 App
PKG Developer ID Installer 身份、安装 payload、安装包签名 可对 flat PKG stapler 只检查 App 签名,忽略 Installer 签名

Apple 说明,站外分发可以使用 ZIP、DMG 和 flat PKG;如果自定义安装器还会下载或安装其他内容,安装器与其 payload 可能需要分别处理。(Apple Developer:打包 macOS 软件进行分发)

06 用一张条件分支决定下一步

按下面的条件执行,不要在所有失败上重复“重新打包、重新上传”:

  • 若没有 Submission ID:先查认证、Keychain Profile、网络连接、Xcode Command Line Tools 和提交命令。
  • 若状态是处理中:保留原 ID,按脚本设定查询;不要并行提交大量相同包。
  • 若状态是 Invalid 且日志有具体 path:回到该路径对应的签名对象,先修复嵌套代码或 entitlement。
  • 若签名验证失败:重新确认 Developer ID 身份、私钥、时间戳和签名后是否发生修改。
  • 若公证 Accepted 但 stapler 失败:检查目标格式、文件可写性、网络访问和当前工具链。
  • 若 stapler 成功但 Gatekeeper 失败:在最终分发文件上重新运行 spctl,确认你测试的不是旧归档。
  • 若本地环境每次都缺证书或 Keychain:不要继续做临时修复,迁移到可持续运行、权限固定并能保存日志的 Mac。

Apple 自 2023 年 11 月 1 日起不再接受通过 altool 或 Xcode 13 及更早版本发起的公证上传;如果旧脚本仍依赖这些路径,先迁移到 notarytool 或当前受支持的工具链。(Apple Developer:macOS 软件公证要求)

07 远程 Mac 发布环境的验收卡

一次合格的验收,不是“某次上传成功”,而是同一个真实发布包能够重复完成以下链路:

阶段 必须保存的证据 通过条件
归档与导出 构建日志、导出路径、构建提交标识 发布包由目标发布配置生成
签名验证 codesign 输出、证书身份、entitlements 摘要 主 App 与嵌套对象均通过严格验证
提交公证 Submission ID、提交文件哈希或唯一构建标识 上传完成,凭据未写入日志
读取日志 脱敏 JSON、状态、问题路径 能区分 Accepted、Invalid 与处理中
票据附加 stapler 输出 目标格式支持附加且文件可写
Gatekeeper 验证 spctl 输出、最终文件路径 测试对象就是准备分发的最终包

你可以把这张验收卡放进发布脚本的产物目录,并为每次构建保存独立目录。不要只保存“成功/失败”两个字段。至少记录构建标识、Submission ID、日志文件、签名摘要、stapler 结果和 Gatekeeper 结果,这样下次失败时能判断是代码变化、环境变化还是凭据变化。

如果你要把发布任务迁移到远程 Mac,可先阅读 Developer ID 证书迁移与 Keychain 安全配置 相关说明,再按 远程 Mac 自动构建 macOS App 的思路固定 Xcode、命令行工具和脚本环境。对于需要长期运行的发布任务,也可以参考 macOS 发布服务器验收清单,重点核对凭据持久化、远程访问权限和日志保留方式。

你完成一次故障修复后,还要问自己一个更实际的问题:下一次发布是否能在同一台 Mac 上复现?本地临时环境常见的缺点是证书和私钥不持久、Keychain 在无人值守任务中被锁定、Xcode 版本容易漂移,失败日志也可能散落在个人电脑上。若你需要持续保存签名身份、重复运行 notarytool、执行 stapler 并保留脱敏记录,租用 CALMVPS 的远程 Mac 通常比反复修补一台不常在线的开发机更适合作为发布节点;但如果你长期高负载构建、必须连接专用物理设备,购买并自持 Mac 仍然更合适。你可以先完成验收卡,再决定是继续本地运行,还是使用 CALMVPS 搭建稳定的远程 macOS 发布环境。