ENGINEERING

The eMudhra certificate compromise: checking and fixing Linux servers, Windows machines and bundled certificate stores

Updated October 21: most Linux distributions have shipped fixes now, so this adds which ones, a one-line test for any machine, Windows machines that never received Microsoft’s block, and software that ships its own copy of the roots.

On October 17, someone reported that the private keys for eight eMudhra intermediate certificate authorities had leaked. Six of them issue TLS certificates and two sign code. With those keys, anyone can mint a certificate for any website that your machines will accept as real, and use it to intercept traffic.

The response from the big platforms was fast. Chrome and Firefox pushed blocks to every browser the same day. Apple shipped one on the 19th and Microsoft on the 20th. Go put out emergency releases (1.26.10 and 1.27.3), and Mozilla removed the root from NSS in 3.131.1.

The compromised intermediates never appeared in Certificate Transparency logs, so browsers that require CT (Firefox, Chrome, Edge and Safari) couldn’t have been fooled by a forged certificate. Most software outside the browser doesn’t check CT, and nobody knows whether anyone used the keys against it.

That’s also the software the browser fixes don’t reach. Linux servers need their distribution’s update, Windows only gets Microsoft’s block if it downloads it, and every app that carries its own copy of the roots needs its own update.

Why Linux is different

Browsers keep their own certificate blocklists and update them themselves. Most other software on Linux doesn’t. curl, apt and dnf, Python’s ssl module, .NET, and anything else built on OpenSSL or GnuTLS trust the distribution’s certificate store, and that store comes from the ca-certificates package. Until your distribution ships an update and you install it, the store still trusts the root these intermediates chain to.

On October 20 I checked two fully patched lab machines, Ubuntu 26.04 and Rocky 9.8. Both still trusted emSign ECC Root CA - G3, which cross-signs eMudhra’s TLS root. That’s the path a forged certificate would use.

Since then Ubuntu, Debian, Fedora, Alpine, Arch and Gentoo have all shipped fixed ca-certificates packages (the incident page lists the exact versions). My Ubuntu machine picked up 20260816ubuntu0.26.04.1 through automatic updates, and that package removes the root. Rocky 9.8 had no update on October 21 and still trusted it, and the incident page doesn’t list a fix for RHEL 9 or its rebuilds yet.

Check one machine

The quickest test uses two sites the incident page runs with certificates from the compromised intermediates. A certificate error means you’re safe:

curl https://dv-g3b-emudhra.we-have-a-problem.com/
curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/

It works for anything that makes TLS connections, so swap in wget, Python or your own tools. On my lab machines, the updated Ubuntu box refused both (tested with wget, since stock Ubuntu desktop doesn’t ship curl), and the Rocky box connected to the first one without complaint.

To check the trust store itself, this prints a line if your system bundle still trusts the root. On RHEL-family systems use /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem instead.

awk '/BEGIN CERT/{c=""} {c=c $0 "\n"} /END CERT/{print c | "openssl x509 -noout -fingerprint -sha256"; close("openssl x509 -noout -fingerprint -sha256")}' \
  /etc/ssl/certs/ca-certificates.crt | grep -i "86:A1:EC:BA"

No output means the root is gone. A line starting with sha256 Fingerprint=86:A1:EC:BA means the machine is exposed.

Fix it until your distribution does

If your distribution has shipped its update, install it. That’s the permanent fix.

If it hasn’t, do what Mozilla did and stop trusting the roots. These commands are the incident page’s, and they also distrust the old Sectigo root AAA Certificate Services, which cross-signed these intermediates. Most current bundles don’t carry AAA any more, so on most machines that half changes nothing.

On Debian and Ubuntu, put a ! in front of the entries and rebuild the store:

sudo sed -i -e 's|^mozilla/emSign_ECC_Root_CA_-_G3.crt|!&|' -e 's|^mozilla/Comodo_AAA_Services_root.crt|!&|' /etc/ca-certificates.conf
sudo update-ca-certificates

On RHEL, Rocky, Alma, CentOS Stream and Amazon Linux 2023, as root:

d=/etc/pki/ca-trust/source/blocklist; [ -d $d ] || d=/etc/pki/ca-trust/source/blacklist
trust extract --overwrite --format=pem-bundle \
  --filter='pkcs11:object=Comodo%20AAA%20Services%20root;type=cert' \
  $d/Comodo_AAA_Services_root.pem &&
trust extract --overwrite --format=pem-bundle \
  --filter='pkcs11:object=emSign%20ECC%20Root%20CA%20-%20G3;type=cert' \
  $d/emSign_ECC_Root_CA_-_G3.pem &&
update-ca-trust

I ran that on the Rocky 9.8 machine: the first test site went from connecting to a certificate error. Run both checks again afterwards. The incident page has the same workaround for openSUSE, Arch, Alpine, NixOS, NetBSD and FreeBSD.

The cost: sites with certificates from emSign ECC Root CA - G3 stop validating on that machine, exactly as they already have in Firefox and Chrome. Sites on eMudhra’s other roots keep working. I checked emsign.com, which uses their G1 root, before and after.

Windows machines that never got Microsoft’s block

Microsoft’s block isn’t a Windows update, and it doesn’t show up in the Untrusted Certificates store. It’s in the disallowed-certificate list Windows downloads by itself in the background. On my patched Windows 11 and Server 2025 lab machines, all sixteen certificates (eight keys, each signed by eMudhra and cross-signed by Sectigo’s AAA Certificate Services, which Windows trusts) came back as explicitly distrusted, with nothing in certlm.msc.

