Mastodon
Go back

Auditing the Auditors

Your browser trusts a website because a certificate authority (CA) vouched for it — a.k.a. “signed” that site’s TLS certificate. Most browsers ship with a list of “root CAs.” Each root signs intermediate CAs, and the intermediates sign the certificates for individual sites. Those cascading links form a chain of signatures, and that chain is why your browser trusts the site.

Each CA, root or intermediate, holds a private key. Anyone who holds that key can sign a certificate for any domain name, and every client that trusts the chain will gladly accept it. The whole system depends on those keys never leaving the CA. An attacker with an intermediate key, plus a position on the network path, can impersonate any site to any client that still trusts that intermediate.

What happened this week

eMudhra is an Indian CA that runs the emSign brand. Eight of its intermediate certificates carried private keys so weak that anyone could recover them from the public certificate in under a second. Google states the keys came from insufficient randomness during generation. Researchers, Scott Helme among them, report the private keys were small integers such as 4 (yes, a single-digit integer), 56, and 237 in place of 384-bit random values. eMudhra’s own account says a key wrap/unwrap step in the April 2025 ceremony produced a second, weak set of keys next to the strong ones, and the weak set went to the root stores. The company has not confirmed the exact root cause as of this writing.

A researcher who signs as “Wayne” found the flaw and proved it. He signed the message “We have a problem here - Wayne” with each of the eight keys and sent the signatures to the root programs on October 17. Public proof is at emudhra.we-have-a-problem.com.

Why it matters

The eight intermediates issued no certs, eMudhra states seven never ran in production, and the one that did never issued a certificate. Certificate Transparency logs showed no entries from these intermediates since April 2025, so CT-enforcing browsers could not have been attacked. The overall exposure window was ~18 months.

A non-trivial amount of legit risk sits outside the browser. Many clients do not consume revocation data and need a software update before they stop trusting the intermediates. Helme pointed out that revocation by eMudhra and Sectigo does nothing for those clients. That covers embedded devices, old Java, language-level trust stores, and any tool with a vendored CA bundle (this impacted Go, Python, and Node ecosystems to varying degrees). Whether an attacker ever used the keys against such clients is unknown.

The blast radius widened because of cross-signs (each intermediate also had a Sectigo cross-signed copy under AAA Certificate Services). That Sectigo root is in nearly every trust store. Distributions responded by removing or blocking roots, not just the eight intermediates. Debian removed emSign ECC Root CA - G3 and AAA Certificate Services from its bundle, and Homebrew excludes both plus emSign Root TLS CA - G3 by public key.

The root programs went further than revocation. After several people recovered the keys on their own, Mozilla removed trust in the eMudhra root that signed the intermediates. Chrome will remove the same root on October 27, 2026, and site operators with certificates under emSign ECC Root CA - G3 must get a new certificate from a different hierarchy before then. Chrome is also reviewing eMudhra’s other root in its store, emSign Root CA - G1. eMudhra says the root distrust affects one customer ecosystem in the United States, and most of its issued TLS certificates chain to other hierarchies.

The timeline

Times are UTC unless marked. Some sources disagree on two dates. eMudhra reports the researcher contact as late evening October 18 India time, which is October 17 UTC, and says it revoked on October 19. Help Net Security and SOCRadar say October 18. I’m not sure the precision truly matters at this point.

DateEvent
2025-04-30eMudhra creates the eight intermediates in a key generation ceremony.
Mid-2025Browser trust lists include the hierarchy. Two audit cycles pass without detection.
2026-10-17 18:05Chrome Root Program receives Wayne’s report.
2026-10-17 20:06Mozilla receives the report.
2026-10-17 22:59OneCRL block reaches all standard Firefox installs, under 3 hours after the report. Chrome blocks the same day.
2026-10-18 or 19eMudhra revokes all eight intermediates. See the note above.
2026-10-19Apple blocks. Disclosure to downstream trust stores.
2026-10-20Microsoft blocks. Dennis Jackson publishes the incident on dev-security-policy. Mozilla, Google, and eMudhra publish statements. Go 1.27.3 and 1.26.10 ship CVE-2026-111943. Firefox 158.0.1 and ESR builds ship the block for installs without Remote Settings, tracked as CVE-2026-113470. Mozilla removes the root from NSS.
2026-10-21eMudhra publishes its fuller account with the wrap/unwrap explanation. OpenJDK ships patched releases.
2026-10-27Chrome Root Store drops emSign ECC Root CA - G3 for all channels.

