TLS セッション再開:Session ID・チケット・0-RTT
再接続のたびに重い証明書検証と鍵交換を繰り返す無駄を、Session ID・チケット・TLS1.3のPSK再開で削れます。0-RTTのリプレイ危険も同時に押さえます。
- 1.セッション再開はECDHEと証明書検証を省き、フルハンドシェイクの往復と公開鍵演算を削減する。鍵自体は前回の交換で確立済みのものを使い回す。
- 2.Session IDはサーバー側キャッシュ方式、セッションチケットは状態を暗号化してクライアントに預けるステートレス方式。TLS1.3では両者がPSKに統合された。
- 3.TLS1.3の0-RTTはPSKから導いた鍵で往復ゼロの送信を実現するが、リプレイ耐性がなく、非冪等な操作に使ってはならない。
なぜ再開が必要か
TLS のフルハンドシェイクは、証明書チェーンの検証・署名計算・ECDHE による鍵交換を伴うため、CPU と往復時間の両方を消費します。とくに公開鍵演算(署名検証や曲線上のスカラー倍)は対称鍵暗号より桁違いに重く、同じサーバーに何度も接続する Web クライアントにとって毎回の繰り返しは無駄です。
そこで TLS は セッション再開(session resumption) を用意します。初回ハンドシェイクで確立した秘密(マスターシークレットや再開用の鍵材料)を両者が覚えておき、次回はそれを使って フルハンドシェイクを省略 します。再開時は新しい証明書検証も ECDHE も省けるため、往復が減り公開鍵演算も不要になります。前提となるハンドシェイクの全体像は /network/tls/ と /network/tls13-handshake-internals/ を参照してください。
再開は「鍵材料」を引き継ぐだけで、実際のトラフィック鍵は接続ごとに導出し直します。後述の TLS 1.3 PSK 再開では、保存した PSK に新しい ECDHE を混ぜることもでき、その場合は再開でも前方秘匿性を保てます。再開=同じ鍵の再利用、ではありません。
Session ID:サーバー側キャッシュ方式
TLS 1.2 以前の最も古い方式が Session ID です。初回ハンドシェイクで、サーバーは ServerHello に一意の Session ID を載せ、その ID とネゴシエート済みのセッション状態(マスターシークレット・暗号スイート等)を 自分のメモリにキャッシュ します。
初回:
-> ClientHello (session_id = 空)
<- ServerHello (session_id = S) ... サーバーが S を生成しキャッシュ
[フルハンドシェイク続行]
再開:
-> ClientHello (session_id = S) ... 前回もらった S を提示
<- ServerHello (session_id = S) ... キャッシュにヒット
{Finished} ... 鍵交換も証明書も省略
-> {Finished}
再開時はマスターシークレットがキャッシュにあるため、ECDHE も証明書送信も省かれ、1往復(1-RTT)の短縮ハンドシェイク で完了します。問題は 状態をサーバーが持つ ことです。
ロードバランサ配下に複数のサーバーがあると、再開要求が初回と別のサーバーに届いた時点でキャッシュミスし、フルハンドシェイクに転落します。これを避けるには全ノードでキャッシュを共有するか、同一クライアントを同一ノードへ固定(スティッキー)する必要があり、どちらも運用コストとメモリを増やします。
セッションチケット:ステートレス方式
この欠点を解決するのが セッションチケット(RFC 5077)です。発想を逆転させ、サーバーは状態を持たず、状態をクライアントに預けます。
サーバーはセッション状態を、自分だけが知る STEK(Session Ticket Encryption Key) で暗号化し、その暗号文(チケット)を NewSessionTicket でクライアントに渡します。再開時、クライアントはチケットをそのまま送り返し、サーバーは STEK で復号して状態を復元します。サーバー側のキャッシュは不要です。
初回終了時:
<- NewSessionTicket (ticket = Enc_STEK(master_secret, cipher, ...))
再開:
-> ClientHello (SessionTicket 拡張 = ticket)
<- ServerHello + {Finished} ... STEK で復号して状態復元、鍵交換を省略
-> {Finished}
チケットは STEK さえ共有すればどのサーバーでも復号できるため、ロードバランサ配下でもキャッシュ共有なしに再開できます。引き換えに、セキュリティの要が STEK に集中します。
STEK が漏れると、過去に発行した全チケットを復号でき、そこに含まれるマスターシークレットから過去セッションが解読されます。これは ECDHE で得たはずの前方秘匿性を帳消しにします。STEK は短い周期で自動ローテーションし、古い鍵は安全に破棄しなければなりません。長期間固定された STEK は重大な弱点です。
TLS 1.3:PSK 再開への統合
TLS 1.3(RFC 8446)は Session ID とチケットを廃止し、両方を PSK(事前共有鍵)による再開 に一本化しました。実装上の運搬手段はチケットですが、概念はすべて PSK に統一されています。
初回ハンドシェイク完了後、サーバーは NewSessionTicket で再開用のチケットを送ります。クライアントはこのチケットと、そこから導いた resumption_master_secret を保存します。再開時は ClientHello の pre_shared_key 拡張にチケットを載せ、サーバーがそれを受理すると、保存済みの PSK を起点に鍵スケジュールを回します。
TLS 1.3 では再開時に psk_dhe_ke モードを選べます。これは PSK に加えて 新しい ECDHE 交換 も行うモードで、ClientHello に key_share を併載します。こうすると再開セッションにも新鮮な鍵共有が混ざり、PSK だけに依存しないため前方秘匿性が保てます。PSK のみの psk_ke モードは速いものの前方秘匿性がありません。
PSK は鍵スケジュールの最初の入力になります。フルハンドシェイクでは Early Secret の入力鍵材料がゼロでしたが、再開時はここに PSK が入り、以降のシークレットがそれを基点に連鎖します。
Early Secret = HKDF-Extract(salt=0, ikm=PSK) ... 再開時は PSK を投入
Handshake Secret = HKDF-Extract(salt=deriv(Early),
ikm=ECDHE 共有秘密) ... psk_dhe_ke なら混ぜる
Master Secret = HKDF-Extract(salt=deriv(Handshake), ikm=0)
0-RTT:往復ゼロとリプレイ問題
PSK 再開の延長として、TLS 1.3 は 0-RTT(early data) を提供します。クライアントは PSK から early_traffic_secret を導出し、ClientHello と 同時に 最初のアプリデータを暗号化して送ります。サーバーの応答を待たないため、追加の往復はゼロです。HTTP/3 が採る QUIC もこの仕組みを取り込んでいます(/network/quic-internals/ と /network/http-versions/ 参照)。
問題は、0-RTT データが 新鮮な ECDHE 交換を経ていない ことです。サーバーには「この early data を以前にも受け取ったか」を判定する状態が(ステートレスなチケット設計ゆえ)ありません。攻撃者が 0-RTT パケットを記録して 再送(リプレイ) すると、サーバーは同じ要求を二度処理しうります。
0-RTT の early data には リプレイ耐性がありません。送金・在庫減算・注文確定のような「2回実行されると困る」操作を 0-RTT に載せてはいけません。安全に許せるのは GET のような冪等で副作用のない読み取りに限ります。完全な防御には、サーバー側でチケットの単回利用を強制する仕組み(後述)が要ります。
リプレイを抑える緩和策はいくつかありますが、いずれも完全ではありません。代表的なのは、チケットに含めた経過時間(obfuscated_ticket_age)を検証して鮮度の合わない再送を弾く方法と、受理したチケット ID を記録して二度目を拒否する 単回利用(single-use ticket) です。前者は時間窓内のリプレイを許し、後者はステートレス性とサーバー間共有のコストを犠牲にします。だからこそ、アプリ層で 0-RTT 経由の要求を識別し、危険な処理を拒否する設計が不可欠です。
三方式の比較
| 項目 | Session ID | セッションチケット | TLS1.3 PSK 再開 |
|---|---|---|---|
| 状態の保持場所 | サーバー側キャッシュ | クライアント(暗号化して預ける) | クライアント(チケット)+PSK |
| クラスタ適性 | 低い(共有/固定が必要) | 高い(STEK 共有のみ) | 高い(STEK 共有のみ) |
| 再開の往復 | 1-RTT | 1-RTT | 1-RTT(0-RTT も可) |
| 前方秘匿性 | 維持 | STEK 漏洩で崩壊 | psk_dhe_ke なら維持 |
| 主なリスク | メモリ枯渇・キャッシュミス | STEK の管理失敗 | 0-RTT リプレイ |
| 対応バージョン | TLS 1.2 以前 | TLS 1.2(拡張) | TLS 1.3 |
実務で確認するコマンド
# 1回目の接続でチケットを保存
openssl s_client -connect example.com:443 -tls1_3 \
-sess_out sess.pem </dev/null
# 保存したチケットで再開を試み、Reused 表示を確認
openssl s_client -connect example.com:443 -tls1_3 \
-sess_in sess.pem </dev/null | grep -E "Reused|Session-ID|TLS session ticket"
出力に Reused, TLSv1.3, Cipher is ... と出れば再開が成立しています。exam 観点では、Session ID とチケットの違い(状態をどちらが持つか)、STEK ローテーションの必要性、そして 0-RTT が冪等処理限定である理由の3点が頻出です。再開は速度のための仕組みであると同時に、状態管理と鍵管理の設計判断そのものだと捉えると、各方式の長短が一貫して理解できます。
ネットワーク Article
TLS セッション再開:Session ID・チケット・0-RTTを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
TLS
比較で見る軸
難易度: advanced / カテゴリ: ネットワーク / タグ数: 6
導入後に効く点
Session IDはサーバー側キャッシュ方式、セッションチケットは状態を暗号化してクライアントに預けるステートレス方式。TLS1.3では両者がPSKに統合された。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ネットワーク
- タグ数
- 6
判断チェックリスト
- 自社の用途が「TLS / セッション再開」に近いか確認する。
- 強みである「セッション再開はECDHEと証明書検証を省き、フルハンドシェイクの往復と公開鍵演算を削減する。鍵自体は前回の交換で確立済みのものを使い回す。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。