> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getprimo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Respond to the eMudhra CA compromise

> Find the trust stores on your devices that still trust the compromised eMudhra roots, and clean them from the cockpit. Primo cleans the Primo agent for you.

In October 2026, the private keys of eight eMudhra intermediate certificate authorities (CAs) turned out to be trivially recoverable. Anyone holding one of them can sign a valid-looking certificate for any hostname. Browsers and operating systems blocked the eight intermediates within days, but many tools, runtimes, and agents keep their own frozen copy of the trusted roots and never received that block.

> [!NOTE]
>
> **What Primo does:** Primo removes the two compromised roots from the Primo agent's own CA bundle on every enrolled Mac, Windows, and Linux device. Devices that are offline receive the fix when they reconnect, and new enrollment packages ship without the two roots. You have nothing to do for the agent.
>
> **What you decide:** the other trust stores on your devices belong to the operating system, to your teams' tools, and to your apps. You choose when and how to clean them. This guide gives you a read-only check to run first, the remediation scripts, and the fixed versions to roll out.

## What happened

eMudhra generated the private keys of eight intermediate CAs in April 2025 as integers smaller than 256, instead of random 384-bit numbers. Trying every value takes milliseconds, so each key can be recomputed from its public certificate. Six of the intermediates issue TLS certificates and two sign code.

Each intermediate exists twice: once issued by eMudhra's `emSign` roots, and once cross-signed by Sectigo's `AAA Certificate Services` root. That gives **16 certificates to distrust** and **two roots to remove** from every CA file that still carries them:

* `emSign ECC Root CA - G3`, the eMudhra root that most devices trust.
* `AAA Certificate Services`, the Sectigo root behind the cross-signs.

| Date (2026) | Event |
| - | - |
| October 17 | A researcher reports the compromised keys. Chrome and Firefox block the 16 certificates. |
| October 18 | eMudhra revokes the eight intermediates. Sectigo's cross-signs are revoked on October 19. |
| October 20 | Microsoft ships the block through Windows' automatic trust list updates. |
| By October 22 | Apple ships the block on macOS, iOS, and iPadOS. |

No TLS certificate issued by these intermediates appears in Certificate Transparency (CT) logs, so browsers that enforce CT (Chrome, Firefox, Edge, Safari) could not have been attacked. Most other clients, `curl` included, do not enforce CT. Whether anyone exploited the keys against them is unknown.

To use a forged certificate, an attacker also needs a position on the network path: a rogue Wi-Fi network, a compromised router or proxy, or DNS hijacking.

## What stays exposed

The vendor blocks reach software that asks the OS to verify certificates or that checks revocation. Software that ships its own file of trusted roots keeps trusting forged chains until that file is updated.

| Trust path | Covered by the vendor blocks? | Worst case if left as is | Who fixes it |
| - | - | - | - |
| Browsers, and apps that verify through the OS (Safari, Mail, Windows Schannel, Go programs on macOS and Windows) | Yes, once the device received the OS update | None | No action: keep the OS up to date |
| The Primo agent's CA bundle | No | An impostor Primo server collects query results and the device's node key, and sends its own queries | **Primo** |
| macOS command-line tools (`curl`, `git`, `python3`, `perl`, `ruby`, `openssl` read `/etc/ssl/cert.pem`) | No: Apple did not update this file | Passwords and API tokens read in transit; `curl \| sh` installers swapped for malware | You |
| Linux system bundle (`ca-certificates` package) | Only after the package update. On October 21, the Rocky Linux 9 and Fedora 44 stable repositories still shipped the root. | Every OpenSSL and GnuTLS program on the device can be impersonated at once: backups, webhooks, monitoring agents, database clients | You |
| Windows devices that missed Microsoft's update (offline, WSUS-only, or automatic updates disabled by Group Policy) | No | Forged TLS certificates accepted; malware signed with the two code-signing intermediates passes signature checks | You |
| Node.js and Electron | No: official builds embed a Mozilla snapshot fixed at release, on every OS | Service-to-service calls and API keys go to the attacker, even on a fully patched device | You |
| Python `certifi` and `pip` installed with pip | No: each is its own CA copy | Same as Node.js | You |
| Go programs on Linux | No: Go reads the system bundle on Linux | Cloud CLIs, `kubectl`, and `terraform` hand their credentials to an impostor API | You |
| Containers, servers, and CI images | No: each image carries its own bundle | Same as the Linux system bundle, inside every image built before the fix | You |

`apt` and `dnf` package installs stay safe: packages are GPG-signed, so a forged TLS certificate cannot swap them.

## What Primo fixes for you

The Primo agent ships its own CA bundle:

| Platform | Path |
| - | - |
| macOS, Linux | `/opt/orbit/certs.pem` |
| Windows | `C:\Program Files\Orbit\certs.pem` |

The agent's osquery component uses this file, not the OS trust store, for every TLS connection to your Primo server. The file is written at installation and never refreshed, so the OS vendor blocks do not reach it.

Primo removes `emSign ECC Root CA - G3` and `AAA Certificate Services` from this file on every enrolled device. Leave the file to Primo: the remediation scripts below detect that it is already clean and move on.

> [!NOTE]
>
> On Linux, the agent's updater reads the system bundle instead. Updating `ca-certificates`, as described in [Fix the system trust stores](#fix-the-system-trust-stores), also protects it. On macOS and Windows, the updater verifies through the OS, so the Apple and Microsoft blocks already cover it.

## Check your devices

The check scripts below are read-only: they change nothing on the device. Each one lists every trust store that still trusts a compromised root, and every runtime that ships an outdated copy of the roots.

**1. Add the two check scripts to the Script library**

In the cockpit, open **Device Management** > **Scripts** > **Library** and select **Add a script**. Name the first one `check-emudhra-exposure.sh`, pick **macOS & Linux (.sh)**, paste the macOS and Linux script below, and select **Add script**. Repeat with `check-emudhra-exposure.ps1` and **Windows (.ps1)**.

**2. Run them on all devices**

