From 45cb8b5b02eee2634a7375ba81a0d86a4f67af75 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 9 Jun 2021 17:16:42 +0200 Subject: [PATCH] android reproducibility: add Signal's "apkdiff.py" tool, and instructions --- contrib/android/Readme.md | 22 +++++++++++ contrib/android/apkdiff.py | 78 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 contrib/android/apkdiff.py diff --git a/contrib/android/Readme.md b/contrib/android/Readme.md index da2e2fe2b..5281cfbf8 100644 --- a/contrib/android/Readme.md +++ b/contrib/android/Readme.md @@ -56,6 +56,9 @@ folder. ./contrib/android/make_apk ``` + Note: this builds a debug apk. `make_apk` takes an optional parameter + which can be either `release` or `release-unsigned`. + This mounts the project dir inside the container, and so the modifications will affect it, e.g. `.buildozer` folder will be created. @@ -63,6 +66,25 @@ folder. 5. The generated binary is in `./bin`. +## Verifying reproducibility and comparing against official binary + +Every user can verify that the official binary was created from the source code in this +repository. + +1. Build your own binary as described above. + Make sure you don't build in `debug` mode (which is the default!), + instead use either of `release` or `release-unsigned`. + If you build in `release` mode, the apk will be signed, which requires a keystore + that you need to create manually (see source of `make_apk` for an example). +2. Note that the binaries are not going to be byte-for-byte identical, as the official + release is signed by a keystore that only the project maintainers have. + You can use the `apkdiff.py` python script (written by the Signal developers) to compare + the two binaries. + ``` + $ python3 contrib/android/apkdiff.py Electrum_apk_that_you_built.apk Electrum_apk_official_release.apk + ``` + This should output `APKs match!`. + ## FAQ diff --git a/contrib/android/apkdiff.py b/contrib/android/apkdiff.py new file mode 100644 index 000000000..802222a9c --- /dev/null +++ b/contrib/android/apkdiff.py @@ -0,0 +1,78 @@ +#! /usr/bin/env python3 +# from https://github.com/signalapp/Signal-Android/blob/2029ea378f249a70983c1fc3d55b9a63588bc06c/reproducible-builds/apkdiff/apkdiff.py + +import sys +from zipfile import ZipFile + +class ApkDiff: + IGNORE_FILES = ["META-INF/MANIFEST.MF", "META-INF/CERT.RSA", "META-INF/CERT.SF"] + + def compare(self, sourceApk, destinationApk): + sourceZip = ZipFile(sourceApk, 'r') + destinationZip = ZipFile(destinationApk, 'r') + + if self.compareManifests(sourceZip, destinationZip) and self.compareEntries(sourceZip, destinationZip) == True: + print("APKs match!") + else: + print("APKs don't match!") + + def compareManifests(self, sourceZip, destinationZip): + sourceEntrySortedList = sorted(sourceZip.namelist()) + destinationEntrySortedList = sorted(destinationZip.namelist()) + + for ignoreFile in self.IGNORE_FILES: + while ignoreFile in sourceEntrySortedList: sourceEntrySortedList.remove(ignoreFile) + while ignoreFile in destinationEntrySortedList: destinationEntrySortedList.remove(ignoreFile) + + if len(sourceEntrySortedList) != len(destinationEntrySortedList): + print("Manifest lengths differ!") + + for (sourceEntryName, destinationEntryName) in zip(sourceEntrySortedList, destinationEntrySortedList): + if sourceEntryName != destinationEntryName: + print("Sorted manifests don't match, %s vs %s" % (sourceEntryName, destinationEntryName)) + return False + + return True + + def compareEntries(self, sourceZip, destinationZip): + sourceInfoList = list(filter(lambda sourceInfo: sourceInfo.filename not in self.IGNORE_FILES, sourceZip.infolist())) + destinationInfoList = list(filter(lambda destinationInfo: destinationInfo.filename not in self.IGNORE_FILES, destinationZip.infolist())) + + if len(sourceInfoList) != len(destinationInfoList): + print("APK info lists of different length!") + return False + + for sourceEntryInfo in sourceInfoList: + for destinationEntryInfo in list(destinationInfoList): + if sourceEntryInfo.filename == destinationEntryInfo.filename: + sourceEntry = sourceZip.open(sourceEntryInfo, 'r') + destinationEntry = destinationZip.open(destinationEntryInfo, 'r') + + if self.compareFiles(sourceEntry, destinationEntry) != True: + print("APK entry %s does not match %s!" % (sourceEntryInfo.filename, destinationEntryInfo.filename)) + return False + + destinationInfoList.remove(destinationEntryInfo) + break + + return True + + def compareFiles(self, sourceFile, destinationFile): + sourceChunk = sourceFile.read(1024) + destinationChunk = destinationFile.read(1024) + + while sourceChunk != b"" or destinationChunk != b"": + if sourceChunk != destinationChunk: + return False + + sourceChunk = sourceFile.read(1024) + destinationChunk = destinationFile.read(1024) + + return True + +if __name__ == '__main__': + if len(sys.argv) != 3: + print("Usage: apkdiff ") + sys.exit(1) + + ApkDiff().compare(sys.argv[1], sys.argv[2])