← Blog

2026.10.23 · 5 min · security · browsers · https · news

How HTTPS decides to trust a site.

The padlock doesn't mean a site's certificate is good. It means a chain of signatures leads to a root your browser already ships. eMudhra's eight intermediate CA keys, each one byte long, show what happens when one link in that chain is weak, and why browsers were protected within hours while curl, Node and Python were not. As of 2026.10.

Eight CA keys your browser trusted were each one byte long. They belonged to intermediates under eMudhra, chained to roots trusted by Mozilla, Microsoft, Apple and Chrome, and anyone who noticed could have recovered one in at most 255 tries. To see why that matters for every domain on the web, not just eMudhra's customers, you need to know what the padlock actually checks.

Watch the 60-second version on Instagram.

The padlock is a chain of signatures

Most of us read the padlock as "this site's certificate is trustworthy". What the browser actually checks is narrower: a chain of signatures leads from the site's certificate to a root it already ships.

  1. The site certificate (example.com) is signed by an intermediate CA.
  2. The intermediate's certificate is signed by a root CA.
  3. The root is in the browser's or the OS's trust store already. Nothing signs it; it's trusted because it was shipped.

The browser verifies each signature up the chain. If they all check out and the chain ends at a root it knows, you get the padlock.

BROWSER / OS STOREROOT CAALREADY TRUSTEDSIGNED BYINTERMEDIATE CAKEY d = 156SIGNED BYSITE CERTEXAMPLE.COMSITE CERTANY-DOMAIN.COMGOLD = WEAK KEY · DASHED = COULD BE FORGEDFIG. 01 / A CHAIN OF SIGNATURES

The catch is in what that check doesn't cover. The browser trusts a few roots and believes whatever any CA under them signs. It checks signatures, not who holds the keys. So whoever holds an intermediate's private key can sign a certificate for any domain, and it verifies up the same chain as the real one. The chain is only as strong as its weakest key.

Eight keys, one byte each

eMudhra had 8 intermediates (6 for TLS, 2 for code signing) with compromised private keys. They use P-384, where a private key should be a random 384-bit number: 48 bytes of randomness. The 8 values were 156, 230, 115, 56, 155, 4, 66 and 237. All under 256. In Scott Helme's words, "47 bytes of zeros and then one byte".

P-384 PRIVATE KEY · 384 BITS · 48 BYTES9Cd = 15647 BYTES OF ZEROSTRY d = 156 OF AT MOST 255MATCHES PUBLIC KEYFIG. 02 / A 384-BIT KEY, ONE BYTE SET

The public key is in the certificate, which anyone can read. With a private key that small, you try d = 1, 2, 3 … until one produces that public key. That's at most 255 tries; the largest of the eight was 237. The figure shows the first one, 156.

Two things the story is not:

  • The root wasn't hacked. The intermediate keys were weak, and the root signed them.
  • No attack has been found. Anyone could have forged certificates with these keys. Mozilla saw no CT-logged certificates from these intermediates and received no reports of misuse, and the intermediates weren't used for customer certificates.

How it happened, per eMudhra's preliminary explanation: during an April 2025 key ceremony, a code flaw in an approved test deviation generated a second, weak key pair on the HSM. Similar key labels then led staff to use the weak keys for the certificate signing requests. So it's a code flaw plus human error, not a script bug alone.

Who was protected, and when

It was reported on 17 Oct 2026. The browsers moved fast:

WhatWhen
Mozilla finishes the OneCRL block17 Oct, 22:59 UTC
Chrome pushes a CRLSet17 Oct, the same evening
Apple corroborates the report17 Oct
eMudhra revokes the intermediates18 Oct
Sectigo revokes its cross-signs19 Oct
Mozilla announces removing the emSign ECC Root CA - G3 root (NSS 3.131.1)20 Oct

Apple and Microsoft followed within days. There's a wrinkle in the middle row: in May 2025 Sectigo had cross-signed all 8 intermediates under AAA Certificate Services. The cross-signs carry the same public keys (identical SPKI), so the browsers' key-based blocks cover them too.

Go also moved: Go 1.27.3 and 1.26.10 crypto/x509 drop chains that pass through these intermediates (the Go issue doesn't name eMudhra).

Revocation is not removal

Browsers blocked the intermediates through their own push lists, OneCRL and CRLSet. Most other clients don't check revocation at all.

With default settings, curl, Python and Node still accepted a chain through these intermediates. They trust a CA bundle on disk and check neither revocation nor Certificate Transparency. So eMudhra revoking the intermediates on 18 Oct changed nothing for them. What changes it is a new bundle, and a bundle only updates when you update it, in every place it's baked in:

refresh-trust.sh
# see the chain a server sends
openssl s_client -connect example.com:443 -showcerts </dev/null
# refresh the trust bundles
sudo apt update && sudo apt install --only-upgrade ca-certificates
pip install -U certifi
# then rebuild Docker images on the updated base image

That last line is the one people skip. A container image built last month carries last month's bundle until it's rebuilt.

On a client project I'd treat ca-certificates and certifi like any security patch: bumped on a schedule, with images rebuilt when they change, not whenever someone next touches the Dockerfile.

The short version

  1. The padlock means a chain of signatures leads from the site's certificate to a root the browser already ships.
  2. Any intermediate under a trusted root can sign a valid certificate for any domain, so one weak key breaks trust for every domain.
  3. eMudhra's 8 intermediate keys were each under 256, recoverable in at most 255 tries. No misuse has been found.
  4. Chrome and Mozilla blocked them the day it was reported, cross-signs included.
  5. curl, Node and Python trust a bundle on disk and ignore revocation: update ca-certificates and certifi, and rebuild your images.

Sources: Scott Helme, Eight bits of trust · Mozilla, oss-security advisory · Mozilla Bugzilla 2083449 · LWN · golang/go #82345