Node の脆弱性?Python の脆弱性?真犯人は中間 CA ― eMudhra 鍵漏えいを仕組みから読む

はじめに

10 月 20 日以降、「Node.js の緊急リリース」「Python の緊急アップデート」「certifi の更新」というニュースが立て続けに流れてきました。脆弱性スキャナーで certifi を指摘されて、慌てて対応した方もいると思います。

これだけ並ぶと、「Node と Python に何か大きな欠陥が見つかったのか」と思うかもしれません。しかし調べてみると、Node や Python のコードに欠陥があったわけではありませんでした。今回は、私が一連のニュースについて調べてわかったことをまとめます。

先に結論を書きます。

  • 根本原因は、インドの認証局 eMudhra(イーマドラ)の 中間 CA の鍵 です。2025 年に行われた鍵の生成作業に不備があり、本来は秘密であるはずの鍵が、公開されている証明書から計算で取り出せる状態になっていました。インドという国の問題ではなく、1 つの認証局の作業の不備です
  • Node と Python(正確には Python に同梱の pip と、ライブラリが使う certifi)は、「信頼する認証局の一覧」を自分で持っています。その一覧を直す手段として、バージョンを上げる必要がありました
  • 主要なブラウザには失効の情報を中央から配る仕組みがあるので、多くの利用者は何もしなくても守られました

この記事では、一連のニュースからわかったことを、仕組みから順にまとめます。なぜ「Node の脆弱性」「Python の脆弱性」という形で表に出たのか、そしてどこを直せば本当に直るのかも説明します。

1. 何が起きたのか

eMudhra の中間 CA 8 つ(TLS 用が 6 つ、コード署名用が 2 つ)の秘密鍵が、公開されている証明書から計算で取り出せる状態になっていました。

原因は、2025 年 4 月(eMudhra の説明では 4 月 30 日)に行われた鍵の生成作業です。この作業で弱い鍵ができてしまい、その鍵がそのまま証明書の発行に使われていました。証明書は誰でも見られるものなので、そこから鍵を逆算できてしまったわけです。

日付出来事
2025 年 4 月 30 日中間 CA の鍵の生成作業。ここで弱い鍵ができる
2026 年 10 月 17 日研究者が問題を報告
2026 年 10 月 20 日問題が公表される。ブラウザ各社がすぐに遮断
10 月 20 日以降Node.js、Python、certifi などが相次いで修正版を公開

約 1 年半のあいだ、弱い鍵の中間 CA が正規の証明書を発行し続けていたことになります。

2. 前提:証明書はいつ作られ、どこにあり、いつ使われるのか

今回の問題をイメージするには、証明書の一生を知っておくのが近道です。この章では、証明書の仕組みを「発行」「保管」「利用」の 3 つの場面に分けて見ていきます。

「証明書」と「秘密鍵」は別物

最初に、2 つの言葉を区別しておきます。

  • 証明書:「この公開鍵は example.com のものです」と書かれ、認証局(CA)が署名した、いわば身分証です。接続してきた相手に毎回見せるものなので、誰に見られても問題ありません
  • 秘密鍵:証明書と対になる鍵で、署名を作ったり、暗号化通信を始めたりするのに使います。持ち主しか持っていないことが前提で、外に出てはいけません

今回漏れたのは「証明書」ではなく、中間 CA の「秘密鍵」です。この区別が、あとの話の鍵になります。

信頼の連鎖

HTTPS の証明書は、「ルート CA → 中間 CA → サーバー証明書」という 3 段の 信頼の連鎖 でできています。

図 1:HTTPS の信頼の連鎖。ルート CA → 中間 CA → サーバー証明書

  • ルート CA:信頼の出発点です。OS やブラウザ、Node、certifi が最初から持っている「信頼する認証局の一覧」に入っています。鍵はとても重要なので、普段はオフラインで保管されます
  • 中間 CA:ルート CA から署名をもらった、日々の発行担当です。申請者がそのドメインの持ち主かを審査し、問題なければ証明書に署名します。実際に証明書を発行しているのは、ルートではなくこの中間 CA です
  • サーバー証明書:Web サイトが接続のたびに提示する証明書です

ここで出てきた「信頼する認証局の一覧」(以下、単に「一覧」と呼びます)は、利用者が自分で取りに行くものではありません。OS やブラウザ、Node、certifi に最初から中に入っています。中身は、開発元が「この CA なら信頼してよい」と判断して選んだルート CA の証明書です。Node と certifi は、Mozilla(Firefox の開発元)が管理している一覧をもとにしています。

一覧が新しくなるのは、それを持っているソフトウェアを更新したときです。OS やブラウザなら自動更新で、Node や certifi ならバージョンを上げたときに、一覧も一緒に入れ替わります。この「一覧はソフトウェアと一緒に届く」という点は、5 章で大事になります。

ここで大事なのは、クライアントの判断の仕方です。クライアントは、提示された証明書から署名をさかのぼり、手元の一覧にあるルートまでたどれるか だけを見ています。たどれれば「本物」、たどれなければ「偽物」です。

逆に言えば、一覧に入っているルートの配下にある中間 CA が署名したものは、すべて本物として扱われます。今回鍵が漏れたのは、この中間 CA でした。

場面 1:発行 ― サイトの運営者が申し込んだとき

証明書は、Web サイトの運営者が CA に申し込んで発行してもらいます。

  1. 運営者が、自分のサーバーで秘密鍵と公開鍵のペアを作ります
  2. 公開鍵とドメイン名を CA に送り、証明書の発行を申し込みます
  3. 中間 CA が、申し込んだ人が本当にそのドメインの持ち主かを確かめます。たとえば「指定したファイルを Web サイトに置いてください」「DNS に指定した値を書いてください」と頼み、それができるかを見ます
  4. 確認できたら、中間 CA が 自分の秘密鍵で署名して、証明書を発行します

証明書には有効期限があるので、この手続きは定期的に繰り返されます。

ここで大事なのは、中間 CA の秘密鍵が使われるのは 4 の瞬間だけで、しかも 3 の審査を通った申し込みにしか使われないことです。「中間 CA の署名がある = 審査を通った」という関係が、HTTPS の安全を支えています。

場面 2:保管 ― それぞれ、どこに置かれているのか

発行が終わると、関係するものはそれぞれ次の場所に置かれます。

もの置き場所公開してよいか
ルート CA の秘密鍵CA の厳重な保管庫。普段はオフラインだめ
中間 CA の秘密鍵CA の発行システムの中だめ(今回漏れたのはこれ)
サーバー証明書と中間 CA の証明書サイトの Web サーバーやロードバランサーよい(接続のたびに配る)
サーバーの秘密鍵サイトの Web サーバーやロードバランサーだめ
ルート CA の証明書(信頼の一覧)クライアント側。OS、ブラウザ、Node、certifi などよい

また、正規に発行された証明書は CT ログ(Certificate Transparency:発行された証明書を記録する公開の台帳)にも登録されます。つまり 証明書は、世界中の誰でも見られる状態 にあります。

場面 3:利用 ― HTTPS で接続するたびに

クライアントが HTTPS で接続するたびに、最初に次のやりとり(TLS ハンドシェイク)が行われます。

  1. クライアントがサーバーに接続します
  2. サーバーが、サーバー証明書と中間 CA の証明書を送ってきます
  3. クライアントは署名をさかのぼって確かめます。サーバー証明書の署名を中間 CA の証明書で、中間 CA の証明書の署名をルート CA の証明書で確かめ、そのルートが 手元の一覧にあるか を見ます。あわせて、証明書のドメイン名が接続先と一致するか、期限が切れていないかも確かめます
  4. すべて問題なければ、サーバーの秘密鍵を使って暗号化の準備をし、通信が始まります

このとき、クライアントは CA に「この証明書はちゃんと審査しましたか?」と問い合わせたりはしません。見ているのは、正しい署名がついているかどうかだけ です。

つまり、今回の何が問題だったのか

3 つの場面をつなげると、今回の問題が見えてきます。

本来、中間 CA の署名を作れるのは、CA の中で管理された秘密鍵だけです。だからこそ「署名がある = 場面 1 の審査を通った」と言えました。

ところが今回は、その秘密鍵が、場面 2 で誰でも見られる状態にある証明書から、計算で取り出せてしまいました。秘密鍵が手に入れば、場面 1 の審査を飛ばして、署名だけを作れます。そして場面 3 のクライアントは署名しか見ないので、その証明書を本物と区別できません。

3. 中間 CA の鍵が漏れると何ができるのか

中間 CA の鍵を手に入れた人は、どのドメインの証明書でも、審査なしで「正規のもの」として発行できます。2 章の場面 1 でいえば、ドメインの持ち主かを確かめる審査を飛ばして、署名だけを作れてしまう状態です。example.com の持ち主でなくても、example.com の証明書を作れます。

しかもその証明書は、クライアントから見て本物と区別がつきません。署名をさかのぼれば、手元の一覧にあるルートまできちんとたどれてしまうからです。

偽の証明書は「通信の途中に割り込む」ために使われる

偽の証明書を作っただけでは、何も起きません。悪用されるのは、攻撃者がクライアントとサーバーのあいだの 通り道 に入り込んだときです。これを中間者攻撃(MITM:Man-in-the-Middle)と呼びます。

通り道に入り込む方法には、たとえば次のようなものがあります。

  • カフェや空港で、本物そっくりの名前の偽 Wi-Fi に接続させる
  • DNS(ドメイン名から接続先の IP アドレスを調べる仕組み)の応答を偽装して、偽のサーバーの IP アドレスを教える
  • 社内やデータセンターのネットワーク機器を乗っ取る

こうして入り込まれると、クライアントは「example.com に接続した」つもりで、実際には攻撃者が用意した偽のサーバーと話し始めます。

それでも普段は、証明書の検証が攻撃を止めてくれます。攻撃者は example.com の本物の証明書を出せないので、クライアントは検証に失敗し、接続を止めるからです。ブラウザで「この接続ではプライバシーが保護されません」といった警告が出るのは、この仕組みが働いた結果です。

鍵が漏れると、この最後の砦が崩れます。

図 2:中間者攻撃(MITM)。鍵が漏れる前と後

上の段が、鍵が漏れる前の状態です。割り込まれても、偽のサーバーは本物の証明書を出せないので、クライアントはそこで接続を止めます。

下の段が、鍵が漏れた後の状態です。攻撃者は漏れた鍵で作った偽の証明書を出し、クライアントはそれを本物と判断してしまいます。攻撃者は裏で本物のサーバーに中継するので、利用者からは普段どおり使えているように見えます。そのあいだ、暗号化されているはずの通信の中身は、攻撃者に読まれたり書き換えられたりします。

「うちは AWS にしか接続しないから関係ない」は通用しない

よく出る疑問に「決まった相手(AWS など)にしか接続しないのだから、関係ないのでは?」というものがあります。

答えは「関係あります」です。図 3 のとおり、攻撃者が入り込むのは接続先のサーバーではなく、その手前の通り道だからです。接続先の指定が正しくても、通り道で偽のサーバーにすり替えられれば防げません。そのとき頼りになるのは証明書の検証だけですが、偽の証明書が「正規」として通ってしまえば、その砦は機能しません。

CT ログは「あとで見つける」仕組みで、その場では止められない

2 章で触れた CT ログは、発行された証明書を記録する公開の台帳です。記録は追加しかできず、あとから消したり書き換えたりできません。

