公证通过,双击即死——一个 entitlement 的线上事故
Pier 2.0.0 复盘:为了消掉一个 keychain 授权弹窗,带上了 restricted entitlement;公证照常通过,用户双击直接被 AMFI 杀掉。发布当晚回退出 2.0.1。
Pier 2.0.0 发布当晚就出了线上事故:下载、安装、双击——「Pier 无法打开」。不是崩溃,是启动即被系统杀掉。修复版 2.0.1 一小时后顶上。这篇复盘整个链条:一个为了改善体验的改动,怎么穿过公证流程炸在用户桌面上。
前传:那个 keychain 授权弹窗
Pier 的许可证状态存在 keychain 里(比 UserDefaults 难篡改,删 app 重装仍在)。1.3.x 推广期收到过反馈:新装用户首次启动,macOS 弹出授权 sheet——「Pier 想要使用 keychain 中的机密信息」。
对一个刚下载的菜单栏工具,这个弹窗观感极差:用户还没建立任何信任,你上来就要碰「机密信息」。当时的止血方案是推广期完全不碰 keychain(免费开关短路掉所有读写)。但 2.0 要开始真实的试用/激活流程,keychain 绕不开了,得根治。
根治方案,以及它怎么炸的
macOS 上消掉这个 sheet 的正路是切到 iOS 风格的 data protection keychain:加 keychain-access-groups entitlement(access group 是 Team ID + bundle id),SecItemAdd 就不再触发授权弹窗。本机开发构建验证通过,打包、签名、公证、上线。
问题在于 keychain-access-groups 是 restricted entitlement:这类 entitlement 要求 app 内嵌一份 provisioning profile 来证明「Apple 允许这个签名主体使用这个能力」。App Store 分发的包天然带 profile;Developer ID 直发的 dmg 默认没有。签名时带上了 entitlement、却没有嵌 embedded.provisionprofile,AMFI(Apple Mobile File Integrity)在加载时校验不过,直接 SIGKILL——连 main 函数都跑不到。
最要命的一环:公证对此完全放行。公证检查的是恶意代码和签名完整性,不校验「entitlement 与分发方式是否匹配」。于是一个必死的构建拿着 Apple 的公证章走完了全部发布流程。
为什么本机没发现
开发路径上没有任何一环会踩这条检查:swift build 直接跑二进制不经过 AMFI 的 entitlement 校验路径;调试构建的签名上下文和 Developer ID 分发包不同。唯一能暴露问题的动作是:把公证完的 dmg 挂载、拖进 Applications、像用户一样双击。这一步当时不在发布清单里。
修复与善后
当晚做了三件事:
- 回退:撤掉 entitlement,回到标准 login keychain,发 2.0.1。授权 sheet 的问题另找出路(新装用户首次写入自己创建的 item 不弹 sheet,实际影响比预想小);
- 立碑:在
Keychain.swift顶部留下注释,写明「不要切 data protection keychain」的完整原因和正确做法(去 Apple Developer 后台建 Developer ID profile 并在打包时嵌入)——防止未来的自己好心办坏事; - 改流程:发布 skill 里加了一步「启动验证」,一句话版本是公证通过 ≠ 能启动。公证后的产物必须完整走一遍用户路径:挂载 dmg、安装、双击。
复盘
- 公证是安全审查,不是可用性审查。它保证包里没有恶意代码,不保证你的 app 能在用户机器上活过第一次 exec;
- restricted entitlement 是带资格审查的能力。加任何一个之前先回答:我的分发方式(App Store / Developer ID / 未签名)有没有资格用它、凭证(profile)嵌了没有;
- 发布清单的最后一项永远是「装一遍」。所有构建、签名、公证环节的绿灯加起来,都替代不了在干净环境里双击那一下;
- 教训要写在代码旁边。事故记忆会淡,
git log没人翻,但下一个想「优化」keychain 的人一定会打开Keychain.swift——注释就等在那里。
2.0.0 在线上活了不到一小时。下载过的用户,重新下载就好——再次抱歉。
留言