cd ~/blog/tls-handshake

2026-09-25

TLSハンドシェイクを図解する

TLSSSL暗号セキュリティネットワーク

ブラウザのアドレスバーの鍵アイコンをクリックすると、接続が保護されている旨のメッセージが出ます。裏側で動いているのがTLS(Transport Layer Security)で、いわゆるSSLもほぼ同じ意味で使われています1。

Chromeでアドレスバーの鍵アイコンをクリックした画面。「この接続は保護されています」という表示とともに、Cookieとサイトデータ、サイトの設定へのリンクが並んでいる

この記事では、実際に通信が暗号化されるまでの手順(ハンドシェイク)を図解します。

前提: 2種類の暗号方式の組み合わせ

TLSは、共通鍵暗号(対称鍵)と公開鍵暗号(非対称鍵)という性質の違う2つの暗号方式を組み合わせて動いています。それぞれの仕組みは前の記事で図解したので、ここでは結論だけ振り返ります。

TLSは、最初だけ公開鍵暗号(を使った鍵交換)で共通鍵を安全に作り、以降のデータ通信はその共通鍵で暗号化するというハイブリッド方式です。ハンドシェイクとは、この最初の共通鍵を安全に作るための手続きのことです。

全体の流れ(TLS 1.3)

TLS 1.3のハンドシェイクは以下のような流れになります。

ポイントは、ServerHelloの直後にはもう双方が共通鍵を計算できることです。そのため証明書やその署名(Certificate、CertificateVerify)はすでに暗号化した状態で送られます。往復が1回で済むので1-RTT(Round Trip Time)ハンドシェイクと呼ばれます。

各ステップの中身

ClientHello

クライアントが最初に送るメッセージです。主に3つの情報が入っています。

  • 対応している暗号スイート(暗号アルゴリズムの組み合わせ)の一覧
  • 鍵交換用の公開値(key_share)。これは後述のDiffie-Hellman鍵交換で使う使い捨ての公開鍵です
  • アクセスしたいホスト名(SNI: Server Name Indication)。1つのIPアドレスで複数ドメインのサイトを運用している場合、サーバーはこれを見てどの証明書を返すか決めます

key_shareを最初から送ってしまうのがTLS 1.3の設計の要です。どの鍵交換方式を使うかを確認してから鍵を送るのではなく、クライアントが先に予測して送ってしまうことで、往復を1回減らしています。

ServerHello + 鍵共有

サーバーは対応する暗号スイートの中から1つを選び、自分側の鍵交換用の公開値を返します。

ここで両者はDiffie-Hellman鍵交換という仕組みを使い、公開値だけをやり取りしながら同じ共通鍵(セッションキー)を別々に計算します。共通鍵そのものはネットワーク上を一度も流れません。

具体的に何をやり取りしているのか、小さい数字で確認してみます。

あらかじめ決まっている公開のパラメータとして、素数 p とその原始根 g があるとします(例: p = 23, g = 5。実際は暗号スイートの一部として決まっている値です)。

  • クライアントは誰にも見せない秘密の数 a を選び(例: a = 6)、公開値 A = ga mod p を計算して送る → A = 56 mod 23 = 8
  • サーバーも同様に秘密の数 b を選び(例: b = 15)、公開値 B = gb mod p を計算して送る → B = 515 mod 23 = 19

ネットワーク上を流れるのはこの A と B だけで、秘密の a, b はどちらも自分の中に留めたままです。

それぞれ、相手から届いた公開値を自分の秘密の数でべき乗すると、同じ値にたどり着きます。

  • クライアント側: Ba mod p = 196 mod 23 = 2
  • サーバー側: Ab mod p = 815 mod 23 = 2

どちらも2という同じ値になりました。これが共通鍵の元になります。

同じ値になる理由は単純で、(gb)a mod p も (ga)b mod p も、指数法則で書き直すとどちらも ga×b mod p になるからです。べき乗の順番を入れ替えても答えは変わりません。

なぜ盗聴されても共通鍵がバレないのか

Diffie-Hellman鍵交換は、公開値から元の秘密値を逆算するのが計算量的に極めて困難という数学的性質(離散対数問題)を利用しています。公開値をいくら盗聴しても、それだけでは共通鍵は再現できません。

また、TLSで実際に使われるのはECDHE(楕円曲線を使ったDiffie-Hellman)です。末尾の「E」はEphemeral(一時的)の意味で、鍵ペアを毎回のセッションごとに使い捨てにします。これにより、仮に将来サーバーの秘密鍵が漏洩しても、過去の通信の共通鍵は復元できません(前方秘匿性)。

証明書と署名検証

共通鍵ができた時点から、以降のメッセージはすべて暗号化されます。サーバーは次の3つを暗号化した状態で送ります。

  • Certificate: サーバー証明書と、その正当性を保証する証明書チェーン
  • CertificateVerify: ここまでのやり取り全体に対して、証明書に対応する秘密鍵で作った署名。確かにこの証明書の持ち主本人と通信していることの証拠になります
  • Finished: ハンドシェイクの内容が改ざんされていないことを確認するためのメッセージ認証コード(MAC2)

