Does Java Trust eMudhra's Compromised Intermediate CAs? Testing CVE-2026-91403 on OpenJDK and Temurin
Contents
TL;DR
What happened On October 21, 2026, OpenJDK published CVE-2026-91403 (CVSS 9.1) in an out-of-band advisory. Java’s bundled root store (cacerts) chains to eight eMudhra intermediate CAs whose private keys anyone could compute. JDK 8, 11, 17, 21, 25, and 27 are affected
Affected versions Per the OpenJDK advisory: 27.0.1, 25.0.5, 21.0.13, 17.0.21, 11.0.33, 8u512 and earlier. That includes the scheduled releases shipped the day before, on October 20
What I saw With default settings, OpenJDK 27.0.1 connected to test hosts behind both the eMudhra root and the Sectigo root. Temurin 25.0.4.1 has no Sectigo AAA Certificate Services root in its cacerts, so it connected only through the eMudhra root
What to do
- Update to a fixed build once your vendor ships it
- If you can’t, replace
blocked.certsor turn on revocation checking. On my machine, both blocked the compromised chains - On Linux, most distribution OpenJDK packages use the OS trust store, so update
ca-certificates. Homebrew and FreeBSD packages need the Java package itself updated
On October 21, 2026, OpenJDK issued an out-of-band vulnerability advisory, separate from its quarterly schedule (OpenJDK Vulnerability Advisory: 2026/10/21).
It lists a single issue, CVE-2026-91403, with a CVSS score of 9.1, and every release family from JDK 8 to JDK 27 is affected.
Oracle and OpenJDK had shipped their scheduled updates only the day before, on October 20, and those builds are still in the affected range.
The fix for CVE-2026-91403 goes into certificate files that Java ships with.
The advisory only says it “concerns eight compromised intermediate CA certificates.” An intermediate CA is a certificate authority that sits under a root certificate and issues server certificates; it is compromised when its key is no longer safe, for example because a third party knows it.
These are eight intermediate CAs that the Indian CA eMudhra created in April 2025. These intermediates use elliptic curve keys, whose private keys are integers, and theirs were below 256, so anyone could brute-force them from the public keys in the certificates (Mozilla Bugzilla 2083449). With the private key, anyone can issue a certificate for any site in the name of that intermediate CA.
I covered the background in my previous post, where I tested curl, Node.js, Python, uv, and Go, but not Java.
Java keeps its own list of root certificates, separate from the browser, so I tested whether it accepts the compromised chains on my M4 Mac mini. I downloaded two OpenJDK builds for this.
Why Java trusts the compromised intermediate CAs
In TLS, the client follows signatures from the server certificate up through intermediate CAs to a root, and accepts the server if it trusts that root.
Java installed on its own, from Oracle, Adoptium, or similar vendors, decides which roots to trust from its bundled lib/security/cacerts file and does not use the OS or browser list.
Many Linux distribution OpenJDK packages use the OS certificates instead.
Of the eight compromised intermediates, six are for TLS and two are for code signing, and they chain to two roots.
One is eMudhra’s emSign ECC Root CA - G3; the other is Sectigo’s AAA Certificate Services.
Sectigo had cross-signed the same public keys so that the same intermediate CAs also chain to a different root. Because the keys are the same, that path is compromised too.
graph TD
A["emSign ECC Root CA - G3"] --> B["emSign Root TLS CA - G3"]
B --> C["Compromised intermediate CAs<br/>(6 for TLS)"]
D["AAA Certificate Services<br/>(Sectigo)"] --> E["Cross-signed certs<br/>with the same keys"]
C --> F["Private key can be computed,<br/>so fake certs can be issued"]
E --> F
Checking the cacerts in the two builds with keytool -list -cacerts showed that they contain different roots.
OpenJDK 27.0.1 from jdk.java.net has both roots, while Eclipse Temurin 25.0.4.1 from Adoptium has only emSign ECC Root CA - G3 and lacks AAA Certificate Services.
Browsers could reject these intermediates because Mozilla, Chrome, Apple, and Microsoft each pushed revocation data through their own mechanisms.
Browsers also require Certificate Transparency (CT, which makes CAs log issued certificates publicly) and reject certificates missing from the logs. According to the test site, no certificates chaining to these intermediates showed up in CT logs before the block, so browsers that enforce CT were not attacked (eMudhra Intermediate CA Compromise).
Java’s standard TLS implementation does not fetch that revocation data by default, and it does not require CT.
Connecting with OpenJDK 27.0.1 and Temurin 25.0.4.1
The reporter’s test site has hosts that serve certificates signed by the compromised intermediates.
dv-g3b-emudhra.we-have-a-problem.com serves a chain to the eMudhra root, and dv-g3b-aaa-emudhra.we-have-a-problem.com serves a chain through Sectigo’s AAA Certificate Services. A Java runtime that connects to these without an error trusts the compromised intermediates.
I ran the tests between 7:00 and 8:00 JST on October 22, 2026, on this setup.
| Item | Details |
|---|---|
| Machine | Mac mini (M4) |
| OS | macOS 26.5.2 |
| OpenJDK | 27.0.1+9 (macOS aarch64 build from jdk.java.net) |
| Temurin | 25.0.4.1+1 (latest JDK 25 on Adoptium) |
Temurin 25.0.5, the build matching the October 20 update, was not out yet at that point.
I verified both downloads against the vendors’ SHA-256 checksums before extracting them.
For the connection I used java.net.http.HttpClient and ran the source file directly with java Fetch.java <URL>.
import java.net.URI;
import java.net.http.*;
public class Fetch {
public static void main(String[] a) throws Exception {
try {
var r = HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI.create(a[0])).build(),
HttpResponse.BodyHandlers.ofString());
System.out.println(r.statusCode());
} catch (Exception e) {
for (Throwable c = e; c != null; c = c.getCause()) System.out.println(c);
}
}
}
In this test, “connected (HTTP status 200)” is the bad result, and an error is the correct behavior.
| Build | Via eMudhra root | Via Sectigo root |
|---|---|---|
| OpenJDK 27.0.1 | Connected (200) | Connected (200) |
| Temurin 25.0.4.1 | Connected (200) | unable to find valid certification path |
With default settings, OpenJDK 27.0.1 connected to both hosts.
Temurin 25.0.4.1 could not reach a trusted root for the Sectigo chain, but only because its cacerts lacks AAA Certificate Services. Through the eMudhra root it still connected.
I also connected to https://www.google.com/ alongside the test hosts to confirm that a normal site returned 200 in every configuration.
Turning on revocation checking
The advisory recommends turning on revocation checking as one option if you can’t update.
Revocation checking fetches the CA’s certificate revocation list (CRL) or similar data on each connection to see whether a certificate has been revoked, and Java’s TLS stack has it off by default. Two system properties turn it on.
java -Dcom.sun.net.ssl.checkRevocation=true \
-Dcom.sun.security.enableCRLDP=true \
Fetch.java https://dv-g3b-emudhra.we-have-a-problem.com/
At first I put the test options in a shell variable and ran java $O Fetch.java.
That made every revocation-enabled case return 200, on both OpenJDK 27.0.1 and Temurin 25.0.4.1. zsh does not split unquoted variables on whitespace, so the two -D options reached Java as one argument and checkRevocation never became true.
After rerunning with the options written out directly as above, both OpenJDK 27.0.1 and Temurin 25.0.4.1 rejected the compromised intermediate with CertificateRevokedException, reason CA_COMPROMISE.
| Build | Via eMudhra root | Via Sectigo root |
|---|---|---|
| OpenJDK 27.0.1 | Rejected (CA_COMPROMISE) | Rejected (CA_COMPROMISE) |
| Temurin 25.0.4.1 | Rejected (CA_COMPROMISE) | Not tested (fails with default settings) |
The debug output from -Djava.security.debug=certpath showed Java fetching the CRL from http://crl.emsign.com/?roottlscag3.crl and finding the intermediate revoked.
The revocation date recorded in Java’s exception was October 18, 2026, 18:24 JST for the eMudhra chain. For the Sectigo chain it was May 14, 2025, the same date the cross-signed certificate was issued.
Revocation checking fetches the CRL on every connection, though, so connections fail where the distribution point is unreachable.
On my machine, one of several back-to-back runs of the same command failed with java.net.SocketException: Connection reset.
Blocking the certificates with a hand-edited blocked.certs
The other option is to replace lib/security/blocked.certs, the list of certificates Java refuses.
The advisory says to download the new blocked.certs for your OS and replace $JDK_HOME/lib/security/blocked.certs with it. On JDK 8 the file is named blacklisted.certs. It also says to back up the file first and move over any lines you had added yourself.
When I tried the ops.java download links from the advisory between 7:00 and 8:00 JST on October 22, 2026, both the Unix and Windows links returned HTTP 403 with an Unexpected outage page.
Instead, I made a copy of OpenJDK 27.0.1 and tried adding entries to its blocked.certs by hand.
blocked.certs is a text file whose first line is Algorithm=SHA-256, followed by one certificate SHA-256 per line, so I took the SHA-256 of 16 certificates (8 intermediates and 8 cross-signed certificates) from the impacted-intermediates.json that the test site publishes and appended them.
python3 -c "import json; [print(x['sha256']) for x in json.load(open('impacted-intermediates.json'))]" \
>> "$JDK_HOME/lib/security/blocked.certs"
With the edited JDK, both hosts failed with Untrusted certificate: CN=EM DV TLS CA - G3B even without revocation checking, and https://www.google.com/ still returned 200.
All I added were those 16 SHA-256 lines, and the distributed blocked.certs is built differently. The fix in OpenJDK itself (8395142: CA Enhancements) adds the same 16 certificates to blocked.certs.pem and also changes how the file is generated so that alternative encodings of ECDSA (elliptic curve) signatures match. Hand-added lines are a stopgap, meant to be replaced with the distributed file once the download is back.
Two of the eight intermediates are for code signing, so signed JAR verification is involved too. The advisory says that on JDK 17 and later, jarsigner -verify -revcheck -strict checks revocation as well.
Both blocked.certs and the revocation settings are separate for each Java installation.
Fixed Java builds and OS packages
The advisory does not list fixed version numbers, and the test site lists the fixed Java versions as 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, and 8u511 (OpenJDK, Oracle JDK).
OpenJDK’s JDK 25 repository got a commit on October 21 titled “Bump update version for OpenJDK: jdk-25.0.5.0.2” (openjdk/jdk25u).
For JDK 8 the numbers conflict: the advisory says 8u512 and earlier are affected, while the test site lists 8u511 as fixed. Which one is right could not be confirmed at the time of writing.
The advisory splits the response by how Java was installed.
| How Java was installed | Certificates used | What to do |
|---|---|---|
| Standalone OpenJDK (Oracle JDK, Temurin, and other tarballs or installers) | Java’s bundled cacerts | Update to a fixed build. If you can’t, replace blocked.certs or turn on revocation checking |
| OpenJDK packages from Arch Linux, Debian, Devuan, Fedora, Gentoo, Oracle Linux, RHEL, Ubuntu | Mostly the OS certificates | Update the OS ca-certificates |
| OpenJDK packages from Homebrew and FreeBSD | Bundled with Java | Update the package itself |
As of 7:21 JST on October 22, 2026, Homebrew had openjdk at 27, openjdk@21 at 21.0.13, and openjdk@17 at 17.0.21. None of them was a .0.2 build yet.
The fixed ca-certificates versions for Debian and Ubuntu are in the table in my previous post.
Oracle’s and OpenJDK’s October 20 updates
CVE-2026-91403 is not in Oracle’s quarterly scheduled fix release, the Critical Patch Update (CPU), released the day before on October 20, 2026 (Oracle Critical Patch Update Advisory - October 2026).
That CPU has 55 Java SE fixes, 49 of which are remotely exploitable without authentication. The highest CVSS score is 7.5, for two issues: a WebKitGTK vulnerability used by JavaFX (CVE-2026-43660) and the Java plugin (CVE-2026-91484).
Oracle notes that its Java CVSS scores assume applets and similar code run with administrator privileges, and that scores are lower without them.
OpenJDK’s advisory the same day listed 26.0.2.1, 25.0.4.1, 21.0.12.1, 17.0.20.1, 11.0.32.1, and 8u504 and earlier as affected (OpenJDK Vulnerability Advisory: 2026/10/20).
Oracle JDK release notes put the security baseline (the oldest version in each family that Oracle says to stay above) at 25.0.5, 21.0.13, 17.0.21, 11.0.33, and 8u511 (JDK 25.0.5 Release Notes).
The Temurin 25.0.4.1 I tested does not include the October 20 fixes either. Adoptium’s API still listed 25.0.4.1 as the latest JDK 25 between 7:00 and 8:00 JST.
According to Oracle’s advisory, security fixes ship on the third Tuesday of every month, and months without a CPU get a Critical Security Patch Update (CSPU). The next one is the CSPU on November 17, 2026, and the Oracle JDK 25.0.5 release notes also recommend not using 25.0.5 after that date.