PSA Java CVE-2026-91403 vulnerability

TL;DR: CVE-2026-91403 (CVSS 9.1) is not a remote code execution bug. Java still trusts eight eMudhra intermediate certificate authorities, and anyone can compute their private keys. An attacker who can sit between your Java application and a server it calls over TLS can impersonate that server. To fix it, update to a patched JDK build. If your vendor hasn’t shipped one yet, add the compromised certificates to blocked.certs and add a second control alongside it. Both are covered below, including a Dockerfile you can adapt.

Updates

October 22, 2026, 10:45 UTC

Removing the roots from cacerts may not be enough on its own. A detailed guide from Safeguard points out that a JVM doesn’t always read cacerts. If the javax.net.ssl.trustStore property is set, the JVM uses that file instead, and if a jssecacerts file exists next to cacerts, it wins silently. Runtimes built with jlink carry their own frozen copy, and client libraries and application servers often take their own trust store path. Each of those needs the same root removal. We’ve added a section on this to the workaround and updated the checklist.

Test both chains. Because builds differ in which roots they ship, a JDK can reject the chain through the emSign root and still accept the one through AAA Certificate Services, or the other way round. Only a failure on both kinds of test host proves the fix. We’ve updated the verification step.

No change for Temurin, Corretto, Oracle or OpenJDK since the previous update.

October 22, 2026, 10:13 UTC

BellSoft (Liberica JDK) recommends removing both roots. In its guidance, BellSoft’s temporary mitigation is the same as the recommended second layer in this post: delete emSign ECC Root CA – G3 and AAA Certificate Services from cacerts. Liberica is more exposed than some other builds because its cacerts still contains the AAA root, and in BellSoft’s tests it accepted all twelve compromised test chains. Builds based on Mozilla’s root list, such as Temurin, don’t ship the AAA root and accepted only the six chains through the emSign root.

Root aliases differ between vendors. BellSoft notes that Liberica names the roots emsigneccrootcag3 [jdk] and comodoaaaca [jdk], while Temurin uses aliases that start with cn_emsign_ecc_root_ca__g3. Check each certificate’s subject with keytool -list -v before deleting anything. We’ve added this to the Dockerfile comments.

No change for Temurin, Corretto, Oracle or OpenJDK since the previous update.

October 22, 2026, 10:03 UTC

Azul Zulu has shipped a fix. Azul published an out-of-cycle release on October 21 for Zulu 6, 7, 8, 11, 17, 21, 25 and 27. It includes the upstream OpenJDK change JDK-8395142, which blocks the compromised intermediates in blocked.certs (blacklisted.certs on Java 8 and older), so no manual workaround is needed after upgrading. For installations that can’t be upgraded yet, Azul publishes a replacement block-list file and script.

Exploiting unpatched clients has become easier. According to SafeDep, mitmproxy 12.3.0, released on October 22, can sign intercepted traffic with a recovered eMudhra key, so an attacker who can intercept an affected client’s traffic doesn’t need custom tooling. CT log researchers also report that certificates for google.com signed with these keys are already public. An attacker still needs a position on the network path, but once there, the rest is off-the-shelf. We’ve updated the risk assessment below accordingly.

No change yet for Temurin and Corretto: neither had published a fixed release when we checked, and the Corretto pull requests were still open.

October 22, 2026, 09:45 UTC

Amazon Corretto has a proposed fix that goes further than blocking the eight intermediates. A set of community pull requests, one for each supported Corretto version (11, 17, 21 and 25), removes both roots involved from Corretto’s trust store: eMudhra’s emSign ECC Root CA – G3 and Sectigo’s AAA Certificate Services. The Corretto 17 pull request was opened first, on October 20, and the others followed on October 21. A Corretto maintainer replied on both the 17 and 21 pull requests that an urgent release is being prepared. All four were still open when we updated this post.

That matches what the rest of the ecosystem is doing. Mozilla removed the emSign ECC root in NSS 3.131.1 on October 20, and Chrome’s removal reaches all devices on October 27. Debian dropped both roots from its ca-certificates package. Chrome and Firefox already stopped trusting AAA Certificate Services for TLS before this incident.

Removing the two roots also closes the fingerprint-bypass gap described further down, which a hand-edited blocked.certs can’t. We’ve updated the Dockerfile, the recommendations and the checklist: removing both roots is now the recommended second layer next to blocked.certs.

What happened

