Files
b70684d797 Make --use-openssl-ca leave out the bundled root certificates (#46542)
### What does this PR do?

Makes `--use-openssl-ca` do what it says. The flag was parsed and then
ignored: `us_get_default_ca_store()` always added the bundled Mozilla
roots, so pointing `SSL_CERT_FILE` / `SSL_CERT_DIR` at a restricted set
of CAs could add trust but never remove any.

With the flag, the default store now holds only OpenSSL's default
certificate file and directory (`$SSL_CERT_FILE` else
`/etc/ssl/cert.pem`, `$SSL_CERT_DIR` else `/etc/ssl/certs`) and
`NODE_EXTRA_CA_CERTS`, as in Node
([`NewRootCertStore`](https://github.com/nodejs/node/blob/v26.3.0/src/crypto/crypto_context.cc#L1099)).
`tls.getCACertificates("default")` lists only the extra certificates
([`lib/tls.js`](https://github.com/nodejs/node/blob/v26.3.0/lib/tls.js#L157)).
`tls.rootCertificates` and `getCACertificates("bundled")` are unchanged.

| `--use-openssl-ca`, empty OpenSSL paths | Node 26.3.0 | Bun, before |
Bun, after |
| --- | --- | --- | --- |
| `tls.getCACertificates("default").length` | 0 | 118 | 0 |
| a server that presents a bundled root | not run | trusted |
`DEPTH_ZERO_SELF_SIGNED_CERT` |
| a server whose private CA is in `SSL_CERT_FILE` | authorized |
authorized | authorized |

#### The fix

- `bun_core::Global::CAStore { Bundled, System, OpenSSL }` replaces the
`Bun__Node__UseSystemCA` bool. It is one choice out of three, and
`root_certs.cpp` and `node:tls` both read it, so "OpenSSL and system"
cannot be represented.
- `root_certs.cpp`: `us_get_default_ca_store()` skips the bundled set
for `OpenSSL`. Every default store (sockets, `fetch`, `bun install`,
WebSocket, QUIC, `addCACert`) is built there. The loader of the default
certificate file drops certificates that the bundled set already holds;
with no bundled set in the store it keeps the whole file.
- `tls.ts`: `getCACertificates("default")` follows the same choice.

#### Every command, and the processes they spawn

A flag only reaches the process it is passed to, and several commands
leave their downloads to a second `bun`. They now pass on the flag they
were given (`Global::ca_store_flag()`). This applies to
`--use-system-ca` and `--use-bundled-ca` as well, which had the same
gaps.

| | before | now |
| --- | --- | --- |
| `bun test --parallel` workers | flag lost | forwarded |
| `bun install`: the security scanner process | flag lost | forwarded |
| `bunx` → `bun add` | skipped as "a flag of Bun's with no value" |
forwarded, and two stores are rejected even when nothing is installed |
| `bun create` → `bun add` / `bun install`, and its own GitHub download
| dropped | honoured and forwarded |
| `bun init` → `bun install` | dropped | forwarded |
| `bun upgrade`, `bun build` (macros, `--compile --target` download) |
accepted, ignored | honoured: the three flags moved from the runtime
parameters to the ones every command has |

#### Deliberate difference from Node

Node 26 exits when `NODE_USE_SYSTEM_CA=1` comes with `--use-openssl-ca`
([`node_options.cc`](https://github.com/nodejs/node/blob/v26.3.0/src/node_options.cc#L212)).
Here the flag wins, which is what `set_ca_store_from_flags` already
documented ("falls back to `NODE_USE_SYSTEM_CA` if none is [passed]").
Bun loads `.env` by itself and hands its values to child processes as
their real environment, so with an error the parent would start and its
test workers, security scanner and lifecycle scripts would exit. The
flag winning only ever narrows trust.

#### Not changed

Same as on main, checked with a 16-row matrix (4 flags x
`NODE_USE_SYSTEM_CA` unset/`0`/`1`/`true`, listing and a real
handshake): the 12 rows without `--use-openssl-ca` are identical.

- `--use-bundled-ca` is still Bun's default store, which also trusts
OpenSSL's default paths, and `NODE_USE_SYSTEM_CA=1` still adds the
system store to it (in Node too).
- `getCACertificates("default")` still counts a `NODE_USE_SYSTEM_CA=1`
that only `.env` sets, which the native store does not see.
- `--no-use-system-ca` is still ignored.
- `bun run --parallel` / `--sequential` with a script file (`./a.ts`)
starts `bun ./a.ts` with none of the runtime flags, so these three are
dropped there like `--smol` or `--preload`. Whether it should pass
runtime flags on is a question for all of them. A `package.json` script
or any other command can be anything and must not get the flag.

Same goal as #46302, written independently.

### How did you verify your code works?

New tests, each next to the code it covers. On the canary build of
`b6a5ecee5` (two commits before this branch's base) every one that
depends on the flag fails and the control rows pass.
`bun-upgrade.test.ts` does not load on a release build, so its new test
was checked with a mutation instead.

- `test/js/node/tls/test-use-system-ca.test.ts`: a local TLS 1.2 server
answers with ServerHello, Certificate, ServerHelloDone, which takes no
private key, so the certificate can be a real bundled root. A client
that trusts it fails on a later check, one that does not fails on the
chain. Covered: `fetch` and `tls.connect`; the flag in argv and in
`BUN_OPTIONS`; the same root given back through `SSL_CERT_FILE`, a
hashed `SSL_CERT_DIR` and `NODE_EXTRA_CA_CERTS`; the same server as the
registry of `bun install`, `bun x`, `bun create` (npm, local template,
`.tsx`) and `bun init` (blank, React); a macro of `bun build`; a request
that succeeds; the flag against `NODE_USE_SYSTEM_CA=1` in a real
handshake; and the lists of `getCACertificates`.
- `parallel.test.ts` (with `NODE_USE_SYSTEM_CA=1` in `.env`),
`bun-install-security-provider.test.ts`, `bunx.test.ts`,
`bun-upgrade.test.ts`.

Mutation checks on a debug build, one at a time: removing the
bundled-set gate, the file-loader gate, the nesting in `tls.ts`, the
handling in `bun upgrade`, or the forwarding in the scanner or `bunx`
each failed exactly the tests written for it. The two forwarding
mutations were run before `ca_store_flag()` was simplified to return the
flag that was passed; the tests were rerun after that, the mutations
were not.

The same `-e` scripts print the same output on Node 26.3.0 and on this
build for the `getCACertificates` lists and for a handshake with a
private CA, with and without it in `SSL_CERT_FILE`.

`bun run rust:check-all` passed for `x86_64-pc-windows-msvc` and
`aarch64-apple-darwin` partway through; the later edits are not
platform-gated. The tests were only run on Linux.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01EtFydK4WUe1Sg3hyHva2Pr)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-10-22 21:26:28 -07:00
..