Anti-fingerprint: measured baseline, harness, T3 patch + CI#39
Open
mdheller wants to merge 61 commits into
Open
Anti-fingerprint: measured baseline, harness, T3 patch + CI#39mdheller wants to merge 61 commits into
mdheller wants to merge 61 commits into
Conversation
Profile hardening: - agent-runtime: 6 prefs → 285-line comprehensive lockdown (private browsing autostart, WebRTC fully off, service workers/push/notifications disabled, all sensor APIs off, full dFPI, DoH strict, disk cache zeroed) - agent-runtime policies.json: 9 → 246 lines with Locked:true on all permission surfaces, extension install blocked, SanitizeOnShutdown locked - human-secure: TLS/OCSP hardening, Fission site isolation, CRLite revocation, IDN homograph protection, media fingerprinting resistance - human-secure: uBlock Origin pre-installed via ExtensionSettings, enterprise CA import disabled (prevents corporate MITM) Local PolicyFabric enforcement engine: - bearbrowser-policy-engine.py: reads local-default-actions.yaml and enforces allow/hold/deny with correct exit codes (0/2/3). Zero external deps. 21/21 verifier tests passing. - bearbrowser-verify-policy-engine.py: full test suite Governed automation sessions: - bearbrowser-playwright.sh: auto-calls policy engine, creates receipt before session, updates receipt on completion or failure - bearbrowser-create-receipt.py + bearbrowser-update-receipt.py: full BrowserAutomationReceipt lifecycle Governance dashboard: - bearbrowser-sidecar-server.py: /api/receipts, /api/hold-queue, /resolve endpoints; dark-mode dashboard with auto-refresh Binary overlay + source build pipeline: - bearbrowser-fetch-librewolf-base.sh: Homebrew-verified base fetch - bearbrowser-overlay-binary.sh: 9-step overlay with identity verification - bearbrowser-build-binary.sh: --lane overlay|source modes - verify-macos-app.sh: --skip-signing flag for development builds - Source build: Makefile-driven Firefox fetch → patch → bootstrap → build with BearBrowser MOZ_APP overrides baked in at compile time CI: - .github/workflows/policy-engine.yml: 9-step policy engine CI - feature-plane.yml: updated to include receipt scripts Homebrew: - 8 new commands exposed: policy-engine, verify-policy-engine, create-receipt, update-receipt, verify-automation-receipt, fetch-librewolf-base, overlay-binary, verify-macos-app
Source build: - bearbrowser-package-source-build.sh: packages obj-*/dist/BearBrowser.app from a completed mach build into a settings-injected, ad-hoc-signed BearBrowser.app ready for development use. Auto-detects workspace, injects user.js and policies.json, writes Info.plist, signs depth-first. macOS native launcher: - BearBrowserWebKitLauncher.m: updated launcher implementation - BearBrowser-start.html: updated start page - install-macos-app-launcher.sh: updated installer - repair-macos-app-launcher.sh: updated repair script Packaging: - Info.plist.template: completed with URL schemes and document types - package-linux-rpm.sh: updated RPM packaging
… filter engine Activates Firefox's built-in ContentClassifierService (already wired into the network stack via AsyncUrlChannelClassifier) with two curated filter lists covering ~500 ad network rules and ~300 tracker/telemetry rules. The service uses adblock-rust 0.12.1 (already vendored) compiled to a Rust FFI layer — no extension process, no JS overhead, requests blocked at the C++ network layer before any page code runs. BearBlockerChild JSWindowActor handles cosmetic filtering: injects a CSS style element hiding ad placeholder elements (the containers left behind after network blocking) plus a MutationObserver for SPA route changes. BearBlockerPolicy provides a PolicyFabric bridge for agent-runtime: every block event appends a signed record to bearblocker-receipts.jsonl, and uncertain matches can be held for human approval via consultPolicy(). bearbrowser-patches.py wires everything into the Firefox source tree at make-dir time: copies filter lists and actor files, patches moz.build DIRS, FINAL_TARGET_FILES, and DesktopActorRegistry actor registration.
Extends bearbrowser-patches.py with six steps that install BearBlocker into the Firefox source tree at make-dir time: 1. Copy bearblocker-ads.txt and bearblocker-privacy.txt into browser/bearblocker/ 2. Create browser/bearblocker/moz.build (FINAL_TARGET_FILES.bearblocker) 3. Copy JSWindowActor files into browser/actors/ 4. Add "bearblocker" to browser/moz.build DIRS 5. Add BearBlocker actor files to browser/actors/moz.build FINAL_TARGET_FILES 6. Register BearBlocker JSWindowActor in DesktopActorRegistry.sys.mjs Also promotes bearbrowser-patches.py to scripts/ (tracked) so the canonical patch pipeline is version-controlled outside the gitignored build/ workspace. The canonical bearbrowser.* StaticPrefList block now includes: bearbrowser.bearblocker.cosmetic.enabled (default true) bearbrowser.runtime.agent (default false, set by agent-runtime profile) bearbrowser.webgl.prompt / bearbrowser.webgl.prompt.hide
Runs on macos-15 (Apple Silicon) at 4 AM UTC daily, on manual dispatch,
and on pushes to main that touch settings, patches, or branding.
Pipeline:
apply-sourceos-overlays.sh → make fetch → make dir → mach bootstrap
→ mach build → bearbrowser-package-source-build.sh → hdiutil → gh release
Artifacts:
- GitHub Actions artifact retained 30 days (always)
- GitHub Release tagged nightly-YYYY-MM-DD (main branch only), prerelease,
same-day release replaced on re-run
No notarization — ad-hoc signed only. For developer/internal use.
Users open via right-click → Open to bypass Gatekeeper on first launch.
Cache layers: Mozilla toolchain (.mozbuild), Firefox source tarball,
Cargo registry. Estimated wall time ~2.5 h on macos-15.
- BearBrowserPolicyQueue.swift: native macOS NSStatusItem app that polls actions.jsonl every 3s for unresolved PolicyFabric hold entries, shows badge count (⚖ N), and exposes per-action Allow/Deny + bulk controls via menubar dropdown; resolution calls bearbrowser-resolve-action.py - scripts/build-hold-queue-app.sh: one-shot swiftc build script; --install flag copies binary to /usr/local/bin - settings/profiles/agent-runtime/user.js: sections 5 and 6 added — BearBlocker filter lists + runtime identity prefs (bearbrowser.runtime.agent, debugger.force_detach, console.logging_disabled)
Three things: 1. bearbrowser-open.sh — auto-launches BearBrowserPolicyQueue on BearBrowser start if the binary exists and isn't already running; syncs any accumulated BearBlocker receipts into events.jsonl at each launch. 2. bearbrowser-sync-blocker-receipts.py — idempotent bridge that reads new lines from bearblocker-receipts.jsonl (cursor-tracked) and emits each network_block / cosmetic_applied entry as an automation.observed event into the main provenance events pipeline. Callable one-shot or with --poll N. 3. LaunchAgent: ai.socioprophet.bearbrowser.policy-queue.plist + install script — installs BearBrowserPolicyQueue as a persistent login item via launchctl bootstrap so hold-queue UI is always present, not just when bearbrowser-open is used. install-policy-queue-agent.sh builds, installs, and loads in one step; --unload tears it down cleanly.
…x CI Extension lockdown (human-secure): - policies.json: InstallAddonsPermission.Default → false; ExtensionSettings.* installation_mode → blocked; removed uBlock auto-install (BearBlocker native replacement makes it redundant); added explicit blocked entries for 1Password extension (use desktop app integration) and known-bad extensions - settings/extensions/registry.json: authoritative registry of 26 evaluated extensions with dispositions: native (8), allowlist (6), blocked (9), review (2) — rationale documented for every entry Unseal mechanism: - scripts/bearbrowser-unseal-extensions.sh: checks registry disposition before unsealing (refuses blocked, warns on review/unknown); backs up policies.json with timestamp; patches ExtensionSettings to normal_installed; schedules auto-reseal background job after N minutes (default 15); --reseal flag for immediate restore; --list to enumerate allowlist-tier extensions Linux CI: - .github/workflows/nightly-linux.yml: ubuntu-24.04 runner, same cache strategy as macOS (mozbuild, tarball, cargo), produces tarball + AppImage (non-fatal if appimage script fails), uploads artifacts and appends to the same nightly-YYYY-MM-DD release tag created by the macOS job
BearSponsor (native SponsorBlock replacement): - BearSponsorParent.sys.mjs: fetches segments from sponsor.ajay.app using 4-char SHA-256 hash prefix — full videoId never leaves the browser - BearSponsorChild.sys.mjs: MutationObserver on <title> for SPA nav detection, 500ms poll on video.currentTime, skips on match, shows 2.5s toast - Categories: sponsor, selfpromo, interaction, intro, outro, preview, filler - pref: bearbrowser.sponsorblock.enabled (true in human-secure, false in agent) BearNav (native Vimium replacement): - BearNavChild.sys.mjs: f=link hints (2-char labels, golden overlay), F=new-tab hints, j/k/d/u scroll, gg/G top/bottom, H/L history, r reload, / URL bar - BearNavParent.sys.mjs: focuses gURLBar on BearNav:FocusUrlBar message - pref: bearbrowser.nav.keyboard.enabled (true in human-secure, false in agent) Build wiring (patches.py): - Step 3 extended to copy from settings/actors/ (BearSponsor, BearNav) in addition to settings/bearblocker/ (BearBlocker) - Step 5 moz.build entry now covers all 7 actor files - Step 6 DesktopActorRegistry now registers BearBlocker, BearNav, BearSponsor - StaticPrefList canonical block updated: adds nav.keyboard.enabled and sponsorblock.enabled in correct alphabetical position Extension registry: SponsorBlock and Vimium C moved from allowlist → native Linux CI: ubuntu/fedora matrix on nightly-linux.yml - matrix.include with distro-specific pkg_install commands (apt vs dnf) - Fedora 41 container on ubuntu-24.04 runner - fail-fast: false so one distro failure doesn't cancel the other - Both distros upload artifacts and append to same nightly release tag
WebKit 21623+ (macOS 26) requires that the WKWebView returned from createWebViewWithConfiguration:forNavigationAction:windowFeatures: be created with the exact cfg argument passed in. Previously we were creating fresh WKWebViewConfiguration objects (via baseConfig: for real tabs, via [[WKWebViewConfiguration alloc]init] for decoy/honeypot views), causing WebKit's SOAuthorizationCoordinator to throw NSException in createNewPage at the first popup attempt. Fix: use cfg directly for both paths. - Decoy (script popup): cfg satisfies WebKit's constraint; we cancel all navigations via decidePolicyForNavigationAction without loading anything. - User-initiated (OAuth, target=_blank): cfg ensures the new view shares the parent's websiteDataStore, which SOAuthorizationCoordinator requires for SSO session cookie access.
lastUserGestureTime was a unique behavioral signal: sites could call window.open() at controlled intervals, probe whether they got a real window or silent absorption, and map the threshold to fingerprint BearBrowser specifically (entropy matching). WebKit's own popup blocker already gates window.open() by user gesture at the engine level before calling createWebViewWithConfiguration:. Our delegate-level re-gate was redundant and exposed a distinguishing signal no other browser has. Removed: lastUserGestureTime property and update, decoyViews set and honeypot WKWebView path, dead decoy check in decidePolicyForNavigation. createWebViewWithConfiguration: now always opens a real tab — matching Safari's implementation exactly.
Cross-referenced against Mozilla Bugzilla RFP bugs and Firefox's resistfingerprinting test suite. Vectors added: Canvas (HIGH) - Hook toBlob in addition to toDataURL — both paths now apply LSB noise - Hook CanvasRenderingContext2D.getImageData directly — closes the bypass where fingerprinters call getImageData instead of toDataURL WebGL (MEDIUM) - VENDOR/RENDERER now return Safari-on-M1 strings instead of "BearBrowser" (distinguishable) — matches actual Safari Metal renderer identity - getSupportedExtensions() frozen to fixed Safari/M1 subset — removes GPU-specific extension list that was fully intact before Screen/window (HIGH — bug 418986) - screen.width/height/availWidth/availHeight → 1280×800 fixed - screen.pixelDepth added alongside colorDepth - window.devicePixelRatio → 2 (Retina — matches dominant Safari traffic) - window.outerWidth/outerHeight → innerWidth/innerHeight (hides chrome height) Navigator (MEDIUM) - navigator.platform → 'MacIntel' - navigator.maxTouchPoints → 0 - navigator.userAgentData deleted (UA-CH leaks OS/arch; Safari omits it) - navigator.connection deleted (NetworkInformation exposes network quality) - navigator.plugins/mimeTypes frozen to empty arrays Timing (HIGH — Firefox browser_reduceTimePrecision_iframes.js) - performance.now() jittered to 1ms granularity + ±0.1ms noise - Date.now() rounded to 100ms buckets Timezone (HIGH — bug 1896836, browser_timezone.js) - Intl.DateTimeFormat.prototype.resolvedOptions → timeZone: 'UTC' - Date.prototype.getTimezoneOffset → 0 AudioContext (HIGH — bug 1358149 FIXED in Firefox) - sampleRate normalized to 44100 - baseLatency normalized to 0.01 - getFloatFrequencyData adds ±0.00005 LSB noise to AnalyserNode output WebSpeech (LOW — bug 2043367, unfixed upstream) - speechSynthesis.getVoices() → empty array - SpeechSynthesisVoice removed window.name (MEDIUM) - Cleared on beforeunload to prevent cross-navigation tracking
…e sensors
These were the remaining surfaces Firefox either hasn't fixed (Bug 2043403,
Bug 2043367 still open/assigned) or explicitly declined (Bug 1336208 WONTFIX).
We fix them all.
WebGPU (Bug 2043403 — ASSIGNED in Firefox, unfixed)
- navigator.gpu → undefined; GPU{Adapter,Device,Buffer,Texture} deleted
- GPU adapter requestAdapterInfo() exposes vendor/arch/device at hardware-serial
granularity. No partial spoof is adequate; the whole API surface goes.
Font enumeration via measureText (Bug 1336208 — WONTFIX in Firefox, we fix)
- CanvasRenderingContext2D.prototype.measureText hooked: adds per-session
consistent FNV-1a noise (±0.2px) keyed on (font, text, session-salt)
- Same font+text always returns the same delta within a session, preventing
statistical averaging; differs across sessions, defeating cross-session tracking
- document.fonts.check() → false: blocks CSS local() font presence oracle
that fingerprinters use to enumerate installed system fonts without canvas
requestAnimationFrame timing (Firefox browser_animationapi_iframes.js)
- requestAnimationFrame callback timestamp truncated to Math.floor (1ms)
- Consistent with performance.now() and Date.now() precision floor already set
Device sensors (Firefox browser_device_sensor_event.js)
- addEventListener blocked for: deviceorientation, devicemotion,
deviceorientationabsolute, compassneedscalibration
- DeviceOrientationEvent, DeviceMotionEvent → undefined
- Generic Sensor API deleted: Accelerometer, Gyroscope, Magnetometer,
AbsoluteOrientationSensor, RelativeOrientationSensor, LinearAccelerationSensor,
GravitySensor, AmbientLightSensor
- screen.orientation normalized to landscape-primary / 0° (fixed form-factor signal)
Three coordinated layers blocking the remaining two JS-unreachable fingerprinting vectors: 1. @font-face local() CSS resolution (Gecko build) - patches.py: gfxUserFontSet.cpp — wraps LookupLocalFont() to return nullptr when bearbrowser.privacy.block_local_fonts is set, making all local() font-face entries fail to match installed fonts - patches.py: StaticPrefList.yaml — registers the new pref - user.js: layout.css.font-visibility.{standard,private,trackingprotection} set to 2/1/2 (base fonts only / hidden in private) — the Gecko pref-level guard that backs the C++ patch 2. Worker / compositor performance.now() precision (Gecko build) - patches.py: Performance.cpp + PerformanceWorker.cpp — wraps ReduceTimePrecisionAsMSecs() in std::floor() to enforce 1ms integer granularity unconditionally; registers bearbrowser.privacy.reduce_time_precision - user.js: privacy.resistFingerprinting.reduceTimerPrecision{,.microseconds} set to true / 1000 — the engine-level RFP hook covering all timing paths 3. Web Worker timing (WKWebView overlay) - BearBrowserWebKitLauncher.m: wraps the Worker constructor to produce a blob URL that prepends our performance.now() / Date.now() precision patch before importScripts()-ing the original script; falls back to the native Worker() if blob creation fails (module workers, CSP restrictions)
…egistry Native actor implementations from prior session, now committed: BearCapture (BearCaptureChild/BearCaptureParent) Scans DOM for video/audio/media links, shows floating badge with count, Cmd+Shift+D panel, queues to Firefox Downloads. Replaces Video DownloadHelper which used webRequest API (full network intercept) for what is a DOM scan. BearClip (BearClipChild/BearClipParent) Cmd+Shift+S captures bibliographic metadata from the current page: citation_* meta tags, Dublin Core, Open Graph, JSON-LD, arXiv IDs, DOIs. Saves to a local library. Zero extension surface. BearVault (BearVaultChild/BearVaultParent) Detects password fields, shows badge, fills from OS Keychain (macOS) / libsecret (Linux) on Cmd+Shift+L. Replaces Bitwarden extension — credentials never enter an extension sandbox or leave the OS keychain. BearSponsor: add #forceHighQuality() to set YouTube player to 4K via localStorage + player API polling. registry.json: mark Video DownloadHelper and Bitwarden as native (replaced). Also adds bundled woff2 font assets for native/macos/fonts/ and build/CI helper scripts (bearbrowser-install-fedora, bearbrowser-invoke-build, download-bundled-fonts, update-content-rules).
…gaps
Native function toString() spoofing
All overridden browser APIs now return "function x() { [native code] }"
when .toString() is called on them. Previously fingerprinters could trivially
detect our shield by checking HTMLCanvasElement.prototype.toDataURL.toString()
or performance.now.toString(). WeakMap-backed registry with object method
shorthand for correct .name and .length — indistinguishable from real natives.
performance.now precision fix
Changed from Math.floor(_pNow()) + Math.random()*0.1 to pure Math.floor().
Random sub-ms jitter was counterproductive: attackers average many samples
to recover the true value. Fixed 1ms integer buckets are resistant to
averaging and match Firefox RFP / Tor Browser's actual approach.
Additional identity normalization
- navigator.doNotTrack = '1'
- navigator.webdriver = undefined (false leaks automation detection surface)
- window.chrome deleted (its presence signals non-Safari vs our UA claim)
Intl locale normalization (JS + engine)
Intl.Collator, Intl.NumberFormat, Intl.ListFormat, Intl.PluralRules
resolvedOptions() all patched to return locale:'en-US'. Previously sites
could recover the OS locale via Intl even though navigator.languages was
spoofed. Gecko build: intl.accept_languages pref default set in
StaticPrefList.yaml; also added to user.js with javascript.use_us_english_locale.
Resource timing restriction
setResourceTimingBufferSize(0) + clearResourceTimings() prevents resource
timing entries from accumulating. PerformanceObserver wrapped to strip
'resource' type entries before delivering to callbacks. Resource timing
exposes precise network transfer durations that fingerprint connection topology.
Retroactive native registration
End-of-shield pass registers all prototype method overrides (canvas, WebGL,
Intl, Date, EventTarget, etc.) plus window-level overrides (rAF, eval,
postMessage, Worker, RTCPeerConnection) in the native map.
…k prefs
WKWebView: Accept-Language header at network layer
Set _HTTPAdditionalHeaders on WKWebViewConfiguration to lock Accept-Language
to en-US,en;q=0.9 for all requests. JS-layer Intl spoofing only covers the
JS context; the HTTP header was still leaking the OS locale on every request.
WKWebView: complete navigator identity for Safari UA consistency
Added vendor, vendorSub, productSub, appName, product assertions matching
Safari 17.6 on macOS. Deleted oscpu and buildID which are Firefox-only
properties that reveal Gecko to fingerprinters even when the UA claims Safari.
Gecko build: network normalization prefs
- network.http.accept-language: en-US,en;q=0.5 (explicit; RFP sets this
but making it explicit survives profile imports and pref resets)
- network.http.altsvc.enabled/oe: false — Alt-Svc headers allow servers
to redirect to different ports/protocols, creating a connection-timing
fingerprint vector via HTTP/3 upgrade probing
…x vector failures Root causes fixed: - AudioContext: Object.create(NativeConstructor) produces a plain object whose inherited .prototype is non-writable; assigning it threw TypeError in strict mode and silently aborted the entire shield after navigator/perf setup. Fix: use the _cAC wrapper function directly (it was built but never wired in). - window.SpeechSynthesisVoice=undefined: bare assignment to a non-writable global in strict mode. Fix: Object.defineProperty with configurable:true. - navigator.doNotTrack / webdriver: Object.defineProperty on the instance fails when the property doesn't exist in non-extensible Playwright context; fix targets Navigator.prototype instead. - canvas toDataURL/getImageData: retroactive _nat() registration was never reached (crash above). Fix: wrap assignments with _nat() at point of definition. - speechSynthesis.getVoices: instance assignment fails silently; fix targets SpeechSynthesis.prototype via Object.defineProperty for guaranteed override. - FontFaceSet.prototype.check: instance assignment fails; fix targets prototype. Also adds scripts/verify-fingerprint-shield.mjs (36-vector Playwright WebKit regression harness) so future edits can't regress without being caught.
WKWebView JS shield additions: - navigator.usb/bluetooth/hid/serial/xr/keyboard/credentials → undefined - navigator.getGamepads() → [] (no controller hardware enumeration) - navigator.mediaDevices.enumerateDevices() → [] (blocks camera/mic presence) - navigator.permissions.query() → 'prompt' for sensitive APIs (normalizes per-user permission state which is otherwise a stable identifier) - StorageManager.prototype.estimate() → fixed 120GB/4MB (storage quota and usage vary by device and form a unique fingerprint signal) - window.matchMedia() → normalized for 15 privacy-sensitive CSS media features including prefers-color-scheme, prefers-reduced-motion, color-gamut, HDR, pointer type, hover — all of which reflect system/display characteristics - Element.prototype.getBoundingClientRect() → ±0.1px per-session position offset (prevents sub-pixel font metric fingerprinting via layout geometry) - TextMetrics bounding box properties → same session-consistent FNV noise applied to actualBoundingBoxAscent/Descent and fontBoundingBoxAscent/Descent AudioContext crash fix: - _cAC wrapper function was created but _ACc (Object.create result) was used instead; assigning .prototype on a plain object that inherits from a native constructor threw TypeError in strict mode, silently aborting the shield Gecko user.js additions: - Gamepad/WebXR/VR APIs disabled - keyboard.layout_map disabled - Extension detection hardening (block_mozAddonManager) - CSS media feature normalization prefs (prefers-color-scheme, reduced-motion) - devPixelsPerPx = 2.0 to match spoofed JS devicePixelRatio - HTTP/3 QUIC disabled (reduces QUIC transport fingerprinting surface) - TLS version max set to 4 (TLS 1.3 ceiling, normalizes ClientHello) Test suite: 36 → 47 vectors (11 new vectors covering all new shield additions)
…tries, mediaCapabilities, RTCRtp codecs, fonts enum
…VG metrics, screen avail*, screenX/Y, window.name, Error.stack, fonts.load
Override 14 Math functions where JavaScriptCore diverges from V8 at the ULP level. creepjs hashes these outputs to detect WKWebView-as-Chrome; returning V8's exact float64 values for each probed input eliminates the signal.
…sOf, RTCPeerConnection ICE, Notification.permission - performance.timeOrigin clamped to 100ms buckets (was returning full-precision float) - Intl.supportedValuesOf intercepted with fixed Chrome-matching calendar/collation/numberingSystem lists; timeZone/currency sorted for ICU-version stability - WebRTC ICE candidate leak properly suppressed: previous iceServers:[] approach did not block mDNS candidates; now drops all icecandidate event listeners at the object level - Notification.permission locked to 'denied' matching Brave/Tor
- Canvas noise upgraded from deterministic XOR-1-every-400 to per-session random seed (1-4) at stride 100; different noise pattern each launch prevents cross-session hash correlation while remaining imperceptible - navigator.pdfViewerEnabled = true (Chrome 104+ property; WKWebView omits it)
…mance.memory stub Remove WebKit-exclusive APIs that creepjs/fingerprintjs probe to distinguish WebKit from Chrome: caretRangeFromPoint, WebKitCSSMatrix, webkitStorageInfo, webkitRequestFileSystem. Add performance.memory stub (Chrome-only non-standard API; its absence in WKWebView was a hard browser signal).
fingerprintjs isChromium() checks ≥5 of 7 legacy webkit-prefixed signals; add stubs for webkitPersistentStorage, webkitTemporaryStorage, webkitResolveLocalFileSystemURL, BatteryManager, webkitMediaStream, webkitSpeechGrammar so the session passes as Chromium. creepjs hashes Object.getOwnPropertyNames(window) against Chrome's key list; add stubs for showOpenFilePicker/SaveFilePicker/DirectoryPicker, EyeDropper, scheduler, trustedTypes, navigation so Chrome-exclusive keys are present.
…r shape Replace window.chrome=undefined with a Chrome-matching object that includes app.isInstalled, csi(), loadTimes(), and runtime.connect/sendMessage/onMessage. fingerprintjs and creepjs both probe window.chrome's presence and structure; undefined immediately identifies the session as non-Chromium.
fingerprintjs isChromium() requires navigator.vendor.indexOf('Google')===0.
The shield was returning Apple's vendor string, directly failing this check
and causing fingerprintjs to classify the session as non-Chromium.
…rgence BearBrowser uses WKWebView with a Safari 17.6 UA. Mixing Chrome-only signals (vendor='Google Inc.', window.chrome object, showOpenFilePicker, EyeDropper, scheduler, trustedTypes, navigation, BatteryManager) with a Safari UA creates a unique 'misconfigured' fingerprint worse than either consistent identity. Revert to Safari-consistent profile: - navigator.vendor: 'Apple Computer, Inc.' (matches Safari UA) - window.chrome: undefined (absent in Safari — consistent) - Remove Chrome-only API stubs that contradict the Safari UA - Remove performance.memory stub (Chrome-only API) - Remove document.caretRangeFromPoint deletion (valid Safari API) - Replace Chrome-specific Chromium-probe stubs with WebKit-prefix consistency stubs - Retain WebKitCSSMatrix/webkitStorageInfo removal (removed from modern Safari 12+) - Retain all genuine privacy improvements (timing, canvas, WebGL, audio, etc.)
… coverage Math overrides from _mPatch now pass through _nat so Math.acos.toString() returns '[native code]' — plain function bodies would expose monkey-patching. Add Intl.supportedValuesOf to retroactive registration block for same reason.
…at, AudioContext AudioContext constructor and WebGL2.getShaderPrecisionFormat were patched but not registered, so their .toString() leaked the modified implementation body.
The JS shield forces matchMedia('prefers-color-scheme: light') to return true,
but WKWebView on a dark-mode OS still renders CSS system color keywords (Canvas,
ButtonText, etc.) using dark-mode palette values. Setting wv.appearance to Aqua
makes these resolve to light-mode RGB, consistent with the JS override.
Applied to both makeWebViewPrivate: and the tab-replacement WKWebView path.
…prefs Gecko-first pivot. Audited human-secure + agent-runtime user.js against the current Tor Browser / arkenfox RFP baseline. The config was already strong; the issues were manual spoofs fighting RFP plus latent bugs: - Remove privacy.resistFingerprinting.randomDataOnCanvasExtract (both profiles): removed from Firefox (bug 1670447); canvas noise is always-on under RFP now (bug 1816189). The pref was a silent no-op. - Remove layout.css.devPixelsPerPx="2.0": the WKWebView JS shield's spoofed DPR leaked into the Gecko profile. Forcing 2x render scale breaks non-Retina/ external displays and desyncs from RFP's own DPR rounding — making the browser MORE fingerprintable. RFP owns devicePixelRatio. - Fix HTTP/3 contradiction: line set the misspelled pref network.http.http3. enabled (trailing 'd') to false — a non-existent pref, silent no-op — while the correct network.http.http3.enable was true. Net: HTTP/3 was on. Keep it on (blends with the QUIC-speaking majority; Tor does not disable it) and remove the broken line. - Add network.http.referer.XOriginTrimmingPolicy=2: genuinely additive cross-origin referer trimming not covered by RFP (arkenfox baseline). - Dedupe the duplicated BearBlocker pref block in human-secure.
verify-gecko-rfp.mjs is the Gecko-surface counterpart to verify-fingerprint- shield.mjs. Two phases: Phase A (static, zero-dep, always runs): audits both user.js profiles for the RFP backbone (required prefs + values), dead/no-op prefs, cohort-desyncing prefs (UA overrides, manual DPR spoof, non-native-theme=false), known typo pref names that silently no-op (e.g. http3.enabled vs http3.enable), and duplicate keys. Would have caught all three bugs from the RFP audit. Phase B (runtime, skips cleanly without a Gecko binary): launches Firefox with the profile's prefs and asserts RFP behaviours (timezone→UTC, getTimezoneOffset →0, hardwareConcurrency→2, languages→en, maxTouchPoints→0). Resolves a binary from $BEARBROWSER_BIN or Playwright's bundled Firefox. On first run Phase A caught real cross-profile drift: agent-runtime was missing reduceTimerPrecision, font-visibility, and locale normalization that human-secure sets — despite its header stating agents must blend into the same RFP cohort. Added those prefs to agent-runtime to match human-secure exactly.
bearbrowser:rfp:verify (both phases) and bearbrowser:rfp:verify:static (static pref audit only, zero-dep, CI-friendly).
With HTTP/3 + DoH enabled, the real cross-connection tracking vectors are server-issued, client-replayed opaque blobs that act as supercookies: - QUIC NEW_TOKEN address-validation tokens (RFC 9000 §8.1/§19.7/§21) - TLS 1.3 / QUIC 0-RTT session tickets / PSK (RFC 9001 §4.6, RFC 8446 §C.4) The decisive mitigation, privacy.partition.network_state, was already set in both profiles — it partitions the connection pool, TLS tickets, 0-RTT state and the QUIC NEW_TOKEN store by first-party site, so a tracker on two sites cannot link the visits. (RFP does NOT cover this — it is purely partitioning + TLS.) Add defense-in-depth: security.tls.enable_0rtt_data=false in both profiles, removing the 0-RTT early-data replay surface (RFC 8446 §8). Lock both privacy.partition.network_state and the 0-RTT pref into the Gecko RFP harness REQUIRED set so the protection cannot silently regress.
Provider change (user preference + privacy rationale): Quad9 is a Swiss
non-profit (GDPR jurisdiction, no commercial data incentive) and does NOT
forward EDNS Client Subnet (RFC 7871) to authoritative servers, unlike
ECS-forwarding resolvers. Uses 9.9.9.9 (filtered + DNSSEC, no ECS); 9.9.9.11
(ECS-forwarding) is explicitly avoided.
Critical: the provider was pinned in TWO places. policies.json DNSOverHTTPS.
ProviderURL (agent-runtime's is Locked:true) is authoritative and silently
overrides user.js network.trr.uri — so changing only user.js would have shipped
Cloudflare anyway. Updated both user.js and policies.json in both profiles.
Also removed a confirmed dead pref: network.trr.bootstrapAddress was renamed to
network.trr.bootstrapAddr in Firefox 89 (bug 1703216) — a silent no-op that only
"worked" because 1.1.1.1 is an IP needing no bootstrap. IP-form 9.9.9.9 likewise
needs no bootstrap (cert SAN covers the IP).
DNS hardening (belt-and-suspenders; mostly default-safe):
- network.trr.disable-ECS=true suppress ECS on the wire (RFC 7871)
- network.trr.allow-rfc1918=false reject private-IP answers (DNS rebinding)
- network.trr.send_user-agent-headers=false no UA leak to resolver
- network.trr.padding=true EDNS(0) padding (RFC 8467)
- network.dns.use_https_rr_as_altsvc=true / upgrade_with_https_rr=true
HTTP/3 discovery + HTTPS upgrade via DNS
HTTPS RR (RFC 9460), since Alt-Svc is off
- network.dns.echconfig.enabled=true / http3_echconfig.enabled=true
Encrypted ClientHello (encrypts SNI)
Harness (verify-gecko-rfp.mjs) extended to lock all of this in:
- REQUIRED: trr.mode=3, disable-ECS, allow-rfc1918=false, no-UA-to-resolver
- KNOWN_TYPOS: network.trr.bootstrapAddress -> bootstrapAddr
- FORBIDDEN value: trr.uri must not be the Quad9 ECS endpoint
- NEW cross-file check: policies.json DNSOverHTTPS.ProviderURL must agree with
user.js network.trr.uri (catches the Locked-policy-overrides-user.js trap) and
must not be an ECS endpoint
Investigated the JSONC-policies.json concern: it was already handled — the build strips comments via strip-json-comments.py in both apply-sourceos-overlays.sh and bearbrowser-overlay-binary.sh before Firefox reads the file (Firefox rejects a commented policies.json wholesale, silently disabling ALL policies including the Locked DoH). So the locked Quad9 DoH policy does apply. Add a regression guard: the Gecko RFP harness now runs the real build stripper in --check mode against each profile's policies.json, so a future structural break (trailing comma, etc.) fails the harness instead of silently shipping a policy file that disables every locked policy. Verified the guard rejects broken JSON.
Cross-profile drift sweep (the recurring bug class: profiles meant to share one RFP/privacy cohort silently diverging). Found and closed real parity gaps — several introduced by the recent per-profile DNS/ECH work: agent-runtime gained (parity with human-secure): - HTTP/3 + DNS HTTPS-RR posture (http3.enable, altsvc off, use_https_rr_as_altsvc, upgrade_with_https_rr) so H3 is discovered via DNS HTTPS records in both - ECH (echconfig.enabled, http3_echconfig.enabled) — encrypts SNI - TLS floor/ceiling (version.min=3/max=4) + refuse unsafe renegotiation - referer XOriginTrimmingPolicy=2 human-secure gained: - privacy.partition.network_state.ocsp_cache (agent-runtime already had it) Left intentional differences alone: agent-runtime disables WebRTC entirely and clears-on-shutdown (ephemeral agents) vs human-secure's hardened-but-on WebRTC and persistent state; keyboard-nav UI toggle. Locked the shared invariants into the harness REQUIRED set (tls.version.min/max, echconfig.enabled, referer XOriginTrimmingPolicy, ocsp_cache partition) so this drift class fails CI instead of shipping.
…MITM
policies.json audit beyond DNS. agent-runtime was missing two security policies
that human-secure has:
- Certificates.ImportEnterpriseRoots=false — do NOT import OS/enterprise root CAs;
importing them is what lets a corporate MITM proxy decrypt the agent's HTTPS
traffic. Trust only Mozilla's vetted bundle. Especially important for an
automation runtime handling sensitive sessions.
- EncryptedMediaExtensions {Enabled:false, Locked:true} — Widevine DRM blob off
at the policy layer (belt-and-suspenders with user.js media.eme.enabled=false).
Harness: after validating policies.json strips to valid JSON, parse it and assert
Certificates.ImportEnterpriseRoots !== true in both profiles, so a future
MITM-enabling policy fails CI. (Absent key = Firefox default false = passes.)
Note: the inline comments in agent-runtime/policies.json were removed by the
concurrent JSONC task; the build-script wiring + strip-json-comments.py from that
task remain unstaged here for it to commit.
measure-fingerprint.mjs launches a real Gecko engine x3 (bare control + our RFP profile x2 sessions) and scores entropy reduction across the surface the real suites (Cover Your Tracks / creepjs / fingerprintjs) hash. Classifies each vector NORMALIZED / RANDOMIZED (per-session, unlinkable) / MATCHES-COHORT / LEAKING. npm: bearbrowser:fp:measure. First measured baseline (human-secure, Gecko 141 via Playwright): neutralized 14/17 high-entropy vectors (82%). Per-device signals all handled: canvas RANDOMIZED, WebGL renderer masked, hardwareConcurrency->2, DPR->2, timezone offset->0, deviceMemory/touch/connection at cohort. Three residuals, characterized: - screen WxH: Playwright headless viewport artifact (not the real monitor); letterboxing only operates windowed -> verify on real build. - audio (OfflineAudioContext): RFP randomizes canvas but NOT audio; value stable across sessions. Known Gecko/Tor residual, no pref fixes it (needs engine patch; the legacy WKWebView shield hand-noised it). - non-base fonts: 13/14 macOS system fonts still detectable under font-visibility=2 (OS-bundled, so uniform across Macs, but a cross-OS signal). The Tor/Mullvad fix is bundled fonts (tranche T3). This converts "we believe it's hardened" into a measured, repeatable 82% with a named residual list driving T1/T3.
WebRTC ICE leak test added to the measurement harness. Empirically verified: - bare Firefox leaks mDNS candidates (obfuscated, not raw IP) - human-secure suppresses ALL candidates (ice.no_host + default_address_only) - agent-runtime removes RTCPeerConnection entirely No raw private/public IP leaks in either profile. Baseline now 15/18 (83%). Empirical conclusion: the config-addressable fingerprint surface is essentially complete. The 3 remaining residuals all need NON-config fixes: - screen letterboxing: verify on real windowed build (headless artifact here) - audio randomization: Gecko engine patch (RFP doesn't noise OfflineAudioContext) - non-base fonts: bundled fonts (font-visibility 1/2/3 all leave 13/14 macOS system fonts detectable — confirmed by experiment; pref is not the lever)
Measured proof that bundling fonts alone is insufficient: measureText width =
453.54998779296875 (full sub-pixel), IDENTICAL control vs RFP — RFP does nothing
to text metrics. That float encodes font + HarfBuzz GPOS kerning + platform
rasterizer = high entropy, fully exposed via measureText/TextMetrics/
getBoundingClientRect. Added 'text-metric readback' vector to the measurement
harness (target 'int', currently LEAKING).
docs/anti-fingerprint-T3-fonts-text-metrics.md specs the three-layer fix:
- Layer A: bundle a metric-compatible font set (Arimo/Tinos/Cousine + Noto +
emoji) AND restrict gfxPlatformFontList to an allowlist via source patch so
system fonts are invisible to enumeration AND local() fallback (font-visibility
pref proven insufficient — 1/2/3 all leak 13/14 macOS fonts).
- Layer B: deterministic shaping/kerning — advances from font hmtx/GPOS, disable
platform hinting/optical adjustments.
- Layer C ("do our own kerning"): nsRFPService patch quantizing measureText, all
TextMetrics fields, getBoundingClientRect/getClientRects (Element+Range), and
SVG text length APIs. Best form derives advances from font units so the value is
identical on every OS. CRITICAL: text metrics must be UNIFORM, not randomized
(randomizing would split the cohort — opposite of canvas/audio).
Rollout T3a (prefs+packaging+allowlist) -> T3b (integer-round quantizer) ->
T3c (font-unit-derived advances, true cross-OS uniformity).
Measured the FULL text-layout readback surface, not just measureText. Found two distinct metric paths that produce different numbers and both leak sub-pixel, RFP-noop: - canvas: measureText = 453.549987 - layout/SVG: getBoundingClientRect/getComputedTextLength = 439.600006 plus offsetWidth (454, integer but carries the metric). A fix on one API leaves the others open. Added 'layout text metric' vector alongside 'canvas text metric' so the harness tracks both (now 15/20). docs/anti-fingerprint-T3-implementation-plan.md is the execution runbook: - Complete surface inventory / holes register (every API + path + coverage). - Architecture: ONE chokepoint at gfxShapedText per-glyph advance — every consumer (canvas/layout/SVG/Range/offsetWidth) inherits the quantized value, vs whack-a-mole per-API (which leaves holes). - Work items W1 font bundle, W2 gfxPlatformFontList allowlist (load-bearing source patch; font-visibility proven insufficient), W3 generic remap (gated to ship WITH W1/W2), W4 advance quantizer (the chokepoint; UNIFORM not randomized), W5 deterministic advance source (HarfBuzz/hmtx not CoreText optical), W6 fold in audio randomization, W7 verification incl. cross-OS proof. - Sequencing, risk/holes register with coverage mapping, rollback gating. Authored ahead of build; compile/integrate/ship is the "after" step.
Grounded in the actual build system (inspected the LibreWolf-style build repo: Makefile, scripts/bearbrowser-patches.py, assets/patches.txt, patches/). Key facts the on-ramp pins down: - Precedent: the project ALREADY ships RFP/FPP patches via this mechanism (patches/fpp-canvas-fix.patch, ui-patches/website-appearance-ui-rfp.patch in assets/patches.txt). Our 3 T3 patches follow that exact pattern. - Source tree to edit: `make patches` extracts firefox-<ver>.source.tar.xz and runs bearbrowser-patches.py -> bearbrowser-<ver>-<release>/ (the tree that only exists post-extract; that's why it vanished mid-session). - Inner loop: edit -> git diff to patches/<name>.patch -> register in assets/patches.txt -> `make check-patchfail` (FAST, verifies apply without a compile) -> `make build` (slow) -> `make run`. - Real-binary measurement needs geckodriver/Marionette, NOT Playwright (Playwright only drives its Juggler build, not stock LibreWolf); reuse the same PROBE. - Where each artifact lands (patches/ + assets/patches.txt for the 3 patches; assets/fonts/ + packaging for fonts; settings/profiles for the gated prefs). - Per-item definition-of-done tied to specific harness vectors + a cross-OS proof. The implementation itself still needs a build env (stable checkout + compiler); this makes that step turnkey rather than exploratory.
Read the existing patches/fpp-canvas-fix.patch to learn how THIS tree plumbs RFP. It plumbs canvas randomization at the DOM layer (dom/canvas/*) via nsRFPService::RandomizePixels(GetCookieJarSettings(), PrincipalOrNull(), ...), because RFP gating needs the principal + cookie-jar settings — which are NOT available deep in gfx/thebes. Correction: the first draft's "single chokepoint in gfxShapedText" is elegant but wrong — gfx code can't gate on RFP. Real architecture (mirrors the canvas patch): one nsRFPService::SpoofTextMetrics helper, called from each DOM entry point that exposes a text metric — CanvasRenderingContext2D::MeasureText, Element/Range getBoundingClientRect, SVGTextContentElement::*. A few faithful call sites instead of one gfx hook, but each has the RFP context and matches the accepted pattern, so it will actually compile. Quantizer math (integer px / font-unit-derived) unchanged.
…es cleanly First actual engine patch, not a sketch. Authored against extracted Firefox 150.0.1 source, grounded in real APIs and verified to apply+reverse cleanly against pristine source (patch -p1 --dry-run + reverse = the check-patchfail equivalent). gecko-patches/anti-fingerprint/anti-fp-canvas-text-metrics.patch: - Adds RFPTarget::CanvasTextMetrics (id 81) to RFPTargets.inc. - In CanvasRenderingContext2D::DrawOrMeasureText MEASURE branch, gates on nsContentUtils::ShouldResistFingerprinting(doc, RFPTarget::CanvasTextMetrics) (the real (const Document*, RFPTarget) overload, confirmed at nsContentUtils.h :406) and quantizes every TextMetrics field to whole CSS px via std::round (already used in-file). Uniform, never randomized. Confirmed present in-tree so it should compile: the overload, std::round, nsContentUtils.h/nsRFPService.h includes, RFPTarget usage. NOT yet compiled — ./mach build is the remaining gate (needs the build env). Follows the existing patches/fpp-canvas-fix.patch pattern. README documents wiring (copy to build repo patches/ + register in assets/patches.txt) and the remaining W4 call sites (layout BCR, SVG) + W2 allowlist + W6 audio as TODO. Honest status: covers the canvas text-metric path (harness vector 'canvas text metric' -> int); layout + SVG paths are separate call sites still to author the same way.
Three tiers, matching what each runner class can actually do: Tier 1 (harness-and-measure, every push/PR, ubuntu-latest): installs Playwright Firefox and runs the config harness (verify-gecko-rfp.mjs — HARD GATE) plus the empirical fingerprint measurement on real Gecko for both profiles, uploading the scorecards as artifacts. This continuously regression-gates the config posture and the measured baseline. Works on stock runners today. Tier 2 (patch-apply, every push/PR, ubuntu-latest): fetches the pinned Firefox source from archive.mozilla.org and dry-run applies every gecko-patches/*.patch, failing on any reject — the check-patchfail equivalent. No compile, so it runs on a stock runner and proves our engine patches still apply as Firefox moves. Tier 3 (full-build, workflow_dispatch only): checks out the build repo, registers our patches into assets/patches.txt, runs make check-patchfail -> make bootstrap -> make build, then measures the real binary via geckodriver. HONEST: a full Firefox build OOMs/overflows the stock 14GB GitHub runner — needs a large or self-hosted runner (repo var BUILD_RUNNER); the project's Forgejo/Woodpecker pipeline is the production build path. This is the GitHub scaffold + wiring. So the compile CAN happen in CI; Tiers 1-2 run anywhere now, Tier 3 needs a big runner (or the existing Forgejo build).
CI on PR #39 caught two real bugs in the tooling (not the config/patches): 1. harness-and-measure failed: verify-gecko-rfp.mjs Phase B asserted the RFP timezone string === 'UTC', but Firefox RFP reports 'Atlantic/Reykjavik' (UTC+0, no DST — privacy-equivalent; offset 0 is checked separately). Same issue I fixed in measure-fingerprint.mjs but missed here. Now accepts either (rfp_timezone_neutral). Verified passing locally. 2. patch-apply failed: double '../' in the patch path — `find ../gecko-patches` already yields a relative path and the step prepended another '../', so `patch -i ../../gecko-patches/...` couldn't open the file. The 150.0.1 source DID download+extract fine (URL was correct). Switched to absolute paths (captured pwd before cd). Verified end-to-end against the real extracted source: anti-fp-canvas-text-metrics.patch applies OK, exit 0. The 3 other PR failures (manifest-validation, validate-feature-plane, validate-native-shell) are pre-existing repo validators on the accumulated main history — confirmed none of this session's files touch their inputs.
No-holes pass on the canvas patch found a real leak path: OffscreenCanvas- RenderingContext2D subclasses CanvasRenderingContext2D and inherits DrawOrMeasureText, but my RFP gate read mCanvasElement->OwnerDoc() — and an OffscreenCanvas (Worker / transferControlToOffscreen) has mCanvasElement == null, so the gate went false and measureText stayed UNQUANTIZED in workers. Fix: gate on both contexts, mirroring the codebase's own pattern (pre-existing uses at CanvasRenderingContext2D.cpp:5512/5556): if (mCanvasElement) -> nsContentUtils::ShouldResistFingerprinting(doc, ...) else if (mOffscreenCanvas) -> mOffscreenCanvas->ShouldResistFingerprinting(...) Re-verified: applies + reverses cleanly against pristine Firefox 150.0.1.
The biggest measured leak (13/14 macOS fonts detectable, font-visibility 1/2/3 all useless) is now closed — WITHOUT a risky engine patch. Discovery: Gecko has a built-in anti-fingerprint allowlist, font.system.whitelist, whose ApplyWhitelist() filters the installed font list to only the named families. Empirically verified on real Gecko: detection drops 13/14 -> 0/14. W1 — bundle (packaging/bundled-fonts/): Arimo (Arial-metric), Tinos (Times), Cousine (Courier) — the Croscore set Tor/Mullvad use, SIL OFL, 10 TTFs / 4.4MB. Activated via gfx.bundled-fonts.activate=1; ActivateBundledFonts() loads from NS_GRE_DIR/fonts = Contents/Resources/fonts. Wired into bearbrowser-overlay- binary.sh (step 6b). W2 — allowlist (both profiles' user.js): font.system.whitelist="Arimo, Tinos, Cousine" so every OS exposes the SAME font set (cross-platform uniformity) + generic remap (serif->Tinos, sans-serif->Arimo, monospace->Cousine). SAFETY: ApplyWhitelist ignores the list if none of the families are present, so the prefs are safe to ship before bundling is verified in a build — worst case a no-op, never a zero-font browser. So no W1/W2 atomicity risk. Harness locks gfx.bundled-fonts.activate + font.system.whitelist (both profiles). Note: on plain Playwright Firefox (no bundle) the measurement font vector stays at 13/14 (whitelist falls back) — real verification is on the built binary, like the screen/letterboxing residual. (Includes strip-json-comments.py + userjs-to-autoconfig.py — helpers the overlay script references — so the packaging step is self-consistent.)
RFP randomizes canvas but NOT WebAudio, so the OfflineAudioContext hash is stable
across sessions (measured: identical 152.77... across control + both sessions).
Brave "farbles" audio; we didn't. This closes it.
gecko-patches/anti-fingerprint/anti-fp-audio.patch (5 files, real Gecko source):
- RFPTarget::WebAudioFarble (id 82, placed by AudioSampleRate so it can't collide
with the canvas patch's RFPTargets edit).
- nsRFPService::FarbleAudioData(global, data, len) — modeled on RandomizePixels;
gated on ShouldResistFingerprinting; applies a per-session, inaudible (±1e-7
multiplicative) factor from RandomUint64 (deterministic within a session so audio
still works, random across sessions so the fingerprint can't be linked).
- Call sites at the audio READ paths: AudioBuffer::RestoreJSChannelData (the single
materialization point behind getChannelData/copyFromChannel — once-per-buffer, no
compounding) and AnalyserNode::GetFloat{Frequency,TimeDomain}Data. Byte variants
skipped (0-255 quantized, 1e-7 is a no-op there — not a hole).
- Forward-declares nsIGlobalObject in nsRFPService.h; adds mozilla/RandomNum.h.
Verified: applies + reverses cleanly on pristine Firefox 150.0.1 AND sequences
cleanly after the canvas patch (non-overlapping RFPTargets edits). Not yet
compiled. Flips harness 'audio (oac)' -> randomized on the real build.
Caveat: per-session (per-process), not yet per-origin like Brave — defeats the
measured cross-session residual; per-origin is a documented follow-up.
…pass Makes Tier-3 real (was a TODO placeholder). measure-fingerprint.mjs is now engine-agnostic: --bin <path> / $BEARBROWSER_BIN drives the actual built binary via geckodriver (Playwright's Juggler build can't drive a stock LibreWolf). Same PROBE + scoring. Validated locally driving a real Firefox binary end-to-end. The geckodriver path immediately earned its keep — it sees what Playwright masks: - screen letterboxing shows as normalized (1366x768 -> 1200x600); headless Playwright couldn't show it. - TIMEZONE: with RFP on (hwConcurrency=2 confirms), geckodriver shows tz=America/New_York, offset=240 — RFP timezone NOT spoofed — while Playwright reports offset 0 because it sets the TZ env var. So the harness Phase-B timezone check was a FALSE PASS. Relabeled rfp_timezone_neutral/tz_offset_zero -> rfp_timezone_indicative (reported, never gated); the authoritative check is the geckodriver/real-binary measurement. (The Playwright FF is a Juggler-modified build, so whether OUR LibreWolf build spoofs tz is decided by Tier-3 — if it doesn't, that's a found gap to fix.) Tier-3 full-build job now: builds, locates the dist binary, runs measure- fingerprint --bin for both profiles, prints LEAKING vectors, uploads scorecards. Adds geckodriver + selenium-webdriver as devDependencies. Net: the stack moves from "verified-applies" toward "verified-works" — and the adapter already caught a real measurement-integrity gap Playwright was hiding.
User chose the Forgejo pipeline for the Tier-3 compile. The build repo is the
read-only librewolf-source-mirror (AGENTS.md: no direct commits), so our patches
must NOT go there — they stay in the overlay (gecko-patches/) and are injected
into the transient workspace clone at overlay-application time.
apply-sourceos-overlays.sh now copies gecko-patches/anti-fingerprint/*.patch into
<workspace>/source/patches/ and registers them in assets/patches.txt (canvas then
audio), so bearbrowser-patches.py applies them during the build.
VERIFIED with the build repo's own check-patchfail.sh: the FULL upstream patch
sequence (~40 LibreWolf patches) + both of ours applied to a fresh Firefox 150.0.1
extraction with ZERO rejects ("All patches were applied successfully"). The
fpp-canvas-fix.patch co-location on CanvasRenderingContext2D.cpp / nsRFPService.cpp
is handled by patch offset detection. So these are proven to apply in the REAL
build order, not just against pristine.
docs/anti-fingerprint-forgejo-build.md documents the build+measure flow:
apply-sourceos-overlays -> make build -> measure-fingerprint --bin (geckodriver,
authoritative). The measurement answers what Playwright masks (timezone,
letterboxing) and flips the patch vectors green on the real binary.
…sure binary The build infra is NOT Forgejo — the overlay has real GitHub Actions build lanes (nightly-linux on ubuntu-24.04, nightly-dmg on macos-15) that run apply-sourceos-overlays.sh -> make fetch -> make dir -> ./mach bootstrap -> ./mach build. Since apply-sourceos-overlays now injects our patches, these lanes compile BearBrowser WITH the anti-fingerprint patches. The 0s "failures" weren't build failures: the push trigger was main-only, so feature-branch pushes produced empty records. workflow_dispatch only works from the default branch (remote main is behind). So to compile THIS branch: - add anti-fingerprint-verify to push branches + gecko-patches/bundled-fonts/ workflow paths (this commit's push triggers it). Revert once merged to main. - disk-cleanup step (frees ~30GB; Firefox build needs ~40GB). - measurement step after ./mach build: measure-fingerprint.mjs --bin drives the compiled binary via geckodriver -> authoritative scorecard (what Playwright masks: timezone, real letterboxing; and whether canvas/audio/fonts flipped green), uploaded as fp-real.json. This commit's push starts the compile of our patched binary.
…e compile ROOT CAUSE of the 0-job / 0s "failures": the "Publish nightly release" step's run: | block had a --notes "..." string whose continuation lines (Not signed..., Commit:...) sat at column 0 — less indented than the block scalar, which ENDS the scalar and makes GitHub's YAML parser reject the entire workflow at startup. So the lane never created jobs and never built (pre-existing, since before this work). Fix: build the release notes with a single-line printf (--notes "$NOTES") so no line breaks out of the block scalar. YAML now parses cleanly (18 steps). With this + the earlier wiring (apply-sourceos-overlays injects our patches, disk cleanup, geckodriver measurement step), this push should finally make the lane create jobs and compile BearBrowser WITH the anti-fingerprint patches, then print the authoritative real-binary scorecard.
…ches.py The real compile (nightly-linux) got through setup + fetch + most of patch application, then died at: sed -i '' 's/9456ca.../60cd124.../g' .../encoding_rs/.cargo-checksum.json `sed -i ''` is BSD/macOS syntax — on GNU sed (Linux CI) the empty '' is parsed as the script and the filename as a second file, so it fails. The author had already converted some seds to Python "for macOS BSD sed compatibility" but missed these. Our patches were never reached — they apply after. Converted all remaining in-place seds to cross-platform Python file replacement: - encoding_rs .cargo-checksum.json (two checksums for rust-build.patch) - appstrings.properties Firefox->BearBrowser rename (os.walk instead of find -exec sed) Script compiles; no `sed -i ''` commands remain. This unblocks patch application so the build reaches ./mach build. (Also carries the concurrent task's _gfx_new sed->Python conversion, same theme.) This push re-triggers the compile.
…apply) After the sed fix, the real Linux build got further and PROVED our patches apply: patch -p1 -i ../patches/anti-fp-canvas-text-metrics.patch -> Hunk #1 succeeded patch -p1 -i ../patches/anti-fp-audio.patch -> applied (authoritative CI proof, beyond local check-patchfail). It then hit another pre-existing infra gap: `cp ../patches/pref-pane/category-bearbrowser.svg` — the pref-pane UI assets (svg/css/xhtml/js) aren't committed anywhere in the repo. The pref-pane is an optional UI feature, orthogonal to the engine and to anti- fingerprinting. Guard the whole block (patch + copies) behind an existence check so the build proceeds without it when the assets are missing, instead of hard- failing. Lets the build reach ./mach build to produce a measurable binary.
The build reached ./mach build and the compiler checks PASSED (clang 20.1.8 working), then configure hard-failed: ERROR: Invalid value --with-l10n-base, .../lw/l10n doesn't exist This is a dev build (en-US only; l10n download is intentionally skipped), so the l10n base dir never exists. A code comment claims --with-l10n-base was removed from the mozconfig, but the mirror's mozconfig.new still carries it. Strip it in bearbrowser-patches.py (overlay-canonical) after both mozconfig copies. Progress so far on the never-worked build lane: BSD sed -> Python, missing pref-pane assets -> graceful skip, now stale l10n-base -> stripped. Our anti-fp patches already proven to apply in CI. This should let configure complete and the actual compile proceed.
Closes the one gap RFP can't reach — the NETWORK LAYER (JA3/JA4, HTTP/2 frame, real IP) — by routing through Tor's uniform exit. Shipped as a TIER (user's choice per session), not a default: - BearBrowser mode: our best-in-class direct-connection hardening (text-metrics, audio, fonts) — beats Tor/Brave on the JS surface. - Tor mode: route through Tor + blend into the Tor Browser cohort. settings/profiles/tor-mode/ (the Tor delta on the human-secure baseline): - Fail-closed SOCKS routing: all traffic -> 127.0.0.1:9050, socks_remote_dns (DNS via Tor), failover_direct=false (never silently go direct). - Every proxy-bypass vector killed: WebRTC off, DoH/TRR off, IPv6 off, HTTP/3 off, no prefetch/predictor/speculative. - Proxy LOCKED via enterprise policy (Proxy + Locked:true) so no site/script can deanonymize. - Cohort alignment: keep the bundled Croscore fonts (Tor ships the SAME set), but DISABLE our unique RFP targets (CanvasTextMetrics, WebAudioFarble) via fingerprintingProtection.overrides — uniqueness defeats anonymity, so we blend into the Tor cohort instead of standing out. THE COHORT PARADOX (docs/tor-mode.md): you can't be uniquely-best-BearBrowser AND network-anonymous at once — uniqueness is the enemy of anonymity. So Tor mode trades our novel protections for blending into Tor's crowd. Honest limit: we're ALIGNED to the Tor cohort, not byte-identical to Tor Browser (different build); network-layer anonymity is full, JS blend-in is best-effort. Skipped JonDonym (defunct) and exotic mixnets (cohort fragmentation); obfs4/ Snowflake ride with Tor in Phase 2. tor-mode registered in the build/overlay profile allowlists.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Kicks off the anti-fingerprint CI (
.github/workflows/anti-fingerprint.yml).What runs on this PR
gecko-patches/*.patchagainst pinned Firefox 150.0.1 source; fails on any reject.workflow_dispatch-only — needs a large/self-hosted runner.Contents (accumulated main work + this session)
measure-fingerprint.mjs(75% baseline, every leak named).anti-fp-canvas-text-metrics.patch) verified to apply+reverse cleanly.Note: this branch carries the accumulated
mainhistory (remote main is behind), so the diff is large — the new work is the anti-fingerprint commits.🤖 Generated with Claude Code