In April 2025, the Indian certificate authority eMudhra created eight intermediate CAs with elliptic-curve keys. Because of a bug in its key-generation script, the private keys of those intermediates were tiny integers, small enough that anyone can recover them by brute force from the public key printed in each certificate (Mozilla bug 2083449). Anyone who has an intermediate CA’s private key can issue a valid-looking certificate for any domain.

Browsers reacted within hours. Chrome pushed a CRLSet block the evening the report arrived (Saturday, October 17, 2026), Mozilla blocked the intermediates through OneCRL the same night, and Apple and Microsoft followed on October 19 and 20.

Java is a separate case. Its default TLS stack (JSSE) doesn’t receive any of those browser revocation feeds, has revocation checking switched off by default, and doesn’t require Certificate Transparency. A standalone JDK therefore keeps accepting these chains. Six of the eight intermediates issue TLS certificates and two issue code-signing certificates. They chain up to two roots: eMudhra’s own emSign ECC Root CA – G3 and Sectigo’s AAA Certificate Services. Sectigo had cross-signed the same keys, so the path through the Sectigo root is compromised too.

On October 21, Oracle and OpenJDK published out-of-band advisories. Oracle lists Java SE 8u511, 8u511-perf, 11.0.33, 17.0.21, 21.0.13, 25.0.5 and 27.0.1, GraalVM for JDK 17.0.21 and 21.0.13, and GraalVM Enterprise Edition 21.3.20 as affected, along with all earlier versions. This list includes the scheduled updates released the day before, on October 20.

What could be exploited, and what can’t

The 9.1 score comes from the CVSS vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N, with JSSE as the affected component. It means network-reachable, no authentication, and high confidentiality and integrity impact. Availability impact is none, and the vector says nothing about code execution. You may see the issue called “RCE” on social media, but neither Oracle nor OpenJDK describes it that way.

A realistic attack requires the attacker to get between your Java process and a server it talks to, for example through a compromised network segment, DNS or BGP hijacking, or a malicious proxy. From there they present a forged certificate for the real hostname, and your application accepts it. They can then read and modify traffic in both directions: API credentials, tokens, payloads and responses. The two code-signing intermediates also matter if you verify signed JARs.

For comparison, Log4Shell scored 10.0 and could be exploited with one crafted string. This vulnerability needs a privileged network position first. Once an attacker has that position, though, the rest is easy: the private keys are public, common interception tooling already supports them, and forged certificates for well-known domains are circulating. Put it through your fast-track patch process and apply the workaround today rather than waiting for the next release window. Treat it as an emergency if your Java services make outbound TLS calls across networks you don’t control, such as the public internet, shared hosting networks or third-party proxies.

How to fix it in Oracle Java

Install the patched build listed in the Java SE patch availability document that Oracle’s security alert links to. According to secondary sources, the fixed builds are 27.0.1.0.2, 25.0.5.0.2, 21.0.13.0.2, 17.0.21.0.2 and 11.0.33.0.2. Confirm the numbers in Oracle’s document before you rely on them.

If you can’t upgrade immediately, Oracle offers a replacement blocked.certs file, with one version for Windows and one for other platforms. Back up the current $JAVA_HOME/lib/security/blocked.certs, copy over any entries you added to it yourself, then swap in Oracle’s file. On Java 8 the file is called blacklisted.certs and lives in the same directory.

How to fix it in other Java distributions

How you fix it depends on where your JDK gets its trusted roots:

How Java was installedWhere trust comes fromWhat to do
Standalone builds: Temurin, Liberica, Zulu, Corretto, tarballs, installers, official Docker imagesThe JDK’s own cacerts and blocked.certsUpdate to your vendor’s fixed build (Azul Zulu has one). Until yours ships, use the workaround below.
Linux distro packages (Debian, Ubuntu, Fedora, RHEL and similar)Usually the OS certificate storeUpdate the ca-certificates package.
Homebrew and FreeBSDBundled with the Java packageUpdate the Java package itself.

Two traps to watch for with standalone builds and containers:

  1. Running apt upgrade ca-certificates inside a vendor JDK image does nothing for Java. Vendor JDKs ship their own cacerts, and Temurin’s USE_SYSTEM_CA_CERTS=1 option only adds the OS roots on top. It never removes anything.
  2. Not every vendor has shipped a fix yet. Azul Zulu released one on October 21, but as of October 22, Temurin had no patched release, and Corretto’s fix was still in review for versions 11, 17, 21 and 25. Watch your vendor’s release announcements and container image tags. If you use an image that is built on top of a vendor JDK image, also check when that image was last rebuilt before you assume it contains the fix.

