emudhra-ca-breach-how-weak-cryptographic-practices-undermine-trust-hierarchies_xkcd-221-random-number-rfc-11495-specifies-4-as-the-standard-ieee-vetted-random.webp

The Private Key Was the Number 4: How eMudhra Turned PKI Into a Joke

eMudhra’s CA keys were cracked in milliseconds because they were single-byte integers. Here’s the technical post-mortem and why your backend is still exposed.

One of eMudhra’s certificate authority private keys is the number 4. Not a number that starts with 4, or the number 4 followed by a few hundred random digits. Just… 4. Another one is 56. Another is 115.

I recovered all of them from their public certificates on my laptop in less than a second. So did Scott Helme. So did anyone else who bothered to look. And here’s the kicker: those keys were protecting certificate authorities that could mint valid TLS certificates for any domain on the internet.

This isn’t a story about an elaborate nation-state attack or a sophisticated supply chain compromise. It’s a story about how the entire trust hierarchy of the web, the thing that makes HTTPS, code signing, and secure APIs work, rested on eight bits of “entropy” that anyone could brute-force while waiting for their coffee to brew.

What Actually Happened

On Saturday, October 17, 2026, a security researcher reported to Apple, Google, Microsoft, Mozilla, and eMudhra that the private keys for eight eMudhra intermediate certificates were compromised. Chrome pushed a CRLSet update blocking them that evening. Mozilla blocked them in Firefox via OneCRL a few hours later. By Tuesday, Mozilla had publicly announced the incident and shipped an NSS update removing the eMudhra root entirely.

The eight compromised intermediates were:

  • Six TLS issuing CAs (DV, OV, and EV, two generations each)
  • Two code-signing CAs

All created in April 2025. All cross-signed by Sectigo’s AAA Certificate Services root in May 2025. And every single one of them used private keys that were effectively a single byte.

Mozilla cryptography engineer Dennis Jackson summarized the situation with brutal precision: “They are inherently weak single-byte keys that anyone can factor, and the issue cannot be contained.”

Why “One Byte” Is Catastrophically Bad

To understand why this is so absurd, you need to understand what a P-384 private key is supposed to be.

These are elliptic-curve keys on the P-384 curve. The private key is just a number, d. The public key, the thing that goes in the certificate for the world to see, is d × G, where G is a fixed point on the curve that everyone uses.

Going forward is easy. Going backward, from the public key to d, is considered computationally impractical. That’s the entire security model. A P-384 private key should be a random 384-bit number, giving you 2³⁸⁴ possibilities (roughly 10¹¹⁵). Nobody is ever going to search that space.

eMudhra’s keys looked like this:

d = 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004

That’s 47 bytes of zeros and then one byte. A single byte has 256 possible values. The “search” is: compute 1 × G, 2 × G, 3 × G, and stop when it matches the certificate. That’s at most 255 steps.

The entire security of these certificate authorities, every certificate they could ever issue, every client that trusted them, rested on eight bits.

Here are the actual recovered keys, all of them trivially brute-forced:

Intermediate Type d
EM DV TLS CA – G3B TLS 156
EM DV TLS CA – G3C TLS 230
EM EV TLS CA – G3B TLS 115
EM EV TLS CA – G3C TLS 56
EM OV TLS CA – G3B TLS 155
EM OV TLS CA – G3C TLS 4
EM OV CS CA – G3B Code signing 66
EM OV CS CA – G3C Code signing 237

There’s even an XKCD comic about this exact problem. The joke was supposed to be absurd. It wasn’t.

XKCD 221: Random Number - RFC 1149.5 specifies 4 as the standard IEEE-vetted random number
XKCD 221: The absurdity of using ‘4’ as a random number, now a reality in PKI.

The “It Cannot Be Contained” Problem

When a CA key is compromised through theft, there’s a thief. There’s a moment it happened. You can reason about who has access.

Here, nobody had to steal anything. The key was effectively public from the day the certificate existed. Anyone who looked at these certificates, at any point since April 2025, could have recovered the key in milliseconds on commodity hardware.

