当你完成代码签名、提交公证、满怀期待地在目标机器上运行应用时,Gatekeeper 仍然弹出一个红框宣告「无法验证开发者」—— 这种挫败感几乎是每一位 macOS 桌面应用开发者在转向正式分发时都会经历的时刻。问题的根源往往不在签名或公证本身,而在于两者之间的验证链路出现了信息缺失或状态不一致。本文聚焦三类高频故障模式,给出从诊断到修复的完整工程路径。
公证成功但 Gatekeeper 仍拦截
这是最常见的「假阳性」场景。开发者看到 xcrun notarytool 返回 Successfully uploaded 且收到票据,却没有意识到公证的对象与最终分发的实体并不对应。公证确认的是被提交 artifacts 的完整性,而 Gatekeeper 在运行时验证的是经过打包、可能经过多次重新签名的分发容器。如果两者不一致,Gatekeeper 就会拒绝执行。
解决这个问题的第一步是确保公证的对象与分发对象完全一致。最稳妥的做法是:先将应用打包为 DMG 或 PKG,再对整个容器进行签名与公证,最后将公证票据 stapled 到分发容器上。如果当前流程是在一个裸二进制上完成公证后再塞进 DMG,就会导致公证票据与分发实体脱节,Gatekeeper 自然无法完成验证。
验证当前分发实体是否包含有效票据,可以使用 spctl --assess --type execute --verbose --context context:execvali --requirement "<requirement string>" /Applications/YourApp.app 命令。如果返回结果是 killcd 或 unsigned,则说明应用尚未通过 Gatekeeper 验证。进一步检查可以使用 codesign --verify --deep --strict --verbose=2 /Applications/YourApp.app 逐层检查嵌套签名状态,任何一层断裂都会导致整条验证链失败。
深层签名与嵌套 dylib 断裂
macOS 的代码签名机制采用扁平化验证策略,主二进制内的所有嵌入组件 —— 包括动态库(dylib)、框架(framework)、插件(plug-in)—— 都必须是有效签名状态,缺一不可。这一特性在实践中经常被忽视,尤其是当项目依赖外部编译产物或通过 CocoaPods/SPM 引入第三方库时。
深层签名验证失败通常表现为两类症状:其一是启动时直接被系统拒绝,错误信息指向某个具体的动态库无法通过签名校验;其二是应用可以启动但运行到特定功能时崩溃,日志显示加载特定 dylib 时发生 cannot load incompatible code signature。前者属于硬性阻断,后者则是运行时签名校验失败,修复难度相近但诊断路径不同。
针对硬性阻断,优先执行 codesign --verify --deep --strict /path/to/YourApp.app/Contents/Frameworks/*.dylib 对所有嵌套二进制逐一检查。如果某一行返回 invalid signature 或 code object is not signed at all,就定位到了故障源。修复方案是确保该库在链接到主应用之前已经完成签名,且签名时使用的身份与主应用一致。对于通过 CMake 或其他构建系统管理的第三方库,建议在构建流程中增加 post-build 签名步骤,使用 codesign --force --sign "Developer ID Application: Your Team Name" --deep /path/to/library.dylib 完成签名。
对于运行时签名校验失败的场景,问题通常出在代码签名标志(code signing flags)与运行时加载机制的冲突。常见原因包括主应用开启 library validation 但加载了未签名的第三方库,或者 dylib 的签名时间戳晚于主应用,导致签名状态不一致。此时可以尝试在签名时添加 --options runtime 参数强制开启运行时签名校验,同时在 entitlements 文件中配置 com.apple.security.cs.allow-unsigned-dependent-libs=true 作为临时调试手段(注意:此参数仅用于开发调试,不得出现在正式分发包中)。
打包格式与票据锚定的工程参数
除了签名与嵌套组件的问题,打包格式的选择与公证票据的锚定方式也直接影响 Gatekeeper 的行为。DMG 与 PKG 在处理公证票据时存在微妙差异:DMG 本身不需要签名,票据锚定在内部的 app bundle 上即可,但分发时的 DMG 文件名建议与公证时一致,避免哈希校验失败;PKG 则必须使用 Developer ID Installer 证书签名,且公证票据应该锚定在 PKG 本身上而非其内部的 app bundle。
对于混合使用 CocoaPods 管理依赖的项目,尤其需要注意主项目与 Pods 目录之间的签名一致性。建议在项目的 post_install 钩子中添加签名配置,确保所有 Pods 中的二进制在集成到主项目之前已经完成统一签名。一个经过验证的参数配置如下:构建阶段使用 CODE_SIGN_IDENTITY="Developer ID Application: Your Team Name"、CODE_SIGN_STYLE=Manual,并通过 OTHER_CODE_SIGN_FLAGS 设置 --deep --force --options runtime。在 CI 环境中,需要确保签名私钥在 Keychain 中设置为不可导出状态,并在签名前通过 security unlock-keychain 解锁对应钥匙串。
如果应用包含启动助手(launcher)或辅助可执行文件,这些组件同样需要签名且签名身份必须与主应用一致。不能出现主应用使用 Developer ID Application 而辅助工具使用 Developer ID Installer 的混用情况,因为 Gatekeeper 在验证整条加载链时要求所有节点的签名身份来自同一 Team ID。
工程实践中的监控与回滚
在 CI/CD 流程中集成签名与公证校验时,建议在构建末尾增加自动化验证步骤。核心验证命令是 xcrun stapler validate /path/to/YourApp.app,如果返回成功则说明票据已正确锚定。对于 DMG 分发,可以额外运行 xcrun stapler validate /path/to/YourDist.dmg 确认容器级别的票据有效性。在测试阶段,使用干净的系统镜像(可以通过 macOS Recovery 或虚拟机实现)模拟终端用户的 Gatekeeper 行为,避免本地开发者证书缓存掩盖潜在问题。
当发布后遇到紧急回滚时,最快的恢复路径是移除应用并清理公证票据缓存:codesign --remove /Applications/YourApp.app 后重新签名与公证。如果问题出在签名链的中段(如某个 dylib 被错误签名),需要定位到具体组件、修复签名、重新打包并提交重新公证,同时通过 notarytool log 查看公证服务返回的详细诊断信息,其中会指出具体的无效组件路径。
资料来源
- Apple Developer Forums: Notarization succeeds, but Gatekeeper check still fails(https://developer.apple.com/forums/thread/126073)
- Debugging macOS Notarization: Solving 'Invalid Signature'(https://coldfusion-example.blogspot.com/2025/12/debugging-macos-notarization-solving.html)
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。