コンテンツへスキップ

TLS証明書の検証の仕組み — chain of trust とは何か

TLS証明書の検証の仕組み — chain of trust とは何か

ブラウザのアドレスバーに鍵アイコンが出ているとき、実際には何が保証されているのだろうか。「通信が暗号化されている」ことは間違いないが、それだけではない。もう一つ、「このドメインの証明書は、信頼できる第三者機関の連鎖をたどって検証できる」という事実も同時に保証されている。この連鎖のことを chain of trust(信頼の連鎖)と呼ぶ。今回はこの仕組みを整理した上で、自社ツールが実際に「検証をきちんと行う場面」と「あえて検証を省く場面」を両方使い分けている実例を見ていく。

証明書は誰かに署名してもらって初めて意味を持つ

TLS証明書はそれ単体では「自分がこのドメインである」という自己申告に過ぎない。これに意味を持たせているのが、証明書に付与された電子署名だ。サーバー証明書(リーフ証明書)は、通常は中間認証局(Intermediate CA)によって署名され、その中間認証局自身の証明書はさらに上位のルート認証局(Root CA)によって署名されている。ルート証明書だけは誰にも署名してもらっておらず、自己署名のまま存在するが、その代わりOSやブラウザに「あらかじめ信頼してよい存在」としてインストール済みの状態になっている(トラストストア)。

検証する側(ブラウザやHTTPクライアント)は、提示されたリーフ証明書から署名をたどって中間CA、さらにルートCAへと連鎖をさかのぼり、最終的にトラストストアに存在するルート証明書に到達できれば「信頼できる」と判断する。途中の署名が1つでも検証に失敗すれば、連鎖はそこで切れ、証明書は信頼されない。

「証明書が有効」と「このドメイン宛て」は別の判定

chain of trust の検証と混同されやすいが、もう一つ別に行われている判定がある。それが「この証明書は本当にこのホスト名向けに発行されたものか」というホスト名の一致確認だ。証明書自体が正規のCAによって正しく署名されていても、証明書に記載されたドメイン名(Common Name / Subject Alternative Name)が接続先のホスト名と一致しなければ、それは「別の誰かの正当な証明書」を提示されているに過ぎず、なりすましの可能性がある。TLS検証は「署名の連鎖が信頼できるか」と「ホスト名が一致するか」という2つの独立したチェックの組み合わせでできている。

コード署名の chain of trust との違い

以前、コード署名の基礎という記事でApple NotarizationとWindows Authenticodeを扱ったが、これも実は同じ「chain of trust」という骨格を共有している。Windows Authenticodeの証明書チェーンも、発行者証明書から上位のルート証明書までさかのぼって信頼を検証する点はTLSと全く同じ構造だ。違うのはトラストストアの中身——TLSはWebブラウザ・OSが管理する「Webサイト向け」のルートCA一覧を参照するのに対し、コード署名はOS(Apple/Microsoft)が管理する「ソフトウェア発行者向け」のルートCA一覧を参照する。連鎖の仕組みは同じでも、何を証明する目的で設計されたトラストストアかが異なる、という位置付けになる。

実例1: 検証を厳格に行う場面 — ライセンス確認・アップデート確認

自社ツールの core/license.pycore/updater.py は、それぞれライセンス確認サーバー・アップデート確認サーバーへHTTPS通信する際、certifi パッケージが同梱するCAバンドルを明示的に指定してSSLコンテキストを生成している。

def _ssl_context() -> ssl.SSLContext:
    try:
        import certifi
        return ssl.create_default_context(cafile=certifi.where())
    except ImportError:
        return ssl.create_default_context()

わざわざ certifi を明示指定しているのには理由がある。PyInstaller でパッケージ化したmacOS版アプリは、OSのシステムキーチェーン(システムのトラストストア)にアクセスできず、明示的にCAバンドルを渡さないと CERTIFICATE_VERIFY_FAILED で通信そのものが失敗してしまう。ライセンス確認やアップデート確認の通信先は自社が管理するサーバーであり、ここでなりすましを許してしまうと偽のライセンス応答や偽の更新ファイルを掴まされかねないため、chain of trust の検証は省略せず、確実に機能する形で厳格に行っている。

実例2: 検証をあえて省く場面 — 保守対象サイトの死活監視

一方で maintenance_agent.py::_http_status_check() は真逆の設定になっている。

_ctx = ssl.create_default_context()
_ctx.check_hostname = False
_ctx.verify_mode = ssl.CERT_NONE

ホスト名の一致確認 (check_hostname) も証明書の検証 (verify_mode) も、意図的にすべて無効化している。この関数の役割は「更新作業の前後でサイトが正常にHTTPステータスを返しているか」を確認することであり、保守対象のサイトは顧客が個別に契約しているレンタルサーバー上で運用されているため、期限切れ証明書・自己署名証明書・ステージング環境特有の証明書不一致などが起こり得る。ここで厳格なTLS検証をかけてしまうと、「サイト自体は正常に動いているのに、証明書設定の些細な問題でロールバック判定を誤る」という副作用の方が大きくなる。この関数が確かめたいのは「相手のサーバーの身元」ではなく「HTTPステータスコードが返ってくるかどうか」だけであり、目的に対して chain of trust の検証はそもそも不要という判断になっている。

まとめ

TLS証明書の chain of trust は「署名の連鎖をルート証明書までさかのぼって検証する」という単純な仕組みだが、これを「常に厳格に検証すべきもの」と一律に考えるのは正確ではない。何を守りたいかによって、検証の要不要は変わる。自社が管理する重要なエンドポイント(ライセンスサーバー・更新サーバー)との通信では、なりすましを防ぐために chain of trust の検証を確実に機能させる必要がある一方、任意の第三者サイトの単純な死活監視では、その目的にとって証明書の身元確認そのものが的外れになる場合がある。「検証するかどうか」も、コード署名と同じく設計判断の一部だと捉えておくと、なぜあるコードは検証を厳格にし、別のコードはあえて緩めているのかが見えてくる。