menu
デジタルファームは、札幌・東京・大阪を拠点に、Web戦略の立案からシステム開発、ホームページ制作、SNS運用、運用改善まで一貫支援。Web活用によるビジネス課題解決をサポートします。

SSL/TLS 証明書チェーンの仕組みを「処理」から理解する

シェア
ツイート
シェア
ブックマーク
タイトルとURLをコピー

公開日:2026/10/02

■この記事でわかること

  • クライアントがTLS/SSL証明書を「信頼できる」と判断するまでに、実際にどんな処理を行っているか
  • 証明書が正しいと判断するために何を確認する必要があるか
  • 同じ証明書でも、クライアントや環境によって検証結果が変わるのはなぜか

はじめに

SSL/TLS 証明書の説明では、「ルート証明書 → 中間CA証明書 → サーバー証明書の3階層」といった表現がよく使われます。ところが、この説明だけで仕組みを理解しようとすると、実際にクライアントが行っている処理や、サーバーに設定する作業と噛み合わない部分が出てきます。

筆者が遭遇したのは、クロス証明書の設定不備によって、一部のクライアントだけが接続に失敗するというトラブルでした。原因を理解するには、証明書チェーンの基礎から整理し直す必要がありました。そこで本記事ではまず基礎部分をまとめ、トラブルそのものは次の記事で扱います。

仕組みそのものは既に多くの記事で説明されていますが、本記事では証明書チェーンの検証を「クライアントが実際に何をしているか」という、あまり語られていない観点から説明します。 

1. 範囲を絞る:TLS がやっている3つのこと

TLS(本記事では SSL も含めて TLS と呼びます)のハンドシェイクは、次の3つの独立した問いを、1回のやり取りに詰め込んだものです。

  1. この証明書は信用できるか?(チェーン検証)
  2. 相手は、その証明書の本当の持ち主か?(所有の確認)
  3. どうやって盗聴されずに話すか?(鍵交換と暗号化)

同じ文脈で「鍵」という言葉が出てくるので、「通信データは証明書の公開鍵で暗号化される」と考えてしまうかもしれません。しかし、TLS 1.3 や、現在主流の ECDHE を使う構成では、そうではありません。データの暗号化には、接続のたびに鍵交換(ECDHE など)で作る、使い捨ての共通鍵が使われています。

これらの構成では、証明書に入っている鍵はデータを暗号化するためのものではありません。「鍵交換の相手が誰か」を確かめるために使われます。

TLS 1.2 と 1.3 では3つの処理の順番が違いますが、それは「詰め込む順番」の違いにすぎません。本記事では1と2、つまり「証明書が正しいか」の判定だけを扱います。

補足(本記事の内容には影響しないので、読み飛ばして構いません): 古い TLS 1.2 の RSA 鍵交換(ECDHE を使わない暗号スイート)に限っては、証明書の公開鍵で鍵の素材(premaster secret)を暗号化していました。TLS 1.3 ではこの方式は廃止されています。

2. 登場人物:CA と証明書

CA(認証局)とは

CA(Certificate Authority、認証局)は、「この公開鍵は、この名前(ドメインや組織)の持ち主のものである」ことを確認し、その内容に署名した証明書を発行する主体です。GlobalSign、SECOM、Let’s Encrypt などの事業者が運営する公的な CA のほか、企業が社内向けに自分で運営するプライベートな CA もあります。

CA は自分の鍵ペア(秘密鍵と公開鍵)を持っています。

  • 秘密鍵:証明書に署名するために使います。CA の外で利用されることはありません。
  • 公開鍵:その CA が署名した証明書を検証するために使います。CA 自身の証明書に入れて公開します。

CA には2種類の役割があります。

  • ルートCA:信頼の起点になる CA です。上位の CA を持たず、自分の証明書には自分で署名します。
  • 中間CA:ルートCA(または別の中間CA)から署名を受けた CA です。実際の発行業務を担い、サーバー証明書に署名します。

ここで注意したいのは、「CA」は鍵を持って署名する主体のことで、「証明書」はその公開鍵を載せたデータのことだという点です。「ルートCA」と「ルート証明書」は日常的に混同されますが、本来は別のものです。署名に使われるのは、証明書ではなく CA の秘密鍵です(詳しくは5章で説明します)。

