eMudhra 有 8 张中间 CA 证书的私钥泄露了。这两天 HN 上讨论最多的是 Apple 那篇:到 10 月 28 日,macOS 自带的 git、curl、python3 还信任这批证书。评论区基本默认这是 macOS 的事,Windows 有微软的证书更新兜着。
我用原文作者公开的测试端点,把自己这台 Windows 开发机上的工具逐个测了一遍。结果是:Git Bash 里的 git 和 curl、Node 24.11、Python 的 requests/pip 全都放行;同一个 git 换成 Windows 自己的 TLS 库就拒绝了。下面是测试命令、原样输出、每个工具的修法,还有几个照着原文测会遇到的坑(挂着代理时演示永远是 INCONCLUSIVE,--use-system-ca 没用,Python 拒绝也只是运气好)。
先说结论
| 工具(本机版本) | 证书从哪来 | 实测 | 修法(均已实测) |
|---|---|---|---|
| Git for Windows 2.31,安装时选了 OpenSSL | 自带 ca-bundle.crt |
放行 ,ls-remote 列出 commit |
git config --global http.sslBackend schannel,或升级到 2.56.0.3+ |
| Git for Windows 2.56.0.2,OpenSSL 后端 | 自带 ca-bundle.crt |
放行,端到端演示 README 以 INTERCEPTED 开头 | 升级到 2.56.0.3+(我测的是 2.56.0.4,已拒绝) |
| 任何版本的 git,用 schannel 后端 | Windows 证书体系 | 拒绝,CRYPT_E_REVOKED |
--- |
| Git Bash 里的 curl 7.75 | 自带 bundle(OpenSSL) | 放行,4 个端点都返回 200 | CURL_SSL_BACKEND=schannel,或升级 Git |
Windows 自带 curl.exe 8.13 |
Schannel | 拒绝 | --- |
| Node 24.11.0 | 编译进 Node 的 Mozilla 根 | 放行 ,--use-system-ca 也一样 |
升级到 24.22.1 / 22.23.4 / 26.12.1 |
| Python 3.11 requests、pip 24.0 | certifi | 放行 | certifi 升到 2026.10.19+,pip 升到 26.2.2+ |
Python 3.11 urllib |
Windows 根证书库 | 拒绝,但只是这台机器碰巧没有那个根 | 不能当成已修 |
自查只需要一条命令。在 Git Bash 里执行:
bash
git ls-remote https://git-emudhra.we-have-a-problem.com/check.git
报证书错误,说明你的 git 不信任这批证书。如果它列出了一个 commit,你连 GitHub 等任何 https 远端的连接都能被中间人冒充。

