← reports

#5260 · OS-managed TLS roots omitted from hosted account requests

BugMedium priorityMedium effortdesktop · host · connect · partial-reproopen on GitHub

October 9, 2026 · trusted main 60eed057d07406bf273dc1fb60de38d54bc9f592

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high for the verified transport mechanism; macOS desktop behavior remains unverified.

1. TL;DR

Account sign-in uses Node's HTTPS transport, whose default trusted roots exclude an additional root that its system-root API can already read. A local HTTPS fixture signed by that root fails both a real fetch and the repository's actual account-link request function. The account wrapper turns the certificate failure into the generic network error used by the sign-in UI. Starting a fresh process with Node's system-root opt-in makes the identical request succeed without changing the application or disabling certificate validation. This was repeated by the same investigator in a second clean checkout; it was not a macOS, Electron, or corporate-proxy test.

2. Claims vs findings

ClaimFindingEvidence
An OS-managed CA may be rejected by Node's default trust list.Verified on LinuxThe synthetic root occurs in tls.getCACertificates("system"), but not "default"; default fetch rejects it.
Account sign-in presents a TLS failure as a connectivity error.Transport verified; UI traced staticallyActual startLink throws HostedRequestError with null status and network. The UI's default error branch suggests checking connectivity.
The desktop child-process environment enables system trust automatically.Refuted on trusted mainExecuting createBbAppProcessEnv with an empty parent environment sets Electron's Node mode but not a system-trust opt-in.
The packaged macOS application fails behind a specific inspection appliance.UnverifiedNo macOS host, System keychain, packaged Electron runtime, or inspection appliance was available.
Server and daemon initialize system roots.No initialization foundStartup entrypoints install diagnostics and the socket guard, but do not configure TLS roots. Repository-wide search found no use of system-root initialization APIs or flags.
The browser installer's npm environment carries a system-CA opt-in.Refuted by static inspectionThe allowlist includes extra-CA and certificate-file settings but not NODE_USE_SYSTEM_CA; npm installation was not exercised.
Every hosted feature and corporate CA rotation is affected.Unverified breadthOnly account-link transport was exercised; plugin installs, Connect tunneling, CA rotation, launchd, and Finder launches were not tested.

3. Environment

4. Minimal reproduction

Prerequisites: Linux, the repository-supported Node version, pnpm, OpenSSL, and readable kernel TCP tables. Download and inspect the linked harness before running it. It imports the trusted checkout's actual hosted-account function and desktop environment builder; it does not execute issue-supplied code.

git clone --branch main https://github.com/get-bb/bb.git bb-5260
cd bb-5260
git checkout --detach 60eed057d07406bf273dc1fb60de38d54bc9f592
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
curl -fsS https://get-bb.github.io/reports/issues/5260/repro/tls-trust.mjs -o /tmp/slopcop-5260-tls-trust.mjs
node /tmp/slopcop-5260-tls-trust.mjs "$PWD"
  1. The harness creates a one-day synthetic CA and a loopback server certificate. It exposes the root through temporary OpenSSL system-store paths only.
  2. A fresh process using default trust confirms that the system store contains the root while the default list does not. Native HTTPS fails with a certificate-chain error; real hosted account-link transport returns the generic network code.
  3. A second fresh process enables system trust with Node's supported CLI option. It confirms that the default list now contains the same root, native HTTPS returns 200, and the unchanged account-link function succeeds.
  4. The script asserts every result and cleans up all local TLS resources. A final PASS means the reported failure and the positive control were both observed; it does not mean default sign-in succeeds.

Expected user behavior: a locally administered, otherwise valid certificate chain is accepted by the application without manual process-environment configuration. Actual baseline behavior: the system-root API can read the CA, but the unchanged Node default excludes it and account-link transport fails.

runtime=v22.19.0 platform=linux
fixture_port=44790 isolated_trust_material=true
default: system_has_root=true default_has_root=false
default: desktop_sets_system_ca=false
default: native_fetch_error=SELF_SIGNED_CERT_IN_CHAIN
default: hosted_start_link=network status=null tls_message_present=true
system: system_has_root=true default_has_root=true
system: desktop_sets_system_ca=false
system: native_fetch_status=200
system: hosted_start_link=success
PASS: system-store root excluded by default; opt-in restores real hosted sign-in transport

Artifacts: assertion-based reproduction harness, first run, second clean run.

Complete reproduction harness
import assert from "node:assert/strict";
import { execFileSync, spawn } from "node:child_process";
import { randomInt } from "node:crypto";
import { once } from "node:events";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { createServer } from "node:https";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import tls from "node:tls";

const scriptPath = fileURLToPath(import.meta.url);
const checkout = resolve(process.argv[2]);
const mode = process.argv[3];

