返回介绍

可重复构建

发布于 2023-08-06 23:35:04 字数 20555 浏览 0 评论 0 收藏 0

F-Droid 支持应用的可重复构建,因此任何人都可以再次运行构建过程,并再现与原始版本相同的 APK。 这意味着 F-Droid 可以验证一个应用是 100% 的自由软件,同时仍然使用原始开发者的 APK 签名。 F-Droid 使用 APK 签名复制来验证可重复构建。

这个概念偶尔也被称为”确定性构建”。 这是一个更严格的标准:这意味着整个过程每次都以相同的顺序运行。 最重要的是,任何人都可以运行这个过程,最后得到完全相同的结果。

截至目前,它是如何实施的

在验证它们与使用 fdroiddata 配方构建的二进制文件匹配后,现在可以从其他地方(例如上游开发人员)发布已签名的二进制文件(APK)。只有在适当匹配的情况下才会发布。这一流程作为 fdroid publish的一部分实现的。发布阶段的可重现性检查遵循以下逻辑:

可重复性检查流程图

同时发布(上游)开发者签名和 F-Droid 签名的 APK

这种方法允许同时发布(上游)开发者签名和 F-Droid 签名的 APK。这使我们能够为从 F-Droid 以外的其他来源(例如 Play Store)安装应用的用户发送更新,同时也为由 F-Droid 构建和签名的应用发送更新。

这需要提取并向 fdroiddata 添加(上游)开发人员签名。然后将这些签名复制到从 fdroiddata 配方构建的 unsigned APK。我们提供了一个命令,可以轻松地从 APK 中提取签名:

$ cd /path/to/fdroiddata
$ fdroid signatures F-Droid.apk

除了本地文件,你还可以向 fdroid 签名 提供 HTTPS 网址。

签名文件会被提取到应用的元数据目录中,准备用 fdroid publish 使用。一个签名由 2-6 个文件组成:一个 v1 签名(清单、签名文件和签名块文件)和/或一个 v2/v3 签名(APK 签名块和偏移量);如果使用 signflinger 而不是 apksigner对 APK 进行 v1 签名,会有一个 differences.json文件。提取这样一个文件的结果将类似于这些文件列表:

$ ls metadata/org.fdroid.fdroid/signatures/1000012/  # v1 signature only
CIARANG.RSA  CIARANG.SF  MANIFEST.MF
$ ls metadata/your.app/signatures/42/                # v1 + v2/v3 signature
APKSigningBlock  APKSigningBlockOffset  MANIFEST.MF  YOURKEY.RSA  YOURKEY.SF

如果你不想安装 fdroidserver (或者有一个不支持提取 v2/v3 签名的旧版本),你也可以使用 apksigcopier(例如在 Debian、 Ubuntu、Arch Linux、NixOS 中可用)而不是 fdroid signatures

$ cd /path/to/fdroiddata
$ APPID=your.app VERSIONCODE=42
$ mkdir metadata/$APPID/signatures/$VERSIONCODE
$ apksigcopier extract --v1-only=auto Your.apk metadata/$APPID/signatures/$VERSIONCODE

只发布(上游)开发者签名的 APK

对这种方法,元数据中的一切都应该和正常的一样,加上 Binaries: 指令来指定从哪里获得二进制文件(APKs)。在这种情况下,F-Droid 不会试图发布由 F-Droid 签名的 APK。如果 fdroid publish 能够验证可下载的 APK 匹配从 fdroiddata 配方构建的 APK,那么可下载的 APK 将被发布。否则 F-Droid 将跳过发布该应用的这个版本。

以下是 Binaries 指令的示例:

二进制文件:https://example.com/path/to/myapp-%v.apk

另见:Build Metadata Reference - Binaries

可重复的签名

F-Droid 使用 APK 签名嵌入式签名的一种形式)来验证可重复构建,这需要将签名从一个已签名的 APK 复制到一个未签名的 APK,然后检查后者是否验证。 旧的 v1 (JAR) 签名只包括 APK 的 内容(例如,与 ZIP 元数据和排序无关),但 V2/V3 签名包括 APK 中所有其他字节。 因此,APK 必须在签名 之前之后 完全相同(除了签名之外)才能正确验证。

复制签名使用的算法与 apksigner 签署 APK 时使用的算法相同。 因此,重要的是,(上游)开发人员在签署 APK 时也要这样做,最好是使用 apksigner

验证构建

许多人或组织对可重建构建感兴趣,以确保 f-droid.org 构建与原始源匹配,并且没有更改任何内容。在这种情况下,不会发布生成的 APK 以供安装。 验证服务器 可以自动完成这个过程。

可重复构建

