The Private Key Was 4: Naming the Eight Certificates Go Distrusted

Yesterday we published eight SHA-256 hashes and no explanation. Go 1.27.3 and 1.26.10 had shipped a single security fix — CVE-2026-111943 — that hard-coded eight SubjectPublicKeyInfo digests into crypto/x509 and filtered any chain containing one. Go named no certificate authority. We tried to identify the keys from the public CCADB corpus, got zero hits, and reported that as a null result while explicitly declining to adopt the attribution circulating elsewhere.

The names are now public, and they are worse than the silence suggested.

On 20 October 2026 Mozilla's Firefox Security Team confirmed that on Saturday 17 October at 20:06 UTC it received a report that the private keys of eight eMudhra intermediate certificates had been compromised. OneCRL distribution of the block completed at 22:59 UTC the same day — under three hours. A coordinated disclosure site maintained by Mozilla's Dennis Jackson publishes the certificates, the public keys, and signatures over the message “We have a problem here - Wayne” produced with each of the eight private keys by the original reporter.

And that site states the cause in two sentences: “The private keys for the intermediate certificates were incorrectly generated by the CA. This means that anyone can recover them from the certificates.”

We checked both claims

Two things needed verifying, and both were verifiable from public artefacts alone, with no traffic to eMudhra or any relying party.

First: are these the same keys Go distrusted? We pulled impacted-intermediates.json from the disclosure site — sixteen certificate records — and the distrustedSPKIHashes map from src/crypto/x509/verify.go on Go's release-branch.go1.27. Rather than trust the JSON's asserted spki_sha256 field, we re-derived each digest from the certificate PEM itself (openssl x509 -pubkey -noout, re-encoded to DER, SHA-256). The sixteen records collapse to eight distinct public keys, because each intermediate appears twice: once issued by an eMudhra root, once as a Sectigo cross-sign under AAA Certificate Services. Those eight digests are, exactly and in full, the eight literals in Go's map:

  • ebed6475…5b5105 — EM DV TLS CA - G3B
  • 49dd353d…34fe5f — EM DV TLS CA - G3C
  • e0254c7f…5a8a41 — EM EV TLS CA - G3B
  • 5c0660a2…1f81a0 — EM EV TLS CA - G3C
  • 43bffa19…e2717b — EM OV TLS CA - G3B
  • 63ecefee…109cf8 — EM OV TLS CA - G3C
  • ec4ffa1a…b4f6b6 — EM OV CS CA - G3B (code signing)
  • 92c063d6…5e0e30 — EM OV CS CA - G3C (code signing)

Eight for eight. Go's silent blocklist was eMudhra, and our earlier inability to find them in CCADB's live intermediate reports is explained precisely as we guessed: they had already been revoked and pulled.

Second: how badly were the keys generated? All eight are ECDSA on secp384r1 (NIST P-384). “Anyone can recover them” is a strong claim, so we tested the narrowest hypothesis available — that the private scalars are tiny. We implemented P-384 point addition in plain Python, walked k·G for k = 1…200000, and compared each resulting point against the eight published public keys.

Every one of the eight hit, and all of them hit almost immediately. The private keys are:

  • d = 4 — EM OV TLS CA - G3C
  • d = 56 — EM EV TLS CA - G3C
  • d = 66 — EM OV CS CA - G3B
  • d = 115 — EM EV TLS CA - G3B
  • d = 155 — EM OV TLS CA - G3B
  • d = 156 — EM DV TLS CA - G3B
  • d = 230 — EM DV TLS CA - G3C
  • d = 237 — EM OV CS CA - G3C

A P-384 private key is a 384-bit integer. These are all below 238. The largest of them fits in a single byte. There is no cryptographic attack here, no lattice, no side channel, no quantum anything — the recovery is counting. Our scan found all eight in well under three minutes of single-threaded interpreted Python, and a purpose-built check would find them in milliseconds. One of the eight is the number four.

This also settles a question some coverage has been careful to leave open. One outlet noted, correctly on the evidence then available, that the public disclosures established only that a researcher demonstrated control of the keys, “not how they were obtained or how quickly any cryptographic attack could recover them.” That caution was right at the time. It is now resolvable from public data, and the answer is that no attack was required.

Eighteen months of exposure, and what saved the browsers

The certificates were issued on 30 April 2025, with the Sectigo cross-signs following on 14 May 2025. The flaw was therefore present from the moment of issuance; anyone who looked would have had signing authority for every one of those eighteen months. eMudhra revoked the eight on 18 October with the reason code ca_compromise, and the cross-signs followed on 19 October.

What prevented this from being a catastrophe for the browser ecosystem was Certificate Transparency. The disclosure states there are no CT log entries for these intermediates in that period, which means CT-enforcing clients — Firefox, Chrome, Edge, Safari — could not have accepted a forged leaf chaining through them. Any attacker using these keys against a browser would have had to log the certificate and thereby announce themselves.

