Guide
eMudhra CA compromise: check whether your servers still trust the bad certificates
By Aman Singh · Published · 9 min read
In October 2026 the private keys of eight eMudhra intermediate certificate authorities turned out to be recoverable from their public certificates. Anyone could have used them to issue a trusted HTTPS certificate for any website. Firefox, Chrome, Apple and Microsoft blocked the intermediates within days, so browsers are protected.
Servers are a different story. A Node.js API calling Stripe, a Python script, a container built in September: each trusts whatever certificate list it was built with. This guide shows which runtimes still trust the bad certificates, with results I measured on 24 October 2026, and gives a short script that checks your own.
Updating Node.js for this? The same releases fix a Brotli flaw: Node2Shell check and patch →
What happened
| Details | |
|---|---|
| What | Private keys of eight eMudhra intermediate CA certificates were compromised; six are TLS issuing CAs |
| Why | The keys were trivially weak: researchers describe values small enough to recover from the public certificate |
| Since | The certificates were created in April 2025, so the keys could have been recovered from then on |
| Reported | 17 October 2026, to the browser root programs |
| Blocked | Firefox and Chrome 17 October; Microsoft 20 October; Apple 19 to 22 October (sources differ) |
| Exploitation | No malicious use confirmed as of 21 October |
Mozilla's summary is blunt: the keys "could allow attackers to issue fraudulent HTTPS certificates for websites they do not control." eMudhra, an Indian certificate authority, has since announced that it is leaving the global SSL/TLS certificate business.
Who is still at risk
Using a fake certificate needs a position on the network path: a compromised proxy or router, public Wi-Fi, a hijacked DNS answer. Against a browser that no longer works. Against software with an old trust store it still does. That means:
- Backend services making outbound HTTPS calls to payment providers, APIs and webhooks.
- Containers built before the fix. Patching the host does not change images that are already built.
- Scripts, CI runners and internal tools on older Node.js, Python, Go or Java.
- Libraries with their own bundled certificate file, which ignore the system store entirely.
What I found when testing
The researchers run test servers whose certificates chain through the compromised intermediates: two logged in Certificate Transparency and two that are not. A safe client refuses all four. Results on Windows 11, 24 October 2026:
| Client | Compromised chains trusted |
|---|---|
| Node.js 24.22.0 and 26.12.0 (before the fix) | All 4 |
| Node.js 24.22.0 with --use-system-ca | All 4 |
| Node.js 20.20.2 (end of life, no fix) | 2 of 4 |
| Node.js 24.22.1 and 26.12.1 | None (CERT_REVOKED) |
| Python with certifi 2026.07.22 | 1 of the 2 tested |
| Python with the Windows store | 1 of the 2 tested |
| Python with certifi 2026.10.23 | None |
| curl on Windows (Schannel) | None (CRYPT_E_REVOKED) |
- Node.js does not check Certificate Transparency, so the "not in CT" chains were trusted just like the logged ones. Browsers that enforce CT get an extra layer of protection Node.js does not have.
- The Node.js fix is a blocklist. 26.12.1, 24.22.1 and 22.23.4 update the root certificates and add a built-in list of blocked keys (pull request #66871), which is why the patched builds report
CERT_REVOKED. - The operating system store did not save unpatched Node.js. Windows itself refused the chains as revoked, but Node.js 24.22.0 with
--use-system-catrusted all four. Do not rely on the system store as a workaround; update Node.js.
Check your own runtime
This script connects to the four test servers with the runtime's own trust store, after checking that HTTPS works at all, so a network failure is never mistaken for a pass. Run it with the same node your app uses, ideally inside the container or on the server.
#!/usr/bin/env node
// emudhra-check.mjs: does this Node.js still trust the eight compromised
// eMudhra intermediate CAs (October 2026)? It connects to the disclosure
// site's test hosts, whose certificates chain through those intermediates,
// using this Node.js trust store. A patched runtime must refuse all of them.
// node emudhra-check.mjs # Node's bundled CA store
// node --use-system-ca emudhra-check.mjs # the operating system's store
// Exit code: 0 = all refused, 1 = at least one trusted, 2 = could not test.
import https from "node:https";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const TEST_HOSTS = [
{ host: "dv-g3b-emudhra.we-have-a-problem.com", what: "compromised intermediate, logged in CT" },
{ host: "dv-g3b-aaa-emudhra.we-have-a-problem.com", what: "same, via the AAA Certificate Services cross-sign" },
{ host: "dv-g3b-noct-emudhra.we-have-a-problem.com", what: "compromised intermediate, not in CT" },
{ host: "dv-g3b-aaa-noct-emudhra.we-have-a-problem.com", what: "cross-signed, not in CT" },
];
const CONTROL = "www.google.com";
/** Connects with the default trust store; resolves to { ok, code }. Never rejects. */
export function probe(host, timeoutMs = 10_000) {
return new Promise((resolve) => {
const req = https.get({ host, path: "/", timeout: timeoutMs, agent: false }, (res) => {
res.resume();
resolve({ ok: true, code: `HTTP ${res.statusCode}` });
});
req.on("timeout", () => req.destroy(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" })));
req.on("error", (err) => resolve({ ok: false, code: err.code ?? err.message }));
});
}
const TLS_REJECTIONS = new Set([
"CERT_REVOKED",
"UNABLE_TO_GET_ISSUER_CERT",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"SELF_SIGNED_CERT_IN_CHAIN",
"CERT_UNTRUSTED",
"CERT_HAS_EXPIRED",
]);
/** Classifies one probe: trusted (bad), refused (good) or unknown (network problem). */
export function classify(result) {
if (result.ok) return "TRUSTED";
if (TLS_REJECTIONS.has(result.code) || /CERT|ISSUER|SIGNATURE/.test(result.code)) return "refused";
return "unknown";
}
export function verdict(classes) {
if (classes.includes("TRUSTED")) return 1;
if (classes.includes("unknown")) return 2;
return 0;
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
const systemCa = process.execArgv.includes("--use-system-ca");
console.log("eMudhra intermediate CA check (October 2026)");
console.log(`node: ${process.version} (${systemCa ? "operating system CA store" : "bundled CA store"})`);
const control = await probe(CONTROL);
console.log(`control ${CONTROL}: ${control.ok ? "reachable" : `FAILED (${control.code})`}`);
if (!control.ok) {
console.log("No working HTTPS connection, so the results below would mean nothing.");
process.exit(2);
}
const classes = [];
for (const t of TEST_HOSTS) {
const r = await probe(t.host);
const c = classify(r);
classes.push(c);
console.log(`${c.padEnd(8)} ${r.code.padEnd(34)} ${t.host} (${t.what})`);
}
const v = verdict(classes);
console.log("");
console.log(
v === 0
? "OK: this runtime refuses the compromised intermediates."
: v === 1
? "ACTION NEEDED: this runtime trusts at least one compromised chain. Update Node.js (26.12.1, 24.22.1, 22.23.4) and the system ca-certificates."
: "Could not decide: some test hosts failed for network reasons. Retry, or open them in a browser.",
);
process.exit(v);
}node emudhra-check.mjs # Node's bundled store
node --use-system-ca emudhra-check.mjs # the operating system's store
echo "exit code: $?" # 0 refused, 1 trusted, 2 network problemeMudhra intermediate CA check (October 2026)
node: v24.22.1 (bundled CA store)
control www.google.com: reachable
refused CERT_REVOKED dv-g3b-emudhra.we-have-a-problem.com
refused CERT_REVOKED dv-g3b-aaa-emudhra.we-have-a-problem.com
refused CERT_REVOKED dv-g3b-noct-emudhra.we-have-a-problem.com
refused CERT_REVOKED dv-g3b-aaa-noct-emudhra.we-have-a-problem.com
OK: this runtime refuses the compromised intermediates.node: v24.22.0 (bundled CA store)
TRUSTED HTTP 200 dv-g3b-emudhra.we-have-a-problem.com
TRUSTED HTTP 200 dv-g3b-aaa-emudhra.we-have-a-problem.com
TRUSTED HTTP 200 dv-g3b-noct-emudhra.we-have-a-problem.com
TRUSTED HTTP 200 dv-g3b-aaa-noct-emudhra.we-have-a-problem.com
ACTION NEEDED: this runtime trusts at least one compromised chain.Show the tests (emudhra-check.test.mjs, 5 passing)
// node --test emudhra-check.test.mjs (no network needed)
import { test } from "node:test";
import assert from "node:assert/strict";
import { classify, verdict, TEST_HOSTS } from "./emudhra-check.mjs";
test("classify: a completed request means the chain was trusted", () => {
assert.equal(classify({ ok: true, code: "HTTP 200" }), "TRUSTED");
});
test("classify: certificate errors count as refused", () => {
for (const code of ["CERT_REVOKED", "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "SELF_SIGNED_CERT_IN_CHAIN", "ERR_TLS_CERT_ALTNAME_INVALID"]) {
assert.equal(classify({ ok: false, code }), "refused", code);
}
});
test("classify: network failures are unknown, not a pass", () => {
for (const code of ["ENOTFOUND", "ECONNRESET", "ETIMEDOUT", "ECONNREFUSED"]) {
assert.equal(classify({ ok: false, code }), "unknown", code);
}
});
test("verdict: any trusted host fails; network gaps are inconclusive", () => {
assert.equal(verdict(["refused", "refused", "refused", "refused"]), 0);
assert.equal(verdict(["refused", "TRUSTED", "refused", "refused"]), 1);
assert.equal(verdict(["refused", "unknown", "refused", "refused"]), 2);
assert.equal(verdict(["TRUSTED", "unknown"]), 1);
});
test("tests both the CT-logged and the non-CT chains", () => {
assert.equal(TEST_HOSTS.length, 4);
assert.equal(TEST_HOSTS.filter((t) => t.host.includes("noct")).length, 2);
});For anything else, the same test servers work with curl or a one-line Python check:
# Any tool that uses your trust store: a certificate error is the good result
curl https://dv-g3b-emudhra.we-have-a-problem.com/
curl https://dv-g3b-aaa-noct-emudhra.we-have-a-problem.com/
# Python: which certifi does this environment use?
python -c "import certifi; print(certifi.__version__)" # want 2026.10.19 or laterUpdate your trust stores
# Debian / Ubuntu
sudo apt-get update && sudo apt-get install --only-upgrade ca-certificates
# Alpine (also in Dockerfiles)
apk update && apk upgrade ca-certificates ca-certificates-bundle
# Python
pip install -U "certifi>=2026.10.19"
# Node.js: 26.12.1, 24.22.1 or 22.23.4 (see the Node.js guide), then restart| Component | Fixed in |
|---|---|
| Node.js | 26.12.1, 24.22.1, 22.23.4 |
| Python certifi | 2026.10.19 |
| Go (CVE-2026-111943) | 1.27.3, 1.26.10 |
| NSS / Firefox | NSS 3.131.1; Firefox 158.0.1, ESR 153.5.1, ESR 115.43.1 |
| Ubuntu ca-certificates | 20260816ubuntu0.22.04.1, 0.24.04.1, 0.26.04.1 |
| Alpine ca-certificates | 20261020-r0 |
| curl CA bundle | 2026-10-20 |
| Electron | 44.9.1, 43.8.1, 42.11.16 |
| Java (JDK cacerts) | Not confirmed: check your JDK vendor |
Versions beyond Node.js and certifi are from Endor Labs' tracking list; confirm with each vendor. Then:
- 1Rebuild and redeploy every container from patched base images, and restart long-running processes.
- 2Look for bundled certificate files (
.pembundles inside dependencies) that ignore the system store, and point those libraries at an up-to-date store instead. - 3Run the checker again where your code actually runs, not only on your laptop.
If your own site uses an eMudhra certificate
Certificates issued by the eight intermediates are now blocked in browsers, so a site still serving one will show security errors. Check which authority issued yours:
# Which CA issued the certificate your site serves?
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -enddateIf the issuer mentions emSign or eMudhra, compare it with the list of affected intermediates on the disclosure site, and replace the certificate with one from another certificate authority. The free launch readiness check also confirms that your site loads over HTTPS without errors.
Common questions
What is the eMudhra CA compromise?+
Am I affected if I only use a browser?+
How do I check if my Node.js app trusts the compromised certificates?+
Does updating ca-certificates fix Node.js?+
Was the eMudhra compromise exploited?+
Want your servers checked?
I update Node.js and Python runtimes, base images and certificate stores, and verify the result where the code runs. See Node.js, Python and Next.js bug fixing.
Sources
Next steps
Keep reading
Need a hand with this?
Tell me what you are working on. Free scoping call, fixed quote within 48 hours.
Book a call