The workaround: patching blocked.certs in your image

blocked.certs lists certificates Java will refuse to trust even if they chain to a trusted root. It’s a plain text file: the first line declares Algorithm=SHA-256, and each following line is one certificate fingerprint. The workaround appends the fingerprints of all 16 certificates: the 8 intermediates plus the 8 cross-signed copies. In independent testing, a JDK patched this way refused the compromised chains even with revocation checking off, while ordinary HTTPS sites kept working.

The reporter publishes all 16 fingerprints in impacted-intermediates.json, and Oracle’s replacement blocked.certs contains the same certificates. Save the list below as emudhra-blocked.certs next to your Dockerfile. Comment lines are fine, because the build step only picks up the fingerprint lines. Before you rely on it, check it against one of those two sources rather than trusting a copy from a blog post, this one included.

# emudhra-blocked.certs: SHA-256 fingerprints of the 16 compromised eMudhra
# intermediate certificates (8 intermediates x 2 issuers).
# Source: impacted-intermediates.json from the incident disclosure site.

# EM DV TLS CA - G3B (issued by emSign Root TLS CA - G3)
42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493

# EM DV TLS CA - G3B (issued by AAA Certificate Services)
45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE

# EM OV TLS CA - G3B (issued by emSign Root TLS CA - G3)
98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B

# EM OV TLS CA - G3B (issued by AAA Certificate Services)
5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699

# EM EV TLS CA - G3B (issued by emSign Root TLS CA - G3)
E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838

# EM EV TLS CA - G3B (issued by AAA Certificate Services)
579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512

# EM DV TLS CA - G3C (issued by emSign Root TLS CA - G3)
0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19

# EM DV TLS CA - G3C (issued by AAA Certificate Services)
62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72

# EM OV TLS CA - G3C (issued by emSign Root TLS CA - G3)
CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F

# EM OV TLS CA - G3C (issued by AAA Certificate Services)
7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489

# EM EV TLS CA - G3C (issued by emSign Root TLS CA - G3)
432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3

# EM EV TLS CA - G3C (issued by AAA Certificate Services)
FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F

# EM OV CS CA - G3B (issued by emSign Root CS CA - G3)
9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4

# EM OV CS CA - G3B (issued by AAA Certificate Services)
A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8

# EM OV CS CA - G3C (issued by emSign Root CS CA - G3)
4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1

# EM OV CS CA - G3C (issued by AAA Certificate Services)
84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD

Then add the patch to your runtime image. The example takes the base image as a build argument, so it works with whichever JDK image and version you run:

# Build with your own base image, pinned by digest, e.g.:
#   docker build --build-arg BASE_IMAGE=eclipse-temurin:<version>-jre@sha256:<digest> .
ARG BASE_IMAGE
FROM ${BASE_IMAGE}

# --- Stopgap for CVE-2026-91403 ---------------------------------------------
# Remove this block once your vendor ships a build with the official fix.
COPY emudhra-blocked.certs /tmp/emudhra-blocked.certs

RUN set -eux; \
    blocked="$JAVA_HOME/lib/security/blocked.certs"; \
    test -f "$blocked"; \
    cp "$blocked" "$blocked.orig"; \
    fps="$(grep -E '^[0-9A-Fa-f]{64}$' /tmp/emudhra-blocked.certs | tr 'a-f' 'A-F')"; \
    # Fail the build if the list is incomplete or malformed
    test "$(printf '%s\n' "$fps" | wc -l)" -eq 16; \
    printf '\n# CVE-2026-91403: compromised eMudhra intermediates\n%s\n' "$fps" >> "$blocked"; \
    rm /tmp/emudhra-blocked.certs

# --- Recommended second layer: stop trusting both affected roots ------------
# Same approach as Corretto's proposed fix. Browsers already distrust both
# roots: emSign ECC Root CA - G3 (Mozilla, Chrome) and, for TLS,
# AAA Certificate Services (Chrome, Firefox).
# List the matching aliases in your image first. Some vendors don't ship
# AAA Certificate Services at all, so there may be only one:
#   keytool -list -cacerts -storepass changeit | grep -i -E 'emsign|aaa|comodo'
# Alias names differ by vendor, e.g. "emsigneccrootcag3 [jdk]" and
# "comodoaaaca [jdk]" in Liberica, "cn_emsign_ecc_root_ca__g3,..." in Temurin.
# Confirm each subject with: keytool -list -v -cacerts -storepass changeit
# then delete each matching alias:
# RUN keytool -delete -cacerts -storepass changeit -noprompt -alias "<alias>"
# Before deploying, check that none of your outbound endpoints still chain
# to either root, or those connections will start failing.

