eMudhra CA Key Compromise: Response Guide for Developers
On this page
Anyone can compute the private keys of eight eMudhra intermediate certificate authorities (CAs) from their public certificates. These keys let attackers forge TLS certificates for any domain. Current browsers block the compromised intermediates, but many other TLS clients still trust them. Use this guide to identify affected systems, test and update them, and decide whether to rotate credentials.
Summary
- What was wrong. eMudhra created eight intermediate CAs in April 2025 with weak private keys. Each private key is a number below 256, so anyone can compute it in less than a second. Six of the intermediates issue TLS certificates. Two issue code signing certificates.
- Who is affected. A TLS client is affected if it trusts the eMudhra or Sectigo root that signed the intermediates, does not block the intermediates, and does not check revocation. Many CI runners, container images, JVMs, Go binaries, Python, Rust and PHP CA bundle packages, Electron apps and macOS built-in tools have this problem.
- Server certificates. Firefox and clients with an updated trust store already reject
emSign ECC Root CA - G3. Chrome will reject it on 27 October 2026. - Attacks. Forged certificates for
google.comand other domains are in public Certificate Transparency (CT) logs since 19 October (ctlogs.dev). As of 21 October, nobody has confirmed that an attacker used one against a client (The Hacker News). - Attack tools. mitmproxy 12.3.0, released on 22 October, can sign intercepted traffic with a recovered eMudhra key (mitmproxy commit). An attacker who can intercept the traffic of an affected client needs no custom tools.
Respond now
Do these first:
- Check each server certificate that you operate. Replace each one that chains to
emSign ECC Root CA - G3now. See Check your own certificates. - Run the probes in one CI job for each runner type: hosted, self-hosted and container jobs. Also test each TLS inspection proxy, because every client behind it depends on it. See Run a probe. To test a real application, see Test the real application with mitmproxy.
- List the images that run in production. Scan each image, rebuild it with the updated OS trust store, and redeploy it. See Scan images.
- Run
go versionon each deployed Go binary. Rebuild each binary built with an affected Go version. - Check each JVM. Upgrade the JDK or add the
blocked.certsentries, then restart. - Upgrade Node.js and restart each Node.js process.
- Check the lockfiles for
certifi,webpki-roots,rustls-webpkiandcomposer/ca-bundle. See the version table.
Next:
- Fix the developer laptops, mainly macOS.
- Check Windows systems that verify code signatures.
- Search TLS inspection and proxy logs for the compromised intermediates.
- Decide whether to rotate credentials. See Investigate past exposure and rotate credentials.
- Notify affected teams and vendors.
What happened
On 30 April 2025 eMudhra created eight intermediate CAs with weak private keys. eMudhra says a flaw in its key generation script generated a second set of weak keys, and staff used the wrong set (Mozilla bug 2083449). The eight intermediates are:
- TLS:
EM DV TLS CA - G3B,EM DV TLS CA - G3C,EM OV TLS CA - G3B,EM OV TLS CA - G3C,EM EV TLS CA - G3B,EM EV TLS CA - G3C - Code signing:
EM OV CS CA - G3B,EM OV CS CA - G3C
Each intermediate has two paths to a trusted root. Sectigo signed a second copy of each intermediate in May 2025. This gives 16 CA certificates with 8 keys. A client that trusts the root of either path accepts a forged certificate, unless it blocks the intermediates or checks revocation.
emSign path: leaf -> EM DV TLS CA - G3B -> emSign Root TLS CA - G3 -> emSign ECC Root CA - G3Sectigo path: leaf -> EM DV TLS CA - G3B (Sectigo copy) -> AAA Certificate ServicesTimeline (UTC):
- 17 October 2026. A researcher credited as Wayne sent proof of the weak keys to the browser vendors. Chrome and Firefox blocked the intermediates the same day.
- 18 and 19 October. eMudhra revoked the eight certificates. Sectigo revoked its eight copies.
- 19 October. Apple blocked the intermediates. Go released 1.27.3 and 1.26.10.
- 20 October. Microsoft blocked the intermediates. Mozilla removed
emSign ECC Root CA - G3from its root list (NSS 3.131.1). - 21 October. Node.js released fixes. Red Hat published RHSB-2026-012. Projects track the trust store updates as CVE-2026-111943.
- 27 October. Chrome will remove
emSign ECC Root CA - G3(Google).
Browsers reject the forged certificates because browser vendors push blocklists to them (Chrome
CRLSet, Firefox OneCRL). Most other clients read a CA bundle file from disk and do not check
revocation. For the full technical analysis, read
Scott Helme and Mozilla bug 2083449. The community tracker at
emudhra.we-have-a-problem.com keeps the test hosts and the current patch list.
Who is affected
A client is affected when all of these are true:
- It trusts
emSign ECC Root CA - G3,emSign Root TLS CA - G3orAAA Certificate Services. Trust stores that come from the Mozilla root list (Debian, Ubuntu, Alpine,certifi, Node.js) do not containAAA Certificate Services. Windows, macOS, older Linux releases and some JDK builds contain it. - It does not block the 16 intermediate certificates.
- It does not check revocation.
A client keeps its trust store in one or more of these places:
| Place | Examples | Fix |
|---|---|---|
| OS trust store | ca-certificates on hosts and in container images | Upgrade the package and rebuild |
| Runtime | Go key blocklist, Node.js roots, JDK cacerts, Deno, Electron | Upgrade, rebuild and restart |
| Package in the lockfile | certifi, webpki-roots, webpki-root-certs, castore, composer/ca-bundle, Mozilla::CA | Upgrade the package |
| CA bundle file in a source tree | cacert.pem in a repository, an SDK, firmware, a GraalVM native image, or a precompiled binary that bundles certifi | Find it with the scan |
Risk by system:
| System | Risk | Why |
|---|---|---|
| CI/CD runners and build containers | High | They download code and hold cloud keys, registry tokens and signing keys |
| Services that call external APIs | High | Payment, OAuth, webhook and cloud SDK calls carry secrets |
curl | sh installers and self-updaters | High | A forged certificate can deliver attacker code |
| IoT devices and appliances | High | Old firmware trusts these CAs until the certificates expire |
| Developer laptops | Medium | macOS /usr/bin/curl, git, python3 and openssl read an old CA bundle |
| Windows code signing | Medium | Microsoft trusts emSign Root CS CA - G3 for code signing |
| Servers with certificates that chain to the G3 root | Outage now | Firefox and patched clients reject the root. Chrome follows on 27 October |
| Current browsers | Low | Browser vendors push a block for the intermediates |
An attack needs a forged certificate and a position where the attacker can intercept the traffic. Examples are a hostile Wi-Fi network, a compromised router, a DNS or BGP hijack, or a shared network of a self-hosted runner.
How to test your systems
1. Run a probe where the client runs
A probe is a short program that connects to a tracker test host. The test hosts serve a forged certificate. Each probe prints one of three results:
| Result | Meaning | Action |
|---|---|---|
AFFECTED | The client trusts the forged certificate | Fix the client. See Update affected systems. |
NOT AFFECTED | The client rejected the certificate | Check that both test hosts give this result |
INCONCLUSIVE | The connection failed for another reason (DNS, proxy, no network) | Use the scan and the version table |
Rules for a correct result:
- Test both paths.
dv-g3b-emudhra.we-have-a-problem.comtests the emSign path.dv-g3b-aaa-emudhra.we-have-a-problem.comtests the Sectigo path. A client is not affected only if both hosts giveNOT AFFECTED. Each command below loops over both hosts. - Run the probe in the real environment: in the CI job, in each production image, and on each
laptop type. Use the same environment variables as the application.
SSL_CERT_FILE,REQUESTS_CA_BUNDLE,CURL_CA_BUNDLE,NODE_EXTRA_CA_CERTSandNODE_OPTIONSchange the trust store that a client reads. - Through a TLS inspection proxy, the probe tests the proxy’s certificate validation rather than the client’s. To test the client directly, use a network path without TLS inspection or the offline checks in sections 2 and 3.
Set the two test hosts once:
HOSTS="dv-g3b-emudhra.we-have-a-problem.com dv-g3b-aaa-emudhra.we-have-a-problem.com"curl. Exit code 0 means AFFECTED. Exit code 60 means NOT AFFECTED. Any other exit code
means INCONCLUSIVE.
for HOST in $HOSTS; do curl -sS -o /dev/null --max-time 10 "https://$HOST/"; echo "$HOST: curl exit code $?"; donePython. The probe tests the standard library, which reads the OS trust store, and requests,
which reads certifi. Run it in each virtual environment. It needs Python 3.6 or later.
# probe.py: python3 probe.py HOSTimport sslimport sysimport urllib.request
host = sys.argv[1] if len(sys.argv) > 1 else "dv-g3b-emudhra.we-have-a-problem.com"url = f"https://{host}/"
def report(name, fetch): try: fetch() print(f"{host} {name}: AFFECTED") except Exception as e: cert_error = isinstance(e, ssl.SSLCertVerificationError) or "CERTIFICATE_VERIFY_FAILED" in str(e) print(f"{host} {name}: {'NOT AFFECTED' if cert_error else 'INCONCLUSIVE'} ({type(e).__name__})")
report("urllib, OS trust store", lambda: urllib.request.urlopen(url, timeout=10))try: import requestsexcept ImportError: print(f"{host} requests: not installed")else: report("requests, certifi", lambda: requests.get(url, timeout=10))for HOST in $HOSTS; do python3 probe.py "$HOST"; doneNode.js. The probe needs Node.js 18 or later.
for HOST in $HOSTS; donode -e 'fetch(`https://${process.argv[1]}/`, { signal: AbortSignal.timeout(10000) }).then( () => console.log(process.argv[1], process.version, "AFFECTED"), (e) => { const code = e.cause?.code ?? String(e.cause ?? e); const result = /CERT|UNABLE_TO|SIGNATURE/.test(code) ? "NOT AFFECTED" : "INCONCLUSIVE"; console.log(process.argv[1], process.version, result, code); });' "$HOST"doneGo. This probe tests the Go version that runs it. It needs Go 1.20 or later. A deployed binary keeps the Go version that
built it. Check deployed binaries with go version in section 3.
// probe.go: go run probe.go HOSTpackage main
import ( "crypto/tls" "errors" "fmt" "net/http" "os" "runtime" "time")
func main() { host := "dv-g3b-emudhra.we-have-a-problem.com" if len(os.Args) > 1 { host = os.Args[1] } client := http.Client{Timeout: 10 * time.Second} resp, err := client.Get("https://" + host + "/") var certErr *tls.CertificateVerificationError switch { case err == nil: resp.Body.Close() fmt.Println(host, runtime.Version(), "AFFECTED") case errors.As(err, &certErr): fmt.Println(host, runtime.Version(), "NOT AFFECTED:", err) default: fmt.Println(host, runtime.Version(), "INCONCLUSIVE:", err) }}for HOST in $HOSTS; do go run probe.go "$HOST"; doneJava. JDK 11 and later can run one source file. For an image with only a JRE, compile the
probe on a JDK with javac --release 11 Probe.java, copy Probe.class into the image, and run
java Probe "$HOST". Java 8 cannot run this probe. Use the offline blocked.certs check in
section 3.
// Probe.java: java Probe.java HOSTimport java.io.IOException;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.time.Duration;import javax.net.ssl.SSLHandshakeException;
public class Probe { public static void main(String[] args) throws Exception { var host = args.length > 0 ? args[0] : "dv-g3b-emudhra.we-have-a-problem.com"; var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); var request = HttpRequest.newBuilder(URI.create("https://" + host + "/")).build(); try { client.send(request, HttpResponse.BodyHandlers.discarding()); System.out.println(host + " " + Runtime.version() + " AFFECTED"); } catch (SSLHandshakeException e) { var certError = String.valueOf(e.getMessage()).matches("(?s).*(PKIX|[Cc]ertificate).*"); System.out.println(host + " " + Runtime.version() + (certError ? " NOT AFFECTED: " : " INCONCLUSIVE: ") + e.getMessage()); } catch (IOException e) { System.out.println(host + " " + Runtime.version() + " INCONCLUSIVE: " + e); } }}for HOST in $HOSTS; do java Probe.java "$HOST"; donePHP. The probe tests the curl extension and PHP streams. In a Composer project it also tests
the CA bundle file of composer/ca-bundle. Run it from the project directory. It needs PHP 8.
<?php// probe.php: php probe.php HOST$host = $argv[1] ?? "dv-g3b-emudhra.we-have-a-problem.com";$url = "https://$host/";
$c = curl_init($url);curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);if (curl_exec($c) !== false) { echo "$host curl extension: AFFECTED\n";} else { $result = curl_errno($c) === 60 ? "NOT AFFECTED" : "INCONCLUSIVE"; echo "$host curl extension: $result (" . curl_error($c) . ")\n";}
function stream_probe(string $host, string $name, string $url, array $ssl = []): void { $context = stream_context_create(["http" => ["timeout" => 10], "ssl" => $ssl]); $errors = ""; set_error_handler(function ($code, $message) use (&$errors) { $errors .= $message . "\n"; return true; }); $body = file_get_contents($url, false, $context); restore_error_handler(); if ($body !== false) { echo "$host $name: AFFECTED\n"; return; } $result = str_contains($errors, "certificate verify failed") ? "NOT AFFECTED" : "INCONCLUSIVE"; echo "$host $name: $result\n";}
stream_probe($host, "streams", $url);if (is_file("vendor/autoload.php")) { require "vendor/autoload.php"; stream_probe($host, "composer/ca-bundle", $url, ["cafile" => \Composer\CaBundle\CaBundle::getBundledCaBundlePath()]);}for HOST in $HOSTS; do php probe.php "$HOST"; doneRust. A probe that you build now gets the newest crates, so it does not show what your
application uses. Check the crate versions in the Cargo.lock of the application instead:
grep -A1 -E 'name = "(webpki-roots|webpki-root-certs|rustls-webpki)"' Cargo.lockAn affected build shows old versions, for example:
name = "rustls-webpki"version = "0.103.15"--name = "webpki-roots"version = "1.0.9"Upgrade webpki-roots and webpki-root-certs to 1.0.10 or later, and rustls-webpki to 0.103.16
or later. In the SafeDep test, either upgrade alone caused the client to reject the forged
certificate. rustls-webpki 0.102 and older get no fix. No output means that the build does not
use these crates.
SafeDep ran each probe on 22 October against two local servers that serve a forged certificate,
one on each path. Each probe printed AFFECTED with the old version and NOT AFFECTED with the
patched version. On the Sectigo path, every trust store from the Mozilla root list gave
NOT AFFECTED, old and new, because it does not contain AAA Certificate Services. Go 1.26.9
with a CA bundle file that contains that root gave AFFECTED, and Go 1.26.10 gave
NOT AFFECTED.
| Client | AFFECTED | NOT AFFECTED |
|---|---|---|
| curl and PHP | Ubuntu ca-certificates 20260601~26.04.1 | Debian ca-certificates 20250419+deb13u1 |
Python requests | certifi 2026.7.22 | certifi 2026.10.19 |
| Node.js | 22.23.3 | 22.23.4 (CERT_REVOKED) |
| Go, with the old CA bundle | go1.26.9 | go1.26.10 |
Rust ureq | rustls-webpki 0.103.15, webpki-roots 1.0.9 | rustls-webpki 0.103.16 or webpki-roots 1.0.10 |
| Java, Temurin 21.0.12.1+1 | Default cacerts | With the blocked.certs entries |
PHP composer/ca-bundle | 1.4.15, 1.5.14 | 1.4.16 |
The tracker has twelve test hosts: one for each of the six TLS intermediates on each path. To test
a trust store, use the two dv-g3b hosts. To test a blocklist (Go, Node.js, Java blocked.certs,
Windows), use all twelve hosts. The host names are {dv,ov,ev}-{g3b,g3c}-emudhra.we-have-a-problem.com and
{dv,ov,ev}-{g3b,g3c}-aaa-emudhra.we-have-a-problem.com.
Test the real application with mitmproxy
The probes test a runtime with one request to a tracker host. mitmproxy 12.3.0 and later can test
the real application against any host, internal hosts too (mitmproxy commit). The
--emudhra option signs each intercepted connection with the recovered key of
EM OV TLS CA - G3C on the emSign path. The --emudhra-old option uses the Sectigo path. Use
mitmproxy only on systems that you own.
Install mitmproxy in a virtual environment:
python3 -m venv mitm && mitm/bin/pip install 'mitmproxy>=12.3.0'Start it:
mitm/bin/mitmdump --emudhra -p 8080In a second terminal, send the client through the proxy. This example uses curl:
HTTPS_PROXY=http://127.0.0.1:8080 NO_PROXY= curl -sS -o /dev/null https://example.com/; echo "curl exit code $?"Read the result:
- The request works:
AFFECTED. - The client reports a certificate error:
NOT AFFECTED. mitmdumpdoes not print the request: the client did not use the proxy. The result isINCONCLUSIVE.
Then stop mitmdump, start it with --emudhra-old, and run the client again.
To send other clients through the proxy:
- Python, Go, curl and most command-line tools read
HTTPS_PROXY. ClearNO_PROXYfor the test host. Go never uses a proxy forlocalhost. - Node.js also needs
NODE_USE_ENV_PROXY=1. - Java needs
-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=8080.
mitmproxy connects to the real host, so the machine that runs mitmproxy must reach that host.
SafeDep ran the curl, Python, Go, Node.js and Java probes through mitmdump --emudhra against
example.com on 22 October. Each probe gave the same result as in the table above.
2. Scan images and CA bundle files
The scan works without network access. It finds each PEM file (a text file of certificates) and
compares the SHA-256 fingerprint of each certificate with the three eMudhra roots,
AAA Certificate Services and the 16 intermediates. It prints FAIL for each match and exits 1.
A FAIL on AAA Certificate Services means that a client which reads that file and has no
blocklist trusts the Sectigo path. It needs GNU grep and OpenSSL.
The scan does not read the JDK cacerts keystore, which is a binary file. It does not see roots
that are compiled into Node.js or Go. Use the version table to check JDK, Node.js and Go versions.
#!/bin/sh# emudhra-scan.sh: find CA bundle files (PEM) that trust the compromised eMudhra intermediates.# Usage: emudhra-scan.sh [DIR ...] (default: /)# Exit 1 on a match.set -u
BAD='86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B FAIL emSign ECC Root CA - G37DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D FAIL emSign Root TLS CA - G3F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5 FAIL emSign Root CS CA - G3D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4 FAIL AAA Certificate Services (Sectigo path)42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493 FAIL EM DV TLS CA - G3B45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE FAIL EM DV TLS CA - G3B (AAA)98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B FAIL EM OV TLS CA - G3B5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699 FAIL EM OV TLS CA - G3B (AAA)E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838 FAIL EM EV TLS CA - G3B579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512 FAIL EM EV TLS CA - G3B (AAA)0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19 FAIL EM DV TLS CA - G3C62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72 FAIL EM DV TLS CA - G3C (AAA)CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F FAIL EM OV TLS CA - G3C7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489 FAIL EM OV TLS CA - G3C (AAA)432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3 FAIL EM EV TLS CA - G3CFD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F FAIL EM EV TLS CA - G3C (AAA)9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4 FAIL EM OV CS CA - G3BA496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8 FAIL EM OV CS CA - G3B (AAA)4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1 FAIL EM OV CS CA - G3C84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD FAIL EM OV CS CA - G3C (AAA)'
[ $# -eq 0 ] && set -- /tmp=$(mktemp -d) || exit 2trap 'rm -rf "$tmp"' EXITfails=0
for root in "$@"; do grep -rlIs --exclude-dir=proc --exclude-dir=sys --exclude-dir=dev \ -e '-----BEGIN CERTIFICATE-----' "$root" 2>/dev/nulldone | sort -u > "$tmp/files"
while IFS= read -r f; do rm -f "$tmp"/c.* awk -v d="$tmp" '/-----BEGIN CERTIFICATE-----/{n++; out=d "/c." n} out{print > out} /-----END CERTIFICATE-----/{close(out); out=""}' "$f" for c in "$tmp"/c.*; do [ -f "$c" ] || continue fp=$(openssl x509 -in "$c" -noout -fingerprint -sha256 2>/dev/null | sed 's/.*=//; s/://g') [ -n "$fp" ] || continue name=$(printf '%s\n' "$BAD" | grep "^$fp " | cut -d' ' -f3-) [ -n "$name" ] || continue printf 'FAIL\t%s: %s\n' "$f" "$name" fails=$((fails + 1)) donedone < "$tmp/files"
echo "files scanned: $(wc -l < "$tmp/files"), matches: $fails" >&2[ "$fails" -eq 0 ]To scan an image, export its file system and scan it on the host:
cid=$(docker create eclipse-temurin:21-jre)mkdir rootfs && docker export "$cid" | tar -x -C rootfs && docker rm "$cid"sh emudhra-scan.sh rootfsOn 22 October the eclipse-temurin:21-jre image (Ubuntu 26.04, ca-certificates 20260601~26.04.1) gave this result:
FAIL rootfs/etc/ssl/certs/ca-certificates.crt: emSign ECC Root CA - G3FAIL rootfs/usr/share/ca-certificates/mozilla/emSign_ECC_Root_CA_-_G3.crt: emSign ECC Root CA - G3files scanned: 123, matches: 2The python:3.13-slim image (Debian 13, ca-certificates 20250419+deb13u1) had no match. The
debian:12 and ubuntu:24.04 base images have no CA bundle file. The file comes from the
ca-certificates package that your Dockerfile installs, so scan your own images. The
alpine:3.22 image ships ca-certificates-bundle 20261020-r0, which has the fix.
To list the images that run in a Kubernetes cluster:
kubectl get pods -A -o jsonpath='{..image}' | tr ' ' '\n' | sort -uDo not check for the root by name. grep -c emSign /etc/ssl/certs/ca-certificates.crt gives 0 on
an affected system, because that file has no names in it. In a bundle file with names, the same
command finds three other eMudhra roots (emSign Root CA - G1, emSign Root CA - C1 and
emSign ECC Root CA - C3) that are not part of this incident. Compare fingerprints.
To check one CA bundle file without the script, download
chains.zip from the tracker and verify both
test chains with OpenSSL. OK means AFFECTED. error 20 (unable to get local issuer
certificate) means NOT AFFECTED. Any other error means INCONCLUSIVE.
for h in dv-g3b-emudhra dv-g3b-aaa-emudhra; do openssl verify -no-CApath -no-CAstore -CAfile /etc/ssl/certs/ca-certificates.crt \ -untrusted "$h.we-have-a-problem.com.pem" "$h.we-have-a-problem.com.pem"done3. Check component versions
This table lists patched versions from the tracker as of 22 October. Check the tracker again before you close the incident.
| Component | How to check | Patched version | Action |
|---|---|---|---|
Ubuntu ca-certificates | dpkg-query -W ca-certificates | 20260816ubuntu0.22.04.1, 20260816ubuntu0.24.04.1, 20260816ubuntu0.26.04.1, 20260816ubuntu0.26.10.1 | Upgrade, rebuild images |
Debian ca-certificates | dpkg-query -W ca-certificates | 20250419~deb12u2 (12), 20250419+deb13u1 (13) | Upgrade, rebuild images |
Alpine ca-certificates, ca-certificates-bundle | apk list -I 'ca-certificates*' | 20261020-r0 | Upgrade, rebuild images |
Chainguard and Wolfi ca-certificates | apk list -I 'ca-certificates*' | 20260909-r3 | Upgrade, or pull a new image digest |
Fedora ca-certificates | rpm -q ca-certificates | 2026.2.92_v9.0.319 | Upgrade, rebuild images |
RHEL and UBI ca-certificates | rpm -q ca-certificates | No package on 22 October | Use the RHSB-2026-012 workaround |
| Go binary | go version BINARY | go1.26.10 or later, go1.27.3 or later. Go 1.25 gets no fix | Rebuild, redeploy |
| Node.js | node --version | 22.23.4, 24.22.1, 26.12.1. Node.js 20 and older get no fix | Upgrade, restart |
| OpenJDK and Oracle JDK, per the tracker | java -version | 27.0.1.0.2, 25.0.5.0.2, 21.0.13.0.2, 17.0.21.0.2, 11.0.33.0.2, 8u511 | Upgrade, restart |
| Temurin | java -version | No fix on 22 October | Add blocked.certs, restart |
| Debian and Ubuntu OpenJDK packages | readlink -f "$JAVA_HOME/lib/security/cacerts" prints /etc/ssl/certs/java/cacerts | Fixed with the OS trust store | Upgrade ca-certificates, restart |
Python certifi | pip show certifi | 2026.10.19 | Upgrade |
| pip, uv | pip --version, uv --version | pip 26.2.2, uv 0.13.4 | Upgrade |
Rust webpki-roots, webpki-root-certs | grep -A1 'name = "webpki-roots"' Cargo.lock | 1.0.10 | Upgrade |
Rust rustls-webpki | grep -A1 'name = "rustls-webpki"' Cargo.lock | 0.103.16 | Upgrade |
PHP composer/ca-bundle | composer show composer/ca-bundle | 1.4.16 or 1.6.0. The 1.5 releases have no fix | Upgrade |
Elixir castore | mix deps | 1.0.22 | Upgrade |
Perl Mozilla::CA | perl -MMozilla::CA -e 'print $Mozilla::CA::VERSION, "\n"' | 20261020 | Upgrade |
| Electron | The version of the app | 44.9.1, 43.8.1, 42.11.16 | Ship a new build |
| Deno | deno --version | 2.9.8 | Upgrade |
Homebrew ca-certificates | brew list --versions ca-certificates | 2026-09-25_1 | brew upgrade |
| mise | mise --version | 2026.10.17 | Upgrade |
| Claude Code | claude --version | 2.1.303 | Upgrade |
Bun, Erlang certifi, Bottlerocket | None | No fix on 22 October | Watch the tracker |
For a Go binary in an image, copy the binary out of the image and run go version on it:
cid=$(docker create IMAGE) && docker cp "$cid":/path/to/binary ./binary && docker rm "$cid"go version ./binary./binary: go1.26.9For a JVM without network access, check whether blocked.certs lists the intermediates. If the
command prints 0 and the JDK is not a patched version, the JVM is affected. On Java 8 the file is
$JAVA_HOME/jre/lib/security/blocked.certs.
grep -c 42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493 "$JAVA_HOME/lib/security/blocked.certs"4. Check lockfiles with vet
CA bundle packages often arrive as transitive dependencies of HTTP clients. Check the resolved
lockfiles. The vulnerability control of vet reports
advisories for some of these packages, for example PYSEC-2026-4320 for certifi and
RUSTSEC-2026-0381 for webpki-roots. This vet policy rule fails the build on each CA bundle
package that is older than its fix:
version: 2rules: - id: stale-ca-bundle description: A CA bundle package older than the eMudhra fix still trusts the compromised intermediates. when: >- (package.is("certifi") && package.version_cmp("2026.10.19") < 0) || (package.is("webpki-roots") && package.version_cmp("1.0.10") < 0) || (package.is("webpki-root-certs") && package.version_cmp("1.0.10") < 0) || (package.is("rustls-webpki") && package.version_cmp("0.103.16") < 0) || (package.is("composer/ca-bundle") && (package.version_cmp("1.4.16") < 0 || (package.version_cmp("1.5.0") >= 0 && package.version_cmp("1.6.0") < 0))) || (package.is("castore") && package.version_cmp("1.0.22") < 0) || (package.is("pip") && package.version_cmp("26.2.2") < 0) action: fail severity: high help: Upgrade the package to the version that removes the eMudhra roots, then rebuild. link: https://emudhra.we-have-a-problem.com/Policy v2 is in the vet v2 alpha. Install it with brew install --cask safedep/tap/vet@edge or
go install github.com/safedep/vet/v2/cmd/vet@latest. Then run:
vet scan --policy vet-policy.yml -o plainExit code 1 means that the gate failed. On a test project with certifi==2026.7.22 and
webpki-roots 1.0.9, the policy rows are the findings of this rule. The vulnerability rows
are advisories, and the requests rows are not part of this incident.
SEVERITY CONTROL SUBJECT WHERE FINDING_IDcritical vulnerability pkg:pypi/[email protected] requirements.txt:2 f-a1adf3f5b5408696critical vulnerability pkg:pypi/[email protected] requirements.txt:2 f-e988805d0d07e1ebhigh policy pkg:pypi/[email protected] requirements.txt:2 f-1cec22867326d431high policy pkg:cargo/[email protected] Cargo.lock:13 f-dbaa4355032eee40medium vulnerability pkg:pypi/[email protected] requirements.txt:1 f-240166541670eda0medium vulnerability pkg:cargo/[email protected] Cargo.lock:13 f-4e2db632712d19b9medium vulnerability pkg:pypi/[email protected] requirements.txt:1 f-d7a860703900ae40A lockfile scan does not find a CA bundle file in a source tree or the Go version of a binary.
Use the CA bundle scan to find files in source trees, and go version to check compiled Go binaries.
5. Check your own certificates
Check the chain of each endpoint that you operate, and of each vendor endpoint that your services call:
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null \ | grep -E '^ *([0-9]+ )?[si]:' \ | grep -E 'emSign ECC Root CA - G3|emSign Root TLS CA - G3|EM (DV|OV|EV) TLS CA - G3[BC]' \ || echo "no match"Read the result:
- A match on
emSign ECC Root CA - G3oremSign Root TLS CA - G3means that the certificate chains to the root that Firefox and patched clients reject now. Replace the certificate now with one from another chain. ACME users can change the directory URL to another CA. Tell the owners of the clients that call this endpoint. - A match on
EM DV,EM OVorEM EV TLS CA - G3BorG3Cmeans that the endpoint serves a certificate from a compromised intermediate. eMudhra issued no TLS certificates under these intermediates before browsers blocked them (Google). Treat the certificate as forged and start an incident. The tracker test hosts are the exception. no matchmeans that the endpoint does not use these chains.
An updated trust store also breaks calls to vendor endpoints with certificates that chain to
emSign ECC Root CA - G3. Check those endpoints before you upgrade, and ask the vendor to replace
the certificate.
Next, look for forged certificates for your own domains. Search the CA pages on
ctlogs.dev. crt.sh did not show these entries on 22 October.
Update affected systems
Start with the systems that hold secrets: CI runners, services that call external APIs, and deployment tools. Use the version table for each patched version.
-
Upgrade the OS trust store and rebuild each image. A host upgrade does not change an image that is already built. Add the upgrade to the Dockerfile, build with
--pull --no-cache, update pinned digests, and redeploy.RUN apt-get update && apt-get install -y --only-upgrade ca-certificates \&& rm -rf /var/lib/apt/lists/*RUN apk upgrade --no-cache ca-certificates-bundle ca-certificatesDistroless and Chainguard images have no package manager. Pull a new image digest and rebuild.
Red Hat has no patched package for RHEL and UBI on 22 October. Use the workaround in RHSB-2026-012, and rebuild UBI images when the package is released.
-
Rebuild each Go binary with Go 1.27.3 or 1.26.10 or later, then redeploy it. The fix is compiled into the binary. A new Go version on the build host does not change a deployed binary. Go 1.25 gets no fix, so move to Go 1.26.10 or later.
-
Update each JVM. Upgrade to a patched JDK. If your vendor has no fix yet, add the 16 intermediates to
blocked.certs.Temurin, Oracle and other vendor JDKs use their own
cacertsfiles. Updating the OS trust store does not update these files. Temurin’sUSE_SYSTEM_CA_CERTS=1option adds roots but does not remove them.Debian and Ubuntu OpenJDK packages use
/etc/ssl/certs/java/cacerts, which the OS trust store builds. For these packages, the OS trust store update in step 1 applies.SafeDep tested the OS trust store update and
USE_SYSTEM_CA_CERTS=1on Temurin 21.0.12.1+1. Java still trusted the forged certificate. SafeDep also tested the OS trust store update with OpenJDK 17 on Debian 12.To block the intermediates, save this file as
emudhra-blocked.certs:42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA149345A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD31995120164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D1962B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D84B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B184112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FDAdd it in the Dockerfile:
COPY emudhra-blocked.certs /tmp/emudhra-blocked.certsRUN cat /tmp/emudhra-blocked.certs >> "$JAVA_HOME/lib/security/blocked.certs" \&& rm /tmp/emudhra-blocked.certsRestart each JVM. After this change, the Java probe printed this against the SafeDep test server:
localhost:8443 21.0.12.1+1-LTS NOT AFFECTED: (certificate_unknown) PKIX path validation failed: java.security.cert.CertPathValidatorException: Untrusted certificate: CN=EM DV TLS CA - G3B, O=eMudhra BV, C=NLblocked.certsblocks the intermediates on both paths and does not remove a root, so legitimate Sectigo chains keep working. GraalVM native images contain the trust store from build time. Add the file to the GraalVM installation and rebuild the native image. -
Upgrade Node.js and restart each process. A running process uses the old version until it restarts. Electron apps need a new release that is built on a patched Electron version.
-
Upgrade the CA bundle packages. Require
certifi2026.10.19 or later in each Python environment, also where the project does not listcertifi. Some libraries loadcertifiwhen it is installed. Upgradewebpki-roots,rustls-webpki,composer/ca-bundle,castoreandMozilla::CA. Replace each CA bundle file that the scan finds in a source tree. -
Fix developer laptops. On macOS,
/usr/bin/curl,git,python3,perl,rubyandopensslread/etc/ssl/cert.pem. Apple did not update that file. Apple’s block covers only apps that use the macOS certificate check. Use Homebrew curl (/opt/homebrew/opt/curl/bin/curlon Apple silicon), or apply the macOS workaround on the tracker. Upgrade pip, uv and mise. -
Check Windows. Microsoft sends the block through automatic certificate trust list updates. For a machine that does not get these updates, the tracker has a PowerShell script that adds the 16 certificates to
LocalMachine\Disallowed. An application with its own CA bundle file still needs its own update. -
Close the Sectigo path. If the scan finds
AAA Certificate Servicesin a CA bundle file, first upgrade the package that ships the file. Remove the root by hand only if no update exists, because it also signs legitimate Sectigo chains. Test the services that use Sectigo certificates after you remove it. Clients with a blocklist (patched Go and Node.js versions, Java withblocked.certs) reject the Sectigo path without this change. -
Replace server certificates that chain to
emSign ECC Root CA - G3now. -
Fix the nodes and the proxies. Kubernetes nodes pull images over TLS with containerd or Docker, which read the node OS trust store. Upgrade the node image. Upgrade each TLS inspection proxy, because it makes the trust decision for every client behind it.
-
Fix GitHub-hosted runners. You cannot change the runner image. Run the probes in a job to see the current result. Until the probes print
NOT AFFECTED, run the job in a container image that you updated, or upgrade the trust store in a step at the start of the job.
Files to change in a repository:
- Dockerfiles: base image tags and digests, and the
ca-certificatesupgrade step. go.mod(goandtoolchainlines) and the Go version in CI..nvmrc,package.jsonengines,.tool-versionsandmise.toml.- The versions in
actions/setup-go,actions/setup-nodeandactions/setup-javasteps. - Dependency lockfiles:
requirements.txt,poetry.lock,uv.lock,Cargo.lock,composer.lock,mix.lock. - Any
cacert.pemor other CA bundle file that the scan finds in the source tree.
Investigate past exposure and rotate credentials
The weakness became widely known between 17 and 20 October 2026. Forged certificates appeared in CT logs from 19 October. Treat clients that were still affected after 17 October as the highest risk. Before that date, an attacker had to discover the weakness independently. Nobody has reported that this happened.
-
Search TLS inspection, proxy and EDR logs for server certificates that one of the 16 intermediates signed. Match on the
spki_sha256values (public key hashes) in impacted-intermediates.json. A match outside the test hosts is a forged certificate. -
List the affected clients that sent secrets: CI jobs with cloud credentials, package publish steps, deployment tools, and API clients with long-lived tokens.
-
Decide on rotation. Use the first rule that applies:
- A log shows a forged certificate. Rotate the credentials of that client now and start an incident.
- The client was affected after 17 October, sent secrets, and used a self-hosted runner, a shared network or public Wi-Fi, or has no logs. Rotate now.
- The client was affected after 17 October and sent secrets. Rotate at the next maintenance window.
- In all other cases, do not rotate.
-
On Windows, check binaries and drivers signed under
EM OV CS CA - G3BorEM OV CS CA - G3CwithGet-AuthenticodeSignatureorsigntool verify /pa /v. Treat such a binary as suspect.
Notify affected teams and vendors
- The security team or the incident lead, with the list of affected systems.
- The owners of each service with a certificate that chains to
emSign ECC Root CA - G3. - Vendors whose endpoints chain to
emSign ECC Root CA - G3. - Customers, if their clients connect to your endpoints on those chains.
References
- Mozilla bug 2083449: eMudhra private key compromise of intermediate certificates
- Google: Protecting Chrome and Android users from compromised eMudhra intermediate CA certificates
- eMudhra intermediate CA compromise tracker: test hosts, patch list, workarounds
- Red Hat RHSB-2026-012
- Node.js 22.23.4 release notes
- Go release history
- Scott Helme: Eight bits of trust
- Guillaume Laforge: The eMudhra CA compromise and the JVM
ctlogs.dev: eMudhra intermediate CA compromise- The Hacker News: Eight eMudhra CA keys compromised
- mitmproxy: Add
--emudhraoption
- supply-chain-security
- pki
- tls
- certificate-authority
- ci-cd-security
- container-security
- incident-response
- emudhra
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering
Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor
Every wheel of LiteLLM 1.107.0 on PyPI installs a credential stealer and a remote code execution backdoor. Both run each time Python starts. The code is not in the upstream GitHub source.
MALFEX Moves Its npm Loader into a Native Windows DLL
The MALFEX package imgdecode contains a native Windows loader. When loaded from a console, it attempts to download, decrypt, and inject shellcode into dllhost.exe.
Forged Commits Push a preinstall Dropper into hugo-paper and mevn-cli
An attacker pushed forged commits to the default branches of hugo-paper and mevn-cli. Running npm install or bun install in either repository downloads and runs a second-stage script from...
ASOS App Hack: Attackers Used Push Notifications to Send a Ransom Note
Attackers got into the platforms ASOS uses to message customers and pushed an extortion note to phones. Here is what is confirmed, what is only claimed, and what customers should do.
Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.