收到一个脱敏后的 Invalid,或者看到 Accepted 却仍然无法让用户打开 App?macOS App 公证失败时,不要先重签或重复上传同一个文件:先保存提交 ID 和日志,确认故障阶段,再修复最终分发产物;即使状态是 Accepted,也必须继续完成 staple、Gatekeeper 检查和独立环境安装验收。

这篇内容适合三类人:使用 Developer ID 在 Mac App Store 之外发布 App 的独立开发者;在远程 Mac 或 CI 中维护 notarytoolstapler 自动化的人;以及发布包含插件、辅助工具、DMG 或 PKG 的小型团队。

里程碑一:先冻结公证现场,而不是盲目重试

遇到失败时,先把本次 Build、Archive、Export、Sign、Package、Submit 的产物和日志保存下来。至少记录以下信息:

  • 脱敏后的提交 ID;
  • notarytool submit 的完整输出;
  • 实际提交文件的 SHA-256 摘要;
  • 当前 xcode-select --print-path 的结果;
  • 最终文件名称、路径和容器类型;
  • 返回状态是认证失败、上传未完成、处理中、Invalid,还是 Accepted 后的后续失败。

Apple 的命令行工作流会返回提交 ID,你可以用这个 ID 继续查询状态和下载 JSON 日志;不要只依赖终端最后一行的摘要。notarytool log 返回的记录会包含问题路径、错误消息、严重级别和整体状态。(developer.apple.com)

先区分故障位置:

现象 优先判断 下一步
无法认证、无法上传 凭据、网络、Xcode 路径或会话环境 保留输出,检查凭据来源和当前工具链
状态长时间为处理中 服务端处理、网络或产物规模 不重复提交,先查询状态;必要时查看系统状态
Invalid 最终产物的签名、权限、嵌套代码或封装问题 下载日志,按首个有效错误修复
Acceptedstapler 失败 容器不可写、网络不可达或提交对象不匹配 确认文件未被替换,再检查 staple
用户下载后被 Gatekeeper 拦截 发布文件不是已验收文件,或票据、隔离属性、签名不一致 在干净 Mac 上重新测试真实下载文件

Apple 文档说明,公证服务通常会快速返回结果,但具体耗时会随文件数量、压缩方式和上传状态变化。官方给出的典型说明是:多数软件在 5 分钟内完成,98% 的软件在 15 分钟内完成;同时,文档还建议每天的公证提交不要超过 75 次。这些数字是服务端工作流提示,不是你可以用来强制判断“超时即失败”的固定规则。(developer.apple.com)

里程碑二:以最终 ZIP、DMG 或 PKG 为准

macOS App 公证失败,最容易踩的坑是只检查 Xcode 工程里的原始 .app,却没有检查真正上传给 Apple 的 ZIP、DMG 或 PKG。

正确顺序应当是:

  1. 完成 Build;
  2. 生成 Archive;
  3. Export 出 Developer ID 分发版本;
  4. 对导出的最终 App 检查签名;
  5. 创建 ZIP、DMG 或 PKG;
  6. 检查外层容器;
  7. 只提交准备交付给用户的那个文件。

Developer ID 不等于 Apple Distribution。Mac App Store 提交和 Mac App Store 之外的直接分发,是两条不同路径;直接分发需要使用适合 Developer ID 的签名和公证流程。Apple 也明确说明,ZIP、UDIF 格式 DMG 和 flat PKG 都可以进入公证流程,但 ZIP 本身不能直接签名或直接 staple。(developer.apple.com)

先检查签名主体和 Hardened Runtime:

codesign -dv --verbose=4 "Example.app"
codesign --verify --deep --strict --verbose=2 "Example.app"
codesign -d --entitlements :- "Example.app"

重点不是看到“有签名”就结束,而是核对:

  • 签名身份是否为预期的 Developer ID;
  • 是否包含安全时间戳;
  • Hardened Runtime 是否启用;
  • 是否残留 com.apple.security.get-task-allow
  • entitlements 是否为格式正确的 XML;
  • App 内的 Framework、插件、Login Item、Helper Tool、命令行工具是否分别签名;
  • 签名完成后是否又被脚本、安装器或打包工具修改。

Apple 明确指出,签名后再修改 Bundle,可能直接导致二进制签名无效;自定义流程还可能因为 get-task-allow、entitlements 编码或不适用的证书类型而无法通过公证。(developer.apple.com)

如果你使用 PKG,检查安装器实际会投放什么。安装器里的辅助工具、守护进程或其他可执行内容不能只依赖外层 PKG 的签名来掩盖内部代码问题。对于 DMG,则要确认创建镜像后没有再替换 App、图标、背景文件或快捷方式。

