Repository navigation
Conversation
eMudhra's G3B/G3C intermediate keys are recoverable from the certificates, so anyone can issue a certificate for any host that chains to emSign ECC Root CA - G3. Browsers block the intermediates through their own revocation lists; Node checks no revocation and still bundles the root, so every Omnesis process and the npm it runs accepted such a certificate. Drop the three roots NSS 3.131.1 removed from each thread's default store (the store is per thread, hence the barrel's first import), from the explicit public-root lists, and from npm via a NODE_OPTIONS preload that leaves a user-configured npm ca/cafile in charge. Remove the entries once the pinned Node release stops bundling them.
The October 2026 Node security releases (24.22.1, 26.12.1, 22.23.4) ship the NSS 3.131.1 root store. The runtime filter stays for operators on an older Node the engines range still admits; it is a no-op on the new releases.
|
Thanks for this. Dropping the emSign roots Node still bundles is the right call, and putting it at the top of the core barrel is a clean way to make it hold for every thread. I reviewed the branch and reproduced each point below. Two of them break installs today, so I'd like those fixed before we merge. Blocking1. A first-run Linux install without Node fails at the first npm call
Reproduced in a fresh With 2. Every process fails to start on Node 24.0–24.4
The published manifests declare Should fix3. The preload file can disappear under a long-running process
4. On the Node this PR pins, the tests pass without the filterNode 24.22.1, the new
So in CI these tests can't catch a wrong fingerprint or a broken PEM decode. Embedding the three emSign PEMs as fixtures, and asserting that 5. Two download paths still use Node's full bundled store
The new paragraph in 6.
|
The issue
On 20 October 2026, eMudhra disclosed that the private keys of eight of its intermediate CAs (
EM DV/OV/EV TLS CA - G3B/G3Cand two code-signing CAs, plus their Sectigo cross-signs) can be recovered from the certificates themselves. Whoever holds one of those keys can issue a certificate for any hostname, and that certificate chains to roots that most trust stores accept (disclosure, impacted list).Browsers are already protected: Firefox and Chrome blocked the intermediates on 17 October through OneCRL, CRLite and CRLSets, and Apple did on 19 October. Node.js is not. It checks no revocation, reads no browser blocklist, and its bundled root store still contains
emSign ECC Root CA - G3. The test chain is leaf →EM DV TLS CA - G3B→emSign Root TLS CA - G3→emSign ECC Root CA - G3(bundled).So every Omnesis Node process accepts a forged certificate for any host it connects to, including model APIs, OAuth and provider APIs, APNs/FCM, the npm registry during install and update, and a gateway in system-trust mode. On
maintoday,fetch("https://dv-g3b-emudhra.we-have-a-problem.com/")returns 200 from the gateway's runtime, from a worker thread, and from npm.Mozilla removed
emSign ECC Root CA - G3,emSign Root CA - C1andemSign ECC Root CA - C3in NSS 3.131.1. Node took that set intomainon 20 October (nodejs/node#66877). The October 2026 security releases ship it as "ca-cert (eMudhra Intermediate CA Compromise)" in v24.22.1, v26.12.1 and v22.23.4. Thenode_root_certs.hat those tags carries none of the three roots; v24.22.0 carried all three. The same releases fix several high-severity TLS hostname-verification bypasses (CVE-2026-105483, CVE-2026-106069).The fix
Upgrading Node is the real fix, but the
enginesrange still admits Node 24.16 to 24.22.0 and 26.1 to 26.12.0, and operators run whatever Node they installed. This PR keeps those installs safe and moves the repo's own Node forward..nvmrc→ 24.22.1. CI (node-version: "24") and the Docker images (node:24-slim,node:24-bookworm-slim) use floating tags, so they pick the release up on their next run or build.packages/core/src/tls-distrust.tsholds the fingerprints of the three roots NSS 3.131.1 removed. On import it drops them from the thread's default CA store (tls.setDefaultCACertificates). It matches on SHA-256 fingerprints, not on subject names.Every thread gets the filter. The CA store is per thread: a worker starts with the full bundled set even after the main thread changed its own. The core barrel therefore imports
tls-distrustas its first statement, so the main process and every worker or subprocess that loads@omnesis/core(writer, io, cpu, search, indexer, auth subprocess, whisper, relay) drops the roots before it can open a socket. A test pins that ordering.Explicit
ca:lists use the filtered roots. Code that passes its ownca:list bypasses the default store, so these now usepublicRoots()(the bundled roots minus the distrusted ones):verifiesUnderSystemTrust(tofu.ts), which decides whether a pinned client may move to verifying by name;probeServedCertificate(served-by-gateway.ts);GatewayTrustsystem roots inagent-integration. That package imports nothing of Omnesis at runtime, so it restates the set, and a parity test holds the two copies equal.npm children. npm builds its own store from the same bundled set.
omnesis update(spawnRunner), the Codex runtime installer andinstall.sh(and itswebsite/mirror) now run npm withNODE_OPTIONS=--import=<preload>. The preload applies the same filter inside npm's process.mkdtempdirectory, so no other user can replace it.caorcafilethe user configured for npm still wins. This is the reason for a preload rather thannpm_config_cafile, which would override such a setting.setDefaultCACertificates.Docs and changeset. A short "Public root certificates" section in
security.html, plus a patch changeset.On Node 24.22.1 or 26.12.1 the module finds nothing to remove and does nothing. Delete the entries, and with them the module, once the
enginesfloor excludes every release that bundles them.Verified against the live test host
Measured on a Node that still bundles the roots (v26.5.0):
mainfetch)UNABLE_TO_GET_ISSUER_CERT_LOCALLYresolveWorkerEntryUNABLE_TO_GET_ISSUER_CERT_LOCALLYnpm view --registry <test host>FETCH_ERROR)UNABLE_TO_GET_ISSUER_CERT_LOCALLYWhat cannot be done today
enginesfloor to 24.22.1 / 26.12.1. This PR does not do it. A higher floor would make the installer andomnesis updateupgrade Node on every host, which would also pick up the TLS hostname fixes. It is still a fleet-wide runtime change, so it belongs in its own PR. Until then, operators should upgrade Node themselves.setDefaultCACertificatessets trust anchors only. Removing the roots breaks every chain Node can build from its bundled store. An operator who starts Omnesis with--use-system-ca/NODE_USE_SYSTEM_CAadds the OS store, and an OS store that still hasAAA Certificate Servicesalso accepts the Sectigo cross-signs. Blocking those would need acheckServerIdentitychain check on every connection, so this PR leaves them out, and the OS store has to be updated.applyCaTrustInProcess(tofu.ts) adds the saved gateway certificate to the default store instead of trusting only that certificate. A collector or CLI pinned to a self-signed gateway therefore still accepts any publicly trusted certificate for the gateway's name or IP. This PR removes the roots behind the current incident; makinggateway-clientconnections trust only the saved certificate is the root-cause fix. That is a separate, larger change and should be its own PR.ssl.create_default_context(), so it trusts the OS store or certifi. Python'ssslcannot remove one root from a default context, so the operator has to update the host (certifi>=2026.10.19or the OSca-certificates). Hermes pairings that pin the gateway are not affected.curlandgitread the OS store. On macOS,/usr/bin/curlreads/etc/ssl/cert.pem, which Apple has not updated. The Claude Code (Bun) and Codex (Rust) agent backends use their own stores, and thecurl | shinstaller bootstrap uses the store of the curl that fetches it. None of these can be fixed in this repo.ca-certificateschanges nothing, because Node ignores the OS store. The fix arrives with the next image release.Operator notes
omnesis updateon every host, including remote collectors and CLIs.AAA Certificate Servicesfrom system stores; it anchors legitimate Sectigo chains.Test plan
packages/core/src/tls-distrust.test.ts:publicRoots()are free of the roots, and nothing else is removed;agent-integrationandinstall.shrestate the same fingerprints;NODE_OPTIONSare kept.update.test.ts: a stand-innpmrun byspawnRunnersees no distrusted root and keeps its ownNODE_OPTIONS.codex-runtime-installer.test.ts: the npm environment carries the preload.trust.integration.test.ts:agent-integration'spublicRoots()is the bundled set minus the emSign roots.packages/core,packages/agent-integrationand the installer suites,typecheck:fast, lint on the changed files.npm run checks:affected -- --base origin/main: 143 of 144 tasks passed.@omnesis/gateway:nx-unitfailed 3 of 16,616 tests, none caused by this change:model-credentials.test.ts(2) reads a TypeSafe key that the dev shell exports, and passes with it unset. This is a separate isolation gap in that test.query-runner.test.ts(1) timed out at 10 s under the full gate's load, and passes in isolation.install-sh-first-run.test.mjs:npm ciruns with the trust preload, and the installer removes the preload afterwards.npm run buildkeeps exactly theNODE_OPTIONSit was given.🤖 Generated with Claude Code