由于 Java 代码经常被各种不同的 Java 版本编译成相同的字节码,因此不少构建工作已经无需额外努力即可验证。 Android SDK 的 build-tools 会在生成的 XML、PNG 等文件中产生差异,但这通常不是问题,因为 build.gradle 包括要使用的确切版本的 build-tools

任何用 NDK 构建的东西都会更敏感。 例如,即使是使用完全相同的NDK版本(例如 r13b),但在不同的平台上(例如 macOS 与 Ubuntu),所生成的二进制文件也会有差异。

此外,我们还必须注意任何对排序敏感的东西,包括时间戳或构建路径等。

Google 也在努力实现 Android 应用的可重复构建,所以使用最新版本的 Android SDK 是有帮助的。 一个具体情况是,从 Gradle Android 插件 v2.2.2 开始,APK 文件的 ZIP 元数据中的时间戳被自动置零了。

Debugging Reproducible Builds

We recommend using diffoscope for easily finding the difference between the reference APK provided by the app developer and the APK that fdroidserver produced.

You can find the APK that fdroidserver produced either under e.g. fdroiddata/build/com.example.app/app/build/outputs/apk/prod/release/example-1.0.0-prod-release-unsigned.apk (when running locally) or in the pipeline artifacts (when using GitLab CI). Adjust the path accordingly (e.g. for flavours other than prod).

Prioritising & fixing differences

HOWTO: diff & fix APKs for Reproducible Builds on the F-Droid wiki has detailed information on the various kinds of differences commonly encountered, which differences should usually be prioritised when debugging, and how to fix common issues.

It also shows how to use various specialised tools that may provide better results when diffoscope is not sufficient.

可重复 APK 工具

如果消除造成差异的原因难以实现,来自 reproducible-apk-tools 的脚本(在 fdroiddata 中作为 srclib 可用)可能有助于使构建可重复,例如,通过固定换行 (CRLF vs LF) 或使 ZIP 顺序确定。根据具体情况,上游开发者需要在签署 APK 之前使用这些脚本,或者根据 fdroiddata 配方使用,或者两者同时使用。

最初创建 disorderfs 的目的是在构建过程中插入非确定性,但也可以出于相反目的使用它:使得从文件系统的读取具有确定性。在某些情况下,这可以使 resources.arsc 可重复。下面是一个来自现有配方的例子:

$ mv my.app my.app_underlying
$ disorderfs --sort-dirents=yes --reverse-dirents=no my.app_underlying my.app

不可重复构建的可能原因

构建不可重复的方式有很多种。有些问题相对容易避免,有些很难解决。我们试图在下面列出一些常见的原因。

另见 这个 gitlab issue

Bug: Android Studio 构建有非确定性 ZIP 顺序

APK 文件中非决定性的 ZIP 项顺序造成构建不可重复(可能需要 Google 账户方可查看)。

注:该问题在 7.1.X及更新版本的 Android Gradle 插件 (com.android.tools.build:gradle / com.android.application) 中应该已被修复。

在用 Android Studio 构建 APK 文件时,APK 中 ZIP 项目的顺序可能不同于直接调用 gradle 进行构建的 APK,这会影响可重现性;顺序可能是完全非确定性的,甚至在相同源码的不同构建之间也不一样。

旧版本的一个变通办法是直接调用 gradle (像在 F-Droid 或 CI 构建期间一样),来绕过 Android Studio:

$ ./gradlew assembleRelease

请注意:取决于你的签名配置,可能需要之后用 apksigner 对 APK 进行签名,因为在这种情况下 APK 签名不是由 Android Studio 执行的。

Bug: baseline.profm not deterministic

Non-stable assets/dexopt/baseline.profm (可能需要 Google 账号才能查看)。

另见 这篇变通方法的文章

Bug: coreLibraryDesugaring not deterministic

注:该问题在 3.0.69及更新版本的 R8 (com.android.tools:r8)中应该已被修复。

在某些情况下,由于 coreLibraryDesugaring 中的错误,构建不可重复(可能需要 Google 帐号才能查看);这曾影响 NewPipe

Bug:Windows 和 Linux 版本间的行结束符差异

Windows 和 Linux 系统上进行构建的换行符差异造成构建不可重复(可能需要 Google 账户才能查看)。

一个变通方法是在行结束符“错误”的未签名 APK 文件上运行 fix-newlines.py 将他们从 LF 更改为 CRLF (或者使用 --from-crlf反向操作)并在之后再次对它进行 zipalign

并发:可重现性可以取决于CPU/核心的数目

这可能影响 .dex 文件(虽然这似乎比较少见)或本机代码(如 Rust)。

只使用 1 个 CPU/核心作为变通办法:

export CPUS_MAX=1
export CPUS=$(getconf _NPROCESSORS_ONLN)
for (( c=$CPUS_MAX; c<$CPUS; c++ )) ; do echo 0 > /sys/devices/system/cpu/cpu$c/online; done