クライアントは証明書チェーンをたどってこの証明書が信頼できる認証局によって発行されたものかを検証し、CertificateVerifyの署名も検証します。この検証の仕組みは次の記事で扱います。

Finished

サーバーからのFinishedを受け取ったら、クライアントも自分のFinishedを送り返します。

Finishedの中身は、ここまでのハンドシェイクメッセージ全部をハッシュ化した値に対して、共通鍵から導出した鍵でMACを計算したものです。サーバー側のFinishedはClientHelloからCertificateVerifyまで、クライアント側のFinishedはさらにサーバーのFinishedまでを含めて計算します。

受け取った側は同じ計算を自分でもやり直し、届いたFinishedと値が一致するか比較します。一致すれば、次の2つが同時に確認できたことになります。

  • 相手も自分と同じ共通鍵を導出できている(鍵交換が正しく成立した証拠)
  • ここまでのメッセージが1バイトも改ざんされていない(途中で1バイトでも変われば元のハッシュが変わり、MACも変わってしまうため)

ここまで確認できてハンドシェイクは完了です。この時点から双方とも共通鍵でアプリケーションデータ(HTTPリクエスト・レスポンス)を暗号化してやり取りします。

TLS 1.2との違い

TLS 1.2では暗号スイートを決める → 鍵交換するという2段階が分かれていて、往復が2回(2-RTT)必要でした。

TLS 1.2はサーバーが鍵交換用パラメータ(ServerKeyExchange)を返してから、クライアントがそれに応じて自分のパラメータを返す、という順番だったため1往復多くかかっていました。TLS 1.3はクライアントが先に予測して送る設計に変えることでこれを1往復に短縮しています。

TLS 1.2TLS 1.3
往復回数2-RTT1-RTT
証明書の暗号化されない(平文)される
使える鍵交換方式RSA鍵交換 / (EC)DHE(EC)DHE のみ
前方秘匿性方式によっては無い(RSA鍵交換の場合)常にある

TLS 1.2で使えたRSA鍵交換(サーバーの公開鍵で共通鍵そのものを暗号化して送る方式)はTLS 1.3で廃止されました。前方秘匿性がなく、サーバーの秘密鍵が将来漏れると過去の通信も遡って復号できてしまうためです。

なぜRSAには前方秘匿性がなく(EC)DHEにはあるのか

前方秘匿性の有無を分けているのは、実はRSAかDHかという暗号方式そのものの違いではなく、鍵を使い捨てにしているかどうかです。

  • RSA鍵交換: クライアントが作った共通鍵そのものを、サーバーの公開鍵で暗号化して送ります。このとき使われる鍵ペアは、証明書に紐づいたサーバーが長期間使い回す鍵ペアです。全セッション共通で、生成し直されることもありません。盗聴者がこの暗号文を記録しておけば、何年後であってもこの鍵さえ漏れれば過去の共通鍵をまとめて復元できてしまいます
  • (EC)DHE: 共通鍵の計算材料になる秘密の指数(前述のa, b)を、ハンドシェイクのたびに新しく生成し、終わったらその場で破棄します(名前の「E」がEphemeral=一時的という意味なのはこのため)。サーバーの長期の秘密鍵は鍵交換用の公開値への署名(CertificateVerify)にしか使われず、共通鍵の計算には一切関与しません。だから長期の秘密鍵が後から漏れても、すでに捨てられたa, bを復元する手がかりにはなりません

理論上はRSAでも、セッションごとに鍵ペアを生成して使い捨てにすれば同じように前方秘匿性を持たせられます(エフェメラルRSA)。実際、輸出規制対応として一時的に使われたことがありますが、後にFREAKという攻撃の温床になり廃止されました。

DHEが実質的にこの役割を担うようになったのは、一時鍵の生成コストの違いが大きいです。RSAの鍵ペア生成は大きな素数を探す重い処理ですが、DHの一時指数は乱数を1つ選ぶだけで済みます。だから毎回のハンドシェイクで鍵を作って捨てる、という運用がDHでは現実的なコストで成立します。


次は、ハンドシェイクの中で証明書を検証すると書いた部分の中身、証明書チェーンの仕組みを図解します。

参照

Footnotes

  1. SSL(Secure Sockets Layer)はTLSの前身のプロトコルで、脆弱性が見つかったため現在はすべてのバージョンが非推奨・廃止されています。今使われているのは後継のTLSですが、慣習として「SSL証明書」のように呼び名が残っています。 ↩

  2. MAC(Message Authentication Code、メッセージ認証コード)は、鍵とデータから計算する検証用の短い値です。ハッシュ関数と似ていますが、ハッシュは誰でも同じ値を計算できるのに対し、MACは鍵を持っている人にしか正しい値を計算・検証できません。この性質を使って、データの改ざん検知と、相手が正しい鍵を持っているかの確認を同時に行えます。 ↩