Reproduzierbare Builds

F-Droid unterstützt reproduzierbare Builds von Apps, so dass jeder den Build-Prozess erneut ausführen und das gleiche APK wie das Original-Release reproduzieren kann. Dies bedeutet, dass F-Droid überprüfen kann, ob eine Anwendung 100% freie Software ist, während die APK-Signaturen des ursprünglichen Entwicklers verwendet werden. Im Idealfall haben alle gebauten APKs den exakt gleichen Hash, aber das ist ein schwierigerer Standard mit geringeren Vorteilen. Im Moment überprüft F-Droid reproduzierbare Builds anhand der APK-Signatur.

Dieses Konzept wird gelegentlich als „deterministische Builds“ bezeichnet. Das ist ein viel strengerer Standard: es bedeutet, dass der gesamte Prozess jedes Mal in der gleichen Reihenfolge abläuft. Das Wichtigste ist, dass jeder den Prozess ausführen kann und am Ende genau das gleiche Ergebnis hat.

Wie es ab sofort umgesetzt wird

Das Veröffentlichen von signierten Binärdateien von anderswo (z.B. vom Upstream-Entwickler) ist, nach Überprüfung, dass sie, sobald sie einem Rezept folgend hergestellt wurden, miteinander übereinstimmen, nun möglich. Die Veröffentlichung erfolgt nur, wenn eine exakte Übereinstimmung vorliegt. (Was sehr unwahrscheinlich ist, sofern nicht genau die gleiche Tool-Chain verwendet wird. So könnte ich mir vorstellen, dass sie nicht übereinstimmen werden, wenn die Person, die die eingehenden Binärdateien herstellt und signiert, nicht fdroidserver zu ihrer Herstellung nutzt und mutmaßlich genau dieselbe Buildserver-ID verwendet. Immerhin besitzen wir aber die Funktionalität, um das zu belegen.)

Diese Prozeduren sind im Rahmen von fdroid publish implementiert. Beim Publizieren folgt die Reproduzierbarkeitsprüfung dieser Logik:

Flussdiagramm zur Reproduzierbarkeitsprüfung

Veröffentlichung von APKs, die sowohl von (Upstream-)Entwicklern als auch von F-Droid signiert sind

Nutzen Sie diese Vorgehensweise zur Auslieferung einer App-Version mit APKs, die sowohl vom (Upstream-)Entwickler als auch von F-Droid signiert sind. Dies ermöglicht es uns, Aktualisierungen an Benutzer auszuliefern, die Apps aus anderen Quellen als F-Droid (z. B. aus dem Play Store) installiert haben, die deshalb durch die App-Entwickler signiert sind, und gleichzeitig Aktualisierungen für Apps zu verschicken, die von F-Droid erstellt und signiert wurden.

Dazu müssen (Upstream-)Entwicklersignaturen in fdroiddata eingefügt werden. Wir bieten einen Befehl zum einfachen Extrahieren von Signaturen aus APKs:

$ cd /pfad/zum/fdroiddata
$ fdroid signatures F-Droid.apk

Sie können fdroid signatures auch direkt HTTPS-URLs anstelle lokaler Dateien zuweisen. Die Signaturdateien werden gebrauchsfertig für fdroid publish in das entsprechende Metadatenverzeichnis extrahiert. Eine Signatur besteht aus 3–5 Dateien und das Ergebnis des Extrahierens ähnelt diesen Dateilisten:

$ 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

Wenn Sie fdroidserver nicht installieren wollen oder eine ältere Version haben, die das Extrahieren von v2/v3-Signaturen noch nicht unterstützt, können Sie auch apksigcopier (verfügbar z.B. in Debian unstable) anstelle von fdroid signatures verwenden:

$ 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

Veröffentlichung von APKs, die ausschließlich vom (Upstream-)Entwickler signiert sind

Um diesen älteren Ansatz zu verwenden, sollte alles in den Metadaten wie üblich sein plus der Anweisung Binaries:, um festzulegen, woher die Binärdateien kommen sollen. In diesem Fall wird F-Droid niemals versuchen, von F-Droid signierte APKs zu versenden. Sollte es fdroid publish gelingen, nachzuweisen, dass ein heruntergeladenes APK reproduzierbar erstellt werden kann, wird das heruntergeladene APK veröffentlicht. Andernfalls überspringt F-Droid die Veröffentlichung dieser Version der App.

Hier ein Beispiel für eine Binaries Direktive:

Binaries: https://foo.com/pfad/zum/com.beispiel-%v.apk

Siehe auch: Build-Metadaten-Referenz - Binaries

Verifikations-Builds

Viele Leute oder Organisationen werden nur daran interessiert sein, Builds zu reproduzieren, um sicherzustellen, dass die f-droid.org-Builds mit dem ursprünglichen Quellcode übereinstimmen und nichts eingefügt wurde. In diesem Fall werden die resultierenden APKs nicht zur Installation veröffentlicht. Der Verifikationsserver automatisiert diesen Vorgang.

