SSL/TLS 信頼ストアとは、システムやツールが HTTPS 通信を行う際に接続先サーバーの証明書チェーンを検証するためのルート証明書・中間証明書の保管場所です。

2026年10月、認証局 eMudhra の中間 CA 秘密鍵において乱数生成のエントロピー不足が発覚し、公開された証明書から 255 ステップ以内の計算で秘密鍵が復元可能だったという衝撃的なインシデントが報告されました1。この事態を受けて eMudhra 社は SSL/TLS 証明書事業からの自主的撤退を発表しています2。

Safari や Google Chrome などの主要ブラウザでは、OneCRL や CRLSets といった動的な失効リスト配信の仕組みにより、侵害された中間 CA が即座に遮断されました。しかし、ターミナルで動作する CLI ツールやスクリプト言語環境は、通信時にリアルタイムで失効情報(CRL/OCSP)を問い合わせず、ローカルディスク上の静的ファイルやバイナリ内の証明書リストに依存しています3。

このニュースを見て「brew upgrade でパッケージを最新にしたから大丈夫」と安心していないでしょうか。手元の Mac 環境で実際に計測してみたところ、ツールの更新だけでは対策として不完全であり、普段使っている Git や Python が依然として漏洩 CA を信頼したまま通信している実態が明らかになりました。専門外の複雑なセキュリティ・信頼ストア問題ではありましたが、AI エージェントと協業しながらコマンドを実行し、経路ごとに実測と切り分けを進めることで安全に対処することができました。

過去の秘密情報保護に関する検証記事 でも触れたように、セキュリティ対策が正しく効いているかを確かめるには「更新した」という申告やバージョン番号を鵜呑みにせず、実際の通信経路ごとに実測し直す姿勢が不可欠です。

本記事では、手元の Mac 環境において各種ツールチェーン(Homebrew、mise、Node.js、macOS システム標準)がどの信頼ストアを参照しているかを実測した記録を共有します。さらに、問題のある証明書の除外手順と、漏洩 CA が拒否されること(陽性確認)だけでなく「GitHub や PyPI などの正規ホストが正常に通信できること(陰性対照)」を 1 回の確認にセットで組み込む検証作法を解説します。

検証環境と調査スコープ

本調査では、macOS 上で日常的な開発作業に使用される代表的な CLI ツールチェーン(curl、git、python3、node)を対象に、テスト用エンドポイントへの接続挙動と参照先ストアを調査しました。Safari や Mail などの Apple ネイティブ GUI アプリケーションは検証スコープ外とし、ターミナル環境の実態に絞って検証しています。

また、本記事ではターミナルログやエラー出力、HTTP ステータスコードなどの「観測された客観的事実(Fact)」と、リンク構造やコンパイル時設定に関する「技術的推測・考察(Inference)」を厳格に分離して記述します。

検証を実施したマシンの環境スペックおよび各ツールのバージョンは以下のとおりです。

項目バージョン / 詳細
OS・アーキテクチャmacOS (Apple Silicon darwin arm64)
計測実施日2026年10月22日
macOS システム標準 curl/usr/bin/curl
Homebrew 版 curlcurl 8.22.0(更新後 8.23.0)
macOS システム標準 python3/usr/bin/python3
mise 管理下 python3python3 3.14.7(python-build-standalone)
Homebrew 版 python3/opt/homebrew/bin/python3
macOS システム標準 git/usr/bin/git
Homebrew 版 gitgit 2.56.0
Node.jsv24.21.0(更新後 v24.22.1)
Homebrew ca-certificates2026-09-25(更新後 2026-10-20)
mise2026.10.0

初期状態(手動更新前)の測定では、試行したすべての CLI クライアント(システム標準 curl/python3、Homebrew curl、mise python3、node)において、漏洩中間 CA で署名されたテスト用ホストへの接続が HTTP ステータスコード 200 で成功し、警告なく信頼されている状態でした。

評価基準と前提となるアーキテクチャ概念

