The October 2026 eMudhra incident is one of the most instructive PKI failures in years. It was not a breach of a data center or a stolen HSM. It was a key-generation defect so severe that eight intermediate certificate authority private keys could be recovered from the public certificates themselves. For anyone running TLS inspection, code-signing validation, embedded devices, or custom trust stores, this incident is a practical stress test of how quickly you can find and remove trust you never intended to grant.
This guide breaks down what happened, why the keys were trivially recoverable, and gives you the commands, queries, and detection logic to verify your exposure across browsers, operating systems, language runtimes, and network sensors.
On 17 October at 20:06 UTC, Mozilla received a report, forwarded by the Chrome Root Program, that the private keys of 8 eMudhra intermediate certificates had been compromised. As Mozilla's Dennis Jackson put it, an attacker with the private key of any of the compromised intermediate CAs could impersonate any website or service. The researcher, identified only as 'Wayne', reported to the Chrome Root Program at 18:05 UTC and to Mozilla at 20:06 UTC. He demonstrated control of each key by signing a message with it reading "We have a problem here, Wayne".
The response from the ecosystem was fast:
Six of the eight intermediates were for TLS and two for code signing: EM DV TLS CA - G3B, EM DV TLS CA - G3C, EM EV TLS CA - G3B, EM EV TLS CA - G3C, EM OV TLS CA - G3B, EM OV TLS CA - G3C, EM OV CS CA - G3B, and EM OV CS CA - G3C, issued under eMudhra BV (NL) for the G3B variants and eMudhra Inc (US) for the G3C variants.
Keep these names handy. Every detection rule in this post keys off them.
This was not a sophisticated cryptanalytic attack. A typical RSA private key is 2,048 or 3,072 bits, but eMudhra's eight compromised keys were the numbers 4, 56, 66, 115, 155, 156, 230 and 237. Researcher Flüpke published one of the keys on GitHub, showing that the private key for the "EM OV TLS CA - G3C" certificate had a value of just 4.
A private value that small only makes sense as an elliptic curve private scalar. In ECC, the public key is the generator point multiplied by the private scalar: Q = d * G. If d is 4, anyone can compute G + G + G + G and compare it to the public key in the certificate. Recovering such a key is a loop, not an attack.
The keys were generated on April 30, 2025, and the weakness went undetected for about 18 months, according to eMudhra's October 21, 2026 statement. eMudhra said the affected intermediates came from a key-generation ceremony in April 2025 and that the incident did not affect its root CA private keys or other intermediate authorities.
The lesson for your own PKI is direct: key ceremonies need independent output validation, not just procedural sign-off. If you operate an internal CA, this is the moment to audit it. Redfox Cybersecurity runs PKI and cryptographic implementation assessments that test key generation, ceremony controls, and HSM configuration against exactly this failure mode.
The following Python script checks whether an EC public key in any certificate corresponds to a small private scalar. Run it against your internal intermediates and issuing CAs.
#!/usr/bin/env python3
# small_scalar_audit.py - flag EC certificates whose private key is a tiny integer
import sys
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import ec
LIMIT = 1 << 16 # scan scalars 1..65535
def load(path):
data = open(path, "rb").read()
try:
return x509.load_pem_x509_certificate(data)
except ValueError:
return x509.load_der_x509_certificate(data)
def audit(path):
cert = load(path)
pub = cert.public_key()
if not isinstance(pub, ec.EllipticCurvePublicKey):
print(f"[skip] {path}: not an EC key")
return
target = pub.public_numbers()
curve = pub.curve
for d in range(1, LIMIT):
cand = ec.derive_private_key(d, curve).public_key().public_numbers()
if cand.x == target.x and cand.y == target.y:
print(f"[CRITICAL] {cert.subject.rfc4514_string()} private scalar = {d}")
return
print(f"[ok] {cert.subject.rfc4514_string()}: no scalar below {LIMIT}")
for p in sys.argv[1:]:
audit(p)
[cta]
Run it across an exported chain directory:
pip install cryptography
find /opt/pki/issued-cas -name '*.pem' -print0 | xargs -0 python3 small_scalar_audit.py
[cta]
For RSA material, complement this with the badkeys project, which checks for known weak-key classes such as shared primes, Fermat-factorable moduli, and ROCA-affected keys:
pip install badkeys
badkeys --update-bl
badkeys -a /opt/pki/issued-cas/*.pem
[cta]
eMudhra said its core infrastructure was not compromised, no data was breached, and its physical and digital security controls remained intact. Co-founder Kaushik Srinivasan told PTI the affected keys had not been used to issue customer certificates.
That is reassuring, but incomplete. As one expert noted, the absence of issued end-entity certificates is encouraging, but someone holding a compromised intermediate key could generate fraudulent certificates without requesting them from eMudhra. eMudhra's issuance logs say nothing about what a third party might have minted offline. It remains unclear whether attackers might have already used the compromised keys to issue fraudulent certificates.
Certificate Transparency is your best public signal. Browsers enforcing CT would reject a forged certificate that was never logged, but a determined attacker targeting non-browser clients (mobile apps, IoT, API clients) has no reason to log anything. Still, check. crt.sh exposes a read-only PostgreSQL interface:
psql -h crt.sh -p 5432 -U guest certwatch <<'SQL'
SELECT ca.name AS issuer,
c.id AS crtsh_id,
x509_commonName(c.certificate) AS subject_cn,
x509_notBefore(c.certificate) AS not_before
FROM certificate c
JOIN ca ON ca.id = c.issuer_ca_id
WHERE ca.name ~* 'EM (DV|OV|EV) TLS CA - G3[BC]'
OR ca.name ~* 'EM OV CS CA - G3[BC]'
ORDER BY not_before DESC
LIMIT 200;
SQL
[cta]
CT log operators were also asked to reject certificates chaining to these compromised intermediates going forward to prevent risk to the CT ecosystem. If you run your own CT monitoring (for example, via certstream or a commercial feed), add alerts for any of your domains appearing under these issuer names.
Mozilla's Dennis Jackson stood up a live test endpoint. Opening dv-g3b-emudhra.we-have-a-problem.com shows a certificate error if you are protected; if a single line reading "That's bad!" appears, the client still trusts the compromised CAs and needs an update.
Test non-browser clients from the command line:
# Expect a verification failure on a patched host
curl -sv https://dv-g3b-emudhra.we-have-a-problem.com/ 2>&1 | grep -Ei 'SSL certificate|verify|That.s bad'
# Show which CA bundle curl is using
curl -sv https://example.com/ 2>&1 | grep -i 'CAfile\|CApath'
# Inspect the served chain and issuer names
openssl s_client -connect dv-g3b-emudhra.we-have-a-problem.com:443 \
-servername dv-g3b-emudhra.we-have-a-problem.com -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject
[cta]
macOS deserves special attention. Jackson warned that macOS's built-in /usr/bin/curl may continue trusting the compromised certificates because its certificate bundle does not check revocation information. Any script, cron job, or CI runner on macOS that relies on system curl should be considered unverified until tested.
Revocation only works when the client checks it. Software that uses its own certificate stores or does not check revocations may keep trusting the compromised intermediates until it is updated. Jackson's tracker lists fixed versions, including NSS 3.131.1, Go 1.27.3 and 1.26.10, Python certifi 2026.10.19, pip 26.2.2, uv 0.13.4, Alpine ca-certificates 20261020-r0, and Arch ca-certificates-mozilla 3.131.1-1, while Debian, FreeBSD and Node.js were still pending at the time of reporting.
# Debian/Ubuntu: update and confirm emSign roots are gone or distrusted
sudo apt-get update && sudo apt-get install --only-upgrade ca-certificates
dpkg -l ca-certificates | tail -1
grep -i emsign /etc/ca-certificates.conf
# RHEL/Fedora: list trust anchors via p11-kit
trust list --filter=ca-anchors | grep -i -A2 emsign
# Explicit block on RHEL-family if updates lag
sudo cp emSign-ECC-Root-CA-G3.pem /etc/pki/ca-trust/source/blocklist/
sudo update-ca-trust extract
# Explicit block on Debian-family: prefix the entry with "!" and rebuild
sudo sed -i 's|^\(mozilla/emSign_ECC_Root_CA_-_G3.crt\)|!\1|' /etc/ca-certificates.conf
sudo update-ca-certificates --fresh
[cta]
Do not forget container images. A patched host does nothing for a two-year-old base image baked into production. Rebuild images and scan them:
# Find images still shipping an emSign anchor
for img in $(docker images --format '{{.Repository}}:{{.Tag}}'); do
if docker run --rm --entrypoint sh "$img" -c \
'cat /etc/ssl/certs/ca-certificates.crt 2>/dev/null' | grep -q 'emSign'; then
echo "[!] $img still contains emSign material"
fi
done
[cta]
The grep above catches bundles that include human-readable comments; for bundles that do not, parse with OpenSSL:
awk 'BEGIN{n=0} /BEGIN CERT/{n++} {print > ("/tmp/c" n ".pem")}' /etc/ssl/certs/ca-certificates.crt
for f in /tmp/c*.pem; do openssl x509 -in "$f" -noout -subject 2>/dev/null; done | grep -i emsign
[cta]
# Python: certifi version and bundle contents
python3 -c "import certifi; print(certifi.__version__, certifi.where())"
pip install --upgrade 'certifi>=2026.10.19'
# Go: embedded fallback roots are tied to toolchain version
go version # rebuild binaries with 1.27.3 / 1.26.10 or later
# Node.js: bundled roots ship inside the binary
node -e "const {X509Certificate}=require('crypto');require('tls').rootCertificates
.map(p=>new X509Certificate(p)).filter(c=>/emSign/i.test(c.subject))
.forEach(c=>console.log(c.subject.replace(/\n/g,', ')))"
# Until a patched Node ships, rely on a patched OS store instead of the bundled list
node --use-openssl-ca app.js
[cta]
Java is the quiet risk. An unpatched JDK could continue to accept a forged certificate signed with a compromised intermediate key if the relevant chain remains trusted. Java does not perform revocation checking by default, so remove the anchor explicitly:
keytool -list -cacerts -storepass changeit | grep -i emsign
keytool -delete -alias "emsigneccrootcag3" -cacerts -storepass changeit
# If you must keep the store but enforce revocation checks at runtime
java -Dcom.sun.net.ssl.checkRevocation=true \
-Dcom.sun.security.enableCRLDP=true \
-Docsp.enable=true -jar service.jar
[cta]
If your estate spans dozens of runtimes and appliances, mapping every embedded trust store by hand gets slow. The Redfox Cybersecurity team can run an infrastructure-wide trust store inventory as part of a targeted assessment, so you know which systems are still exposed rather than guessing.
Google, Mozilla, Apple, and Microsoft all took emergency measures, but endpoints that are offline, air-gapped, or blocked from Windows Update will not pick up those changes automatically. Push explicit distrust through your management tooling.
# Find any emSign anchors or the compromised intermediates on Windows
Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\CA |
Where-Object { $_.Subject -match 'emSign|EM (DV|OV|EV) TLS CA - G3|EM OV CS CA - G3' } |
Select-Object PSParentPath, Subject, Thumbprint, NotAfter
# Add each compromised intermediate to the Untrusted (Disallowed) store
Get-ChildItem .\emudhra-ica\*.cer | ForEach-Object {
certutil -addstore -f Disallowed $_.FullName
}
[cta]
# macOS: locate and explicitly deny
security find-certificate -a -c "emSign" -Z \
/System/Library/Keychains/SystemRootCertificates.keychain | grep -E 'SHA-256|labl'
sudo security add-trusted-cert -d -r deny \
-k /Library/Keychains/System.keychain ./EM_DV_TLS_CA_G3B.cer
[cta]
Palo Alto's Unit 42 recommends the same approach at scale: deploy updated root and intermediate trust stores via MDM across all endpoints, servers and container images to explicitly distrust or revoke the affected eMudhra CAs.
Two of the eight keys were code-signing intermediates. Compromised code-signing keys posed similar risks for software authentication. A forged Authenticode signature chained to EM OV CS CA - G3B or G3C could make malware look like legitimately signed software to any validator that still trusts the chain.
$pattern = 'EM OV CS CA - G3[BC]'
Get-ChildItem -Path C:\ -Recurse -File -Include *.exe,*.dll,*.sys,*.msi,*.ps1 `
-ErrorAction SilentlyContinue |
ForEach-Object {
$sig = Get-AuthenticodeSignature -FilePath $_.FullName -ErrorAction SilentlyContinue
if ($sig.SignerCertificate) {
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
$null = $chain.Build($sig.SignerCertificate)
if ($chain.ChainElements.Certificate.Subject -match $pattern) {
[pscustomobject]@{
Path = $_.FullName
Signer = $sig.SignerCertificate.Subject
Status = $sig.Status
SHA256 = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
}
}
}
} | Export-Csv .\emudhra_cs_hits.csv -NoTypeInformation
[cta]
Validate any hits with signtool to see the full chain and timestamp:
signtool verify /pa /v /all "C:\path\to\suspect.exe"
[cta]
# Verify and print the signer chain for a Windows binary
osslsigncode verify -in suspect.exe 2>&1 | grep -E 'Subject|Issuer|Signature verification'
# Extract the PKCS#7 blob and dump every certificate in it
osslsigncode extract-signature -in suspect.exe -out sig.p7
openssl pkcs7 -inform DER -in sig.p7 -print_certs -noout | grep -E 'subject|issuer'
[cta]
Feed hits into your EDR as a hunting query. Most modern EDR platforms expose signer issuer fields; a rule that flags any process image whose signing chain includes these issuer strings is cheap and high-signal. If you want a red team to test whether your EDR and application control actually block a binary signed under a revoked-but-unchecked chain, Redfox Cybersecurity's adversary simulation services can build that scenario safely in your environment.
Endpoint patching takes time. Network sensors can tell you immediately whether anything in your environment is negotiating TLS with a chain issued by one of these intermediates.
alert tls any any -> $HOME_NET any (msg:"PKI Leaf certificate issued by compromised eMudhra intermediate"; \
flow:established,to_client; tls.cert_issuer; \
pcre:"/EM (DV|OV|EV) TLS CA - G3[BC]/"; \
classtype:bad-unknown; sid:9100101; rev:1; metadata:created_at 2026_10_28;)
alert tls any any -> $HOME_NET any (msg:"PKI Compromised eMudhra intermediate present in served chain"; \
flow:established,to_client; tls.certs; \
content:"TLS CA - G3"; pcre:"/EM (DV|OV|EV) TLS CA - G3[BC]/"; \
classtype:bad-unknown; sid:9100102; rev:1;)
[cta]
Note that TLS 1.3 encrypts certificates on the wire, so these rules only fire on TLS 1.2 and earlier or where you terminate and inspect traffic. For TLS 1.3, pull the same logic into your TLS inspection proxy or into JA4X fingerprints if your stack supports them.
module EmudhraWatch;
export {
redef enum Notice::Type += { Compromised_Issuer };
}
const bad_issuers = /EM (DV|OV|EV) TLS CA - G3[BC]|EM OV CS CA - G3[BC]/;
event x509_certificate(f: fa_file, cert_ref: opaque of x509, cert: X509::Certificate)
{
if ( bad_issuers in cert$issuer || bad_issuers in cert$subject )
{
NOTICE([$note=Compromised_Issuer,
$msg=fmt("eMudhra compromised CA in chain: subject=%s issuer=%s",
cert$subject, cert$issuer),
$sub=cert$serial,
$identifier=cert$serial]);
}
}
[cta]
Retroactively search your existing Zeek logs too:
zcat /opt/zeek/logs/*/x509.*.log.gz | zeek-cut ts certificate.subject certificate.issuer \
| grep -E 'EM (DV|OV|EV) TLS CA - G3[BC]|EM OV CS CA - G3[BC]'
[cta]
A lot of confusion in India centered on whether digital signature certificates and eSign were affected. eMudhra stated the incident does not affect its India DSC and eSign businesses, which operate on separate infrastructure from the global TLS business. MeitY said the impact on Indian websites and applications had been very limited and for a very short interval. eMudhra said the impact was confined to one customer ecosystem in the United States, whose certificates are being replaced through an alternate publicly trusted CA hierarchy.
If you hold any certificates chaining to emSign G3 roots, follow Unit 42's guidance: immediately revoke and reissue them through an alternative, fully trusted CA. With the Chrome root removal rolling out from October 27, those certificates will fail in Chrome regardless.
The eMudhra case exposes several systemic weaknesses that apply far beyond one CA:
Key ceremonies need cryptographic output validation. Procedural checklists and witnesses confirmed the ceremony happened; nothing confirmed the keys were sound. Every generated key should be tested for basic entropy and weak-key properties before the CSR is signed.
Revocation is not a universal kill switch. Browsers have CRLSets and OneCRL. Most other software has a static bundle and no revocation checking. Your exposure window is defined by your slowest-updating trust store, not by the CA's revocation timestamp.
Trust store inventory is an asset management problem. Most organizations cannot answer "which systems trust root X" within an hour. That should be a standing capability, not an incident-time scramble.
Code signing trust is often overlooked. Teams patched browsers in a day. Few checked whether application allowlisting or EDR would accept a binary signed under the compromised CS intermediates.
Building these capabilities before the next CA incident is far cheaper than improvising during one. Redfox Cybersecurity helps organizations harden PKI governance, validate trust store hygiene, and test detection coverage through hands-on penetration testing and purple team exercises.