On each script's row, open the actions menu and select **Run script**. Under **Targeting**, select **Target all devices**, then **Run script**. Each device runs the script at its next check-in. Offline devices run it when they reconnect.

**3. Read the results in Run History**

Open the **Run History** tab and select the run. **Success** means the device has nothing left to fix. **Failed** means the device has at least one finding: open the execution to read its output.

```bash check-emudhra-exposure.sh theme={null}
#!/bin/bash
# check-emudhra-exposure.sh
# Read-only check for the eMudhra CA compromise (October 2026). It changes nothing.
# Lists the trust stores on this Mac or Linux device that still trust a compromised root,
# and the runtimes that ship their own outdated copy of the roots.
# Exit: 0 = nothing left to fix, 2 = at least one finding (read the output), 1 = error.
# Guide: https://help.getprimo.com/guides/security/emudhra-ca-compromise

set -u
export PATH="/usr/bin:/bin:/usr/sbin:/sbin"
shopt -s nullglob

# A fragment of the base64 body of each compromised root. PEM line wrapping differs
# between bundles, so files are compared with their whitespace removed.
EMSIGN_ECC_G3="QyBSb290IENBIC0gRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOx"   # SHA-1 3043fa4ff257dca0c380ee2e58ea78b23fe6bbc1
AAA_SERVICES="jNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8Xl"    # SHA-1 d1eb23a46d17d68fd92564c2f1f1601764d8e349

OS=$(uname -s)
FINDINGS=0
SEEN=" "

row()     { printf '%-9s %-15s %s\n' "$1" "$2" "$3"; }
finding() { row "$1" "$2" "$3"; FINDINGS=$((FINDINGS + 1)); }

# Names of the compromised roots a PEM file still trusts, or nothing.
roots_in() {
    local flat hits=""
    flat=$(tr -d ' \t\r\n' < "$1" 2>/dev/null) || return 0
    case "$flat" in *"$EMSIGN_ECC_G3"*) hits="emSign ECC Root CA - G3" ;; esac
    case "$flat" in *"$AAA_SERVICES"*) hits="${hits:+$hits, }AAA Certificate Services" ;; esac
    printf '%s' "$hits"
}

# Report one bundle once, even when several paths lead to it.
check_bundle() {
    local area="$1" file="$2" hint="$3" real hits
    [ -r "$file" ] || return 0
    real=$(readlink -f "$file" 2>/dev/null) || real="$file"
    case "$SEEN" in *" $real "*) return 0 ;; esac
    SEEN="$SEEN$real "
    hits=$(roots_in "$file")
    if [ -n "$hits" ]; then finding EXPOSED "$area" "$file trusts $hits. $hint"
    else row OK "$area" "$file"; fi
}

# 0 when a Node.js version carries the fix, 1 when it does not, 2 when it is a newer line
# to review, 3 when the name is not a version (an alias such as "lts").
node_fixed() {
    local v="${1#v}" major minor patch
    v="${v%%[_-]*}"
    case "$v" in [0-9]*.[0-9]*.[0-9]*) ;; *) return 3 ;; esac
    major=${v%%.*}; v=${v#*.}; minor=${v%%.*}; patch=${v#*.}; patch=${patch%%.*}
    case "$major$minor$patch" in *[!0-9]*) return 3 ;; esac
    if [ "$major" -gt 26 ]; then return 2; fi
    case "$major" in
        26) [ "$minor" -gt 12 ] || { [ "$minor" -eq 12 ] && [ "$patch" -ge 1 ]; } ;;
        24) [ "$minor" -gt 22 ] || { [ "$minor" -eq 22 ] && [ "$patch" -ge 1 ]; } ;;
        22) [ "$minor" -gt 23 ] || { [ "$minor" -eq 23 ] && [ "$patch" -ge 4 ]; } ;;
        *) return 1 ;;
    esac
}

check_node() {
    local where="$1" version="$2"
    node_fixed "$version"
    case $? in
        0) row OK "Node.js" "$version ($where)" ;;
        2) row REVIEW "Node.js" "$version ($where): newer release line, check its release notes" ;;
        3) ;;
        *) finding UPDATE "Node.js" "$version ($where): bundles its own roots. Update to 22.23.4, 24.22.1 or 26.12.1 or later" ;;
    esac
}

echo "----- system trust store -----"
if [ "$OS" = "Darwin" ]; then
    check_bundle "System bundle" /etc/ssl/cert.pem "Apple's curl, git, python3 and openssl use it."
    valid=/private/var/protected/trustd/valid.sqlite3
    mtime=$(stat -f %m "$valid" 2>/dev/null || echo "")
    if [ -z "$mtime" ]; then
        row INFO "Apple block" "cannot read $valid"
    elif [ "$mtime" -ge 1792368000 ]; then
        row OK "Apple block" "trustd valid database updated $(date -u -r "$mtime" '+%Y-%m-%d')"
    else
        finding STALE "Apple block" "trustd valid database last updated $(date -u -r "$mtime" '+%Y-%m-%d'), before Apple's block. Check that the device reaches Apple's servers."
    fi
else
    for f in /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt \
             /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /var/lib/ca-certificates/ca-bundle.pem /etc/ssl/cert.pem; do
        check_bundle "System bundle" "$f" "Every OpenSSL and GnuTLS program trusts it, including the Primo agent's updater."
    done
fi

echo "----- Homebrew -----"
for prefix in /opt/homebrew /usr/local /home/linuxbrew/.linuxbrew; do
    check_bundle "Homebrew" "$prefix/etc/ca-certificates/cert.pem" "Run: brew upgrade ca-certificates"
done

echo "----- Python certifi -----"
# Copies installed with pip. Distribution packages follow the system bundle and are covered above.
for dir in /usr/local/lib/python3*/site-packages /usr/local/lib/python3*/dist-packages \
           /opt/homebrew/lib/python3*/site-packages \
           /Library/Frameworks/Python.framework/Versions/*/lib/python3*/site-packages \
           /Library/Python/3*/site-packages /Users/*/Library/Python/3*/lib/python/site-packages \
           /Users/*/.pyenv/versions/*/lib/python3*/site-packages /home/*/.pyenv/versions/*/lib/python3*/site-packages \
           /home/*/.local/lib/python3*/site-packages /root/.local/lib/python3*/site-packages; do
    check_bundle "Python certifi" "$dir/certifi/cacert.pem" "Run: pip install --upgrade 'certifi>=2026.10.19'"
    check_bundle "Python pip" "$dir/pip/_vendor/certifi/cacert.pem" "Run: pip install --upgrade 'pip>=26.2.2'"
done

echo "----- Node.js -----"
# Official Node.js builds embed a frozen copy of the roots, on every OS.
for d in /opt/homebrew/Cellar/node /opt/homebrew/Cellar/node@* /usr/local/Cellar/node /usr/local/Cellar/node@* \
         /home/linuxbrew/.linuxbrew/Cellar/node /home/linuxbrew/.linuxbrew/Cellar/node@*; do
    for v in "$d"/*; do check_node "Homebrew $(basename "$d")" "$(basename "$v")"; done
done
header=/usr/local/include/node/node_version.h
if [ -r "$header" ]; then
    v=$(awk '/#define NODE_(MAJOR|MINOR|PATCH)_VERSION /{printf "%s%s", sep, $3; sep="."}' "$header")
    [ -n "$v" ] && check_node "/usr/local" "$v"
fi
for v in /Users/*/.nvm/versions/node/v* /home/*/.nvm/versions/node/v* /root/.nvm/versions/node/v* \
         "/Users/"*"/Library/Application Support/fnm/node-versions/v"* /home/*/.local/share/fnm/node-versions/v* \
         /Users/*/.volta/tools/image/node/* /home/*/.volta/tools/image/node/* \
         /Users/*/.asdf/installs/nodejs/* /home/*/.asdf/installs/nodejs/* \
         /Users/*/.local/share/mise/installs/node/* /home/*/.local/share/mise/installs/node/*; do
    [ -d "$v" ] && [ ! -L "$v" ] && check_node "$(dirname "$v")" "$(basename "$v")"
done
if command -v dpkg-query >/dev/null 2>&1; then
    v=$(dpkg-query -W -f='${Version}' nodejs 2>/dev/null)
    case "$v" in *nodesource*) check_node "NodeSource package" "${v%%-*}" ;; esac
fi
if command -v rpm >/dev/null 2>&1; then
    v=$(rpm -q --qf '%{VERSION}-%{RELEASE}' nodejs 2>/dev/null)
    case "$v" in *nodesource*) check_node "NodeSource package" "${v%%-*}" ;; esac
fi

echo "----- Primo agent -----"
agent=/opt/orbit/certs.pem
if [ -r "$agent" ]; then
    hits=$(roots_in "$agent")
    if [ -n "$hits" ]; then row PRIMO "Primo agent" "$agent trusts $hits. Primo cleans this bundle on its own: nothing to do."
    else row OK "Primo agent" "$agent"; fi
fi

echo "----- result -----"
if [ "$FINDINGS" -gt 0 ]; then
    echo "$FINDINGS finding(s) to fix on this device."
    exit 2
fi
echo "Nothing left to fix on this device."
exit 0
```

