Javathoughts Logo
Javathoughts
Published on
Views

The Private Key Was 4. Why Your Java App Was Wide Open While Your Browser Wasn't

Authors
  • avatar
    Name
    Javed Shaikh
    Twitter
The private key was 4

I want you to picture the key to your house. Now picture that the key is the number 4. Not a serial number that starts with 4. Not 4 followed by a few hundred random digits. Just 4.

That is roughly what happened at eMudhra, an Indian certificate authority whose certificates were trusted by Chrome, Firefox, Safari, Windows, and, importantly for us, by every standard JDK on the planet. In October, someone looked at the public certificates and noticed something was off. Security researcher Scott Helme put it better than I can:

"One of eMudhra's certificate authority private keys is the number 4... I recovered all of them from their public certificates on my laptop just now, and it took less than a second."

The other keys were 56, 66, 115, 155, 156, 230 and 237. A P-384 key is supposed to be a random 384-bit number. Every one of these was smaller than 256. You could brute-force the lot with a for loop before your coffee got cold.

Short version: anyone could forge an HTTPS certificate for any website. Browsers blocked it within hours. Java kept trusting it for days, the first fix got bypassed, and GraalVM native images need a rebuild. Checklist at the bottom.

How does a CA end up with a key of 4?

Nobody hacked anything. That's the part that gets me.

According to Mozilla's incident report, eMudhra generated a batch of 24 intermediate certificates on April 30, 2025. Eight came out with these toy keys. Six were for issuing website certificates, two for code signing, and Sectigo cross-signed all eight a month later, which made them trusted even more widely.

eMudhra's explanation, posted on Mozilla's bug tracker, is almost mundane. A script used during the key ceremony produced a second set of weak keys alongside the real ones. The labels looked alike. Someone picked the wrong set. Several people, over several steps, didn't notice.

The key was effectively public from the day the certificate existed, as Helme said. For about 17 months. Nobody knows if anyone used it.

eMudhra has since revoked the certs, stopped issuing from that root, and estimates around 30,000 customer certificates need replacing. Mozilla pulled trust in the root entirely, and Chrome is reviewing what's left.

Browsers: fixed before most of us heard about it

A community member reported the keys to Mozilla on Saturday, October 17 at 20:06 UTC. Chrome and Firefox blocked the certificates the same day. eMudhra revoked them on the 18th. Apple followed on the 19th, Microsoft on the 20th. Cloudflare had its whole edge patched by the evening of the 20th.

If you use a browser, you were protected before the story hit the news. You never pressed a button. Chrome's CRLSets and Firefox's OneCRL exist precisely for this, a small blocklist that ships to every browser in the background within hours.

So far, a good story about the web's immune system working. Now the Java part.

Java: a static file and a switch that's off

Browser versus JVM

Here is the uncomfortable truth about how a JVM decides who to trust. It reads a file. One file, lib/security/cacerts, bundled with the JDK you downloaded. Nobody pushes updates to it. It changes when you update the JDK and at no other time.

On top of that, revocation checking is off by default. Even though eMudhra had revoked the certificates on October 18, a stock JVM never asks. There's no Certificate Transparency check either. So a JVM running on October 21 would happily complete a TLS handshake with a server waving one of these forged certificates. A security researcher tested exactly that on OpenJDK 27.0.1 and got a clean HTTP 200 from the test hosts.

To be fair, this isn't Java being sloppy. A server-side runtime that phones home for revocation data on every connection is a different set of trade-offs than a browser. But the result in October was the same. Your browser was safe. Your Spring Boot service was not.

One week, two emergency patches, one twist

Timeline of the Java response

Oracle's quarterly Java patch landed on October 20, the day the story went public, and it did not contain a fix. Bad timing.

On October 21, OpenJDK shipped its first out-of-band fix, tracked as CVE-2026-91403 with a CVSS of 9.1. The approach was sensible on paper: add the SHA-256 fingerprints of the 16 bad certificates (eight originals plus eight Sectigo cross-signs) to lib/security/blocked.certs. They even anticipated alternate encodings and shipped 48 fingerprints to cover them.