Reproduzierbare Builds

Viele Builds verifizieren bereits ohne zusätzlichen Aufwand, da Java-Code oft von einer Vielzahl von Java-Versionen in den gleichen Bytecode kompiliert wird. Die build-tools des Android SDKs erzeugen Unterschiede in den resultierenden XML-, PNG-, usw. Dateien, aber das ist normalerweise kein Problem, da die build.gradle die genaue Version von build-tools enthält.

Alles, was mit dem NDK gebaut wird, wird viel empfindlicher sein. Selbst für Builds, die genau die gleiche Version des NDK verwenden (z.B. r13b), aber auf verschiedenen Plattformen (z.B. OSX-Version, Ubuntu), werden die resultierenden Binärdateien Unterschiede aufweisen.

Zusätzlich müssen wir nach allem Ausschau halten, was Zeitstempelinformationen, Sortierreihenfolge usw. enthält.

Google arbeitet auch an reproduzierbaren Builds von Android-Apps, so dass die Verwendung aktueller Versionen des Android-SDKs hilft. Ein spezieller Fall beginnt mit dem Gradle Android Plugin v2.2.2, Zeitstempel im ZIP-Header der APK-Datei werden automatisch auf Null gesetzt.

Reproduzierbare Signaturen

F-Droid verifiziert reproduzierbare Builds anhand der APK-Signatur, was das Kopieren der Signatur von einer signierten APK auf eine unsignierte APK erfordert, um dann zu prüfen, ob letztere verifiziert werden kann. Die alten v1 (JAR)-Signaturen decken nur den Inhalt der APK ab (ZIP-Metadaten und Reihenfolge sind irrelevant), aber v2/v3-Signaturen decken alle anderen Bytes in der APK ab. Die APKs müssen also vor und nach dem Signieren völlig identisch sein (abgesehen von der Signatur), um korrekt verifiziert werden zu können.

Das Kopieren der Signatur verwendet denselben Algorithmus, den apksigner beim Signieren einer APK verwendet. Es ist daher wichtig, dass (Upstream-)Entwickler beim Signieren von APKs dasselbe tun, idealerweise mit apksigner.

platform Revisionen

Die Android SDK-Tools wurden geändert im Jahr 2014 auf stick two data elements in AndroidManifest.xml als Teil des Build-Prozesses: platformBuildVersionName und platformBuildVersionCode. platformBuildVersionName enthält die „Revision“ des Pakets platforms, gegen das gebaut wurde (z.B. android-23), jedoch können verschiedene “Revisionen” desselben Pakets platforms nicht parallel installiert werden. Außerdem unterstützen die SDK-Tools nicht die Angabe der erforderlichen Revision als Teil des Build-Prozesses. Dies führt oft zu einem ansonsten reproduzierbaren Build, wobei der einzige Unterschied das Attribut platformBuildVersionName ist.

Die „Plattform“ ist Teil des Android SDK, das die Standardbibliothek darstellt, die auf dem Telefon installiert ist. Die Version besteht aus zwei Teilen: „Versionscode“, eine ganze Zahl, die das SDK-Release repräsentiert, und die „Revision“, die Bugfix-Versionen für jede Plattform repräsentiert. Diese Versionen sind in der mitgelieferten Datei build.prop zu finden. Jede Revision hat eine andere Nummer in ro.build.version.incremental. Gradle hat keine Möglichkeit, die Revision in compileSdkVersion oder targetSdkVersion anzugeben. Es kann jeweils nur eine „Plattform-23“ installiert werden, im Gegensatz zu build-tools, wo jede Version parallel installiert werden kann.

Hier sind zwei Beispiele, bei denen ich denke, dass alle Unterschiede nur von verschiedenen Revisionen der Plattform herrühren:

  • 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 Crush/Crunch

Ein Standardbestandteil des Android-Buildprozesses ist es, eine Art PNG-Optimierungswerkzeug wie aapt singleCrunch oder pngcrush auszuführen. Diese liefern keine deterministische Ausgabe, es ist immer noch eine offene Frage, warum. Da PNGs normalerweise in das Quell-Repo committed sind, besteht eine Lösung für dieses Problem darin, das Tool Ihrer Wahl auf den PNG-Dateien auszuführen und diese Änderungen dann in das Quell-Repo zu übertragen (z.B. git). Deaktivieren Sie dann den standardmäßigen PNG-Optimierungsprozess, indem Sie diesen in build.gradle einfügen:

android {
    aaptOptions {
        cruncherEnabled = false
    }
}

R8-Optimierer

Es kommt vor, dass bestimmte, in nichtdeterministischer Weise umgesetzte R8-Optimierungen, unterschiedlichen Bytecode bei unterschiedlichen Build-Durchläufen erzeugen.

