大きな画面での利用をおすすめします — アニメーションと操作がより快適になります。
入力

TLS ハンドシェイク

TLS ハンドシェイクは、確立済みの TCP 接続に暗号化と認証を加えます。この教材は TLS 1.3 のフルハンドシェイクです。双方が公開鍵シェアを交換し、同じ秘密を別々に導出します。サーバーは今回のハンドシェイクの要約に署名し、証明書に対応する秘密鍵を持つことを示します。通常は TCP 接続後に 1 RTT かかります。

色を混ぜる例を考えます。双方が公開の黄色に自分だけの色を混ぜ、混合色を交換し、受け取った色に自分の色を加えると同じ色になります。観察者が見るのは混合色だけです。これは Diffie–Hellman の合意を表しますが、相手の身元までは保証しません。その認証がこの教材の焦点です。

TLS ハンドシェイクの仕組み

クライアントは秘密値 a を選び、A = g^a mod p だけを送ります。サーバーは b を選び、B = g^b mod p だけを送ります。クライアントの B^a mod p とサーバーの A^b mod p は、ともに g^(ab) mod p となり、これを S とします。a、b、S は送信しません。適切な実パラメーターでは公開値から秘密を求める計算が困難です。本例の小さな数値は簡単に破れるため、計算確認専用です。

双方は S とハンドシェイク記録を HKDF の鍵導出に使い、ハンドシェイク鍵を得ます。ServerHello より後のハンドシェイク内容は証明書も含めて暗号化されます。ただしレコードヘッダーは見え、互換用 ChangeCipherSpec も例外です。以後の全バイトが隠れるという意味ではありません。

Certificate は、証明書チェーンで認証される身元と公開鍵を提供します。例えば、その鍵が example.com に対応することを示します。証明書は公開情報です。有効な証明書をコピーしても秘密鍵の所持は証明できません。

CertificateVerify は、今回のハンドシェイク記録のハッシュに規定のコンテキストを付け、証明書に対応する秘密鍵で署名します。記録には双方の key share が入るため、別の接続から署名を持ち込めません。検証に成功して初めて、現在の相手が秘密鍵を持つと確認できます。証明書検証とこの署名検証の両方が必要です。

最後に双方が Finished を送ります。派生した Finished 鍵で、それまでのハンドシェイク記録のハッシュに HMAC を計算します。S を直接 HMAC 鍵にするわけではありません。平文の hello を途中で変えると記録が食い違い、検証が失敗します。TLS は別途バージョンダウングレード保護も持つため、すべてを Finished だけで防ぐという意味ではありません。

TLS ハンドシェイクのステップ

初期設定の full handshake は example.com:443 への接続で、TCP は確立済みです。手計算できる p = 23、g = 5 を使います。

  1. 初期状態。 TCP は使えますが、TLS 鍵はまだありません。それまでのバイトは経路上から読めます。
  2. クライアントのシェア。 秘密値 a = 6、公開値 A = 5^6 mod 23 = 8。秘密値の行には鍵マークが付き、パケットに出ません。
  3. 平文の ClientHello。 key_share A = 8 と SNI ホスト名を送ります。ECH は使いません。実パラメーターでは a の復元が困難ですが、この小さな数値にはその安全性がありません。
  4. サーバーのシェア。 秘密値 b = 15、公開値 B = 5^15 mod 23 = 19。一時的な秘密値は接続ごとに作り、使用後に破棄します。
  5. サーバーが S を導出。 A^b mod 23 = 8^15 mod 23 = 2。秘密を受信せずに鍵を導出できます。
  6. ServerHello。 key_share B = 19 を送ります。この流れの最後の非暗号化ハンドシェイクメッセージです。レコードヘッダーまで消えるわけではありません。
  7. クライアントが S を導出。 B^a mod 23 = 19^6 mod 23 = 2。同じ値を得ますが S は送信していません。図の帯より下が暗号化されたハンドシェイク内容です。
  8. Certificate。 画面は身元を CN = example.com と略記します。実際のホスト名検証は CN ではなく subjectAltName を使います。証明書はサーバーの公開鍵を持ち、チェーンが信頼する CA に達します。転送の暗号化はプライバシーのためで、証明書自体は公開できます。
  9. CertificateVerify。 対応する秘密鍵で今回の記録ハッシュと規定のコンテキストに署名します。
  10. サーバー Finished。 派生したサーバー Finished 鍵で記録ハッシュの HMAC を計算します。
  11. 証明書の 3 検査。 チェーンの信頼、要求したホスト名、有効期間を確認します。すべて通っても認証はまだ終わりません。
  12. 署名検証。 簡略表現 verify(cert.pub, sig, H(transcript)) が true。証明書は鍵の身元を、署名は現在の相手の秘密鍵所持を示します。コピーしただけの証明書はここで失敗します。
  13. 記録の検証。 クライアントが自分の記録から Finished HMAC を再計算します。一致は、認証された記録がここまで整合していることを示します。
  14. クライアント Finished。 応答を返し、双方でハンドシェイクが完了します。
  15. アプリケーション鍵。 HKDF で別のトラフィック鍵を導出します。HTTP リクエストをハンドシェイク用トラフィック鍵で暗号化しません。
  16. GET / HTTP/1.1 を送信。 暗号化済みで、TCP 接続後から 1 RTT です。
  17. 結果。 画面は転送 7 回、ハンドシェイク署名の検証 1 回、鍵の秘密の送信 0 回を数えます。7 回にはアプリケーション要求が入り、EncryptedExtensions を省くため、実 TLS のハンドシェイクメッセージ総数ではありません。盗聴者が見るのは A = 8、B = 19、アドレス、時刻、サイズ、この例の SNI で、要求内容ではありません。