That protection is exactly as wide as CT enforcement, and no wider. The disclosure is explicit that many clients outside the browser ecosystem do not enforce CT, and that whether attackers exploited the flaw against those clients is not known. That population is the entire subject of this site: API clients, SDK HTTP stacks, agent gateways, MCP servers dialling out to tool endpoints, CI runners, language runtimes shipping their own CA bundles. Almost none of them do CT. Most of them do not check revocation either. Two of the eight compromised intermediates were code-signing CAs, where the relying party is a software installer, not a browser — and where CT does not apply at all.

Why the fix list is forty entries long

The remediation table on the disclosure site is the clearest picture anyone has published this year of how many independent trust stores the modern software stack actually has. It lists, among others, NSS 3.131.1, Firefox 158.0.1 and ESR 115.43.1 / 153.5.1, the curl CA bundle dated 2026-10-20, Python certifi 2026.10.19, pip 26.2.2, uv 0.13.4, Rust rustls-webpki 0.103.16 and webpki-roots 1.0.10, Electron 44.9.1 / 43.8.1 / 42.11.16, Composer ca-bundle 1.4.16 and 1.6.0, Elixir castore 1.0.22, Alpine ca-certificates 20261020-r0, Chainguard 20260909-r3, Debian and Ubuntu packages — and Go 1.27.3 and 1.26.10. Node.js, Bun, Fedora and FreeBSD were still listed as pending when we reviewed the page.

Each of those is a separate shipment, a separate upgrade decision, and a separate fleet to redeploy. The macOS note is the most instructive single item on the page: macOS itself blocks the compromised intermediates, but the bundled /usr/bin/curl verifies against /etc/ssl/cert.pem without checking revocation, and that file does not get updated. An operator who patched the operating system and stopped there still has a vulnerable HTTPS client in $PATH.

Go's design decision looks different in this light. We criticised it yesterday for being compiled in — a Go binary only stops trusting these chains after a rebuild with a patched toolchain, so upgrading the build host changes nothing about running services. That remains the operational cost, and it is real. But set against a trust-store landscape where the fix has to be shipped forty separate times and some of those shipments are still pending days later, hard-coding the hashes is what let Go's block land on 19 October, before the CA was named, without waiting for anybody's certificate bundle. The tradeoff is reach versus latency, and Go bought latency.

What to do

The short version: this is a rebuild-and-redeploy problem, not a apt upgrade problem, and the inventory question is “which of my processes carries its own CA bundle?”

  • Rebuild Go services with 1.27.3 or 1.26.10 and redeploy. Container images pinning an older toolchain stay on the old policy silently and indefinitely.
  • Patch the language-level bundles separately from the OS: certifi, webpki-roots, ca-bundle, castore, Electron. A host-level ca-certificates update does not reach any of them.
  • Test rather than assume. The disclosure site serves live endpoints chaining through the compromised intermediates; a client that errors is a client that is protected. Point your SDK's HTTP stack, not just your browser, at one.
  • Treat the code-signing pair as its own workstream. CT never covered them, and installer trust paths are rarely in the same inventory as TLS trust paths.
  • Resist the root-removal temptation. The disclosure's manual update-ca-trust recipes distrust roots, including Sectigo's widely used AAA Certificate Services. That is an expert measure with a real outage radius; prefer shipping the patched bundles.

The broader lesson is not about eMudhra. It is that the Web PKI's response machinery — OneCRL, CRLite, CRLSets — worked in three hours for the browsers and is still, days later, propagating through everything else. Every agent framework, SDK and runtime that bundles its own roots has quietly opted out of the fast path and into a software-update path, and most of their operators have never thought of those bundles as security-critical inventory. This is the same structural gap we flagged in the OpenTelemetry empty-certificate-variable bug and in the ccTLD registry hijacks that produced unauthorised Google certificates: verification either means something or it does not, and the difference is invisible until someone publishes a list.

One more thing is worth saying plainly. The root cause here is not an exotic failure. A publicly trusted certificate authority generated eight CA signing keys as small integers and shipped them, and no part of the issuance pipeline — not eMudhra's own controls, not the cross-signing partner, not any audit — noticed for eighteen months. The check that would have caught all eight is to confirm that a freshly generated public key is not a small multiple of the generator. It is a handful of lines. Nobody ran it.

Our verification was read-only and used only published artefacts. The 17 October 20:06 UTC report time, the 22:59 UTC OneCRL completion, the Firefox 158.0.1 / ESR 115.43.1 / ESR 153.5.1 releases and the NSS 3.131.1 trust-store change were read first-hand from Mozilla’s security blog. The eight intermediates, their issuance and revocation dates, revocation reason codes, Sectigo cross-signs, root chaining, the stated cause, the Certificate Transparency observation and the remediation table were read from the coordinated disclosure site. Go’s distrustedSPKIHashes map was read from src/crypto/x509/verify.go on release-branch.go1.27. The SPKI SHA-256 digests were re-derived locally from each certificate PEM rather than taken from the disclosure’s metadata, and the eight private scalars were recovered locally by scalar multiplication on secp384r1 against the published public keys. No certificate authority, relying party or third-party host was probed, and no private key material is reproduced here beyond the integers already derivable by anyone from public certificates. The CVSS-style severity framing some outlets have applied to this incident is not adopted. Brave Search was used for discovery only.

Sources: