危殆化したeMudhraの中間CAにJavaが接続するか試した(CVE-2026-91403)
目次
TL;DR
問題 Javaが同梱しているルート証明書の一覧(cacerts)から、秘密鍵が誰でも計算できたeMudhraの中間CA 8枚へたどれてしまう。CVSS 9.1で、OpenJDKの8、11、17、21、25、27が対象
対象の版 OpenJDKのアドバイザリでは27.0.1、25.0.5、21.0.13、17.0.21、11.0.33、8u512以前。前日10月20日の定例アップデートで出た版もまだ対象
手元の結果 OpenJDK 27.0.1は、eMudhraとSectigoのどちらのルート経由のチェーンにも既定設定で接続できた。Temurin 25.0.4.1は cacerts にSectigoのルートがなく、eMudhraルート経由のチェーンにだけ接続できた
更新できないとき blocked.certs の差し替えか失効確認の有効化。手元ではどちらの方法でも、危殆化した中間CAのチェーンへの接続がエラーになった
OSのJava Linuxディストリビューションのパッケージは多くがOSの証明書を使うので、ca-certificates の更新で対応できる。HomebrewとFreeBSDのJavaはパッケージ本体の更新が要る
2026年10月21日、OpenJDKが四半期ごとの定例とは別に、臨時の脆弱性アドバイザリを出した(OpenJDK Vulnerability Advisory: 2026/10/21)。
載っているのはCVE-2026-91403の1件だけで、CVSS(深刻度の指標、10点満点)は9.1、対象はJDK 8から27までのすべての系列になっている。
前日の10月20日にOracleとOpenJDKが定例の修正版を出したばかりで、その修正版もまだ対象の版に含まれる。
CVE-2026-91403の修正は、Javaが同梱している証明書まわりのファイルに入る。
アドバイザリには「8枚の危殆化した中間CA証明書に関するもの」とだけ書かれている。中間CAはルート証明書の下でサーバー証明書を発行する認証局で、その鍵が第三者に知られるなどして安全でなくなった状態を危殆化と呼ぶ。
対象の中間CAは、インドの認証局eMudhraが2025年4月に作った8枚だ。この中間CAが使う楕円曲線暗号の秘密鍵は整数で、それが256未満だったので、公開されている証明書の公開鍵から総当たりで誰でも計算できた(Mozilla Bugzilla 2083449)。秘密鍵を計算できれば、この中間CAの名義で任意のサイトの証明書を発行できる。
経緯は前回の記事にまとめていて、そこではcurl、Node.js、Python、uv、Goを試したが、Javaは試していなかった。
Javaはブラウザとは別に、ルート証明書の一覧を自分で持っているので、危殆化したチェーンを受け入れるかを手元のM4 Mac miniで試してみた。OpenJDKのビルドを2つ落としてきて使った。
Javaが危殆化した中間CAを信頼する理由
TLSでは、接続先のサーバー証明書から中間CA、ルート証明書へと署名をたどり、最後のルートを信頼しているかで相手を本物と判断している。
OracleやAdoptiumなどから単体で入れたJavaは、どのルートを信頼するかを同梱の lib/security/cacerts というファイルで決めていて、OSやブラウザの一覧は使わない。
LinuxディストリビューションのOpenJDKパッケージには、OSの証明書を使うものが多い。
危殆化した8枚の中間CAは、6枚がTLS用、2枚がコード署名用で、2つのルートからたどれる。
1つはeMudhraの emSign ECC Root CA - G3 で、もう1つはSectigoの AAA Certificate Services だ。
Sectigoは、別のルートからも同じ中間CAをたどれるよう、同じ公開鍵にクロス署名をしていた。鍵が同じなので、こちらの経路も危殆化している。
graph TD
A["emSign ECC Root CA - G3"] --> B["emSign Root TLS CA - G3"]
B --> C["危殆化した中間CA<br/>(TLS用6枚)"]
D["AAA Certificate Services<br/>(Sectigo)"] --> E["同じ鍵のクロス署名証明書"]
C --> F["秘密鍵を計算でき<br/>偽の証明書を発行できる"]
E --> F
手元で落とした2つのビルドの cacerts を keytool -list -cacerts で確かめると、入っているルートが違った。
jdk.java.net のOpenJDK 27.0.1には両方のルートが入っていたが、AdoptiumのEclipse Temurin 25.0.4.1には emSign ECC Root CA - G3 だけで、AAA Certificate Services が入っていなかった。
ブラウザがこの中間CAを拒否できたのは、Mozilla、Chrome、Apple、Microsoftがそれぞれ失効の情報を独自の仕組みで配ったからだ。
もう一つ、ブラウザはCertificate Transparency(CT、発行された証明書を公開ログへ記録させる仕組み)を要求していて、ログに載っていない証明書を受け入れない。テスト用サイトによると、遮断の前にこの中間CAにつながる証明書はCTログに見当たらなかったので、CTを要求するブラウザは攻撃を受けなかったとされる(eMudhra Intermediate CA Compromise)。
Javaの標準のTLS実装は、既定の設定ではこうした失効情報を取りに行かないし、CTも要求しない。
OpenJDK 27.0.1とTemurin 25.0.4.1で接続してみた
報告者が公開しているテスト用サイトには、危殆化した中間CAで署名した証明書を返すホストがある。
dv-g3b-emudhra.we-have-a-problem.com はeMudhraのルートへつながるチェーンを、dv-g3b-aaa-emudhra.we-have-a-problem.com はSectigoの AAA Certificate Services 経由のチェーンを返す。ここへエラーなしで接続できたJavaは、危殆化した中間CAを信頼している。
試したのは2026年10月22日7時台(JST)で、環境は次のとおり。
| 項目 | 内容 |
|---|---|
| 機種 | Mac mini(M4) |
| OS | macOS 26.5.2 |
| OpenJDK | 27.0.1+9(jdk.java.netのmacOS aarch64版) |
| Temurin | 25.0.4.1+1(AdoptiumのJDK 25の最新版) |
10月20日の修正版にあたるTemurin 25.0.5は、この時点ではまだ配布されていなかった。
どちらも配布元のSHA-256と照合してから展開した。
接続には java.net.http.HttpClient を使い、ソースファイルを java Fetch.java <URL> でそのまま実行した。
import java.net.URI;
import java.net.http.*;
public class Fetch {
public static void main(String[] a) throws Exception {
try {
var r = HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI.create(a[0])).build(),
HttpResponse.BodyHandlers.ofString());
System.out.println(r.statusCode());
} catch (Exception e) {
for (Throwable c = e; c != null; c = c.getCause()) System.out.println(c);
}
}
}
この試験では「接続できた(HTTPステータス200)」が危ない側の結果で、エラーになるのが正しい挙動だ。既定の設定ではこうなった。
| ビルド | eMudhraルート経由 | Sectigoルート経由 |
|---|---|---|
| OpenJDK 27.0.1 | 接続できた(200) | 接続できた(200) |
| Temurin 25.0.4.1 | 接続できた(200) | unable to find valid certification path |
OpenJDK 27.0.1は、両方のホストへそのまま接続した。
Temurin 25.0.4.1はSectigo経由のチェーンではルートまでたどれずに失敗したが、これは cacerts に AAA Certificate Services が入っていないからで、eMudhraルート経由ではやはり接続できた。
同じホストへ https://www.google.com/ と並べて接続し、通常のサイトはどの設定でも200になることも確かめた。
失効確認を有効にしたら拒否した
アドバイザリは、更新できない場合の方法の一つとして失効確認を有効にするよう勧めている。
失効確認は、CAが公開している失効リスト(CRL)などを取りに行き、証明書が取り消されていないかを接続のたびに確かめる処理で、JavaのTLSでは既定で無効になっている。次の2つのシステムプロパティで有効にする。
java -Dcom.sun.net.ssl.checkRevocation=true \
-Dcom.sun.security.enableCRLDP=true \
Fetch.java https://dv-g3b-emudhra.we-have-a-problem.com/
最初は、試す条件をシェルの変数にまとめて java $O Fetch.java の形で回していた。
これだとOpenJDK 27.0.1でもTemurin 25.0.4.1でも、失効確認ありの全ケースが200になった。zshは引用符なしの変数を空白で分割しないので、2つの -D が1つの引数としてJavaへ渡り、checkRevocation の値が true にならなかった。
上のようにオプションを直接書いて実行し直すと、OpenJDK 27.0.1もTemurin 25.0.4.1も、危殆化した中間CAを CertificateRevokedException で拒否した。理由は CA_COMPROMISE(CAの鍵の危殆化)だった。
| ビルド | eMudhraルート経由 | Sectigoルート経由 |
|---|---|---|
| OpenJDK 27.0.1 | 拒否(CA_COMPROMISE) | 拒否(CA_COMPROMISE) |
| Temurin 25.0.4.1 | 拒否(CA_COMPROMISE) | 未試験(既定の設定で接続できない) |
-Djava.security.debug=certpath を足したデバッグ出力では、http://crl.emsign.com/?roottlscag3.crl からCRLを取ってきて、中間CAの失効を見つけていた。
Javaの例外に記録された失効日は、eMudhraルート経由では2026年10月18日18時24分(JST)だった。Sectigo経由では2025年5月14日で、クロス署名証明書が発行された日と同じ日付になっている。
ただし、失効確認は接続のたびにCRLを取りに行くので、配布元へ届かない環境では接続が失敗する。
手元では、同じコマンドを続けて実行したうち1回が java.net.SocketException: Connection reset でエラーになった。
blocked.certsに追加して拒否させてみた
もう一つの方法として、Javaが拒否する証明書の一覧 lib/security/blocked.certs を差し替える手がある。
アドバイザリでは、OS別の新しい blocked.certs をダウンロードし、$JDK_HOME/lib/security/blocked.certs と置き換えるよう案内している。JDK 8ではファイル名を blacklisted.certs にする。すでに自分で追加した行があれば、バックアップを取ってから新しいファイルへ移すよう書かれている。
アドバイザリからリンクされている ops.java 上のダウンロード先は、2026年10月22日7時台(JST)に試すと、UnixとWindowsのどちらもHTTP 403で Unexpected outage のページを返した。
代わりに、OpenJDK 27.0.1のコピーを作って blocked.certs に手で追加してみた。
blocked.certs は先頭行が Algorithm=SHA-256 で、そのあとに証明書のSHA-256が1行ずつ並ぶテキストファイルなので、テスト用サイトが公開している impacted-intermediates.json から16枚(中間CA 8枚とクロス署名証明書8枚)のSHA-256を取り出して末尾に足した。
python3 -c "import json; [print(x['sha256']) for x in json.load(open('impacted-intermediates.json'))]" \
>> "$JDK_HOME/lib/security/blocked.certs"
追加したJDKでは、失効確認を有効にしなくても両方のホストで Untrusted certificate: CN=EM DV TLS CA - G3B になり、https://www.google.com/ は200のままだった。
手で足したのはSHA-256の16行だけで、配布版の blocked.certs とは作り方が違う。OpenJDKの本体に入った修正(8395142: CA Enhancements)では、同じ16枚の証明書を blocked.certs.pem に加えたうえで、ECDSA署名(楕円曲線暗号の署名)の別の表し方でも一致するようにファイルを作る処理も変えている。手で足した行は、ダウンロード先が復旧したら配布版に置き換える前提の応急処置になる。
8枚のうち2枚はコード署名用なので、署名付きJARの検証にも関わってくる。JDK 17以降なら jarsigner -verify -revcheck -strict で失効まで確かめられるとアドバイザリに書かれている。
blocked.certs も失効確認の設定も、Javaのインストールごとに別々になっている。
Javaの修正版とOSのパッケージ
修正版の番号はアドバイザリに書かれておらず、テスト用サイトの一覧では、Javaの修正済みの版が27.0.1.0.2、25.0.5.0.2、21.0.13.0.2、17.0.21.0.2、11.0.33.0.2、8u511(OpenJDK、Oracle JDK)になっている。
OpenJDKの25系列のリポジトリには、10月21日に「Bump update version for OpenJDK: jdk-25.0.5.0.2」というコミットが入っていた(openjdk/jdk25u)。
JDK 8は、アドバイザリでは8u512以前が対象なのに、テスト用サイトでは8u511が修正済みになっていて、番号が食い違っている。どちらが正しいかは、この時点では確かめられていない。
アドバイザリは、Javaの入れ方で対応を分けている。
| Javaの入れ方 | 使う証明書 | 対応 |
|---|---|---|
| 単体で入れたOpenJDK(Oracle JDK、Temurinなどのtarballやインストーラー) | Java同梱の cacerts | 修正版へ更新。できなければ blocked.certs の差し替えか失効確認 |
| Arch Linux、Debian、Devuan、Fedora、Gentoo、Oracle Linux、RHEL、UbuntuのOpenJDKパッケージ | ほとんどがOSの証明書 | OSの ca-certificates を更新 |
| HomebrewとFreeBSDのOpenJDKパッケージ | Java同梱 | パッケージ本体を更新 |
Homebrewのformulaは、2026年10月22日7時21分(JST)の時点で openjdk が27、openjdk@21 が21.0.13、openjdk@17 が17.0.21だった。いずれも.0.2の版にはなっていない。
DebianやUbuntuの ca-certificates の修正済みの版は、前回の記事の表に載せた。
前日10月20日の定例アップデート
CVE-2026-91403は、前日の2026年10月20日に出たOracleの四半期ごとの定例修正、Critical Patch Update(CPU)には載っていない(Oracle Critical Patch Update Advisory - October 2026)。
このCPUでのJava SEの修正は55件で、そのうち49件は認証なしにネットワーク経由で悪用されうるものだった。CVSSの最高は7.5で、JavaFXが使っているWebKitGTKの脆弱性(CVE-2026-43660)とJavaプラグイン(CVE-2026-91484)の2件がそれにあたる。
OracleのCVSSは、Javaアプレットなどを管理者権限で動かす前提で計算されていて、管理者権限なしで動かす場合はスコアが下がるとも書かれている。
同じ日のOpenJDKのアドバイザリでは、26.0.2.1、25.0.4.1、21.0.12.1、17.0.20.1、11.0.32.1、8u504以前が対象とされた(OpenJDK Vulnerability Advisory: 2026/10/20)。
Oracle JDKのリリースノートでは、この時点のセキュリティベースライン(各系列でこれより古い版は使わないよう求める最低の版)が25.0.5、21.0.13、17.0.21、11.0.33、8u511になっている(JDK 25.0.5 Release Notes)。
手元で試したTemurin 25.0.4.1は、この20日の修正も入っていない版だった。Adoptiumの配布APIでは、7時台の時点でJDK 25の最新は25.0.4.1のままだった。
Oracleのアドバイザリによると、セキュリティ修正は毎月第3火曜日に出していて、CPUのない月はCritical Security Patch Update(CSPU)になる。次は2026年11月17日のCSPUで、Oracle JDK 25.0.5のリリースノートも、この日以降は25.0.5を使わないよう勧めている。