# --- Optional extra layer: revocation checking ------------------------------
# Rejects the revoked intermediates, but every TLS handshake now needs to
# reach the CA's CRL endpoint, which breaks in egress-restricted networks.
# ENV JAVA_TOOL_OPTIONS="-Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true"
# ----------------------------------------------------------------------------

# ... the rest of your image (app, user, entrypoint) ...

The same RUN step works on any image whose JDK sets JAVA_HOME, including application-server images built on a vendor JDK. Restart running JVMs after deploying, since blocked.certs is read at startup.

Check which trust store your JVM actually uses. blocked.certs lives in the JDK itself, but root removal only helps in the trust store the JVM reads, and that isn’t always cacerts. Look for these before you assume you’re covered:

  • A javax.net.ssl.trustStore system property, in JVM options, JAVA_TOOL_OPTIONS or startup scripts. If it’s set, the JVM ignores cacerts.
  • A jssecacerts file next to cacerts in $JAVA_HOME/lib/security. It takes precedence over cacerts.
  • Trust stores configured inside client libraries, application servers, database and messaging drivers. Each one is a separate file to fix.
  • Runtimes built with jlink, which contain their own copy of cacerts and blocked.certs. Patch the runtime you ship, not just the JDK you built it from.

To verify the patch, use the public test hosts listed on the reporter’s incident page, which serve certificates from the compromised intermediates. Some hosts serve the chain through the emSign root and others the chain through AAA Certificate Services, and a JDK can reject one while accepting the other, so test at least one of each. An HTTPS request from inside your patched container, using the same JVM and trust settings as your service, should fail with an untrusted-certificate error on every test host. The same request to any normal site should still succeed.

Why blocked.certs alone isn’t enough

Fingerprint blocklists have a known weakness here. A fingerprint is a hash of a certificate’s exact bytes, and Sonatype’s researchers showed that an attacker can change those bytes without the private key while the signature stays valid. For ECDSA, they swapped the signature’s s value for its mathematically equivalent n − s. For RSA, they dropped an optional NULL parameter. In both cases Java blocked the listed certificate but trusted the re-encoded one, because its fingerprint was different.

The official fix covers this case. The upstream OpenJDK change adds the same 16 certificates and also generates the blocklist so that alternative ECDSA signature encodings are matched too. A hand-edited file doesn’t have that protection.

So use the hand-edited blocked.certs as a first layer only, and remove both affected roots as the second layer. In Sonatype’s tests, removing the relevant roots from the trust store rejected both re-encoded chains, and it’s the same approach as Corretto’s proposed fix for versions 11, 17, 21 and 25. The cost is low for most applications: browsers are dropping the emSign root anyway, so sites that chain to it have to move to another CA, and Chrome and Firefox haven’t trusted AAA Certificate Services for TLS for some time. Still check your outbound dependencies first. Revocation checking is an extra option if your network can reach the CAs’ CRL endpoints. Replace the workaround with a vendor build as soon as one is available.

Checklist

  1. Inventory every Java runtime you run: services, containers, CI runners and desktop tools. Note which trust store each one actually reads: its own cacerts, the OS trust store, a jssecacerts file, or a custom file set with javax.net.ssl.trustStore.
  2. Distro-packaged OpenJDK: update ca-certificates.
  3. Oracle JDK and Azul Zulu: install the patched build, or use the vendor’s replacement block-list file in the meantime.
  4. Other standalone JDK builds: patch blocked.certs now, remove the emSign ECC and AAA Certificate Services roots, and watch for the vendor fix.
  5. Restart JVMs, then verify against test hosts for both the emSign and the AAA chain.
  6. Once fixed builds ship, remove the workaround and bump your base images.

Sources


Credits

This post was written with the help of Claude, Anthropic’s AI assistant. Thanks to Maciej Pyszka, who worked out the blocked.certs workaround described above together with Claude.

Computer Science and MBA graduate, responsible for managing IT teams for the last 10 years. Has experience working for different-size companies ranging from small start-ups to corporation that deal with different IT businesses (firmware, device drivers, mobile, eCommerce, social media, big data, financial markets, car analytics, medical, parking). In the past involved in organising Tricity Java User Group and Polish edition of Washington Business Week. Interested in behavioural economics and psychology.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.