なぜブラウザと CLI ツールでは失効の反映速度が根本的に異なるのでしょうか。そして、Mac の中では証明書ストアがどのように多重化されているのでしょうか。

ブラウザ環境と CLI 環境の失効モデルの違い

Web ブラウザでは、認証局の鍵漏洩や誤発行が発生した際、ベンダー(Google、Mozilla、Apple、Microsoft)が集中管理する失効リスト(Chrome の CRLSets、Firefox の OneCRL、CRLite など)がバックグラウンドでプッシュ配信されます4。これにより、OS の更新を待つことなく数時間から数日単位で該当証明書が全世界のブラウザで無効化されます。

一方、ターミナル上で動作する多くの CLI ツールやスクリプト言語ランタイムは、パフォーマンスやオフライン動作への配慮から、通信のたびに CRL(証明書失効リスト)のダウンロードや OCSP(オンライン証明書状態プロトコル)の問い合わせを行いません1。そのため、手元のマシンに保存された静的な証明書ファイル(CA バンドル)が更新されない限り、失効した証明書を有効なものとして信頼し続けます。

Mac における信頼ストアの多重構造

Mac 上で開発を行っている場合、信頼ストアは単一の「キーチェーン(Keychain)」だけにまとまっているわけではありません。利用するツールチェーンごとに、参照先が以下のように分散しています。

  1. macOS システム証明書バンドル(/etc/ssl/cert.pem): Apple が OS レベルで配置している静的な証明書ファイルです。Apple のセキュリティフレームワークを利用するネイティブアプリ(Safari 等)はキーチェーンの動的ブロックを参照しますが、macOS に同梱される CLI ツール(/usr/bin/curl や /usr/bin/git)はこの /etc/ssl/cert.pem を直接参照します5。なお、このファイルを手動で修正しても、macOS のソフトウェアアップデートによって Apple 標準の未修正ファイルで上書きされてしまう問題が指摘されています6。
  2. Homebrew の CA バンドル(/opt/homebrew/etc/ca-certificates/cert.pem): Homebrew で管理されるツール群が参照するバンドルです。brew upgrade ca-certificates を実行することで最新の Mozilla CA ルートに更新されます。
  3. Python の証明書ストア: インタプリタがコンパイルされた際のビルド設定(ssl.get_default_verify_paths() で確認可能)に依存します7。OS の OpenSSL ディレクトリを参照するか、あるいは certifi パッケージ同梱の cacert.pem を利用するかによって挙動が分かれます。
  4. Node.js の内蔵ストア: Node.js は外部の証明書ファイルに依存せず、バイナリ自体に Mozilla のルート証明書リストをコンパイル時に組み込んでいます。そのため、Node.js 自体のマイナーバージョンアップによって失効情報が反映されます。

評価基準(2 つの確認軸)

