可重复构建,签名密钥,和二进制存储库

今年早些时候,我们报告了在可重复构建方面的进展。与此同时,越来越多的应用正在使用可重复构建。你可以在这里看到一些统计数据:和 2022 年 11 月时的 20 个应用相比,到 2023 年 9 月,可重复构建的应用数量增长了近十倍,达到了 191 个。大约三分之二的新增应用使用了可重复构建。但是简单来说,不使用那些“术语”,“可重复构建”到底意味着什么呢?

多年以来,F-Droid 为每个应用创建专属密钥对发布的 APK 签名,但现在 F-Droid 通过可重复构建发布使用上游开发者签名的 APK。这向你证明开发者确认:“这是我想要发布的,这是从我的代码构建的”。同时由 F-Droid 分发你可以得知:“F-Droid 也确认了这是从开发者提供的代码构建的 APK。”因此双方都没有塞进应用源代码存储库中没有的东西。

F-Droid 中的验证,仍然使用简单的说法,是这样进行的:应用在 F-Droid 的构建服务器上从源代码构建。然后抓取开发者构建的相应 APK 与之进行比较。唯一的不同应该是签名文件 - 因为来自开发者的 APK 使用他们的私钥签名,F-Droid 无法获取。如果是这种情况(即两个 APK 匹配),它被证明是完全相同的“二进制文件” - 所以 F-Droid 可以分发由开发者签名的 APK。

你可以在这里看到这个流程的大纲,相关技术细节参见这个文档

收益和风险

一些优势显而易见:首先想到的是更高的信任程度,因为现在双方(F-Droid 和开发者)都能确认分发的 APK 的完整性。但还有更多好处。例如,如果开发者发布了一个“紧急更新”(例如,为了修复安全问题,或者一些关键功能损坏)你不再需要等待一个或者两个构建周期。因为你从 F-Droid 安装的 APK 是由开发者的私钥签名,你可以直接从其他渠道获取开发者提供的 APK(例如,从应用在 Codeberg, GitLab 或 Github 上的存储库中)并更新 - 如果你信任开发者 - 你只能如此,因为这些构建尚未被 F-Droid 验证。

那么有什么“缺点”吗?为什么标题提到了“风险”?唔… F-Droid 现在提供不是自己签名的 APK。所以如果一个应用的源代码存储库被恶意方接管了,然后修改代码并提供他们自己的发布,而原作者,例如,正在休假,住院或遭遇其他状况,会发生什么?他们当然会用自己的私钥签名应用(但愿原作者的私钥是安全的)。但是我们必须考虑并处理这种情况。因此我们有:

AllowedAPKSigningKeys

在 F-Droid,每次打包一个可重复构建应用时,相应的用于 APK 签名的开发者证书的哈希值和其他元数据一起存储在 F-Droid 这边。构建元数据中的键是 AllowedAPKSigningKeys。因此当开发者的 APK 被抓取用于比较时,签名将按如下方式比较:

apksigner verify --print-certs app-release.apk 2>/dev/null \
 | sed -n 's/^Signer #1 certificate SHA-256 digest: \(.*\)/\1/p'

将给出用于签名 APK 的证书的 SHA-256 哈希值。如果它不匹配,应用会被拒绝 - 并且构建会被认为“失败”。这确保了 F-Droid 仅分发预期的应用,由合适的密钥签名 - 并前上述潜在的“恶意方”无法轻易“塞进东西”。一个在这种场景下很好的安全特性,尽管它原本有其他目的:

二进制文件存储库

你问那是什么?呃,F-Droid 并不与 F-Droid 运行的单一存储库绑定。每个人都可以建议自定义存储库。F-Droid 的所有代码也都是自由开源的,和它分发的所有应用一样。最知名的第三方存储库之一可能是 “IzzyOnDroid Repo” 或 “IzzySoft Repo”,目前提供超过 1,111 款应用。在那里,它的所有应用都在 2023 年 8 月的第一周设置了 AllowedAPKSigningKeys。因为这个存储库直接从开发者的存储库获取 APK 而不是从源代码构建,这个额外的安全措施尤其有用 - 出于上面概述的理由:为了确保所有更新都是“合法的”(而不是被恶意行为者放在存储库里的)。

因此每当更新器从相应应用在 Codeberg, GitLab, Github 等上的存储库中抓取一个新 APK 时,fdroidserver 验证它是使用作者的密钥签名的。否则,它永远不会被包含到存储快的索引中(所以它不会提供给你因此无法危害你)。反之,存储库的维护者会收到提醒并进行调查:

2023-09-01 20:56:25,845 WARNING: "com.example.app_123.apk" is signed by a key that is not allowed:
a0fe1234567890abcdefa0fe1234567890abcdefa0fe1234567890abcdef1234

这意味着:现在到了调查发生什么了的时候了。因为在存储库维护者知道新密钥之前 APK 永远不会进入索引,可以进行彻底调查而不必匆忙。

这真的是一个常见问题吗?

