browsec/FIREFOX_INTEGRATION.md

13 KiB

Firefox integration spike

Status: active design and source reconnaissance
Date: 2026-08-17

First running prototype

The adjacent Firefox checkout now has branch codex/browsec-spike with commit 5ec348b0f1 (Add Velvet Hammer certificate investigation prototype).

The prototype adds a visibly distinct, browser-owned Velvet Hammer frame to about:certerror. Its investigation action reads Firefox's protected FailedCertSecurityInfo, displays the requested host, original NSS error, failure category, conventional override eligibility, HSTS state, and the number of handshake certificates, then computes and displays SHA-256 fingerprints from the original DER certificate strings.

It deliberately remains read-only and displays NOT TRUSTED; no prototype UI action silently changes Firefox trust. The dedicated browser test browser_aboutCertError_velvetHammer.js passes all seven assertions, including the complete DER-derived fingerprint and unchanged terminal trust state.

This first visible surface uses data already exposed to Firefox's trusted certificate-error document. It does not yet provide the richer immutable TlsFacts, append-only journal, plugin chain, policy broker, or fresh retry required by the architecture below.

The following commit e4623c33b3 (Connect certificate investigation to parent trust host) establishes the first real process boundary. A built-in BrowsecTrustHost.sys.mjs runs in the Firefox parent process through the NetErrorParent actor. It validates bounded DER input, computes fingerprints, normalizes a Trust API 0.1 failure record, attributes evidence to the Firefox TLS adapter, and invokes the irremovable fail-closed final handler. The content page now only requests evaluation and renders the returned facts, journal, and Boolean verdict. Its browser test passes 11/11 assertions.

This is a host integration slice, not yet the complete TrustLab runner. It has one evidence adapter and the built-in final handler; package discovery, capability grants, ordered third-party plugins, deadlines, and interactive decisions remain to be ported. The input also still travels from trusted about:certerror to the parent actor. It becomes authoritative only when the native PSM continuation freezes and registers the observation before any page is created.

Pinned reconnaissance baseline

The first source inspection uses an adjacent, shallow Firefox checkout:

Checkout:    /home/sergeych/dev/browsec-firefox
Upstream:    https://github.com/mozilla-firefox/firefox.git
Branch:      main
Depth:       1
Commit:      b462c13f11417e13461f1202d71b14e2784f5db0
Commit date: 2026-08-17T09:27:35Z
Version:     156.0a1

This is a reconnaissance baseline, not the release baseline. Browsec will select an ESR only after the integration seam has been proven. The exact commit, rather than the moving branch name, defines all source references in this document.

The Firefox checkout is deliberately not nested in this repository. Git history can be added only when needed:

git fetch --deepen=50 origin main
# Or, only if full history becomes necessary:
git fetch --unshallow origin

Architectural boundary

An ordinary WebExtension cannot enforce Browsec trust semantics. Firefox core must remain the reference monitor. It alone may:

  • receive authoritative results and certificate bytes from PSM/NSS;
  • pause or terminate a TLS connection;
  • validate a plugin verdict against identity, capabilities, scope, lifetime, policy version, connection identity, and overridable error classes;
  • make a stored policy visible to later certificate verification;
  • require a genuinely fresh connection after a new decision; and
  • host browser chrome that content and ordinary extensions cannot imitate.

Velvet Hammer and other security extensions remain JavaScript/TypeScript. They receive immutable facts, append attributed journal entries, and return the strict API's Boolean verdicts. They never receive an NSS handle, arbitrary XPCOM access, or direct access to another extension's data.

Confirmed certificate-verification path

At the pinned Firefox revision, the ordinary TLS path is:

  1. AuthCertificateHookInternal in security/manager/ssl/SSLServerCertVerification.cpp captures the peer DER chain and dispatches an SSLServerCertVerificationJob.
  2. SSLServerCertVerificationJob::Run() executes on a certificate-verification thread in the parent process.
  3. AuthCertificate() calls CertVerifier::VerifySSLServerCert() and obtains the constructed DER chain, validation result, EV state, CT information, issuer-source information, and related facts.
  4. AuthCertificateParseResults() categorizes a failed result and consults Firefox's existing nsICertOverrideService for eligible exact-certificate overrides.
  5. Both successful and failed results are moved through BaseSSLServerCertVerificationResult::Dispatch().
  6. SSLServerCertVerificationResult::Run() executes on the socket transport thread, populates CommonSocketControl, and finally calls SetCertVerificationResult(mFinalError).

The result object already carries both the constructed chain and peer-presented chain as original DER byte arrays. This makes it the narrowest promising seam for producing TlsFacts without reparsing lossy display objects.

HTTP/3 has an additional path in netwerk/protocol/http/Http3Session.cpp, which uses AuthCertificateHookWithInfo(). It reaches the same verification machinery, but must receive explicit integration tests; shared implementation does not by itself prove equivalent pause, retry, and connection-reuse semantics.

Proposed native seam

Add a browser-internal BrowsecTrustHost continuation between receipt of the PSM result and the final call to SetCertVerificationResult:

NSS/PSM verification
        |
        v
SSLServerCertVerificationResult::Run
        |
        v