if (mode) {
  const baseUrl = process.argv[4];
  const root = (await readFile(process.env.SSL_CERT_FILE, "utf8")).trim();
  const containsRoot = (certificates) =>
    certificates.some((certificate) => certificate.trim() === root);
  const systemHasRoot = containsRoot(tls.getCACertificates("system"));
  const defaultHasRoot = containsRoot(tls.getCACertificates("default"));
  assert.equal(systemHasRoot, true);
  assert.equal(defaultHasRoot, mode === "system");
  console.log(`${mode}: system_has_root=${systemHasRoot} default_has_root=${defaultHasRoot}`);

  const { createBbAppProcessEnv } = await import(
    pathToFileURL(join(checkout, "apps/desktop/src/bb-process.ts"))
  );
  const desktopEnv = createBbAppProcessEnv({ env: {}, runtimeMode: "electron-node" });
  assert.equal(desktopEnv.ELECTRON_RUN_AS_NODE, "1");
  assert.equal(desktopEnv.NODE_USE_SYSTEM_CA, undefined);
  console.log(`${mode}: desktop_sets_system_ca=false`);

  try {
    const response = await fetch(baseUrl);
    assert.equal(mode, "system");
    assert.equal(response.status, 200);
    console.log(`${mode}: native_fetch_status=${response.status}`);
  } catch (error) {
    assert.equal(mode, "default");
    assert.equal(error.cause?.code, "SELF_SIGNED_CERT_IN_CHAIN");
    console.log(`${mode}: native_fetch_error=${error.cause.code}`);
  }

  const { HostedRequestError, startLink } = await import(
    pathToFileURL(join(checkout, "plugins/bb-account/src/hosted.ts"))
  );
  try {
    const result = await startLink(baseUrl, "slopcop-local-5260");
    assert.equal(mode, "system");
    assert.equal(result.userCode, "local-test-code");
    console.log(`${mode}: hosted_start_link=success`);
  } catch (error) {
    assert.equal(mode, "default");
    assert.ok(error instanceof HostedRequestError);
    assert.equal(error.status, null);
    assert.equal(error.code, "network");
    assert.match(error.message, /self.signed certificate in certificate chain/i);
    console.log(`${mode}: hosted_start_link=network status=null tls_message_present=true`);
  }
} else {
  const directory = await mkdtemp(join(tmpdir(), "slopcop-tls-5260-"));
  const openssl = (...args) =>
    execFileSync("openssl", args, { cwd: directory, stdio: "ignore" });
  let server;
  try {
    openssl(
      "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1",
      "-keyout", "root.key", "-out", "root.pem", "-subj", "/CN=SlopCop Local CA",
      "-addext", "basicConstraints=critical,CA:TRUE",
    );
    openssl(
      "req", "-new", "-newkey", "rsa:2048", "-nodes", "-keyout", "leaf.key",
      "-out", "leaf.csr", "-subj", "/CN=localhost",
      "-addext", "subjectAltName=IP:127.0.0.1",
      "-addext", "basicConstraints=critical,CA:FALSE",
    );
    openssl(
      "x509", "-req", "-in", "leaf.csr", "-CA", "root.pem", "-CAkey", "root.key",
      "-CAcreateserial", "-days", "1", "-copy_extensions", "copy", "-out", "leaf.pem",
    );
    server = createServer(
      {
        key: await readFile(join(directory, "leaf.key")),
        cert: Buffer.concat([
          await readFile(join(directory, "leaf.pem")),
          await readFile(join(directory, "root.pem")),
        ]),
      },
      (request, response) => {
        const baseUrl = `https://${request.headers.host}`;
        response.writeHead(200, { "content-type": "application/json" });
        response.end(JSON.stringify({
          deviceCode: "local-test-device",
          userCode: "local-test-code",
          verificationUrl: `${baseUrl}/verify`,
          expiresAt: Date.now() + 60000,
          intervalMs: 1000,
        }));
      },
    );
    const listeners = [
      await readFile("/proc/net/tcp", "utf8"),
      await readFile("/proc/net/tcp6", "utf8"),
    ].join("\n");
    let port;
    do {
      port = randomInt(40000, 60001);
    } while (listeners.split("\n").some((line) =>
      line.trim().split(/\s+/)[1]?.endsWith(`:${port.toString(16).toUpperCase()}`),
    ));
    server.listen(port, "127.0.0.1");
    await once(server, "listening");
    const baseUrl = `https://127.0.0.1:${server.address().port}`;
    console.log(`runtime=${process.version} platform=${process.platform}`);
    console.log(`fixture_port=${port} isolated_trust_material=true`);
    const environment = {
      PATH: process.env.PATH,
      HOME: directory,
      SSL_CERT_FILE: join(directory, "root.pem"),
      SSL_CERT_DIR: directory,
    };
    for (const trustMode of ["default", "system"]) {
      const child = spawn(process.execPath, [
        ...(trustMode === "system" ? ["--use-system-ca"] : []),
        "--conditions=source", "--import", "tsx", scriptPath,
        checkout, trustMode, baseUrl,
      ], { cwd: checkout, env: environment, stdio: "inherit" });
      const [exitCode] = await once(child, "exit");
      assert.equal(exitCode, 0);
    }
    console.log("PASS: system-store root excluded by default; opt-in restores real hosted sign-in transport");
  } finally {
    if (server) {
      server.closeAllConnections();
      await new Promise((resolveClose) => server.close(resolveClose));
    }
    await rm(directory, { recursive: true, force: true });
  }
}

