すべての HTTPS リクエストは、ほとんどのエンジニアが目を向けることのない会話から始まります。ブラウザーが GET / を送る前に、ブラウザーとサーバーはプロトコルのバージョンを合意し、鍵を交換し、証明書を確認し、そのどれも改ざんされていないことを証明します。これがうまくいかないと handshake_failure や bad_certificate のような分かりにくいエラーが出て、遅ければすべてのページがその代償を払います。
TLS 1.3 は、ハンドシェイクを TLS 1.2 より短く、よりプライベートなものにしました。この記事では、シーケンス図を使ってメッセージを 1 つずつたどり、本番環境で出会うバリエーションを取り上げます。HelloRetryRequest、セッション再開と 0-RTT、相互 TLS、Encrypted Client Hello、耐量子鍵交換です。
ハンドシェイクが果たすべきこと
TLS ハンドシェイクは、アプリケーションデータが流れる前に 4 つの仕事をこなします。
- パラメーターを交渉する:プロトコルのバージョン、暗号スイート、鍵交換のグループです。
- 共有鍵を確立する:両者は秘密を一度も送ることなく、同じ秘密を導出します。
- サーバーを認証する:証明書を使い、必要に応じてクライアントも認証します。
- 完全性を確認する:両者は同じハンドシェイクメッセージを見たことを確かめるので、攻撃者は交渉をダウングレードしたり改変したりできません。
現在の仕様は 2026 年 7 月に公開された RFC 9846 です。RFC 8446 を置き換えますが、プロトコルのバージョンは同じままなので、旧 RFC に基づいて作られた TLS 1.3 クライアントは、新しい RFC に基づくサーバーとも引き続き通信できます。
完全なハンドシェイクをメッセージごとに
TLS 1.3 に必要なのは 1 往復です。クライアントはサーバーがどの鍵交換を受け入れるかを予想し、最初のメッセージで自分の側の鍵交換を送ります。
| メッセージ | 方向 | 暗号化 | 運ぶ内容 |
|---|---|---|---|
| ClientHello | クライアント → サーバー | なし | 乱数、暗号スイート、supported_versions、key_share、signature_algorithms、server_name(SNI)、ALPN |
| ServerHello | サーバー → クライアント | なし | 選ばれた暗号スイート、サーバーの key_share |
| EncryptedExtensions | サーバー → クライアント | あり | ALPN プロトコルなど、残りの交渉済み拡張 |
| Certificate | サーバー → クライアント | あり | サーバーの証明書チェーン |
| CertificateVerify | サーバー → クライアント | あり | それまでのハンドシェイクに対する、証明書の秘密鍵による署名 |
| Finished | サーバー → クライアント | あり | トランスクリプト全体に対する MAC |
| Finished | クライアント → サーバー | あり | クライアントによるトランスクリプトの MAC |
重要なのは ServerHello の後の線です。クライアントがサーバーのキーシェアを受け取ると、両者は鍵スケジュール(共有秘密とトランスクリプトに対する HKDF)を実行し、ハンドシェイクのトラフィック鍵を導出します。そこから先は、すべてのハンドシェイクメッセージが暗号化されます。TLS 1.2 ではネットワーク上の観測者がサーバーの証明書を読めましたが、TLS 1.3 では読めません。
CertificateVerify は、サーバーが送ったばかりの証明書の秘密鍵を持っていることを証明します。Finished は、両者が同じメッセージを見たことを証明します。攻撃者が ClientHello から暗号スイートを 1 つ取り除けば、トランスクリプトが食い違い、Finished の検証は失敗します。
サーバーは自分の Finished の直後にアプリケーションデータを送れます。クライアントも自分の Finished の直後に最初のリクエストを送ります。ClientHello から 1 往復で、暗号化された接続の準備が整います。
TLS 1.2 からの変更点
| TLS 1.2 | TLS 1.3 | |
|---|---|---|
| 新規接続の往復回数 | 2 | 1 |
| 鍵交換 | RSA 鍵輸送または (EC)DHE | (EC)DHE のみ、または (EC)DHE 付きの PSK |
| 前方秘匿性 | 任意 | 常にあり(PSK のみの再開を除く) |
| 暗号スイート | CBC や RC4 を含む数十種類 | 5 種類、すべて AEAD |
| 通信路上のサーバー証明書 | 平文 | 暗号化 |
| 圧縮と再ネゴシエーション | サポート | 削除 |
暗号スイートのリストは TLS_AES_128_GCM_SHA256、TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256 と 2 つの CCM スイートにまで絞られました。TLS 1.3 の暗号スイートが指定するのは、レコードの暗号化とハッシュだけになり、鍵交換と署名アルゴリズムは別に交渉されます。RSA 証明書は今も使えます。なくなったのは RSA 鍵輸送です。これはクライアントがサーバーの公開鍵で秘密を暗号化する方式で、鍵が盗まれると記録済みのすべてのセッションが復号されてしまいました。
1 往復が 2 往復になるとき:HelloRetryRequest
1 往復のハンドシェイクは予想に依存しています。クライアントは、サーバーが受け入れると見込んだグループ、たいていは X25519 のキーシェアを送ります。サーバーがそのどれにも対応していなければ、受け入れ可能なグループを示した HelloRetryRequest を返し、クライアントは新しい ClientHello でやり直します。これで丸 1 往復が余分にかかります。
これが起きるのは、クライアントとサーバーのグループが合わないときです。たとえば P-256 だけに設定されたサーバーと、X25519 のシェアしか送らないクライアントの組み合わせです。解決策は設定です。クライアントが最初に提示するグループを、サーバーが優先するグループと一致させましょう。
セッション再開と 0-RTT
ハンドシェイクの後、サーバーは NewSessionTicket メッセージを送れます。チケットがあれば、クライアントは後で完全な証明書交換の代わりに事前共有鍵(PSK)でセッションを再開できます。再開時も通常は新しい (EC)DHE 交換を行うので、再開したセッションも前方秘匿性を保ちます。
チケットがあれば、クライアントは 0-RTT と呼ばれる早期データも送れます。最初のリクエストが ClientHello と同じフライトで送られるのです。1 往復を節約できますが、RFC 9846 は、0-RTT データには接続をまたいだリプレイに対する保証がないことを明記しています。最初のフライトを捕らえた攻撃者は、それを再送できます。
実践的なルールは次のとおりです。
- 早期データとして送るのは冪等なリクエストだけにします。副作用のない
GETは可、支払いや注文は不可です。 - サーバー側では、アプリケーションがリプレイを拒否または許容できる場合に限って 0-RTT を有効にします。多くのサーバーは早期データのリクエストに印を付けるので(たとえば RFC 8470 で定義された
Early-Data: 1ヘッダー)、アプリケーションは425 Too Earlyで応答できます。 - リプレイについて十分に検討していない限り、認証済みの操作に 0-RTT を使ってはいけません。
相互 TLS
相互 TLS(mTLS)では、サーバーがクライアントにも証明書を求めます。サーバーは EncryptedExtensions の後に CertificateRequest を加え、クライアントは自分の Finished の前に、自身の Certificate と CertificateVerify で応答します。往復回数は変わりません。
mTLS はメッシュ内のサービス間のデフォルトです。各ワークロードは短命な証明書を受け取り、サイドカーやプロキシーがハンドシェイクを処理します。それらの証明書がどこから来るかはサービスメッシュのアーキテクチャ図で、リクエストの送信元ネットワークよりもリクエストごとの ID が重要な理由はゼロトラストのアーキテクチャ図で説明しています。
プライバシーと耐量子鍵:2026 年の新しい動き
最近の 2 つの RFC が、通信路上でのハンドシェイクの見え方を変えています。
Encrypted Client Hello。 ClientHello は鍵がまだ何もない段階で送られるため、SNI、つまりクライアントが求めるホスト名が平文で流れます。2026 年 3 月に公開された RFC 9849 は Encrypted Client Hello(ECH)を定義しています。クライアントは本当のホスト名を含む内側の ClientHello を、サーバーが DNS で公開する鍵で暗号化します。観測者に見えるのは公開用の外側の名前だけで、通常は大手プロバイダーの名前です。
ハイブリッドな耐量子鍵交換。 今日記録されたトラフィックが、将来量子コンピューターで復号されるおそれがあるため、まず鍵交換が移行しつつあります。2026 年 8 月に公開された RFC 10024 は、X25519 と ML-KEM-768 を組み合わせた X25519MLKEM768 のようなハイブリッドを定義しています。両方のシェアが鍵スケジュールに入るので、どちらかのアルゴリズムが持ちこたえる限り接続は安全です。Chrome と Edge の 131 以降、Firefox の 132 以降は、これをデフォルトで提示します。
代償はサイズです。X25519MLKEM768 のクライアントシェアは 1,216 バイト、サーバーシェアは 1,120 バイトで、単独の X25519 ではそれぞれ 32 バイトです。ClientHello が 2 つのパケットにまたがることもあるようになり、そんなことは起きないと想定していたミドルボックスが露呈しました。
往復回数というコスト
接続の準備がどれだけ早く整うかを決めるのは、帯域幅ではなく往復回数です。クライアントとサーバーの往復時間が 100 ms で、DNS を除くと次のようになります。
| 構成 | 応答までの往復回数 | 時間 |
|---|---|---|
| TCP + TLS 1.2 + リクエスト | 4 | 400 ms |
| TCP + TLS 1.3 + リクエスト | 3 | 300 ms |
| TCP + TLS 1.3(0-RTT) | 2 | 200 ms |
| QUIC(HTTP/3)、新規接続 | 2 | 200 ms |
| QUIC(0-RTT) | 1 | 100 ms |
QUIC は同じ TLS 1.3 ハンドシェイクを独自のパケットで運び(RFC 9001)、トランスポートのハンドシェイクと統合します。余分な 1 往復はそこで消えます。TLS をユーザーに近い場所、つまり CDN やエッジのロードバランサーで終端すれば、往復時間そのものが短くなります。終端がどこで行われることが多いかはロードバランサーの図で扱っています。
ハンドシェイクの図
ダイアグラムの内容
- Client (browser)
- Server
- mutual TLS only
- ClientHello: key_share, supported_versions, SNI, ALPN
- ServerHello: key_share, chosen cipher suite
- {EncryptedExtensions}
- {CertificateRequest} (mTLS only)
- {Certificate}: server chain
- {CertificateVerify}: signature over the transcript
- {Finished}: MAC over the transcript
- {Certificate}: client chain
- {CertificateVerify}
- {Finished}
- [Application data], both directions
- NewSessionTicket (for resumption)
図の読み方は次のとおりです。
- 2 本のライフライン。 左がクライアント、右がサーバーで、時間は下に向かって進みます。
- 普通の矢印は平文で送られます。ClientHello と ServerHello です。「handshake keys」と記された線より下は、すべて暗号化されています。
- サーバーのフライトは EncryptedExtensions、Certificate、CertificateVerify、Finished を一度に運びます。だからハンドシェイクが 1 往復に収まるのです。
- オプションのフラグメントには、相互 TLS の場合にだけ送られるクライアントの Certificate と CertificateVerify が入っています。それを求めるのが、サーバーのフライト内の破線の CertificateRequest です。
- Markdown ブロックは、ハンドシェイクの成否を決める拡張を並べています。バージョン、グループ、署名アルゴリズム、SNI、ALPN です。
デバッグに使えるプロトコル図を描く
- 暗号化の境界を示します。 TLS について最もよくある質問は、観測者に何が見えるかです。線を引きましょう。
- 重要な拡張だけに名前を付けます。 全部ではありません。
key_shareとsupported_versionsで、ほとんどの失敗は説明できます。 - 任意の経路や代替の経路はフラグメントに入れます。 クライアント証明書には
optフラグメント、HelloRetryRequest にはaltフラグメントを使えば、メインの流れが読みやすく保てます。 - 1 つの問いに 1 つの図にします。 完全なハンドシェイク、0-RTT を伴う再開、mTLS の交換は、それぞれに詳細が必要なら 3 つの図に分けます。
ライフライン、フラグメント、命名規則についてはシーケンス図の例で詳しく扱っています。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- シーケンスパックの ライフライン ノードを 2 つ追加し、Client と Server という名前を付けます。
- ライフラインの間に、メッセージを上から下へエッジとして描きます。ClientHello、ServerHello、続いてサーバーの 4 つの暗号化メッセージとクライアントの Finished です。各エッジには、メッセージ名と主なフィールドのラベルを付けます。
- クライアントの Certificate と CertificateVerify を囲むように、演算子
optを指定したシーケンスパックの 複合フラグメント を追加し、サーバーの CertificateRequest を破線のエッジで描きます。 - 拡張と往復回数を書いた基本パックの Markdown ブロックと、暗号化が始まる位置を示す メモ を追加します。
この例で使うダイアグラムノードは 4 個で、Free プランの範囲に収まります。
よくある質問
TLS 1.3 のハンドシェイクには何往復かかりますか? 新規接続では 1 往復で、TLS 1.2 の 2 往復より少なくなります。HelloRetryRequest があると 2 往復目が加わり、0-RTT による再開なら、クライアントはハンドシェイクの往復なしに最初のリクエストを送れます。
TLS 1.3 ではサーバー証明書は暗号化されますか? はい。Certificate メッセージを含め、ServerHello 以降はすべてハンドシェイク鍵で暗号化されます。ClientHello は暗号化されず、その隙間を埋めるのが Encrypted Client Hello です。
0-RTT を有効にしても安全ですか? リプレイされても安全なリクエストに限ります。早期データは捕捉されて再送される可能性があるので、冪等なリクエストに限定し、それ以外はアプリケーションが 425 Too Early で拒否できるようにします。
TLS 1.3 は今も RSA 証明書をサポートしていますか? はい。RSA 署名は今もサーバーの認証に使われています。TLS 1.3 が削除したのは RSA 鍵輸送です。これはセッションの秘密をサーバーの RSA 鍵で暗号化する方式で、鍵が漏れると過去のトラフィックが露出していました。
TLS と mTLS の違いは何ですか? 通常の TLS では、証明書を提示するのはサーバーだけです。相互 TLS では、サーバーがクライアントにも証明書を求めるので、同じハンドシェイクの中で両者が認証されます。