build: make the Arti (Tor) native build reproducible

The libarti_android.so shipped in the APK is the one binary we compile
ourselves, and it was the remaining blocker to a verifiable build: a Rust
cdylib is only reproducible when the compiler, the dependency graph, and the
embedded build paths are all pinned. None were.

Pin all three:
- rust-toolchain.toml pins rustc (rustup auto-installs it + the Android
  targets), so codegen is stable across machines.
- Cargo.lock is now generated and committed (501 packages); both build
  scripts run `cargo --locked` so transitive versions can't drift.
- repro-env.sh (sourced by build-arti.sh and build-arti-host.sh) rewrites
  host-specific absolute paths with --remap-path-prefix, disables incremental
  compilation, and sets a fixed SOURCE_DATE_EPOCH derived from the Arti tag.

With these, an independent rebuild of the pinned tag reproduces the committed
.so bit-for-bit, which is what lets F-Droid / Zapstore verify it from source
instead of trusting a prebuilt blob. README documents the pins and a
two-path build-and-diff verification recipe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JtjUcSjjpu4auFndw1QKeU
This commit is contained in:
Claude
2026-06-26 23:01:48 +00:00
parent 69457ec10d
commit f7a15a8407
6 changed files with 5538 additions and 11 deletions
+5400
View File
File diff suppressed because it is too large Load Diff
+53 -8
View File
@@ -18,9 +18,40 @@ JNI wrapper built directly from Arti source.
Pre-built `.so` files should be committed to `amethyst/src/main/jniLibs/`. You only need to Pre-built `.so` files should be committed to `amethyst/src/main/jniLibs/`. You only need to
rebuild if you want to verify binaries, update the Arti version, or modify the JNI wrapper. rebuild if you want to verify binaries, update the Arti version, or modify the JNI wrapper.
## Reproducible builds
The shipped `.so` is **built to be byte-for-byte reproducible** so anyone —
F-Droid, Zapstore, or an independent auditor — can rebuild it from this tag and
confirm the committed binary wasn't tampered with. Three pins make that hold:
| Source of non-determinism | Pinned by |
|---|---|
| `rustc` / cargo version | [`rust-toolchain.toml`](rust-toolchain.toml) (rustup auto-installs it) |
| transitive dependency versions | committed [`Cargo.lock`](Cargo.lock); builds run `cargo --locked` |
| absolute build paths baked into the binary | `--remap-path-prefix` in [`repro-env.sh`](repro-env.sh) |
`repro-env.sh` (sourced by both build scripts) also sets `CARGO_INCREMENTAL=0`
and a fixed `SOURCE_DATE_EPOCH` derived from the Arti tag. The size-optimized
release profile in `Cargo.toml` (`lto`, `codegen-units = 1`, `strip`,
`panic = "abort"`) is itself deterministic for a fixed toolchain.
### Verify the committed binary reproduces
```bash
# Build twice into different checkout paths and confirm identical bytes.
# (Path remapping is what lets two different directories produce the same .so.)
cp -r tools/arti-build /tmp/arti-a && (cd /tmp/arti-a && ./build-arti.sh --release)
cp -r tools/arti-build /tmp/arti-b && (cd /tmp/arti-b && ./build-arti.sh --release)
sha256sum /tmp/arti-{a,b}/../../amethyst/src/main/jniLibs/arm64-v8a/libarti_android.so
```
A clean run prints the same SHA-256 for both, and matches the committed
`amethyst/src/main/jniLibs/arm64-v8a/libarti_android.so`.
## Prerequisites ## Prerequisites
1. **Rust toolchain** 1. **Rust toolchain** — the exact version is pinned in `rust-toolchain.toml`;
rustup installs it automatically. You only need rustup itself:
```bash ```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
``` ```
@@ -92,13 +123,17 @@ The first LOAD segment alignment should be `0x4000` (16384 bytes).
``` ```
tools/arti-build/ tools/arti-build/
├── README.md # This file ├── README.md # This file
├── ARTI_VERSION # Pinned Arti git tag (e.g., arti-v1.9.0) ├── ARTI_VERSION # Pinned Arti git tag (e.g., arti-v1.9.0)
├── Cargo.toml # Rust dependencies and build profile ├── rust-toolchain.toml # Pinned rustc version + Android targets (reproducibility)
├── build-arti.sh # Build script ├── Cargo.toml # Rust dependencies and build profile
├── Cargo.lock # Pinned transitive dependency versions (reproducibility)
├── repro-env.sh # Deterministic build env (path remapping, epoch) — sourced by both scripts
├── build-arti.sh # Build script (Android targets, shipped in APK)
├── build-arti-host.sh # Build script (host target, for JVM integration tests)
├── src/ ├── src/
│ └── lib.rs # JNI bridge (Rust → Kotlin) │ └── lib.rs # JNI bridge (Rust → Kotlin)
└── .arti-source/ # [gitignored] Cloned Arti repository └── .arti-source/ # [gitignored] Cloned Arti repository
``` ```
## Updating Arti version ## Updating Arti version
@@ -119,7 +154,17 @@ tools/arti-build/
https://gitlab.torproject.org/tpo/core/arti/-/raw/arti-v1.10.0/crates/arti-client/Cargo.toml https://gitlab.torproject.org/tpo/core/arti/-/raw/arti-v1.10.0/crates/arti-client/Cargo.toml
``` ```
4. Rebuild and test: 4. Regenerate the committed lockfile so the new versions are pinned (builds run
`--locked` and will fail until this is refreshed):
```bash
./build-arti.sh --clean # clones the new tag + sets up the wrapper
cp .arti-source/arti-android-wrapper/Cargo.lock ./Cargo.lock
```
If you also bump the Rust toolchain, edit `channel` in `rust-toolchain.toml`.
5. Rebuild, then re-verify reproducibility (see "Reproducible builds" above) and
commit the regenerated `.so` files **together with** `Cargo.lock` /
`rust-toolchain.toml`:
```bash ```bash
./build-arti.sh --clean ./build-arti.sh --clean
``` ```
+9 -2
View File
@@ -26,7 +26,8 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
WRAPPER_DIR="$SCRIPT_DIR/.arti-source/arti-android-wrapper" ARTI_SOURCE_DIR="$SCRIPT_DIR/.arti-source"
WRAPPER_DIR="$ARTI_SOURCE_DIR/arti-android-wrapper"
if [ ! -d "$WRAPPER_DIR" ]; then if [ ! -d "$WRAPPER_DIR" ]; then
echo "Arti source / wrapper not found at $WRAPPER_DIR." echo "Arti source / wrapper not found at $WRAPPER_DIR."
@@ -37,6 +38,12 @@ fi
# Sync the latest wrapper sources into the .arti-source clone — build-arti.sh # Sync the latest wrapper sources into the .arti-source clone — build-arti.sh
# normally does this, but if you've only edited lib.rs the host build needs it too. # normally does this, but if you've only edited lib.rs the host build needs it too.
cp "$SCRIPT_DIR/src/lib.rs" "$WRAPPER_DIR/src/lib.rs" cp "$SCRIPT_DIR/src/lib.rs" "$WRAPPER_DIR/src/lib.rs"
# Keep the dependency lock in sync with the committed one (build is --locked).
cp "$SCRIPT_DIR/Cargo.lock" "$WRAPPER_DIR/Cargo.lock"
# Same deterministic build env as the Android path (path remapping, pinned epoch).
# shellcheck source=repro-env.sh
source "$SCRIPT_DIR/repro-env.sh"
HOST_TARGET="$(rustc -vV | sed -n 's/^host: //p')" HOST_TARGET="$(rustc -vV | sed -n 's/^host: //p')"
case "$HOST_TARGET" in case "$HOST_TARGET" in
@@ -54,7 +61,7 @@ OUT_DIR="$PROJECT_ROOT/amethyst/src/test/native-libs/$DEST_TAG"
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"
echo "Building Arti shim for $HOST_TARGET → $OUT_DIR/libarti_android.so" echo "Building Arti shim for $HOST_TARGET → $OUT_DIR/libarti_android.so"
cargo build --release \ cargo build --release --locked \
--manifest-path "$WRAPPER_DIR/Cargo.toml" \ --manifest-path "$WRAPPER_DIR/Cargo.toml" \
--target "$HOST_TARGET" --target "$HOST_TARGET"
+17 -1
View File
@@ -131,6 +131,17 @@ setup_wrapper() {
cp "$SCRIPT_DIR/Cargo.toml" "$wrapper_dir/Cargo.toml" cp "$SCRIPT_DIR/Cargo.toml" "$wrapper_dir/Cargo.toml"
cp "$SCRIPT_DIR/src/lib.rs" "$wrapper_dir/src/lib.rs" cp "$SCRIPT_DIR/src/lib.rs" "$wrapper_dir/src/lib.rs"
# Reproducibility: build against the committed lockfile so transitive
# dependency versions are identical for everyone. `cargo --locked` (below)
# fails if this lock is missing or stale rather than silently re-resolving.
if [ -f "$SCRIPT_DIR/Cargo.lock" ]; then
cp "$SCRIPT_DIR/Cargo.lock" "$wrapper_dir/Cargo.lock"
print_success "Pinned dependencies from committed Cargo.lock"
else
print_error "tools/arti-build/Cargo.lock missing — generate it with: cargo generate-lockfile (see README)"
exit 1
fi
# Patch Cargo.toml to use local arti-client from the source tree # Patch Cargo.toml to use local arti-client from the source tree
# instead of pulling from crates.io # instead of pulling from crates.io
cd "$wrapper_dir" cd "$wrapper_dir"
@@ -170,7 +181,7 @@ build_for_target() {
-t "$target" \ -t "$target" \
--platform "$MIN_SDK_VERSION" \ --platform "$MIN_SDK_VERSION" \
-o "$OUTPUT_DIR" \ -o "$OUTPUT_DIR" \
build --release \ build --release --locked \
--manifest-path "$ARTI_SOURCE_DIR/arti-android-wrapper/Cargo.toml" --manifest-path "$ARTI_SOURCE_DIR/arti-android-wrapper/Cargo.toml"
if [ -f "$out_dir/$LIB_NAME" ]; then if [ -f "$out_dir/$LIB_NAME" ]; then
@@ -229,6 +240,11 @@ main() {
clone_or_update_arti clone_or_update_arti
setup_wrapper setup_wrapper
# Deterministic build env (needs ARTI_SOURCE_DIR cloned above for SOURCE_DATE_EPOCH).
# shellcheck source=repro-env.sh
source "$SCRIPT_DIR/repro-env.sh"
print_success "Reproducible build env loaded (RUSTFLAGS path remapping, SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-unset})"
for target in "${TARGETS[@]}"; do for target in "${TARGETS[@]}"; do
build_for_target "$target" build_for_target "$target"
done done
+41
View File
@@ -0,0 +1,41 @@
# Deterministic build environment for libarti_android.so.
#
# Sourced by build-arti.sh (Android targets) and build-arti-host.sh (host target)
# so the two paths stay in lockstep. Makes the Rust build byte-for-byte
# reproducible, which is what lets F-Droid / Zapstore / any third party rebuild
# the shipped .so from this tag and confirm it matches.
#
# The caller must already have set:
# SCRIPT_DIR — tools/arti-build
# ARTI_SOURCE_DIR — the Arti clone (.arti-source)
#
# The three things that otherwise make a Rust cdylib non-reproducible, and the
# fix for each:
# 1. Compiler version -> pinned by rust-toolchain.toml (rustup auto-installs).
# 2. Dependency versions -> pinned by the committed Cargo.lock + `cargo --locked`.
# 3. Absolute build paths embedded in panic locations / strings
# -> --remap-path-prefix rewrites them to stable virtual
# paths so two machines with different $HOME / checkout
# dirs produce identical bytes.
# Disable incremental compilation — its on-disk cache can perturb codegen order.
export CARGO_INCREMENTAL=0
# Where Cargo caches the crates.io registry + git deps (absolute, host-specific).
export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
# Rewrite every host-specific absolute prefix that rustc would otherwise bake
# into the binary. (Rust's own std is already remapped to /rustc/<hash> by the
# distributed toolchain, so pinning the version in rust-toolchain.toml covers it.)
REPRO_RUSTFLAGS="--remap-path-prefix=${CARGO_HOME}=/cargo"
REPRO_RUSTFLAGS="${REPRO_RUSTFLAGS} --remap-path-prefix=${ARTI_SOURCE_DIR}=/arti"
REPRO_RUSTFLAGS="${REPRO_RUSTFLAGS} --remap-path-prefix=${SCRIPT_DIR}=/arti-build"
export RUSTFLAGS="${RUSTFLAGS:-} ${REPRO_RUSTFLAGS}"
# Pin SOURCE_DATE_EPOCH to the commit the Arti tag points at — deterministic for
# a given ARTI_VERSION, and independent of when the build actually runs.
if [ -d "${ARTI_SOURCE_DIR}/.git" ]; then
_epoch="$(git -C "${ARTI_SOURCE_DIR}" log -1 --format=%ct 2>/dev/null || true)"
[ -n "${_epoch}" ] && export SOURCE_DATE_EPOCH="${_epoch}"
unset _epoch
fi
+18
View File
@@ -0,0 +1,18 @@
# Pin the exact Rust toolchain used to build libarti_android.so.
#
# Reproducibility: rustc output is only stable for a fixed compiler version, so
# everyone who rebuilds the shipped .so (us, F-Droid, an independent verifier)
# must use this exact toolchain. rustup reads this file automatically and
# installs the pinned version + the Android targets on first invocation of the
# build scripts. Bump this in lockstep with ARTI_VERSION / Cargo.lock and
# re-verify (see README.md → "Reproducible builds").
[toolchain]
channel = "1.94.1"
profile = "minimal"
components = ["rustc", "cargo", "rust-std"]
targets = [
"aarch64-linux-android",
"x86_64-linux-android",
"armv7-linux-androideabi",
"i686-linux-android",
]