```powershell check-emudhra-exposure.ps1 theme={null}
# check-emudhra-exposure.ps1
# Read-only check for the eMudhra CA compromise (October 2026). It changes nothing.
# Tells whether Windows distrusts the compromised intermediates, and lists the runtimes
# that ship their own outdated copy of the roots.
# Exit: 0 = nothing left to fix, 2 = at least one finding (read the output), 1 = error.
# Guide: https://help.getprimo.com/guides/security/emudhra-ca-compromise

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2

# A fragment of the base64 body of each compromised root. PEM line wrapping differs
# between bundles, so files are compared with their whitespace removed.
$CompromisedRoots = [ordered]@{
    'emSign ECC Root CA - G3'  = 'QyBSb290IENBIC0gRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOx'  # SHA-1 3043FA4FF257DCA0C380EE2E58EA78B23FE6BBC1
    'AAA Certificate Services' = 'jNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8Xl'  # SHA-1 D1EB23A46D17D68FD92564C2F1F1601764D8E349
}

# EM DV TLS CA - G3B, once through each root. Windows reports ExplicitDistrust for both
# once Microsoft's block (or an explicit entry in Untrusted Certificates) is in place.
$SampleIntermediates = [ordered]@{
    'EM DV TLS CA - G3B (via emSign)'       = 'MIIDaDCCAu6gAwIBAgIQAKSu5njyM2K0wORgt3LyFDAKBggqhkjOPQQDAzBWMQswCQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTU1NDA4WhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE557ZwEqXc8RzXlERHahawr1eZKs3S4DhLLwgOOweTw47gS/JhZ4eZ6+wTi+LyX90hbplqpEe6KTkW2LVzAPVa+Z4s8OMtNyTfKJGDiOEU8iSk10p1E+R4VYGtjzO2rRoo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0OBBYEFOIeL+ccGTkOVUtiwSAkVzUkiiUUMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYBBAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AtYi5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQi5wN2MwNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5jcmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0EAwMDaAAwZQIwLmsGXi6oFMJ2klkcEMXMug6gvxQWZWkewiDWVzQyl2L6z6DuQfPOIbh1grvdULDCAjEAxZvLlxUavG69J6wnSjC8XBbqNIQsJ6CE2QdNOe84Ptxb6VG8gt2TlfJbHii+KNvK'
    'EM DV TLS CA - G3B (via Sectigo AAA)'  = 'MIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAwezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0yODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJWMRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BOL4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2PM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44hAQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICEwqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxMQkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMlbN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5VGUyRCEIDhF5TBUuKF4E3kf1h'
}

# SHA-1 of the 16 compromised intermediates (8 keys, each issued by emSign and cross-signed by Sectigo AAA).
$IntermediateSha1 = @(
    'A99B289464F3B44343270A7A1E910F02370483FE', '1396BE1372BC93DFA2A433CA964DBA91548A6E57',
    '5F014E0DB3BB8F950E0598AB280846D383FC57BE', '5DFA19197D1C1CF629AF7C0CD865543C5050227C',
    'F33ACD1F5D31AA81AC18C72C928446226D3D5914', '19069BD6533BFF93FB67C71FB3376B4EA7402C3B',
    '8AD23BC78845D6FAAE1D534BAC89F0E36BA55A11', '4B0944C14F8C2CCBA4E4F32104AD920FA15EE331',
    '7FCC31AAB874CE6C8EE8584F54CFD08646B54117', '305FC1E253DCAB71B95D4995DAE3CB09D562F22A',
    'C988F3D0CC1629FFC43165D6464C9B6EF73D6E48', '8588708E875A1F8AA83524F3559080F7E17068C7',
    'D33195A244046A3263FB5E0E83C6803A5B2E7BC3', 'EE26D7AE3CC71718F6E07D7D0095076F0C158F63',
    '1F4A5D178F8B6A8EF0F6784155BD5FEA91158B12', '7FDE64753A957B98F172FD2AB0D922551909C7BA'
)

$script:Findings = 0
$Drive = $env:SystemDrive
if (-not $Drive) { $Drive = 'C:' }
$ProgramFiles64 = $env:ProgramW6432
if (-not $ProgramFiles64) { $ProgramFiles64 = $env:ProgramFiles }
if (-not $ProgramFiles64) { $ProgramFiles64 = "$Drive\Program Files" }

function Write-Row([string]$Status, [string]$Area, [string]$Detail) {
    Write-Output ('{0,-9} {1,-15} {2}' -f $Status, $Area, $Detail)
}
function Add-Finding([string]$Status, [string]$Area, [string]$Detail) {
    Write-Row $Status $Area $Detail
    $script:Findings++
}

# Names of the compromised roots a PEM file still trusts.
function Get-RootsIn([string]$Path) {
    $flat = [IO.File]::ReadAllText($Path) -replace '\s', ''
    return @($CompromisedRoots.Keys | Where-Object { $flat.Contains($CompromisedRoots[$_]) })
}

function Test-Bundle([string]$Area, [string]$Path, [string]$Hint) {
    $roots = @(Get-RootsIn $Path)
    if ($roots.Count -gt 0) { Add-Finding 'EXPOSED' $Area "$Path trusts $($roots -join ', '). $Hint" }
    else { Write-Row 'OK' $Area $Path }
}

# 0 when a Node.js version carries the fix, 1 when it does not, 2 when it is a newer line
# to review, 3 when the text is not a version.
function Get-NodeFixState([string]$Version) {
    $m = [regex]::Match($Version, '^v?(\d+)\.(\d+)\.(\d+)')
    if (-not $m.Success) { return 3 }
    $v = [version]"$($m.Groups[1].Value).$($m.Groups[2].Value).$($m.Groups[3].Value)"
    if ($v.Major -gt 26) { return 2 }
    $fixed = @{ 22 = [version]'22.23.4'; 24 = [version]'24.22.1'; 26 = [version]'26.12.1' }
    if ($fixed.ContainsKey($v.Major) -and $v -ge $fixed[$v.Major]) { return 0 }
    return 1
}

function Test-Node([string]$Where, [string]$Version) {
    switch (Get-NodeFixState $Version) {
        0 { Write-Row 'OK' 'Node.js' "$Version ($Where)" }
        1 { Add-Finding 'UPDATE' 'Node.js' "$Version ($Where): bundles its own roots. Update to 22.23.4, 24.22.1 or 26.12.1 or later" }
        2 { Write-Row 'REVIEW' 'Node.js' "$Version ($Where): newer release line, check its release notes" }
    }
}

function Get-Files([string[]]$Patterns) {
    foreach ($p in $Patterns) {
        Get-ChildItem -Path $p -Force -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer }
    }
}

function Invoke-Check {
    Write-Output '----- Windows certificate store -----'
    $distrusted = 0
    foreach ($name in $SampleIntermediates.Keys) {
        $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 (, [Convert]::FromBase64String($SampleIntermediates[$name]))
        $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
        $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
        [void]$chain.Build($cert)
        $flags = @($chain.ChainStatus | ForEach-Object { $_.Status.ToString() })
        if ($flags -contains 'ExplicitDistrust') { $distrusted++ }
        $chain.Reset()
    }
    if ($distrusted -eq $SampleIntermediates.Count) {
        Write-Row 'OK' 'Windows block' 'Windows distrusts the compromised intermediates'
    } else {
        Add-Finding 'EXPOSED' 'Windows block' "Windows still trusts the compromised intermediates ($distrusted of $($SampleIntermediates.Count) distrusted). Microsoft's update has not reached this device: run the remediation script."
    }

    $explicit = @(Get-ChildItem -Path 'Cert:\LocalMachine\Disallowed' -ErrorAction SilentlyContinue |
        Where-Object { $IntermediateSha1 -contains $_.Thumbprint }).Count
    Write-Row 'INFO' 'Untrusted store' "$explicit of $($IntermediateSha1.Count) compromised intermediates listed in Untrusted Certificates"

    $policy = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\SystemCertificates\AuthRoot' -ErrorAction SilentlyContinue
    if ($policy) {
        $names = $policy.PSObject.Properties.Name
        if (($names -contains 'DisableRootAutoUpdate') -and $policy.DisableRootAutoUpdate -eq 1) {
            Write-Row 'INFO' 'Group Policy' "DisableRootAutoUpdate=1: Microsoft's automatic trust updates cannot reach this device"
        }
        if (($names -contains 'EnableDisallowedCertAutoUpdate') -and $policy.EnableDisallowedCertAutoUpdate -eq 0) {
            Write-Row 'INFO' 'Group Policy' "EnableDisallowedCertAutoUpdate=0: Microsoft's distrust updates cannot reach this device"
        }
    }

    Write-Output '----- Python certifi -----'
    # Copies installed with pip. Each one is a CA store of its own.
    $sitePackages = @(
        "$Drive\Python*\Lib\site-packages",
        "$ProgramFiles64\Python*\Lib\site-packages",
        "${env:ProgramFiles(x86)}\Python*\Lib\site-packages",
        "$Drive\Users\*\AppData\Local\Programs\Python\Python*\Lib\site-packages",
        "$Drive\Users\*\AppData\Roaming\Python\Python*\site-packages"
    )
    foreach ($f in Get-Files ($sitePackages | ForEach-Object { "$_\certifi\cacert.pem" })) {
        Test-Bundle 'Python certifi' $f.FullName "Run: pip install --upgrade ""certifi>=2026.10.19"""
    }
    foreach ($f in Get-Files ($sitePackages | ForEach-Object { "$_\pip\_vendor\certifi\cacert.pem" })) {
        Test-Bundle 'Python pip' $f.FullName "Run: python -m pip install --upgrade ""pip>=26.2.2"""
    }

    Write-Output '----- Node.js -----'
    # Official Node.js builds embed a frozen copy of the roots.
    $uninstall = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
                   'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*')
    foreach ($app in Get-ItemProperty -Path $uninstall -ErrorAction SilentlyContinue) {
        $props = $app.PSObject.Properties.Name
        if (($props -contains 'DisplayName') -and $app.DisplayName -eq 'Node.js' -and ($props -contains 'DisplayVersion')) {
            Test-Node 'installer' ([string]$app.DisplayVersion)
        }
    }
    $managed = @("$Drive\Users\*\AppData\Roaming\nvm\v*", "$Drive\nvm4w\v*",
                 "$Drive\Users\*\AppData\Roaming\fnm\node-versions\v*")
    foreach ($d in Get-ChildItem -Path $managed -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer }) {
        Test-Node $d.Parent.FullName $d.Name
    }

    Write-Output '----- Primo agent -----'
    $agent = [IO.Path]::Combine($ProgramFiles64, 'Orbit', 'certs.pem')
    if ([IO.File]::Exists($agent)) {
        $roots = @(Get-RootsIn $agent)
        if ($roots.Count -gt 0) { Write-Row 'PRIMO' 'Primo agent' "$agent trusts $($roots -join ', '). Primo cleans this bundle on its own: nothing to do." }
        else { Write-Row 'OK' 'Primo agent' $agent }
    }

    Write-Output '----- result -----'
    if ($script:Findings -gt 0) { Write-Output "$($script:Findings) finding(s) to fix on this device." }
    else { Write-Output 'Nothing left to fix on this device.' }
}

try {
    Invoke-Check
} catch {
    Write-Output "ERROR: $($_.Exception.Message)"
    exit 1
}
if ($script:Findings -gt 0) { exit 2 }
exit 0
```

