TLS 1.3 Handshake
HTTPSの裏側で何が起きているのか
ブラウザのアドレスバーに鍵マークが出てるとき、裏ではTLSハンドシェイクが完了している。 HTTPSはHTTP + TLSで、TLSがやっているのは「通信相手が本物かどうかの確認」と「通信内容の暗号化」。この2つをたった1往復(1-RTT)で済ませるのがTLS 1.3。
TCPの上にTLSが乗る
HTTPSの接続確立は2段階。まずTCPの3-way handshakeでトランスポート層の接続を確立して、その上にTLSハンドシェイクを重ねる。
TCPが繋がった時点では暗号化は一切ない。ここからTLSが始まる。
TLS 1.3 ハンドシェイク
TLS 1.3はたった1往復で鍵交換と認証を完了する。1-RTT。TLS 1.2が2-RTTだったのに比べて、接続確立のレイテンシが半分になった。
{...} は暗号化されたメッセージ。ServerHelloの直後から暗号化が始まるのがTLS 1.3の大きな特徴。TLS 1.2ではCertificateが平文で流れていた。
ClientHello
クライアントが最初に送るメッセージ。中身は対応する暗号スイートの一覧、ECDHE(楕円曲線Diffie-Hellman鍵共有)の公開鍵、対応するTLSバージョン。
TLS 1.2との決定的な違いは、ClientHelloの時点でECDHEの公開鍵を送ること。TLS 1.2ではServerHelloのあとに別途鍵交換のラウンドトリップが必要だった。TLS 1.3は「先に鍵を送っておくから、サーバ側もHelloで鍵を返してね」という設計。これで1往復短縮できる。
ServerHello
サーバが暗号スイートを1つ選んで、自分のECDHE公開鍵と一緒に返す。
この時点でクライアントとサーバの両方がECDHEの共有秘密(shared secret)を計算できる。クライアントの秘密鍵 とサーバの公開鍵 から 、サーバの秘密鍵 とクライアントの公開鍵 から 。同じ値 が両方で得られる。これが楕円曲線Diffie-Hellmanの本質。
この共有秘密からHKDF(HMAC-based Key Derivation Function)でセッション鍵を派生させる。以降の通信は全てこのセッション鍵で暗号化される。
Certificate と CertificateVerify
サーバは自分のTLS証明書を送る。証明書にはサーバの公開鍵、ドメイン名、認証局(CA)の署名が含まれている。クライアントはCAの公開鍵で署名を検証して、このサーバが本当に example.com の持ち主かどうかを確認する。証明書チェーンをルートCAまで辿って、ブラウザに内蔵されたルートCA証明書と一致すれば信頼できる。
CertificateVerifyはサーバがハンドシェイクの全メッセージのハッシュに秘密鍵で署名したもの。これで「証明書の公開鍵に対応する秘密鍵を本当に持っている」ことが証明される。
Finished
ハンドシェイクの全メッセージのHMACを送り合って、改竄がないことを相互に確認する。ここまで完了すればApplication Data(実際のHTTPリクエスト/レスポンス)を暗号化して送受信できる。
TLS 1.2 との比較
TLS 1.2は2往復かかる。ClientHelloには鍵情報が含まれていないから、ServerKeyExchangeでサーバの鍵を受け取ってから、ClientKeyExchangeでクライアントの鍵を送る。余分な1往復。
他にもTLS 1.3で変わったこと。RSA鍵交換が廃止された(前方秘匿性がないから)。CBCモード暗号が廃止されてAEADのみになった。ChangeCipherSpecメッセージが廃止された。暗号スイートの数が大幅に削減されて、安全なものだけが残った。
前方秘匿性(Forward Secrecy)
TLS 1.3がRSA鍵交換を廃止してECDHEに一本化した理由。
RSA鍵交換では、クライアントがプリマスターシークレットをサーバの公開鍵で暗号化して送る。もしサーバの秘密鍵が将来漏洩したら、過去に記録した全ての通信を復号できてしまう。NSAがトラフィックを全部保存しておいて、10年後に秘密鍵を入手して一括復号、みたいな攻撃が理論的に可能。
ECDHEでは毎接続で使い捨ての鍵ペアを生成する(ephemeral = 一時的)。接続が終わったら秘密鍵を破棄する。サーバの長期秘密鍵が漏洩しても、過去のセッション鍵は復元できない。これが前方秘匿性。
0-RTT リザンプション
TLS 1.3にはさらに速い接続方法がある。0-RTT。以前接続したことがあるサーバに再接続するとき、前回のセッションで得たPSK(Pre-Shared Key)を使って、ClientHelloと同時にApplication Dataを送る。サーバのレスポンスを待たずにデータを送れるからレイテンシがゼロ。
ただし0-RTTにはリプレイ攻撃のリスクがある。攻撃者がClientHelloとearly_dataをキャプチャして再送すると、サーバは2回目のリクエストも受け付けてしまう可能性がある。だから0-RTTはべき等な操作(GETリクエスト等)にだけ使うべきで、POSTやPUT(状態を変更する操作)には使わない。
AEAD暗号
TLS 1.3で使える暗号はAEAD(Authenticated Encryption with Associated Data)だけ。
AEADは暗号化と認証を1つの操作で同時にやる。従来のTLS 1.2ではAES-CBCで暗号化してからHMACで認証する(Encrypt-then-MAC)という2段階だったけど、AEADは1ステップ。暗号化されたデータに認証タグが付いていて、復号時に改竄があれば即座に検出できる。
TLS 1.3で使える暗号スイートは5つだけ。
TLS_AES_128_GCM_SHA256TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_CCM_SHA256TLS_AES_128_CCM_8_SHA256
AES-GCMはIntelのAES-NI命令で高速化される。ChaCha20-Poly1305はAES-NIがないモバイルデバイスで高速。GoogleがAndroidのChromeでChaCha20を推した理由がこれ。
TLS 1.2では37種類以上の暗号スイートが使えて、そのうち安全でないもの(RC4、3DES、MD5ベースの認証)も含まれていた。TLS 1.3は「安全なものだけ残す」という設計思想で、選択肢を大幅に削った。
証明書チェーン
ブラウザが証明書を信頼する仕組み。サーバ証明書は中間CA(Intermediate CA)が署名していて、中間CAはルートCA(Root CA)が署名している。ブラウザにはルートCAの証明書が内蔵されていて(Windows/macOS/Linuxのトラストストア)、ルートCAまでチェーンを辿れれば信頼できると判断する。
Let's Encryptの登場で無料のTLS証明書が手に入るようになって、HTTPSの普及率は2025年時点で95%を超えている。かつてはHTTPSは「銀行とECサイトだけ」だったけど、今はHTTPのサイトのほうが珍しい。ChromeはHTTPサイトに「保護されていない通信」の警告を出すようになったし、HTTP/2とHTTP/3はTLS必須。(実際このサイトもhttpsだしね)
鍵マークの裏側で起きていることを知っていれば、少なくとも面接で恥をかくことはないと思う。多分。