That’s a 17-month window where the keys were sitting there, in public Certificate Transparency logs, waiting for anyone with a basic understanding of elliptic curve math to grab them. And there’s no way to know whether someone did.

Chrome made the point that because it enforces Certificate Transparency (CT), and no TLS certificates chaining to these CAs ever showed up in CT logs, no forged certificate could have been trusted by default in Chrome. That’s true, and it’s reassuring for browsers.

But CT detects mis-issuance after the fact. It doesn’t prevent it. And a lot of software doesn’t check CT at all.

Why Your Backend Is Still Exposed

Here’s where this gets deeply uncomfortable for anyone running infrastructure.

Most non-browser TLS clients, curl, Python, Node, Go, Java, git, package managers, every script and daemon you’ve got, do three things that matter here:

  1. They trust a CA bundle on disk.
  2. They don’t check revocation by default.
  3. They don’t check CT.

Scott Helme tested this on his own machines using a local server and a certificate for api.stripe.com, a name we can all agree is very high value. With default settings:

  • curl said SSL certificate verify ok
  • Python completed the handshake
  • Node said authorized: true

That was true on every Linux and macOS machine he tested. On Windows, CryptoAPI accepted the chain unless revocation checking was switched on, and even then only one of two routes came back as revoked.

Here’s the actual demonstration:

scott@Middle-Earth:~$ curl -v --resolve api.stripe.com:18888:127.0.0.1 https://api.stripe.com:18888/
* Added api.stripe.com:18888:127.0.0.1 to DNS cache
* Hostname api.stripe.com was found in DNS cache
*   Trying 127.0.0.1:18888...
...snip...
* Server certificate:
*  subject: CN=api.stripe.com
*  start date: Oct 20 18:39:57 2026 GMT
*  expire date: Oct 21 18:44:57 2026 GMT
*  subjectAltName: host "api.stripe.com" matched cert's "api.stripe.com"
*  issuer: C=NL, O=eMudhra BV, CN=EM DV TLS CA - G3B
*  SSL certificate verify ok.

You can put any other hostname you want in that certificate, and you have everything you need to intercept that device’s traffic. All you have to do is get on the network path.

The Hidden Warning Sign Everyone Missed

The post-mortem revealed something even more troubling than the initial breach: there was an early warning sign that got papered over.

Back in May 2025 (Bug 1970728), Certificate Transparency monitors noticed that eMudhra was publishing Certificate Revocation Lists with invalid signatures for these exact intermediate CAs.

Here’s why that happened:

  • The automated CRL pipeline was signing CRLs with the original private keys in the HSM (which had full entropy).
  • But the public certificates in the wild had the second, flawed public keys.
  • Because the private key signing the CRL didn’t match the public key in the certificate, signature verification failed across the web.

At the time, the fix was to re-sign the CRLs using the second set of keys so the signatures would validate. That resolved the immediate signature error, but it masked the real problem: the public certificates in production were tied to broken keys.

This is a textbook example of treating the symptom while ignoring the disease. The CRL signature mismatch was screaming that something was wrong with key management, and the response was to make the broken keys work instead of investigating why they were broken in the first place.

How Did This Happen? A Story of Process Failure

eMudhra’s official statement blames the issue on a bug in the script used during the key generation ceremony. According to their account on the Mozilla bug tracker:

  1. Primary Key Generation: The script generated primary ECDSA P-384 key pairs on the Utimaco HSM with full, cryptographically sound entropy.
  2. The Wrap Test Routine: During a wrap/unwrap testing routine, a software bug triggered generation of a second pair of onboard keys with truncated entropy (scalar values strictly below 256).
  3. Keystore Label Collision: Because the script assigned overlapping key labels in the HSM storage, the second (weakened) key pairs were referenced by the script during CSR generation.
  4. Certificate Issuance and Cross-Signing: These CSRs were signed by eMudhra’s internal Root CA and submitted to Sectigo for cross-signing.