本検証では、各ツールの安全性を確認するために以下の 2 つの確認軸を設定しました。

  • 陽性確認(Positive Check): 漏洩中間 CA で署名されたテスト用ホスト(https://dv-g3b-emudhra.we-have-a-problem.com/)にアクセスし、正しく接続拒否(SSL/TLS エラー)となることを確認する。
  • 陰性対照(Negative Control): 開発業務に不可欠な正規ホスト(https://github.com/、https://pypi.org/)にアクセスし、正常疎通(HTTP 200)が維持されていることを確認する。

遮断の成否(陽性)だけを確認して満足すると、設定ミスで正常な通信まで巻き添えで壊してしまったことに気づけません。必ず両面をセットで評価します。

各ツールチェーンの個別挙動と特徴

パッケージマネージャ経由でツールや証明書の更新を手動で実施した後、テスト用ホストへの接続を再測定しました。その結果、ツールの挙動は真っ二つに分裂しました。

1. 更新によって正常に遮断されたツール

Homebrew 版の curl を 8.23.0 に、ca-certificates を 2026-10-20 に、そして Node.js を v24.22.1 にアップデートしたところ、これらは期待どおりテスト用ホストへの接続を遮断しました。

  • curl (PATH): 接続拒否(終了コードおよびステータス 000)
  • /opt/homebrew/bin/python3: urllib.error.URLError(証明書検証エラー)
  • node: ERR CERT_REVOKED(内蔵失効テーブルによる拒否)

2. 更新後も漏洩 CA を信頼し続けていたツール

一方で、最新版をインストールしているはずの以下のツールは、テスト用ホストに対して依然として警告なく接続できてしまいました。

  • python3 (PATH)(mise で管理している Python 3.14.7): HTTP ステータス 200 で接続成功
  • git(Homebrew で導入した最新の git 2.56.0): TLS ハンドシェイク成功(証明書情報 issuer: C=NL; O=eMudhra BV; CN=EM DV TLS CA - G3B を取得)
  • macOS 標準の /usr/bin/curl および /usr/bin/python3: HTTP ステータス 200 で接続成功

盲点の原因分析

なぜ Homebrew の Git や mise の Python は、更新後も漏洩 CA を信頼し続けていたのでしょうか。各ツールの内部動作を調査したところ、以下の構造が明らかになりました。

まず、mise でインストールした Python 3.14.7(python-build-standalone 提供のビルド)で ssl.get_default_verify_paths() を実行したところ、デフォルトの cafile が /private/etc/ssl/cert.pem に固定されていました。コンパイル時にシステムの OpenSSL 設定ディレクトリが指定されているため、Homebrew の証明書バンドルが新しくなっても、システム側のファイルを読みに行っていたのです。

次に、Homebrew 版の git 2.56.0 です。GIT_CURL_VERBOSE=1 を指定して詳細ログを確認したところ、読み込まれていたのは CAfile: /etc/ssl/cert.pem でした。Homebrew の Git フォーミュラは uses_from_macos "curl" と定義されており、macOS システム標準の動的ライブラリ /usr/lib/libcurl.4.dylib にリンクされています。Git は独自に設定(http.sslCAInfo など)を行わない限りリンク先 libcurl の既定パスを参照するため8、Homebrew 側で curl バイナリを更新しても、Git が利用する libcurl はシステムの /etc/ssl/cert.pem を参照し続けていたのです。

実際に /etc/ssl/cert.pem のタイムスタンプを確認したところ、更新日は Sep 24 のままでした。さらに中身を検索すると、問題の中間 CA が連鎖するルート証明書(emSign ECC Root CA - G3 および AAA Certificate Services)が合計 4 件そのまま残存していました。

共通点と相違点の比較

同じ Mac のターミナル上で動かしていても、ツールチェーンごとに参照している信頼ストアと失効への耐性は大きく異なっていました。

主要な 4 つのツールチェーンにおける挙動の違いを以下の表にまとめます。

ツールチェーンデフォルトの参照先証明書ストア手動更新後の挙動(テストホスト接続)遮断に至る修正経路
Homebrew curlHomebrew 管理の CA バンドル遮断(接続拒否 000)brew upgrade curl ca-certificates
Node.jsバイナリ内蔵のルート証明書ストア遮断(ERR CERT_REVOKED)mise / nvm 等で Node.js バージョン更新
mise python3/private/etc/ssl/cert.pem未遮断(200 接続成功)/etc/ssl/cert.pem の修正、または SSL_CERT_FILE
Homebrew git/etc/ssl/cert.pem(macOS libcurl 経由)未遮断(TLS 接続成功)/etc/ssl/cert.pem の修正、または http.sslCAInfo

このように、ツール本体やランタイムを最新版に上げただけでは、システムの /etc/ssl/cert.pem に依存しているツールチェーンの穴は塞がりません。なお、Homebrew の curl はデフォルトで Homebrew 管理下の CA バンドルを探索しますが、環境変数 CURL_CA_BUNDLE や SSL_CERT_FILE によって参照先パスを上書きする仕様となっています9。

ベストプラクティスと推奨実装

手元の Mac 環境で安全な状態を確立するための、具体的な確認・修正手順を説明します。

1. Python の参照先 CA ストア確認

実行中の Python インタプリタがどの証明書ファイルを参照しているかを特定します。

python3 -c "import ssl, sys; print(sys.executable); print(ssl.get_default_verify_paths())"

実行結果ログ(抜粋):

/Users/<username>/.local/share/mise/installs/python/3/bin/python3
DefaultVerifyPaths(cafile='/private/etc/ssl/cert.pem', capath='/private/etc/ssl/certs', openssl_cafile_env='SSL_CERT_FILE', openssl_cafile='/private/etc/ssl/cert.pem', openssl_capath_env='SSL_CERT_DIR', openssl_capath='/private/etc/ssl/certs')

ssl.get_default_verify_paths() により、OpenSSL のビルド時に指定されたデフォルトの cafile が /private/etc/ssl/cert.pem であることが確認できます。もしここがシステムのパスを指している場合、OS 側のバンドルが未修正であれば影響を受けます。

2. Git の参照先 CA ストアと TLS ハンドシェイク確認

Git コマンドが HTTPS 通信時にどの証明書バンドルを読み込んでいるかをデバッグ出力から特定します。

GIT_CURL_VERBOSE=1 command git ls-remote https://dv-g3b-emudhra.we-have-a-problem.com/ 2>&1 | grep -iE 'CAfile|issuer'

実行結果ログ(抜粋):

13:32:04.383950 http.c:994              == Info:  CAfile: /etc/ssl/cert.pem
13:32:04.735320 http.c:994              == Info:  issuer: C=NL; O=eMudhra BV; CN=EM DV TLS CA - G3B

Git が内部で使用している libcurl により、CAfile: /etc/ssl/cert.pem が読み込まれ、侵害された中間 CA(EM DV TLS CA - G3B)が発行者として認識されていることが確認できます。

3. /etc/ssl/cert.pem から侵害ルートを除外する修正コマンド

システムの /etc/ssl/cert.pem に依存しているツールを保護するため、バックアップを作成した上で、該当する 2 つのルート証明書(emSign ECC Root CA - G3 および AAA Certificate Services)を除外します。

引用元: 筆者の検証セッションの記録(2026-10-22。検証サイトのワークアラウンド手順に基づく実行ログ)

sudo cp -p /etc/ssl/cert.pem /etc/ssl/cert.pem.bak && sudo /usr/bin/awk '/^(###|===) /{skip=0} /^=== .*\/CN=(AAA Certificate Services|emSign ECC Root CA - G3)$/{skip=1} !skip' /etc/ssl/cert.pem.bak | sudo tee /etc/ssl/cert.pem > /dev/null

元ファイルをパーミッション保持のまま .bak として複製し、awk スクリプトによって該当する Common Name を持つ証明書ブロックをスキップして新しい cert.pem を生成しています。

4. 修正後の証明書バンドル検算コマンド

除外処理によって目的の証明書のみが正確に削除されたかを検算します。

grep -cE 'CN=(AAA Certificate Services|emSign ECC Root CA - G3)$' /etc/ssl/cert.pem
grep -c 'BEGIN CERT' /etc/ssl/cert.pem /etc/ssl/cert.pem.bak

実行結果ログ(抜粋):

0
/etc/ssl/cert.pem:126
/etc/ssl/cert.pem.bak:128

侵害ルートのヒット数が 4 から 0 になり、全体の証明書数が 128 枚から 126 枚へと正確に 2 枚分減少していることが確認できます。

実機検証結果と検証ログ

証明書バンドルの修正後、未対策だったツールを含む全クライアントでの接続結果(陽性確認)と、正規ホストへの通信維持(陰性対照)を一括で検証しました。

陽性と陰性対照を一括確認する検証スクリプト

テスト用ホストでの遮断と、開発で日常的に使用する主要ホスト(GitHub、PyPI)への正常疎通を 1 回のスクリプトで同時に確認します。

# テスト用ホストでの遮断確認(陽性)
for u in https://dv-g3b-emudhra.we-have-a-problem.com/ https://dv-g3b-aaa-emudhra.we-have-a-problem.com/; do
  echo "## $u"
  /usr/bin/curl -sS -o /dev/null -w '%{http_code}\n' "$u" 2>&1 | tail -1
  curl -sS -o /dev/null -w '%{http_code}\n' "$u" 2>&1 | tail -1
  /usr/bin/python3 -c "import urllib.request as r; print(r.urlopen('$u').status)" 2>&1 | tail -1
  python3 -c "import urllib.request as r; print(r.urlopen('$u').status)" 2>&1 | tail -1
  command git ls-remote "$u" 2>&1 | tail -1
  /usr/bin/git ls-remote "$u" 2>&1 | tail -1
  node -e "require('https').get('$u', r => console.log(r.statusCode)).on('error', e => console.log('ERR', e.code))" 2>&1 | tail -1
done

# 正規ホストでの疎通確認(陰性対照)
echo "## sanity"
/usr/bin/curl -sS -o /dev/null -w '%{http_code}\n' https://github.com/
python3 -c "import urllib.request as r; print(r.urlopen('https://pypi.org/').status)"

実行結果ログ(抜粋):

## https://dv-g3b-emudhra.we-have-a-problem.com/
000
000
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)>
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1144)>
fatal: unable to access 'https://dv-g3b-emudhra.we-have-a-problem.com/': SSL certificate problem: unable to get local issuer certificate
fatal: unable to access 'https://dv-g3b-emudhra.we-have-a-problem.com/': SSL certificate problem: unable to get local issuer certificate
ERR CERT_REVOKED
## https://dv-g3b-aaa-emudhra.we-have-a-problem.com/
000
000
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)>
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1144)>
fatal: unable to access 'https://dv-g3b-aaa-emudhra.we-have-a-problem.com/': SSL certificate problem: unable to get local issuer certificate
fatal: unable to access 'https://dv-g3b-aaa-emudhra.we-have-a-problem.com/': SSL certificate problem: unable to get local issuer certificate
ERR CERT_REVOKED
## sanity
200
200

