Why were eMudhra's CA keys easy to guess?

Eight eMudhra intermediate CAs signed with P-384 private keys below 256. One key was the number 4. This post shows why a small key falls in milliseconds, how a key ceremony on certified HSMs made it, and the Go checks that stop it.

Why Were eMudhra's CA Keys Easy to Guess? Go Code and Ceremony Checks

Image: METAHEURISTIC

Alexander Myasoedov

+Alexander Myasoedov Alexander writes about the operational side of shipping production AI - agents, retrieval, evals, and the guardrails that keep them from going sideways.

Why were eMudhra’s CA keys easy to guess? Each of the eight private keys was a small integer below 256. A P-384 private key must be a random number of approximately 384 bits. The public key in each certificate is the private key multiplied by a fixed curve point. Thus an attacker can try 1, 2, 3 and so on, and compare each result with the public key. At most 255 tries find the key. Scott Helme recovered all eight keys in less than one second on a laptop.

eMudhra is an Indian certificate authority. Its explanation is a faulty script in a key ceremony on 30 April 2025. The script made a second, weak key pair on the HSM. The labels of the two key pairs were similar, and the team used the weak pair for the certificate signing requests.

Why were the keys easy to guess? The short answer

Curve
NIST P-384
A private key d is a number below the curve order, approximately 2^384. The public key is Q = d·G.
Keys
4 to 237
The eight values were 4, 56, 66, 115, 155, 156, 230 and 237. Each value fits in one byte.
Attack
Count up
Calculate 1·G, 2·G, 3·G until the result is Q. The guess count is the private key.
Cause
Ceremony script
A script made a second weak key pair. A similar label caused the wrong key to go into the CSR.
Impact
Any domain
Six keys were for TLS and two for code signing. The chains went to roots that all major browsers trusted.
Fix
Distrust
Chrome, Firefox, Apple, Microsoft, Go, Node.js and Linux distributions blocked the intermediates.

What happened?

A researcher, Wayne, examined the certificates in the Common CA Database (CCADB). He found that he could recover eight private keys. He signed the message “We have a problem here - Wayne” with each key. Then he sent the proof to Apple, Google, Microsoft, Mozilla and eMudhra on 17 October 2026. Dennis Jackson from Mozilla published the disclosure site on 20 October.

Date (UTC)Event
30 Apr 2025Key ceremony for the G3B and G3C intermediates on Utimaco HSMs.
14 May 2025Sectigo cross-signs the eight intermediates.
Jun 2025Six CRLs have incorrect signatures. eMudhra signs them again (bug 1970728).
17 Oct 2026, 18:05Chrome gets the report. At 19:45, Chrome blocks the six TLS intermediates.
17 Oct 2026, 22:59Mozilla completes the OneCRL update.
18 Oct 2026, 09:38eMudhra revokes the eight intermediates.
19 Oct 2026Apple platforms and Android block the intermediates.
20 Oct 2026Microsoft blocks them. Go releases a fix. Mozilla makes the incident public.

The certificates were valid for approximately 17 months before the report. Mozilla and Cloudflare say that they found no malicious use. Nobody can prove that nobody used the keys in those 17 months.

One of eMudhra’s certificate authority private keys is the number 4.

Scott Helme, Eight bits of trust, October 2026

Why is a small private key easy to guess?

An elliptic curve key pair has two parts. The private key is a number d. The public key is a point Q on the curve. To get Q, you add the fixed point G to itself d times. The certificate contains Q, thus each person who has the certificate has Q.

The security of the key comes from the size of d. To find d from Q, the attacker must try values of d. If d is random in the range of P-384, there are approximately 2^384 values. If d is below 256, there are 255 values. The attacker starts at G, adds G at each step, and stops when it gets to Q.

Figure - Walk G, 2G, 3G until you land on the public key (toy curve)
private key d
A toy curve, y² = x³ + 2x + 3 over the integers mod 263, with 270 points. The red ring is the public key Q = d·G. Press Walk and watch the attacker add G to itself, one guess at a time, until the point lands on Q. The key values are the eMudhra values from Scott Helme's table. On P-384 a random d needs about 2384 guesses; these needed at most 237.

The figure uses a small toy curve, because the real P-384 points do not fit on a screen. The procedure is the same on P-384. This Go function does the attack against a certificate. It uses only the standard library:

// scalar returns d as a 48-byte big-endian P-384 private key.
func scalar(d uint64) []byte {
	b := make([]byte, 48)
	binary.BigEndian.PutUint64(b[40:], d)
	return b
}

// recoverSmallKey tries d = 1, 2, 3, ... until d*G equals the public key in the certificate.
func recoverSmallKey(cert *x509.Certificate, limit uint64) (uint64, bool) {
	pub, ok := cert.PublicKey.(*ecdsa.PublicKey)
	if !ok {
		return 0, false
	}
	target, err := pub.ECDH()
	if err != nil {
		return 0, false
	}
	want := target.Bytes()
	for d := uint64(1); d < limit; d++ {
		k, err := ecdh.P384().NewPrivateKey(scalar(d))
		if err != nil {
			continue
		}
		if bytes.Equal(k.PublicKey().Bytes(), want) {
			return d, true
		}
	}
	return 0, false
}

I did not use the real eMudhra certificates. I made a toy CA certificate for each of the eight values with ecdsa.ParseRawPrivateKey. Then I ran the function against each certificate. This is the output on my laptop with Go 1.27.1:

d=4   recovered=4   ok=true in 1.217ms
d=56  recovered=56  ok=true in 8.118ms
d=66  recovered=66  ok=true in 8.445ms
d=115 recovered=115 ok=true in 14.983ms
d=155 recovered=155 ok=true in 20.473ms
d=156 recovered=156 ok=true in 19.203ms
d=230 recovered=230 ok=true in 24.361ms
d=237 recovered=237 ok=true in 25.305ms
random key recovered in 65536 tries: false

The time increases with d, because each try is one more scalar multiplication. A correctly generated key did not fall in 65,536 tries. It will not fall in any number of tries that a computer can do.

The private key is the guess count The attack does not break P-384. The curve, the signature algorithm and TLS work correctly. Only the choice of d was wrong, and d is the one value that must be random.

How small is too small?

A random P-384 key gives approximately 192 bits of security. The best general attacks, such as Pollard’s rho, need approximately the square root of the key range. A key below 2^k is weaker. Baby-step giant-step finds it in approximately 2^(k/2) steps. Thus a 64-bit key falls in approximately 2^32 steps, and a laptop can do that.

The eMudhra keys did not need a clever algorithm. A key below 256 falls to the simplest loop. The comparison below shows the two cases:

Correct P-384 key

  • d is random in [1, n-1], n is approximately 2^384
  • Approximately 2^192 operations for the best attack
  • No practical attack

eMudhra keys

  • d is in [4, 237]
  • At most 255 scalar multiplications
  • Less than one second on a laptop

How did a ceremony on certified HSMs make weak keys?

eMudhra gave its account in Mozilla bug 2083449. The keys came from one ceremony on 30 April 2025. The HSMs were Utimaco devices with FIPS 140-2 Level 3 validation. The team used an approved deviation from the usual ceremony script. The deviation was a test of key wrap and unwrap, because these keys were for export.

The script made the correct key pair on the HSM. Then it also made a second, weak key pair on the HSM. The two pairs had similar labels. The team selected the weak pair to make the certificate signing requests (CSRs), and sent the CSRs to Sectigo for the cross-signature. eMudhra also said that human errors occurred in more than one step.

The HSM validation applies to the device. It does not apply to the script that tells the device what to do. A script can ask a correct HSM to import or make a key with a value that a person selected. The HSM does what the script tells it to do.

There was an earlier signal. In June 2025, six CRLs from these intermediates had incorrect signatures. eMudhra says that the CRLs were signed with the original keys first. Because those signatures did not verify, the CRLs were signed again with the weak keys. The weak keys matched the certificates, and the correct keys did not. Nobody asked why at that time.

Labels are not keys A label is a name that a person types. Two keys can have similar names. Only the public key shows which key is in the CSR.

What do people get wrong about the keys?

The r/technology thread about this incident had three common errors. Each error makes the failure look simpler or stranger than it was.

Error
"Somebody typed 4"
eMudhra's account in the Mozilla bug is a script that made a second key pair. Nobody selected the value 4 by hand, and the HSMs have hardware RNGs.
Error
"It is an RSA key"
RSA uses two large primes p and q. These are P-384 ECDSA keys. Here the private key is one number d, and the script made d small.
Error
"The public key showed it"
4·G looks like any other point on P-384. The public key, the file size and the HSM metadata all look normal.