背景(一句话版)
据事件主页:eMudhra 生成这 8 张中间证书私钥的方式有缺陷,任何人都能从证书本身把私钥恢复出来,而且这个问题从 2025 年 4 月证书签发时就存在。报告人 10 月 17 日用 8 把私钥分别签了一句 "We have a problem here" 交给各家根证书计划。之后各家陆续拦截:Firefox 和 Chrome 从 17 日起,微软平台从 20 日起,Apple 从 22 日起。
麻烦在于浏览器以外的客户端。主页原文是 "many other clients do not consume revocation information and require software updates":自带 CA 列表的程序不读吊销信息,只能等程序自己发新版换掉列表。另外这批证书从没进过 CT 日志,强制检查 CT 的浏览器不会被骗,但 curl 这类命令行工具大多不查 CT。
实测
环境:Windows 10,Git for Windows 2.31.1(2021 年装的,装的时候选了 OpenSSL),Node 24.11.0,Python 3.11.9。本机 HTTPS_PROXY 指向一个本地代理,这一点后面会讲到。
1. git:一条命令自查,换后端立刻拒绝
text
$ git config --show-origin --get-all http.sslBackend
file:D:/Program Files/Git/etc/gitconfig openssl
$ git ls-remote https://git-emudhra.we-have-a-problem.com/check.git
2975b9930644b321e18ecae09e29c15b955e6103 HEAD
2975b9930644b321e18ecae09e29c15b955e6103 refs/heads/main
$ git -c http.sslBackend=schannel ls-remote https://git-emudhra.we-have-a-problem.com/check.git
fatal: unable to access 'https://git-emudhra.we-have-a-problem.com/check.git/': schannel: next InitializeSecurityContext failed: CRYPT_E_REVOKED (0x80092010)
同一个 git.exe 连同一个地址,唯一的区别是用哪套 TLS 库验证证书。OpenSSL 后端读 Git 安装目录里的 ca-bundle.crt,这份文件 2021 年之后就没更新过;schannel 交给 Windows 验证,Windows 已经把这批中间证书判成吊销。
用 openssl 看一眼服务器发的证书链,就知道 bundle 里哪个根在起作用:
text
$ openssl s_client -connect git-emudhra.we-have-a-problem.com:443 -servername git-emudhra.we-have-a-problem.com
0 s:CN = git-emudhra.we-have-a-problem.com
i:C = NL, O = eMudhra BV, CN = EM DV TLS CA - G3B
1 s:C = NL, O = eMudhra BV, CN = EM DV TLS CA - G3B
i:C = IN, O = eMudhra Technologies Limited, CN = emSign Root TLS CA - G3
2 s:C = IN, O = eMudhra Technologies Limited, CN = emSign Root TLS CA - G3
i:C = IN, OU = emSign PKI, O = eMudhra Technologies Limited, CN = emSign ECC Root CA - G3
再看三份 ca-bundle.crt 里有没有这个根(另外 6 张 Sectigo 交叉签名的中间证书链到 AAA Certificate Services,一起查):
text
== D:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crt (2.31.1,2021-03-27)
subject=C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services
subject=C = IN, OU = emSign PKI, O = eMudhra Technologies Limited, CN = emSign ECC Root CA - G3
== PortableGit 2.56.0.2 (2026-10-05)
subject=C = IN, OU = emSign PKI, O = eMudhra Technologies Limited, CN = emSign ECC Root CA - G3
== PortableGit 2.56.0.4 (2026-10-25)
(两个都没有了)
(为了好读,我把输出里其他 emSign 根删掉了,它们和这次事件无关。)
Git for Windows 2.56.0.3 的发布说明第一句就是 "This is a security fix release, addressing the eMudhra Intermediate CA Compromise",说明修复靠的就是换掉 bundle。
2. 端到端演示:我前三次都没跑出来
原文的演示命令用 http.curloptResolve 把 github.com 临时指到测试服务器。如果克隆成功、README 以 INTERCEPTED 开头,就说明 git 把冒充 GitHub 的服务器当成了真的。原文特意提醒要保留 fake-user@ 前缀,否则 git 会把你的 GitHub 凭据发给测试服务器。我额外加了 -c credential.helper= 和 GIT_TERMINAL_PROMPT=0,免得本机的凭据管理器插手。
第一次,用本机的 2.31.1:
text
$ git -c http.curloptResolve=github.com:443:15.235.19.186 clone https://fake-user@github.com/galadran/emudhra-git-test demo-openssl
Cloning into 'demo-openssl'...
$ head -5 demo-openssl/README.md
# INCONCLUSIVE: you reached the real GitHub
This README belongs in the real repository at
https://github.com/galadran/emudhra-git-test. You are reading it because your
git connected to the real github.com, not to our test server, so the test did
README 里给了原因:"a git older than 2.37, which silently ignores the http.curloptResolve setting"。2.31 不认识这个配置,也不报错,直接连到了真的 GitHub。
第二次 ,我下载了 MinGit 2.56.0.2(修复前的最后一版)和 2.56.0.4,并按发布说明核对了 SHA-256。结果还是 INCONCLUSIVE。原因是我开着代理:HTTPS_PROXY 生效时,git 让代理去解析 github.com,curloptResolve 就不起作用了。国内开发机大多挂着代理,照原文命令测,大概率也只会看到 INCONCLUSIVE。把代理变量去掉以后,两个 MinGit 都报了 CRYPT_E_REVOKED。MinGit 和便携版默认用的就是 schannel,所以本来就安全。
第三次 ,为了模拟安装时选 OpenSSL 的用户,我在便携版 2.56.0.2 上加 -c http.sslBackend=openssl,得到 fatal: Unsupported SSL backend 'openssl'. Supported SSL backends: schannel。奇怪的是不加 curloptResolve 时 OpenSSL 后端正常(下面第 3 节的 ls-remote 就是这么跑的),两个配置一起用才报错。我没有深挖原因,直接在测试副本里把 libexec/git-core/libcurl-4.dll 换成同目录的 libcurl-openssl-4.dll,这才跑出结果:
text
$ git --version
git version 2.56.0.windows.2
$ git -c http.sslBackend=openssl -c http.curloptResolve=github.com:443:15.235.19.186 clone https://fake-user@github.com/galadran/emudhra-git-test demo-pg2.56.0.2-openssl
Cloning into 'demo-pg2.56.0.2-openssl'...
$ cat demo-pg2.56.0.2-openssl/README.md
# INTERCEPTED: your git accepted a certificate from a compromised eMudhra CA
This repository was served by the emudhra.we-have-a-problem.com test server,
using a certificate issued by one of the compromised eMudhra certificate
authorities, and your git accepted it without any warning. If you cloned a
github.com URL, this did not come from GitHub.
...
$ git --version
git version 2.56.0.windows.4
$ git -c http.sslBackend=openssl -c http.curloptResolve=github.com:443:15.235.19.186 clone https://fake-user@github.com/galadran/emudhra-git-test demo-pg2.56.0.4-openssl
Cloning into 'demo-pg2.56.0.4-openssl'...
fatal: unable to access 'https://github.com/galadran/emudhra-git-test/': SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
克隆地址写的是 github.com,拿到的却是测试服务器给的仓库,git 没有任何提示。换成 2.56.0.4,因为 bundle 里已经没有那个根,就拒绝了。
日常自查不用走这一套。第 1 节那条 ls-remote 隔着代理也能测准(代理只转发加密流量,证书还是 git 自己验),我的第 1 节就是开着代理跑的。
3. 修复前后的 OpenSSL 后端对比(不用演示仓库)
text
$ git --version; git -c http.sslBackend=openssl ls-remote https://git-emudhra.we-have-a-problem.com/check.git
git version 2.56.0.windows.2
2975b9930644b321e18ecae09e29c15b955e6103 HEAD
2975b9930644b321e18ecae09e29c15b955e6103 refs/heads/main
$ git --version; git -c http.sslBackend=openssl ls-remote https://git-emudhra.we-have-a-problem.com/check.git
git version 2.56.0.windows.4
fatal: unable to access 'https://git-emudhra.we-have-a-problem.com/check.git/': SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
4. curl:Git Bash 里的和 Windows 自带的不是一个 curl
text
$ curl --version | head -1
curl 7.75.0 (x86_64-w64-mingw32) libcurl/7.75.0 OpenSSL/1.1.1k (Schannel) zlib/1.2.11 ...
$ curl -sS -o /dev/null -w '%{http_code}\n' https://dv-g3b-emudhra.we-have-a-problem.com/
200
$ curl -sS -o /dev/null -w '%{http_code}\n' https://dv-g3b-aaa-emudhra.we-have-a-problem.com/
200
$ curl -sS -o /dev/null -w '%{http_code}\n' https://dv-g3b-noct-emudhra.we-have-a-problem.com/
200
$ curl -sS -o /dev/null -w '%{http_code}\n' https://dv-g3b-aaa-noct-emudhra.we-have-a-problem.com/
200
版本行里的 OpenSSL/1.1.1k (Schannel) 表示两套 TLS 库都编译进去了,默认用 OpenSSL(括号里的是备选)。所以修法不用换程序,设一个环境变量就行:
text
$ CURL_SSL_BACKEND=schannel curl -sS -o /dev/null -w "%{http_code}\n" https://dv-g3b-noct-emudhra.we-have-a-problem.com/
000
curl: (35) schannel: next InitializeSecurityContext failed: CRYPT_E_REVOKED (0x80092010) - ......
$ CURL_SSL_BACKEND=schannel curl -sS -o /dev/null -w "%{http_code}\n" https://github.com/
200
(中文 Windows 下行尾的「证书已被吊销」会以乱码显示,这里省略了。)C:\Windows\System32\curl.exe(8.13.0,纯 Schannel)测 4 个端点都拒绝。所以在 PowerShell 里敲 curl.exe 是安全的,在 Git Bash 里敲 curl 就不一定,取决于 PATH 里哪个排在前面。
5. Node:--use-system-ca 救不了
测试脚本(probe.mjs):
js
// node probe.mjs:对 4 个测试端点各发一次 GET,打印状态码或证书错误码
import https from 'node:https';
const hosts = ['dv-g3b-emudhra', 'dv-g3b-aaa-emudhra', 'dv-g3b-noct-emudhra', 'dv-g3b-aaa-noct-emudhra'];
for (const h of hosts) {
const r = await new Promise((ok) => https.get(`https://${h}.we-have-a-problem.com/`, (res) => { res.resume(); ok(res.statusCode); })
.on('error', (e) => ok(e.code || e.message)).setTimeout(15000, function () { this.destroy(new Error('timeout')); }));
console.log(h.padEnd(24), r);
}
text
$ node -v
v24.11.0
$ node probe.mjs
dv-g3b-emudhra 200
dv-g3b-aaa-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
dv-g3b-noct-emudhra 200
dv-g3b-aaa-noct-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
$ node --use-system-ca probe.mjs
dv-g3b-emudhra 200
dv-g3b-aaa-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
dv-g3b-noct-emudhra 200
dv-g3b-aaa-noct-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
链到 emSign 根的两条被放行了;链到 AAA 的两条被拒,是因为 Node 自带的 Mozilla 根列表里本来就没有 AAA Certificate Services,和这次修复无关。
我原本以为加上 --use-system-ca 让 Node 用 Windows 证书库就能解决,结果一样放行。Node 文档写得很清楚:这个选项是把系统证书库 "along with the --use-bundled-ca option" 一起用,也就是在自带列表之外再加系统里的根,自带的根照样有效;而且 "Node.js currently does not support distrust/revocation of certificates from another source based on system settings",Windows 里的拦截它读不到。
真正有效的修法有两个:
text
# 修法 1:升级 Node
$ node -v
v24.22.1
$ node probe.mjs
dv-g3b-emudhra CERT_REVOKED
dv-g3b-aaa-emudhra CERT_REVOKED
dv-g3b-noct-emudhra CERT_REVOKED
dv-g3b-aaa-noct-emudhra CERT_REVOKED
# 修法 2:暂时不能升级时,--use-openssl-ca + SSL_CERT_FILE 指向一份已修复的 bundle
#(这里借用 Git for Windows 2.56.0.4 自带的 ca-bundle.crt)
$ node -v
v24.11.0
$ SSL_CERT_FILE=<2.56.0.4 的 ca-bundle.crt> node --use-openssl-ca probe.mjs
dv-g3b-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
dv-g3b-aaa-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
dv-g3b-noct-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
dv-g3b-aaa-noct-emudhra UNABLE_TO_GET_ISSUER_CERT_LOCALLY
$ SSL_CERT_FILE=<同上> node --use-openssl-ca -e "https.get(github.com)" # 确认正常站点不受影响
github.com 200
24.22.1 返回的是 CERT_REVOKED,说明新版 Node 是专门把这批中间证书列成了不信任,而不只是删掉一个根。
6. Python:urllib 拒绝,requests 和 pip 放行
text
$ python -I probe.py # 标准库 urllib
dv-g3b-emudhra SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local i
...(4 个都一样)
$ python -I probe_requests.py
certifi 2026.06.17 | pip vendored certifi 2023.07.22
dv-g3b-emudhra requests+certifi 200
dv-g3b-emudhra pip vendored certifi 200
dv-g3b-noct-emudhra requests+certifi 200
dv-g3b-noct-emudhra pip vendored certifi 200
选题阶段我只测了 urllib,差点就写成「Python 没事」了。实际上项目里大多用 requests,pip 自己也带了一份 certifi,这两个都放行。certifi 2026.10.19 已经修复(我只下载了 wheel 取出 cacert.pem 来测,没改本机环境):
text
$ python -I -c "requests.get(..., verify=<certifi 2026.10.19 的 cacert.pem>)"
dv-g3b-emudhra SSLError
dv-g3b-noct-emudhra SSLError
github 200
那 urllib 为什么会拒绝?Windows 版 Python 默认从本机证书库加载根证书:
text
$ python -I -c "ctx=ssl.create_default_context(); 统计根证书数量、找 emSign/AAA"
roots loaded: 240
emSign/AAA: []
Windows 的根证书库是用到时才从微软补进本机的,这台机器从来没装过 emSign ECC Root CA - G3。换一台根证书库里已经有这个根的机器,urllib 的结果可能就不一样了。所以「urllib 拒绝」不能当成「Python 已修」,官方修复版本是 3.11.18 / 3.12.16 / 3.13.18 等。
深入 / 踩坑
1. 升级 Git 不会帮你改掉当初选的 OpenSSL。 我读了 Git for Windows 安装器源码(build-extra/installer/install.iss):HTTPS 后端那一页的默认值写的是 ReplayChoice('CURL Option','WinSSL'),新装默认 schannel,但升级时会沿用上次的选择。选 OpenSSL 时安装器会写 http.sslBackend=openssl 和 http.sslCAInfo=<安装目录>/.../ca-bundle.crt。所以当年选过 OpenSSL 的人(比如为了公司内网的自签证书),升级后依然是 OpenSSL,只是 bundle 换成了新的。这也说明一定要升到 2.56.0.3 以上,光升级不够新也没用。
2. 挂代理测端到端演示,永远是 INCONCLUSIVE。 上面已经说过:要么临时去掉 HTTPS_PROXY/HTTP_PROXY 再跑,要么直接用 ls-remote 那条。
3. GIT_CONFIG_GLOBAL 在旧 git 上不生效,我因此改坏了自己的真实配置。 我想用 GIT_CONFIG_GLOBAL=<临时文件> 模拟 git config --global,不想动本机配置。结果 2.31 不认这个变量(2.32 才加),--show-origin 显示配置直接写进了 C:/Users/<我>/.gitconfig。测完我用 git config --global --unset http.sslBackend 加 --remove-section http 还原了。写自查脚本时如果想「不改配置测一下」,用 git -c http.sslBackend=schannel ...,这个所有版本都支持。
4. schannel 也有代价。 切到 schannel 以后,git 用的是 Windows 证书库。公司内网的自签 CA 如果只放在 ca-bundle.crt 里,没导入 Windows 证书库,切完就会连不上内网仓库。先确认内网 CA 在 certlm.msc 的「受信任的根证书颁发机构」里,再切。
5. 线上那台老服务器也中招了,而且等不到补丁。 我顺手在自己一台老 CentOS 机器上跑了同样的检查。这一步是让 SpectrAI 里的 Agent 连 SSH 逐条执行的,只跑了只读命令:
text
$ cat /etc/redhat-release; uname -r; rpm -q ca-certificates
CentOS Linux release 8.5.2111
3.10.0-1160.119.1.el7.x86_64
ca-certificates-2023.2.60_v7.0.306-72.el7_9.noarch
$ git ls-remote https://git-emudhra.we-have-a-problem.com/check.git
2975b9930644b321e18ecae09e29c15b955e6103 HEAD
2975b9930644b321e18ecae09e29c15b955e6103 refs/heads/main
$ curl -sS -o /dev/null -w '%{http_code}\n' https://<4 个端点>/
dv-g3b-emudhra 200
dv-g3b-aaa-emudhra 200
dv-g3b-noct-emudhra 200
dv-g3b-aaa-noct-emudhra 200
$ yum -q list updates ca-certificates
Cannot find a valid baseurl for repo: appstream/8.5-1.2111.el8/x86_64
ca-certificates 还是 2023 年的包,yum 源也已经失效。原文列出的 RHEL 修复版本是 2026.2.92_v9.0.319-70.3.el7_9,停止维护的 CentOS 拿不到。这类机器只能按原文给的 RHEL 系做法,用 trust extract 把两个根放进 blocklist 再 update-ca-trust。这台是生产机,出站请求会不会有别的链依赖 AAA Certificate Services 我还没排查完,所以暂时没动,只记了下来。
暂时做不了怎么办 / 适用边界
- 我没测的 :Windows 上的 CI runner(GitHub Actions 的 windows 镜像、自建 runner)、Java(原文显示 OpenJDK 还是 pending)、Electron 应用(原文点名 Slack、Discord 要等应用自己换 Electron 版本)、Docker Desktop 和 VS Code(原文列了修复版本,我没逐个测)。最省事的办法是在 runner 里加一步
git ls-remote https://git-emudhra.we-have-a-problem.com/check.git,能连上就让这一步失败。 - 不能马上升级 Git :给当前用户设
git config --global http.sslBackend schannel,影响见踩坑 4。 - 不能马上升级 Node :
--use-openssl-ca+SSL_CERT_FILE指向一份新 bundle(curl 官方 2026-10-20 之后的cacert.pem,或新版 Git 自带的都行)。 - Windows 本身 :原文提到,收到微软自动证书更新的机器不需要额外处理。离线或关了自动更新的机器,原文仓库里有
Block-EmudhraCompromisedCA.ps1,会把 16 张中间证书和交叉签名证书导入LocalMachine\Disallowed。但它同样只对走 Windows 证书体系的程序有效,管不到上面那些自带 bundle 的工具。 - 测试端点是原文作者搭的,哪天下线了自查命令就会失效。到时可以用原文提供的
chains.zip离线验证。
清单
- Git Bash 里跑
git ls-remote https://git-emudhra.we-have-a-problem.com/check.git,应该报证书错误 -
git config --show-origin --get-all http.sslBackend:是 openssl 的话,升到 2.56.0.3+ 或切成 schannel - Git Bash 里跑
curl https://dv-g3b-noct-emudhra.we-have-a-problem.com/,应该报错;返回 200 就升级 Git,或者设CURL_SSL_BACKEND=schannel -
node -v不低于 24.22.1 / 22.23.4 / 26.12.1,别指望--use-system-ca -
python -m pip show certifi不低于 2026.10.19;pip --version不低于 26.2.2 - CI runner、Docker 基础镜像、老服务器各跑一遍同样的检查
参考
- 事件主页(含各软件修复版本表和各系统临时修法):emudhra.we-have-a-problem.com/
- Apple's git still trusts the compromised eMudhra certificates(自检和演示命令出处):emudhra.we-have-a-problem.com/apple.html
- HN 讨论:news.ycombinator.com/item?id=502...
- Git for Windows v2.56.0.windows.3 发布说明:github.com/git-for-win...
- Git for Windows 安装器源码 install.iss:github.com/git-for-win...
- Node.js 24 CLI 文档
--use-system-ca:nodejs.org/docs/latest...