登場する証明書

以上を踏まえると、TLS 証明書の文脈では、役割の異なる主に3種類の証明書が登場します。

種類 中身 署名しているのは
ルート証明書 ルートCAの公開鍵 ルートCA自身(自己署名)
中間CA証明書 中間CAの公開鍵 上位のCA(通常はルートCA)の秘密鍵
サーバー証明書 ドメイン名、サーバーの公開鍵、有効期限など 中間CAの秘密鍵

 

このように、各 CA の秘密鍵による署名を順につないで、チェーンを作っています。チェーンの終点にあるルート証明書には、それ自体の正しさを証明する上位の存在がありません。そこで、OS やブラウザがあらかじめルート証明書をトラストストアに収録しています。チェーンをたどった結果、自身のトラストストアに収録されている証明書にたどり着ければ、信頼できる証明書だと判断します。

なお、トラストストアについては8章で詳しく扱います。

3. チェーンの構造:段数は構成次第

一般的な公的証明書は、2章の3種類が1枚ずつ並ぶ「サーバー証明書 → 中間CA → ルート」の3段構成です。ただし、仕組みとしては3段である必要はありません。検証のルールは「トラストストアにある証明書まで署名がつながるか」だけなので、段数は構成によって変わります。

段数 構成例
1 自己署名のサーバー証明書(いわゆるオレオレ証明書)。証明書そのものをトラストストアに入れれば、検証は成立します。https://localhost での接続などで、ブラウザにエラーを出させないようにするときに利用されます。
2 ルートが直接サーバー証明書に署名。社内 PKI や、mkcert などで開発用のローカルCAを作る場合に採用されます。
3 ルート → 中間CA → サーバー証明書。公的な証明書の標準的な形です。
4以上 中間CAを複数重ねる。用途や組織ごとに中間CAを分けるなど、理論上は何段でも可能です。


ここでの「段」は、チェーンに並ぶ証明書の枚数のことで、2章で挙げた証明書の役割(3種類)とは別のものです。世間では段数のことも「◯階層」と呼ぶことが多いので、注意してください(この区別は次の記事で重要になります)。

公的な証明書が3段以上になる理由

2段でも検証は成立するのに、公的な証明書で中間CAを挟むのは、次のような運用上の理由からです。

  • ルートの秘密鍵をオフラインで厳重に保管したまま、日常の発行業務を行える
  • 中間CAに問題が起きても、その中間CAだけを失効させれば済む

特に重要なのは後者です。秘密鍵の漏洩などの問題が発生すると、その CA は信用できなくなります。それがルートCAの場合は特に深刻です。信頼できるかどうかは、各トラストストアに配布されている証明書との一致で判定しています。そのため、すべてのトラストストアから問題のあったルート証明書を削除し、新しい証明書を配布しなければなりません。しかし、これをすべての機器に行き渡らせるのは、現実的に不可能です。

この問題を防ぐため、現在は業界の基準である CA/Browser Forum Baseline Requirements によって、ルートから利用者向けの証明書を直接発行することが禁止されています。そのため、公的な証明書は必ず3段以上になります。

自分で作ったルートでも仕組みは同じ

検証が見ているのは「手元のトラストストアにあるか」だけで、有名な CA であることに特別な意味はありません。自分で作ったルートCAを機器に組み込めば、その機器との間でだけ正常に TLS が成立します。社内システムや IoT 機器で普通に使われている構成です。

4. 証明書の構造

証明書は、次の3つを連結したバイト列(ASN.1 を DER でエンコードしたもの)です。

Certificate ::= SEQUENCE {
    tbsCertificate       -- 本体:ドメイン名、公開鍵、有効期限、発行者名(Issuer) など
    signatureAlgorithm   -- 例: sha256WithRSAEncryption
    signatureValue       -- 本体に対する発行者の署名
}


ポイントは、署名値が本体の外に別途添付されていることです。署名は本体が確定してから作ります。本体に含めると「署名を作るには署名が必要」という循環構造になってしまうため、本体と署名値を分けて並べることで、これを回避しています。

