F-Droid 是一个广受欢迎的 Android 应用商店替代品,尤其以其专注于自由和开源软件(FOSS)的主仓库而闻名。F-Droid 经常被安全和隐私爱好者推荐,但在实际使用中,它与 Google Play 商店相比如何呢?本文将重点讨论 F-Droid 存在的一些主要安全问题,供你在选择时参考。
在开始之前,有几点需要注意:
- 本文的主要目的是为用户提供信息,帮助他们做出负责任的选择,而不是贬低他人的工作。我对任何出于善意的工作都表示尊重。同样,请不要误解本文的意图。
- 你有自己的理由使用开源或自由软件,这些理由不会在这里讨论。开发模式不应成为不良实践的借口,也不应让你相信它能提供它无法提供的强有力保证。
- 本文中的许多信息来自官方和可信来源,但欢迎你自己进行研究。
- 这些分析没有考虑威胁模型和个人偏好。作为本文的作者,我只对事实感兴趣,而不是意识形态。
这不是一篇深入的安全审查,也不详尽无遗。
1. 信任第三方的问题
要理解为什么这是一个问题,你需要了解一些关于 F-Droid 架构的知识,它与其他应用商店的不同之处,以及 Android 平台的安全模型(本文列出的一些问题有些超出了操作系统安全模型的范围,但大多数问题都在其范围内)。
与其他应用商店不同,F-Droid 使用自己的签名密钥(每个应用唯一)对主仓库中的所有应用进行签名,除了极少数可重现构建的应用。签名是一种数学方案,用于保证你下载的应用的真实性。在安装应用时,Android 会在整个操作系统中固定签名(包括用户配置文件):这就是所谓的首次使用信任模型,因为所有后续的应用更新都必须具有相应的签名才能安装。
通常情况下,开发者应该在上传到分发渠道之前对自己的应用进行签名,无论是通过网站还是传统应用商店(或两者兼有)。你不需要信任来源(通常由开发者推荐),除了第一次安装:未来的更新将通过加密方式保证其真实性。F-Droid 的问题在于,所有应用都由同一个第三方(F-Droid)签名,而这个第三方并不是开发者。你现在需要信任另一个第三方,因为你仍然需要信任开发者,这并不理想:信任的第三方越少越好。
另一方面,Google Play 商店现在也管理应用签名密钥,因为自 2021 年 8 月起,新应用必须使用 Play 应用签名功能。这些签名密钥可以上传或自动生成,并由 Google Cloud 密钥管理服务安全存储。需要注意的是,开发者仍然需要使用上传密钥对应用进行签名,以便 Google 在签名之前验证其真实性。对于 2021 年 8 月之前创建的应用,可能尚未选择加入 Play 应用签名,开发者仍然管理私钥并负责其安全性,因为私钥泄露可能允许第三方签名和分发恶意代码。
F-Droid 要求应用的源代码不包含任何专有库或广告服务,根据其包含政策。通常,这意味着一些开发者必须维护一个稍微不同的代码库版本,以符合 F-Droid 的要求。此外,他们的“质量控制”几乎没有提供任何保证,因为访问源代码并不意味着可以轻松地进行审查。说 Play 商店充满了恶意应用并不是重点:虚假的安全感是一个真正的问题。用户不应认为 F-Droid 主仓库中没有恶意应用,但不幸的是,许多人倾向于相信这一点。
但是……我不能只信任 F-Droid 就完事了吗?
你不必相信我的话:他们自己也公开承认这是一个非常基础的过程,依赖于不良枚举(这并不奏效),即通过一些脚本扫描代码以查找专有二进制文件和已知的跟踪器。因此,你仍然需要信任上游开发者,这对任何应用商店都是如此。
一个诱人的想法是将 F-Droid 与桌面 Linux 模型进行比较,用户默认信任他们的发行版维护者(如果你已经信任操作系统,这可能是合理的),但桌面平台本质上是一个混乱且异构的环境,好坏参半。它真的不应该以任何方式与 Android 平台进行比较。
虽然我们已经看到 F-Droid 控制着签名服务器(类似于 Play 应用签名),但 F-Droid 还完全控制着用于构建应用的临时虚拟机(VM)的构建服务器。从 2022 年 6 月到 11 月,他们的客户虚拟机镜像正式运行了一个已经停止支持的 Debian LTS 版本。值得注意的是,Debian LTS 是 Debian 的一个独立项目,旨在延长 Debian 项目认为已经停止支持的版本的寿命,并且不受 Debian 安全团队的处理。他们使用的版本(Debian Stretch)实际上在两年前就已经停止支持。毫无疑问,这引发了对其整个基础设施安全性的质疑。
你如何确保应用商店对其交付的代码负责?
F-Droid 的答案是有趣但很少使用的可重现构建。虽然确定性构建在理论上是一个好主意,但它要求开发者的工具链与 F-Droid 提供的工具链相匹配。这在两端都需要额外的工作,有时会导致应用更新严重滞后,因此可重现构建并不像我们希望的那样普遍。需要注意的是,主仓库中的可重现构建只能由开发者签名。
Google 的方法是应用捆绑包的代码透明度,这是一个简单的想法,解决了 Play 应用签名的一些问题。在应用捆绑包上传到 Play 商店之前,开发者会使用私钥签署一个 JSON Web Token(JWT)。该令牌包含 DEX 文件和原生 .so
库的列表及其哈希值,允许最终用户验证运行代码是由应用开发者构建和签名的。然而,代码透明度有一些已知的限制:并非所有资源都可以验证,并且这种验证只能手动完成,因为它不是 Android 平台本身的一部分(因此操作系统目前无法强制执行代码透明度文件的要求)。尽管不完整,代码透明度仍然有帮助,易于实现,因此随着时间的推移,我们应该会看到更多的应用采用它。
其他应用商店如亚马逊呢?
据我所知,亚马逊应用商店一直在用自己的代码包装 APK(包括他们自己的跟踪器),这意味着他们实际上是在重新签名提交的 APK。
如果你正确理解了上面的信息,Google 不能对尚未选择加入 Play 应用签名的应用这样做。至于涉及 Play 应用签名的应用,虽然 Google 技术上可以像亚马逊一样引入自己的代码,但他们不会在不告知的情况下这样做,因为这很容易被开发者和研究人员发现。他们在 Android 应用开发平台上有其他手段来实现这一点。然而,基于这一原则相信他们不会这样做并不是一个强有力的保证:因此上面提到的关于应用捆绑包代码透明度的段落。
华为应用市场似乎采用了类似的方法,提交的应用可以由开发者签名,但新应用将由华为重新签名。
2. F-Droid 荒谬的包含政策及其后果
F-Droid 为了贯彻其“对 Android 平台上自由和开源软件(FOSS)的热情”,要求开发者遵守严格的包含政策,以便他们的应用能够托管在主仓库中。根据这一政策,F-Droid 要求应用的源代码不包含任何专有库或广告服务。这一严格的要求已被证明对开发者甚至最终用户有害。
由于 F-Droid 的包含政策,通常一些开发者必须维护一个稍微不同的代码库版本,以使他们的应用符合 F-Droid 的要求。对开发者来说,这不仅意味着花费更多的时间和精力,而且在某些情况下,还意味着使用可能过时的库和组件。有时,F-Droid 包含政策所施加的限制也会对最终用户产生连锁反应,如下面的 Snikket 案例所示。
2022 年 12 月,Snikket 项目发布了一篇博客文章,向从 F-Droid 下载其应用的用户发出警告。他们试图缓解用户的恐慌,如果用户收到 F-Droid 的警告,“告诉他们该应用 [Snikket] 存在漏洞,并‘建议立即卸载’”。在随后的博客文章中,Snikket 澄清说,F-Droid 的这一警告“并不完全准确,因为问题不在于 Snikket 应用本身,而在于 F-Droid 自己构建的应用 使用了 过时的 WebRTC 库”(强调部分)。
事实上,正如 Snikket 项目的第一篇博客文章所详述的那样,Snikket 的 F-Droid 版本的 WebRTC 组件从 Google 的 Maven 仓库中拉取了第三方二进制文件(该仓库在2020 年 1 月停止发布新版本),可能是为了遵守包含政策中禁止使用“非自由”依赖项和构建工具的部分。请注意,发布在 Play 商店上的开发者签名版本的 Snikket 不受此问题影响,因为它们是用现代 WebRTC 版本构建的。此外,Snikket 的第二篇博客文章揭示了用于其 F-Droid 应用的较旧第三方 WebRTC 版本实际上阻碍了从上游添加新改进。
总的来说,这个案例研究突显了 F-Droid 的包含政策如何最终通过迫使应用开发者采用可能过时的开发工具和构建流程来损害最终用户,以服务于其主导的 FOSS 意识形态。
3. 更新缓慢且不规律
由于你在混合中添加了另一个第三方,该第三方现在负责提供应用的适当构建版本:这在传统的 Linux 发行版及其打包系统中很常见。他们必须定期跟上上游的步伐,但很少有人能做到这一点(Arch Linux 是其中之一)。其他发行版,如 Debian,更喜欢进行广泛的下游更改,并为分配给 CVE 的漏洞子集提供安全修复(是的,这听起来很糟糕,但这是另一个话题)。
F-Droid 不仅要求应用进行特定更改以符合其包含政策,这通常会导致更多的维护工作,而且它还有一种相当奇怪的方式来触发新构建。其构建过程的一部分似乎是自动化的,这是你至少可以期待的。现在的问题是:应用签名密钥位于一个隔离的服务器上(意味着它没有连接到任何网络,至少他们声称如此:参见他们的建议作为参考),这迫使更新周期变得不规律,需要人工手动触发签名过程。这远非理想情况,你可能会说这是至少可以预期的,因为将所有签名密钥委托给一个第三方,你也可能引入一个单点故障。如果他们的系统被攻破(无论是内部还是外部),这可能会导致影响大量用户的严重安全问题。
这是 Signal 拒绝支持在 F-Droid 官方仓库中包含第三方构建的主要原因之一。虽然这个 GitHub 问题已经相当老了,但许多观点在今天仍然成立。
考虑到所有这些,以及他们的构建过程经常因使用过时的工具而中断的事实,你必须预期更新速度远远慢于传统的分发系统。更新缓慢意味着你将比应有的情况更频繁地暴露在安全漏洞中。例如,通过 F-Droid 官方仓库更新完整的浏览器是不明智的。F-Droid 的第三方仓库在一定程度上缓解了更新缓慢的问题,因为它们可以直接由开发者管理。但这也不是理想的,正如你将在下面看到的那样。
4. 客户端和应用的低目标 API 级别(SDK)
SDK 代表软件开发工具包,是构建特定平台应用的软件集合。在 Android 上,更高的 SDK 级别意味着你可以利用现代 API 级别,每个迭代都带来了安全和隐私的改进。例如,API 级别 31 利用了 Android 12 上的所有这些改进。
你可能已经知道,Android 有一个强大的沙盒模型,每个应用都在沙盒中运行。可以说,使用最高 API 级别编译的应用受益于应用沙盒的所有最新改进;而使用较旧 API 级别编译的过时应用则具有较弱的沙盒。
# b/35917228 - /proc/misc 访问
# 这将在未来的 Android 版本中消失
allow untrusted_app_25 proc_misc:file r_file_perms;
# 访问 /proc/tty/drivers,以允许应用确定它们是否在模拟环境中运行。
# b/33214085 b/33814662 b/33791054 b/33211769
# https://github.com/strazzere/anti-emulator/blob/master/AntiEmulator/src/diff/strazzere/anti/emulator/FindEmulator.java
# 这将在未来的 Android 版本中消失
allow untrusted_app_25 proc_tty_drivers:file r_file_perms;
这只是较旧 API 级别上必须做出的 SELinux 例外的一个示例,以便你理解为什么这很重要。
事实证明,官方的 F-Droid 客户端并不太关心这一点,因为它远远落后,目标 API 级别为 25(Android 7.1),上面展示了一些 SELinux 例外。作为一种变通方法,一些用户推荐使用第三方客户端,如 Foxy Droid 或 Aurora Droid。虽然这些客户端在技术上可能更好,但其中一些客户端的维护状况不佳,而且它们还引入了另一个第三方。Droid-ify(最近更名为 Neo-Store)在大多数方面似乎比官方客户端更好。
此外,F-Droid 没有强制执行官方仓库的最低目标 SDK。Play 商店对此相当严格,要求新应用和更新应用至少达到一定的 API 级别:
- 自 2021 年 8 月起,Play 商店要求新应用至少目标 API 级别为 30。
- 自 2021 年 11 月起,现有应用必须至少目标 API 级别为 30 才能提交更新。
虽然这可能看起来很麻烦,但这是保持应用生态系统现代化和健康的必要条件。在这里,F-Droid 向开发者(甚至用户)传递了错误的信息,因为他们应该关心这一点,这也是为什么我们中的许多人认为它甚至可能对 FOSS 生态系统有害。向后兼容性通常是安全的敌人,虽然在便利性和过时性之间有一个中间地带,但它不应该被夸大。由于这种理念,F-Droid 的主仓库中充满了来自另一个时代的过时应用,只是为了这些应用能够在十多年前的 Android 4.0 Ice Cream Sandwich 上运行。我们不要犯桌面平台同样的错误:相反,向你的供应商抱怨他们销售的设备没有良好的操作系统/固件支持。
开发者没有理由不随着每个 Android 版本的发布而增加目标 SDK 版本(targetSdkVersion
)。此属性与应用程序所针对的平台版本相匹配,并允许在现代操作系统上访问现代改进、规则和功能。应用程序仍然可以确保向后兼容性,以便它可以在较旧的平台上运行:minSdkVersion
属性通知系统应用程序运行所需的最低 API 级别。不过,将其设置得太低并不实际,因为这需要大量回退代码(其中大部分由通用库处理)和单独的代码路径。
在撰写本文时:
- Android 9 是仍在接收安全更新的最旧 Android 版本。
- 全球约 80% 的 Android 设备至少运行 Android 8.0 Oreo。
总体统计数据并不反映给定应用程序的真实使用情况(使用旧设备的人不一定使用你的应用程序)。如果有什么不同的话,它应该被视为低估。
5. 普遍缺乏良好实践
F-Droid 客户端允许在同一个应用程序中共存多个仓库。上面强调的许多问题都集中在主官方仓库上,大多数 F-Droid 用户无论如何都会使用它。然而,在单个应用程序中拥有其他仓库也违反了 Android 的安全模型,该模型根本不是为此设计的。操作系统希望你信任一个应用程序仓库作为应用程序的单一来源,但 F-Droid 并不是这样设计的,因为它在一个应用程序中混合了多个仓库。这一点很重要,因为操作系统管理 API 和功能(例如 UserManager,可用于阻止用户安装第三方应用程序)并不是为此设计的,并将 F-Droid 视为单一来源,因此你信任应用程序客户端不会搞砸的事情远远超过你应该信任的范围,尤其是当特权扩展发挥作用时。
确实存在一个严重的安全问题,即操作系统第一方来源功能被滥用,因为特权扩展以不安全的方式使用了 INSTALL_PACKAGES
API(即没有实施适当的安全检查)。特权扩展接受来自 F-Droid 的任何请求,而 F-Droid 本身存在各种错误和安全问题,并且设计上允许用户定义的仓库。很多事情都可能出错,绕过安全检查以获取强大的 API 绝对不应该掉以轻心。
值得一提的是,仓库元数据格式没有通过整个文件签名和密钥轮换进行正确签名。他们的索引 v1 格式使用 JAR 签名与 jarsigner
,这存在严重的安全缺陷。似乎正在开发 v2 格式以支持 apksigner
,尽管最终实现仍有待观察。这似乎是一种过度工程化和有缺陷的方法,因为可以使用更适合的工具(如 signify
)来签名元数据 JSON。
事实上,新的无人值守更新 API 在 API 级别 31(Android 12)中添加,允许应用程序仓库在没有特权访问的情况下无缝更新应用程序(这种方法与安全模型不兼容),它将无法与 F-Droid “原样”配合使用。应该提到的是,上述第三方客户端 Neo-Store 支持此 API,尽管 F-Droid 基础设施的潜在问题在很大程度上仍然存在。事实上,这个允许无人值守更新的安全 API 不仅要求应用程序仓库客户端目标 API 级别为 31,而且要更新的应用程序也必须至少目标 API 级别为 29。
F-Droid 的官方客户端还缺乏TLS 证书固定。证书固定是一种应用程序通过提供已知良好证书的公共密钥哈希来增加其与服务连接安全性的方法,而不是信任预安装的 CA。这可以避免某些情况下可能发生的拦截(_中间人_攻击),并导致各种安全问题,考虑到你信任一个应用程序来为你提供其他应用程序。
证书固定是一个重要的安全功能,自 Android 7.0(API 级别 24)以来,使用声明性网络安全配置可以轻松实现。GrapheneOS 应用商店使用了此功能;请参阅 GrapheneOS 如何在其应用程序仓库客户端中固定根证书和 CA 证书:
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false"/>
<domain-config>
<domain includeSubdomains="true">apps.grapheneos.org</domain>
<pin-set>
<!-- ISRG Root X1 -->
<pin digest="SHA-256">C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=</pin>
<!-- ISRG Root X2 -->
<pin digest="SHA-256">diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=</pin>
<!-- Let's Encrypt R3 -->
<pin digest="SHA-256">jQJTbIh0grw0/1TkHSumWb+Fs0Ggogr621gT3PvPKG0=</pin>
<!-- Let's Encrypt E1 -->
<pin digest="SHA-256">J2/oqMTsdhFWW/n85tys6b4yDBtb6idZayIEBx7QTxA=</pin>
...
</pin-set>
</domain-config>
</network-security-config>