That download is the catch. Locked-down shops often turn it off with the Group Policy setting Turn off Automatic Root Certificates Update, and offline networks never get it at all. I turned it off on the Windows 11 lab machine and removed the downloaded list. Windows then trusted all eight intermediates through the Sectigo cross-signs, the two code-signing ones included.

Ars Technica also found Edge on Windows 10 still connecting to vulnerable hosts when its story went live, and Microsoft wouldn’t say why.

This asks Windows itself, using the affected certificates from the incident page. Run it in an elevated PowerShell window. On a protected machine it prints nothing:

$j = Invoke-RestMethod https://emudhra.we-have-a-problem.com/impacted-intermediates.json
foreach ($c in $j) {
  $x = [Security.Cryptography.X509Certificates.X509Certificate2]::new([Convert]::FromBase64String(($c.pem -replace '-----[^-]+-----|\s', '')))
  $ch = New-Object Security.Cryptography.X509Certificates.X509Chain
  $ch.ChainPolicy.RevocationMode = 'NoCheck'
  if ($ch.Build($x)) { "TRUSTED: $($c.name) via $($c.issued_by)" }
}

On an exposed machine you get eight TRUSTED: lines, all via AAA Certificate Services. (No internet on that machine? Copy the JSON file over and use Get-Content ... | ConvertFrom-Json instead of Invoke-RestMethod.)

The best fix is turning automatic certificate updates back on and letting Windows download the list. Where that isn’t allowed, add the sixteen certificates to Untrusted Certificates yourself:

$j = Invoke-RestMethod https://emudhra.we-have-a-problem.com/impacted-intermediates.json
$store = New-Object Security.Cryptography.X509Certificates.X509Store('Disallowed', 'LocalMachine')
$store.Open('ReadWrite')
foreach ($c in $j) { $store.Add([Security.Cryptography.X509Certificates.X509Certificate2]::new([Convert]::FromBase64String(($c.pem -replace '-----[^-]+-----|\s', '')))) }
$store.Close()

Run the check again and it prints nothing. To undo it, delete them from Untrusted Certificates in certlm.msc.

Software with its own copy of the roots

Some software ignores both the Windows store and the Linux system store and checks certificates against a list it ships itself: Python’s certifi (which other apps bundle inside themselves), Java’s cacerts, curl bundles, Node.js, and container images. Fixing the operating system does nothing for these. Each one needs its own update.

On the same Windows 11 lab machine I found nine: seven ODBC drivers inside Power BI Desktop 2.158, each with its own cacerts.pem trusting both roots, and two certifi copies inside Azure CLI 2.91. The Ubuntu machine had one inside a copy of Blender’s bundled Python. Node.js 22 (22.23.3 on Windows, 22.22.1 on Ubuntu) still trusts the emSign root in its built-in list, and Node’s fix is still pending.

On Linux, this lists every bundle file that still trusts either root:

sudo find / -xdev \( -name cacert.pem -o -name cacerts.pem -o -name ca-bundle.crt -o -name curl-ca-bundle.crt \) -print0 2>/dev/null |
  while IFS= read -r -d '' f; do
    awk '/BEGIN CERT/{c=""} {c=c $0 "\n"} /END CERT/{print c | "openssl x509 -noout -fingerprint -sha256"; close("openssl x509 -noout -fingerprint -sha256")}' "$f" \
      | grep -qiE '86:A1:EC:BA|D7:A7:A0:FB' && echo "$f"
  done

On Windows:

$sha = [Security.Cryptography.SHA256]::Create()
Get-ChildItem 'C:\Program Files', 'C:\Program Files (x86)' -Recurse -Include cacert.pem, cacerts.pem, ca-bundle.crt, curl-ca-bundle.crt -ErrorAction SilentlyContinue |
  ForEach-Object {
    foreach ($m in [regex]::Matches((Get-Content -LiteralPath $_.FullName -Raw), '(?s)BEGIN CERTIFICATE-----(.+?)-----END')) {
      $h = -join ($sha.ComputeHash([Convert]::FromBase64String(($m.Groups[1].Value -replace '\s', '')))[0..3] | ForEach-Object { $_.ToString('X2') })
      if ($h -in '86A1ECBA', 'D7A7A0FB') { $_.FullName; break }
    }
  }

And for Node.js on either one, true means it still trusts the emSign root:

node -p "require('tls').rootCertificates.some(c => c.replace(/\s/g, '').includes('AAQjpQy4LRL1KPOxst3iAhKAnjlfSU2f'))"

Both file scripts compare certificate fingerprints instead of searching the text, and the Node one strips line breaks first, because Node wraps its certificates at 72 characters a line instead of the usual 64. While I was testing, a plain text search for the root came back false on a Node that trusts it.

The fix is updating whatever owns each file. certifi 2026.10.19 dropped the root, so pip install -U certifi fixes your own Python environments. The incident page tracks the rest: pip 26.2.2, uv 0.13.4 and Electron 44.9.1, 43.8.1 and 42.11.16 are fixed, and Node.js and Bun are still pending. For copies bundled inside an app (Power BI, Azure CLI, Blender), you need that app’s update. For Node.js, Java and container images, update to a release built after the fix.

I ran every command in this post on lab machines before publishing it. The Windows check and fix were tested both ways: on the exposed machine and after the fix.

Doing this across a fleet

In RFF, the Script Library has entries for all of this:

Once your distribution ships the ca-certificates update, deploy it like any other package and re-run the check. RFF’s own Linux agent also refuses these eight keys when it talks to us, starting with Linux agent 1.3.152.

RFF is free for your first 100 endpoints: getrff.com.

Sources