5. 署名と検証(1段分)

発行時(中間CA側)

  1. 本体(tbsCertificate)のバイト列を、証明書に記載のハッシュ関数(SHA-256 など)にかけます。
  2. そのハッシュ値を、中間CAの秘密鍵で署名して署名値を作ります。
  3. 本体・署名アルゴリズム・署名値を連結して、証明書として発行します。

検証時(クライアント側)

  1. サーバー証明書の Issuer(発行者名)を読み、同じ Subject(主体者名)を持つ証明書を、サーバーから送られてきた中間CA証明書とトラストストアの中から探して、発行者を特定します。名前に加えて、Authority Key Identifier(AKI)と発行者側の Subject Key Identifier(SKI)の一致も手がかりにします。
  2. サーバー証明書から本体部分のバイト列を切り出し、指定されたハッシュ関数でハッシュ値を計算します。
  3. 発行者の証明書に入っている公開鍵を使い、署名値がそのハッシュ値に対する正しい署名かを判定します。

失敗した場合は、候補がなくなるまで手順1に戻って次の候補を試します。

署名値の発行・検証の式は以下のようになります(RSA や ECDSA で、ハッシュ計算に SHA-256 を利用する場合)。

signature = sign(中間CAの秘密鍵, sha256(tbs))              # 発行時
ok        = verify(中間CAの公開鍵, sha256(tbs), signature)  # 検証時


ここで区別しておきたいのが、ハッシュ値と署名値の違いです。

  • ハッシュ値:アルゴリズムは公開された標準のものであり、同じ入力なら必ず同じバイト列を生成します。そのため、クライアントは発行時と同じ値を再現できます。
  • 署名値:署名値の生成には秘密鍵が必要なので、クライアントが署名値を1から作ることはできません。一方、公開鍵を用いれば「正しい署名か」を判定できます。生成時に乱数を使う方式(ECDSA など)では毎回値が変わりますが、それでも正しい署名かは判定できます。

署名値の検証が通れば、次の2点が確定します。

  • 本体は改ざんされていないこと(1ビットでも変わればハッシュ値が変わります)
  • 署名したのは、その公開鍵と対になる秘密鍵の持ち主であること

ただし、これで分かるのは「この発行者証明書に入っている公開鍵の持ち主が署名した」ところまでであり、発行者証明書そのものが本物かはまだ分かりません。そこで、トラストストアにある証明書にたどり着くまで、同じ手順を繰り返します。

補足:本記事では署名のつながりに絞って説明しますが、実際のチェーン検証では各段で次の項目も確認しています。

  • 有効期限
  • 親が CA であること(basicConstraints の CA:TRUE、pathLenConstraint)
  • 鍵の用途(keyUsage/extendedKeyUsage の serverAuth)
  • 失効状態(扱いは実装によって異なります)

6. チェーン構築は「ループ」

証明書の出どころは2つ

5章の検証時の手順1で示した通り、クライアントが検証に使う証明書は、次の2か所から取得します。

  • サーバーが送る証明書:サーバー証明書と、サーバー証明書とルートの間をつなぐ中間CA証明書。中間CA証明書は複数枚のこともあります。
  • トラストストア(クライアント):信頼することを決めた、環境固有のルート証明書のリスト。

サーバーや CDN の設定画面では、入力欄にサーバー証明書と中間CA証明書を連結したものを設定することが多くあります。例えば Apache では、2.4.8 より前はサーバー証明書と中間CA証明書を別ファイルとして個別に指定していました(SSLCertificateChainFile)。2.4.8 以降はこのディレクティブが非推奨となり、これらを1つのファイルに連結したものを SSLCertificateFile に設定するようになっています。

なお、連結したファイルでは、サーバー証明書を先頭に置く必要があります。クロスルート証明書を含む場合など、中間CA証明書が複数あるときの並び順は、TLS 1.2 の仕様(RFC 5246)では「後ろの証明書が直前の証明書の親になる順」が求められています。ただし、多くのクライアントは名前で親を探すため、実際には影響しないことがほとんどです。この並び順については、次の記事で説明します。 

