Others

macOS 应用每次更新后都弹钥匙串密码框,我以为固定自签名证书能解决,实测不行

2016 字约 7 分钟

摘要我参与的一个 Electron 桌面应用在 macOS 上每次更新后都要用户输入一次登录密码来授权钥匙串。助手先判断换成固定的自签名证书就能消除,我写了测试应用去验证,结果推翻了这个判断,原因出在钥匙串里一个叫分区列表的检查上。

我参与了一个开源的 Electron 桌面应用,macOS 上有一个用户体验问题:每次更新到新版本,第一次打开都会弹出一个系统对话框,说应用想要访问钥匙串里的一个密钥,要求输入登录密码。点“始终允许”也没用,下个版本又来一次。

我想给项目提个 issue,让助手先评估一下能不能做到不弹。

为什么每个版本都弹

应用用 Electron 的 safeStorage 保存用户凭据。它的加密密钥放在钥匙串里,是一项叫 <应用名> Safe Storage 的条目。macOS 判断“现在来读这个条目的程序,是不是之前被允许的那个”,依据的是程序签名里的指定要求(designated requirement)。

助手只读地检查了两个应用的签名,没有启动,也没碰钥匙串。

应用签名指定要求
正式发布的包ad-hoc 签名cdhash H"c511b6ff…"
我们本机验收用的包一张固定的自签名证书identifier "…" and certificate root = H"142c…"

正式包打包时执行的是 codesign --sign -,也就是 ad-hoc 签名。没有证书,系统能认的身份只剩下程序内容的哈希,而内容哈希每个版本都不一样。于是每次更新,系统都把它当成一个陌生程序。

本机验收用的包,指定要求是“包标识符加证书”,换新版本也不变。更巧的是,之前一次真机验收里,我用这个签名连续升级了两轮,全程没有弹过钥匙串对话框,这是已经看到过的证据。

助手的判断:换成固定自签名证书

助手给了几个方案:

  • 买 Apple 开发者账号做 Developer ID 签名加公证。最彻底,但每年 99 美元,要老师或学校来定,我直接否决了。
  • 用一张固定的自签名证书给所有官方版本签名,私钥放在 CI 的密钥里。不花钱,有本机验收的证据,改动也小。
  • 不建议的做法有两个:给 ad-hoc 签名手写一条只看包标识符的指定要求,这样任何自称同一标识符的程序都能读走凭据,是个安全漏洞;或者绕开钥匙串,把密钥存成文件,等于明文保存。

我对第二个方案有一个疑问:“不是说要内容的哈希吗?怎么做到呢?我这做了别人那也能有用吗?”

助手这样解释。签名里有两样东西,作用不同。内容哈希每个版本都会变,只用来检查程序有没有被改过。身份规则才是钥匙串放行名单里存的东西。ad-hoc 签名没有证书,规则只能写成“内容哈希等于某个值”,所以每个版本都不一样。用证书签名后,规则变成“包标识符是 X,并且是用这张证书签的”,新版本内容变了,规则仍然成立。

别人的电脑上也一样,用户什么都不用做,不用安装或信任这张证书,因为匹配只比对证书的哈希。

这段话听起来很顺,但“不用信任证书”这一条,只在我装过这张证书的 Mac 上测过。助手自己也说,应该在一个干净的环境里验证。我同意先把这个验证做完,再决定要不要提 issue。

验证:证书固定,照样弹

助手写了测试应用,准备了几个变体:

  • 生成一张新的自签名证书,放在测试目录里一个单独的钥匙串文件中,不加入钥匙串搜索列表,也不受信任,模拟用户电脑上没有这张证书的情况。
  • 用这张证书签出 1 号版本和 2 号版本,两个版本内容不同,证书相同。
  • 1 号版本创建一个钥匙串条目,2 号版本去读它。

1 号版本正常创建了条目。2 号版本去读的时候,系统弹出了“想要访问你的钥匙串中的密钥”,要求输入登录密码。

助手上一轮说“固定自签名证书就能解决”,是错的。

测试进程卡在钥匙串调用里,不响应普通的结束信号,助手只好强制结束它。弹窗不会随进程消失,还要手动点一下“拒绝”。

原因:钥匙串有两道检查

我问它为什么会这样。助手解释,登录钥匙串对每个条目有两道检查。

第一道是访问名单(ACL)。 名单里记着哪些程序可以读,依据的就是前面说的指定要求。固定证书确实能通过这一道。

第二道是分区列表(partition list),macOS 10.12 开始引入。 每个条目上贴着“属于哪一类程序”的标签,系统按调用者的签名算一个分区 ID:

程序的签名分区 ID
Apple 自己的程序apple:
用 Apple 签发的证书签名,带 Team IDteamid:团队号
其他情况,包括 ad-hoc 和自签名证书cdhash:内容哈希

Team ID 只会出现在 Apple 签发的证书里。自签名证书哪怕固定不变,也只能落在最后一类,分区 ID 还是内容哈希。新版本的哈希不在条目的分区列表里,就过不了第二道。

分区列表不匹配时,必须输入钥匙串密码才能把新程序加进去,这就是每次更新看到的那个弹窗。如果只是 ACL 不匹配,点“允许”就行,不用输密码。10 月 8 日一次只读检查也看到过,本机验收那个条目的分区是 cdhash:,只是当时没往这个方向想。

为什么系统要这样设计

没有 Apple 给出的身份,系统没有办法判断“新版本是不是同一个开发者发布的”,只能请用户确认。要绕开这一步,就等于让任何程序都能读这把钥匙。

结论

不买 Apple 开发者账号,在产品层面没有正当的办法消除这个弹窗。每个版本更新后弹一次,是 ad-hoc 签名的必然代价。有了增量更新也一样,每个版本都会弹一次。

我提出的另一个疑问,助手也没解释清楚:之前那次验收,用验收证书升级为什么没弹窗?可能是那张证书在我的 Mac 上是受信任的,也可能是 Sparkle 在同一个路径上原地替换应用,而这次测试的两个版本放在不同目录。助手提议再补两组对照,把原因定下来,我觉得不影响结论,就没有做,这个疑点至今没有解释。

既然 Developer ID 不在考虑范围内,这个 issue 就没有可以实施的方案。助手建议,即使不提 issue,也可以在文档的已知限制里加一句“应用使用 ad-hoc 签名,每次更新后需要授权一次钥匙串”,让用户有个心理准备。我最后选择什么都不做:不提 issue,不改仓库,把测试清理干净,就当没发生过。

助手清理得很彻底:钥匙串里测试应用创建的两个条目,系统里这两个测试应用的权限记录和打开方式的注册,外接盘上带 4 份 Electron 应用副本的测试目录,约 1 GB,都挪进了废纸篓。

一个教训

我们手里有一个“已经实测过的证据”,就很容易顺着它推广。这次的证据是在一台特殊的机器上得到的:那张证书是我自己装过的。把它推到“别人那里也行”之前,应该先在一个没有这些前提的环境里验证。验证用的是一张没人信任的新证书和一个全新的钥匙串条目,只花了几分钟,却推翻了一个差点写进 issue 的结论。

评论