diff --git a/gecko-patches/anti-fingerprint/anti-fp-audio.patch b/gecko-patches/anti-fingerprint/anti-fp-audio.patch index ea81561..e047c5e 100644 --- a/gecko-patches/anti-fingerprint/anti-fp-audio.patch +++ b/gecko-patches/anti-fingerprint/anti-fp-audio.patch @@ -66,7 +66,7 @@ + // fingerprint cannot be linked between sessions — RFP randomizes canvas but + // not audio, and this closes that residual). + static const double sFarbleFactor = []() { -+ uint64_t r = mozilla::RandomUint64OrZero(); ++ uint64_t r = mozilla::RandomUint64().valueOr(0); + double frac = (double)(r % 2000001) / 1000000.0 - 1.0; // [-1, 1] + return 1.0 + frac * 1e-7; + }(); diff --git a/scripts/apply-sourceos-overlays.sh b/scripts/apply-sourceos-overlays.sh index dd44bb1..dcfc287 100755 --- a/scripts/apply-sourceos-overlays.sh +++ b/scripts/apply-sourceos-overlays.sh @@ -249,6 +249,14 @@ afp_dir="$repo_root/gecko-patches/anti-fingerprint" patches_txt="$workspace/source/assets/patches.txt" if [ "$profile" = "tor-mode" ]; then echo "feature-layer: tor-mode omits the canvas/audio anti-fp patches (disabled to match cohort; see docs/tor-mode.md)" + # Drop msix.patch (Windows Store packaging) from the tor-mode stack: it's + # irrelevant to a Linux/macOS Tor browser AND it fails to apply on the 140 ESR + # tree (hunk drift), which aborts the whole `make` build. The CI patch-apply + # gate already excludes it; do the same for the real build. + if [ -f "$patches_txt" ]; then + grep -v 'patches/msix\.patch' "$patches_txt" > "$patches_txt.tmp" && mv "$patches_txt.tmp" "$patches_txt" + echo "feature-layer: tor-mode dropped msix.patch (Windows-only; drifts on ESR)" + fi # ...but tor-mode DOES need the OS-spoof patch: Tor forces the Windows identity # on all desktop platforms, which is a cohort REQUIREMENT (not one of our extra # protections). It's gated on -DBEARBROWSER_FORCE_WIN_SPOOF, set below. diff --git a/scripts/gcp-build-linux.sh b/scripts/gcp-build-linux.sh new file mode 100755 index 0000000..dc09143 --- /dev/null +++ b/scripts/gcp-build-linux.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Builds BearBrowser from source on a GCP VM and pulls back binaries + fingerprint +# scorecards. NO SSH is used — this org blocks port 22 and OS Login key +# registration, so the VM is driven entirely through GCS + a startup script: +# upload repo -> VM builds autonomously -> VM pushes artifacts + DONE to GCS -> +# we poll GCS, download, and tear the VM down. (Linux binaries: proves the engine +# patches + gives the scorecard; a runnable macOS app needs Apple hardware.) +# +# scripts/gcp-build-linux.sh --dry-run # free: validate auth/machine/image/bucket +# scripts/gcp-build-linux.sh # build human-secure + tor-mode +set -uo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="${BEARBROWSER_HOME:-$(cd "$script_dir/.." && pwd)}" + +INSTANCE="${BB_GCP_INSTANCE:-bearbrowser-build-$(date +%Y%m%d-%H%M%S)}" +ZONE="${BB_GCP_ZONE:-us-central1-a}" +MACHINE="${BB_GCP_MACHINE:-c2d-standard-32}" +IMAGE_FAMILY="${BB_GCP_IMAGE_FAMILY:-ubuntu-2404-lts-amd64}" +IMAGE_PROJECT="${BB_GCP_IMAGE_PROJECT:-ubuntu-os-cloud}" +DISK_GB="${BB_GCP_DISK_GB:-150}" +MAX_HOURS="${BB_GCP_MAX_HOURS:-5}" +SERVICE_ACCOUNT="${BB_GCP_SA:-synapseiq-build@socioprophet-platform.iam.gserviceaccount.com}" +BUCKET="${BB_GCP_BUCKET:-sourceos-artifacts-socioprophet}" +PROFILES="${BB_PROFILES:-human-secure tor-mode}" +art_out="$repo_root/build/gcp-artifacts" +keep=""; dry_run="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --profiles) PROFILES="${2:?}"; shift 2 ;; + --machine) MACHINE="${2:?}"; shift 2 ;; + --zone) ZONE="${2:?}"; shift 2 ;; + --bucket) BUCKET="${2:?}"; shift 2 ;; + --instance) INSTANCE="${2:?}"; shift 2 ;; + --keep) keep="1"; shift ;; + --dry-run) dry_run="1"; shift ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "ERROR: unknown arg: $1" >&2; exit 1 ;; + esac +done + +PREFIX="gs://$BUCKET/bearbrowser-builds/$INSTANCE" +created="" +cleanup() { + local rc=$? + if [ -n "$created" ] && [ -z "$keep" ]; then + echo ">> Tearing down $INSTANCE ..." + gcloud compute instances delete "$INSTANCE" --zone="$ZONE" --quiet 2>/dev/null \ + && echo ">> $INSTANCE deleted." \ + || { echo "!! WARNING: delete failed — $INSTANCE MAY STILL BE BILLING."; \ + echo "!! Run: gcloud compute instances delete $INSTANCE --zone=$ZONE --quiet"; } + elif [ -n "$created" ]; then + echo ">> --keep set: $INSTANCE left running (remember to delete it)." + fi + exit "$rc" +} +trap cleanup EXIT INT TERM + +# ── Preflight (all free) ──────────────────────────────────────────────────── +echo ">> Preflight..." +gcloud auth print-access-token >/dev/null 2>&1 \ + || { echo "ERROR: gcloud not authenticated. Run: gcloud auth login"; exit 1; } +echo " account: $(gcloud config get-value account 2>/dev/null) project: $(gcloud config get-value project 2>/dev/null)" +gcloud compute machine-types describe "$MACHINE" --zone="$ZONE" --format='value(name)' >/dev/null 2>&1 \ + || { echo "ERROR: machine type '$MACHINE' not available in '$ZONE'."; exit 1; } +img="$(gcloud compute images describe-from-family "$IMAGE_FAMILY" --project="$IMAGE_PROJECT" --format='value(name)' 2>/dev/null)" +[ -n "$img" ] || { echo "ERROR: image family '$IMAGE_FAMILY' not found."; exit 1; } +gcloud storage ls "gs://$BUCKET" >/dev/null 2>&1 \ + || { echo "ERROR: cannot access bucket gs://$BUCKET"; exit 1; } +echo " machine: $MACHINE image: $img bucket: gs://$BUCKET profiles: [$PROFILES]" +[ -f "$script_dir/gcp-vm-startup.sh" ] || { echo "ERROR: missing gcp-vm-startup.sh"; exit 1; } +if [ -n "$dry_run" ]; then echo ">> DRY RUN OK — auth + machine + image + bucket all valid. No VM, no cost."; exit 0; fi + +# ── Package + upload repo to GCS ──────────────────────────────────────────── +echo ">> Packaging + uploading repo to $PREFIX/bb-repo.tgz ..." +tarball="/tmp/bb-repo-$$.tgz" +tar --exclude='./build' --exclude='./node_modules' --exclude='./.git' \ + --exclude='*.tar.gz' --exclude='*.tar.xz' -czf "$tarball" -C "$repo_root" . \ + || { echo "ERROR: repo tar failed"; exit 1; } +gcloud storage cp "$tarball" "$PREFIX/bb-repo.tgz" >/dev/null 2>&1 \ + || { echo "ERROR: repo upload failed"; exit 1; } +rm -f "$tarball" +echo " uploaded." + +# ── Provision with startup script (no SSH) ────────────────────────────────── +echo ">> Creating VM $INSTANCE ($MACHINE, $ZONE, ${DISK_GB}GB)..." +gcloud compute instances create "$INSTANCE" \ + --zone="$ZONE" --machine-type="$MACHINE" \ + --image-family="$IMAGE_FAMILY" --image-project="$IMAGE_PROJECT" \ + --boot-disk-size="${DISK_GB}GB" --boot-disk-type=pd-ssd \ + --max-run-duration="$(( MAX_HOURS*3600 ))s" --instance-termination-action=DELETE \ + --service-account="$SERVICE_ACCOUNT" --scopes=cloud-platform \ + --metadata="bb-prefix=$PREFIX,bb-profiles=$PROFILES" \ + --metadata-from-file="startup-script=$script_dir/gcp-vm-startup.sh" \ + --quiet || { echo "ERROR: instance create failed"; exit 1; } +created="1" + +# ── Poll GCS for the DONE marker (no SSH) ─────────────────────────────────── +echo ">> Building on VM (polling GCS every 60s; safety cap ${MAX_HOURS}h)..." +deadline=$(( $(date +%s) + MAX_HOURS*3600 )) +build_rc="1" +while :; do + if gcloud storage cat "$PREFIX/DONE" >/tmp/bb-done 2>/dev/null; then + build_rc="$(tr -dc 0-9 /dev/null | grep -aE '===|profile|make build|BUILT|FAIL|OS-spoof' | tail -1)" + echo " [$(date -u +%H:%M:%S)] ${line:-(booting / installing toolchain…)}" + sleep 60 +done + +# ── Collect artifacts from GCS ────────────────────────────────────────────── +mkdir -p "$art_out" +echo ">> Downloading artifacts from $PREFIX/ ..." +gcloud storage cp -r "$PREFIX/artifacts/*" "$art_out/" >/dev/null 2>&1 || echo " (no artifacts — see build.log)" +gcloud storage cp "$PREFIX/build.log" "$art_out/build.log" >/dev/null 2>&1 || true +echo ">> Build finished (rc=$build_rc). Artifacts in $art_out:" +ls -lh "$art_out" 2>/dev/null | sed 's/^/ /' +exit "$build_rc" # teardown in trap diff --git a/scripts/gcp-remote-build.sh b/scripts/gcp-remote-build.sh new file mode 100755 index 0000000..619607f --- /dev/null +++ b/scripts/gcp-remote-build.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Runs ON the GCP build VM (Ubuntu). Builds one or more BearBrowser profiles from +# source (full Gecko compile with our engine patches), measures the real binary, +# and stages artifacts in ~/artifacts. Driven by scripts/gcp-build-linux.sh. +# +# Not set -e: one profile failing must not abort the others. Each profile is +# isolated and its failures are logged + collected. +set -uo pipefail + +profiles="${1:-human-secure tor-mode}" +repo="${BB_REPO:-$HOME/BearBrowser}" +art="$HOME/artifacts" +mkdir -p "$art" +log() { echo "[$(date -u +%H:%M:%S)] $*"; } +fail=0 + +log "BearBrowser GCP build — profiles: [$profiles]" +log "cores=$(nproc) mem=$(free -g | awk '/Mem:/{print $2}')GB disk_free=$(df -h "$HOME" | awk 'NR==2{print $4}')" + +# ── Base OS dependencies (Mozilla's own toolchain comes from `mach bootstrap`) ── +log "Installing base apt dependencies..." +sudo apt-get update -qq +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + git python3 python3-dev python3-pip python3-venv curl wget mercurial zstd \ + build-essential pkg-config libssl-dev libxml2-dev nodejs npm xz-utils \ + >/dev/null 2>&1 || log "WARN: some apt deps failed (continuing — mach bootstrap covers most)" + +# Node deps for the fingerprint scorecard (geckodriver + selenium are in package.json). +cd "$repo" || { log "FATAL: repo not found at $repo"; exit 1; } +log "Installing node measurement deps..." +npm ci >/dev/null 2>&1 || npm install >/dev/null 2>&1 || log "WARN: npm deps failed (scorecard may be skipped)" + +for p in $profiles; do + log "=================== PROFILE: $p ===================" + pfail=0 + + log "[$p] overlay prep (apply-sourceos-overlays --profile $p)..." + if ! bash scripts/apply-sourceos-overlays.sh --profile "$p" --ref latest > "$art/$p-overlay.log" 2>&1; then + log "[$p] FAIL: overlay prep — see $p-overlay.log"; fail=1; continue + fi + ws="$(find build/workspaces -maxdepth 2 -type d -name source 2>/dev/null | sort | tail -1)" + if [ -z "$ws" ] || [ ! -f "$ws/Makefile" ]; then + log "[$p] FAIL: no workspace/Makefile produced"; fail=1; continue + fi + log "[$p] workspace: $ws (firefox version: $(cat "$ws/version" 2>/dev/null))" + + ( + cd "$ws" || exit 1 + echo "=== make bootstrap (Mozilla toolchain + fetch/extract/patch source) ===" + MOZBUILD_STATE_PATH="$HOME/.mozbuild" make bootstrap || exit 11 + echo "=== make build (compile — the long step) ===" + make build || exit 12 + ) > "$art/$p-build.log" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + log "[$p] FAIL: build rc=$rc — see $p-build.log (tail below)"; tail -25 "$art/$p-build.log"; fail=1; continue + fi + + # The branded build names the binary bearbrowser/librewolf, not firefox. + bin="$(find "$ws" -type f \( -name bearbrowser -o -name librewolf -o -name firefox \) -path '*dist/bin*' 2>/dev/null | head -1)" + if [ -z "$bin" ]; then + log "[$p] FAIL: build succeeded but no dist/bin binary found (looked for bearbrowser/librewolf/firefox)" + find "$ws" -type f -path '*dist/bin*' 2>/dev/null | grep -viE '\.(so|js|json|txt|xpi|ini)$' | head -20 + fail=1; continue + fi + log "[$p] BUILT: $bin" + "$bin" --version > "$art/$p-version.txt" 2>&1 || true + + # ── Scorecard: drive the REAL binary (authoritative) ── + log "[$p] measuring fingerprint scorecard..." + PATH="$repo/node_modules/.bin:$PATH" \ + node scripts/measure-fingerprint.mjs --profile "$p" --bin "$bin" --json \ + > "$art/$p-scorecard.json" 2> "$art/$p-measure.log" || log "[$p] WARN: measurement failed — see $p-measure.log" + + # tor-mode: assert the OS-spoof actually compiled in (Windows identity on this Linux host) + if [ "$p" = "tor-mode" ] && [ -s "$art/$p-scorecard.json" ]; then + if grep -q "Windows NT 10.0" "$art/$p-scorecard.json" && grep -q "Win32" "$art/$p-scorecard.json"; then + log "[$p] ✓ OS-spoof VERIFIED: binary reports Windows identity" + else + log "[$p] ✗ OS-spoof NOT present: binary did not report Windows — the mozconfig flag did not take effect"; fail=1 + fi + fi + + # ── Package the runnable dist for download ── + distbin="$(dirname "$bin")" # .../obj-*/dist/bin + log "[$p] packaging dist..." + tar -C "$(dirname "$distbin")" -czf "$art/bearbrowser-$p-linux-x86_64.tar.gz" bin 2>/dev/null \ + || log "[$p] WARN: packaging failed" + log "=================== DONE: $p (size: $(du -h "$art/bearbrowser-$p-linux-x86_64.tar.gz" 2>/dev/null | cut -f1)) ===================" +done + +log "Artifacts staged in $art:" +ls -lh "$art" 2>/dev/null | sed 's/^/ /' +[ "$fail" -eq 0 ] && log "ALL PROFILES OK" || log "ONE OR MORE PROFILES FAILED (rc collected; see per-profile logs)" +exit "$fail" diff --git a/scripts/gcp-vm-startup.sh b/scripts/gcp-vm-startup.sh new file mode 100644 index 0000000..3d7d242 --- /dev/null +++ b/scripts/gcp-vm-startup.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# VM startup script (runs as root at boot via instance metadata). No SSH is used — +# this org blocks port 22 + OS Login key registration, so the VM is fully +# autonomous: pull the repo from GCS, build (as a non-root user), push artifacts + +# a DONE marker back to GCS. The orchestrator polls GCS and tears the VM down. +set -uo pipefail +exec > >(tee /var/log/bb-build.log) 2>&1 # also visible via serial console +md() { curl -s -H 'Metadata-Flavor: Google' "http://metadata.google.internal/computeMetadata/v1/instance/attributes/$1"; } + +BUCKET_PREFIX="$(md bb-prefix)" # gs://bucket/path/ +PROFILES="$(md bb-profiles)" +echo "=== BearBrowser VM build start $(date -u) — prefix=$BUCKET_PREFIX profiles=[$PROFILES] ===" + +# gsutil/gcloud: Ubuntu GCP images don't ship the CLI — install via snap. +if ! command -v gsutil >/dev/null 2>&1; then + echo "=== installing google-cloud-cli ===" + snap install google-cloud-cli --classic || { echo "FATAL: cannot install gcloud cli"; exit 1; } +fi + +mark_done() { echo "$1" | gsutil cp - "$BUCKET_PREFIX/DONE" 2>/dev/null; } +push_log() { gsutil cp /var/log/bb-build.log "$BUCKET_PREFIX/build.log" 2>/dev/null || true; } +trap 'push_log' EXIT + +# Non-root build user (mach refuses to build as root); passwordless sudo for apt + mach bootstrap. +id builder >/dev/null 2>&1 || useradd -m -s /bin/bash builder +echo 'builder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/builder + +echo "=== fetching repo from $BUCKET_PREFIX/bb-repo.tgz ===" +if ! gsutil cp "$BUCKET_PREFIX/bb-repo.tgz" /tmp/bb-repo.tgz; then echo "FATAL: repo fetch failed"; mark_done 90; exit 1; fi +sudo -u builder mkdir -p /home/builder/BearBrowser +tar xzf /tmp/bb-repo.tgz -C /home/builder/BearBrowser +chown -R builder:builder /home/builder/BearBrowser + +echo "=== building (as builder): profiles [$PROFILES] ===" +su - builder -c "cd ~/BearBrowser && bash scripts/gcp-remote-build.sh '$PROFILES'" +rc=$? +echo "=== build finished rc=$rc ===" + +echo "=== uploading artifacts to $BUCKET_PREFIX/artifacts/ ===" +gsutil -m cp -r /home/builder/artifacts/'*' "$BUCKET_PREFIX/artifacts/" 2>/dev/null || echo "WARN: artifact upload partial" +push_log +mark_done "$rc" +echo "=== DONE (rc=$rc) — orchestrator will collect + tear down ==="