処理の流れ

クライアントは「今の証明書の親は誰か」を1つずつ調べるループを回しています。単純化すると、次のようなループになります。

今 = サーバー証明書
loop:
    親の名前 = 今.Issuer
    if 親がトラストストアにある:  親の公開鍵で「今」を検証 → 成功して終了
    elif 親が中間CA欄にある:     親の公開鍵で「今」を検証 → 今 = 親 にして続行
    else:                        失敗(チェーンが途切れた)

3段の構成なら、次のようになります。

  1. 今=サーバー証明書。親は中間CA → ストアにない → 中間CA欄にある → 検証して進む
  2. 今=中間CA。親はルート → ストアにある → 検証して終了

段数が違っても、このループが回る回数が変わるだけです。

実際の実装では、親の候補が複数見つかることがあります。その場合、多くの実装は候補ごとに検証を試し、失敗したら別の候補で経路を探し直します(深さ優先探索)。ただし、どこまで別の経路を試すかは実装によって異なります。この違いは、次の記事で扱うクロス証明書で重要になります。

「中間証明書を設定し忘れる」というよくあるトラブルは、1回目のループで elif に該当する証明書がなく、else に落ちている状態です。なお、足りない中間CA証明書を自動で補完して通してしまうクライアントもあります(8章で説明します)。

7. 所有の確認

5・6章で扱ったのは、1つ目の問い「この証明書は、信頼できる発行元が発行したものか?(チェーン検証)」でした。チェーン検証が通れば、証明書は信頼できる CA が発行したもので、改ざんもされていないことが分かります。

しかし、それだけでは2つ目の問い「この証明書を送ってきた相手は、証明書に書かれた本人か?」には答えられません。ここで確認すべきことは2つあります。

ホスト名の一致

まず、証明書が「接続しようとしている相手」のものかを確認します。攻撃者が自分のドメインの正規の証明書を送ってきても、チェーン検証は問題なく通ってしまうからです。そこでクライアントは、証明書の SAN(SubjectAltName)に、接続先のホスト名が含まれているかを確認します。

秘密鍵の所有

次に、相手がその証明書の本当の持ち主かを確認します。証明書は公開情報なので、誰でも入手してそのまま送れます。偽のサーバーが本物のサーバーの証明書を送ってきても、チェーン検証もホスト名の確認も同じように通ります。

身分証にたとえると、チェーン検証は「この免許証は本物か」、ホスト名の確認は「免許証の名前は、会いに来たはずの相手と一致するか」、秘密鍵の所有は「この免許証を見せている人は、写真の本人か」の確認です。免許証が本物で名前が合っていても、見せている人が別人なら意味がありません。

証明書で「写真と顔を見比べる」役割を果たすのが、秘密鍵です。証明書には公開鍵が入っていて、それと対になる秘密鍵は、本来の持ち主しか持っていません。そこでサーバーは、ハンドシェイク中のやり取りに自分の秘密鍵で署名して送ります(TLS 1.3 では CertificateVerify、TLS 1.2 の ECDHE では ServerKeyExchange)。クライアントがそれを証明書の公開鍵で検証できれば、相手が秘密鍵を持っている本人であることが確定します。

本記事では、チェーン検証・ホスト名の一致・秘密鍵の所有のすべてが通ることを「証明書が正しい」と呼びます。

補足:curl -k とクライアント証明書

curl -k は、チェーン検証とホスト名の検証をクライアントが自分で放棄しているだけです。鍵交換と暗号化は行われますが、相手が偽物でも気づけない状態になります。なお、サーバーの秘密鍵と証明書が合っていない場合は秘密鍵の所有の確認で失敗するので、-k を付けても通信できません(多くのサーバーソフトウェアは、そもそも設定の読み込み時にエラーになります)。

サーバー証明書は「クライアントを守るため」の仕組みで、アクセス制御ではありません。「許可した機器しか接続させない」ためには、同じ仕組みを逆向きに使うクライアント証明書(mTLS)を用います。

8. トラストストアの実例