正規の CA は、証明書を発行するたびにこの台帳へ登録します。ドメインの持ち主や研究者は、監視サービスを使って台帳を見張り、「自分のドメインの証明書が、身に覚えなく発行されていないか」を確かめられます。偽の証明書が台帳に載れば、いずれ誰かが気づき、CA の失効やブラウザの遮断につながります。

ただし、これには 2 つの限界があります。

図 3:CT ログの限界。台帳に載った場合と載せなかった場合

1 つ目は、見つかるまでに時間がかかる ことです。CT ログは「記録」であって「遮断」ではありません。偽の証明書が攻撃に使われているその瞬間に、通信を止めてはくれません(上の段)。

2 つ目は、台帳に載らない偽の証明書もありうる ことです。漏れた鍵を持つ攻撃者は、CA を通さずに自分で署名できるので、台帳に登録しないこともできます。主要なブラウザは台帳に登録されていない証明書を受け付けないので、この手は通じにくくなっています。しかし Node や Python などは、台帳への登録を確認しません。そのため、記録に残らない偽の証明書でも通ってしまいます(下の段)。

結局、攻撃を止めるには、クライアントの側で「この CA はもう信頼しない」と判断できる必要があります。

4. ブラウザと OS では、多くの利用者が何もしなくても守られた

ブラウザと OS には、「この CA はもう信頼しない」という情報を 中央から配る仕組み があります。

  • Chrome:CRLSets(失効した証明書の一覧を、ブラウザ本体の更新とは別に配る仕組み)
  • Firefox:OneCRL と CRLite(同じく、失効の情報をブラウザへ配る仕組み)
  • OS:Windows や macOS、Linux ディストリビューションの証明書ストアの更新。ただし Linux の ca-certificates のように、パッケージを更新しないと反映されないものもあります

今回も、ブラウザ各社は公表と同時に問題の CA を遮断しました。失効の情報はバックグラウンドで届くので、多くの利用者は何も意識しなくても守られています。ブラウザを開いているだけの人にとっては、この事故は「起きたことにも気づかない」ものだったはずです。ただし、パッケージの更新が必要な OS や、更新を止めている端末、サポートの切れた古いバージョンには、自動では届きません。

では、同じことがアプリケーションやスクリプトでは起きなかったのはなぜでしょうか。ここからが本題です。

5. では、なぜ「Node と Python」なのか

理由は 2 つあります。自分専用の一覧を持っていること と、失効を確認しないこと です。

理由 1:OS の一覧を使わず、自分の一覧を持っている

Node.js は、信頼する CA の一覧を バイナリの中に組み込んでいます。OS の証明書ストアが更新されても、Node が見ているのは自分の中の一覧なので影響を受けません。

Python の世界では、certifi というパッケージが同じ役割を担っています。certifi は cacert.pem という CA の一覧ファイルを配るだけのパッケージです。requests・httpx・botocore といった定番のライブラリは、この cacert.pem を使って検証し、OS の証明書ストアは見ません(botocore は、certifi が入っていればそちらを使います)。

pip も同じです。pip は内部に certifi を持っているので、OS を更新しても pip には反映されません。

