Recovering eMudhra's Weak P-384 CA Keys and Testing curl, Node.js, and uv on macOS
Contents
TL;DR
What happened Eight intermediate CA certificates from the Indian CA eMudhra (six TLS, two code signing) had P-384 private keys between 1 and 255. Anyone can compute the keys from the public certificates alone.
Browsers Chrome and Firefox blocked them on October 17, 2026 (UTC). Apple and Microsoft followed on October 19 and 20. No matching certificates appeared in Certificate Transparency (CT) logs before the block, so CT-enforcing browsers could not have been attacked.
Who is exposed TLS clients outside the browser. On my Mac, macOS’s built-in /usr/bin/curl, Node.js v25.3.0, and uv 0.13.3 and earlier connected to the compromised test chains without any error.
What to do
- Upgrade uv to 0.13.4, certifi to 2026.10.18 or later, pip to 26.2.2, and Go to 1.27.3 or 1.26.10
- Update your distribution’s
ca-certificatespackage - Node.js has no fix yet as of October 21, 2026
On October 17, 2026, a researcher reported that the private keys of eight intermediate CA certificates issued by the Indian certificate authority (CA) eMudhra could be computed by anyone.
A CA signs a website’s certificate to say “the holder of this key controls this domain,” and browsers and operating systems trust that signature to decide that a server is genuine.
Anyone holding an intermediate CA’s private key can issue certificates for any domain within the scope that intermediate is allowed to sign.
P-384 itself was not broken. The problem was how the keys were generated.
A P-384 (384-bit elliptic curve) private key should be a random integer with roughly 2^384 possible values. Mozilla found that all eight keys were zero in every byte except the last of their 48 bytes, which makes each one an integer below 256 (Mozilla Bugzilla 2083449).
With only 255 candidates, brute force finishes instantly.
Browsers blocked the certificates within hours of the report, but I wanted to know how package managers and command-line tools fared.
I recovered the keys on my M4 Mac mini and checked whether several TLS clients would accept the compromised certificate chains on the researcher’s test site.
The eight affected intermediate CAs
Browsers and operating systems follow the chain of signatures from a site’s certificate through an intermediate CA up to a root certificate, and accept the connection only if that root is on their trusted list.
An intermediate CA sits below the root and is the certificate that actually signs site certificates. Which roots are trusted is decided by the root programs that Chrome, Mozilla, Microsoft, and Apple each run.
On April 30, 2025, eMudhra held a key generation ceremony that created 24 intermediate CAs in one batch. The eight affected certificates are the P-384 keys from its B series (G3B) and C series (G3C).
| Type | Intermediate CA | Use | Main root programs that trusted it |
|---|---|---|---|
| TLS | EM DV / OV / EV TLS CA - G3B | Website certificates | Chrome, Mozilla, Microsoft, Apple |
| TLS | EM DV / OV / EV TLS CA - G3C | Website certificates | Chrome, Mozilla, Microsoft, Apple |
| Code signing | EM OV CS CA - G3B / G3C | Software signing | Microsoft |
The TLS intermediates chain through eMudhra’s emSign Root TLS CA - G3 to emSign ECC Root CA - G3.
Separately, Sectigo’s AAA Certificate Services root had signed the same public keys in eight cross-signed certificates. A cross-sign lets a different root vouch for the same intermediate, and because the key is the same, the cross-signed certificates are compromised too.
Sectigo revoked its cross-signs around October 19 (Bugzilla 2083449 comment 13).
graph TD
A["emSign ECC Root CA - G3<br/>(eMudhra root)"] --> B["emSign Root TLS CA - G3"]
B --> C["EM DV/OV/EV TLS CA<br/>G3B and G3C (6 certs)"]
D["AAA Certificate Services<br/>(Sectigo root)"] --> E["Cross-signed certs<br/>with the same keys"]
C --> F["Private key is an<br/>integer from 1 to 255"]
E --> F
eMudhra says its RSA intermediates went through a different process and are not affected.
The P-384 keys in the A and A-1 series came from the same ceremony, but Mozilla could not recover them despite considerable effort.
How the weak keys were created
According to eMudhra’s explanation in Bugzilla comment 11, the keys were generated on a hardware security module (HSM), a device that stores keys and performs cryptographic operations without letting the keys leave it. The HSMs were Utimaco units certified to FIPS 140-2 Level 3.
eMudhra had approved a deviation from the usual ceremony to test wrap/unwrap for key export. A flaw in that code created a second, weaker pair of keys inside the HSM.
Because the key labels were similar, the weak keys were used by mistake, signed by the root, and sent to Sectigo for cross-signing.
eMudhra acknowledged that “There were human errors in multiple steps of the process” and is re-verifying all of its key ceremonies.
A weak key like this is still a mathematically valid elliptic curve private key, so public key format checks and sign-and-verify tests pass without errors.
eMudhra and the reporter describe issuance differently: eMudhra says no end-entity certificates (the certificates handed to websites) were issued from these intermediates. The reporter’s timeline says G3B issued nothing beyond OCSP (revocation status responses), while EM DV TLS CA - G3C was live until it was disabled on October 17.
Timeline from report to block
From the April 2025 key generation to the October 17, 2026 block, anyone could compute the keys for about a year and a half.
By contrast, the major browsers blocked them less than five hours after the report. All times are UTC.
| Time (UTC) | Event | Source |
|---|---|---|
| April 30, 2025 | Key generation ceremony for 24 intermediate CAs | Bugzilla 2083449 |
| October 17, 2026 18:05 | A researcher reports to the Chrome Root Program | dev-security-policy (Chrome) |
| October 17, 19:45 | Chrome blocks the public keys of the six TLS intermediates via CRLSet, Chrome’s pushed revocation list | Same |
| October 17, 20:06 | Mozilla receives the report | Mozilla Security Blog |
| October 17, 21:48 | Mozilla independently recovers the keys | Bugzilla 2083449 |
| October 17, 22:59 | Firefox finishes blocking via OneCRL, Mozilla’s pushed revocation list | Same |
| October 18, 09:24 to 09:38 | eMudhra revokes the eight certificates as CA compromise | Same |
| October 19 | Apple blocks the certificates | Test site |
| October 20 | Microsoft blocks them. NSS 3.131.1 removes emSign ECC Root CA - G3 | Same, Mozilla Security Blog |
Certificate Transparency (CT) requires issued certificates to be recorded in public logs, and clients that enforce CT reject certificates that are not in those logs.
According to Chrome, no TLS certificates chaining to the six intermediates were logged to CT before the block, so none could have been trusted by default in CT-enforcing browsers. The test site likewise says Firefox, Chrome, Edge, and Safari enforce CT and could not have been attacked.
It adds that many clients other than browsers do not enforce CT, and it is unknown whether attackers targeted them (test site).
Because multiple people had recovered the keys publicly, Mozilla removed trust in emSign ECC Root CA - G3 on an emergency basis (Bug 2084101).
NSS is Mozilla’s TLS library used by Firefox, and it also carries the root certificate list. Chrome says version 43 of the Chrome Root Store will remove the eMudhra root.
Firefox 158.0.1 and the Extended Support Releases (ESR) 115.43.1 and 153.5.1 apply the same block to installations whose settings prevent OneCRL updates (MFSA 2026-111, CVE-2026-113470).
eMudhra stopped issuing under the G3 root and is moving subscribers to G1-chained intermediates. About 30,000 certificates need to migrate, and about 97% of them were obtained through ACME, the automated issuance protocol used by Let’s Encrypt and others.
Recovering all eight keys from public certificates
With only 255 candidates, I tried recovering the keys myself on an M4 Mac mini.
| Item | Details |
|---|---|
| Machine | Mac mini (M4) |
| OS | macOS 26.5.2 |
| Python | 3.14.4 |
| Library | cryptography 50.0.2 |
| Input | impacted-intermediates.json from the test site |
The test site published by the reporter (emudhra.we-have-a-problem.com) hosts impacted-intermediates.json, a list of the affected intermediate certificates.
A certificate’s public key is the private key k multiplied by the curve’s base point G. There is no known efficient way to work backward from the public key to k, and normally there are far too many candidates to try them all.
Here there were only 255, so I generated the public key for every k from 1 to 255 and looked for one that matched each certificate. The certificate holds only the public key, and the candidate k whose public key matches is the private key.
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
fmt = (serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint)
table = {ec.derive_private_key(k, ec.SECP384R1()).public_key().public_bytes(*fmt): k
for k in range(1, 256)}
cert = x509.load_pem_x509_certificate(pem) # PEM from the certificate list
print(table.get(cert.public_key().public_bytes(*fmt)))
Generating all 255 public keys took 28.7 milliseconds.
All eight certificates matched one of the values, and the eMudhra-issued certificate and the Sectigo cross-sign for each intermediate gave the same value.
| Intermediate CA | Use | Private key value |
|---|---|---|
| EM DV TLS CA - G3B | TLS | 156 |
| EM OV TLS CA - G3B | TLS | 155 |
| EM EV TLS CA - G3B | TLS | 115 |
| EM DV TLS CA - G3C | TLS | 230 |
| EM OV TLS CA - G3C | TLS | 4 |
| EM EV TLS CA - G3C | TLS | 56 |
| EM OV CS CA - G3B | Code signing | 66 |
| EM OV CS CA - G3C | Code signing | 237 |
The private key for EM OV TLS CA - G3C was 4. With nothing but the public certificates and the cryptography library, I could compute a CA signing key without breaking into the CA.
These intermediates are already revoked and blocked, so updated clients reject any certificate signed with these values during verification.
Testing curl, Node.js, Python, uv, and Go on macOS against the compromised chains
Browsers blocked the intermediates through CRLSet and OneCRL, but software outside the browser verifies certificates in its own way.
The test site has hosts that serve certificates signed by the compromised intermediates. dv-g3b-emudhra serves a chain to emSign ECC Root CA - G3, and dv-g3b-aaa-emudhra serves one through Sectigo’s AAA Certificate Services.
A client that connects to these hosts without an error trusts the compromised intermediates.
I ran the tests on October 21, 2026, on the same M4 Mac mini (macOS 26.5.2).
In this test, “connected” and “passed TLS verification” are the bad outcomes. A verification failure or error is the correct behavior.
| Client | Version | Via eMudhra root | Via Sectigo root |
|---|---|---|---|
/usr/bin/curl (macOS built-in) | 8.7.1 (SecureTransport) | Connected (200) | Connected (200) |
Node.js fetch | v25.3.0 | Connected (200) | Connected (200) |
| Python + certifi | 2026.7.22 | Connected (200) | Verification failed |
| Python + certifi | 2026.10.18 / 2026.10.19 | Verification failed | Verification failed |
uv pip compile | 0.9.21 (Homebrew) | Passed TLS verification | Not tested |
uv pip compile | 0.13.3 | Passed TLS verification | Not tested |
uv pip compile | 0.13.4 | invalid peer certificate: UnknownIssuer | Not tested |
Go net/http | 1.26.2 (macOS) | certificate is revoked | certificate is revoked |
The first client I tried, macOS’s built-in curl, connected to both hosts even after Apple’s October 19 block.
The test site explains that /usr/bin/curl does not check revocation and uses an outdated /etc/ssl/cert.pem. When I checked /etc/ssl/cert.pem on this machine, both emSign ECC Root CA - G3 and AAA Certificate Services were still in it.
Node.js v25.3.0’s fetch also connected to both hosts. Node.js embeds Mozilla’s root certificate list in its binary, and the test site lists both Node.js and Bun as pending as of October 21, 2026.
With certifi 2026.7.22, Python already failed on the Sectigo chain.
That certifi release no longer includes AAA Certificate Services but still includes emSign ECC Root CA - G3. From 2026.10.18 onward, the emSign root is gone as well.
For uv, I first pointed uv pip install --dry-run at the test host with --index-url, but it stopped before TLS with an error about a missing virtual environment.
Switching to pip compile, the Homebrew-installed 0.9.21 passed TLS verification and got as far as the HTTP response (Unsupported Content-Type "text/plain"). Running the same command with 0.13.4 fetched through uvx failed at the TLS stage with UnknownIssuer, while the previous release, 0.13.3, got through just like 0.9.21.
By default uv bundles root certificates taken from webpki-root-certs, and 0.13.4 removed emSign ECC Root CA - G3 from that bundle (GHSA-hq85-5cpj-mr62).
brew info uv showed that Homebrew already ships 0.13.4.
On macOS, Go hands certificate verification to the OS Security framework, so Apple’s block applied directly and the result was certificate is revoked.
Go 1.27.3 and 1.26.10 were released on October 19, and the release history says only that they include security fixes to the crypto/x509 package (Go Release History).
Package manager checks that still work when TLS verification is defeated
If a package manager verifies file hashes or signatures separately from TLS, a fake package is rejected at that step even when TLS verification has been defeated.
That only works if the correct hash or signature is already known in advance.
| Tool | Verification beyond TLS | When it still protects you if TLS is defeated |
|---|---|---|
| apt | Repository metadata is GPG-signed (SecureApt) | Usually, because the public keys that verify the signatures are already on the system |
| Go modules | Checksum database (sum.golang.org) and go.sum (Go Modules Reference) | When go.sum or the public checksum database can be checked |
| npm / pnpm | Lockfile integrity (package-lock.json) | When an existing lockfile has the hash |
| pip | Hash checking with --require-hashes (Secure installs) | When you use a requirements.txt with hashes |
| uv | Hashes recorded in uv.lock | When an existing lockfile is present |
When you resolve dependencies from scratch without a lockfile, there is no correct hash to compare against yet.
If TLS verification has been defeated at that point, files from an attacker impersonating the package registry are accepted as the real thing.
CI/CD container images that still carry old uv or certifi releases, and jobs that add dependencies without a lockfile, are protected only by TLS verification.
Cloudflare says its security review has not found active exploitation on Cloudflare services (Cloudflare Blog). That statement covers Cloudflare’s own services and says nothing about other environments.
The same post warns that disabling certificate verification to make the errors go away removes the authentication protection these updates are meant to restore. It also notes that updating an SDK alone does not change the result if the TLS library or the OS trust store (where trusted root certificates are kept) underneath is still outdated.
Fixed versions as of October 21, 2026
These are the main entries from the test site’s list.
| Software | Fixed version |
|---|---|
| uv | 0.13.4 |
| Python certifi | 2026.10.18 or later (the list shows 2026.10.19) |
| pip | 26.2.2 (vendored certifi updated to 2026.10.19) |
| Go | 1.27.3, 1.26.10 |
| Rust webpki-roots / webpki-root-certs | 1.0.10 |
| Rust rustls-webpki | 0.103.16 |
| Composer ca-bundle | 1.4.16, 1.6.0 |
| Electron | 44.9.1, 43.8.1, 42.11.16 |
| NSS | 3.131.1 |
| Ubuntu ca-certificates | 20260816ubuntu0.22.04.1 / 0.24.04.1 / 0.26.04.1 / 0.26.10.1 |
| Debian ca-certificates | 20250419~deb12u2, 20250419+deb13u1, 20260816+nmu1 |
| Alpine ca-certificates | 20261020-r0 |
| Node.js, Bun, FreeBSD ca_root_nss | Not fixed yet (listed as pending) |
Composer’s ca-bundle released 1.4.16 and 1.6.0 on October 20, and added CaBundle::findDistrustedCertificate to detect chains that include the compromised intermediates (composer/ca-bundle 1.6.0).
You can check whether your environment still trusts the compromised intermediates by connecting to the test site’s hosts. A certificate error means the client does not trust them.
curl https://dv-g3b-emudhra.we-have-a-problem.com/
curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/
The test site also documents how to remove the two roots from /etc/ssl/cert.pem or /etc/ca-certificates.conf. Removing a whole root breaks every legitimate certificate that chains to it, so the site treats this as a last resort for expert users.
When I tried again at 19:47 JST on October 21, 2026, the built-in macOS curl on my Mac still returned 200 from both hosts.