ここまで「トラストストア」と一括りにしてきましたが、実際にはクライアントごとに別々のトラストストアがあります。同じ PC の中でも、ブラウザやプログラムによって参照しているものが違います。

クライアント 参照するトラストストア 更新のされ方
Linux 上の curl、wget、OpenSSL を使うプログラム OS のストア。Debian/Ubuntu は ca-certificates パッケージ(/etc/ssl/certs)、RHEL 系は ca-certificates(/etc/pki/ca-trust) パッケージの更新。独自のルートを追加したら update-ca-certificates(Debian 系)や update-ca-trust(RHEL 系)で反映
Windows のアプリ Windows の証明書ストア(Microsoft のルートプログラム) Windows Update。足りないルートを必要時に取得する仕組みもあります
macOS / iOS キーチェーン(Apple のルートプログラム) OS のアップデート
Chrome Chrome 独自の Chrome Root Store(iOS 版は Apple のストア)。OS のストアにユーザーや管理者が追加したルートも信頼します Chrome のアップデート
Edge Microsoft のルートプログラム Edge/Windows のアップデート
Firefox Mozilla 独自のストア(NSS)。Windows と macOS では、OS に追加された企業ルートも既定で読み込みます Firefox のアップデート
Java JDK に同梱された cacerts JDK のアップデート
Python(requests) certifi パッケージに同梱されたストア pip でのパッケージ更新
Node.js Node.js 本体に同梱されたストア(既定)。NODE_EXTRA_CA_CERTS で追加可能 Node.js のアップデート

ここから、実務でよく出会う現象が説明できます。

  • Chrome と Edge で結果が違う:同じ Windows 上で動いていても、Chrome は Chrome Root Store、Edge は Microsoft のルートプログラムと、参照するストアが違います。片方にだけ新しいルートが入っていれば、片方だけでエラーになります。
  • ブラウザでは見えるのに、サーバーからの API 呼び出しが失敗する:ブラウザは頻繁に更新されますが、サーバーの ca-certificates や、コンテナイメージに焼き込まれたストアは、更新しない限り古いままです。
  • curl では通るのに、Python や Java では失敗する:OS のストアではなく、言語ランタイムやパッケージに同梱されたストアを見ているためです。
  • 一部のコールバックサービスで、Let’s Encrypt の証明書では動かないといった報告がある:コールバックを呼び出すプログラムが使用するトラストストアに、Let’s Encrypt のルート証明書が入っていない場合があります。
  • 組み込み機器が突然つながらなくなる:ファームウェアを更新しない機器のストアは、出荷時点のまま止まっています。

なお、「足りない中間CA証明書を自動で取りに行く(AIA による補完)」「過去に見た中間CA証明書をキャッシュしておく」といった補完の有無も、クライアントの実装によって異なります。これも、環境によって結果が違う原因になります。

9. トラストストアの更新という問題

ここまでの話から、1つ大きな問題が見えてきます。

CA が新しいルートを作っても、世界中のトラストストアに行き渡るまでには何年もかかります。OS・ブラウザ・言語ランタイム・組み込み機器が、それぞれ別々の周期で、別々の判断で更新されるからです。中には、二度と更新されない機器もあります。

一方で、CA がルートを新しくしたい理由はいくらでもあります。古いルートの期限が近い、暗号アルゴリズムを新しくしたい、ブラウザ側のルートプログラムの要件が変わった、などです。

新しいルートの配下で発行した証明書を、新しいルートをまだ持っていないクライアントにも信頼させるには、どうすればよいでしょうか。この問題を解決するために使われるのが、クロスルート証明書(クロス証明書)です。簡単に言えば、既に普及している古いルートに、新しいルートを保証してもらう仕組みです。

クロス証明書は、本記事で説明したループの上でどのように扱われるのか。そして、なぜ設定を誤るとエラーになるのか。次の記事で、実案件で遭遇したトラブルを題材に詳しく説明します。

まとめ

クライアントの立場から「TLS 証明書によって、通信相手が安全であることをどのように確認しているのか」を、具体的な手順を添えてまとめました。概念ではなく具体的な手順を知ることで、あやふやになりがちなこの領域への理解が深まれば幸いです。

参考