テスト用ホストへの接続は、すべてのツールで証明書検証エラーや接続拒否(000、CERT_REVOKED、unable to get local issuer certificate)となり、漏洩した中間 CA が確実に無効化されたことが確認できました。

同時に実施した陰性対照の確認では、macOS 標準の /usr/bin/curl による github.com へのアクセス、および mise の python3 による pypi.org へのアクセスがいずれも 200 で正常に成功しました。これにより、除外処理によって正規の通信環境を損なっていないことが実証されました。

よくある質問(FAQ)

Q. Homebrew で git や curl を最新にアップデートしたのに、なぜ古い証明書が使われるのですか?

A. Homebrew の git が macOS システム標準の libcurl を使用するようにビルドされており、システムの /etc/ssl/cert.pem を直接参照する構成になっているためです。ツール本体のバイナリが最新であっても、動的リンク先のライブラリが参照する証明書ストアが更新されていなければ、システム側の古い証明書が有効なままとなります。

Q. 証明書バンドルを手動で修正した後に気をつけるべき注意点は何ですか?

A. macOS のソフトウェアアップデートを実行すると、/etc/ssl/cert.pem が Apple 標準のファイルで上書きされて元に戻る点です。Dennis Jackson 氏の検証でも、手動修正後にリリースされた macOS 27.2 へのアップデートによって修正が初期化され、再度脆弱な状態に戻ってしまった事実が報告されています6。OS のアップデートを適用した後は、該当の証明書が再度混入していないか確認し、必要に応じて除外処理を再実行する必要があります。