Then came the twist. Sonatype's researchers showed that if you re-encode the certificate, it still validates, but its fingerprint changes. The blocklist matched the published bytes, not the key. A blocklist based on fingerprints was never going to beat a key that is the number 4.

So on October 24, OpenJDK shipped a second emergency fix, CVE-2026-114820, and took the blunt route: delete four root certificates from cacerts entirely. The three eMudhra roots, plus Sectigo's "AAA Certificate Services" root that did the cross-signing. If your JDK has the October 21 build only, you are not done.

The GraalVM catch

If you ship GraalVM native images, there's an extra surprise. The trust store gets baked into the binary at build time. Patching the JDK on the server changes nothing for a binary that was already built. GraalVM's advisory is blunt about it: fix the trust store on the build machine, then rebuild and redeploy every native image. Yes, every one.

The same goes for anything that vendors its own JDK: Docker images with a bundled runtime, fat installers, that one CI runner nobody has touched since 2024.

The 20-minute checklist

The 20-minute checklist
  1. Check first. OpenJDK published a tiny CheckCerts.java at openjdk.org/emudhra. It runs on JDK 8 and up and tells you in seconds whether that runtime still trusts the bad chains.
  2. Update to a build that fixes JDK-8395690. Look for CVE-2026-114820 in the vendor's release note. If it only mentions CVE-2026-91403, that's the bypassable one.
  3. No vendor build yet? Back up cacerts, then delete the four roots yourself:
keytool -delete -cacerts -alias "emsignrootcag1 [jdk]"
keytool -delete -cacerts -alias "emsignrootcag2 [jdk]"
keytool -delete -cacerts -alias "emsigneccrootcag3 [jdk]"
keytool -delete -cacerts -alias "comodoaaaca [jdk]"

Ignore "alias does not exist" errors, restart the JVMs, and repeat for every JDK on the box.

  1. Rebuild your native images. Fix the build machine's trust store, rebuild, redeploy.
  2. On Debian or Ubuntu, apt upgrade isn't enough because of a long-standing bug in ca-certificates-java. Run rm -f /etc/ssl/certs/java/cacerts and then update-ca-certificates --fresh as root.
  3. Turn revocation checking on, at least for services that talk to the public internet:
-Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true

Next time something like this happens, let your JVM behave a bit more like your browser.

Dates for the calendar

Oracle had already been moving Java from quarterly to monthly security updates this year. The next one is the November 17 Critical Security Patch Update, with another on December 15. If your runtimes aren't patched by the 17th, you'll be two updates behind on a 9.1-severity issue.

Spring made a similar move in September. The whole portfolio now releases on one day, the Thursday after the third Monday of each month, which the team is calling "Patch Thursday." The reason is telling: they've been getting nearly 80 security reports a month from the community since March, up from about six and a half historically, and had one month with 91 advisories. A lot of that comes from AI-driven scanning finding bugs faster than release trains were built to handle. I wrote about where that trend is heading in the Project Glasswing post. The eMudhra keys weren't found by AI, but the pressure on the patch pipeline is the same story.

What I actually took from this

The scary number isn't 4. It's 17 months. A trust anchor for the whole internet sat there with a guessable key for a year and a half, and the only thing that saved us was that someone eventually looked.

Browsers were built assuming CAs will fail. Java was built assuming they won't. Patch the JDK, rebuild the native images, and turn on the revocation check. Twenty minutes, honestly.

If I've got any of the details wrong, tell me in the comments and I'll fix it.


Sources I used: Help Net Security's write-up of Mozilla's incident report and eMudhra's response, The Register's reporting with Scott Helme's analysis, Cloudflare's incident post, openjdk.org/emudhra and the OpenJDK vulnerability advisories of October 21 and 24, the GraalVM native-image advisory, Sonatype's blocklist bypass report via Security Boulevard and the Red Hat bug thread, Oracle's "Transitioning Java to more frequent security updates" post, and Spring's "Releasing Spring for Modern Challenges" post.