A security researcher reported on October 17, 2026 that they had recovered the private keys of eight intermediate CA certificates belonging to eMudhra, an Indian certificate authority. Every one of those keys was a number below 256. Browsers blocked the affected certificates within hours, and The Hacker News reports that eMudhra will exit the global SSL/TLS business.
What happened
According to The Hacker News, the researcher recovered the private keys of eight intermediate CA certificates, six of them for TLS. In the article's words: "Each private key should have been a huge random number, but all eight were below 256, so anyone could work them out almost instantly."
The cause has not been made public. No random number generator, seed, tool or generation method has been named. Anything below about why the keys came out this way describes the general class of bug, not what happened at eMudhra.
The timeline as reported:
- On October 17 the researcher reported the issue to Apple, Google, Microsoft, Mozilla and eMudhra.
- Chrome and Firefox blocked the six TLS intermediates within hours. Google said no website certificate issued from them appeared in Certificate Transparency logs before the block.
- On October 20 Mozilla said it would stop trusting the emSign ECC Root CA - G3 (not G1).
- Chrome will also stop trusting G3, expected on the morning of October 27, Pacific time. Chrome posted 12 questions to eMudhra on October 21 and told site operators they "must immediately acquire and install a new TLS certificate from another PKI hierarchy included in the Chrome Root Store."
eMudhra said it "never used the eight to issue certificates to websites or users", that "its systems were not breached", and that it will replace affected customers' certificates free of charge through partners. Its G1 RSA roots continue during the transition.
There is history here too. In 2025 eMudhra regenerated invalid certificate revocation lists using keys it later said were "resigned with flawed keys." Chrome said that 2025 disclosure "raises serious concerns regarding eMudhra's transparency and incident handling."
Why it worked
A private key is a secret number. For an elliptic-curve key on a common curve, it should be drawn uniformly from a range around 2^256 or 2^384. If it is below 256, there are only 255 candidates: an attacker tries each one against the public key and finds the match at once. The mathematics of the algorithm is fine; the secret just isn't secret.
Keys end up small or predictable through a short list of mistakes:
- A non-cryptographic generator. Language defaults like Python's
randomor JavaScript'sMath.random()are predictable by design. - Hand-rolled key generation. Converting random bytes to a key by hand, and getting the length, slicing or byte order wrong.
- A broken or unseeded source. A generator with almost no entropy produces a tiny set of possible keys.
- No check on the output. Nobody looks at the generated value, so a degenerate key ships.
Here is an illustrative example, not eMudhra's code and not a claim about their cause. It shows how hand-rolled generation can quietly produce a key below 256:
# Illustrative only: hand-rolled key generation (vulnerable)
import random
def new_private_key() -> int:
seed = random.getrandbits(384).to_bytes(48, "big") # predictable PRNG
return int.from_bytes(seed[:1], "big") # slicing bug: one byte, so < 256Two bugs, each enough on its own: random is not a cryptographic generator, and the slice keeps one byte. Both pass a quick test because the function still returns a number.
The fix is to not write this function at all. Let a vetted library generate the key from the operating system's CSPRNG, and add a cheap smoke test on the output:
# Illustrative only: let the library generate the key (fixed)
from cryptography.hazmat.primitives.asymmetric import ec
def new_private_key() -> ec.EllipticCurvePrivateKey:
key = ec.generate_private_key(ec.SECP384R1())
# Smoke test, not a proof of randomness: a P-384 scalar this short is a red flag.
if key.private_numbers().private_value.bit_length() < 256:
raise RuntimeError("degenerate key generated; refusing to use it")
return keyFor the most sensitive keys, such as a CA's, the key should be generated inside a hardware security module (HSM) and never leave it. The same principle applies to tokens, session ids and password-reset codes in ordinary apps: use secrets.token_urlsafe() in Python or crypto.randomBytes() in Node, never the default random module.
What to do now
If you run websites:
- Check which CA issued each of your certificates, including the intermediates in the chain your server sends. If any chain goes through the affected eMudhra hierarchy, replace the certificate with one from another CA in the Chrome Root Store before the G3 distrust date.
- Make certificate issuance automated (for example with ACME) so that switching CAs is a configuration change, not a project.
- Keep an inventory of where certificates live: load balancers, CDNs, ingress controllers, appliances. A swap is only fast if you know every place to swap.
If your code generates keys or tokens:
- Use your platform's CSPRNG (
secretsin Python,crypto.randomBytes/crypto.getRandomValuesin JavaScript,crypto/randin Go). NeverrandomorMath.random()for anything secret. - Generate keys with a vetted library call, not by converting random bytes yourself.
- Use current algorithms and key sizes (for example P-256/P-384 or RSA 3072+) and drop obsolete ones.
- Add tests that reject degenerate output: key length, value range, and that two generated keys differ.
- Keep high-value signing keys in an HSM or a cloud KMS so the key material is never handled by application code.
How OpenRouting helps
What happened at eMudhra was a CA-side failure. No scan of a customer's repository or website would have found it. What OpenRouting can check is the same class of mistake in your own code, and the HTTPS basics of your own sites.
- Semgrep crypto rules in a repository scan flag non-cryptographic randomness such as
randomorMath.random()used for tokens or keys, weak or obsolete algorithms, and small key sizes. - The cryptography agent reviews those findings with the surrounding code. AI triage, by Claude, via Amazon Bedrock, gives each one a verdict, a plain-English explanation and a suggested fix, such as swapping a hand-rolled generator for a library call. Secret values are redacted before any AI call.
- PR review runs the same checks on the files a pull request changes, so a new token generator built on
Math.random()is flagged before merge. - A passive website scan checks HTTPS hygiene on your site, such as HSTS and mixed content. It sends no attack payloads.
What it won't catch
- OpenRouting does not inspect a CA's private keys, and it does not tell you which CA issued your certificate chain. Check that with your CA, your hosting provider or a TLS inspection tool.
- It scans your repositories and, passively, your websites. It is not a monitor of browser root programs or CA trust decisions.
- Static rules catch the obvious patterns (wrong generator, weak algorithm, small key size). A subtle bug like the slicing example above may need a human, or the cryptography agent's review, to notice; and a broken entropy source on a machine is invisible to a code scan.
- Findings appear when a scan runs, from the dashboard or on a pull request review.
For more on passive checks and their limits, see passive vs active web scanning.