The third error is the important one. In a correct ceremony, no person sees the private key. The HSM shows a key name, a key ID and a public key. A public key for d = 4 has the same length and the same random look as a public key for a random d. Thus a review of the ceremony output cannot find the problem by eye. Only a test finds it: count up from 1, or check the bit length inside the HSM.

The thread also had a rumor: the team read “384-bit key” as “a number from 1 to 384”. The values do not support this rumor, because they are all below 256, and eMudhra gives a different cause. One commenter gave a better summary. The process had no final entropy check. Also, the team continued to use the keys after the CRL signatures failed.

How do you check a key before it signs?

Two checks find this failure before a CSR leaves the ceremony. Each check is a few lines of Go.

The first check examines the size of the private key. For a random 384-bit scalar, the probability that the top 64 bits are all zero is 2^-64. If a key has this property, the key generation is wrong:

// weakScalar reports a private key whose top 64 bits are all zero.
func weakScalar(key *ecdsa.PrivateKey) (int, bool) {
	b, err := key.Bytes()
	if err != nil {
		return 0, true
	}
	bits := new(big.Int).SetBytes(b).BitLen()
	return bits, bits <= 384-64
}

This check needs the private key, thus it runs inside the ceremony. An HSM does not export the private key. In that case, run the second check, and run the recovery loop from the previous section against the public key. A loop to 2^24 takes a short time and needs no secret.

The second check compares the public key in the CSR with the public key that the ceremony recorded. It does not use the label:

csr, err := x509.ParseCertificateRequest(csrDER)
if err != nil {
	return err
}
if !ceremonyKey.Equal(csr.PublicKey) {
	return fmt.Errorf("CSR public key does not match the ceremony key")
}

In my toy test, the CSR from the weak key failed this check:

weak: 3 bits, reject=true
random: 384 bits, reject=false
CSR key equals ceremony key: false

To add these checks to a key ceremony, do these steps:

  1. Make the key pair on the HSM.
  2. Record the SHA-256 fingerprint of the public key in the ceremony log.
  3. Make the CSR.
  4. Compare the public key in the CSR with the recorded fingerprint.
  5. Run the small-key recovery loop against the public key.
  6. If a check fails, stop the ceremony. Do not send the CSR.

How do you check your own systems?

Browsers got the fix through their own updates. Other software uses its own trust store, and some of that software still trusts the intermediates. Examples are old containers, language runtimes and tools that bundle a CA file.

Go blocked the eight intermediates in crypto/x509 as CVE-2026-111943 (Go issue 82345). Verify now removes each chain that contains one of these intermediates. The fix has backports for Go 1.26 and Go 1.27. Node.js, curl, NSS, Python certifi and the major Linux distributions also have fixes.

The disclosure site has two test hosts. Use them to test a client:

curl https://dv-g3b-emudhra.we-have-a-problem.com/
curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/

A certificate error is the correct result. If curl shows the page, the client still trusts the compromised intermediates. Update the runtime or the trust store of that client. Cloudflare also gives a warning: do not disable certificate verification to stop these errors.

eMudhra says that it stopped issuance under the emSign ECC Root CA - G3 and will move approximately 30,000 customer certificates to another chain. Mozilla removed trust in that root. Google said that Chrome will do the same.

Did this attack need AI?

No. The attack is a loop of approximately 30 lines of Go. Scott Helme added a note to his post: an AI agent recovered the keys from one prompt. eMudhra wrote in the Mozilla bug that it checked its other issuers with “mathematical models facilitated by AI”.

Both statements show the same fact. The tools to find a weak key are now available to each person who asks for them. Thus a CA must find the weak key first, inside the ceremony, before it signs anything.

What to take from this Do not trust a key because of its label or the HSM model. Verify the public key in each CSR, and test it for small values before you send it.

So, why were eMudhra’s CA keys easy to guess?

Each private key was a number below 256. The public key in each certificate is that number times a known point. An attacker counts from 1 and compares, and at most 255 tries give the key. The curve and the HSMs worked correctly. A ceremony script made a weak second key pair, and a similar label sent that key into the CSRs. A size check on the key and a public-key comparison on the CSR find this error before the CSR leaves the room. For a related post about checks that run before you trust code, read how to check a Claude Code skill for malware.

Follow in Google

Make Metaheuristic a preferred source.

One tap and posts like this one surface higher in your Top Stories.

Work with us

Production AI, with guardrails.

Start with a fixed-scope AI Workflow Audit. We map the opportunity and quote a build.

Start a Discovery Sprint →