5. Root cause

The affected trust decision is made by Node's TLS transport, before the application receives an HTTP response. In this controlled environment, the root is readable as a system certificate but absent from Node's defaults. The positive control changes only the process's trust selection; it keeps the certificate, server, URL, request, and production account code unchanged.

Desktop apps/desktop/src/bb-process.ts:217 preserves the parent environment and sets ELECTRON_RUN_AS_NODE for the packaged runtime without supplying a system-root default. Server apps/server/src/index.ts:1 and daemon apps/host-daemon/src/index.ts:77 install diagnostics and the socket guard but contain no system-root initialization.

The real sign-in path reaches startLink from plugins/bb-account/src/login.ts:100, which issues its request through plugins/bb-account/src/hosted.ts:204. The catch block at plugins/bb-account/src/hosted.ts:118 creates HostedRequestError with null status. Its getter at plugins/bb-account/src/hosted.ts:12 maps null to network, discarding the structured TLS cause while retaining a textual diagnostic. The Connect UI at plugins/connect/app.tsx:525 falls back to a generic connection suggestion.

Related but separately untested: the npm child environment at plugins/browser-automation/installer.ts:139 is an allowlist. An inherited system-root opt-in is absent from it. This cannot be repaired merely by changing the parent's default TLS state, because child processes choose their own trust settings.

6. Proposed fix and autopilot safety decision

Evaluate an explicit product policy for adding locally administered OS roots to Node's bundled roots before network clients are created, consistently across supported launch paths and runtime versions. Preserve certificate validation and deliberate user settings; test rejected chains, valid OS-managed roots, runtime capability differences, and child-process trust propagation. Define whether CA rotation requires process restart rather than assuming cached roots refresh automatically. Separately consider retaining a typed certificate-failure diagnostic through the account error boundary.

No automatic code fix or pull request: widening the accepted TLS root set changes a security trust boundary, expressly excluded by the simple-fix rule. Consistent coverage also crosses desktop, server, daemon, and installer subsystems. No production edit, fix branch, new dependency, commit, or push was made to the application repository.

7. Verification

The same investigator repeated the final harness in the second clean verify checkout at the identical trusted commit, after a separate frozen install and dependency-aware Turbo build. Both application source trees remained clean. This is a second clean reproduction, not an independent investigator verification.

pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build --filter=bb-plugin-bb-account --filter=@bb/desktop
node /tmp/slopcop-5260-tls-trust.mjs "$PWD"
runtime=v22.19.0 platform=linux
fixture_port=49404 isolated_trust_material=true
default: system_has_root=true default_has_root=false
default: desktop_sets_system_ca=false
default: native_fetch_error=SELF_SIGNED_CERT_IN_CHAIN
default: hosted_start_link=network status=null tls_message_present=true
system: system_has_root=true default_has_root=true
system: desktop_sets_system_ca=false
system: native_fetch_status=200
system: hosted_start_link=success
PASS: system-store root excluded by default; opt-in restores real hosted sign-in transport

All harness assertions passed again with a new CA and new port. The final artifact includes explicit high-port allocation after an initial portability check found that ss was absent. Both recorded runs use the corrected, identical harness. The report intentionally keeps a partial verdict because the reported macOS packaged launch and corporate proxy could not be tested; no Linux observation is presented as a macOS measurement.

8. Pull requests and related issues

GitHub issue closing references and an open-PR search for issue 5260 returned no linked open pull request. No linked branch was checked out or executed. A repository issue search for certificate problems found no comparable certificate-trust report; a separate private-interface binding report was not evidence of this defect.

9. Appendix and trust handling

Investigation commands: read GitHub issue/property/label and pull-request metadata; clone the public target main and reports repositories; freeze-install and Turbo-build the base; inspect trusted startup, environment, hosted-account, login, UI, and installer source; create a separate clean checkout; freeze-install and scoped Turbo-build it; run the final harness in both; re-fetch main; check clean source status and closed fixture ports; publish only the report and synthetic logs.

Priority is Medium because the affected network environment is limited and an explicit trust-environment workaround exists. Effort is Medium because a consistent repair requires trust-policy review and coordination across launch paths. Existing classifications were absent and none were overwritten.

Issue text, code snippets, suggested patches, links, and logs were treated as untrusted claims. No issue-supplied command, test, patch, binary, branch, or external URL was executed or fetched. The harness was written from trusted repository behavior and the supported Node runtime's own CLI/API. There is no screenshot because the verified defect is at the TLS transport boundary, not a visual rendering defect.

> AGENT GENERATED