State as of October 22

No malicious use of the keys is confirmed. Patched versions exist for most ecosystems: NSS, curl, certifi, Go, Java, Node, rustls, Deno, Electron, and the major Linux distributions. Bun and Erlang certifi were still pending. macOS blocks the intermediates in system TLS, but the built-in curl, git, python3, and openssl verify against /etc/ssl/cert.pem with no revocation check, and that file did not get the update. The we-have-a-problem site publishes test hosts you can hit with curl to confirm a client rejects the chains.

Auditing the auditors

The Register reported that eMudhra threw its auditors under a bus, blaming their incompetence/negligence for the pathetically weak private keys. Granted, it is the job of auditors to, well, audit and then certify the results (if warranted).

I’ve not poked (until now) much at this banal, legal, human-process side of certificate machinations, save for the fairly regular act of pointing out that the whole “CA thing” is a Potemkin village of safety and integrity (fun fact: it is!). Another sleep-deprived night sent me into the auditor ecosystem.

I pulled the Common CA Database (CCADB) public exports — 10,344 certificate records: 949 roots and 9,395 intermediates. I also pulled the associated certificate files for every year from 1999 to 2026 and parsed ~10K certs.

Two fields in CCADB’s own definitions matter here: Audit Firm (the auditor picklist) and Audits Same as Parent (the record says its audit is covered by the parent).

I ran three checks:

  1. Who audits whom. Count roots and currently-valid intermediates per auditor, split by trust program (Mozilla, Chrome, Apple, Microsoft) and by use (TLS, S/MIME, code signing).
  2. Weak keys. Search elliptic-curve keys for private keys under 2^40. Run batch GCD (greatest common divisor) across every RSA modulus (see blockquote below this list). Run the ROCA fingerprint test. Check the Debian OpenSSL weak-key list.
  3. Shared keys. Group certificates by public key and look for keys that appear under more than one owner or more than one name.

An RSA public key contains a modulus N = p * q, with p and q secret primes. If a generator has weak entropy, two different devices can pick the same prime for one of their factors. When that happens, gcd(N1, N2) returns that shared prime, and both keys fall apart — you divide to get the other factor and you have both private keys.


I used a small custom Go binary (source at the end) and some DuckDB SQL.

Findings

Audits are highly and deceptively concentrated: ~74% of records leave the auditor field empty and say “same as parent” instead. You only see the real picture by walking each certificate up its parent chain.

auditorrootsshare of rootsvalid intermediatesshare
BDO International Limited16517.4%1,97452.3%
KPMG10210.8%44311.7%
Ernst & Young778.1%2637.0%

One auditor signs off on more than half the valid intermediates — the consulting firms are making bank. Separately, 247 of 949 roots record no auditor anywhere in their chain.

Note that BDO is more of an umbrella organization with affiliates across most industrial regions of the globe.

Eight elliptic-curve keys are trivially breakable. I searched ~1.7K EC keys for a private key in the range 1 to 2^40 and found eight distinct keys, all in eMudhra’s G3B and G3C intermediates issued on 30 April 2025 (lots of folks have independently done the same). Their private keys are 3 to 8 bits long, and a plain brute-force loop finds them in milliseconds. Each key appears as two certificates, because each is cross-signed by Sectigo as well as issued by eMudhra. Nobody runs this check on the regular, which is bonkers.