Q. 陰性対照(negative control)の確認はなぜ必要なのですか?

A. 遮断対策の過程で証明書バンドルを誤って破損させたり、必要な正規ルート証明書まで削除してしまったりする「過剰遮断(巻き添えによる通信障害)」を即座に検知するためです。テスト用ホストで拒否されること(陽性確認)と、GitHub や PyPI などの正規ホストへ正常に接続できること(陰性対照)を 1 回の確認にセットで入れることで、安全に運用を継続できることが保証されます。

まとめ

  • 申告ベースやバージョン番号の「更新した」を過信しない: ツールのバージョンを上げただけでは不十分であり、PATH 上の実行体ごとに実際の接続先証明書ストアを実測で特定する必要があります。
  • Mac の信頼ストアは多重構造であることを意識する: キーチェーンだけでなく、/etc/ssl/cert.pem、Homebrew の ca-certificates、Python のビルド時埋め込みパス、言語内蔵ストアが分散して存在しており、特に Homebrew の Git や mise の Python はシステムの /etc/ssl/cert.pem を見続ける盲点があります。
  • セキュリティ遮断の検証には必ず陰性対照をセットにする: テストホストでの遮断(陽性)だけでなく、業務に必要な正規ホストへの疎通(陰性対照)を 1 回の確認に組み込むことで、過剰遮断による事故を防ぎます。
  • 手作業の修正は OS アップデートごとの再点検を前提にする: システムの /etc/ssl/cert.pem を手動修正した場合、macOS のアップデートによって上書きされるリスクがあるため、定期的なヘルスチェックの仕組みを整えることが大切です。

