TLDR
- eMudhra generated the keys of eight intermediate CAs so that anyone can recover them from the public certificates. Browsers have blocked them since October 17, yet Liberica JDK 21.0.10 completed a TLS handshake with all twelve test hosts that use them.
- The temporary mitigation is two
keytool -deletecommands. It works on a host and in a Dockerfile. - Updating the operating system's CA bundle does nothing for a JDK that ships its own
cacerts, and thebellsoft/liberica-runtime-containerimages we tested do.
What happened
eMudhra generated the private keys of eight of its intermediate CA certificates incorrectly, so anyone can recover them from the public certificates. That has been possible since the intermediates were issued in April 2025. On October 17, 2026, a reporter signed the message "We have a problem here" with each of the eight keys and sent the signatures to the browser root programs. The details went public on October 20 at emudhra.we-have-a-problem.com. Six of the intermediates issue TLS certificates (EM DV, OV and EV TLS CA - G3B and G3C). The other two issue code-signing certificates.
Each intermediate exists twice: signed by an eMudhra emSign root, and cross-signed by Sectigo's AAA Certificate Services. In a JDK cacerts, the chains end at one of two roots: emSign ECC Root CA - G3 (through a cross-sign) or AAA Certificate Services.
Firefox and Chrome have blocked the intermediates since October 17, Apple since October 19 and Microsoft since October 20. The CA revoked all of them on October 18 and 19 with reason CA_COMPROMISE. Anyone holding one of these keys can issue a certificate for any hostname, and a client that trusts the chain will accept it.
Why Java still trusts them
Mozilla, Google, Apple and Microsoft pushed the block through their own revocation channels, such as OneCRL, CRLite and CRLSets. The JDK's default TLS implementation consumes none of them. Two defaults keep it exposed:
- Unless an application sets
javax.net.ssl.trustStore, the JDK trusts the roots in$JAVA_HOME/lib/security/jssecacertsif that file exists, and in$JAVA_HOME/lib/security/cacertsotherwise. A vendor-bundledcacertsgets no browser blocklists and no operating-system CA updates. It changes when an administrator edits it or a JDK update replaces it. - The JDK does not check revocation unless you enable it (
com.sun.net.ssl.checkRevocationisfalseby default). The CA's revocation is invisible to a default configuration.
The JDK does not enforce Certificate Transparency either. The source page notes that the intermediates never appeared in CT logs, which is why CT-enforcing browsers could not have been attacked. It also says nobody knows yet whether the keys were used against clients that skip CT. Every JVM we tested accepted the test chains without any CT-based rejection.
The source page lists a test host for every intermediate and chain combination, twelve in total. We tested all of them on Linux x64 on October 21 with a small HttpsURLConnection program. It runs on Java 8 and every later release:
import java.net.URI;
import java.net.http.*;
import javax.net.ssl.SSLHandshakeException;
public class Check {
public static void main(String[] args) throws Exception {
var client = HttpClient.newHttpClient();
for (var level : new String[]{"dv", "ov", "ev"})
for (var gen : new String[]{"g3b", "g3c"})
for (var via : new String[]{"", "-aaa"}) {
var host = level + "-" + gen + via + "-emudhra.we-have-a-problem.com";
try {
client.send(HttpRequest.newBuilder(URI.create("https://" + host + "/")).build(),
HttpResponse.BodyHandlers.discarding());
System.out.println("TRUSTED " + host);
} catch (SSLHandshakeException e) {
System.out.println("TLS_REJECTED " + host);
} catch (Exception e) {
System.out.println("ERROR " + host + " (" + e + ")");
}
}
}
}
On JDK 11 or later, run it with java Check.java. On Java 8, or in an image that has only a JRE, compile it first: javac Check.java && java Check. A TRUSTED line means the JDK accepts a compromised intermediate. ERROR means the check could not reach the host, so it proves nothing either way.
| JDK | Trusted |
|---|---|
| Liberica JDK 21.0.10 | 12 of 12 |
Liberica JDK 8u504 in bellsoft/liberica-openjdk-alpine:8 | 12 of 12 |
Liberica JDK 21.0.12.1 in bellsoft/liberica-runtime-container:jdk-21-musl | 12 of 12 |
| Eclipse Temurin 25.0.4.1 | 6 of 12 (emSign chains) |
| Arch Linux OpenJDK 27 (system CA store) | 6 of 12 (emSign chains) |
The difference is AAA Certificate Services. Mozilla had already removed that root, and Temurin and the Arch store are both built from the Mozilla list. Liberica JDK's cacerts still contains it. We also listed the cacerts of Liberica JDK 17.0.18, 26.0.2 and 27.0.0. All three contain both roots, though we ran the handshake test only on 8u504, 21.0.10 and 21.0.12.1.
Check your JDK
Look for the two roots by alias:
keytool -list -cacerts -storepass changeit | grep -E 'comodoaaaca|emsigneccrootcag3'
On the Liberica JDK releases listed above, this prints comodoaaaca [jdk] and emsigneccrootcag3 [jdk]. Other releases and vendors may use different aliases (Temurin uses cn_emsign_ecc_root_ca__g3,...), so check the certificate subject with keytool -list -v before deleting an entry.
Java 8
The same Check program, compiled with javac, showed Liberica JDK 8u504 trusting all twelve hosts.
The Java 8 keytool has no -cacerts option, and its cacerts lives under jre/. The aliases in 8u504 are the same as in the newer releases:
keytool -list -keystore "$JAVA_HOME/jre/lib/security/cacerts" -storepass changeit | grep -E 'comodoaaaca|emsigneccrootcag3'
Every keytool -delete -cacerts command below becomes keytool -delete -keystore "$JAVA_HOME/jre/lib/security/cacerts" on Java 8. With the two aliases deleted that way, 8u504 printed TLS_REJECTED for all twelve hosts. The revocation flags in the optional section work unchanged on 8u504 and also rejected all twelve.
If your JDK came from a Linux distribution package, check where cacerts points. On Arch it is a symlink to /etc/ca-certificates/extracted/java-cacerts.jks. JDKs set up that way are fixed by the distribution's ca-certificates update, and the source page lists the patched package versions.
Temporary mitigation (we recommend this): remove the two roots
These are the same two roots the source page tells Linux and macOS users to distrust. Run as the user who owns the JDK:
keytool -delete -cacerts -storepass changeit -alias "comodoaaaca [jdk]"
keytool -delete -cacerts -storepass changeit -alias "emsigneccrootcag3 [jdk]"
If you can't modify the JDK installation, delete the aliases from a copy and point the JVM at it:
cp "$JAVA_HOME/lib/security/cacerts" ./trimmed-cacerts
keytool -delete -keystore ./trimmed-cacerts -storepass changeit -alias "comodoaaaca [jdk]"
keytool -delete -keystore ./trimmed-cacerts -storepass changeit -alias "emsigneccrootcag3 [jdk]"
java -Djavax.net.ssl.trustStore=./trimmed-cacerts -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar
With the trimmed store, Liberica JDK 21.0.10 printed TLS_REJECTED for all twelve hosts, failing with unable to find valid certification path to requested target. https://sectigo.com and https://github.com still connected.
This removes two whole trust anchors, which is broader than the eight intermediates. Removing emSign ECC Root CA - G3 breaks any legitimate server whose chain ends only at that root. Removing AAA Certificate Services can also break legitimate chains. Major browser root programs have already dropped it, but that tells you nothing about whether your Java dependencies still use it. Test the services your application calls before you roll this out.
A Liberica JDK reinstall or upgrade can replace the modified cacerts, so run the check again after every update.
Optional: turn on revocation checking
The CA has revoked every compromised intermediate, so a JVM that checks revocation rejects them without editing cacerts. Enable CRL distribution points and OCSP:
echo 'ocsp.enable=true' > revocation.security
java -Dcom.sun.net.ssl.checkRevocation=true \
-Dcom.sun.security.enableCRLDP=true \
-Djava.security.properties=revocation.security \
-jar app.jar
Liberica JDK 21.0.10 then printed TLS_REJECTED for all twelve hosts, with this cause:
java.security.cert.CertificateRevokedException: Certificate has been revoked,
reason: CA_COMPROMISE, revocation date: Sun Oct 18 11:24:32 CEST 2026,
authority: CN=EMSIGN ROOT TLS CA - G3 OCSP 2026 - 4, O=eMudhra Technologies Limited, C=IN
It ranks second because it changes how every certificate is validated. The JVM makes OCSP or CRL lookups during path validation, and a handshake can fail when the revocation status cannot be determined. Before enabling it globally, test in your environment whether the responders are reachable, how much latency they add, and how failures behave. Use it if you already run with revocation checking, or alongside the root removal.
Containers
The bellsoft/liberica-runtime-container:jdk-21-musl image we tested uses the JDK's own cacerts at /usr/lib/jvm/liberica21-lite/lib/security/cacerts. Updating Alpaquita Linux's ca-certificates package does not modify that file. Delete the aliases at build time:
FROM bellsoft/liberica-runtime-container:jdk-21-musl
RUN keytool -delete -cacerts -storepass changeit -alias "comodoaaaca [jdk]" \
&& keytool -delete -cacerts -storepass changeit -alias "emsigneccrootcag3 [jdk]"
Built from this file, the image printed TLS_REJECTED for every host. Pulling a newer base image doesn't help yet. On October 21, 2026, the jdk-21-musl tag contained Liberica JDK 21.0.12.1, which accepted all twelve test chains. The jre-21-musl, jre-21-slim-musl and jre-21-glibc images also include keytool, so the same two lines work there. For any other image variant, check that keytool exists first.
Hardened images
The bellsoft/hardened-liberica-runtime-container images ship the same two roots. On October 21, 2026, we found comodoaaaca [jdk] and emsigneccrootcag3 [jdk] in the jdk-21-musl, jdk-21-distroless-musl, jre-21-nonroot-musl and jre-21-distroless-musl tags. Both JRE tags trusted all twelve test hosts.
The Dockerfile above doesn't build on the distroless tags, which have no /bin/sh: a shell-form RUN line exits with stat /bin/sh: no such file or directory. The distroless and nonroot tags run as appuser, and keytool then fails with java.io.FileNotFoundException: .../lib/security/cacerts (Permission denied). Both tags still include keytool. Switch to root for the edit, use the exec form, and switch back:
FROM bellsoft/hardened-liberica-runtime-container:jre-21-distroless-musl
USER root
RUN ["keytool", "-delete", "-cacerts", "-storepass", "changeit", "-alias", "comodoaaaca [jdk]"]
RUN ["keytool", "-delete", "-cacerts", "-storepass", "changeit", "-alias", "emsigneccrootcag3 [jdk]"]
USER appuser
Images built this way from jre-21-distroless-musl and jre-21-nonroot-musl printed TLS_REJECTED for all twelve hosts. The final USER appuser line restores the non-root runtime user, so the running container keeps the hardened image's default. The JRE has no compiler, so we compiled Check.java on the host and ran the class in those images.
What's next
BellSoft is preparing Liberica JDK updates that remove these roots. Until they ship, run Check.java against your JDKs and images, apply the root removal where it prints TRUSTED, and run the check again.