Beispielsweise versucht R8, die ServiceLoader-Nutzung zu optimieren, indem eine statische Liste aller Dienste im Code erzeugt wird. Die Anordnung dieser Liste kann bei jedem Build-Durchlauf unterschiedlich (oder sogar unvollständig) sein. Die einzige Möglichkeit, dieses Verhalten zu verhindern, ist die Deaktivierung solcher Optimierungen durch Deklaration optimierter Klassen in proguard-rules.pro:

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

Seien Sie vorsichtig im Umgang mit R8. Testen Sie Ihre Builds immer mehrere Male und deaktivieren Sie Optimierungen, die eine nichtdeterministische Ausgabe produzieren.

Ressourcen schrumpfen

Es ist möglich die APK-Dateigröße zu reduzieren, indem nicht verwendete Ressourcen aus dem Paket entfernt werden. Das ist praktisch, wenn ein Projekt von bestimmten aufgeblähten Bibliotheken, wie AppCompat, abhängt, insbesondere wenn R8/ProGuard zum Schrumpfen des Codes verwendet wird.

Allerdings kann es geschehen, dass der Ressourcen-Schrumpfer die APK-Größe auf verschiedenen Plattformen erhöht, besonders dann, wenn es nicht allzu viele zu schrumpfende Ressourcen gibt, sodass dann das Original-APK anstatt des geschrumpften verwendet wird (nichtdeterministisches Verhalten des Gradle-Plugin). Vermeiden Sie die Verwendung des Ressourcen-Schrumpfers, solange die APK-Dateigröße nicht signifikant verkleinert wird.

coreLibraryDesugaring

In einigen Fällen sind Builds nicht reproduzierbar aufgrund eines Fehlers in coreLibraryDesugaring (erfordert ein Google-Konto zur Ansicht); dies betrifft derzeit NewPipe.

zipflinger

Neuere Versionen des Android-Gradle-Plugins verwenden zipflinger – das den Inhalt der APK anders anordnet – was dazu führen kann, dass z.B. apksigcopier in manchen Fällen nicht funktioniert. Sie können das Plugin anweisen, zipflinger nicht zu verwenden, indem Sie android.useNewApkCreator=false in gradle.properties setzen.

Native Bibliothek strippen

Es scheint, dass das Strippen von nativen Bibliotheken, z.B. libfoo.so, zeitweise Reproduzierbarkeitsprobleme verursachen kann. Es ist wichtig, beim Neuaufbau die genaue NDK-Version zu verwenden, z. B. r21e. Das Deaktivieren von Stripping kann manchmal helfen. Gradle scheint freigegebene Bibliotheken standardmäßig zu strippen, auch wenn die App die freigegebenen Bibliotheken über eine AAR-Bibliothek erhält. Hier erfahren Sie, wie Sie es in Gradle deaktivieren können:

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

NDK build-id

Auf verschiedenen Build-Maschinen werden unterschiedliche NDK-Pfade und unterschiedliche Pfade zum Projekt (und damit zu seinem jni-Verzeichnis) verwendet. Dies führt zu unterschiedlichen Pfaden zu den Quelldateien in Debug-Symbolen, was den Linker veranlasst, unterschiedliche Build-ID zu erzeugen, die nach dem Strippen erhalten bleibt.

Eine mögliche Lösung ist das Hinzufügen von LOCAL_LDFLAGS += -Wl,--build-id=none zu Android.mk Dateien, wodurch die Generierung der Build-ID vollständig deaktiviert wird.

Server-IDs erstellen

Um die von F-Droid-Builds verwendete Build-Umgebung zu beschreiben, haben APKs zwei Dateien eingefügt:

  • META-INF/fdroidserverid - git commit hash von fdroidserver für den Build verwendet
  • META-INF/buildserverid - git commit hash von makebuildserver für den Build verwendet

Um die Reproduzierbarkeit zu gewährleisten, verwenden Sie genau die gleiche Revision von ./makebuildserver und fdroid build. Sie können den Commit-Hash von fdroidserver finden, indem Sie in Ihrem Git-Clone git log -n1 ausführen. Die Build-Server-Instanz wird bei der Erstellung mit dem Git-Commit-Hash versehen und die ID ist in den Builds enthalten.

ZIP-Eintrag Info

Das ZIP-Format wurde ursprünglich für das MSDOS FAT-Dateisystem entwickelt. UNIX-Dateiberechtigungen wurden als Erweiterung hinzugefügt. APKs benötigen nur das einfachste ZIP-Format, ohne irgendwelche Erweiterungen. Diese Erweiterungen werden oft bei der finalen Versionssignierung entfernt. Aber der APK-Build-Prozess kann sie hinzufügen. Zum Beispiel:

--- 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

Migration auf reproduzierbare Builds

TODO

  • jar-Sortierreihenfolge für APKs
  • aapt Versionen liefern unterschiedliche Ergebnisse (XML und res/ Unterordnernamen)

Quellen