ちなみに、Python 本体の ssl モジュールは、普通は OS の証明書ストアを使います。CPython の ssl にも同様の防御を入れる提案(CPython #160040)が出ていますが、まだ提案の段階です。

理由 2:既定の TLS は失効を確認しない

ブラウザとは違い、Python・Ruby・Go・Node.js や、ほとんどのコマンドラインツールの既定の TLS は、証明書が失効しているかどうかを確認しません。

つまり、「この CA は失効しました」という情報を外から届ける経路がないのです。危ない CA を信頼しないようにするには、一覧そのものを書き換えるしかありません。そして一覧はバイナリやパッケージの中にあるので、書き換えるには バージョンを上げる しかありません。

これが、今回の事故が「Node と Python の更新」という形で表に出た理由です。

実際に何が直されたのか

世の中での見え方実際に直したもの
NodeNode の脆弱性・更新Node 本体に組み込まれた CA の一覧。本体のバージョンを上げる以外に直す方法がない
PythonPython の緊急アップデートPython に同梱の pip の中の certifi。ssl モジュールは変わっていない
Python のライブラリcertifi の更新requests・httpx・botocore などが使う certifi

具体的には、次のような修正が入っています。

  • Node.js(22.23.4・24.22.1・26.12.1):侵害された CA の証明書を、明示的に不信任にしています。接続しようとすると CERT_REVOKED で拒否されます
  • Python(3.13.17・3.14.9・3.15.1):同梱の pip を 26.2.2 に、その中の certifi を 2026.10.19 に更新したものです
  • certifi(2026.10.19):侵害された中間 CA につながるルート emSign ECC Root CA - G3 が削除されています

失効の届き方の違い

ブラウザと OS には失効の情報が中央から自動で届きます。一方、自分の一覧を持つアプリには、バージョンを上げるまで何も届きません。

図 4:失効の届き方の比較。ブラウザ・OS と、自分の一覧を持つアプリ

上の段(ブラウザ・OS)では、利用者の手を介さずに失効が届きます。下の段(アプリ)では、真ん中の「利用者が作業する」が抜けると、古い一覧のまま動き続けます。

スキャナーには「Node の脆弱性」「Python の脆弱性」に見える

最後に、タイトルの答え合わせです。

脆弱性スキャナーは、パッケージや実行環境の バージョン を見て判定します。「このバージョンの Node は修正前」「このバージョンの certifi は古い」という具合です。そのため画面上は、Node や Python、certifi そのものに脆弱性があるように表示されます。

しかし実際に直されたのは、プログラムのコードではなく、その中に入っている 信頼の一覧 です。「Node の脆弱性」「Python の脆弱性」というラベルの裏にいた真犯人は、eMudhra の中間 CA の鍵だったのです。

6. 落とし穴:Python を上げても直らない場所

Python については、もう 1 つ注意が必要です。Python 本体を 3.13.17 などに上げても、それだけでは直らない場所が残ります。

venv やロックファイルの中の certifi

Python 本体の緊急リリースで新しくなるのは、本体に同梱された pip の中の certifi です。プロジェクトごとの仮想環境(venv)に入っている certifi は、別物です。

requests や botocore が使うのは、この venv の中の certifi です。uv.lock や poetry.lock、固定した requirements.txt に古い certifi が書かれていれば、Python 本体を何度上げても、古い一覧が使われ続けます。

ロックファイルのない環境

ロックファイルがない環境も安心できません。たとえば AWS Lambda のデプロイパッケージは、ファイルが変わらない限りビルドし直されないことがよくあります。その場合、最初にビルドした時点の古い certifi が残り続けます。

コンテナのイメージ

コンテナのイメージも同じです。イメージの中の Node や certifi は、ビルドした時点の一覧のまま です。依存の定義を直しても、イメージを作り直してデプロイし直さない限り、動いているコンテナは古い一覧で検証を続けます。

まとめると、「信頼の一覧」は Python 本体の中だけでなく、venv、ロックファイル、ビルド済みの成果物の中にも散らばっています。それぞれを個別に直す必要があります。

7. 今やるべきこと

ここからは、私が担当しているシステムでも実際に行った対応をもとに、やるべきことをまとめます。1〜4 が「今すぐやること」、5・6 が「漏れを防ぐためにやること」です。

なお、ブラウザや OS は多くの場合、自動で守られています。ただし、更新を後回しにしていたり、しばらく再起動していなかったりする端末では、まだ反映されていないことがあります。心当たりがあれば、念のためアップデートしておくと安心です。Linux の場合は、ca-certificates パッケージの更新も確認してください。

今すぐやること

1. Node を修正版に上げる

22.23.4・24.22.1・26.12.1 以上 に上げます。

対象は本番環境だけではありません。手元の PC の既定のバージョン(nvm や asdf で設定しているもの)、CI のイメージ、Docker のベースイメージも確認してください。普段使っている環境ほど見落としがちです。

実際に試したところ、手元の nvm の既定になっていた Node 24.21.0 では、侵害された CA のテストサイトに そのまま接続できてしまいました。本番だけ直して安心していると、開発者の PC や CI が無防備なまま残ります。

2. Python は「本体」と「certifi」を別々に上げる

本体 は 3.13.17・3.14.9・3.15.1 以上に上げます。同梱の pip が 26.2.2 になり、その中の certifi が新しくなります。

certifi は、本体を上げても venv の中身は変わらないので、環境ごとに上げます。

  • ロックファイルがある場合:uv.lock・poetry.lock・固定した requirements.txt の certifi を 2026.10.19 以上 に更新します
  • ロックファイルがない場合:依存の定義に certifi>=2026.10.19 を下限として書きます。コードで直接 certifi を使っていなくても、requests や botocore を通して使っているので、明示的に書いておきます

ここで ==(固定)ではなく >=(下限)にしているのには理由があります。固定してしまうと、今後 certifi が更新されても取り込まれなくなるからです。下限であれば、ビルドのたびにその時点の最新バージョンが入ります。

3. ビルドし直して、デプロイし直す

コンテナのイメージ、Lambda の zip、配布済みの成果物は、作った時点の一覧のまま残っています。依存の定義を直しただけでは、動いているものは何も変わりません。

ビルドが動く条件にも注意が必要です。たとえば「特定のファイルが変わったときだけビルドする」CI のルールがあると、依存の定義を直してもビルドが走らないことがあります。

4. 直ったことを確かめる

certifi のバージョンを確認します。

python -c "import certifi; print(certifi.__version__)"

cacert.pem に問題のルートが残っていないかを確認します。0 件になれば OK です。

grep -c "emSign ECC Root CA - G3" "$(python -c 'import certifi; print(certifi.where())')"

いちばん確実なのは、侵害された CA のテストサイトに実際に接続して、拒否されること を確かめる方法です。修正済みなら、Python では CERTIFICATE_VERIFY_FAILED、Node では CERT_REVOKED のエラーになります。バージョンの数字を見るより確実です。

漏れを防ぐためにやること

5. 他にも「自分の一覧」を持つものがないか洗い出す

Node と certifi 以外にも、自分の一覧を持つものがあります。

  • Java:cacerts(JDK の中にあり、OS を更新しても変わらない)
  • Go:1.27.3・1.26.10 が修正版として報じられています
  • Composer:対応済みと報じられています
  • Bun:10 月 21 日の時点で未対応と報じられています
  • slim 系のコンテナイメージ:ca-certificates パッケージが古いままになっていないか

6. スキャナーの結果だけで安心しない

スキャナーは「宣言されている依存バージョン」で判定します。そのため、次のようなものは画面に出てこないことがあります。

  • 依存の依存として入っている certifi
  • ライブラリの中に同梱された CA の一覧
  • ビルドし直されていない成果物

スキャナーが静かになったことは、「直った」ことの証明にはなりません。4 の確認まで行って、はじめて「直った」と言えます。

まとめ

「Node の脆弱性」「Python の脆弱性」というラベルの裏には、認証局 eMudhra の中間 CA の鍵の事故がありました。Node と Python のコードに欠陥があったわけではなく、それぞれが持っている「信頼の一覧」を直すために、バージョンを上げる必要があったのです。

主要なブラウザや OS には中央から失効の情報が届くので、多くの利用者は何もしなくても守られました。一方、自分の一覧を持つアプリは、バージョンを上げ、ビルドし直し、デプロイし直すまで守られません。

認証局の事故は、今回が最後ではないはずです。自分のシステムのどこに「信頼の一覧」があるのかを知っておけば、次に同じことが起きても慌てずに済みます。この機会に、Node、certifi、Java の cacerts、コンテナの ca-certificates など、手元の一覧の在りかを棚卸ししておくことをおすすめします。

参考

新規CTA