Check if eMudhra CA Is Still Trusted in Your Runtime

SitePoint TeamPublished inProgramming·Web Security·
October 21, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
How to Check If Compromised eMudhra CAs Are Still Trusted
- Identify every runtime in your stack that makes outbound TLS connections (curl, Python, Node.js, Go, Rust, containers).
- Run each runtime’s TLS client against the official per-intermediate test endpoints (for example
https://dv-g3b-emudhra.we-have-a-problem.com/) to test for trust of the compromised intermediates. - Download
chains.zipfrom Scott Helme’s disclosure post and verify its SHA-256 hash. - Verify each extracted PEM chain offline against your system and bundled CA stores using
openssl verify. - Update system CA packages (
ca-certificates), language-level bundles (certifi,webpki-roots), and runtime versions. - Rebuild Docker images and CI environments with updated base images and CA certificates.
- Automate the diagnostic check in your CI pipeline to catch regressions from image rebuilds or dependency changes.
- Subscribe to upstream advisories from Mozilla, Chrome, Apple, Microsoft, and relevant package registries.
Eight eMudhra intermediate Certificate Authority private keys are trivially recoverable from their public certificates. Any attacker can mint trusted TLS certificates for arbitrary domains. Browsers have already blocked these compromised intermediates through revocation mechanisms, but most non-browser runtimes — curl, Python, Node.js, Go, and Rust — still silently trust them. This affects any team operating server-side infrastructure, CI pipelines, or CLI tooling that makes outbound TLS connections. If your stack trusts the eMudhra chain, an attacker can forge certificates your code will accept without complaint.
This guide provides a diagnostic sequence: test every runtime in the stack against the compromised certificates, remediate by updating trust stores, and verify the fix sticks. The primary
Table of Contents
- What Went Wrong: The eMudhra Intermediate CA Compromise Explained
- Step 1: Online Diagnostic with emudhra.we-have-a-problem.com
- Step 2: Offline Verification with chains.zip
- Step 3: Remediate and Update Your Trust Stores
- Reference Table: Patched Runtime Versions and Root Removal Caveats
- Automate Ongoing Monitoring
- Don’t Assume Your Runtime Is Safe
What Went Wrong: The eMudhra Intermediate CA Compromise Explained
How a Single-Byte Private Key Breaks the Chain of Trust
In RSA key generation, you derive the private exponent d (the Carmichael totient of the modulus), where λ(n) depends on the prime factors of n. RSA’s security depends entirely on d being large — typically hundreds of digits — making it computationally infeasible to derive from the public key alone
eMudhra generated eight intermediate CA certificates with single-byte d values. Specific recovered values include d=4, d=56, and d=115, as documented in Scott Helme’s disclosure post. These are not large numbers. They are recoverable from the public certificate in milliseconds. Anyone with access to the public certificate (which is, by definition, public) can reconstruct the private key and sign certificates that chain up to eMudhra’s trusted root. That means arbitrary TLS certificates for any domain, indistinguishable from legitimate ones to any client that trusts the eMudhra chain.
An attacker holding one of these recovered keys can issue a certificate for any hostname, and any TLS client trusting the eMudhra intermediate will accept it without complaint.
The practical consequence is total impersonation capability. An attacker holding one of these recovered keys can issue a certificate for any hostname, and any TLS client trusting the eMudhra intermediate will accept it without complaint.
Why Browsers Are Already Safe (and Your Server Isn’t)
Major browsers have deployed mitigations through their respective revocation infrastructure. The compromise was reported to Apple, Google, Microsoft, Mozilla, and eMudhra on Saturday, October 17, 2026. Chrome pushed a CRLSet update blocking the intermediates the same evening, Mozilla blocked them in Firefoxober 19, and Microsoft on October 20. Current browser versions already reject the compromised intermediates
The problem lies everywhere else. Non-browser TLS clients skip revocation checks by default. curl does not check CRLs or OCSP unless explicitly configured to do so. Python’s requests and urllib3 libraries rely on the certifi bundle and perform no revocation checking. Python’s standard library urllib.request module uses the system SSL store, not certifi. Node.js’s https module uses either a compiled-in root store or the system store depending on build configuration, with no active revocation. Go’s crypto/tls loads system roots but does not perform revocation checks. Rust’s rustls paired with webpki-roots uses a bundled Mozilla root set, while native-tls defers to the system store; neither checks revocation by default.
This creates a trust store gap. The system CA bundle on a Linux server, the certifi bundle shipped with Python, and the compiled-in roots in Node.js might all still contain the eMudhra intermediates or trust the root that chains to them. Without manual intervention, these runtimes remain vulnerable.
Step 1: Online Diagnostic with emudhra.we-have-a-problem.com
What the Test Endpoints Do
The incident’s official test infrastructure, documented on the status page at emudhra.we-have-a-problem.com, provides twelve TLS endpoints: one for each of the six compromised TLS intermediates, served twice – once chaining to eMudhra’s emSign ECC root and once chaining to Sectigo’s cross-signing AAA Certificate Services root. Host names follow the pattern dv-g3b-emudhra.we-have-a-problem.com (eMudhra chain) and dv-g3b-aaa-emudhra.we-have-a-problem.com (Sectigo cross-signed chain), with dv/ov/ev and g3b/g3c variants covering all six TLS intermediates. The diagnostic logic is straightforward: if a runtime connects successfully, it still trusts the compromised chain and is vulnerable. If the TLS handshake fails with a certificate verification error, the runtime correctly distrusts the intermediate. The bare domain emudhra.we-have-a-problem.com serves only the status page itself over an ordinary Let’s Encrypt certificate, so connecting to it proves nothing – always test the per-intermediate subdomains. Test at least one -emudhra host and its -aaa-emudhra counterpart: a store can distrust one root path while still trusting the other.
Important: A network or DNS error will also produce a connection failure. Before concluding SAFE, confirm the endpoint is reachable by running
curl -v --insecure https://dv-g3b-emudhra.we-have-a-problem.com/and verify that any TLS error is certificate-related (e.g., “certificate verify failed”), not a network connectivity error.
Testing curl and OpenSSL
Run curl with verbose output against the test endpoint:
curl-v --max-time 15 https://dv-g3b-emudhra.we-have-a-problem.com/curl-v --max-time 15 https://dv-g3b-aaa-emudhra.we-have-a-problem.com/If the connection succeeds and returns an HTTP response, the system’s CA bundle still trusts the compromised intermediate. Look for SSL certificate verify ok in the verbose output. If instead curl reports an error such as SSL certificate problem: unable to get local issuer certificate or certificate verify failed, the intermediate is distrusted and the runtime is safe.
For a more granular view, use OpenSSL’s s_client directly:
openssl s_client -connect emudhra.we-have-a-problem.com:443 </dev/null 2>&1|grep"Verify return code"A Verify return code: 0 (ok) means the chain is trusted and the system is vulnerable. Any non-zero return code, such as Verify return code: 20 (unable to get local issuer certificate), indicates the intermediate is not trusted.
Testing Python, Node.js, Go, and Rust
Each runtime below connects to the same test endpoint. A successful HTTP response means the runtime trusts the compromised chain and is vulnerable. A certificate verification error means the runtime correctly rejects it.
Python: System SSL Store
python3 -c"import urllib.request; urllib.request.urlopen('https://dv-g3b-emudhra.we-have-a-problem.com/')"2>&1This tests the system SSL store only; urllib.request uses the system SSL context, not the certifi bundle. An ssl.SSLCertVerificationError confirms the intermediate is distrusted. Check the error message to confirm it references a certificate verification failure rather than a configuration or network problem.
Python: certifi Bundle
python3 -c"import ssl, certifi, urllib.request; ctx = ssl.create_default_context(cafile=certifi.where()); urllib.request.urlopen('https://dv-g3b-emudhra.we-have-a-problem.com/', context=ctx)"2>&1A successful response here means the certifi bundle specifically trusts the compromised chain, independent of the system store.
Node.js
node-e"const req = require('https').get('https://dv-g3b-emudhra.we-have-a-problem.com/', res => {console.log('VULNERABLE: status', res.statusCode);res.resume();});req.setTimeout(10000, () => {console.error('ERROR: request timed out');req.destroy();});req.on('error', e => {console.log('SAFE:', e.message);});"If Node.js prints a status code, it trusts the compromised intermediate. A certificate-related error in the output means it does not. Whether Node.js uses its compiled-in roots or the system store depends on build configuration, so this test reflects whichever store your build uses.
Initialize a module first, then save the following as check.go:
go mod init emudhracheckpackage mainimport("fmt""io""net/http""time")funcmain(){client :=&http.Client{Timeout:10* time.Second}resp, err := client.Get("https://dv-g3b-emudhra.we-have-a-problem.com/")if err !=nil{fmt.Println("SAFE:", err)return}defer resp.Body.Close()io.Copy(io.Discard, resp.Body)fmt.Println("VULNERABLE: connection succeeded, status", resp.StatusCode)}Run with go run check.go. Go loads system rootsre has been updated
Add the following to your Cargo.toml:
[dependencies]reqwest={version="=0.12.5",default-features=false,features=["blocking","rustls-tls"]}usestd::time::Duration;fnmain(){let client =reqwest::blocking::Client::builder().timeout(Duration::from_secs(10)).build().expect("failed to build HTTP client");match client.get("https://dv-g3b-emudhra.we-have-a-problem.com/").send(){Ok(_)=>println!("VULNERABLE: connection succeeded"),Err(e)=>println!("SAFE: {}", e),}}Run with cargo run. With rustls + webpki-roots, the result depends on whether the bundled root set has been updated. With native-tls, the result reflects the system store.
Step 2: Offline Verification with chains.zip
Download and Extract the Certificate Chains
The disclosure rete chains for each of the eight compromised intermediates. Download chains.zip from Scott Helme’s disclosure post at https://scotthelme.co.uk/eight-bits-of-trust-emudhras-ca-keys-cracked-in-milliseconds/. Verify the file’s SHA-256 hash against the value published on that page before use. Do not use copies from unverified mirrors
unzip chains.zip -d emudhra-chains/The extracted directory contains individual PEM files, each representing a certificate chain involving a compromised intermediate.
Verify Against Your System Trust Store
Use a shell loop to test each chain individually against the system CA bundle. The glob form (openssl verify ... *.pem) is unreliable across OpenSSL versions — multi-file positional arguments require OpenSSL ≥ 3.0 and can silently skip files on older versions. Use the loop instead:
forchainin emudhra-chains/*.pem;doecho"Checking $chain..."openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt "$chain"doneOn Debian/Ubuntu systems, the bundle is typically at /etc/ssl/certs/ca-certificates.crt. On RHEL/Fedora/CentOS, look for /etc/pki/tls/certs/ca-bundle.crt. On macOS, the system bundle is at /etc/ssl/cert.pem, though Homebrew-installed OpenSSL uses its own path.
If any chain returns OK, the system store still trusts that compromised intermediate and is vulnerable. A verification failure means it is distrusted.
Verify Against Bundled Trust Stores
Python’s certifi bundle location can be found with:
python3 -c"import certifi; print(certifi.where())"Node.js behavior varies: some builds use a compiled-in root store derived from Mozilla’s NSS roots, while others defer to the system store. The --use-openssl-ca flag forces Node.js to use the system’s OpenSSL CA store instead of its compiled-in bundle. This flag only works on Node.js builds compiled with --openssl-use-def-ca-store. Verify support with node --help | grep openssl-ca; if no output appears, the flag has no effect on that build.
Go’s crypto/x509 package loads system roots at runtime, so Go programs inherit the system trust store. There is no separate bundle to verify.
Run a shell loop to verify all eight chains against the certifi bundle:
CERTIFI_PATH=$(python3 -c "import certifi; print(certifi.where())")forchainin emudhra-chains/*.pem;doecho"Checking $chain..."openssl verify -CAfile"$CERTIFI_PATH""$chain"doneAny OK result means the certifi bundle is still vulnerable.
Step 3: Remediate and Update Your Trust Stores
Update System CA Packages (Linux)
The following commands apply to specific distributions. Run openssl version first to confirm your baseline.
sudoapt update &&sudoapt upgrade ca-certificatessudo dnf update ca-certificates &&sudo update-ca-trustapk upgrade ca-certificatesIf the distribution has not yet issued a patch removing the eMudhra intermediates, you must manually distrust the certificates. The correct mechanism is distribution-specific:
- Debian/Ubuntu: Prefix the certificate filename with
!in/etc/ca-certificates.conf, then runsudo update-ca-certificates. - RHEL/Fedora: Place the PEM file in
/etc/pki/ca-trust/source/blacklist/and runsudo update-ca-trust. - Systems with
p11-kitinstalled (which trust): You can also usetrust anchor --remove 'pkcs11:...'to remove by anchor identifier. This is not available on all distributions.
After any manual changes, regenerate the consolidated bundle by running the appropriate update command for your distribution and re-verify with the shell loop above.
Update on macOS
brew upgrade ca-certificates && brew upgrade opensslImportant: Homebrew’s CA bundle only affects Homebrew-linked binaries (e.g., Homebrew-installed curl, python3). System tools (/usr/bin/curl, Safari, system Python) use the macOS Keychain and require a macOS security update you are using with which curl
macOS caveat: Apple’s October 19 platform block covers Safari and Keychain-based clients, but the built-in curl, git, python3, perl, and ruby verify against /etc/ssl/cert.pem without checking revocation, and that file has not been updated at time of writing. The incident status page publishes an official workaround that strips the two affected roots from /etc/ssl/cert.pem, plus a PIP_CERT fix for the built-in pip. Homebrew’s ca-certificates 2026-09-25_1 is patched.
For the system Keychain, open Keychain Access and search for eMudhra intermediates. Apple’s platform-level block propagates through macOS security updates, though exact timelines for older macOS versions differ. Older macOS versions that no longer receive security updates will never receive the distrust entry.
Update Language-Level Trust Stores
For Python, upgrade the certifi package:
pip install--upgrade certifiVerify the new version by checking pip show certifi and comparing against upstream release notes. Applications that pin an older certifi version in their dependency files remain vulnerable until that pin is updated.
For Node.js, upgrading to a patched release that includes updated compiled-in roots is the primary path. Alternatively, launching Node.js with --use-openssl-ca defers trust decisions to the system store, which you might have already patched. This flag only works on Node.js builds compiled with --openssl-use-def-ca-store; verify support with node --help | grep openssl-ca. Pre-built binaries from nodejs.org might not honor this flag. Different LTS lines will receive the updated root store at different times.
Go relies entirely on the system store, so confirming the system CA update has been applied is sufficient.
For Rust projects using rustls with webpki-roots, run cargo update to pull the latest version of the webpki-roots crate, which derives its root set from Mozilla’s NSS. Projects using native-tls defer to the system store.
Docker and CI Environments
Rebuild container images with updated CA certificates. Pinned base images are a particular risk — they can lag behind security patches indefinitely.
FROM debian:bookworm-slimCOPY emudhra-chains/ /tmp/emudhra-chains/RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates openssl && rm -rf /var/lib/apt/lists/*RUN set -e; FOUND=0; for chain in /tmp/emudhra-chains/*.pem; do if openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt "$chain" 2>&1 | grep -q ": OK$"; then echo "ERROR: eMudhra chain still trusted: $chain"; FOUND=1; fi; done; [ "$FOUND" -eq 0 ] || { echo "Rebuild with updated ca-certificates"; exit 1; }CI pipelines using cached or pinned base images should pull fresh images or explicitly upgrade ca-certificates as a build step.
Electron Apps
Electron’s trust store source depends on version and platform. Consult the Electron documentation on certificate verification to determine whether your version uses Node.js’s compiled-in root store or the OS certificate store — the boundary has shifted across major releases, and no single version threshold applies cleanly. Upgrading the Electron version or overriding certificate verification via the session.setCertificateVerifyProc() method are the available remediation paths. Verify the specific behavior against the Electron version in use.
Reference Table: Patched Runtime Versions and Root Removal Caveats
| Runtime / Store | Minimum Patched Version | Mechanism | Caveats |
|---|---|---|---|
| Firefox | Intermediates blocked Oct 17 (OneCRL/CRLite); NSS 3.131.1 removes the eMudhra root | Revocation list push + root store update | Safe if updated in the last few days |
| Chrome | Intermediates blocked Oct 17 (CRLSet); eMudhra root removal in Chrome Root Store v43 | Revocation list push + root store update | Chromium forks might lag |
| Apple (Safari/macOS/iOS) | Blocked Oct 19 (platform block) | OS-level distrust | Older macOS versions might not receive the update; built-in curl/git/python3 still use /etc/ssl/cert.pem (see macOS section) |
| Microsoft (Edge/Windows) | Blocked Oct 20 (platform block) | CTL update | Windows Server might require manual certutil sync |
| ca-certificates (Debian/Ubuntu) | Debian 20250419~deb12u2 / 20250419+deb13u1 / 20260816+nmu1; Ubuntu 20260816ubuntu0.* series | Package update | Update from your release’s security repo, then re-run the offline verification |
| ca-certificates (Fedora/RHEL family) | Fedora 2026.2.92_v9.0.319; RHEL update in progress | Package update | update-ca-trust required after install; interim manual distrust documented on the status page |
| Alpine / Arch / Gentoo | Alpine 20261020-r0; Arch ca-certificates-mozilla 3.131.1-1; Gentoo 20260816.3.125 | Package update | FreeBSD and a few others still pending at time of writing |
| certifi (Python) | 2026.10.19 | pip install --upgrade certifi | Apps pinning old certifi remain vulnerable; pip 26.2.2 and uv 0.13.4 ship the update |
| Node.js | 22.23.4, 24.22.1, 26.12.1 | Runtime upgrade (compiled-in root store) | Upgrade your LTS line; --use-openssl-ca only works on specific builds |
| webpki-roots (Rust) | webpki-roots 1.0.10; rustls-webpki 0.103.16 | cargo update | Only applies if using rustls; native-tls uses the system store |
| Go | 1.27.3, 1.26.10 | Runtime upgrade | Also ensure the system CA store is updated on Linux/macOS |
| Electron | 44.9.1, 43.8.1, 42.11.16 | Framework upgrade | Trust store source varies by version and platform |
| Java / Bun / FreeBSD | Pending at time of writing | Pending | Track the incident status page for updates |
Distrust at the intermediate level stops the current attack, but it is fragile: new intermediates cross-signed under the same root would be trusted by default. Mozilla has therefore gone further and removed the eMudhra root entirely in NSS 3.131.1, and Chrome has confirmed the root is being removed in Chrome Root Store v43. Note also that the compromised intermediates were cross-signed by Sectigo’s AAA Certificate Services root, so a chain can build through either root – blocking only the eMudhra root path does not close the Sectigo one, which is why the incident’s official Linux workaround distrusts both roots.
Automate Ongoing Monitoring
Add the Diagnostic Check to Your CI Pipeline
The following shell script runs openssl verify against all eight chains and exits non-zero if any chain is still trusted. Drop this into a CI step or run it as a cron job.
- The
emudhra-chains/directory must be populated before running this script. Download and extractchains.zipas described in Step 2. - The
CA_BUNDLEpath defaults to the Debian/Ubuntu location. Override it by setting theCA_BUNDLEenvironment variable. On RHEL, useCA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt. On macOS, useCA_BUNDLE=/etc/ssl/cert.pem.
#!/bin/bashset-uo pipefailCHAINS_DIR="./emudhra-chains"CA_BUNDLE="${CA_BUNDLE:-/etc/ssl/certs/ca-certificates.crt}"VULNERABLE=0PEM_COUNT=$(find"$CHAINS_DIR"-maxdepth1-name'*.pem'2>/dev/null |wc-l)if[!-d"$CHAINS_DIR"]||["$PEM_COUNT"-eq0];thenecho"ERROR: $CHAINS_DIR is missing or contains no .pem files. Download and extract chains.zip first."exit2fiforchainin"$CHAINS_DIR"/*.pem;doRESULT=$(openssl verify -CAfile"$CA_BUNDLE""$chain"2>&1)||trueifecho"$RESULT"|grep-q": OK$";thenecho"VULNERABLE: $chain is still trusted"VULNERABLE=1elseecho"SAFE: $chain is distrusted"fidoneif["$VULNERABLE"-eq1];thenecho"EXIT: vulnerable chains detected"exit1fiecho"EXIT: all chains distrusted"exit0- Mozilla’s Security Blog and NSS release notes
- Chrome’s release blog at
https://chromereleases.googleblog.com/ - Apple’s security updates page
- Microsoft’s Trusted Root Certificate Program updates
certifiandwebpki-rootsrelease notes on PyPI and crates.io- Scott Helme’s blog and the
emudhra.we-have-a-problem.comstatus page for ongoing coverage of this specific incident
Don’t Assume Your Runtime Is Safe
Browsers auto-protect through active revocation infrastructure. Everything else — server-side HTTP clients, CLI tools, containerized services — requires manual verification and remediation.
Browsers auto-protect through active revocation infrastructure. Everything else — server-side HTTP clients, CLI tools, containerized services — requires manual verification and remediation. The action sequence is: (1) test against the online diagnostic endpoints, (2) verify offline against local trust stores, (3) update all system and language-level CA bundles, and (4) automate the check in CI so regressions from image rebuilds or dependency changes are caught immediately.
Sharing our passion for building incredible internet things.