実際のメッセージは次のコマンドで観察できます。OpenSSL は接続の当事者なので、暗号化レコード内の内容を復号して表示できます。鍵のない受動的キャプチャとは異なります。

openssl s_client -connect example.com:443 -tls1_3 -msg

有効な証明書だけでは足りない理由

On-path attacker を選びます。攻撃者は途中のルーターを制御し、転送や書き換えができます。そのまま中継すると、クライアントとサーバーだけが知る鍵ができるため、key share を置き換えます。

攻撃者は m = 9、M = 11 を使います。クライアントの A = 8 を保持し、サーバーには M を送り、クライアントへも B の代わりに M を返します。クライアント側との秘密は M^a mod 23 = 9、サーバー側とは M^b mod 23 = 10。攻撃者は双方と秘密を共有しますが、クライアントとサーバーは互いに共有していません。

このシナリオのステップ 11 では、両方の導出が成功し、未認証の暗号化チャネルは攻撃者を検出していません。Diffie–Hellman は「ある相手」と鍵を共有する仕組みで、その相手の身元は別途確かめます。

攻撃者は本物の証明書を変更せず転送します。チェーン、名前、日付の検査は通ります。しかし CertificateVerify の署名はサーバーが見た H(A=11 · B=19) を対象とし、クライアントは H(A=8 · B=11) を検証します。記録が違うため失敗し、攻撃者はサーバーの秘密鍵がないので正しい署名を作れません。クライアントは decrypt_error を送り、接続を閉じます。ブラウザーはハンドシェイク失敗を表示し得ますが、ホテルなどの証明書警告がすべてこの攻撃を示すわけではありません。

この身元との結び付けをプロトコルからなくすと、攻撃者は各接続の認証コードを計算し直し、アプリケーションデータを中継できます。証明書が公開されていることではなく、今回の交換に対する秘密鍵所持の証明が必要なのです。

セッション再開で省く処理

Session resumption は以前のチケットと関連 PSK を使います。ClientHello がチケット識別子と binder を提供します。binder は対応する秘密から派生した鍵で、binder 値を除いた ClientHello の記録を認証します。識別子をコピーできても PSK の知識は証明できません。

サーバーは対応する秘密を取得して binder を検証します。このモデルは PSK 認証が証明書認証を置き換えるため、Certificate、CertificateVerify、証明書チェックのパネルがありません。表示は 7 転送から 5 転送へ減ります。アプリケーション要求を含み、EncryptedExtensions を省く点は同じです。再開では証明書とハンドシェイク署名を検証しません。

必要なネットワーク往復は 1 RTT のままですが、証明書認証の計算が減れば実経過時間は短くなり得ます。公開鍵計算がすべてなくなるわけではありません。この設定は新しい a を生成し、一時鍵交換を行います。新しい S とチケットの PSK の両方を鍵導出へ入力します。