很不幸,是的。在更新 IzzyOnDroid 存储库元数据期间,1105 个被检查的应用中的 26 个遇到了这个问题:自从第一个版本出现以来签名密钥发生了变化。这占到了被检查的应用的 2.35%。仿佛是这还不够坏,在接下来的 4 周中,没有一周没有至少又一个应用出现这个问题。原始数据为:一年有 52 周,存储库里有 1000+ 应用 - 每周一个应用,大约每 20 个应用里就有一个 (5%) 发生这个问题至少一次!

对于每个案例,将联系相应的开发者找到原因,然后(但愿)修复问题。你或许想知道这可能是什么造成的,所以这里给出了一些原因。所有这些意味着这样或那样的方式:

> “啊,我把签名密钥搞丢了……”

  • 硬盘崩溃(或者整台电脑都坏了)
  • 不小心删掉了有“重要内容”的文件夹
  • 签名曾经是一名已经离开(带着密钥)的团队成员做的
  • 开发环境从头设置了,或者换到新设备了 - 并且不知为何密钥库没有一起迁移(当发现这个问题时,原来的环境已经无法访问了)
  • 开发者在开始开发时用了一个“调试密钥”(如果你在本地给自己开发没问题 - 但不要这样做如果应用会被分发),所以他们不得不切换到一个“发布密钥”
  • 原来的密钥“太弱”并且不得不用更强的替代

在最后两种情况中,合法性很容易通过提供最新 APK 的两种变体证明:一个用原来的密钥签名,另一个用新密钥。因此两个 APK 可以用类似于可重复构建使用的流程进行比较 - 如果原来的密钥没有因为已经“废弃”被删掉。

学到的第一课:存储库维护者

在你的二进制存储库中为所有应用设置 AllowedAPKSigningKeys 是个好措施。这个问题太过常见以至于无法忽略。你分发的应用必须安全:你对使用你的存储库的人们负有责任。进入这样的更新会被已经安装此应用的设备拒绝(“不兼容的签名”),第一次“全新”安装此应用的设备无法检测到这个问题。也要保护他们。

好的一面是:没有任何一个案例里,密钥更换是由于恶意行为。这并不意味着这种情况永远不会发生 - 但很高兴知道我们可以相对信任我们的 FOSS 开发者。

怀德一面是:全包密钥库安全的重要性似乎不够不言自明。我们需要提高认知度。

学到的第二课:怎样保管好你的密钥,丢失之后应该采取什么措施?

很明显:备份!不要仅仅在你的开发设备上。设置另一个备份 - 例如在一个放在安全地方的加密移动存储器上,在一个其他地方(朋友,家人或者云服务)的可信服务器/设备上 -再一次,一个加密备份)。并且确保你知道如何恢复。你可能不需要一个 F-Droid 团队执行的那种备份仪式 - 但需要实施一些措施。不,将它们上传到你的应用的公开 git 存储库是一个好主意,甚至对于备份也是不行(别笑,这也发生过)。

不太显而易见的是:确保某人/某物可以为你“担保”。再一次,从遇到的案例中:

  • 签名你的提交 (GPG/PGP),最好是全部并且从一开始就是。当然也要保管好你的 GPG/PGP 密钥。这样你在事件发生“之后”的提交可以证明你仍然控制这密钥。不太可能有人掌握了你的 Git 存储库你的 GPG/PGP 却没有你给应用签名的密钥库。
  • 提供不同的(独立)联系方式。你的 Git 存储库被接管可能意味着它通过你被接管的邮件账户进入 - 但可能没有你的 XMPP 或 Matrix 账户。这个信息也应该从之前就有:所有之后提供的信息都可能来自“恶意行为者”所以不能证明你的所有权。
  • 再一次提前介绍一个认识你并且可以为你担保的人 - 例如他们可以通过给你打电话或线下见面验证你的故事,并证实此事。理想情况下,此人是你的存储库的贡献者。但是最重要的是社区必须知道他们可以信任此人。要么是因为此人是知名人士
    • 或者至少是事件之前很久就出现的人。

学到的第三课:发生之后要做什么?

最重要的是:绝对不要简单地“掩盖”或“沉默”。这是一个安全事件;把它扫到地毯下面只会让你彻底失去信任。没有卸载/重新安装无法更新,所以人们总会注意到的(除非在 Google Play 商店上,它重新签名 APK)。宁愿对此保持透明:

  • 在更新日志中(以及每个版本的更新日志中如果你使用 FastlaneTriple-T)提及此事。
  • 或许可以写一篇关于它的短文(例如,你的应用存储库中的一个公开议题,或者一篇博客)解释发生了什么,你不得不做什么以及你学到了什么(所以别人也可以从此事中学习)。
  • 如果你想要证明你的身份(你一直签名提交,你有可以担保你的人,等等),在更新日志/文章中提及以便其他人验证。如果你仍然可以访问你的旧密钥库,但出于一些理由不得不更换,提供一个使用旧密钥签名的相同 APK,这可以证明:验证可重复构建的方法可以用于比较两个 APK - 应该仅仅签名不同。
  • 没人“生来就聪明” - 我们终生都是学生(“学而有止境者不是真学者”)。所以是的,这不好,你可能感到“羞愧”。但通过承认你的错误可以彰显品质建立信任。由此人们知道你不会“愚弄”他们并且他们可以相信你。