Each line of the output starts with a status:

| Status | Meaning | Counts as a finding |
| - | - | - |
| `OK` | This store or runtime no longer trusts the compromised roots. | No |
| `EXPOSED` | A trust store still trusts a compromised root, or Windows does not distrust the intermediates yet. The line ends with the fix. | Yes |
| `UPDATE` | A Node.js install older than the fixed releases. | Yes |
| `STALE` | macOS only: Apple's block has not reached the device. Check that the device reaches Apple's servers and installs updates. | Yes |
| `REVIEW` | A Node.js release line newer than the ones listed in this guide. Check its release notes. | No |
| `PRIMO` | The Primo agent's bundle is not cleaned yet. Primo handles it. | No |
| `INFO` | Context, such as a Group Policy that blocks Microsoft's automatic trust updates. | No |

The scripts exit with `0` when nothing is left to fix, `2` when at least one finding remains, and `1` on error.

> [!TIP]
>
> A device whose `PRIMO` line remains after a full day online needs a look on our side: [contact support](mailto:support@getprimo.com) with the device name.

The check covers the system trust store, Homebrew's CA bundle, `certifi` and `pip` installed in the standard Python locations, and Node.js installed with Homebrew, the official installer, nvm, fnm, Volta, asdf, mise, or NodeSource packages. Virtual environments, containers, and binaries outside these locations are not scanned: see [Update the runtimes that carry their own roots](#update-the-runtimes-that-carry-their-own-roots).