開発環境や運用パイプラインのセキュリティ点検、ツールチェーンのヘルスチェック設計に関するご相談は、コンタクトフォーム よりお問い合わせください。

Footnotes

  1. Eight Bits of Trust; eMudhra’s CA keys cracked in milliseconds Scott Helme、2026年10月公開 技術解析記事。中間 CA 秘密鍵のエントロピー不足(255 ステップ以内の計算で復元可能だった事実)、および非ブラウザ TLS クライアントが CRL/OCSP/CT を検証しない構造的弱点。 ↩ ↩2

  2. eMudhra to Exit global SSL/TLS Certificate Business eMudhra Limited、2026年10月22日 プレスリリース。eMudhra 社によるグローバル SSL/TLS 証明書事業からの自主的撤退発表。 ↩

  3. eMudhraの中間CAの秘密鍵が公開された証明書から復元可能と判明、pip・uvなどの開発ツールに修正版 技術評論社(gihyo.jp)、2026年10月公開 ニュース解説記事。Python ツール群(pip, uv, certifi 等)の修正版リリース、動的失効配信の欠如、およびアプリケーション実接続確認の重要性。 ↩

  4. eMudhra Intermediate CA Compromise Dennis Jackson 他、2026年10月公開。侵害された中間 CA 8 件の背景、ブラウザ向け動的失効配信(OneCRL, CRLite, CRLSets)、診断用テストホストの提供、および /etc/ssl/cert.pem の一括手動除外 awk スクリプト。 ↩

  5. About the compromised eMudhra intermediate CAs Apple Inc.、2026年10月公開、Support Document HT149555。Apple セキュリティフレームワークと /etc/ssl/cert.pem の検証乖離、macOS 付属 CLI ツール(curl, git, python3 等)が OS 自動ブロックの対象外である事実、および Apple による /etc/ssl/cert.pem の手動削除手順(DN 全体を指定した個別除外コマンド)。 ↩

  6. Apple’s git still trusts the compromised eMudhra certificates Dennis Jackson、2026年10月公開。Apple への通知経緯、macOS 付属 git/curl/python3 が侵害証明書を信頼し続けていた事実、OS アップデート(macOS 27.2)による手動修正の上書き。 ↩ ↩2

  7. ssl — TLS/SSL wrapper for socket objects Python Software Foundation、Python 3 Documentation。ssl.get_default_verify_paths() の仕様、戻り値の DefaultVerifyPaths フィールド(cafile, capath 等)と OpenSSL デフォルトパスの参照構造。 ↩

  8. Git - git-config Documentation Git Community、Official Documentation。http.sslCAInfo, http.sslCAPath, 環境変数 GIT_SSL_CAINFO の仕様、および libcurl 既定パスの参照動作。 ↩

  9. curl - SSL CA Certificates Daniel Stenberg / curl project、Official Documentation。curl における CA 証明書探索順序、環境変数 CURL_CA_BUNDLE / SSL_CERT_FILE の仕様。 ↩