Eight Private Keys That Fit in a Byte — eMudhra, Go's x509 Denylist, and Why govulncheck Came for My Toolchain
Table of Contents
On October 17, Mozilla received a report — forwarded by the Chrome Root Program — that the private keys of eight intermediate certificates belonging to the Indian certificate authority eMudhra had been compromised. The report included proof of control of all eight keys.
Then came the punchline. At 21:48 UTC, Mozilla recovered the keys itself, without learning the reporter’s method. All eight P-384 private keys were integers less than 256. Effectively all zeroes. Anyone with a REPL and five minutes could do the same thing.
From the report to full distrust in Firefox’s OneCRL took under three hours. From key ceremony (April 30, 2025) to global embarrassment took about six months. And I got dragged into it on October 24, when govulncheck on main told me my standard library was vulnerable.
The timeline, because it’s instructive
- Oct 17 — Mozilla receives the compromise report with proof; reproduces the key recovery independently (Bugzilla 2083449). Six of the eight intermediates are TLS CAs trusted by the Chrome, Mozilla, Microsoft, and Apple root programs; the other two are code-signing certs trusted by Microsoft. eMudhra says no end-entity certificates were ever issued from the compromised intermediates — which is cold comfort, because anyone can compute the keys and forge a cert for any domain from them.
- Oct 18 — eMudhra revokes all eight certificates with reason
caCompromise. - Oct 20 — Mozilla announces emergency removal of trust in the root
emSign ECC Root CA - G3, specifically to protect clients that don’t check revocation or can’t selectively distrust intermediates. The Chrome Root Program commits to the same path the same day. eMudhra halts issuance under the root and estimates ~30,000 certificates need migrating, about 97% of them issued via ACME. - Oct 22 — eMudhra’s board announces it is voluntarily exiting the global SSL/TLS certificate business. Which, honestly, is the correct response to shipping P-384 keys that fit in a byte.
- Oct 21 — the Go vulnerability database publishes GO-2026-6809 (CVE-2026-111943) against
crypto/x509, fixed in go1.26.10 and go1.27.3.
What Go actually did
Go’s approach to this is interesting, and different from the browsers’. Browsers have fast revocation lists they can flip server-side. Go programs — and every curl, Python, Node, and Java process on your servers — depend on system trust stores or bundled certificate sets, which do not update themselves when a CA in Pune has a bad week.
From the GO-2026-6809 report:
On most platforms, Go uses the given system root certificates. However, in some narrower cases, the system trust stores may not be updated without further action. To defend against the narrower set of cases that may remain affected, a denylist was added in
crypto/x509to return an error when callingVerifywith any of the known intermediate certificates that are no longer considered to be trustworthy.
So it’s a hardcoded denylist compiled into the standard library: x509.Certificate.Verify now fails on the known-bad intermediates regardless of what your OS trust store says. The fix ships as a point release — go1.26.10 / go1.27.3 — with the change in CL 851047, tracked as issue 82345.
This is a quiet admission worth sitting with: a patch release of the Go toolchain is now incident response for a third party’s key ceremony. Your dependency graph includes a CA you’ve never heard of, and its hygiene is your problem at verify-time.
How it showed up in my repo
I run govulncheck as part of the dependency hygiene my mule-dependabot agent does for me. On October 24 it flagged main:
Vulnerability GO-2026-6809: crypto/x509
@ go1.26.9
reachable via: pirc.Bridge.SendExtensionUIResponse -> x509.Certificate.Verify
fixed in: go1.26.10
Two things I like about this workflow. First, reachability matters: I don’t panic because govulncheck says my binary actually executes the vulnerable symbol — certificate verification on the pi RPC bridge’s outbound TLS. “You import crypto/x509” is noise; “your code path calls Verify” is signal.
Second, the fix was nearly free. My go.mod already declares go 1.26.0, so this wasn’t a dependency upgrade at all — it was a single toolchain directive:
module github.com/mule-ai/mule
go 1.26.0
+toolchain go1.26.10
Build passed, tests passed (20 packages ok, 0 failures), govulncheck re-run came back clean for GO-2026-6809. Committed as 5ff5d8c, reviewed, merged. The whole incident cost me less time than writing this paragraph.
That’s the toolchain directive doing exactly what it was designed for: pinning a minimum Go version per-module so a security point release is a one-line diff that CI enforces, instead of “works on my laptop” archaeology about which Go each builder has installed.
What I took away
- Treat point releases of your toolchain as security patches, not feature drops. The gap between “fix available” (Oct 21) and my merge (Oct 24) was three days, and two of those were mine being busy. Automate the bump.
- “The browsers fixed it” is not a server story. OneCRL and Chrome’s CRLset cover browsers. Your Go services, sidecars, and CLI tools need the toolchain patch or a
ca-certificatesupdate, and nothing will nag you unless you build the nagging. - Run
govulncheck, trust reachability. It cut straight to the one call path in my codebase that actually verifies certificates. - Key ceremony entropy is someone else’s supply chain. A P-384 key less than 256 passed every structural check a PKI has — correct curve order, valid signature, proper encoding — because nothing in the chain asked “is this key surprising?” Entropy checks belong at ceremony time, and denial-of-the-obviously-broken belongs at verify time. Go chose verify time. Reasonable.
A CA whose board exited the industry in five days is a rare failure. The lesson generalizes anyway: the trust you compile is the trust you maintain. Patch your toolchain; it’s carrying more than you think.