## Fix the system trust stores

Fleet, the open-source engine behind Primo device management, maintains one remediation script per OS. Download them from Fleet's repository and add them to the Script library like the check scripts:

| Script | What it changes |
| - | - |
| [remediate-emudhra-ca-compromise-macos.sh](https://github.com/fleetdm/fleet/blob/main/docs/solutions/macos/scripts/remediate-emudhra-ca-compromise-macos.sh) | Removes both roots from `/etc/ssl/cert.pem`. Upgrades the Homebrew `ca-certificates` and `certifi` formulae, as the Homebrew owner. |
| [remediate-emudhra-ca-compromise-linux.sh](https://github.com/fleetdm/fleet/blob/main/docs/solutions/linux/scripts/remediate-emudhra-ca-compromise-linux.sh) | Upgrades the `ca-certificates` package (apt, dnf, yum, zypper, apk). Where the distribution has no fix yet, distrusts the roots the native way: a `!` entry in `/etc/ca-certificates.conf` on Debian, Ubuntu, and Alpine, a p11-kit blocklist file on Fedora, RHEL, Arch, and openSUSE. |
| [remediate-emudhra-ca-compromise-windows.ps1](https://github.com/fleetdm/fleet/blob/main/docs/solutions/windows/scripts/remediate-emudhra-ca-compromise-windows.ps1) | Adds the 16 compromised intermediates to the machine's **Untrusted Certificates** store. It works even where Microsoft's automatic update is blocked or the device is offline. |

Each script also handles the Primo agent's bundle, which is already clean once Primo's fix has reached the device. Each one is idempotent, keeps a `*.pre-emudhra` backup of every file it edits, and exits with `0` when the device is protected, `2` when something needs a look, and `1` on error. A **Failed** run in **Run History** carries the reason in its output.

**1. Add the scripts to the Script library**

Download each script from Fleet's repository (**Download raw file** on GitHub), then drop the file into **Device Management** > **Scripts** > **Library** > **Add a script**. The form fills in the name and platform.

**2. Run each script on a pilot group**

Select **Run script** > **Target by device group** and pick a small group, ideally IT devices. Check their output in **Run History**.

**3. Run them on all devices**

Run each script again with **Target all devices**.

**4. Run the check again**

Run the check scripts once more. The `EXPOSED` lines for system bundles and the Windows block should be gone.

> [!WARNING]
>
> Most programs read the CA bundle only at startup. On Linux servers, restart long-running services after the bundle changes. On macOS, an OS update can restore `/etc/ssl/cert.pem`: run the check again after each macOS update.

> **Fix a single device by hand**
>
> **macOS** — remove both roots from the command-line bundle:
>
> ```bash theme={null}
> sudo cp -p /etc/ssl/cert.pem /etc/ssl/cert.pem.bak &&
> sudo awk '/^(###|===) /{skip=0}
>   /^=== .*\/CN=(AAA Certificate Services|emSign ECC Root CA - G3)$/{skip=1}
>   !skip' /etc/ssl/cert.pem.bak | sudo tee /etc/ssl/cert.pem > /dev/null
> ```
>
> **Debian and Ubuntu** — install the fixed package, or distrust the roots where it is not available yet:
>
> ```bash theme={null}
> sudo apt-get update && sudo apt-get install --only-upgrade ca-certificates
> # Only if the fixed package is not available yet:
> 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
> ```
>
> **Fedora, RHEL, Rocky Linux, AlmaLinux, and Amazon Linux 2023** — run as root:
>
> ```bash theme={null}
> dnf upgrade -y ca-certificates
> # Only if the fixed package is not available yet:
> 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
> ```
>
> **Alpine** — run as root: `apk add --upgrade ca-certificates`.
>
> **Windows** — run the Windows remediation script from an elevated PowerShell prompt:
>
> ```powershell theme={null}
> powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\remediate-emudhra-ca-compromise-windows.ps1
> ```
>
> These commands leave the Primo agent's bundle alone, since Primo cleans it.

> **Roll back a change**
>
> * **macOS and Linux:** copy a `*.pre-emudhra` backup over the file it came from. On Linux, also delete the `emudhra-*.pem` files the script added to the p11-kit blocklist directory, restore `/etc/ca-certificates.conf.pre-emudhra` if present, then run `update-ca-certificates` or `update-ca-trust`.
> * **Windows:** open `certlm.msc` and delete the 16 added entries from **Untrusted Certificates**.
>
> Do not restore the Primo agent's bundle: it would trust the compromised roots again.

> **Use the Fleet console instead**
>
> If your team works in the Fleet console, Fleet publishes the same detection as three reports, one per OS, in [emudhra-ca-compromise.reports.yml](https://github.com/fleetdm/fleet/blob/main/docs/solutions/all/reports/emudhra-ca-compromise.reports.yml). The matching policies are in [emudhra-ca-compromise.policies.yml](https://github.com/fleetdm/fleet/blob/main/docs/solutions/all/policies/emudhra-ca-compromise.policies.yml): set each policy's **Run script** automation to its remediation script and failing devices fix themselves.
>
> The macOS policy passes when neither root is left in `/etc/ssl/cert.pem` or the agent's bundle:
>
> ```sql theme={null}
> SELECT 1 FROM os_version WHERE NOT EXISTS (
>   SELECT 1 FROM certificates
>   WHERE path IN ('/etc/ssl/cert.pem', '/opt/orbit/certs.pem')
>     AND sha1 IN ('3043fa4ff257dca0c380ee2e58ea78b23fe6bbc1', 'd1eb23a46d17d68fd92564c2f1f1601764d8e349'));
> ```
>
> The Linux policy passes when neither root is left in any certificate store or the agent's bundle:
>
> ```sql theme={null}
> SELECT 1 FROM os_version WHERE NOT EXISTS (
>   SELECT 1 FROM certificates
>   WHERE sha1 IN ('3043fa4ff257dca0c380ee2e58ea78b23fe6bbc1', 'd1eb23a46d17d68fd92564c2f1f1601764d8e349'))
> AND NOT EXISTS (
>   SELECT 1 FROM certificates
>   WHERE path = '/opt/orbit/certs.pem'
>     AND sha1 IN ('3043fa4ff257dca0c380ee2e58ea78b23fe6bbc1', 'd1eb23a46d17d68fd92564c2f1f1601764d8e349'));
> ```
>
> The Windows policy passes when the agent's bundle no longer holds either root. Windows osquery cannot parse PEM files, so it matches one line unique to each root:
>
> ```sql theme={null}
> SELECT 1 FROM os_version WHERE NOT EXISTS (
>   SELECT 1 FROM file f JOIN file_lines l ON l.path = f.path
>   WHERE f.path = 'C:\Program Files\Orbit\certs.pem'
>     AND l.line IN ('CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7',
>                    '8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C'));
> ```

## Update the runtimes that carry their own roots

Scripts cannot fix these: each one embeds its own copy of the roots and needs a new release. Update them on developer workstations, servers, and container images, then rebuild the images and the binaries you build yourself.

| Software | Fixed in |
| - | - |
| Node.js | 22.23.4, 24.22.1, 26.12.1 |
| Electron (and the desktop apps built on it) | 42.11.16, 43.8.1, 44.9.1 |
| Go (rebuild every binary built with an older toolchain) | 1.26.10, 1.27.3 |
| Java | 8u511, 11.0.33.0.2, 17.0.21.0.2, 21.0.13.0.2, 25.0.5.0.2, 27.0.1.0.2 |
| Python | 3.13.17, 3.14.9, 3.15.1 |
| Python `certifi` | 2026.10.19 |
| `pip` | 26.2.2 |
| `uv` | 0.13.4 |
| Conda | 26.9.4 |
| Bun | 1.4.4 |
| Deno | 2.9.8 |
| Rust `webpki-roots` / `webpki-root-certs` | 1.0.10 |
| Rust `rustls-webpki` | 0.103.16 |
| Docker Desktop | 4.96.2 |
| Homebrew `ca-certificates` | 2026-09-25\_1 |
| curl CA bundle (`cacert.pem`) | 2026-10-20 |

Linux distributions ship the fix in their `ca-certificates` package:

| Distribution | Fixed `ca-certificates` version |
| - | - |
| Ubuntu 22.04, 24.04, 26.04, 26.10 | `20260816ubuntu0.22.04.1`, `20260816ubuntu0.24.04.1`, `20260816ubuntu0.26.04.1`, `20260816ubuntu0.26.10.1` |
| Debian 12, Debian 13 | `20250419~deb12u2`, `20250419+deb13u1` |
| Fedora | `2026.2.92_v9.0.319` |
| Alpine | `20261020-r0` |
| Arch Linux (`ca-certificates-mozilla`) | `3.131.1-1` |

Distribution-packaged `certifi` (for example `python3-certifi` on Debian and Ubuntu) points at the system bundle and follows the `ca-certificates` update. The live list of patched software is on the [disclosure site](https://emudhra.we-have-a-problem.com/).

## Verify

Two public test hosts serve certificates signed by a compromised intermediate: one chains through the emSign root, the other through the Sectigo cross-sign. A protected client refuses both.

```bash theme={null}
curl -sS -o /dev/null -w '%{http_code}\n' https://dv-g3b-emudhra.we-have-a-problem.com/
curl -sS -o /dev/null -w '%{http_code}\n' https://dv-g3b-aaa-emudhra.we-have-a-problem.com/
```

```powershell theme={null}
Invoke-WebRequest -UseBasicParsing https://dv-g3b-emudhra.we-have-a-problem.com/
```

Test the runtimes the same way:

```bash theme={null}
node -e "fetch('https://dv-g3b-emudhra.we-have-a-problem.com/').then(r => console.log(r.status), e => console.log('refused', e.cause?.code))"
python3 -c "import requests; print(requests.get('https://dv-g3b-emudhra.we-have-a-problem.com/').status_code)"
```

A certificate error means the client is protected. A `200` means the client still trusts the forged chain. If your network inspects TLS traffic, run the test from a network that does not, otherwise you measure the proxy.

## Reference: compromised certificates

The two roots to remove from CA bundles:

| Root | SHA-1 | Included in |
| - | - | - |
| `emSign ECC Root CA - G3` | `3043fa4ff257dca0c380ee2e58ea78b23fe6bbc1` | Apple, Chrome, Microsoft, Mozilla |
| `AAA Certificate Services` (Sectigo) | `d1eb23a46d17d68fd92564c2f1f1601764d8e349` | Apple, Microsoft (removed from Chrome and Mozilla) |

Two more roots issue the intermediates directly. Only Microsoft's store includes them, and Microsoft blocks the intermediates rather than these roots:

| Root | SHA-1 |
| - | - |
| `emSign Root TLS CA - G3` | `39d3236e4a609ab6190cbfa1ad02b3cc61dfc2c3` |
| `emSign Root CS CA - G3` | `1f74b6010a219fd9ca058d07aa053d15efd9300d` |

The 16 compromised intermediates:

| Intermediate | Use | Issued by | SHA-1 |
| - | - | - | - |
| EM DV TLS CA - G3B | TLS | emSign Root TLS CA - G3 | `a99b289464f3b44343270a7a1e910f02370483fe` |
| EM DV TLS CA - G3B | TLS | AAA Certificate Services | `1396be1372bc93dfa2a433ca964dba91548a6e57` |
| EM OV TLS CA - G3B | TLS | emSign Root TLS CA - G3 | `5f014e0db3bb8f950e0598ab280846d383fc57be` |
| EM OV TLS CA - G3B | TLS | AAA Certificate Services | `5dfa19197d1c1cf629af7c0cd865543c5050227c` |
| EM EV TLS CA - G3B | TLS | emSign Root TLS CA - G3 | `f33acd1f5d31aa81ac18c72c928446226d3d5914` |
| EM EV TLS CA - G3B | TLS | AAA Certificate Services | `19069bd6533bff93fb67c71fb3376b4ea7402c3b` |
| EM DV TLS CA - G3C | TLS | emSign Root TLS CA - G3 | `8ad23bc78845d6faae1d534bac89f0e36ba55a11` |
| EM DV TLS CA - G3C | TLS | AAA Certificate Services | `4b0944c14f8c2ccba4e4f32104ad920fa15ee331` |
| EM OV TLS CA - G3C | TLS | emSign Root TLS CA - G3 | `7fcc31aab874ce6c8ee8584f54cfd08646b54117` |
| EM OV TLS CA - G3C | TLS | AAA Certificate Services | `305fc1e253dcab71b95d4995dae3cb09d562f22a` |
| EM EV TLS CA - G3C | TLS | emSign Root TLS CA - G3 | `c988f3d0cc1629ffc43165d6464c9b6ef73d6e48` |
| EM EV TLS CA - G3C | TLS | AAA Certificate Services | `8588708e875a1f8aa83524f3559080f7e17068c7` |
| EM OV CS CA - G3B | Code signing | emSign Root CS CA - G3 | `d33195a244046a3263fb5e0e83c6803a5b2e7bc3` |
| EM OV CS CA - G3B | Code signing | AAA Certificate Services | `ee26d7ae3cc71718f6e07d7d0095076f0c158f63` |
| EM OV CS CA - G3C | Code signing | emSign Root CS CA - G3 | `1f4a5d178f8b6a8ef0f6784155bd5fea91158b12` |
| EM OV CS CA - G3C | Code signing | AAA Certificate Services | `7fde64753a957b98f172fd2ab0d922551909c7ba` |

> **SHA-256 fingerprints, serial numbers, and key identifiers**
>
> Both certificates of an intermediate share its key, so they share its subject key identifier (SKI).
>
> ```text theme={null}
> EM DV TLS CA - G3B (via emSign)
>   SHA-256  42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493
>   Serial   A4AEE678F23362B4C0E460B772F214
>   SKI      E21E2FE71C19390E554B62C120245735248A2514
>
> EM DV TLS CA - G3B (via Sectigo AAA)
>   SHA-256  45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE
>   Serial   A024FDF4BA665C785CE4A020483858B7
>   SKI      E21E2FE71C19390E554B62C120245735248A2514
>
> EM OV TLS CA - G3B (via emSign)
>   SHA-256  98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B
>   Serial   6CADA367FB1C100088120EA202D568
>   SKI      9BD86551AFE7E72C37AD08EE6919D9BF3784AE0A
>
> EM OV TLS CA - G3B (via Sectigo AAA)
>   SHA-256  5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699
>   Serial   E94223BDE92F914E5AB9ACABFDAC3710
>   SKI      9BD86551AFE7E72C37AD08EE6919D9BF3784AE0A
>
> EM EV TLS CA - G3B (via emSign)
>   SHA-256  E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838
>   Serial   C7C41EFE92D825F05EAE841A4A82C1
>   SKI      3EDC20D9894698877B873FD88147B25C15D84706
>
> EM EV TLS CA - G3B (via Sectigo AAA)
>   SHA-256  579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512
>   Serial   D8F8685E4F2BD9D82402A85A1C1EB731
>   SKI      3EDC20D9894698877B873FD88147B25C15D84706
>
> EM DV TLS CA - G3C (via emSign)
>   SHA-256  0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19
>   Serial   588B2E3C2BB8CDA13E1A1209254321
>   SKI      AE7E105C3C2996C1A847270559F1E03DD4E50AF3
>
> EM DV TLS CA - G3C (via Sectigo AAA)
>   SHA-256  62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72
>   Serial   91F347F1A5B7D39962B95E0EE4E299D1
>   SKI      AE7E105C3C2996C1A847270559F1E03DD4E50AF3
>
> EM OV TLS CA - G3C (via emSign)
>   SHA-256  CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F
>   Serial   296926C227A3A1526392B19482A504
>   SKI      34EAEA0CEBC90ADAF9D0C5E2ED9B78C9879E1B6C
>
> EM OV TLS CA - G3C (via Sectigo AAA)
>   SHA-256  7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489
>   Serial   8D5A4811939EF447D77111CDD9849AE9
>   SKI      34EAEA0CEBC90ADAF9D0C5E2ED9B78C9879E1B6C
>
> EM EV TLS CA - G3C (via emSign)
>   SHA-256  432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3
>   Serial   8009928A64107601AF05FA2F19F109
>   SKI      A6255E80A94DB7F3967C1BC791DF92BCEE4BEB5F
>
> EM EV TLS CA - G3C (via Sectigo AAA)
>   SHA-256  FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F
>   Serial   05F078752DECE7A1BB8595D303167171
>   SKI      A6255E80A94DB7F3967C1BC791DF92BCEE4BEB5F
>
> EM OV CS CA - G3B (via emSign)
>   SHA-256  9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4
>   Serial   7818933F8F85B4619A2B35FBA83D0A
>   SKI      79FAE8ECCFDB0D758DA458D69FB6EDDC7C49DEE6
>
> EM OV CS CA - G3B (via Sectigo AAA)
>   SHA-256  A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8
>   Serial   41E32B73AC185D6CE0D7480BACF84E69
>   SKI      79FAE8ECCFDB0D758DA458D69FB6EDDC7C49DEE6
>
> EM OV CS CA - G3C (via emSign)
>   SHA-256  4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1
>   Serial   544B7F922C31AE220A4A922916F6E6
>   SKI      EE6F73298D6FBFA7B31BC39534F61556447866B7
>
> EM OV CS CA - G3C (via Sectigo AAA)
>   SHA-256  84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD
>   Serial   D6A5EF613B390F32EF9CDF349F772841
>   SKI      EE6F73298D6FBFA7B31BC39534F61556447866B7
>
> emSign Root TLS CA - G3 (root)
>   SHA-256  7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D
>   Serial   0E760672F143459FC8FE0AB0BC05E394
>   SKI      E13AAB217CF1E34D54AED472B7FD8B5942209054
>
> emSign Root CS CA - G3 (root)
>   SHA-256  F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5
>   Serial   018FCEC927C116F59C997EDC0CCD2E2B
>   SKI      687403F885CA8D5450571D16E0C8DE450DABCD2D
>
> AAA Certificate Services (root)
>   SHA-256  D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4
>   Serial   01
>   SKI      A0110A233E96F107ECE2AF29EF82A57FD030A4B4
>
> emSign ECC Root CA - G3 (root)
>   SHA-256  86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B
>   Serial   3CF607A968700EDA8B84
>   SKI      7C5D028413D4CC8A9B81CE171C2E291E9C486342
> ```

## Sources

* [eMudhra intermediate CA compromise](https://emudhra.we-have-a-problem.com/): disclosure site, certificate list, test hosts, and patched versions
* [Mozilla Bugzilla 2083449](https://bugzilla.mozilla.org/show_bug.cgi?id=2083449): incident report and root cause
* [Fleet: find and fix the trust stores your browser doesn't cover](https://fleetdm.com/articles/emudhra-ca-compromise-trust-stores-fleet): detection, remediation scripts, and lab results
* [Google](https://blog.google/security/protecting-chrome-and-android-users-from-compromised-emudhra-intermediate-ca-certificates/) and [Mozilla](https://blog.mozilla.org/security/2026/10/20/protecting-firefox-users-following-the-compromise-of-emudhra-intermediate-certificates/): browser response
* [Node.js CLI documentation](https://nodejs.org/api/cli.html#--use-bundled-ca---use-openssl-ca) and [Go crypto/x509](https://pkg.go.dev/crypto/x509): how each runtime picks its roots


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.