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是否成功上传。- 返回的是
Accepted、Invalid,还是仍在处理中。 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"
先检查日志里的 status、statusSummary、path、message 和 severity。如果没有 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 发布环境。