里程碑三:Invalid 后先读首个有效错误

notarytool 显示 Invalid 后的日志路径

不要把所有日志行都当成同等重要。先按 JSON 中的 pathseveritymessage 排序,优先处理第一个指向具体文件或目录的有效错误。

示例命令如下,文件名和提交 ID 必须使用你自己的脱敏记录:

xcrun notarytool info "<SUBMISSION_ID>" \
  --keychain-profile "<PROFILE_NAME>"

xcrun notarytool log "<SUBMISSION_ID>" \
  --keychain-profile "<PROFILE_NAME>" \
  "notary-log.json"

如果 info 显示 Invalid,再打开 notary-log.json,不要重新生成一个新提交来“验证是否只是偶发失败”。新的提交会让你失去原始产物与原始日志之间的对应关系。

常见错误可以这样分层处理:

  • 签名无效:查看日志指向的二进制路径,再用 codesign --verify 检查该组件;不要直接对整个 App 使用递归签名覆盖问题。
  • 缺少安全时间戳:确认签名命令或 Xcode 导出流程使用了适合分发的签名方式,然后重新签署受影响的组件。
  • 存在 get-task-allow:检查导出后的 entitlements,而不是只看 Debug 配置;该权限通常意味着你提交了调试用途的产物。
  • entitlements 格式错误:检查 XML、编码和 BOM;Apple 文档特别提醒命令行工具与 App 的 entitlements 文件需要使用正确格式。(developer.apple.com)
  • 证书类型不匹配:确认不是把 Mac App Store 使用的 Apple Distribution 身份拿来做 Developer ID 直接分发。
  • 嵌套组件遗漏:沿日志中的路径向上检查 Framework、插件、Helper 和 Login Item,确认每一层都来自同一套最终产物。

这里的关键是“按路径修复”,而不是“增加签名命令数量”。递归签名可能暂时改变输出,却会掩盖组件为什么没有在正确阶段签名。

里程碑四:由内到外重新签名和封装

嵌套代码的处理顺序决定了后面的检查是否有效。建议按以下时间线回退并重做:

  1. 清理旧的导出目录,不覆盖原始失败产物;
  2. 修复最内层可执行文件、插件或 Framework;
  3. 为这些组件重新签名;
  4. 再签署 App Bundle;
  5. 重新创建 PKG 或 DMG;
  6. 若 DMG 中包含 PKG,再签署外层 DMG;
  7. 重新计算最终文件摘要;
  8. 只提交最外层的最终交付文件。

Apple 的封装文档给出的原则是:如果 App 位于 PKG 中,PKG 又位于 DMG 中,应先签 App,再创建并签 PKG,最后创建并签 DMG;嵌套容器应从最低层向最高层处理。直接分发时,通常只提交最外层容器进行公证。(developer.apple.com)

三种常见格式的决策不要混用:

  • ZIP:适合直接解压 App,但 ZIP 不能直接签名,也不能直接 staple;应对其中的 App 或其他可 staple 项目处理后,再重新创建 ZIP。
  • DMG:适合带有拖拽安装界面的单 App 分发;签名和 staple 都作用于 DMG 文件本身。
  • PKG:适合需要把多个组件放入指定位置,或需要执行安装脚本的场景;应同时检查 PKG 内部将要安装的内容。

如果使用第三方安装器,而且安装器会投放其他可执行内容,不能默认一次公证覆盖所有阶段。Apple 的自定义工作流文档说明,这类场景可能需要先公证安装器的 payload,再把已公证内容放进外层安装器并再次公证外层文件。(developer.apple.com)

条件分支:该修复、回退还是重建

  • 若日志只指向一个内层组件,保留当前提交记录,修复该组件后重新导出。
  • 若多个路径同时出现签名无效,回退到 Archive 或 Export 阶段,重新生成干净产物。
  • 若外层容器在签名后被修改,删除外层容器并重新封装,不要继续使用旧 DMG 或 PKG。
  • 若凭据疑似错误,先验证当前凭据是否仍可用,再决定是否轮换;删除旧凭据前保存名称、来源和恢复方式。
  • 若远程 Mac 的 Xcode 路径与本地不同,先固定 DEVELOPER_DIRxcode-select,再比较两边的签名和封装输出。

里程碑五:Accepted 之后仍有发布验收

Accepted 后是否还要运行 stapler

需要。Accepted 表示 Apple 的公证服务接受了提交,并生成了可供 Gatekeeper 查询的票据;它不代表你正在分发的文件已经完成离线验收。

