mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-22 07:06:05 +00:00
274 lines
11 KiB
Markdown
274 lines
11 KiB
Markdown
# Reproducible builds
|
|
|
|
Bitchat's canonical phone-app release build produces byte-for-byte reproducible
|
|
unsigned APKs and an unsigned Android App Bundle (AAB). CI builds the release
|
|
twice in independent jobs and exposes a verified release artifact only when
|
|
every canonical byte matches. The `:wear` module is tested and
|
|
dependency-locked but is outside this phone artifact contract until a separate
|
|
Wear release and signing process is defined.
|
|
|
|
Signing remains local: no keystore or signing password is stored in or exposed
|
|
to GitHub Actions. Anyone can reproduce the unsigned artifacts; maintainers
|
|
download the verified CI output, sign the selected GitHub APKs and Play upload
|
|
AAB locally, then manually publish those exact files.
|
|
|
|
Maintainers should use the complete
|
|
[Android maintainer release guide](maintainer-release-guide.md) for the
|
|
step-by-step release procedure, artifact inventory, local signing commands, and
|
|
GitHub/Google Play publication checklist.
|
|
|
|
## What is pinned
|
|
|
|
- Gradle wrapper version and distribution SHA-256
|
|
- dependency versions, strict Gradle dependency locks, and downloaded-artifact
|
|
SHA-256 verification metadata
|
|
- exact Temurin JDK release and digest-pinned Linux builder image
|
|
- Android platform, Platform Tools, and Build Tools archives by filename and
|
|
SHA-256, plus the accepted SDK license-text SHA-1 required to use them
|
|
- Kotlin/JVM toolchain and bytecode target
|
|
- Arti source tag and full commit, stable native build epoch, Rust, `cargo-ndk`,
|
|
Android NDK, Cargo lockfile, digest-pinned Rust builder image, and immutable
|
|
Debian package snapshot
|
|
- immutable full commit SHAs for every third-party GitHub Action
|
|
|
|
The build uses a clean source tree, an isolated Gradle user home, UTC, a stable
|
|
locale, `SOURCE_DATE_EPOCH` from the Git commit, no Gradle build or configuration
|
|
cache, fresh tasks, and a non-incremental in-process Kotlin compiler. Native
|
|
builds remap source paths and release validation rejects host paths in packaged
|
|
libraries. The container overlays a canonical `local.properties`, so an ignored
|
|
Android Studio file cannot redirect Gradle to a host-specific SDK.
|
|
|
|
AGP's embedded VCS record is disabled because its Git discovery depends on the
|
|
host checkout layout. The canonical `BUILDINFO.json` and GitHub provenance
|
|
attestation record the host-verified commit instead.
|
|
|
|
The authoritative pins are:
|
|
|
|
- `gradle/wrapper/gradle-wrapper.properties`
|
|
- `gradle/libs.versions.toml`
|
|
- `settings-gradle.lockfile`
|
|
- `app/gradle.lockfile`
|
|
- `wear/gradle.lockfile`
|
|
- `gradle/verification-metadata.xml`
|
|
- `tools/reproducible-builds/TOOLCHAIN.env`
|
|
- `tools/arti-build/TOOLCHAIN.env`
|
|
- `tools/arti-build/Cargo.lock`
|
|
|
|
## Reproduce a release locally
|
|
|
|
Requirements are Git, Docker with Linux/amd64 support, and enough free space for
|
|
the Android and Gradle images and dependencies.
|
|
|
|
```bash
|
|
git clone https://github.com/permissionlesstech/bitchat-android.git
|
|
cd bitchat-android
|
|
git checkout vX.Y.Z
|
|
tools/reproducible-builds/build-in-container.sh \
|
|
.reproducible-build/local-vX.Y.Z
|
|
```
|
|
|
|
The output contains:
|
|
|
|
- unsigned APKs for arm64, armv7, x86, x86_64, and universal installs
|
|
- `bitchat-android-release-unsigned.aab`
|
|
- `BUILDINFO.json`
|
|
- `SHA256SUMS.unsigned`
|
|
|
|
The output directory must not already contain files. The script rejects a dirty
|
|
checkout so the commit in `BUILDINFO.json` identifies all source inputs.
|
|
|
|
To test reproducibility yourself, build into two empty directories and compare:
|
|
|
|
```bash
|
|
BITCHAT_CONTAINER_GRADLE_HOME_NAME=gradle-home-first \
|
|
tools/reproducible-builds/build-in-container.sh .reproducible-build/first
|
|
BITCHAT_CONTAINER_GRADLE_HOME_NAME=gradle-home-second \
|
|
tools/reproducible-builds/build-in-container.sh .reproducible-build/second
|
|
tools/reproducible-builds/compare-release.sh \
|
|
.reproducible-build/first \
|
|
.reproducible-build/second
|
|
```
|
|
|
|
If a comparison fails and `diffoscope` is installed, the comparison script
|
|
automatically reports the first differing artifact.
|
|
|
|
## Verify a GitHub release
|
|
|
|
Install the GitHub CLI, authenticate it if necessary, check out the release tag,
|
|
and run:
|
|
|
|
```bash
|
|
git checkout vX.Y.Z
|
|
tools/reproducible-builds/verify-github-release.sh vX.Y.Z
|
|
```
|
|
|
|
That command:
|
|
|
|
1. downloads all release APKs, the AAB, build information, and checksum files;
|
|
2. verifies the canonical unsigned build's GitHub artifact-attestation subjects
|
|
against this repository;
|
|
3. verifies `BITCHAT_SHA256SUMS`;
|
|
4. checks that the local source commit is the release commit;
|
|
5. rebuilds in the pinned container; and
|
|
6. byte-compares every unsigned APK, the unsigned AAB, build information, and
|
|
the unsigned checksum manifest.
|
|
|
|
To verify the published checksums and attestations without rebuilding:
|
|
|
|
```bash
|
|
tools/reproducible-builds/verify-github-release.sh vX.Y.Z --no-rebuild
|
|
```
|
|
|
|
For a manual signature check, use the exact `apksigner` from Android Build Tools
|
|
37.0.0:
|
|
|
|
```bash
|
|
apksigner verify --verbose --print-certs bitchat-android-universal.apk
|
|
```
|
|
|
|
Compare the reported signer certificate SHA-256 with
|
|
`BITCHAT_GITHUB_RELEASE_CERT_SHA256` in `gradle.properties`. A matching
|
|
certificate proves who signed the APK; the checksum, attestation, and local
|
|
unsigned rebuild establish which source and build produced it. A third party
|
|
cannot recreate the signed bytes without the private release key.
|
|
|
|
You can also prove that the signed APK contains the same archive entries and
|
|
uncompressed payload bytes as the reproduced unsigned APK:
|
|
|
|
```bash
|
|
tools/reproducible-builds/compare-archive-payloads.sh \
|
|
.reproducible-build/local-vX.Y.Z/bitchat-android-universal-unsigned.apk \
|
|
bitchat-android-universal.apk
|
|
```
|
|
|
|
GitHub's manual equivalents are:
|
|
|
|
```bash
|
|
gh release download vX.Y.Z
|
|
sha256sum -c BITCHAT_SHA256SUMS
|
|
gh attestation verify bitchat-android-universal-unsigned.apk \
|
|
--repo permissionlesstech/bitchat-android
|
|
```
|
|
|
|
## Verify a Google Play release
|
|
|
|
Google Play App Signing changes the verification boundary:
|
|
|
|
- maintainers upload a signed AAB using the upload key;
|
|
- Google Play generates optimized, device-specific APK splits from that AAB;
|
|
- Google signs the delivered APKs with the app-signing key.
|
|
|
|
Consequently, a Play-delivered APK is not expected to be byte-identical to the
|
|
GitHub universal APK or to a locally built APK. Use this procedure instead:
|
|
|
|
1. In Play Console, open **Test and release > App bundle explorer**, select the
|
|
release/version code, and download the original app bundle if that option is
|
|
available to your account. Compare its unsigned payload with the reproduced
|
|
`bitchat-android-release-unsigned.aab`:
|
|
|
|
```bash
|
|
tools/reproducible-builds/compare-archive-payloads.sh \
|
|
.reproducible-build/local-vX.Y.Z/bitchat-android-release-unsigned.aab \
|
|
downloaded-from-play.aab
|
|
```
|
|
|
|
The helper excludes JAR-signing metadata and compares every other entry name
|
|
and uncompressed byte.
|
|
2. In **Setup > App integrity**, record the SHA-256 fingerprint under **App
|
|
signing key certificate**. This is different from the upload-key certificate
|
|
and may be different from the GitHub release certificate.
|
|
3. In App bundle explorer, download the Play-generated universal APK or the APKs
|
|
for a representative device. Verify each APK:
|
|
|
|
```bash
|
|
apksigner verify --verbose --print-certs downloaded-from-play.apk
|
|
```
|
|
|
|
The signer SHA-256 must equal the Play Console app-signing certificate.
|
|
4. Confirm package name `com.bitchat.droid`, version code, version name, and
|
|
manifest/security configuration with Android's `apkanalyzer` or `aapt2`.
|
|
5. Recreate Google's split-generation behavior from the reproduced AAB with the
|
|
same `bundletool` version and a saved device specification:
|
|
|
|
```bash
|
|
bundletool build-apks \
|
|
--bundle=bitchat-android-release-unsigned.aab \
|
|
--output=local.apks \
|
|
--device-spec=device.json
|
|
```
|
|
|
|
This last check validates bundle-to-APK behavior, but it is not a byte-equality
|
|
claim: Play's server-side `bundletool` version, optimization, and signing inputs
|
|
are controlled by Google. The strongest public Play verification requires
|
|
maintainers to retain the uploaded AAB, publish its digest and provenance, and
|
|
record the Play version code and app-signing certificate fingerprint alongside
|
|
the release.
|
|
|
|
The GitHub workflow builds and attests the canonical unsigned AAB. A maintainer
|
|
locally creates `bitchat-android-play-upload.aab` from that exact file and
|
|
uploads it manually to Google Play.
|
|
|
|
## Maintainer release process
|
|
|
|
Follow the
|
|
[Android maintainer release guide](maintainer-release-guide.md). It is the
|
|
authoritative operational runbook from version preparation through the signed
|
|
tag, GitHub Actions artifact download, local APK/AAB signing, GitHub draft,
|
|
Play internal test, public rollout, and post-release verification.
|
|
|
|
The workflow has no signing secrets and never publishes a release by itself.
|
|
|
|
## Updating dependencies or toolchains
|
|
|
|
Dependency changes must update and review both the lock state and verification
|
|
metadata:
|
|
|
|
```bash
|
|
./gradlew testDebugUnitTest lintDebug resolveIdeRuntimeClasspathCopyLocks \
|
|
--write-locks \
|
|
--write-verification-metadata sha256
|
|
```
|
|
|
|
`resolveIdeRuntimeClasspathCopyLocks` records the transient runtime classpath
|
|
copies that Android Studio resolves during model import. Their selected versions
|
|
are persisted under the generated copy configuration names while the canonical
|
|
debug and release runtime classpaths remain strictly locked.
|
|
|
|
Generate release lock entries in separate invocations because split APK and AAB
|
|
intermediates cannot coexist:
|
|
|
|
```bash
|
|
./gradlew :app:clean :app:bundleRelease \
|
|
--write-locks \
|
|
--write-verification-metadata sha256
|
|
./gradlew :app:clean :app:assembleRelease \
|
|
--write-locks \
|
|
--write-verification-metadata sha256
|
|
```
|
|
|
|
Review every new repository, component, artifact name, version, and checksum.
|
|
Do not accept verification metadata generated after an unexplained checksum
|
|
failure.
|
|
|
|
The verification metadata deliberately trusts only IDE documentation and source
|
|
attachments (`*-javadoc.jar`, `*-sources.jar`, and Gradle's `*-src.zip`). Android
|
|
Studio resolves these outside the build dependency graph, and they are not build
|
|
inputs. Compiled artifacts and dependency metadata remain checksum-verified.
|
|
|
|
When changing Gradle, update the wrapper and independently verify the new
|
|
distribution SHA-256. When changing JDK or Android tools, update the exact
|
|
version, archive checksum, and base-image digest together. Native updates follow
|
|
[`tools/arti-build/README.md`](../tools/arti-build/README.md).
|
|
|
|
## References
|
|
|
|
- [Gradle dependency locking](https://docs.gradle.org/current/userguide/dependency_locking.html)
|
|
- [Gradle dependency verification](https://docs.gradle.org/current/userguide/dependency_verification.html)
|
|
- [Gradle wrapper checksum verification](https://docs.gradle.org/current/userguide/best_practices_security.html#use_the_gradle_wrapper_and_verify_the_wrapper_checksum)
|
|
- [GitHub Actions security hardening](https://docs.github.com/en/actions/reference/security/secure-use)
|
|
- [GitHub artifact attestation verification](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations)
|
|
- [Google Play App Signing](https://support.google.com/googleplay/android-developer/answer/9842756)
|
|
- [Play Console App bundle explorer](https://support.google.com/googleplay/android-developer/answer/9859152)
|
|
- [Android `bundletool`](https://developer.android.com/tools/bundletool)
|
|
- [Android `apksigner`](https://developer.android.com/tools/apksigner)
|