freeze Browsec TLS observation
        |
        +---- conventional success ---> bounded pre-accept chain
        |
        `---- conventional failure ---> normal failure + investigation record
                                           |
                                           v
                                   protected Velvet Hammer UI

This should be an asynchronous native state machine, not a thread-blocking call from C++ into JavaScript. On conventional success, the socket remains in its existing certificate-authentication wait state while the request is dispatched to the privileged extension host. Each JavaScript onBeforeTlsAccept callback must return synchronously once invoked, but delivery and continuation across Firefox threads are naturally asynchronous. A short browser-enforced deadline fails closed.

Heavy work, network access, and interactive UI are forbidden in that hook. Extensions perform such work in the background and consult prepared local state when the hook arrives.

On conventional failure, the first slice need not retain the failed socket. Core freezes an investigation record, completes the failure normally, and correlates the resulting navigation with a protected investigation page. If the user creates a valid exception, Browsec stores the policy and starts a fresh navigation on a fresh connection.

Why the existing override service is only a bootstrap adapter

nsICertOverrideService already stores host, port, origin attributes, exact certificate, and session-or-persistent lifetime. PSM consults it only for errors Firefox categorizes as overridable and rejects overrides where policy such as HSTS forbids them. This is useful for the first exact-leaf prototype.

It cannot represent Browsec's complete policy model:

  • exact DER authority trust restricted to a namespace;
  • explicit distrust of otherwise accepted authorities;
  • arbitrary timed lifetime;
  • plugin identity and decision provenance;
  • journal references and the exact set of errors authorized by a verdict;
  • independent extension evidence storage; or
  • user-inspectable policy history.

Therefore the adapter must be named and documented as provisional. Browsec's durable policy overlay remains the source of truth even while an exact-leaf rule is mirrored into the Firefox service for the first demonstration.

Privileged extension host

The initial host should be a built-in Firefox WebExtension API namespace, not a legacy external Experiment loaded from an arbitrary path. Current Firefox implements parent-process extension APIs under toolkit/components/extensions/parent/, with schemas and explicit manifest permissions controlling exposure.

The Browsec API is available only to packages that pass all of these checks:

  1. Browsec security-extension package type;
  2. verified package identity;
  3. requested capability in the package manifest;
  4. local capability grant;
  5. permitted execution mode for the current profile/context; and
  6. API-version compatibility.

The built-in Velvet Hammer package is compiled into the product, always enabled, and always ordered last. Its irremovability is enforced by the host, not by an ordinary extension preference.

First vertical slice

The spike is complete when an automated browser test demonstrates:

  1. Navigate a clean Browsec profile to a local HTTPS server with an unknown self-signed certificate.
  2. Capture original DER, host, port, origin attributes, Firefox error, and both available chain representations.
  3. Display them in a browser-owned Velvet Hammer investigation page.
  4. Obtain an explicit exact-certificate, exact-host, exact-port, session-scoped user decision.
  5. Validate and append the verdict to the audit journal.
  6. Persist the Browsec policy rule and provisionally mirror it through nsICertOverrideService.
  7. Abandon the failed connection and start a fresh navigation.
  8. Load the site and display a persistent Browsec-controlled indication that conventional validation failed and named local policy permitted it.
  9. Revoke the rule and prove that another fresh navigation fails again.

No page content, cookie, credential, authorization header, private key, or TLS session secret is included in the investigation record.

Patch sequence

Patch 1: built-in UI and exact-leaf prototype

  • add the built-in Velvet Hammer package and protected about: surface;
  • expose immutable failed-connection facts through a minimal internal API;
  • implement the browser-owned Record security concern chrome action;
  • execute the existing TrustLab failure chain;
  • use the provisional exact-leaf override adapter; and
  • add browser and xpcshell tests.

This patch can begin in artifact-build mode if it changes only packaged JavaScript, HTML, CSS, and existing scriptable interfaces.

Patch 2: native pre-accept continuation

  • add the BrowsecTrustHost continuation before SetCertVerificationResult(0);
  • bind every observation and verdict to a unique connection attempt;
  • enforce deadline, cancellation, and fail-closed behavior;
  • ensure that plugin failure never becomes implicit trust; and
  • test ordinary TLS and HTTP/3 paths.

This requires a full compiled Firefox build.

Patch 3: native Browsec policy overlay

  • stop depending on Firefox's override store as the source of truth;
  • enforce certificate and authority rules with precise error substitution;
  • implement explicit distrust on conventional-success paths;
  • add timed and persistent policy lifetimes; and
  • make policy snapshots auditable and atomically replaceable.

Patch 4: network-path audit

Cover top-level navigation, redirects, subresources, WebSocket, workers, service workers, speculative connections, proxy CONNECT, TLS resumption, HTTP/2 and HTTP/3 coalescing, private browsing, containers, and client certificate interactions. A path is not considered covered merely because it eventually calls a shared verifier; its connection reuse and enforcement behavior must be tested.

Immediate implementation questions

The spike should resolve these from code and tests rather than policy intuition:

  • which identifier safely binds the UI investigation to one network attempt;
  • where a fresh-retry flag can prohibit reuse and coalescing;
  • how much constructed-chain material remains available for every failure;
  • which process owns the immutable investigation registry;
  • whether the first built-in API can remain JS-only or requires new WebIDL;
  • how the protected page proves to the user that it is browser chrome; and
  • which Firefox error classes can safely use the provisional override adapter.

Maintenance rule

Every Firefox rebase must rerun the integration tests and re-audit the call path from VerifySSLServerCert through SetCertVerificationResult, including the HTTP/3 entry point. Silent upstream movement of this boundary is a release blocker, not an implementation detail.