请注意:这种变通方法影响整台机器,因此推荐在非持久性的虚拟机或容器中使用它。

对于 Rust 代码,你可以设置 codegen-units = 1

另见 这个 gitlab issue

嵌入的构建路径

嵌入的构建路径是可重复性问题的一个来源,影响使用 Flutter、python-for-android 或原生代码(如 Rust、C/C++、各种 libfoo.so)构建的应用。完全用 Java 和/或 Kotlin 编写的应用一般不会受影响。

通常来说,最简单的解决方案是在构建时始终使用相同的工作目录;如,/builds/fdroid/fdroiddata/build/your.app.id (F-Droid CI), /home/vagrant/build/your.app.id (F-Droid build server)、 /tmp/build或创建一个来镜像上游所用的文件夹,如对于 macOS 系统是 /Users/runner

注:使用全局可写的 tmp的子目录可能会有安全影响(在多用户系统上)。

如果 SDK 路径最后嵌入在 Flutter 中,可以将 SDK 移至配方 中的路径并用 flutter config --android-sdk <path>进行配置, 因为光设置 ANDROID_SDK_ROOT可能不够。

内嵌的时间戳

内嵌的时间戳是可重复性问题最常见的来源,最好避免。

AboutLibraries Gradle 插件

要避免这个插件 (com.mikepenz.aboutlibraries.plugin) 添加时间戳到它生成的 JSON 文件,你可以添加这个到 build.gradle

aboutLibraries {
    // Remove the "generated" timestamp to allow for reproducible builds
    excludeFields = ["generated"]
}

对于 build.gradle.kts,请添加这个:

aboutLibraries {
    // 移除 "generated" 时间戳以允许可重复构建
    excludeFields = arrayOf("generated")
}

本地库的剥离

似乎剥离原生库,例如 libfoo.so,可能会导致间歇性重现性问题。重建时使用确切的 NDK 版本很重要,例如 r21e。禁用剥离有时会有所帮助。 Gradle 似乎默认剥离共享库,甚至应用也通过 AAR 库接收共享库。以下是在 Gradle 中禁用它的方法:

android {
    packagingOptions {
        doNotStrip '**/*.so'
    }
}

NDK build-id

在不同的构建机器上,使用不同的 NDK 路径和不同的项目路径(及其 jni 目录)。 这导致调试符号中源文件的路径不同,造成该链接器生成不同的_build-id_,剥离后保留。

一个可能的解决方案是传递 --build-id=none 到链接器,这会彻底禁止生成 build-id

NDK 哈希样式

LLVM 在不同平台上传递给链接器的默认值也是不同的。在此提交 被合并入 NDK 后, --hash-style=gnu 默认用于 Debian。要更改哈希样式,可以传递 --hash-style=gnu到链接器。

platform 修订版

Android SDK 工具在2014年改为在构建过程中在 AndroidManifest.xml添加两个数据元素platformBuildVersionNameplatformBuildVersionCodeplatformBuildVersionName 包括 platforms 包的”修订版”,根据该包构建(例如:android-23),然而同一 platforms 包的不同”修订版”不能并行安装。 另外,SDK 工具不支持指定所需的修订作为构建过程的一部分。 这往往会导致另一种可重复构建,它和真正可重复构建之间唯一的区别是 platformsBuildVersionName 属性。

_platform_是 Android SDK 的一部分,代表安装在手机上的标准库。 它们的版本有两部分:“版本代码”,它是一个整数,代表 SDK 版本,以及“修订版”,它代表每个平台的错误修复版本。 这些版本可以在包含的 build.prop 文件中看到。 每个修订版在 ro.build.version.incremental 中有不同的编号。 Gradle 无法在 compileSdkVersiontargetSdkVersion 中指定修订版本。一次只能安装一个platform-23,不像 build-tools,每个版本都可以并行安装。

这里有两个例子,其中所有的差异都涉嫌来自于平台的不同修订:

  • https://verification.f-droid.org/de.nico.asura_12.apk.diffoscope.html
  • https://verification.f-droid.org/de.nico.ha_manager_25.apk.diffoscope.html

PNG 优化/压缩

Android 构建过程的一个标准部分是运行某种 PNG 优化工具,例如 aapt singleCrunchpngcrushzopflipngoptipng。这些不提供确定性的输出,关于原因仍然是一个悬而未决的问题。由于 PNG 通常提交到源存储库,因此解决此问题的方法是在 PNG 文件上运行你选择的工具,然后将这些更改提交到源存储库(例如 git)。然后,通过将其添加到 build.gradle 来禁用默认的 PNG 优化过程:

android {
    aaptOptions {
        cruncherEnabled = false
    }
}

请注意,svgo 等工具可以对 SVG 文件进行类似的优化。

生成自矢量可绘制对象的 PNG 图片

