By Dor Dankner, Co-founder & CTO, Anchor Browser

For almost two decades, xkcd 221 has been the internet's favorite joke about bad randomness. This week it turned into an incident report. One of the private keys behind eight publicly trusted eMudhra (emSign) intermediate CA certificates was, as commenters on Hacker News quickly worked out, literally the number 4. The others were not much better. As the Python advisory thread puts it, all eight had private keys with d < 256. A P-384 private key is supposed to be a random number about 384 bits long. These keys fit in a single byte.
So anyone could recompute them, and anyone holding them could sign a certificate for any website that clients would accept.
What happened
According to Google's Chrome and Android security teams, the Chrome Root Program got a report from a security researcher on Saturday, October 17, 2026. The report said the private keys of eight eMudhra subordinate CA certificates had been "generated with insufficient randomness." That same day, Chrome had confirmed the problem and pushed a CRLSet update that blocks the affected intermediates by their Subject Public Key Info (SPKI) hash. Android got an equivalent update through Google Play Services. Google is also removing the root that signed them, emSign ECC Root CA - G3, from the Chrome Root Store. That change ships October 27.
The public disclosure page fills in the rest. The reporter proved the compromise by signing a message with every one of the keys. Firefox and Chrome have blocked the certificates since October 17, Apple since the 19th, and Microsoft since the 20th. Six of the intermediates issue TLS certificates and two issue code-signing certificates. Each one is also cross-signed by Sectigo's AAA Certificate Services, so the same weak keys show up under two different roots.
The good news is that no certificates chaining to these intermediates were logged to Certificate Transparency (CT) before the block. That means CT-enforcing browsers could not have been attacked. The bad news, in the disclosure page's own words: "many clients outside of the browser ecosystem do not enforce Certificate Transparency. We do not know if attackers exploited the flawed key generation to target these clients." The weak certificates have existed since April 2025.
To show how low the bar is: mitmproxy merged a --emudhra option that signs interception certificates with the d=4 key. Against any client that still trusts the chain, that gives you working, warning-free TLS interception. (In a companion change, mitmproxy also updated its bundled certifi to drop the eMudhra CAs.) The patch is open source and anyone can run it.
Why this matters for browser agents
Your agent trusts whatever its browser trusts. When an agent logs into a vendor portal, submits a form, or reads a dashboard, TLS is the only thing standing between it and an impostor. Agents don't squint at the padlock or notice a strange redirect. If the certificate validates, they type in the credentials.
A cloud browser fleet is not desktop Chrome. Chrome's fast fix arrived as a CRLSet, which is a component update. Automation stacks commonly launch Chromium with component updates disabled (Playwright and Puppeteer both pass --disable-component-update by default). Container images often pin a Chromium build, which freezes its root store and CT log list at build time. Chromium is also built to stop enforcing CT once its log list goes stale, so a pinned build that's old enough loses that safety net too. And the root removal on October 27 only reaches browsers that actually update.
The browser isn't your only TLS client. Agent platforms also run Python workers, Node services, Go sidecars, and curl | sh setup scripts. Each one may carry its own trust bundle. The Python thread warns that pip versions older than 26.2.2 keep trusting their vendored certifi bundle no matter what the OS trust store says.
Proxies sit in the middle by design. Agent traffic often goes through third-party egress or residential proxies. An attacker needs to be in exactly that position to use a forged certificate.
And if you run with ignoreHTTPSErrors: true, none of this affects you, because your agents already trust everyone. Turn it off.
How we handled it at Anchor
We got to work as soon as we heard about it and had a fix in production shortly after.
Anchor sessions run on our own Chromium build, and that build doesn't receive Chrome's CRLSet updates. So out of the box it still trusted the compromised intermediates, which is exactly the gap described above. We closed it with Chromium's own enterprise policy mechanism instead of waiting for a new root store. We added a managed CADistrustedCertificates policy to the browser image that explicitly distrusts emSign ECC Root CA - G3 (SHA-256 86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B). That's the root the compromised intermediates chain to in Chromium's root store, and the same one Chrome is removing on October 27. We're just getting there early.
{
"CADistrustedCertificates": [
"MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAK... // base64 DER of emSign ECC Root CA - G3"
]
}On Linux, Chromium reads managed policies from JSON files in /etc/chromium/policies/managed/, so if you build your own browser images you can do the same thing. The trade-off is deliberate: legitimate sites that still chain to this root fail TLS in Anchor sessions until they get a new certificate. That's the same breakage Chrome users will see after the root is removed.
The policy ships inside the browser image, so it applies to every session launched on the updated image. The change involved no SDK or API changes.
We verified the fix against the disclosure site's test hosts (the ones in the checklist below). With the policy in place, the browser fails the TLS handshake against the compromised-chain hosts, and the site's "Run check", which also covers the Sectigo cross-signed variants, passes.
How to protect your own stack
If you run your own browsers or agent infrastructure, here's the checklist we'd give a friend.
1. Update the browser and let it update itself. Move to a current Chrome or Chromium. If you pin a version, bump it, and plan to bump again after the October 27 root removal. Where your setup allows, stop disabling the component updater so CRLSets and CT log lists keep flowing.
2. Update OS trust stores and rebuild your images. The disclosure page lists patched versions. Examples include NSS 3.131.1, Alpine ca-certificates 20261020-r0, and updated Debian, Ubuntu, Fedora, and Arch packages. A patched package does nothing for an image built last month, so rebuild. Then restart long-running processes, because most of them load trust at startup.
3. Update language runtimes and bundled stores. Python: certifi 2026.10.19, pip 26.2.2, and uv 0.13.4 (Python advisory). The disclosure page also lists patched Go (1.27.3 / 1.26.10), Node.js (22.23.4 / 24.22.1 / 26.12.1), Java, rustls-webpki, and Electron releases.
4. Distrust keys, not names. This is the important conceptual point. The cross-signs reuse the same key pairs, so blocking one certificate or one root name leaves gaps. Chrome blocked by SPKI hash. According to the Python thread, Go shipped the same kind of SPKI denylist, and a matching patch has been proposed for CPython's ssl module. If you control certificate verification anywhere in your stack, you can add a defense-in-depth check yourself: after normal validation, hash every certificate's SubjectPublicKeyInfo and reject the chain if any hash is on the list.
import hashlib
from cryptography import x509
from cryptography.hazmat.primitives import serialization
# SHA-256 SPKI hashes of the six TLS intermediates (each also cross-signed by Sectigo).
# Source of truth: https://emudhra.we-have-a-problem.com/impacted-intermediates.json
BLOCKED_SPKI_SHA256 = {
"EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105", # EM DV TLS CA - G3B
"43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B", # EM OV TLS CA - G3B
"E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41", # EM EV TLS CA - G3B
"49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F", # EM DV TLS CA - G3C
"63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8", # EM OV TLS CA - G3C
"5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0", # EM EV TLS CA - G3C
}
def chain_uses_blocked_key(chain_der: list[bytes]) -> bool:
for der in chain_der:
spki = x509.load_der_x509_certificate(der).public_key().public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
if hashlib.sha256(spki).hexdigest().upper() in BLOCKED_SPKI_SHA256:
return True
return False(On Python 3.13+, SSLSocket.get_verified_chain() gives you the chain to check.) If you maintain custom CA bundles and can live with some breakage, you can also drop emSign ECC Root CA - G3 now rather than waiting for upstream.
5. Verify with a real test. The disclosure page runs test hosts that serve certificates from the compromised chains. A patched client should refuse to connect:
curl https://dv-g3b-emudhra.we-have-a-problem.com/ # expect a certificate errorRun the same check from inside your browser sessions, with HTTPS errors not ignored:
const page = await context.newPage();
const result = await page
.goto("https://dv-g3b-emudhra.we-have-a-problem.com/")
.catch((e) => e);
if (!(result instanceof Error)) throw new Error("Browser still trusts compromised eMudhra intermediates");Put that check in CI for your base images so the next PKI incident gets caught by a failing build, not a blog post.
The real lesson

The fix for "the key was 4" doesn't take cryptographic genius. It takes plumbing: how fast you can push a trust decision to every TLS client you run, including the ones hiding in sidecars, proxies, and pinned images. The browser vendors measured that in hours. That's the bar for anyone running browsers on other people's behalf. PKI failures will happen again, so it's worth measuring your own time to distrust before the next one does.