選んでいるのは PSK + 一時 (EC)DHE モードです。TLS 1.3 は PSK-only も許しますが、同じ前方秘匿性は持ちません。本例で ClientHello と binder を再送しても、攻撃者は新しい共有秘密を得ません。後で PSK が漏れても、一時鍵交換が保護したアプリケーショントラフィックはそれだけでは復元できません。PSK だけに依存する方式では、後の PSK 入手により記録した通信を解読できる場合があります。

境界条件

  • TCP は確立済み。 TLS の 1 RTT に TCP の 3 ウェイハンドシェイクは含めません。新規接続では通常両方の待ち時間がかかります。
  • 0-RTT 早期データ。 再開時の最初の送信にアプリケーションデータを付けられますが、リプレイの危険があります。アプリケーションが再実行を許容できるか判断し、防御を行う必要があります。冪等という分類だけでは、すべての副作用の安全性は決まりません。
  • HelloRetryRequest。 提供した key share をサーバーが受け入れられなければ再試行を要求でき、1 RTT 増えます。共通パラメーターがなければ失敗します。適切なグループを先に提供すれば再試行を避けられます。
  • クライアント証明書。 サーバーがクライアントにも Certificate と CertificateVerify を求めることがあります。内部サービスの認証などで使います。
  • 証明書の失敗。 期限切れ、信頼されないチェーン、名前の不一致は本例の 3 検査で落ちます。誤発行でも受理されるチェーンと名前があれば通る場合があり、全誤発行を検出する保証はありません。
  • ログの違い。 チェーン、名前、日付の問題は unable to get local issuer certificate、ホスト名不一致、期限切れなどです。CertificateVerify の署名不一致も Finished 不一致も decrypt_error を使い得ます。名前だけでなく失敗した段階を確認します。
  • 失効。 秘密鍵漏えい後に証明書を失効できますが、ブラウザーごとに確認方法と失敗時の方針が異なります。短い有効期間はリスク期間を減らしても、全失効確認の代わりにはなりません。

よくある間違い

  • 証明書が接続を直接暗号化すると思う。 アプリケーション鍵は鍵導出から得ます。証明書の公開鍵はハンドシェイク認証に、対応する秘密鍵は署名に使います。
  • 有効な証明書だけで現在の相手を確認したと思う。 攻撃シナリオでも証明書検査はすべて通り、今回の記録に対する署名で失敗します。
  • 安全な鍵交換だけで接続の認証も済むと思う。 未認証の Diffie–Hellman は攻撃者とも鍵を共有します。
  • TLS が通信先の手掛かりをすべて隠すと思う。 ECH のない本例では SNI、宛先 IP、時刻、サイズが見えます。内容を保護しても通信の存在は隠れません。
  • 再開で必ず 1 RTT 減ると思う。 通常の再開は認証処理を減らし、ネットワーク RTT は保ちます。0-RTT は別機能です。計算の短縮は総時間を改善し得ます。
  • TLS 1.2 の図をそのまま読む。 フル TLS 1.2 は通常 2 RTT で、証明書を平文で送り、前方秘匿性のない RSA 鍵配送もありました。TLS 1.3 は RSA 鍵配送を削除しました。本例の一時鍵交換では、一時秘密の破棄後にサーバー署名鍵が漏れても過去の鍵は復元できません。PSK-only と 0-RTT の性質は異なります。

モデルの簡略化

p = 23、g = 5 は 19^6 mod 23 = 2 を手計算するための値で、実用上の安全性はありません。TLS 1.3 は X25519、NIST P-256 などの楕円曲線グループを使え、有限体グループも定義します。楕円曲線演算は、ここでの累乗剰余を 256 ビット整数へ置き換えただけのものではありません。

暗号スイートは 1 つで交渉を省き、HKDF を少数のステップにまとめています。実際は early、handshake、application traffic secret、方向別の鍵、exporter secret、resumption secret を派生し、各段階の記録に結び付けます。ServerHello の直後に必須の EncryptedExtensions は描きません。

0-RTT、HelloRetryRequest、クライアント証明書、チェーン構築、失効の詳細、ECH は省略します。RFC 9849 は ECH を規定し、非公開の SNI を含む ClientHello 内容を暗号化できます。本例の平文 SNI とは異なります。TLS 1.3 は RFC 8446、名前検証は RFC 9525 を参照してください。