One of eMudhra's certificate authority private keys is the number 4.
Not a number that starts with 4, or the number 4 followed by a few hundred random digits, just, 4... Another one is 56, and another is 115. I know, because I recovered all of them from their public certificates on my laptop just now, and it took less than a second.
What happened
On Saturday 17th October a security researcher reported to Apple, Google, Microsoft, Mozilla and eMudhra that the private keys for eight eMudhra intermediate certificates were compromised. Chrome pushed a CRLSet update blocking them that evening and Mozilla blocked them in Firefox via OneCRL a few hours later. On Tuesday 20th, Mozilla announced it publicly and shipped an NSS update removing the eMudhra root, and Chrome confirmed the root is going in Chrome Root Store v43.
The eight intermediates are six TLS issuing CAs (DV, OV and EV, two generations each) and two code-signing CAs, all created in April 2025. Every one of them was also cross-signed by Sectigo's AAA Certificate Services root in May 2025.
That's the "what", but it's the "how" that's the interesting part, and it was summed up in one line on the thread by Ian Carroll:
They are inherently weak single-byte keys that anyone can factor, and the issue cannot be contained.
What a private key actually is
These are elliptic-curve keys on the P-384 curve. The private key is just a number, d. The public key, the thing that goes in the certificate for the world to see, is d Γ G, where G is a fixed point on the curve that everyone uses and "Γ" means adding G to itself d times using the curve's special arithmetic.
Going forwards is easy, but going backwards, from the public key to d, is considered to be impractical. That's the entire security model, and it only holds if d is enormous. A P-384 private key should be a random 384-bit number, which gives you 2Β³βΈβ΄ possibilities (or roughly 10ΒΉΒΉβ΅), so nobody is ever going to search that.
eMudhra's keys look like this:
d = 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004
A P-384 private key should look like this:
d = 0x48ac06790886c80c48d31691fd4c0fe9ce3207f9807f2fe571d6e4ec426d78644298fc53df2b92566970e757ac998c34That's 47 bytes of zeros and then one byte, and all eight keys are like that. A single byte has 256 possible values, so the "search" is: compute 1 Γ G, 2 Γ G, 3 Γ G, and stop when it matches the certificate, which is at most 255 steps.
So the whole thing, every certificate those CAs could ever issue, every client that trusted them, rested on eight bits.
I checked
Mozilla attached the affected certificates to their announcement, so I pulled all 16 (8 keys, each used in two issued certificates: once by eMudhra and once by Sectigo) and brute-forced them. Every single one fell over instantly, the six TLS values, and the two code-signing keys are just as weak:
| Intermediate | Type | d |
|---|---|---|
| EM DV TLS CA - G3B | TLS | 156 |
| EM DV TLS CA - G3C | TLS | 230 |
| EM EV TLS CA - G3B | TLS | 115 |
| EM EV TLS CA - G3C | TLS | 56 |
| EM OV TLS CA - G3B | TLS | 155 |
| EM OV TLS CA - G3C | TLS | 4 |
| EM OV CS CA - G3B | Code signing | 66 |
| EM OV CS CA - G3C | Code signing | 237 |
It's interesting that nothing about these certificates looks wrong. A public key derived from d = 4 is a perfectly valid point on the curve, the encoding is fine, the curve is fine, every structural check passes. The only way to find out is to try.
Why "it cannot be contained"
Normally when a CA key is compromised, someone stole it! There's a thief, there's a moment it happened, and you can at least reason about who now has access to that key.
Here, nobody had to steal anything. The key was effectively public from the day the certificate existed. Anyone who looked at one of these certificates, at any point since April 2025, could have recovered the key in milliseconds on commodity hardware. That's a 17 month window and there is no way to know whether someone did.
Chrome made the point that because Chrome enforces Certificate Transparency, and no TLS certificates chaining to these CAs ever showed up in CT, no forged certificate could have been trusted by default in Chrome. That's true and it's reassuring for browsers. CT detects mis-issuance though, it doesn't prevent it, and a lot of software doesn't check CT at all.
Who's exposed today
Mainstream and updated browsers are likely absolutely fine. Chrome and Firefox block the intermediates directly and both require CT, and Safari requires CT too.
Everything else is a different story, though. Most non-browser TLS clients (so basically everything else, with default settings), things like curl, Python, Node, Go, Java, git, package managers and every script and daemon you've got, do three things that matter here: they trust a CA bundle on disk, they don't check revocation by default, and they don't check CT. So the fact that eMudhra and Sectigo have revoked these intermediates doesn't help them at all. This is something that I find often when talking about TLS and PKI, understanding just how massive the client ecosystem is can be overlooked. We often focus on this in training when we look at modern solutions for revocation like CRLite, which works wonders on the desktop browsers, but does nothing for any other TLS client. Consider what needs its root store updating now, and this is just the tip of the iceberg:
- curl | sh installers and self-updaters. Many tools install or update by downloading a script or binary over HTTPS with no signature check (rustup, nvm, Homebrew's installer, countless "install.sh" pages, IoT firmware updaters). A forged certificate here means running the attacker's code, often as root.
- CI/CD pipelines. GitHub/GitLab runners, Jenkins and build containers fetch dependencies and push artifacts, while holding cloud keys, registry tokens and signing keys. They run on stale base images in shared networks, and they're all curl, Python, Go and Node underneath.
- Server-to-server API calls. Payment providers, webhooks, OAuth token exchanges and cloud SDKs (AWS CLI and boto use their own certifi bundle; Terraform and kubectl use Go). These carry API secrets and customer data in headers.
- IoT, embedded and OT devices. Cameras, routers, cars, medical devices, smart appliances, payment terminals and industrial controllers phoning home for commands and updates. Their bundles are frozen until 2039, which is the "cannot be contained" point made real.
- Windows code signing. It isn't TLS, but it's the same flaw: Windows trusts the eMudhra CS root directly. Malware signed with EM OV CS CA - G3B/G3C looks like signed software from a real publisher to anything that doesn't check revocation.
I tested this on my own machines using a local server and a certificate for api.stripe.com, a name that we can all likely agree is very high value. With default settings, curl said SSL certificate verify ok, Python completed the handshake, and Node said authorized: true. That was true on every Linux and macOS machine I tested. On Windows, CryptoAPI accepted the chain unless revocation checking was switched on, and even then only one of the two routes came back as revoked.
scott@Middle-Earth:~$ curl -v --resolve api.stripe.com:18888:127.0.0.1 https://api.stripe.com:18888/
* Added api.stripe.com:18888:127.0.0.1 to DNS cache
* Hostname api.stripe.com was found in DNS cache
* Trying 127.0.0.1:18888...
...snip...
* Server certificate:
* subject: CN=api.stripe.com
* start date: Oct 20 18:39:57 2026 GMT
* expire date: Oct 21 18:44:57 2026 GMT
* subjectAltName: host "api.stripe.com" matched cert's "api.stripe.com"
* issuer: C=NL; O=eMudhra BV; CN=EM DV TLS CA - G3B
* SSL certificate verify ok.
> GET / HTTP/1.1
> Host: api.stripe.com:18888
> User-Agent: curl/7.68.0
> Accept: */*
>
...snip...
< HTTP/1.0 200 ok
< Content-type: text/html
<
<HTML><BODY BGCOLOR="#ffffff">
<pre>
s_server -accept 127.0.0.1:18888 -cert /tmp/tmp.rBcIpjTbZV/leaf.pem -key /tmp/tmp.rBcIpjTbZV/leaf.key -cert_chain /home/scott/code/test/intermediate-sectigo.pem -www -quiet
...snip...
</pre></BODY></HTML>
...snip...
* Closing connection 0
* TLSv1.3 (OUT), TLS alert, close notify (256):
scott@Middle-Earth:~$You can put any other hostname you want in that certificate and you have everything you need to intercept that device's traffic, all you have to do is get on the network path.
What you should do now
It's fortunate that the steps required here aren't particularly complicated for the devices you can update, and the advice essentially boils down to 'update them'. The problem is the devices you can't update or update easily.
- Update everything that ships a CA bundle: your OS
ca-certificatespackage, certifi, Node, Homebrew, and rebuild your container images once the base images pick up the change. - Devices you can't update are the hard part. Routers, printers, TVs, appliances with frozen firmware will trust these certificates until expiry, which is July 2039...
Eight bits. For 17 whole months, that's all that stood between a huge amount of non-browser software and a forged certificate for any site blowing the security on all of your encrypted traffic.
Sources
MDSP thread: https://groups.google.com/a/mozilla.org/g/dev-security-policy/c/0R3XLJL6FuE/m/Kp1v6CPuAAAJ
Research announcement: https://emudhra.we-have-a-problem.com/
Update 20th Oct 2026 20:16 UTC
Shortly after publishing, Mozilla made their incident ticket public: https://bugzilla.mozilla.org/show_bug.cgi?id=2083449
eMudhra have given their account of what happened on that ticket. It seems there was a bug in the script used during the ceremony, a second, weak key pair was created on the HSM, and having similar names a human error lead to the wrong keys being used for the CSR. Nothing in the text of my article above has changed as a result of this new information. I can also confirm that a simple prompt in an AI agent allowed me to recover the private keys of the CA. Claude was able to recover the private keys with a single prompt.