针对 2026 年 10 月 eMudhra / emSign 中间 CA 私钥泄露事件 的检测工具。八个中间 CA 使用 P-384 密钥签发,其私钥标量为极小的整数(恢复出的值为 4, 56, 66, 115, 155, 156, 230, 237),因此任何人都可以轻易伪造能链接到这些 CA 的证书。浏览器和操作系统信任存储很快将其标记为不信任——但 大多数非浏览器信任存储不会自动更新,这正是本工具所要发现的问题。
状态:该事件仍在进行中,因此请将内置的 IOC 视为快照(每次运行都会打印其
generated日期)。如需更新,请从上游跟踪器刷新tools/sources/*.json并重新运行生成器(见下文)。
trustscan(Python)和 trustscan.ps1(Windows/PowerShell)会清点主机上的信任存储和 CA 捆绑包,并标记任何与受影响证书匹配的项。仅依赖标准库 + cryptography;无其他依赖。
需要 Python 3 和 cryptography(唯一的依赖):
pip install -r requirements.txt
# scan this host's trust stores and CA bundles
python trustscan.py
# machine-readable for ticketing
python trustscan.py --format csv > findings.csv
# Windows-native: cert stores (both scopes) + GPO roots + env bundles
pwsh -File trustscan.ps1
退出码:0 = 干净,1 = 发现项,2 = 错误。仅含 INFO 的结果(见下方
--flag-aaa)退出码为 0。
每次运行结束时都会输出一行关于 AAA 交叉签名路径的提示(见
范围与限制)。在 text 模式下,它是
报告的一部分;在 json/csv 模式下,它输出到 stderr,以便机器可读
输出保持干净。
在下方任何位置找到的每张证书都会被计算指纹,并以四种方式检查:
| 匹配 | 严重性 | 含义 |
|---|---|---|
compromised_ca_cert | CRITICAL | 完全匹配的受泄露中间证书(证书 SHA-256) |
compromised_key_reuse | CRITICAL | 携带受泄露密钥的另一张证书(SPKI SHA-256)——可捕获 Sectigo 交叉签名及任何重新签发 |
issued_by_compromised_ca | HIGH | 其 AuthorityKeyIdentifier 指向受泄露中间证书的叶子证书/子 CA——可被伪造 |
distrust_root_present | CRITICAL | 一个不受信任的 emSign 根证书在此处仍受信任。其自身密钥完好——但中间证书密钥是公开的,因此只要该根证书受信任,为任何域伪造的证书都会对所有不检查吊销状态的客户端(大多数非浏览器客户端)验证通过。请移除它 / 更新证书包。仅此一项并不能关闭 AAA 交叉签名路径(见下文) |
还有一项检查是可选的(--flag-aaa / -FlagAaa):
| 匹配 | 严重性 | 含义 |
|---|---|---|
cross_sign_root_present | INFO | 合法的 Sectigo 根证书 AAA Certificate Services 存在。它交叉签署了这八个中间证书,因此它锚定了它们的第二条信任路径;其自身密钥完好。Mozilla 和 Chrome 已于 2025 年 4 月弃用它,但 Windows、Apple、较旧的 Linux 证书包以及许多 Java 密钥库仍然信任它。仅在请求时报告,供决定是否移除它的团队参考(Debian 针对此事件的更新会移除它);绝不影响退出码 |
默认情况下不会标记 AAA Certificate Services:其自身密钥完好, 弃用它是一项有意的选择,而非默认的修复措施。关于这一点为何重要,请参见下方的 限制说明。
trustscan 的检查位置| 来源 | 说明 | 检查的位置 |
|---|---|---|
certifi | Python CA 证书包 | certifi.where()(运行时解析) |
bundles → 发行版 | 操作系统 ca-certificates 聚合包 | Debian/Arch/Alpine /etc/ssl/certs/ca-certificates.crt · RHEL /etc/pki/tls/certs/ca-bundle.crt、/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem、合并后的 Java cacerts、管理员 anchors/ 目录 · SUSE /etc/ssl/ca-bundle.pem、/var/lib/ca-certificates/ca-bundle.pem · Debian 源目录 /usr/share/ca-certificates/ · /etc/ssl/cert.pem |
bundles → FreeBSD | ca_root_nss | /usr/local/share/certs/ca-root-nss.crt · /usr/local/etc/ssl/cert.pem |
bundles → Homebrew / MacPorts | brew / port ca-certificates、openssl | /opt/homebrew/etc/ca-certificates/cert.pem · /opt/homebrew/etc/openssl*/cert.pem · /usr/local/etc/...(Intel) · /opt/local/etc/{openssl,ssl}/cert.pem |
bundles → curl | 独立 curl 证书包 | /usr/local/share/curl/curl-ca-bundle.crt · /opt/local/share/curl/curl-ca-bundle.crt · C:\Program Files\Git\...\ca-bundle.crt |
bundles → conda | 每个环境的 ssl/cacert.pem | $CONDA_PREFIX · /opt/conda · ~/anaconda3、~/miniconda3、~/miniforge3(含 envs/*) |
bundles → AWS CLI v2 | 内置的 botocore | /usr/local/aws-cli/.../botocore/cacert.pem · C:\Program Files\Amazon\AWSCLIV2\... |
bundles → 云 SDK / 注册表 / 生态系统 | 更多内置证书包 | gcloud / Azure CLI third_party/certifi · 容器注册表 /etc/{docker,containers}/certs.d/*/ · Perl Mozilla::CA · Erlang/Elixir certifi |
bundles → pip | pip 内置的 certifi | <pip>/_vendor/certifi/cacert.pem(通过 import pip 解析) |
nss | Firefox / Thunderbird / Chrome-Linux + 系统 NSS 数据库 | ~/.mozilla/firefox/*/cert9.db · ~/.thunderbird/*/cert9.db · ~/snap/firefox/common/.mozilla/firefox/*/cert9.db · ~/.pki/nssdb/cert9.db · /etc/pki/nssdb/cert9.db · %APPDATA%\Mozilla\Firefox\Profiles\*\cert9.db · %APPDATA%\Thunderbird\Profiles\*\cert9.db |
| 来源 | 检查的位置 |
|---|---|
os(Linux) | /etc/ssl/certs · /usr/local/share/ca-certificates · /etc/pki/tls/certs · /etc/pki/ca-trust/extracted/pem(递归),以及 Android/AOSP /system/etc/security/cacerts、/apex/com.android.conscrypt/cacerts、cacerts-added/(以哈希命名的文件) |
os(macOS) | 对默认钥匙串 + SystemRootCertificates.keychain 执行 security find-certificate -a -p |
os(Windows,Python) | 对 ROOT、CA、MY、TrustedPublisher、AuthRoot 调用 ssl.enum_certificates + 组策略推送的 LocalMachine 根证书(注册表) |
java | $JAVA_HOME + JVM 根目录(/usr/lib/jvm/*、/opt/java/*、/Library/Java/JavaVirtualMachines/*、C:\Program Files\Java\*、Adoptium、Corretto、Microsoft)→ .../lib/security/cacerts |
env | SSL_CERT_FILE、REQUESTS_CA_BUNDLE、CURL_CA_BUNDLE、NODE_EXTRA_CA_CERTS 中的文件;SSL_CERT_DIR 中的目录 |
containers(可选启用) | Docker /var/lib/docker/overlay2/*/diff、podman(root + rootless,含其他用户)、containerd overlayfs 快照 - 扫描每一层的聚合证书包;需要 root,位置会标明层 |
apptrust(可选启用) | 应用服务器与数据基础设施的 Java 信任存储(Tomcat、WildFly/JBoss、Kafka、Elasticsearch)以及 /opt/*/jre/.../cacerts、C:\Program Files\*\jre\... 下的应用内置 JRE - 主机 Java 扫描会遗漏的私有厂商内置 JRE |
--path 目标 | 配置引用的存储(nginx、Apache、HAProxy、Oracle sqlnet.ora、LDAP),以及各项目的证书包(Composer vendor/composer/ca-bundle/...) - 将 --path 指向它们 |
paths | 你传入的任何文件/目录 |
--rootfs <dir> | 解压后的镜像或固件 rootfs 内部的发行版 / curl / Java / Android 存储位置 |
env变量保存的是路径,而非证书数据 -trustscan会读取该路径下的文件。
--pathcertifi 并非唯一自带 CA 根证书的 Python 包。以下这些包在 site-packages 内自带 *.pem 文件,独立于 certifi 和操作系统证书存储:
| 包 | 自带文件 |
|---|---|
| botocore (AWS CLI / boto3) | botocore/cacert.pem |
| grpcio (gRPC) | grpc/_cython/_credentials/roots.pem |
| httplib2 | httplib2/cacerts.txt(扩展名特殊——请直接传入该文件) |
| requests(旧版 <2.16) | requests/cacert.pem |
默认扫描仅自动定位 certifi 和 pip 内置的 certifi,且仅针对运行 trustscan.py 的解释器。上述其他所有包——以及机器上所有其他 virtualenv 或 conda 环境——都必须通过将 --path 指向 site-packages 来显式扫描:
python trustscan.py "$(python -c 'import site;print(site.getsitepackages()[0])')"
# or cover many environments at once
python trustscan.py /opt/venvs/ /home/*/.virtualenvs/ /opt/conda/envs/
# Windows / PowerShell equivalent
python trustscan.py (python -c "import site; print(site.getsitepackages()[0])")
操作系统根存储(/etc/ssl/certs 等)不包含这些证书——库之所以自带一份副本,正是为了不依赖它。目录扫描只会打开证书扩展名的文件,因此像 cacerts.txt 这样的特殊名称必须作为文件直接传入。
trustscan.ps1)更进一步Python 的 os 来源覆盖 LocalMachine 存储和 LocalMachine GPO 根证书。trustscan.ps1 补充了 Python 标准库在没有 Win32 互操作时无法触及的一项:CurrentUser 作用域(相同的逻辑存储)——此外它还原生处理 env 捆绑包和 -Path 文件/目录。当需要按用户信任时使用它;否则 Python 工具的 Windows 覆盖范围是等效的。
PEM/DER X.509、PKCS#7(.p7b/.p7c)、PKCS#12(.p12/.pfx,尝试使用 changeit/空密码),以及 JKS 密钥库——全部仅用标准库解析。
干净的结果并不意味着主机是安全的。 被入侵的中间证书还被 Sectigo 的 AAA Certificate Services 交叉签名,这是一个合法根证书,Mozilla 和 Chrome 于 2025 年 4 月根据 15 年根证书年龄政策将其从根存储中移除,但 Windows 和 Apple 仍然信任它,较旧的 Linux ca-certificates 包以及许多 Java 和设备密钥库也是如此(Debian 针对此事件的安全更新将其移除)。本工具默认不会标记它,因为移除它是一个深思熟虑的决定,而非默认的修复措施。不检查吊销状态的客户端即使所有 emSign 根证书都已消失,仍可能通过该路径接受伪造的证书链。真正的修复方法是按 SPKI 阻止这八个中间密钥(正如 Chrome 和 Go 所做的那样——交叉签名的副本是密钥相同但证书不同的证书,因此按证书哈希阻止会漏掉它们)、修补客户端,或启用吊销检查;仅移除根证书本身是不够的。此扫描器查找过时和已知不良的信任锚;它不能证明客户端会拒绝伪造的证书链。(--flag-aaa / -FlagAaa 将 AAA 的存在报告为 INFO 级别发现,供已选择移除它的团队使用。)
trustscan 无法覆盖编译进二进制的根存储。 Go、Node.js、uv、Electron、Bun 以及 Rust 的 webpki-roots/rustls-webpki 将其 CA 根证书烘焙进二进制文件——没有证书文件可供扫描,因此扫描器无法检测它们。许多将根证书编译进固件的嵌入式/IoT 设备(mbedTLS、wolfSSL)也是如此。对于这些,唯一有效的检查是*“运行时是否 ≥ 已修补版本?”*——即版本/补丁检查,这超出了本工具的范围。
它是按主机、时间点进行的清单。 在机群规模下,将 CSV/JSON 输入到现有流水线中,或在 CI 中对每个镜像使用 --rootfs。
emsign_iocs.json(16 个证书条目 = 8 个密钥 × 两条签发路径、它们的 SPKI 哈希、被入侵的 CA 密钥标识符以及受影响的根证书)是生成并验证的,而非转录的。tools/build_iocs.py 会重新解析每个证书、重新计算每个指纹、交叉核对序列号,并在写入文件之前通过恢复其小标量来重新确认每个密钥是弱密钥。