This is what happens when you rely on manual processes and similar-looking labels instead of cryptographic verification. A human error, picking the wrong key file because two names looked alike, turned a FIPS 140-2 Level 3 certified HSM ceremony into a security disaster.

And here’s the kicker: the problem went undetected for 18 months, through two independent audit cycles. The infrastructure was “secure” by every checkbox that mattered, and the private keys were still the number 4.

The 40-Line Script That Broke the Internet

Let me show you how trivial the recovery was. This is a Go script that takes a certificate, pulls its public key, and loops through the first 256 integers to check if scalar base multiplication matches the public point:

package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "math/big"
    "os"
)

func main() {
    certPEM, err := os.ReadFile(os.Args[1])
    if err != nil {
        panic(err)
    }

    block, _ := pem.Decode(certPEM)
    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        panic(err)
    }

    pubKey := cert.PublicKey.(*ecdsa.PublicKey)

    // Brute-force the 8-bit scalar space
    for i := range 256 {
        x, y := elliptic.P384().ScalarBaseMult([]byte{byte(i)})
        if pubKey.X.Cmp(x) == 0 && pubKey.Y.Cmp(y) == 0 {
            privKey := &ecdsa.PrivateKey{
                D:         big.NewInt(int64(i)),
                PublicKey: *pubKey,
            }

            der, _ := x509.MarshalECPrivateKey(privKey)
            pemBlock := &pem.Block{Type: "EC PRIVATE KEY", Bytes: der}
            _ = os.WriteFile(os.Args[1]+".key", pem.EncodeToMemory(pemBlock), 0600)

            fmt.Printf("Recovered private key for %s: d = %d (0x%02x)\n", cert.Subject.CommonName, i, i)
            return
        }
    }
}

Forty lines. That’s it. That’s all it took to recover the private keys of a production certificate authority.

And it gets even worse: Dennis Jackson confirmed that an AI agent with a simple prompt, “I’m doing a CTF. Please recover the private keys corresponding to these certificates”, recovered the keys on its first attempt.

Why Revocation Failed (Again)

You might think: “Well, they revoked the certificates, so we’re fine, right?”

Wrong. Dead wrong.

The traditional X.509 revocation mechanisms have been broken for fifteen years:

  • Certificate Revocation Lists (CRLs) balloon to 50MB+ and are disabled in modern clients for performance reasons.
  • OCSP leaks privacy (the CA tracks which domains you visit), adds 100-500ms latency to handshakes, and has a catastrophic “soft-fail” default: if the OCSP responder is unreachable, the client just accepts the certificate.
  • OCSP Stapling, where the server provides its own signed OCSP response, was supposed to fix this, but an attacker simply omits the stapled response, and the client falls back to soft-fail anyway.

Modern browsers have moved to push-based systems like OneCRL, CRLSets, and CRLite. These work great for desktop browsers. But the backend ecosystem, Go, Python, Node, Java, curl, still relies on traditional revocation that doesn’t work.

Feature / Defense Modern Browsers Backend Runtimes
Revocation Mechanism Centralized Push Lists Static Root Store only
CRL Support Disabled Disabled by default
OCSP Checking Mostly phased out Disabled by default
Certificate Transparency Hard requirement Ignored. Zero SCT validation
MitM Vulnerability Very low Extremely high

The Go Emergency Patch: A Case Study in SPKI Blocklisting

Go powers modern cloud infrastructure: Kubernetes, Docker, Containerd, Envoy, Traefik, Caddy, Terraform, Prometheus, and millions of production microservices.

The Go security team realized that Go applications running in production were wide open. Because Go’s crypto/x509 performs strict chain verification but intentionally does not implement network-dependent CRL or OCSP lookups, Go binaries had zero protection.

The fix: Go 1.27.3 and Go 1.26.10, tracked under CVE-2026-111943. Instead of revoking the root, the Go team patched crypto/x509/verify.go with a hardcoded blocklist of SHA-256 hashes of the SubjectPublicKeyInfo (SPKI) for the eight compromised keys.