.app.dmg.pkg,可以继续执行:

xcrun stapler staple "<FINAL_FILE>"
xcrun stapler validate "<FINAL_FILE>"

ZIP 不能直接 staple。你应当对 ZIP 内需要交付的 App 或容器完成 staple,再重新创建一个新的 ZIP,并对这个新 ZIP 做摘要和测试。Apple 说明,staple 的价值在于让 Gatekeeper 即使无法联网,也能从分发文件中找到票据。(developer.apple.com)

随后使用 Gatekeeper 相关检查:

spctl --assess --type execute --verbose=4 "Example.app"
spctl --assess --type install --verbose=4 "Example.pkg"

spctl 的结果受测试 Mac 的安全策略影响,因此它不是唯一验收依据。你还需要把真实下载文件放到一台没有开发缓存、没有旧版本 Keychain 状态的 Mac 上,执行以下验收:

  • 从实际下载地址取得文件,而不是直接测试构建目录;
  • 删除旧 App,避免误启动旧版本;
  • 断开网络或限制网络后测试安装;
  • 从 DMG、PKG 或 ZIP 的真实流程开始;
  • 检查首次启动时的 Gatekeeper 行为;
  • 验证 App、插件、辅助工具和更新流程。

Apple 的发布文档也建议在不同于开发机的 Mac 上测试分发产物,因为开发机上的缓存、信任记录和本地工具链可能掩盖真实用户问题。(developer.apple.com)

里程碑六:远程 Mac 自动公证的固化

本地公证成功但远程 Mac 失败的定位方式

这种情况通常不是“远程 Mac 不能公证”,而是两台机器没有使用同一套输入条件。先逐项固定:

  • Xcode 路径:记录 xcode-select --print-path,必要时使用 DEVELOPER_DIR
  • 工具版本:记录 xcrun notarytool --help 的实际来源和脚本执行路径;
  • 凭据来源:优先使用 Keychain Profile 或受保护的密钥注入方式,避免把密码写入脚本;
  • 产物摘要:提交前后都保存 SHA-256;
  • 日志关联:把提交 ID、文件名、提交时间和构建编号写入同一份记录;
  • 图形会话与 SSH:不要假设通过 SSH 执行的任务拥有和图形会话相同的 Keychain 解锁、环境变量和权限。

Apple 的迁移文档说明,安装了多个 Xcode 时,xcrun 使用哪个 notarytool 取决于当前选择的开发者目录;也可以通过 DEVELOPER_DIR 在单次命令中指定工具链。(developer.apple.com)

自动化任务还要明确四个边界:

  1. 上传阶段多久没有进展时记录超时;
  2. Processing 状态如何查询,而不是立即重传;
  3. 哪些错误允许自动重试,哪些错误必须人工停止;
  4. 断线、重启或会话退出后,如何根据提交 ID 恢复,而不是重新提交。

远程 Mac 适合承载长期运行的签名、公证和发布链路,但前提是权限隔离、私钥保护和日志留存都已经设计好。你可以先阅读 远程 Mac 无人值守代码签名排障 相关内容,再把公证步骤接入现有发布脚本;如果还要处理 DMG 和 PKG,建议同时整理一份 macOS App 发布环境备份清单

如果当前开发电脑无法稳定保留 Developer ID 私钥、固定 Xcode 工具链和长期公证日志,比较稳妥的做法是:先在本地完成一次故障定位,再把签名、公证、staple 和 Gatekeeper 验收迁移到权限隔离的常驻远程 Mac,并用一个非正式版本完整跑通。需要临时建立这类环境时,可以先查看 VMSPIN 的 Mac 远程方案,重点比较会话保持、SSH 访问、图形界面和任务恢复是否符合你的发布流程。

macOS App 公证失败真正消耗的,通常不是一次上传本身,而是错误产物、未保存日志、多版本 Xcode、SSH 会话差异和用户侧验收缺失叠加后的返工。与其在一台经常关机、磁盘紧张或无法稳定保管私钥的开发电脑上反复尝试,不如把发布链路拆成“固定工具链、保存提交 ID、修最终文件、独立 Mac 验收”四个阶段。对需要临时算力或测试环境的独立开发者来说,租用 VMSPIN 的远程 Mac 会比临时拼接本地 Mac、Windows 虚拟机或不稳定 CI 更容易保留完整发布现场;但如果你需要长期满负载编译、物理 USB 设备或完全自主管理硬件,购买并维护自己的 Mac 仍然更合适。