Android Gradle 插件为旧 Android 版本从矢量可绘制图形生成 PNG 资源。不幸的是,生成的 PNG 文件不可重复。

你可以通过添加这个到 build.gradle 来禁止生成 PNG:

android {
    defaultConfig {
        vectorDrawables.generatedDensities = []
    }
}

R8 优化器

似乎某些 R8 优化以不确定的方式完成,在不同的构建运行中产生不同的字节码。

例如,R8 尝试优化 ServiceLoader 的使用,在代码中制作所有服务的静态列表。每次构建运行时,此列表的顺序可能不同(甚至不完整)。避免这种行为的唯一方法是禁用在 proguard-rules.pro 中声明优化类的优化:

-keep class kotlinx.coroutines.CoroutineExceptionHandler
-keep class kotlinx.coroutines.internal.MainDispatcherFactory

使用 R8 要小心。始终多次测试你的构建,并禁用产生非确定性输出的优化。

资源压缩器

可以通过从包中删除未使用的资源来减小 APK 文件的大小。当项目依赖于一些臃肿的库(例如 AppCompat)时,这很有用,尤其是在使用 R8/ProGuard 代码压缩时。

然而,在不同的平台上,资源收缩器可能会增加 APK 的大小,尤其是在没有许多资源需要压缩的情况下,在这种情况下,将使用原始的 APK 而不是收缩后的 APK(Gradle 插件的非确定性行为)。避免使用资源压缩器,除非它能显著减少 APK 文件的大小。

ZIP 元数据

APKs 使用 ZIP 文件格式,ZIP 格式最初是围绕 MSDOS 的 FAT 文件系统设计的。 UNIX 文件权限是作为一个扩展添加的。 APK 只需要最基本的 ZIP 格式,没有任何的扩展。 在最后的发布签名过程中,这些扩展往往被剥离出来。 但 APK 构建过程中可以添加它们。例如:

--- a2dp.Vol_137.apk
+++ sigcp_a2dp.Vol_137.apk
@@ -1,50 +1,50 @@
--rw----     2.0 fat     8976 bX defN 79-Nov-30 00:00 AndroidManifest.xml
--rw----     2.0 fat  1958312 bX defN 79-Nov-30 00:00 classes.dex
--rw----     1.0 fat    78984 bx stor 79-Nov-30 00:00 resources.arsc
+-rw-rw-rw-  2.3 unx     8976 b- defN 80-000-00 00:00 AndroidManifest.xml
+-rw----     2.4 fat  1958312 b- defN 80-000-00 00:00 classes.dex
+-rw-rw-rw-  2.3 unx    78984 b- stor 80-000-00 00:00 resources.arsc

不匹配的工具链

工具链不同,产生的二进制文件便可能不一样。常见的情况是使用不止一个 JDK 版本/分发来构建 apk 文件。有时即便是 Gradle 也会混合不同版本的 JDK 来构建一个 apk 文件。要避免这样的问题,请去掉不使用的 JDK。

APK diff 在 classes.dex 文件中的条目形如 Java 17 vs Java 11:

-    .annotation system Ldalvik/annotation/Signature;
-        value = {
-            "()V"
-        }
-    .end annotation

特定于编程语言的操作指南

原生库可能由各种工具和语言所构建。虽然它们在可重复构建方面遇到的问题差不多,但修复方法却不同。下面列举一些已知的解决方案:

ndk-build

LOCAL_LDFLAGS += -Wl,<linker args> 可被添加到 Android.mk 文件或 build.gradle/build.gradle.kts

android {
    defaultConfig {
        externalNativeBuild {
            ndkBuild {
                arguments "LOCAL_LDFLAGS += -Wl,<linker args>"
            }
        }
    }
}
CMake

对于 3.13 起的 CMake 版本,可以全局添加add_link_options(LINKER:<linker args>)CMakeLists.txt。 对于 3.13 之前的 CMake 版本, 可以对每个目标使用 target_link_libraries(<target> LINKER:<linker args>)

Golang

链接器参数可被添加到 CGO_LDFLAGS。一些其他可被传递到 go build 的有用参数是 -ldflags="-buildid="-trimpath(避免内嵌的构建路径)和 -buildvcs=false

Rust

编译器和链接器参数可被添加到 Cargo build.rustflagsrustc Codegen Options。 链接器参数可带-C link-args=-Wl,<linker args>;可以添加 `–remap-path-prefix==来剥离构建路径。

Rust 工具链应被固定在和上游一样的版本。安装 rustup 时带上参数 rustup-init.sh -y --default-toolchain <version> 可以做到这一点。

迁移到可重复构建

TODO

  • APK 的 jar 排序顺序
  • aapt 版本产生不同的结果(XML 和 res/ 子文件夹名称)

来源

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文