eMudhra's CA keys were below 256: keys that aren't random

$ cat blog/emudhra-guessable-ca-keys.md
✓ incident
✓ cryptography
✓ TLS
✓ SAST
6 min read
$ 

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 random or JavaScript's Math.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 < 256

Two 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 key

For 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.

attack, as reported where OpenRouting checks 1 CA generates intermediate CA keys✓ In your code: crypto rules 2 Eight private keys are all below 256cause not public 3 Researcher recovers the keysCA-side, outside any scan 4 Browsers block the six TLS intermediatesbrowser root programs 5 Sites must move to another CA✓ HTTPS hygiene: passive scan
The attack as reported, and where an OpenRouting check applies. Illustrative.

What to do now

If you run websites:

  1. 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.
  2. Make certificate issuance automated (for example with ACME) so that switching CAs is a configuration change, not a project.
  3. 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:

  1. Use your platform's CSPRNG (secrets in Python, crypto.randomBytes / crypto.getRandomValues in JavaScript, crypto/rand in Go). Never random or Math.random() for anything secret.
  2. Generate keys with a vetted library call, not by converting random bytes yourself.
  3. Use current algorithms and key sizes (for example P-256/P-384 or RSA 3072+) and drop obsolete ones.
  4. Add tests that reject degenerate output: key length, value range, and that two generated keys differ.
  5. 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 random or Math.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.

Sources

Questions

What happened with eMudhra's CA keys?

A researcher reported on October 17, 2026 that they had recovered the private keys of eight eMudhra intermediate CA certificates, six of them for TLS, because every key was a number below 256. Chrome and Firefox blocked the six TLS intermediates within hours, and The Hacker News reports that eMudhra will exit the global SSL/TLS business.

Why were eMudhra's private keys below 256?

The cause has not been made public: no random number generator, seed, tool or generation method has been named. In general, keys come out small or predictable through non-cryptographic generators, hand-rolled key generation, a broken or unseeded entropy source, or no check on the output.

Do I need to replace my TLS certificate after the eMudhra distrust?

If any certificate chain your servers send goes through the affected eMudhra hierarchy, replace it with a certificate from another CA in the Chrome Root Store before the G3 distrust date. Chrome told site operators they must immediately acquire and install a new TLS certificate from another PKI hierarchy.

How do I generate keys and tokens safely?

Use your platform's CSPRNG (secrets in Python, crypto.randomBytes or crypto.getRandomValues in JavaScript, crypto/rand in Go), never random or Math.random() for anything secret. Generate keys with a vetted library call, test the output for degenerate values, and keep high-value signing keys in an HSM or a cloud KMS.

Keep reading