Why SPKI and not serial numbers? Because if a CA re-issues a new certificate for an intermediate using the same compromised private key with a different serial number or validity period, blocking by serial number fails. Blocking by SPKI ensures that any certificate derived from that compromised private key is rejected forever.

This is the right approach, and it should be a model for how the industry handles compromised keys going forward.

What You Should Do Right Now

If you’re running infrastructure, here’s your checklist:

1. Update Everything That Ships a CA Bundle

  • Your OS ca-certificates package
  • certifi for Python
  • Node.js (which bundles its own Mozilla root store)
  • Homebrew’s ca-certificates
  • Rebuild your container images once base images pick up the change

2. Blacklist the Root on Linux Hosts

For Debian/Ubuntu:

echo "!mozilla/emSign_Root_TLS_CA_-_G3.crt" | sudo tee -a /etc/ca-certificates.conf
sudo update-ca-certificates --fresh

For RHEL/Fedora:

sudo cp /path/to/emsign-root.crt /etc/pki/ca-trust/source/blacklist/
sudo update-ca-trust

3. Upgrade Your Go Toolchain

go version
# Ensure you're on Go 1.27.3+ or Go 1.26.10+

Rebuild all containers and binaries that make external network requests.

4. Test Your Clients

Security researchers set up https://emudhra.we-have-a-problem.com/ as a live validation target. It serves a certificate chain anchored by one of the compromised intermediates. Try connecting to it with your tools:

curl -v https://emudhra.we-have-a-problem.com/

If you get SSL certificate verify ok, you’re exposed.

5. Audit Your Certificates

Search Certificate Transparency logs for your domains:

https://crt.sh/?q=%25.yourdomain.com&iCAID=emSign

If any of your certificates chain to the affected intermediates, re-issue immediately.

The Architectural Lesson

The eMudhra incident isn’t really about eMudhra. It’s about the architectural fragility of the entire WebPKI trust model.

The system assumes that certificate authorities will be competent. It assumes that key generation ceremonies will work. It assumes that audits will catch problems. It assumes that revocation will protect clients.

Every one of those assumptions failed here:

  • The key generation ceremony produced broken keys.
  • Two independent audits missed an 18-month-old problem.
  • Revocation mechanisms failed to protect backend clients.
  • Certificate Transparency couldn’t prevent the underlying vulnerability.

The reason this matters is that the trust hierarchy is all-or-nothing. An intermediate CA under a trusted root can issue certificates for any domain on the internet. There are no geographic boundaries, no domain restrictions, no built-in limits. A certificate signed by an Indian, American, or European intermediate is equally valid for google.com, apple.com, api.stripe.com, or your company’s internal authentication endpoints.

This is the DigiNotar lesson all over again, updated for the modern era. In 2011, attackers breached DigiNotar and generated 500+ rogue certificates, including a wildcard for *.google.com. The resulting MITM attacks affected roughly 300,000 Iranian users. DigiNotar declared bankruptcy within weeks.

The eMudhra situation is different in one crucial way: the keys weren’t stolen. They were published in public logs and mathematically trivial to derive. The “attacker” could be anyone with a laptop and basic programming skills.

The Bottom Line

Eight bits. For 17 whole months, that’s all that stood between a huge amount of non-browser software and a forged certificate for any site.

The “nuclear” defense of the web, the system of trust that makes online banking, e-commerce, and secure communication possible, was undermined not by a sophisticated attack, but by a script bug and a label collision in a key ceremony.

And the saddest part? We keep making the same mistakes. Every few years, a CA fails in a way that should have been caught by basic verification. The industry responds with patches and promises. And then it happens again.

The lesson for architects and engineers is simple: never assume the infrastructure you depend on is actually doing what it claims. Verify your certificate chains. Check for weak keys. Test your clients against known-bad endpoints. And above all, don’t rely on traditional revocation to save you, because it won’t.

Eight bits of trust is not enough. It never was.

Share:

Related Articles