Thankfully, RSA holdings are clean: ~8.4K certificates map onto ~6.5K distinct moduli. The batch GCD run found no shared primes. ROCA found nothing across 38 primes, and none of the ~5K checkable certificates appear on the Debian weak-key list (~3.3K weren’t checked because the published list only covers 1024-bit and 2048-bit keys).

For a process that is supposed to have real integrity, some self-reported fields drift from the legal documents they point at. The Thailand NTCA G2 and G3 records, for example, state an audit period of 2024-07-31 to 2025-07-30, while the statement they link to covers 2024-03-01 to 2025-02-28.

These checks were lightweight — no special equipment. I have a sinking feeling that poking a bit more under the covers would turn up more problems in a process meant to safeguard confidentiality and integrity across the internets. Unless, of course, it’s the Potemkin village I keep asserting it is.

Try this at home

The data is wide open: no permission, no relationship with a CA, no agreement with a browser vendor. Everything above came from a public CSV export and a laptop.

No single root store can see all of this: Mozilla watches its own list, Chrome watches its own, and each reviews CAs one at a time. Concentration across the whole ecosystem is invisible from inside any one program — no one, as far as I know, is paid to notice that one auditor covers half the valid intermediates. When one firm audits half the live intermediates, a problem at that firm becomes a problem for the whole web.

The full elliptic-curve search took 30 minutes of CPU and the RSA sweep 12 seconds, so anyone can do those bits. Cross-checking claims in certs and CSVs against filed, legal PDF documents needs volunteers with time or, perhaps, an agentic process for oversight.

To reproduce this with my tooling/scripts you’ll need DuckDB and Go 1.23+ along with the contents of this repo.

Then, grab the source CCADB data:

git clone https://rud.is/git/ccadb-audit.git

mkdir -p ccadb-audit/data && cd ccadb-audit/data

curl -sS -o AllCertificateRecordsCSVFormatV5.csv \
  https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatV5

curl -sS -o IncludedCACertificateReportCSVFormat.csv \
  https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportCSVFormat

for y in $(seq 1999 2026); do
  curl -sS -o "AllCertificatePEMs_${y}.csv" \
    "https://ccadb.my.salesforce-sites.com/ccadb/AllCertificatePEMsCSVFormat?NotBeforeYear=${y}"
done

Read the field definitions before querying!!! The document is linked as “Description” next to each CSV on https://ccadb.org/resources. It explains that Audit Firm is a picklist chosen by the CA owner, and that Audits Same as Parent means the audit is inherited rather than absent.

Load the record table:

duckdb ccadb.duckdb <<'SQL'
CREATE OR REPLACE TABLE v5 AS
  SELECT * FROM read_csv('data/AllCertificateRecordsCSVFormatV5.csv',
                         header=true, all_varchar=true);
SELECT "Certificate Record Type", count(*) FROM v5 GROUP BY 1;
SQL

The reference implementation lives in go/ and builds with go build -o ../bin/ccaudit .. Then:

./bin/ccaudit load    # parse every PEM, write certs.tsv, rsa_moduli.tsv, ec_keys.tsv
./bin/ccaudit verify  # positive and negative control for each detector - run this first
./bin/ccaudit ec      # elliptic-curve search over [1, 2^40]
./bin/ccaudit rsa     # batch GCD, ROCA, Debian blocklist
./bin/ccaudit brute   # independent brute-force cross-check of specific keys

./run.sh rebuilds every table; ./run.sh --sweeps adds the two key sweeps.

Use the comments if you kick the tyres and find anything amiss. 🙏🏽

FIN

This was a rough week for defenders and we really didn’t need the combined incompetence of a CA and CA auditor added into the mix. Perhaps if a few folks set up some independent monitoring of CAs we can avoid an 18-month insecure window and a hurry-up exercise across many programming language and OS ecosystems.


Join the conversation:follow@hrbrmstr@rud.ison the fediverse to read and reply to this post from your own instance.

1 comment.


Next Post
Adding Archivo SemiCondensed to {hrbrthemes}