Eight eMudhra CA keys were weak enough to brute-force, and your JVM may still trust them. Here's what happened, who's exposed, the right fix for vendor JDKs, Debian and distroless images, and a scanner to check your containers....
TL;DR: do this now
On 17 October 2026, a researcher found that eight intermediate CA certificates issued by eMudhra had private keys that could be recovered from public data. Six of the eight issue TLS certificates; two sign code.
Their chains end at one of two roots:
eMudhra says only one of the eight was used in production, and none issued certificates for websites. No exploitation had been reported as of 21 October
The keys are ECDSA on curve P-384. A P-384 private key should carry about 384 bits of randomness. These reportedly carried about 8. The eight private keys were reported as 4, 56, 66, 115, 155, 156, 230 and 237, each a single byte.
With 256 possible keys, recovery is a loop. Try each candidate and compare it with the public key in the certificate:
# Educational: recover a P-384 key with a tiny keyspace from its public key
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509 import load_pem_x509_certificate
cert = load_pem_x509_certificate(open("intermediate.pem", "rb").read())
target = cert.public_key().public_numbers()
for d in range(1, 256):
pub = ec.derive_private_key(d, ec.SECP384R1()).public_key().public_numbers()
if (pub.x, pub.y) == (target.x, target.y):
print("private key found:", d)
break
The reported cause is a bug in the key-generation script. It created a second, weak key set on the HSM, and those keys ended up in the certificate requests. eMudhra says it hasn't yet confirmed the root cause.
| Leaked site (leaf) key | Compromised intermediate key | |
|---|---|---|
| What an attacker can forge | One domain | Any domain, plus signed code for the two code-signing CAs |
| Who trusts the forgery | Clients of that one site | Every client whose trust store holds the root |
| How it's fixed | Revoke one certificate | Revocation, blocklists or root removal on every client |
The exposure window was about 17 months, from April 2025 to October 2026. Browsers were protected by Certificate Transparency: no certificate from the affected intermediates was ever logged, and Chrome and Safari reject unlogged certificates. Java doesn't check CT.
| Date | Event |
|---|---|
| Apr 2025 | Intermediates issued |
| 17 Oct 2026 | Reported; Chrome and Firefox block them |
| 18–19 Oct | eMudhra revokes them |
| 19–20 Oct | Apple and Microsoft block them |
| 20 Oct | Debian ca-certificates fix; Mozilla NSS removes the G3 root |
| 21 Oct | Oracle, OpenJDK and Azul ship out-of-band JDK builds |
| 24 Oct | OpenJDK updates its guidance (see below) |
| 27 Oct | Chrome's root removal takes effect |
Each ecosystem got its own CVE: CVE-2026-91403 (Java), CVE-2026-114406 (RHEL ca-certificates), CVE-2026-111943 (Go), CVE-2026-113812 (Perl Mozilla::CA).
Java is the weak spot for four reasons:
cacerts. OS and browser updates don't touch it, unless your JDK is a distro package linked to the OS store.api.partner.com, signed with a recovered intermediate key.The two code-signing intermediates mean forged signed JARs can pass verification too.
| Your situation | Priority |
|---|---|
| JVM calls third parties over HTTPS (payment gateways, banks, SaaS, identity providers) | Patch now |
| JVM verifies signed JARs, plugins or artifacts at runtime | Patch now |
| JVM calls cloud provider APIs over the public internet | High |
| JVM calls only internal services with a private CA and a custom trust store | Medium: check the custom store doesn't contain these roots |
| Service only accepts inbound HTTPS, terminated at a load balancer | Lower, but most services make some outbound call |
| Your own site's certificate chains to an emSign root | Replace the certificate |
You are affected if all three hold:
blocked.certs doesn't block the compromised certificates.Fix any one and you're protected.
Deleting the roots also breaks legitimate sites whose chains end there, as CERT-In has warned. Indian government and enterprise endpoints using emSign certificates are the likeliest to break. Test the endpoints your services actually call, and prefer the vendor blocklist where it exists.
| Source | What it says |
|---|---|
| OpenJDK advisory, 21 Oct | CVE-2026-91403 in security-libs/javax.net.ssl, CVSS 9.1. Affects 27.0.1, 25.0.5, 21.0.13, 17.0.21, 11.0.32.1, 8u504 and earlier |
| openjdk.org/emudhra | Prefer a vendor update. Workarounds: remove four roots, or turn on revocation checking. Checker: CheckCerts.java |
| Oracle Security Alert | Out-of-band Oracle JDK builds, 21 Oct |
| Azul workaround | Replacement blocked.certs with a published SHA-256 |
| Red Hat RHSB-2026-012 | Rated Critical; fix via ca-certificates errata; rebuild containers |
| Debian DLA-4845-1 / DSA-6572-1 | Roots removed in bookworm 20250419~deb12u2 and trixie 20250419+deb13u1 |
Updated 24 Oct. OpenJDK's guidance now says to look for vendor builds whose release notes cite JDK-8395690 / CVE-2026-114820, and it lists no fixed builds yet. Builds from 21 Oct may be a first fix rather than the final one. Verify your running image, not the version number.
Vendor builds reported as of 23 October (roundup):
| Vendor | 8 | 11 | 17 | 21 | 25 |
|---|---|---|---|---|---|
| Oracle | 8u511 b32 | 11.0.33.0.2 | 17.0.21.0.2 | 21.0.13.0.2 | 25.0.5.0.2 |
| Azul Zulu | 8u512 | 11.0.33 | 17.0.21 | 21.0.13 | 25.0.5 |
| Amazon Corretto | 8.512.08.1 | 11.0.33.0.9 | 17.0.21.0.10 | 21.0.13.0.10 | 25.0.5.0.9 |
Corretto's changelog doesn't mention eMudhra. Temurin, Microsoft and Red Hat builds were unconfirmed at that date.
/opt): install the vendor's fixed build.openjdk-*, RHEL java-*-openjdk): update ca-certificates. On Debian, also rebuild the Java store (next section).| Option | What it does | Watch out for |
|---|---|---|
A. Vendor blocked.certs | Replace lib/security/blocked.certs (blacklisted.certs on JDK 8) with the vendor's file | Check its SHA-256. Never edit it by hand |
| B. Remove four roots | keytool -delete emSign Root CA G1, G2, ECC G3 and AAA Certificate Services | Breaks legitimate chains; aliases differ by vendor |
| C. Trimmed trust store | Copy cacerts, remove the roots in the copy, run with -Djavax.net.ssl.trustStore | Every JVM needs the flag |
| D. Revocation checking | -Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true | Adds network calls and latency to each handshake |
Option B on JDK 11 or later:
for a in "emsignrootcag1 [jdk]" "emsignrootcag2 [jdk]" "emsigneccrootcag3 [jdk]" "comodoaaaca [jdk]"; do
keytool -delete -cacerts -storepass changeit -alias "$a" || true # "does not exist" is fine
doneConfirm the aliases first, because they vary by vendor:
keytool -list -v -cacerts -storepass changeit | grep -iE 'Owner:.*(emSign|AAA Certificate)'
Distroless images have no shell, so use exec-form RUN:
FROM
USER root
RUN ["keytool","-delete","-cacerts","-storepass","changeit","-alias","emsigneccrootcag3 [jdk]"]
# repeat for each alias
USER nonroot That includes batch workers, queue consumers and serverless functions.
A JDK or base-image upgrade can overwrite your edited cacerts or blocked.certs.
On Debian-based images, the JDK doesn't use its own cacerts. It follows a symlink to a keystore that Debian builds from the OS certificate store.
Because of a long-standing Debian bug, updating ca-certificates and ca-certificates-java doesn't remove old entries from the Java keystore. OpenJDK's guidance is to rebuild it:
rm -f /etc/ssl/certs/java/cacerts
update-ca-certificates --freshDo this in your Dockerfile and rebuild. Don't patch running containers.
| Image family | Where cacerts comes from | Fixed by |
|---|---|---|
Debian/Ubuntu with openjdk-* packages | /etc/ssl/certs/java/cacerts | ca-certificates update plus a --fresh rebuild |
| Distroless Java on Debian 12/13 | Keystore baked into the image | A new upstream digest, then verify |
| Distroless on Debian 11 | Baked in; no Debian fix found | Move to a Debian 12 or 13 base |
| Temurin, Corretto, Zulu, Liberica images | The JDK's own lib/security/cacerts | A new JDK build, or interim A–D |
RHEL/UBI with java-*-openjdk | The OS-extracted Java keystore | ca-certificates errata |
Pinned digests (FROM …@sha256:…) are good practice, but they freeze the problem in. Bump them deliberately and re-scan.
cacerts or PEM bundle from a ConfigMap or Secret, set via -Djavax.net.ssl.trustStore or JAVA_TOOL_OPTIONS. Rebuilding the image doesn't fix these.useDefaultCAs: true copy a default CA package into every namespace. Check that your trust-manager version ships a package without these roots.linux/amd64 by default). amd64 and arm64 images have separate trust stores.docker create). Nothing runs, but its files can be copied out. That's why it works on distroless images, which have no shell.release file to report the Java version.lib/security/cacerts is a symlink to /etc/ssl/certs/java/cacerts. The scanner follows the link inside the image and copies the real file out.keytool -list -v and matches certificate subjects against the four eMudhra and AAA roots. A store it can't read, or one that comes back empty, makes the result inconclusive.java against public test hosts that serve certificates from the compromised chains. If the handshake succeeds, the image is vulnerable. If the certificate path is rejected, it's safe. Any other failure, such as DNS, a proxy or no network, is inconclusive.The static checks explain why an image is or isn't vulnerable. The live handshake is the proof, because it uses that image's exact JVM and settings.
What it doesn't see. The scanner checks the JDK's default trust store. It can't see a different store the app selects with -Djavax.net.ssl.trustStore in its entrypoint or config, or one mounted at deploy time from a ConfigMap or Secret. Check those separately.
Find the trust store the JVM actually reads. On Debian and distroless, cacerts is a symlink, so resolve it against the image rather than your machine:
link=$(readlink "$jdk/lib/security/cacerts" || true)
case "$link" in
"") store="$jdk_in_image/lib/security/cacerts" ;;
/*) store="$link" ;;
*) store="$jdk_in_image/lib/security/$link" ;;
esac
docker cp -qL "$id:$store" "$dir/cacerts" \
|| { echo "roots: ERROR - cannot copy $store"; status=inconclusive; }
Match roots by subject, not alias. Aliases differ across vendors; subjects don't. This is the same approach as OpenJDK's CheckCerts.java:
BAD='CN=emSign Root CA - G1|CN=emSign Root CA - G2|CN=emSign ECC Root CA - G3|CN=AAA Certificate Services'
listing=$(keytool -list -v -keystore "$dir/cacerts" -storepass changeit 2>&1) \
|| { echo "roots: ERROR - keytool failed"; status=inconclusive; }
[ "$(grep -c '^Alias name:' <<<"$listing")" -gt 0 ] \
|| { echo "roots: ERROR - empty store"; status=inconclusive; }
bad=$(grep -E '^Owner:' <<<"$listing" | grep -E "$BAD")
An empty or unreadable store is never reported as OK.
Live handshake, with three outcomes. Only a certificate-path rejection counts as safe:
} catch (javax.net.ssl.SSLHandshakeException e) {
boolean pathFail = false;
for (Throwable c = e; c != null; c = c.getCause())
if (c instanceof java.security.cert.CertPathValidatorException
|| c.getClass().getName().endsWith("SunCertPathBuilderException")) pathFail = true;
System.out.println((pathFail ? "OK " : "INCONCLUSIVE ") + u);
} catch (Exception e) {
System.out.println("INCONCLUSIVE " + u + " (" + e + ")"); // DNS, proxy, no egress
}
A few rules for this test:
--release 8 so it runs on any JDK.bin/java.Example output (illustrative):
===== example/java-app:1.4 (linux/amd64)
roots : FAIL - still trusted: CN=emSign ECC Root CA - G3
live TLS : VULNERABLE https://dv-g3b-emudhra.we-have-a-problem.com/
===== example/java-app:1.5
roots : OK - no eMudhra roots
live TLS : OK (certificate path rejected)
===== Summary =====
Clean (1) · Vulnerable (1) · Inconclusive (0)The script exits 0 when everything is clean, 1 when any image is vulnerable, and 2 when results are inconclusive, so CI can fail or warn.
As of 26 October 2026.