|
| 1 | +# Reproducible Builds |
| 2 | + |
| 3 | +Monolith's release APK is built reproducibly: anyone can rebuild the apk from the |
| 4 | +public source code inside a pinned build environment and confirm, **bit-for-bit** via hash comparison, that |
| 5 | +the published APK contains exactly the code provided on Github. |
| 6 | + |
| 7 | + |
| 8 | +Two different SHA-256 values are involved: |
| 9 | + |
| 10 | +| Hash | Of what | Answers | |
| 11 | +|------|---------|---------| |
| 12 | +| Download hash | the **signed** APK on the website | "Do I have the same APK the developer published?"| |
| 13 | +| Reproducible hash| the **unsigned** APK you rebuild here | "Prove that it was built from the Github source code"| |
| 14 | + |
| 15 | +## Trust roots (pinned for this release) |
| 16 | + |
| 17 | +| Item | Value | |
| 18 | +|------|-------| |
| 19 | +| Source | this repository | |
| 20 | +| Build image | `aronbasalt/monolith-build@sha256:2c3cb23203e54f5f83cc30dfb479a49e679a8bc27c431f176d392ea3704e8dee` | |
| 21 | +| Expected unsigned APK SHA-256 | `245dcfd1d02b0c94ccbd0aebac72327119cdc007cbc17b464a274b6e2909111c` | |
| 22 | + |
| 23 | +The build image is identified **by digest**, not by tag: a tag can be re-pointed, |
| 24 | +a digest is the content's fingerprint and cannot be swapped. It freezes the whole |
| 25 | +toolchain (JDK 17, Android build-tools 35.0.0, NDK 27.0.12077973, CMake 3.22.1, |
| 26 | +Gradle 8.13). |
| 27 | + |
| 28 | +## Prerequisites |
| 29 | + |
| 30 | +- Docker (the image targets `linux/amd64`; on Apple Silicon it runs via emulation) |
| 31 | +- Internet connection |
| 32 | + |
| 33 | +## Step 1: Get the source code |
| 34 | + |
| 35 | +Download and unpack the source code from Github |
| 36 | + |
| 37 | +## Step 2: Pull the pinned build image by digest |
| 38 | + |
| 39 | +```bash |
| 40 | +docker pull aronbasalt/monolith-build@sha256:2c3cb23203e54f5f83cc30dfb479a49e679a8bc27c431f176d392ea3704e8dee |
| 41 | +``` |
| 42 | + |
| 43 | +## Step 3: Build the APK |
| 44 | + |
| 45 | +Run from the repository root (the folder that contains `gradlew`). Mount the source |
| 46 | +at **exactly** `/src`. |
| 47 | + |
| 48 | +Linux / macOS: |
| 49 | +```bash |
| 50 | +docker run --rm --platform=linux/amd64 -v "$(pwd):/src" \ |
| 51 | + aronbasalt/monolith-build@sha256:2c3cb23203e54f5f83cc30dfb479a49e679a8bc27c431f176d392ea3704e8dee \ |
| 52 | + bash -lc "./gradlew --no-daemon clean assembleRelease && sha256sum app/build/outputs/apk/release/app-release-unsigned.apk" |
| 53 | +``` |
| 54 | + |
| 55 | +Windows (PowerShell): |
| 56 | +```powershell |
| 57 | +docker run --rm --platform=linux/amd64 -v "${PWD}:/src" ` |
| 58 | + aronbasalt/monolith-build@sha256:2c3cb23203e54f5f83cc30dfb479a49e679a8bc27c431f176d392ea3704e8dee ` |
| 59 | + bash -lc "./gradlew --no-daemon clean assembleRelease && sha256sum app/build/outputs/apk/release/app-release-unsigned.apk" |
| 60 | +``` |
| 61 | + |
| 62 | +## Step 4: Check the reproducible hash |
| 63 | + |
| 64 | +The printed SHA-256 must equal: |
| 65 | +``` |
| 66 | +245dcfd1d02b0c94ccbd0aebac72327119cdc007cbc17b464a274b6e2909111c |
| 67 | +``` |
| 68 | +A match means your independent build is bit-for-bit identical to the reference |
| 69 | +unsigned APK. |
| 70 | + |
| 71 | +## Step 5: Compare against the published (signed) APK |
| 72 | + |
| 73 | +The public APK differs from your rebuild **only** by the signature. Confirm this by comparing with the signature |
| 74 | +excluded: |
| 75 | +- the APK Signing Block (v2/v3) |
| 76 | +- `META-INF/*.SF`, `META-INF/*.RSA`, `META-INF/*.EC`, `META-INF/*.MF` (v1) |
| 77 | + |
| 78 | +**Recommended — apksigcopier** (clear pass/fail). `apksigcopier` is a Python tool and |
| 79 | +internally calls `apksigner`; **neither ships in the build image**, so install them inside |
| 80 | +a throwaway container — this does **not** change the published image (it lives only in that |
| 81 | +one container and is discarded with `--rm`). Put the downloaded signed APK at the repo root |
| 82 | +as `published.apk`, then run: |
| 83 | +```bash |
| 84 | +docker run --rm -v "$(pwd):/work" \ |
| 85 | + aronbasalt/monolith-build@sha256:2c3cb23203e54f5f83cc30dfb479a49e679a8bc27c431f176d392ea3704e8dee \ |
| 86 | + bash -lc ' |
| 87 | + apt-get update -qq && apt-get install -y -qq python3-pip >/dev/null |
| 88 | + pip3 install --quiet apksigcopier # if pip refuses: add --break-system-packages |
| 89 | + ln -sf /opt/android-sdk/build-tools/35.0.0/apksigner /usr/local/bin/apksigner |
| 90 | + apksigcopier compare /work/published.apk --unsigned /work/app/build/outputs/apk/release/app-release-unsigned.apk |
| 91 | + echo "exit: $? (0 = identical apart from the signature = verified)" |
| 92 | + ' |
| 93 | +``` |
| 94 | + |
| 95 | +**To inspect any remaining difference — diffoscope:** |
| 96 | +```bash |
| 97 | +diffoscope <PUBLISHED_APK> app/build/outputs/apk/release/app-release-unsigned.apk |
| 98 | +``` |
| 99 | + |
| 100 | +Nothing should differ other than the signature. |
| 101 | + |
| 102 | +## Known exception — `libsodium.so` (prebuilt) |
| 103 | + |
| 104 | +`app/src/main/cpp/libs/arm64-v8a/libsodium.so` is a **prebuilt** binary (from the |
| 105 | +Kalium JNI bindings 2.0.2), committed to the repository with a documented SHA-256 |
| 106 | +(see `app/src/main/cpp/libs/PROVENANCE.md`). It is **not** compiled from source |
| 107 | +here, so it is not independently reproducible — but being fixed in Git, it |
| 108 | +contributes **identically** to every build. Trust is anchored via its SHA-256 and |
| 109 | +the Kalium release. |
| 110 | + |
| 111 | +## Scope & honest limitations |
| 112 | + |
| 113 | +- The comparison covers the **unsigned** APK content; the signature is intentionally |
| 114 | + excluded (only the developer holds the private signing key). |
| 115 | +- The build **image** is the trust root and is pinned **by digest**. It is published |
| 116 | + as a binary artifact and is **not** bit-for-bit rebuildable from the `Dockerfile` |
| 117 | + alone, because `apt` and `sdkmanager` fetch packages at image-build time. Verify |
| 118 | + the image by its digest to not rely on rebuilding it. |
| 119 | +- Verification is performed on `linux/amd64` (the image enforces this platform). |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +*Reproducibility setup behind this: pinned Gradle/AGP/NDK/CMake versions, Gradle |
| 124 | +dependency verification (`gradle/verification-metadata.xml`), `distributionSha256Sum` |
| 125 | +for the Gradle distribution, no VCS stamp (`vcsInfo.include = false`), neutral native |
| 126 | +build flags (`-ffile-prefix-map`, `--build-id=none`), and a digest-pinned container |
| 127 | +image.* |
0 commit comments