概要
ネットショッピングで住所や支払い情報を入力する。
スマホアプリでログインする。
大学やカフェのWi-FiからWebサービスにアクセスする。
普段はあまり意識しませんが、このような通信では、裏側でいくつもの安全確認が行われています。
たとえば、通信内容を途中で盗み見られないようにすること。
入力した金額や住所が、通信の途中で勝手に書き換えられていないこと。
接続している相手が、本当にそのサービスのサーバーであること。
これらを支える代表的な仕組みが、HTTPSで使われる TLS です。
ただし、TLSは「通信を暗号化するだけの仕組み」と考えると、少しもったいないです。
実際のTLS 1.3では、証明書、デジタル署名、鍵共有、ハッシュ関数、HKDF、認証付き暗号など、複数の暗号技術が役割分担しています。
イメージとしては、1つの道具だけで通信を守っているというより、複数の担当者がチームで通信を守っているようなものです。
- 証明書は「接続先が意図したドメインに対応しているか」を確認する
- デジタル署名は「その接続先が対応する秘密鍵を持っているか」を確認する
- 鍵共有は「通信に使う秘密の鍵」を準備する
- HKDFは「用途ごとの鍵」を整理して作る
- AEADは「通信内容を隠し、改ざんも検知する」
この記事では、TLS 1.3をいきなり仕様書の細かい構造から読むのではなく、まず身近な通信の例から出発します。
そのうえで、TLS 1.3の中で暗号技術がどのように組み合わされているのかを、順番に整理します。
💡 豆知識
ブラウザの鍵マークは、「そのサイトに書かれている内容が正しい」「そのサービスが安全に運営されている」ことまで保証するものではありません。
主に、通信経路がTLSで保護されていることや、証明書によって接続先を確認できていることを示します。
そのため、鍵マークがあっても、偽サイトや詐欺ページには注意が必要です。
この記事の立ち位置
この記事は、暗号技術の全体像を整理した記事から派生する、TLS 1.3に焦点を当てた記事です。
暗号技術全体を見ると、共通鍵暗号、公開鍵暗号、ハッシュ関数、MAC、デジタル署名、鍵共有など、たくさんの道具が出てきます。
それぞれを個別に学ぶことは大切ですが、実際のサービスでは、それらが単体で使われることはあまりありません。
TLS 1.3は、暗号技術が「実際の通信の中でどう組み合わされるのか」を学ぶ題材として、とても分かりやすい例です。
| よくある理解 | 本記事で整理したい見方 |
|---|---|
| TLSは通信を暗号化するもの | TLSは認証・鍵共有・暗号化・改ざん検知を組み合わせるもの |
| HTTPSの鍵マークがあればすべて安全 | TLSが守る範囲と、守れない範囲を分けて考える |
| 公開鍵暗号で通信全体を暗号化している | 最初に鍵を準備し、その後は共通鍵系の暗号で通信を守る |
| 暗号スイート名を覚えればよい | それぞれの暗号技術がどの役割を持つかを見る |
この記事で分かること
この記事で分かることは以下です。
- TLS 1.3が、身近な通信のどこで使われているのか
- TLS 1.3が守ろうとしている性質
- ハンドシェイクとレコード保護の大まかな役割
- 証明書、デジタル署名、鍵共有、HKDF、AEADの役割
- TLS 1.2以前と比べたときのTLS 1.3の主な特徴
- 0-RTTの便利さと注意点
- TLS 1.3で守れること、守れないこと
対象読者
この記事は、次のような人を想定しています。
- HTTPSやTLSという言葉は聞いたことがあるが、中身はまだ曖昧な人
- 暗号化、署名、鍵共有、証明書の関係を整理したい人
- TLS 1.3を仕様書に入る前の段階でざっくり理解したい人
- 情報セキュリティを学び始めた人
- 暗号技術が実サービスでどう組み合わされるかを知りたい人
本記事で扱わないこと
本記事では、TLS 1.3の全仕様を網羅することは目的にしません。
そのため、以下は深掘りしすぎず、必要な範囲に絞って扱います。
- TLSライブラリの具体的な設定方法
- OpenSSLやnginxなどの詳細な設定例
- 各暗号アルゴリズムの数学的な安全性証明
- パケットキャプチャを使った詳細な解析
- 証明書発行やPKI運用の細かい実務
- QUICやHTTP/3におけるTLS 1.3の詳細
今後、実装や設定に踏み込む場合は、別記事として分ける想定です。
コード例について
本記事では、TLS 1.3の考え方を理解しやすくするために、Pythonの短いコード例をいくつか使います。
ただし、これらはTLSを自作するためのコードではありません。
TLS 1.3の実装は、OpenSSL、BoringSSL、NSS、各言語の標準ライブラリなど、十分に検証された実装を使うべきです。
ここで示すコードは、AEAD、HMAC、HKDF、ECDHなどの部品が「どのような役割を持つのか」を観察するための最小例として扱います。
実行例には、Python標準ライブラリの ssl モジュールや、cryptography ライブラリを使う箇所があります。
記事内のコードを試す場合は、利用しているPythonやOpenSSL、ライブラリのバージョンによって出力が変わることがあります。
また、外部サイトへ接続するコード例は、実行環境・ネットワーク・接続先サーバーの設定によって結果が変わる可能性があります。
記事全体の流れ
この記事は、身近な通信の例から始めて、TLS 1.3の中で暗号技術がどのように組み合わされるのかを順番に見ていきます。
最初に、HTTPSの裏側で何が起きているのかを整理します。
その後、TLS 1.3が守る性質、ハンドシェイク、証明書、鍵共有、HKDF、AEADの順に少しずつ深掘りします。
最後に、TLS 1.2からの変更点、0-RTTの注意点、TLS 1.3で守れること・守れないことを確認します。
| 章 | 見出し | 主に扱うこと |
|---|---|---|
| 概要 | 身近な通信からTLS 1.3を見る | ネットショッピング、ログイン、Wi-Fiなどから導入する |
| 1章 | HTTPSの裏側では何が起きているのか | 鍵マーク、HTTPとTLSの関係、TLSが必要になる背景 |
| 2章 | TLS 1.3が守る3つの性質 | 認証、機密性、完全性 |
| 3章 | TLS 1.3の全体像 | ハンドシェイクとレコード保護 |
| 4章 | サーバーが本物かをどう確認するのか | 証明書、認証局、証明書チェーン、CertificateVerify |
| 5章 | 通信用の鍵はどうやって準備されるのか | ECDHE、PSK、Forward Secrecy |
| 6章 | HKDFは何をしているのか | 鍵導出、Key Schedule、transcript hash |
| 7章 | 実際の通信データはどう守られるのか | TLSレコード、AEAD、nonce、KeyUpdate |
| 8章 | TLS 1.2から何が変わったのか | 古い方式の整理、暗号スイート、1-RTT化 |
| 9章 | 0-RTTは便利だが注意も必要 | 早期データ、リプレイ攻撃、425 Too Early |
| 10章 | TLS 1.3で守れること・守れないこと | 鍵マークの限界、端末・サーバー侵害、アプリ脆弱性 |
| 11章 | まとめ | 暗号技術の組み合わせとして振り返る |
読み方の目安
全体像だけをつかみたい場合は、まず概要〜3章と11章を読むと流れを追いやすいです。
証明書・鍵共有・HKDF・AEADを詳しく見たい場合は、4〜7章を中心に読むと、TLS 1.3の暗号技術の組み合わせが見えやすくなります。
TLS 1.2との差分や運用上の注意を確認したい場合は、8〜10章を読むとよいです。
1. HTTPSの裏側では何が起きているのか
この章では、TLS 1.3の細かい仕組みに入る前に、「なぜTLSが必要なのか」を身近な場面から整理します。
まずは、ブラウザに表示される鍵マークの裏側で、どのような安全確認が行われているのかを見ていきます。
ネットショッピングで商品を購入するとき、私たちは住所、氏名、支払い情報などを入力します。
大学やカフェのWi-Fiから、いつものWebサービスにログインすることもあります。
このとき、画面上では「送信」ボタンを押しているだけに見えます。
しかし実際には、あなたの端末からWebサーバーまでの間に、Wi-Fiアクセスポイント、ルーター、通信事業者のネットワークなど、いくつもの経路が関係しています。
もし通信内容がそのまま流れていたら、途中のどこかで内容を盗み見られたり、書き換えられたりする危険があります。
また、見た目が本物そっくりの偽サイトに接続してしまうと、利用者は本物のサービスだと思って情報を入力してしまうかもしれません。
このような通信上の問題を防ぐために使われるのが、HTTPSです。
HTTPSは、HTTPの通信をTLSで保護する仕組みとして理解できます。
RFC 9110では、https URIはTLS接続を確立できるオリジンサーバーに対する識別子として定義されており、その文脈で「secured」とは、サーバーがその権限を持つ主体として認証され、HTTP通信に機密性と完全性の保護があることを意味すると説明されています。
参考: RFC 9110 - HTTP Semantics, Section 4.2.2
ここで大切なのは、HTTPSは単に「URLに s が付いている」という飾りではないことです。
その裏側では、通信相手の確認、通信内容の保護、改ざん検知などが組み合わされています。
1.1 通信で起こり得る3つの困りごと
まず、TLS 1.3が必要になる背景を、身近な言葉で整理します。
| 困りごと | やさしく言うと | 身近な例 | TLSで関係する性質 |
|---|---|---|---|
| 盗聴 | 通信内容を第三者に見られる | カフェWi-Fiでログイン情報や入力内容を盗み見られる | 機密性 |
| 改ざん | 通信内容を途中で書き換えられる | 送信した金額や宛先が途中で変えられる | 完全性 |
| なりすまし | 本物ではない相手を本物だと思い込む | 偽サイトに接続してパスワードを入力してしまう | 認証 |
この3つは、情報セキュリティを学ぶとよく出てくる考え方です。
ただし、言葉だけを見ると少し固く感じるかもしれません。
そこで、ネットショッピングを例にして考えてみます。
まず、住所や支払い情報を入力したとき、その内容を第三者に読まれたくありません。
これが 機密性 です。
次に、あなたが「1,000円を支払う」と送った内容が、途中で「10,000円を支払う」に変えられたら困ります。
これを検出するために必要なのが 完全性 です。
そして、そもそも接続している相手が本物のショッピングサイトでなければ、どれだけ通信を守っても意味がありません。
この「相手が本物か」を確認する考え方が 認証 です。
RFC 8446でも、TLSの主な目的は通信する2者の間に安全な通信路を提供することであり、その性質として、認証、機密性、完全性が整理されています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 1
1.2 HTTPSでは、HTTPをTLSで包んでいる
Webページを見るとき、ブラウザとWebサーバーの間ではHTTPというルールでやり取りが行われます。
たとえば、ブラウザが「このページをください」とリクエストし、サーバーがHTMLや画像などをレスポンスとして返します。
ただし、HTTPだけでは通信内容の保護が十分ではありません。
そこで、HTTPのやり取りをTLSで保護したものがHTTPSです。
かなり簡略化すると、次のようなイメージです。
もう少し具体的に見ると、TLSは「安全な通信路」を作り、その上でHTTPのデータをやり取りします。
この図では細かいメッセージ名を省いています。
ここで押さえたいのは、TLSが「HTTPの中身を安全に運ぶための土台」として働いていることです。
たとえるなら、HTTPは手紙の本文、TLSはその手紙を入れる封筒と封印のようなものです。
封筒に入れることで中身を読まれにくくし、封印によって途中で開けられたり書き換えられたりしていないかを確認しやすくします。
ただし、このたとえにも限界があります。
実際のTLSでは、封筒を用意する前に「相手が本物か」「どの暗号方式を使うか」「通信に使う鍵をどう作るか」といった準備も行われます。
1.3 ブラウザの鍵マークが示すもの
ブラウザでWebサイトを開くと、アドレスバーに鍵マークが表示されることがあります。
この鍵マークは、一般にHTTPSで通信していることを示す目印として使われます。
ただし、ここで注意したいことがあります。
鍵マークは、主に「通信経路がTLSで保護されていること」や「証明書によって接続先を確認できていること」に関係します。
一方で、次のようなことまでは保証してくれません。
| 鍵マークから分かること | 鍵マークだけでは分からないこと |
|---|---|
| 通信がTLSで保護されている | そのサイトの商品や情報が正しいこと |
| 接続先のドメインと証明書の対応を確認できている | そのサイトが詐欺目的ではないこと |
| 通信中の盗聴や改ざんを防ぐ仕組みが働いている | 端末がマルウェアに感染していないこと |
| ブラウザとサーバーの通信路が保護されている | サーバー内部が侵害されていないこと |
つまり、鍵マークはとても重要ですが、「鍵マークがあるから何をしても安全」という意味ではありません。
安全な通信路と、安全なサービス運営は分けて考える必要があります。
💡 豆知識
URLのhttps://のsは、一般には secure の意味として説明されることが多いです。
ただし、より正確には「そのHTTP通信がTLSで保護される前提のURIスキーム」と理解するとよいです。
RFC 9110では、httpsの通信における secure は、サーバー認証、機密性、完全性の保護と結びつけて説明されています。
1.4 TLS 1.3は「暗号化だけ」ではない
ここまでの話を見ると、TLSは通信内容を読まれないようにする技術、つまり暗号化の仕組みだと感じるかもしれません。
もちろん、それは大切な役割です。
しかし、TLS 1.3を理解するときは、暗号化だけに注目すると全体像を見失いやすくなります。
実際には、TLS 1.3では次のような処理が組み合わされています。
| やりたいこと | TLS 1.3で関係する主な仕組み | ざっくりした役割 |
|---|---|---|
| 相手が本物か確認したい | 証明書、デジタル署名 | サーバーの身元や秘密鍵の保有を確認する |
| 通信用の鍵を安全に準備したい | ECDHE、PSK | 通信に使う秘密の材料を共有する |
| 用途ごとの鍵を作りたい | HKDF | ハンドシェイク用、通信用などの鍵を分けて作る |
| 通信内容を隠したい | AEAD | データを暗号化する |
| 途中で書き換えられていないか確認したい | AEAD、Finishedメッセージ | 改ざんを検出する |
このように、TLS 1.3は1つの暗号方式ではありません。
複数の暗号技術を組み合わせて、安全な通信路を作るためのプロトコルです。
派生元の記事でも整理したように、HTTPSでは証明書、デジタル署名、鍵共有、共通鍵暗号、AEADなどが組み合わされます。
本記事では、その中でもTLS 1.3に焦点を当てて、「どの処理で、どの暗号技術が、何のために使われているのか」をもう少し詳しく見ていきます。
1.5 この章のまとめ
この章では、TLS 1.3の細かい仕様に入る前に、HTTPSの裏側で何が守られているのかを整理しました。
重要なポイントは、次の3つです。
- Web通信では、盗聴、改ざん、なりすましといったリスクを考える必要がある
- HTTPSは、HTTPの通信をTLSで保護する仕組みとして理解できる
- TLS 1.3は暗号化だけでなく、認証、鍵共有、改ざん検知などを組み合わせるプロトコルである
ここまでで、「なぜTLS 1.3が必要なのか」が少し見えてきました。
次の章では、TLS 1.3が守ろうとしている性質である 認証・機密性・完全性 を、もう少し丁寧に整理します。
2. TLS 1.3が守る3つの性質
この章では、TLS 1.3が守ろうとしている代表的な性質を整理します。
キーワードは 認証・機密性・完全性 です。
難しく聞こえるかもしれませんが、身近な通信に置き換えるとかなり理解しやすくなります。
前の章では、HTTPSの裏側ではTLSによって通信が保護されていることを見ました。
では、TLS 1.3は具体的に何を守っているのでしょうか。
RFC 8446では、TLSの主な目的は、通信する2者の間に安全な通信路を提供することだと説明されています。
そして、その安全な通信路が提供すべき性質として、主に 認証、機密性、完全性 が整理されています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 1
この3つを、まずは身近な言葉で整理すると次のようになります。
| TLS 1.3が守る性質 | やさしく言うと | ネットショッピングで考えると |
|---|---|---|
| 認証 | 相手が本物か確認する | 本物のショップのサーバーにつながっているか確認する |
| 機密性 | 中身を他人に読まれないようにする | 住所や支払い情報を途中で盗み見られないようにする |
| 完全性 | 中身が途中で変わっていないか確認する | 「1,000円を支払う」が「10,000円を支払う」に変えられていないか確認する |
ここで大切なのは、TLS 1.3が「暗号化だけ」をしているわけではないことです。
暗号化はたしかに重要ですが、暗号化だけでは「相手が本物か」「途中で内容が変わっていないか」までは十分に説明できません。
TLS 1.3は、複数の暗号技術を組み合わせて、この3つの性質を実現しています。
2.1 認証:通信相手が本物かを確認する
まず1つ目は 認証 です。
認証というと、IDとパスワードを入力してログインする場面を思い浮かべるかもしれません。
しかしTLS 1.3でまず重要になるのは、利用者がサーバーにログインする前に、そもそも接続先のサーバーが本物かを確認すること です。
たとえば、銀行サイトにアクセスしたつもりでも、実際には見た目だけをまねた偽サイトにつながっていたら危険です。
その状態でIDやパスワードを入力してしまうと、通信経路が暗号化されていたとしても、情報は攻撃者が用意したサーバーに届いてしまいます。
つまり、通信内容を守る前に、まず「誰と通信しているのか」を確認する必要があります。
TLS 1.3では、サーバー側は通常、証明書を使って自分の身元を示します。
RFC 8446では、TLSの認証について、サーバー側は常に認証され、クライアント側の認証は任意であると説明されています。
認証には、RSA、ECDSA、EdDSAのような非対称暗号、またはPSKが使われるとされています。
参考: RFC 8446 - Section 1
ここで出てくる証明書は、ざっくり言えば「この公開鍵は、このドメインのサーバーに対応しています」と確認するための材料です。
ただし、証明書だけを受け取れば終わりではありません。
ブラウザは、証明書が信頼できる認証局からつながっているか、接続先のドメイン名と合っているか、有効期限が切れていないかなどを確認します。
さらにTLS 1.3のハンドシェイクでは、CertificateVerifyというメッセージを通じて、サーバーが証明書に対応する秘密鍵を持っていることも確認します。
この流れにより、ブラウザは「このサーバーは、証明書に対応する秘密鍵を持っているらしい」と確認できます。
💡 豆知識
証明書は「このWebサイトの内容が安全です」と保証するものではありません。
主に、「このドメインと公開鍵の対応関係を確認する」ためのものです。
そのため、鍵マークが表示されていても、サイト自体が詐欺目的で作られている可能性はあります。
2.2 機密性:通信内容を途中で読まれないようにする
2つ目は 機密性 です。
機密性は、通信内容を第三者に読まれないようにする性質です。
これは、TLSと聞いて多くの人が最初にイメージする「暗号化」に近い部分です。
たとえば、ネットショッピングで住所や支払い情報を入力するとします。
その内容が通信経路の途中でそのまま読めてしまうと、Wi-Fiアクセスポイントや中継経路上の攻撃者に情報を盗み見られる危険があります。
TLS 1.3では、通信路が確立された後に送られるデータは、基本的に両端の当事者だけが読めるように保護されます。
RFC 8446でも、通信路の確立後に送られるデータはエンドポイントにだけ見えることが機密性として説明されています。
ただし、TLSは送信するデータの長さまでは完全には隠さないことも明記されています。
参考: RFC 8446 - Section 1
ここで重要なのは、TLS 1.3では公開鍵暗号だけで通信内容を全部暗号化しているわけではない、という点です。
実際には、最初のハンドシェイクで通信に使う秘密の材料を準備し、その後のアプリケーションデータはAEADのような共通鍵系の仕組みで保護します。
この方が、大量の通信データを効率よく安全に扱えるからです。
次のコードは、TLS 1.3そのものを実装するものではありません。
あくまで「同じ平文でも、鍵とnonceを使って暗号文に変換される」という感覚をつかむための、AEADの小さなデモです。
注意: 実際のTLS 1.3では、鍵やnonceは仕様に従って安全に導出・管理されます。
下のコードは理解用の最小例であり、TLSを自作するためのコードではありません。
# 実行には cryptography ライブラリが必要です。
# pip install cryptography
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# AES-GCMで使う128ビット鍵を生成します。
# TLS 1.3では、このような鍵はハンドシェイク結果からHKDFなどで導出されます。
key = AESGCM.generate_key(bit_length=128)
aesgcm = AESGCM(key)
# nonceは暗号化ごとに使う値です。
# AES-GCMでは同じ鍵でnonceを再利用しないことが非常に重要です。
nonce = os.urandom(12)
# 暗号化したい通信内容の例です。
plaintext = b"amount=1000&to=example-shop"
# AADは暗号化はしないが、改ざんされていないか確認したい追加データです。
# TLS 1.3では、レコードヘッダがadditional dataとして扱われます。
aad = b"record-header-like-data"
# 平文を暗号化します。
# 出力には暗号文に加えて、改ざん検知に使う認証タグも含まれます。
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
print(ciphertext.hex())
このコードでは、plaintext の中身はそのままでは読めない ciphertext に変換されます。
TLS 1.3の実際のRecord Protocolでは、ここにさらにレコード番号、nonce導出、鍵更新などの細かい処理が加わります。
ただし、基本的なイメージとしては、通信内容をそのまま流さず、共有された鍵を使って読みにくい形に変換する と考えると分かりやすいです。
2.3 完全性:途中で書き換えられていないか確認する
3つ目は 完全性 です。
完全性は、データが途中で勝手に書き換えられていないことを確認する性質です。
これは「中身を読まれないようにする」とは別の話です。
たとえば、あなたがネットショッピングで「1,000円を支払う」というリクエストを送ったとします。
通信内容が第三者に読めなかったとしても、もし途中で「10,000円を支払う」に書き換えられてしまえば大問題です。
TLS 1.3では、Record ProtocolでAEADを使い、暗号化とあわせて改ざん検知を行います。
RFC 5116では、AEADは暗号化された平文の機密性に加えて、完全性と真正性を確認する仕組みだと説明されています。
参考: RFC 5116 - An Interface and Algorithms for Authenticated Encryption
また、RFC 8446では、TLS 1.3のRecord保護においてAEADを使い、復号・検証に失敗した場合はエラーとして扱うことが示されています。
参考: RFC 8446 - Section 5.2
先ほどのAEADのコードに、改ざん検知の例を加えると次のようになります。
# 実行には cryptography ライブラリが必要です。
# pip install cryptography
import os
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=128)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
aad = b"record-header-like-data"
plaintext = b"amount=1000&to=example-shop"
# 平文を暗号化し、同時に認証タグも作ります。
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
# 攻撃者が暗号文の一部を書き換えた、という状況を作ります。
tampered = bytearray(ciphertext)
tampered[0] ^= 0x01 # 先頭1ビットを反転させて、暗号文を壊します。
try:
# 改ざんされた暗号文を復号しようとします。
# 認証タグの検証に失敗すると、InvalidTagが発生します。
decrypted = aesgcm.decrypt(nonce, bytes(tampered), aad)
print(decrypted)
except InvalidTag:
print("改ざんを検出しました")
この例では、暗号文のたった1ビットを書き換えただけでも、復号時に検証エラーになります。
このように、AEADは「中身を隠す」だけでなく、「途中で変えられていないかを確認する」役割も持っています。
💡 豆知識
暗号化だけを見ると「読めなければ安全」と考えたくなります。
しかし実際の通信では、「読めないこと」と「書き換えられていないこと」の両方が必要です。
TLS 1.3でAEADが重要なのは、この2つをまとめて扱えるからです。
2.4 ハッシュだけでは「誰が作ったか」までは分からない
完全性の説明では、よくハッシュ関数が登場します。
ハッシュ関数は、データから固定長の値を作る仕組みで、少しでも入力が変わると出力が大きく変わる性質があります。
次のコードを見ると、1000 が 10000 に変わっただけで、SHA-256の結果が大きく変わることが分かります。
import hashlib
# メッセージからSHA-256のハッシュ値を計算する関数です。
def sha256_hex(message: str) -> str:
return hashlib.sha256(message.encode("utf-8")).hexdigest()
original = "amount=1000&to=example-shop"
tampered = "amount=10000&to=example-shop"
print("original:", sha256_hex(original))
print("tampered:", sha256_hex(tampered))
このように、ハッシュ関数は「データが変わったか」を確認する材料になります。
ただし、ハッシュ値だけでは「そのハッシュ値を誰が作ったのか」までは分かりません。
攻撃者がメッセージを書き換えたうえで、新しいハッシュ値も一緒に作り直してしまえば、単純なハッシュ比較だけでは見抜けない場合があります。
そこで、TLS 1.3のハンドシェイクでは、単なるハッシュだけでなく、鍵を使ったHMACや署名が重要になります。
RFC 8446では、Finishedメッセージがハンドシェイクと計算された鍵の認証に不可欠であり、verify_data は finished_key を使ったHMACで計算されると説明されています。
参考: RFC 8446 - Section 4.4.4 Finished
TLS 1.3そのものではありませんが、HMACのイメージを小さなPythonコードで見ると次のようになります。
import hashlib
import hmac
# 共有された秘密鍵の例です。
# TLS 1.3では、Finished用の鍵はHKDFを使って導出されます。
finished_key_like_secret = b"shared-finished-key"
# ハンドシェイクで交換されたメッセージ全体を、ここでは簡略化して表します。
handshake_transcript = b"ClientHello | ServerHello | Certificate | CertificateVerify"
# HMACを使って、ハンドシェイク内容に対する確認用の値を作ります。
tag = hmac.new(
finished_key_like_secret,
handshake_transcript,
hashlib.sha256,
).hexdigest()
# 攻撃者がハンドシェイク内容を書き換えた場合を考えます。
tampered_transcript = b"ClientHello | FakeServerHello | Certificate | CertificateVerify"
# 受信側も同じ鍵でHMACを計算し、値が一致するか確認します。
expected_tag = hmac.new(
finished_key_like_secret,
tampered_transcript,
hashlib.sha256,
).hexdigest()
# compare_digestは、タイミング差による情報漏えいを避けるために使います。
print("HMACが一致するか:", hmac.compare_digest(tag, expected_tag)) # False になる
この例では、ハンドシェイク内容が少しでも変わるとHMACの結果が一致しません。
TLS 1.3のFinishedメッセージは、ハンドシェイク全体が途中で都合よく書き換えられていないかを確認するために重要です。
2.5 3つの性質は別々ではなく、組み合わせて働く
ここまで、認証、機密性、完全性を分けて説明しました。
ただし、実際のTLS 1.3では、これらが独立してバラバラに動くわけではありません。
たとえば、サーバー証明書によって接続先を確認しても、その後の通信内容が暗号化されていなければ、入力内容を盗み見られる可能性があります。
逆に、通信内容が暗号化されていても、偽サーバーに対して暗号化して送っているだけなら意味がありません。
また、暗号化されていても、途中でデータを壊されたり差し替えられたりする可能性があるため、完全性の確認も必要です。
| 認証 | 機密性 | 完全性 | 何が起きるか |
|---|---|---|---|
| あり | あり | あり | 本物の相手と、安全な通信路でデータをやり取りできる |
| なし | あり | あり | 偽サーバーに対して安全に送ってしまう可能性がある |
| あり | なし | あり | 本物の相手でも、通信内容を盗み見られる可能性がある |
| あり | あり | なし | 中身は読まれにくくても、改ざんに気づけない可能性がある |
TLS 1.3は、この3つを組み合わせることで、ネットワーク上の攻撃者が通信を盗み見たり、書き換えたり、別の相手になりすましたりすることを難しくします。
もちろん、TLS 1.3があればサービス全体が完全に安全になるわけではありません。
フィッシング、端末のマルウェア感染、サーバー側の脆弱性、パスワードの使い回しなどは、TLSとは別に考える必要があります。
それでも、TLS 1.3が提供する認証・機密性・完全性は、WebサービスやAPI通信を安全に使うための重要な土台です。
2.6 この章のまとめ
この章では、TLS 1.3が守ろうとしている3つの性質を整理しました。
| 性質 | 役割 | TLS 1.3で関係する主な仕組み |
|---|---|---|
| 認証 | 相手が本物か確認する | 証明書、デジタル署名、CertificateVerify、PSK |
| 機密性 | 通信内容を第三者に読まれないようにする | 鍵共有、HKDF、AEAD |
| 完全性 | 通信内容が途中で書き換えられていないか確認する | AEAD、Finishedメッセージ、HMAC、Transcript-Hash |
認証は「誰と通信しているか」を確認するための性質です。
機密性は「通信内容を読まれないようにする」ための性質です。
完全性は「通信内容が途中で変えられていないか」を確認するための性質です。
TLS 1.3では、この3つを実現するために、証明書、デジタル署名、鍵共有、HKDF、AEAD、HMACなどが組み合わされています。
次の章では、これらの仕組みがTLS 1.3の中でどのような流れで使われるのかを、ハンドシェイク と レコード保護 という2つの大きな部分に分けて見ていきます。
3. TLS 1.3の全体像:ハンドシェイクとレコード保護
この章では、TLS 1.3を大きく ハンドシェイク と レコード保護 に分けて整理します。
2章で見た「認証・機密性・完全性」が、TLS 1.3のどの流れで実現されているのかを、全体図としてつかむことが目的です。
前の章では、TLS 1.3が守る性質として、認証、機密性、完全性を整理しました。
では、TLS 1.3はその3つをどのような流れで実現しているのでしょうか。
細かいメッセージや暗号計算に入る前に、まずはTLS 1.3を大きく2つに分けると見通しがよくなります。
| 大きな部分 | ざっくりした役割 | 身近なたとえ |
|---|---|---|
| ハンドシェイク | 相手確認、暗号方式の合意、鍵の準備を行う | 会話を始める前の本人確認と合鍵作り |
| レコード保護 | 実際の通信データを暗号化し、改ざんも検知する | 封筒に入れて封印して送る |
RFC 8446でも、TLSは主に2つの構成要素から成ると説明されています。
1つは、通信相手を認証し、暗号方式やパラメータを合意し、共有鍵素材を確立する Handshake Protocol です。
もう1つは、ハンドシェイクで決めた鍵やパラメータを使って、通信データを保護する Record Protocol です。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 1
かなり大ざっぱに見ると、TLS 1.3の流れは次のようになります。
この図のポイントは、TLS 1.3ではいきなりHTTPの中身を暗号化して送るのではなく、その前に「安全に通信するための準備」をしていることです。
つまり、TLS 1.3は次のような順番で動いていると考えると分かりやすいです。
- まず、通信相手や使う暗号方式を決める
- 次に、通信に使う鍵の材料を準備する
- その後、実際のHTTPデータを暗号化・改ざん検知しながら送る
3.1 ハンドシェイクは「安全に会話を始めるための準備」
ハンドシェイクは、TLS通信を始める前の準備です。
日常会話でたとえるなら、いきなり秘密の話を始めるのではなく、まず次のような確認をする段階です。
- あなたは本当に話したい相手なのか
- どの言語やルールで話すのか
- 2人だけが分かる合言葉や鍵をどう準備するのか
- ここまでのやり取りが途中で書き換えられていないか
TLS 1.3のハンドシェイクでも、これに近いことを行います。
RFC 8446では、ハンドシェイクによって、プロトコルバージョンの合意、暗号アルゴリズムの選択、必要に応じた相互認証、共有秘密鍵素材の確立が行われると説明されています。
参考: RFC 8446 - Section 2 Protocol Overview
TLS 1.3の代表的なフルハンドシェイクを、かなり簡略化すると次のようになります。
この図では、理解しやすくするために細部を省略しています。
たとえば、クライアント証明書を使う場合や、PSKを使った再開、0-RTTを使う場合などでは流れが変わります。
ただし、初学者向けには、まずこの流れを押さえると十分です。
| メッセージ | 主な役割 | 関係する暗号技術 |
|---|---|---|
| ClientHello | クライアントが対応する方式や鍵共有の材料を送る | key_share、対応暗号スイート、署名アルゴリズム候補 |
| ServerHello | サーバーが使う方式を選び、鍵共有の材料を返す | key_share、選択された暗号スイート |
| EncryptedExtensions | 追加の接続パラメータを送る | ServerHello後に導出された鍵による保護 |
| Certificate | サーバー証明書を送る | X.509証明書、公開鍵 |
| CertificateVerify | サーバーが秘密鍵を持つことを署名で示す | デジタル署名 |
| Finished | ハンドシェイク全体が改ざんされていないことを確認する | HMAC、Transcript-Hash |
| Application Data | 実際のHTTPデータを送る | AEAD、traffic key |
💡 豆知識
TLS 1.3のハンドシェイク図では、{Certificate}のように波かっこで書かれるメッセージと、[Application Data]のように角かっこで書かれるメッセージがあります。
RFC 8446の図では、波かっこはハンドシェイク用の鍵で保護されたメッセージ、角かっこはアプリケーションデータ用の鍵で保護されたメッセージを表しています。
つまり、TLS 1.3ではハンドシェイクの途中から、すでに一部のハンドシェイクメッセージも暗号化されます。
3.2 ハンドシェイクは3つの段階に分けると読みやすい
TLS 1.3のフルハンドシェイクは、仕様上は多くのメッセージで構成されています。
しかし、RFC 8446では、基本的なフルハンドシェイクを大きく次の3段階として説明しています。
参考: RFC 8446 - Section 2 Protocol Overview
| 段階 | 何をするか | 代表的なメッセージ | 初学者向けの見方 |
|---|---|---|---|
| Key Exchange | 共有鍵素材を確立し、暗号パラメータを選ぶ | ClientHello、ServerHello | 「どの方式で、どう鍵を準備するか」を決める |
| Server Parameters | 追加の接続パラメータを伝える | EncryptedExtensions、CertificateRequest | 「通信に必要な追加条件」を伝える |
| Authentication | サーバー認証、鍵確認、ハンドシェイク完全性確認を行う | Certificate、CertificateVerify、Finished | 「相手が本物か」「ここまで改ざんされていないか」を確認する |
この3段階で見ると、TLS 1.3のハンドシェイクは、かなり整理して理解できます。
最初の Key Exchange では、暗号方式や鍵共有の材料を決めます。
ここで得られた情報をもとに、以降のメッセージを保護するための鍵が導出されます。
次の Server Parameters では、サーバーが追加の設定情報を送ります。
TLS 1.3では、ServerHelloより後の多くのハンドシェイクメッセージが暗号化されるため、TLS 1.2以前よりもハンドシェイク中に見える情報が整理されています。
最後の Authentication では、サーバー証明書、署名、Finishedメッセージなどを使って、相手確認とハンドシェイクの確認を行います。
ここまで完了して、ようやく本格的にHTTPリクエストやHTTPレスポンスを送る準備が整います。
3.3 レコード保護は「実際の通信データを小分けにして守る」
ハンドシェイクが終わると、ブラウザとサーバーは実際のHTTPデータをやり取りします。
ただし、HTTPデータを1つの大きな塊としてそのまま送るわけではありません。
TLSでは、通信データを レコード という単位に分けて扱います。
RFC 8446では、Record Protocolはハンドシェイクで確立されたパラメータを使って通信を保護し、通信内容を一連のレコードに分割し、それぞれをtraffic keyで独立に保護すると説明されています。
参考: RFC 8446 - Section 1
イメージとしては、長い手紙を何枚かの封筒に分けて送るようなものです。
それぞれの封筒には封印があり、中身を読まれにくくしつつ、途中で書き換えられていないかも確認します。
TLS 1.3のRecord保護では、AEADが中心になります。
AEADは、暗号化と改ざん検知をまとめて扱う仕組みです。
RFC 8446では、TLS 1.3の暗号化されたレコードについて、AEADの入力として、鍵、nonce、平文、additional dataが使われると説明されています。
また、復号と検証に失敗した場合は、平文ではなくエラーとして扱われます。
参考: RFC 8446 - Section 5.2 Record Payload Protection
| AEADの入力 | TLS 1.3でのイメージ | 役割 |
|---|---|---|
| key | client_write_key / server_write_key | 暗号化・復号に使う鍵 |
| nonce | レコード番号とIVから作られる値 | 同じ鍵でもレコードごとに処理を変えるための値 |
| plaintext | HTTPデータやハンドシェイクデータなど | 守りたい中身 |
| additional data | レコードヘッダ | 暗号化はしないが、改ざんされていないか確認したい情報 |
ここで大切なのは、TLS 1.3では「暗号化」と「改ざん検知」が別々の後付け処理ではなく、AEADとしてまとまっていることです。
2章で見たように、暗号化だけでは、途中で書き換えられていないかを十分に確認できない場合があります。
TLS 1.3のRecord保護では、AEADを使うことで、通信内容を隠しながら、改ざんも検出できるようにしています。
3.4 ハンドシェイクとレコード保護の関係
ハンドシェイクとレコード保護は、別々の処理ではありますが、完全に独立しているわけではありません。
ハンドシェイクで決まった情報が、レコード保護に使われます。
逆に言えば、ハンドシェイクが正しく終わらなければ、その後の通信データも安全に守れません。
この流れを見ると、TLS 1.3の暗号技術は、1つずつ単独で働いているのではなく、前の処理の結果を次の処理が使うように組み合わされていることが分かります。
たとえば、鍵共有で得た秘密は、そのまま通信データの暗号化鍵として使うわけではありません。
TLS 1.3では、HKDFを使って、ハンドシェイク用、アプリケーションデータ用など、用途に応じた鍵を導出します。
また、Finishedメッセージでは、ここまでのハンドシェイク内容が改ざんされていないことを確認します。
これにより、「正しい相手と、正しい条件で、同じ鍵を共有できているか」を確認してから、アプリケーションデータのやり取りに進めます。
3.5 実際にPythonでTLS接続の結果だけ見てみる
TLS 1.3のハンドシェイクそのものを自作する必要はありません。
むしろ、暗号プロトコルを自作するのは避けるべきです。
ただし、Pythonの標準ライブラリ ssl を使うと、実際に接続したときに、どのTLSバージョンや暗号スイートが使われたかを確認できます。
Python公式ドキュメントでも、ssl モジュールはTLS/SSLによる暗号化とピア認証の機能を提供し、create_default_context() は一般的な用途向けの安全寄りの既定設定を持つ SSLContext を返すと説明されています。
参考: Python Documentation - ssl — TLS/SSL wrapper for socket objects
次のコードは、TLS 1.3の仕組みを実装するものではありません。
あくまで「ハンドシェイクの結果として、TLSバージョンや暗号スイートが決まる」ことを確認するための小さな例です。
import socket
import ssl
# 接続先の例です。
# 実行時点や環境によって、サーバー側の対応状況は変わる可能性があります。
hostname = "www.python.org"
port = 443
# 証明書検証やホスト名検証を含む、クライアント向けのデフォルトコンテキストを作成します。
# 実務では、検証を無効化しないことが重要です。
context = ssl.create_default_context()
# TLS 1.3だけを確認したい場合は、最小バージョンをTLS 1.3に設定します。
# 接続先やローカル環境がTLS 1.3に対応していない場合、接続に失敗します。
context.minimum_version = ssl.TLSVersion.TLSv1_3
# TCP接続を作成し、その上にTLSを重ねます。
with socket.create_connection((hostname, port), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as tls_sock:
# ネゴシエートされたTLSバージョンを表示します。
print("TLS version:", tls_sock.version())
# ネゴシエートされた暗号スイートを表示します。
# 例: ('TLS_AES_128_GCM_SHA256', 'TLSv1.3', 128)
print("Cipher:", tls_sock.cipher())
# サーバー証明書の一部を表示します。
# ここでは subject だけを見ていますが、実際には有効期限やSANなども検証対象になります。
cert = tls_sock.getpeercert()
print("Certificate subject:", cert.get("subject"))
このコードで確認できるのは、あくまで「接続の結果」です。
ClientHelloやServerHelloの中身、鍵共有の詳細、HKDFによる鍵導出、AEADによるRecord保護の内部処理までは表示していません。
それでも、TLS 1.3では接続時にバージョンや暗号スイートが合意され、その結果にもとづいて通信データが保護される、という流れをつかむ助けになります。
注意
学習目的であっても、証明書検証を無効化するコードを安易に使うのは避けた方がよいです。
たとえばCERT_NONEやcheck_hostname = Falseを使うと、接続先が本物かを確認できなくなり、TLSの重要な性質である認証を弱めてしまいます。
3.6 この章のまとめ
この章では、TLS 1.3の全体像を、ハンドシェイクとレコード保護に分けて整理しました。
| 観点 | ハンドシェイク | レコード保護 |
|---|---|---|
| 主な目的 | 通信前の準備 | 実際の通信データの保護 |
| 何を決めるか | TLSバージョン、暗号方式、鍵共有、認証 | レコードごとの暗号化・改ざん検知 |
| 関係する主な技術 | 証明書、デジタル署名、ECDHE、PSK、HKDF、HMAC | AEAD、traffic key、nonce、additional data |
| 身近なたとえ | 本人確認と合鍵作り | 封筒と封印 |
TLS 1.3では、まずハンドシェイクで安全に通信するための準備を行います。
その後、ハンドシェイクで得られた鍵やパラメータを使って、Record Protocolが実際のHTTPデータを保護します。
このように見ると、TLS 1.3は「暗号化機能」だけではなく、認証、鍵共有、鍵導出、改ざん検知を組み合わせたプロトコルであることが分かります。
次の章では、この中でも特に サーバーが本物かをどう確認するのか に注目し、証明書とデジタル署名の役割を整理します。
4. サーバーが本物かをどう確認するのか
この章では、TLS 1.3のハンドシェイクの中でも、サーバー認証 に注目します。
キーワードは、証明書、認証局、証明書チェーン、デジタル署名、CertificateVerify です。
前の章では、TLS 1.3を大きくハンドシェイクとレコード保護に分けました。
ハンドシェイクでは、通信に使う鍵の準備だけでなく、接続先のサーバーが本物かどうか も確認します。
この確認をしないまま暗号化通信を始めてしまうと、かなり危険です。
たとえば、あなたが銀行サイトにアクセスしたつもりだったとします。
通信内容がきれいに暗号化されていたとしても、接続先が攻撃者の用意した偽サーバーだったらどうでしょうか。
その場合、通信は「安全に偽サーバーへ送られている」だけになってしまいます。
つまり、暗号化の前に、まず 誰と通信しているのか を確認する必要があります。
TLS 1.3では、この確認に証明書とデジタル署名が関係します。
なお、本記事では読みやすさを優先して「サーバーが本物か」と表現しています。
ただし、技術的には「利用者がアクセスしようとしているドメイン名に対して、有効な証明書を提示でき、その証明書に対応する秘密鍵を持っていることを確認する」という意味です。
そのサイトの内容、運営者の信頼性、販売している商品やサービスの安全性まで自動的に保証するわけではありません。
4.1 証明書は「ドメイン名と公開鍵の対応」を示す材料
TLSで使われる証明書は、ざっくり言うと Webサーバーの身分証明書 のようなものです。
ただし、ここでいう身分証明書は、単に「このサイトは安全です」と書かれたラベルではありません。
重要なのは、証明書が ドメイン名と公開鍵の対応関係 を示すための材料になっていることです。
RFC 5280では、公開鍵証明書は公開鍵の値を主体、つまり人やシステムなどに結びつけるデータ構造であり、その結びつきは信頼されたCAが証明書にデジタル署名することで主張されると説明されています。
参考: RFC 5280 - Internet X.509 Public Key Infrastructure Certificate and CRL Profile
Webサーバー証明書には、代表的には次のような情報が含まれます。
| 証明書に含まれる主な情報 | やさしく言うと | 確認したいこと |
|---|---|---|
| Subject / Subject Alternative Name | 証明書の対象になる名前 | 接続先ドメインと対応しているか |
| Public Key | サーバーに対応する公開鍵 | 署名検証などに使う鍵はどれか |
| Issuer | 証明書を発行した認証局 | 誰がこの証明書を発行したか |
| Validity | 有効期間 | 期限切れではないか |
| Signature | 発行者による署名 | 証明書が改ざんされていないか |
| Key Usage / Extended Key Usage | 鍵の用途 | TLSサーバー認証に使ってよい証明書か |
ここで大切なのは、証明書そのものが魔法のように安全性を生み出すわけではないことです。
ブラウザやOSは、証明書の内容を確認し、信頼できる認証局からつながる証明書かどうかを検証します。
4.2 認証局と証明書チェーン
では、ブラウザはなぜWebサーバーの証明書を信じられるのでしょうか。
ここで登場するのが、認証局 です。
認証局は Certificate Authority、略して CA と呼ばれます。
かなり身近な例でいうと、認証局は「身分証明書を発行する機関」に近いです。
ただし、TLSの世界では、認証局が直接すべてのWebサイトをブラウザに登録しているわけではありません。
多くの場合、次のように証明書のつながり、つまり 証明書チェーン を使って確認します。
ブラウザやOSには、あらかじめ信頼するルートCAの情報が入っています。
Webサーバーが提示した証明書が、その信頼済みルートCAまで正しくつながっていれば、「この証明書は信頼できる経路から来ている」と判断できます。
RFC 5280では、証明書パス検証は、SubjectまたはSubject Alternative Nameと公開鍵の結びつきを検証する処理として説明されています。
また、証明書パスでは、各証明書のSubjectとIssuerの関係、有効期間、信頼アンカーなどが検証されます。
参考: RFC 5280 - Section 6 Certification Path Validation
TLS 1.3のCertificateメッセージでは、エンドポイントの証明書チェーンを相手に伝えます。
RFC 8446では、Certificateメッセージがエンドポイントの証明書チェーンを伝えるものであり、X.509証明書を使う場合には、送信者自身の証明書が最初に置かれ、続く証明書は直前の証明書を直接証明するものが望ましいと説明されています。
参考: RFC 8446 - Section 4.4.2 Certificate
このあたりは少し複雑ですが、初学者向けには次のように考えると分かりやすいです。
| 見たいこと | 具体的な確認 |
|---|---|
| その証明書は誰が発行したか | Issuerを確認する |
| その発行者は信頼できるか | 中間CA、ルートCAまでチェーンをたどる |
| 証明書は期限切れではないか | 有効期間を確認する |
| 接続先ドメインと合っているか | Subject Alternative Nameなどを確認する |
| 鍵の用途は合っているか | Key Usage / Extended Key Usageを確認する |
💡 豆知識
証明書チェーンは、紹介状の連鎖に少し似ています。
いきなり知らない人から「私は信頼できます」と言われても困りますが、信頼している機関から順番に紹介状がつながっていれば、相手を信頼する材料になります。
TLSでは、この紹介状にあたるものがデジタル署名で検証されます。
4.3 証明書だけではなく「秘密鍵を持っていること」も確認する
ここまでを見ると、「証明書が正しければ、それだけでサーバー確認は終わり」と思うかもしれません。
しかし、TLS 1.3ではもう一段大切な確認があります。
それが CertificateVerify です。
証明書には公開鍵が含まれています。
しかし、通信相手がその公開鍵に対応する 秘密鍵 を本当に持っているとは限りません。
そこでTLS 1.3では、サーバーがハンドシェイク全体に対してデジタル署名を作り、クライアントが証明書内の公開鍵でその署名を検証します。
RFC 8446では、CertificateVerifyはCertificateメッセージ内の公開鍵に対応する秘密鍵を使って、ハンドシェイク全体に対して署名するメッセージだと説明されています。
参考: RFC 8446 - Section 4.4.3 Certificate Verify
流れを簡略化すると、次のようになります。
この流れで確認していることを分けると、次のようになります。
| 確認対象 | 何を確認しているか | 主に使うもの |
|---|---|---|
| 証明書チェーン | その公開鍵とドメイン名の対応を信頼できるか | CAの署名、信頼済みルートCA |
| ドメイン名 | 今アクセスしている名前と証明書の名前が合うか | Subject Alternative Nameなど |
| 秘密鍵の保有 | 相手が証明書の公開鍵に対応する秘密鍵を持っているか | CertificateVerifyのデジタル署名 |
| ハンドシェイクの結びつき | 署名が今回のハンドシェイク内容に対するものか | Transcript-Hash |
この仕組みによって、TLS 1.3では「証明書を持ってきただけの相手」ではなく、「証明書に対応する秘密鍵を持ち、今回のハンドシェイクに参加している相手」を確認できます。
たとえるなら、身分証のコピーを見せるだけでなく、その場で本人しか書けない確認用サインをしてもらうようなイメージです。
4.4 デジタル署名は「中身を隠す」ものではない
CertificateVerifyではデジタル署名が使われます。
ここで、デジタル署名の役割を少し整理しておきます。
デジタル署名は、秘密鍵で署名を作り、公開鍵で検証する仕組みです。
主な役割は、次のように整理できます。
| 役割 | 説明 | TLS 1.3での例 |
|---|---|---|
| 秘密鍵の保有確認 | 対応する秘密鍵を持つ相手だけが署名できる | CertificateVerify |
| 改ざん検知 | 署名対象のデータが変わると検証に失敗する | ハンドシェイク内容への署名 |
| 本人性の確認 | 公開鍵と証明書の対応が正しければ相手確認につながる | サーバー認証 |
ここで注意したいのは、デジタル署名は データを隠すための技術ではない という点です。
署名は「このデータは、この秘密鍵を持つ相手によって作られたらしい」「途中で変わっていないらしい」と確認するためのものです。
中身を読めなくするには、別途暗号化が必要です。
💡 豆知識
「署名」と聞くと、紙に名前を書くサインを想像しがちです。
しかしデジタル署名は、見た目のサイン画像ではありません。
データと秘密鍵から作られる、数学的に検証できる値です。
そのため、署名画像を貼り付けるだけのものとは意味が大きく異なります。
4.5 Pythonで証明書の中身を少し見てみる
TLS 1.3の証明書検証や署名検証を自作する必要はありません。
実務では、Pythonの ssl モジュール、OpenSSL、ブラウザ、OS、Webサーバーなど、十分に使われている実装に任せるべきです。
ただし、学習目的で「証明書にはどのような情報が入っているのか」を見ることは役に立ちます。
Pythonの ssl モジュールは、TLS/SSLによる暗号化とピア認証の機能を提供します。
また、create_default_context() は、証明書検証やホスト名検証を含む、一般的なクライアント用途向けの設定を持つ SSLContext を作成します。
参考: Python Documentation - ssl — TLS/SSL wrapper for socket objects
次のコードは、指定したサーバーにTLS接続し、サーバー証明書の一部を表示する例です。
注意: このコードは証明書検証の仕組みを自作するものではありません。
ssl.create_default_context()による検証を有効にしたまま、検証済み接続から証明書情報を観察するための例です。
import socket
import ssl
from pprint import pprint
# 接続先の例です。
# 実行時点やネットワーク環境によって、結果は変わる可能性があります。
hostname = "www.python.org"
port = 443
# 証明書検証とホスト名検証を含む、クライアント向けの標準的なTLS設定を作ります。
# 学習目的でも、検証を無効化しないことが重要です。
context = ssl.create_default_context()
# ここではTLS 1.3以上を要求します。
# 接続先またはローカル環境がTLS 1.3に対応していない場合は失敗します。
context.minimum_version = ssl.TLSVersion.TLSv1_3
# TCP接続を作成し、その上にTLSを重ねます。
with socket.create_connection((hostname, port), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as tls_sock:
# 実際にネゴシエートされたTLSバージョンを表示します。
print("TLS version:", tls_sock.version())
# 実際に選ばれた暗号スイートを表示します。
print("Cipher:", tls_sock.cipher())
# 検証済み接続から、サーバー証明書の情報を取得します。
cert = tls_sock.getpeercert()
# 証明書の対象を表示します。
# 実際のホスト名確認では、Subject Alternative Nameなどが重要になります。
print("Subject:")
pprint(cert.get("subject"))
# 証明書の発行者を表示します。
print("Issuer:")
pprint(cert.get("issuer"))
# 証明書の有効期間を表示します。
print("Validity:")
print(" notBefore:", cert.get("notBefore"))
print(" notAfter :", cert.get("notAfter"))
# Subject Alternative Nameを表示します。
# 接続先ホスト名と証明書の名前が対応しているかを見るうえで重要です。
print("Subject Alternative Name:")
pprint(cert.get("subjectAltName"))
このコードを実行すると、証明書のSubject、Issuer、有効期間、Subject Alternative Nameなどを確認できます。
ここで大切なのは、証明書の中身を「見る」ことと、証明書を「正しく検証する」ことは別だという点です。
証明書検証では、証明書チェーン、有効期間、ホスト名、用途、失効情報など、複数の観点が関係します。
そのため、実務では証明書検証を自作したり、検証を無効化したりせず、標準ライブラリや実績のあるTLS実装に任せることが重要です。
4.6 証明書が有効でも、サイト全体が安全とは限らない
最後に、証明書についてよくある誤解を整理します。
証明書が有効で、ブラウザに鍵マークが表示されていると、「このサイトは安全だ」と感じるかもしれません。
しかし、証明書が示しているのは、主に ドメイン名と公開鍵の対応関係 です。
つまり、証明書が有効であることと、Webサイトの内容や運営が安全であることは別問題です。
| 証明書で確認しやすいこと | 証明書だけでは分からないこと |
|---|---|
| 接続先ドメインと証明書の対応 | そのサイトが詐欺目的ではないこと |
| 証明書チェーンが信頼済みCAにつながること | 商品や情報の内容が正しいこと |
| 証明書の有効期限 | サーバー内部が侵害されていないこと |
| 証明書に対応する秘密鍵をサーバーが持つこと | 利用者が入力する情報の扱いが適切であること |
たとえば、攻撃者がそれらしいドメインを取得し、そのドメインに対して正規の証明書を発行してもらうことはあり得ます。
この場合、TLSとしては「そのドメインに対する安全な通信路」は作れます。
しかし、そのサイトが本当に利用者の意図したサービスかどうかは、URL、サービス名、入力内容、リンク元なども含めて確認する必要があります。
💡 豆知識
証明書は「このサイトは絶対に安全です」というお墨付きではありません。
主に「この公開鍵はこのドメインに対応している」と確認するための材料です。
そのため、証明書が有効なフィッシングサイトも存在し得ます。
4.7 この章のまとめ
この章では、TLS 1.3でサーバーが本物かを確認する仕組みを整理しました。
重要なポイントは、次のとおりです。
- 証明書は、ドメイン名と公開鍵の対応関係を確認するための材料になる
- ブラウザやOSは、信頼済みルートCAまでつながる証明書チェーンを検証する
- TLS 1.3ではCertificateメッセージで証明書チェーンを送り、CertificateVerifyで秘密鍵の保有を署名によって示す
- デジタル署名は、データを隠す技術ではなく、秘密鍵の保有や改ざん有無を確認するための技術である
- 証明書が有効でも、Webサイトの内容や運営が安全であることまでは保証されない
ここまでで、TLS 1.3の「相手を確認する」部分が見えてきました。
次の章では、通信相手を確認した後に、通信用の鍵をどうやって準備するのか を見ていきます。
共通鍵をそのまま送るのではなく、ECDHEなどを使って通信ごとの秘密を作る流れを整理します。
5. 通信用の鍵はどうやって準備されるのか
この章では、TLS 1.3の「鍵共有」と「Forward Secrecy」を整理します。
ざっくり言うと、TLS 1.3では通信に使う共通鍵をそのまま送るのではなく、クライアントとサーバーがそれぞれ材料を出し合って、同じ秘密を別々に計算します。
前の章では、証明書とデジタル署名によって「接続先のサーバーが本物か」を確認する流れを見ました。
ただし、相手が本物だと分かっただけでは、まだ安全な通信は始められません。
実際の通信内容を守るには、ブラウザとサーバーの間で 通信用の鍵 を準備する必要があります。
ここで出てくる疑問が、次のものです。
共通鍵暗号で通信を守るなら、その共通鍵をどうやって相手に渡すのか?
もし共通鍵をそのままネットワークに流してしまうと、途中で盗み見られたときに意味がありません。
せっかく金庫を用意しても、その鍵を誰でも見える場所に置いてしまうようなものです。
TLS 1.3では、この問題を解決するために、主に (EC)DHE や PSK を使って鍵の材料を準備します。
RFC 8446では、ClientHelloに key_share や pre_shared_key を含めることができ、ServerHelloが選択したパラメータを示し、ClientHelloとServerHelloの組み合わせによって共有鍵が決まる、と説明されています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3
5.1 まずは用語を整理する
この章で出てくる用語を、最初にざっくり整理します。
| 用語 | やさしく言うと | TLS 1.3での位置づけ |
|---|---|---|
| 共通鍵 | 同じ鍵で暗号化・復号するための鍵 | 実際の通信データを高速に守るために使う |
| 鍵共有 | 通信相手と安全に鍵の材料を作る仕組み | ハンドシェイクのKey Exchangeで行う |
| DHE | Diffie-Hellman Ephemeralの略 | 一時的な鍵を使う鍵共有 |
| ECDHE | 楕円曲線を使うDHE | TLS 1.3でよく使われる鍵共有の考え方 |
| PSK | Pre-Shared Keyの略 | 以前の接続などで得た共有鍵を使って再接続を効率化する仕組み |
| Forward Secrecy | 長期鍵が後で漏れても、過去の通信内容が復号されにくい性質 | TLS 1.3で重要な改善点の一つ |
ここで特に大切なのは、共通鍵をそのまま送らない という点です。
TLS 1.3では、クライアントとサーバーがそれぞれ一時的な秘密情報を持ち、その一部に対応する公開情報を交換します。
そして、相手から受け取った公開情報と自分の秘密情報を使って、同じ共有秘密を計算します。
この共有秘密は、まだそのまま通信鍵として使われるわけではありません。
実際には、次の章で扱うHKDFによって、ハンドシェイク用、アプリケーションデータ用など、用途別の鍵に展開されます。
5.2 ECDHEは「通信ごとの秘密」を作るための仕組み
TLS 1.3のフルハンドシェイクでは、典型的にはECDHEによって通信ごとの共有秘密を作ります。
ECDHEは、Elliptic Curve Diffie-Hellman Ephemeralの略です。
少し長い名前ですが、分解すると次のように見えます。
| 部分 | 意味 |
|---|---|
| Elliptic Curve | 楕円曲線を使う |
| Diffie-Hellman | 2者が共通の秘密を作る鍵共有方式 |
| Ephemeral | 一時的な、使い捨ての |
初学者向けには、まず Ephemeral(一時的) という部分が重要です。
ここで使う鍵は、サーバー証明書に入っている長期的な公開鍵とは別物です。
通信ごとに一時的な鍵ペアを作り、その接続で使う共有秘密を作ったら、不要になった秘密情報は破棄する、という考え方です。
身近なたとえで言うと、家の合鍵をずっと使い回すのではなく、その場限りの入室コードを毎回発行するイメージに近いです。
仮に後から長期的な鍵に問題が起きても、過去に使った入室コードまでそのまま分かるわけではありません。
TLS 1.3のフルハンドシェイクを簡略化すると、鍵共有は次のような流れになります。
この図で大切なのは、通信経路を見ている第三者には、公開情報しか見えないことです。
クライアントとサーバーは、それぞれ自分の秘密情報を外に出さずに、同じ共有秘密を計算します。
RFC 8446でも、(EC)DHEが使われる場合、ServerHelloにはサーバーの一時的なDiffie-Hellman shareが含まれ、そのshareはクライアントが提示したグループのいずれかと同じグループである必要がある、と説明されています。
参考: RFC 8446 Section 2 - Protocol Overview
💡 豆知識
ECDHEのEは「Elliptic Curve」ですが、最後のEは「Ephemeral」です。
同じEでも意味が違うので、最初は少し紛らわしいです。
特にTLS 1.3を読むときは、「一時的な鍵を使うからForward Secrecyにつながる」と押さえると理解しやすくなります。
5.3 小さなコードでECDHの雰囲気を確認する
ここでは、Pythonの cryptography ライブラリを使って、ECDH系の鍵共有の雰囲気だけを確認します。
以下のコードでは、X25519を使って、クライアント側とサーバー側が同じ共有秘密を計算できることを確認します。
X25519は、Curve25519を使う楕円曲線Diffie-Hellman鍵共有です。cryptography の公式ドキュメントでも、X25519は安全でない通信路上でも2者が共有秘密に合意できる鍵共有として説明されています。
参考: cryptography - X25519 key exchange
注意
これはTLS 1.3そのものの実装ではありません。
TLS 1.3の実際の鍵スケジュールは、ハンドシェイク履歴や複数段階のHKDFなどを含みます。
ここでは「相手の公開情報と自分の秘密情報から、同じ共有秘密を計算できる」という直感をつかむための最小例として扱います。
# 実行には cryptography ライブラリが必要です。
# pip install cryptography
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
# クライアント側の一時的な秘密鍵を生成します。
# TLS 1.3のECDHEでは、このような一時的な鍵を接続ごとに使うイメージです。
client_private_key = X25519PrivateKey.generate()
client_public_key = client_private_key.public_key()
# サーバー側の一時的な秘密鍵を生成します。
server_private_key = X25519PrivateKey.generate()
server_public_key = server_private_key.public_key()
# クライアントは、自分の秘密鍵とサーバーの公開鍵から共有秘密を計算します。
client_shared_secret = client_private_key.exchange(server_public_key)
# サーバーは、自分の秘密鍵とクライアントの公開鍵から共有秘密を計算します。
server_shared_secret = server_private_key.exchange(client_public_key)
# 両者が同じ共有秘密を得られているか確認します。
print(client_shared_secret == server_shared_secret) # True になる
# 共有秘密は、そのまま通信鍵として使うのではなく、通常はKDFに通します。
# ここではHKDFを使って、32バイトのデモ用鍵を導出します。
# TLS 1.3の実際のHKDF利用は、これよりも段階的で複雑です。
derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b"demo key for learning",
).derive(client_shared_secret)
print(derived_key.hex())
このコードで確認したいことは、次の2点です。
- クライアントとサーバーは、秘密鍵そのものを相手に送っていない
- それでも、相手の公開鍵と自分の秘密鍵を使うことで、同じ共有秘密を計算できる
この「同じ秘密を作れるが、秘密そのものは通信路に流さない」という考え方が、TLS 1.3の鍵共有を理解する入口になります。
また、コードの最後ではHKDFでデモ用の鍵を導出しています。
RFC 5869では、HKDFは入力となる鍵材料から、1つ以上の暗号学的に強い秘密鍵を導出するためのHMACベースのKDFとして説明されています。
参考: RFC 5869 - HMAC-based Extract-and-Expand Key Derivation Function
HKDFについては、次の章で改めて整理します。
5.4 PSKは「前に作った秘密」を再接続に使う仕組み
TLS 1.3では、毎回フルハンドシェイクを行うだけでなく、以前の接続で得た情報を使って再接続を効率化する仕組みもあります。
ここで出てくるのが PSK です。
PSKはPre-Shared Keyの略で、日本語では事前共有鍵と呼ばれます。
「事前に共有された秘密を使って、次回以降の接続を少し効率よく始める」と考えると分かりやすいです。
TLS 1.3では、PSKは外部で事前に設定される場合もありますが、よく出てくるのは セッション再開 の文脈です。
RFC 8446では、ハンドシェイク完了後にサーバーがクライアントへPSK identityを送り、クライアントが将来のハンドシェイクでそのPSKを使うことができる、と説明されています。
参考: RFC 8446 Section 2.2 - Resumption and Pre-Shared Key
ただし、PSKには注意点もあります。
TLS 1.3では、PSKを単独で使う方式と、PSKに(EC)DHEを組み合わせる方式があります。
RFC 8446では、PSKは(EC)DHEと組み合わせることでForward Secrecyを提供できる一方、PSK単独で使うとアプリケーションデータについてForward Secrecyを失う、と説明されています。
参考: RFC 8446 Section 2.2 - Resumption and Pre-Shared Key
整理すると、次のようになります。
| 鍵確立の形 | ざっくりした説明 | Forward Secrecy |
|---|---|---|
| 証明書 + (EC)DHE | 初回接続などで使う基本的な形 | あり |
| PSK + (EC)DHE | 再接続を効率化しつつ、一時的な鍵共有も行う | あり |
| PSKのみ | 事前共有鍵だけで鍵を準備する | なし |
この表から分かるように、PSKは「速くなるから常に安全」という単純なものではありません。
どのモードで使うかによって、安全性の性質が変わります。
💡 豆知識
PSKは「会員証を見せると受付が早くなる」ようなイメージです。
ただし、会員証だけで全部を済ませるのか、その場で新しい一時コードも作るのかによって、後から問題が起きたときの影響範囲が変わります。
5.5 Forward Secrecyは「あとから漏れても過去を守りやすくする」考え方
TLS 1.3を学ぶときに大切なキーワードが Forward Secrecy です。
日本語では「前方秘匿性」や「将来にわたる秘匿性」と訳されることがあります。
言葉だけ見ると少し難しいですが、考え方は次のようなものです。
もし将来、サーバーの長期的な秘密鍵が漏れてしまっても、過去に記録された通信内容まで一気に復号されないようにする。
たとえば、攻撃者が今日の通信をすべて保存していたとします。
その時点では復号できなかったとしても、数か月後にサーバーの秘密鍵が漏れたら、過去の通信まで読めてしまう設計だと困ります。
ECDHEのように通信ごとの一時的な鍵共有を行い、その一時的な秘密をきちんと破棄していれば、後から長期鍵が漏れても、過去の通信鍵を復元しにくくなります。
RFC 8446では、TLS 1.3で静的RSAと静的Diffie-Hellmanの暗号スイートが削除され、公開鍵ベースの鍵交換はForward Secrecyを提供するようになったと説明されています。
参考: RFC 8446 Section 1.2 - Major Differences from TLS 1.2
もちろん、Forward Secrecyがあるから何をしても安全というわけではありません。
セッション鍵そのものが漏れたり、端末やサーバーが通信中に侵害されたりすれば、その通信は守れません。
あくまで、長期鍵が後から漏れた場合に、過去の通信までまとめて危険になりにくくする性質 と考えるのが自然です。
5.6 この章のまとめ
この章では、TLS 1.3で通信用の鍵をどう準備するのかを整理しました。
ポイントは次のとおりです。
- TLS 1.3では、通信データ本体を守るために共通鍵系の暗号を使う
- ただし、その共通鍵をそのまま通信路に流すわけではない
- ECDHEでは、クライアントとサーバーが一時的な鍵ペアを使い、同じ共有秘密を別々に計算する
- PSKは、以前の接続などで得た秘密を使って再接続を効率化する仕組みである
- PSKは(EC)DHEと組み合わせるとForward Secrecyを提供できるが、PSK単独ではその性質を失う
- TLS 1.3では静的RSAや静的Diffie-Hellmanの暗号スイートが削除され、公開鍵ベースの鍵交換はForward Secrecyを提供する形になっている
ここまでで、「相手を確認する」「通信ごとの秘密を作る」という流れが見えてきました。
ただし、ECDHEで得られた共有秘密を、そのまま全部の用途に使うわけではありません。
次の章では、TLS 1.3の中でHKDFがどのように用途別の鍵を作っているのかを見ていきます。
6. HKDFは何をしているのか
この章では、5章で準備した「共有秘密」を、TLS 1.3がどのように用途別の鍵へ分けているのかを整理します。
キーワードは HKDF と Key Schedule です。
前の章では、TLS 1.3で通信用の鍵の材料をどう準備するのかを見ました。
ECDHEでは、クライアントとサーバーがネットワーク上に秘密そのものを流さずに、同じ共有秘密をそれぞれ計算します。
ただし、ここで得られた共有秘密を、そのまま全部の用途に使うわけではありません。
TLS 1.3の通信では、ハンドシェイク中のメッセージを守る鍵、アプリケーションデータを守る鍵、セッション再開に使う秘密など、用途がいくつもあります。
もし1つの秘密をあちこちで雑に使い回してしまうと、ある用途で問題が起きたときに、別の用途へ影響が広がりやすくなります。
そこでTLS 1.3では、HKDF という仕組みを使って、元になる秘密から用途別の秘密や鍵を段階的に導出します。
RFC 8446では、TLS 1.3の鍵導出処理はHKDF-ExtractとHKDF-Expandを使うと説明されています。
また、鍵導出には入力秘密だけでなく、ハンドシェイクの履歴である handshake transcript も取り込まれます。
参考: RFC 8446 Section 7.1 - Key Schedule
6.1 HKDFは「鍵を用途別に整える」ための仕組み
HKDFは、HMAC-based Extract-and-Expand Key Derivation Functionの略です。
日本語にすると、「HMACを使って鍵材料を取り出し、必要な長さの鍵へ広げる仕組み」といった意味になります。
いきなり名前だけ見ると難しいですが、役割はかなり実用的です。
たとえば、料理で考えてみます。
前の章で得たECDHE共有秘密を「大きな食材」だとします。
その食材を、そのままスープにも炒め物にもソースにも全部同じ形で使うのではなく、料理ごとに切り分けたり、下味をつけたりして使いやすくします。
HKDFもそれに近いです。
1つの秘密から、用途ごとに別々の鍵材料を作ります。
TLS 1.3では、この考え方によって、ハンドシェイク用、アプリケーションデータ用、再開用などの秘密を分けて扱います。
RFC 5869では、HKDFは extract-then-expand、つまり「取り出してから広げる」という考え方に従うと説明されています。
Extract段階では入力鍵材料から固定長の擬似ランダム鍵を作り、Expand段階ではそれを必要な長さの鍵材料へ展開します。
参考: RFC 5869 - HMAC-based Extract-and-Expand Key Derivation Function
| 段階 | ざっくりした役割 | 料理でたとえると |
|---|---|---|
| Extract | 元の秘密から扱いやすい強い鍵材料を取り出す | 食材を下ごしらえする |
| Expand | 用途に応じて必要な長さ・目的の鍵を作る | スープ用、炒め物用、ソース用に分ける |
💡 豆知識
HKDFは「暗号化そのもの」を行う仕組みではありません。
データを読めない形にするのは、後の章で扱うAEADなどの役割です。
HKDFは、そのAEADなどが使う鍵を安全に準備するための裏方です。
6.2 なぜ共有秘密をそのまま使わないのか
ECDHEで得られた共有秘密は、とても重要な材料です。
しかし、その共有秘密をそのまま通信の暗号化鍵として使うのは、設計としてあまりよくありません。
理由はいくつかあります。
| 理由 | 説明 |
|---|---|
| 用途を分けたい | ハンドシェイク用、アプリケーションデータ用、再開用などで鍵を分けたい |
| 必要な長さが違う | AES-128なら16バイト、AES-256なら32バイトなど、方式によって鍵長が違う |
| 文脈を結びつけたい | そのハンドシェイクで合意した内容と鍵を結びつけたい |
| 使い回しを避けたい | 同じ秘密からでも用途ごとに異なる値を作りたい |
特にTLS 1.3では、ハンドシェイクの履歴が鍵導出に関係します。
この履歴には、ClientHelloやServerHelloなど、通信の最初にやり取りされたメッセージが含まれます。
これにより、同じPSKを使う場合でも、ハンドシェイクごとに異なるtraffic secretが得られます。
RFC 8446では、handshake transcriptにHelloメッセージのランダム値が含まれるため、同じ入力秘密を使っても各ハンドシェイクで異なるtraffic secretになると説明されています。
参考: RFC 8446 Section 7.1 - Key Schedule
この図のポイントは、HKDFが「1つの秘密をそのまま全用途に使い回す」のではなく、通信の文脈に合わせて用途別の鍵材料へ変換している ことです。
6.3 TLS 1.3のKey Scheduleをざっくり見る
TLS 1.3では、鍵導出の流れを Key Schedule として整理しています。
Key Scheduleは、PSKや(EC)DHE共有秘密を入力として、段階的に複数のsecretを作る流れです。
RFC 8446では、TLS 1.3の入力秘密として、PSKと(EC)DHE共有秘密が挙げられています。
そこからEarly Secret、Handshake Secret、Master Secretが段階的に作られ、さらにtraffic secretやresumption master secretが導出されます。
参考: RFC 8446 Section 7.1 - Key Schedule
厳密な図はRFC 8446に掲載されていますが、初学者向けにかなり簡略化すると、次のようになります。
それぞれのsecretを、ざっくり整理すると次のようになります。
| secret | ざっくりした役割 |
|---|---|
| Early Secret | PSKや0-RTTに関係する早い段階の秘密 |
| Handshake Secret | ハンドシェイク中のメッセージ保護に関係する秘密 |
| Master Secret | アプリケーションデータや再開用の秘密を導く土台 |
| client_handshake_traffic_secret | クライアント側のハンドシェイク通信に関係する秘密 |
| server_handshake_traffic_secret | サーバー側のハンドシェイク通信に関係する秘密 |
| client_application_traffic_secret | クライアントから送るアプリケーションデータ保護に関係する秘密 |
| server_application_traffic_secret | サーバーから送るアプリケーションデータ保護に関係する秘密 |
| resumption_master_secret | 次回のセッション再開に関係する秘密 |
ここで注目したいのは、クライアント用とサーバー用のsecretが分かれていることです。
同じ通信でも、「クライアントからサーバーへ送るデータ」と「サーバーからクライアントへ返すデータ」では、別の鍵材料が使われます。
これは、通信の方向ごとに鍵を分けることで、役割を明確にし、影響範囲を小さくするためです。
💡 豆知識
TLS 1.3では、同じ接続の中でも「クライアントが送る用」と「サーバーが送る用」で鍵が分かれます。
たとえるなら、同じ部屋で会話していても、入口用の鍵と出口用の鍵を分けて管理するようなイメージです。
少し細かく見えますが、このような分離が安全な設計につながります。
6.4 HKDFの雰囲気をPythonで見る
ここでは、HKDFの雰囲気をPythonで確認してみます。
注意点として、これは TLS 1.3の完全な実装ではありません。
TLSの処理を自作するためのコードではなく、「同じ元の秘密から、ラベルや文脈を変えると別の鍵材料が導出される」ことを理解するための簡易例です。
import hashlib
import hmac
import math
import struct
def hkdf_extract(salt: bytes | None, ikm: bytes, hashmod=hashlib.sha256) -> bytes:
"""
HKDF-Extract の簡易実装です。
salt:
取り出し処理で使う値です。
None の場合は、ハッシュ長ぶんの 0 バイトを使います。
ikm:
Input Keying Material の略で、元になる鍵材料です。
TLS 1.3では、PSKやECDHE共有秘密などが関係します。
"""
if salt is None:
salt = b"\x00" * hashmod().digest_size
# HMAC(salt, ikm) によって、扱いやすい疑似ランダム鍵 PRK を作ります。
return hmac.new(salt, ikm, hashmod).digest()
def hkdf_expand(prk: bytes, info: bytes, length: int, hashmod=hashlib.sha256) -> bytes:
"""
HKDF-Expand の簡易実装です。
prk:
HKDF-Extract で得られた疑似ランダム鍵です。
info:
用途や文脈を表す追加情報です。
TLS 1.3ではラベルや transcript hash が関係します。
length:
必要な出力バイト数です。
"""
hash_len = hashmod().digest_size
n = math.ceil(length / hash_len)
if n > 255:
raise ValueError("length is too large for HKDF")
okm = b""
t = b""
# RFC 5869のHKDF-Expandと同じ考え方で、必要な長さまでHMAC出力を連結します。
for counter in range(1, n + 1):
t = hmac.new(prk, t + info + bytes([counter]), hashmod).digest()
okm += t
return okm[:length]
def hkdf_expand_label(
secret: bytes,
label: bytes,
context: bytes,
length: int,
hashmod=hashlib.sha256,
) -> bytes:
"""
TLS 1.3の HKDF-Expand-Label に近い形の簡易実装です。
TLS 1.3では、通常のHKDF-Expandに直接ラベルを渡すのではなく、
"tls13 " という接頭辞付きのラベル構造を作って渡します。
"""
full_label = b"tls13 " + label
# RFC 8446のHkdfLabel構造に近いバイト列を作ります。
# ここでは理解しやすさを優先し、最小限の形だけを示しています。
hkdf_label = (
struct.pack("!H", length)
+ bytes([len(full_label)]) + full_label
+ bytes([len(context)]) + context
)
return hkdf_expand(secret, hkdf_label, length, hashmod)
# ===== ここから、TLS 1.3の鍵導出の雰囲気を確認します =====
hash_len = hashlib.sha256().digest_size
zero = b"\x00" * hash_len
# 説明用の固定値です。
# 実際のTLSでは、ECDHE共有秘密は鍵共有の結果として得られます。
psk = b"\x00" * hash_len
ecdhe_shared = bytes.fromhex(
"00112233445566778899aabbccddeeff"
"102132435465768798a9babbdcddedef"
)
# 実際のTLS 1.3では、ClientHelloやServerHelloなどの
# ハンドシェイクメッセージ列から transcript hash を計算します。
transcript_hash = hashlib.sha256(b"ClientHello...ServerHello").digest()
# Early Secretを作ります。
early_secret = hkdf_extract(zero, psk)
# 次の段階へ進むための派生値を作ります。
derived_secret = hkdf_expand_label(early_secret, b"derived", b"", hash_len)
# ECDHE共有秘密を取り込んでHandshake Secretを作ります。
handshake_secret = hkdf_extract(derived_secret, ecdhe_shared)
# クライアント用とサーバー用で、異なるラベルを使います。
client_hs_secret = hkdf_expand_label(
handshake_secret,
b"c hs traffic",
transcript_hash,
hash_len,
)
server_hs_secret = hkdf_expand_label(
handshake_secret,
b"s hs traffic",
transcript_hash,
hash_len,
)
# 最終的にAEADなどで使う鍵長に合わせて、鍵を導出します。
# ここではAES-128をイメージして16バイトにしています。
client_key = hkdf_expand_label(client_hs_secret, b"key", b"", 16)
server_key = hkdf_expand_label(server_hs_secret, b"key", b"", 16)
print("client handshake key:", client_key.hex())
print("server handshake key:", server_key.hex())
print("same?", client_key == server_key)
実行すると、たとえば次のような出力になります。
client handshake key: 47002edd6c470849ce618586b4b63b94
server handshake key: ef31edaa8c234c6a44756dcee79dca48
same? False
ここで見てほしいのは、クライアント用とサーバー用で違う鍵が出ていることです。
元になるhandshake secretは同じでも、ラベルを変えることで異なる鍵材料が導出されています。
TLS 1.3では、このようにラベルやハンドシェイクの文脈を使いながら、用途ごとに鍵材料を分けています。
6.5 transcript hashは「この会話の流れ」を鍵に結びつける
TLS 1.3の鍵導出では、単にPSKやECDHE共有秘密だけを見るのではありません。
ClientHello、ServerHelloなどのハンドシェイクメッセージの履歴も、transcript hashとして鍵導出に関係します。
これは、鍵を「この接続で実際に合意した内容」と結びつけるために重要です。
たとえば、同じPSKを使って再接続する場合でも、毎回ClientHelloやServerHelloのランダム値は変わります。
そのため、handshake transcriptを取り込むことで、同じPSKを使っていても接続ごとに異なるtraffic secretを得やすくなります。
これを身近な例で言うと、「同じ会員証を使って入店しても、その日の受付番号や注文内容が違えば、発行される伝票番号も変わる」ようなものです。
PSKやECDHE共有秘密だけでなく、その接続で実際にやり取りした内容も鍵導出に含めることで、「この会話のための鍵」として結びつきます。
6.6 Key Scheduleは、暗号技術の分業を支える裏方
ここまで見ると、HKDFはかなり地味に見えるかもしれません。
証明書や鍵共有、AEADのように、目立つ処理ではないからです。
しかし、TLS 1.3全体を見ると、HKDFはかなり重要な裏方です。
| TLS 1.3の部品 | HKDFとの関係 |
|---|---|
| PSK | Early Secretの入力になる |
| ECDHE共有秘密 | Handshake Secretの入力になる |
| transcript hash | 鍵導出をハンドシェイク内容と結びつける |
| ハンドシェイク保護 | handshake traffic secretから鍵を導出する |
| アプリケーションデータ保護 | application traffic secretから鍵を導出する |
| セッション再開 | resumption master secretが次回接続に関係する |
HKDFがあることで、TLS 1.3は1つの秘密を雑に使い回すのではなく、目的に応じて鍵を分けることができます。
これは、暗号技術を安全に組み合わせる上でとても大切な設計です。
💡 豆知識
TLS 1.3の暗号スイート名には、たとえばTLS_AES_128_GCM_SHA256のようにハッシュ関数名が入っています。
ここでのSHA256は、AEADそのものだけでなく、HKDFやtranscript hashで使われるハッシュ関数にも関係します。
TLS 1.3の暗号スイートは、TLS 1.2以前と比べて「何を指定しているか」が整理されている点も特徴です。
6.7 この章のまとめ
この章では、TLS 1.3の鍵導出に使われるHKDFを整理しました。
ポイントは次のとおりです。
- ECDHEやPSKで得た秘密を、そのまま全用途に使い回すわけではない
- HKDFは、元になる秘密から用途別の鍵材料を導出する仕組みである
- RFC 5869では、HKDFはExtractとExpandの2段階で整理されている
- TLS 1.3では、HKDF-Extract、HKDF-Expand、HKDF-Expand-Labelなどを使ってKey Scheduleを構成する
- TLS 1.3の鍵導出では、PSKやECDHE共有秘密だけでなく、ハンドシェイクの履歴であるtranscript hashも関係する
- クライアント用、サーバー用、ハンドシェイク用、アプリケーションデータ用など、用途ごとに鍵材料が分かれる
ここまでで、TLS 1.3が「相手を確認し、鍵の材料を作り、その材料から用途別の鍵を作る」流れが見えてきました。
次の章では、HKDFで導出された鍵が、実際の通信データを守るためにどのように使われるのかを見ていきます。
ここで登場するのが、AEAD と Record Protocol です。
7. 実際の通信データはどう守られるのか
この章では、TLS 1.3の Record Protocol と AEAD を整理します。
6章までで準備した鍵が、実際のHTTPリクエストやレスポンスを守るときにどう使われるのかを見ていきます。
前の章では、HKDFによって用途別の鍵材料を作る流れを見ました。
ここまでで、TLS 1.3は次の準備を終えています。
- サーバーが本物かを証明書と署名で確認する
- ECDHEやPSKを使って鍵の材料を準備する
- HKDFでハンドシェイク用・アプリケーションデータ用などの秘密を導出する
では、実際のHTTPリクエストやレスポンスは、どのように守られるのでしょうか。
ここで登場するのが、TLS 1.3の Record Protocol です。
Record Protocolは、アプリケーションデータを小さな単位に分け、それぞれを暗号化し、改ざんされていないか確認できる形にして送ります。
たとえるなら、大きな荷物をそのまま丸ごと送るのではなく、小さな箱に分け、それぞれに封筒と封印を付けて送るようなイメージです。
RFC 8446では、TLS Record Protocolが送信されるメッセージを扱いやすいブロックに分割し、保護して送信する仕組みとして定義されています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 5
7.1 TLSレコードは「小分けにして守る単位」
Webページを開くとき、ブラウザとサーバーの間ではHTML、CSS、JavaScript、画像、APIレスポンスなど、さまざまなデータが流れます。
これらをすべて1つの巨大な暗号文として扱うのではなく、TLSでは record という単位に分けて処理します。
ざっくり図にすると、次のようになります。
この章で大切なのは、TLS 1.3が通信データをただ暗号化しているだけではない、という点です。
通信データはレコードごとに保護され、受信側では「正しく復号できるか」「途中で書き換えられていないか」が確認されます。
| 処理 | やっていること | 身近なたとえ |
|---|---|---|
| 分割 | 大きな通信データをレコード単位に分ける | 荷物を小箱に分ける |
| 暗号化 | 中身を第三者に読めない形にする | 封筒に入れる |
| 認証 | 途中で書き換えられていないか確認する | 封印を付ける |
| 復号 | 正しい相手だけが中身を取り出す | 封筒を開ける |
7.2 AEADは「封筒」と「封印」をまとめて扱う
2章でも少し触れましたが、TLS 1.3の通信データ保護では AEAD が中心になります。
AEADは、Authenticated Encryption with Associated Dataの略です。
日本語では「関連データ付き認証暗号」などと呼ばれます。
名前だけ見ると少し難しいですが、まずは次のように理解すると分かりやすいです。
AEADは、データを読まれないようにする暗号化と、途中で書き換えられていないかを確認する認証を、まとめて扱う仕組みです。
RFC 5116では、Authenticated Encryptionは、暗号化された平文の機密性に加えて、完全性と真正性を確認する方法を提供すると説明されています。
また、AEADでは、暗号化しない関連データについても完全性と真正性を確認できるとされています。
参考: RFC 5116 - An Interface and Algorithms for Authenticated Encryption
TLS 1.3では、通信データを保護するときに、次のような情報が関係します。
| AEADに渡すもの | ざっくりした意味 | TLS 1.3でのイメージ |
|---|---|---|
| key | 暗号化と認証に使う鍵 | HKDFから導出されたwrite key |
| nonce | レコードごとに変わる値 | write IVとシーケンス番号から作る値 |
| plaintext | 守りたい中身 | HTTPリクエスト、レスポンス、ハンドシェイクメッセージなど |
| additional data | 暗号化はしないが改ざんを検知したい情報 | TLSレコードヘッダなど |
ここで少し面白いのは、additional dataです。
これは「中身を隠す対象ではないが、勝手に変えられると困る情報」です。
たとえば、荷物の中身は封筒の中に隠します。
一方で、封筒の外側に書かれた配送ラベルは見えるかもしれません。
しかし、配送ラベルを勝手に書き換えられると困るので、封印の確認対象には含めたい、というイメージです。
7.3 TLS 1.3のRecord保護の流れ
TLS 1.3では、実際に送るデータを TLSInnerPlaintext という形にしてからAEADで保護します。
かなり簡略化すると、流れは次のようになります。
- HTTPリクエストやレスポンスなどのデータを用意する
- データの末尾に、本来のデータ種別を示す
ContentTypeを付ける - 必要に応じてゼロ値のパディングを付ける
- AEADで暗号化し、認証タグを付ける
- 外側のTLSレコードとして送信する
RFC 8446では、TLS 1.3の保護されたレコードは外側から見ると application_data 型として扱われ、実際のContentTypeは暗号化された内側に含まれる形になります。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 5.2
図にすると、次のようなイメージです。
ここで、内側のContentTypeが暗号化される点はTLS 1.3の特徴の1つです。
外側のレコード型だけを見ると、多くの場合 application_data のように見えるため、以前よりも一部のメタ情報が見えにくくなります。
ただし、TLS 1.3が通信のすべてのメタ情報を隠すわけではありません。
IPアドレス、通信量、通信タイミングなど、通信経路上で観測できる情報は残ります。
この点は、10章の「TLS 1.3で守れること・守れないこと」で改めて整理します。
7.4 TLS 1.3で使われるAEAD方式
TLS 1.3では、暗号スイートの考え方がTLS 1.2以前から整理されています。
TLS 1.3の暗号スイートは、主にAEADアルゴリズムとHKDFで使うハッシュ関数を指定します。
RFC 8446では、TLS 1.3の暗号スイートとして、たとえば次のようなものが定義されています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Appendix B.4
| 暗号スイート | AEAD方式 | ハッシュ関数 | ざっくりした見方 |
|---|---|---|---|
TLS_AES_128_GCM_SHA256 |
AES-128-GCM | SHA-256 | AES-GCMを使う代表的な方式 |
TLS_AES_256_GCM_SHA384 |
AES-256-GCM | SHA-384 | より長いAES鍵とSHA-384を使う方式 |
TLS_CHACHA20_POLY1305_SHA256 |
ChaCha20-Poly1305 | SHA-256 | ソフトウェア実装で使いやすい方式 |
TLS_AES_128_CCM_SHA256 |
AES-128-CCM | SHA-256 | CCMモードを使う方式 |
TLS_AES_128_CCM_8_SHA256 |
AES-128-CCM-8 | SHA-256 | 短い認証タグを使うCCM方式 |
ここで注意したいのは、SHA256 や SHA384 が「通信データそのものをSHAで暗号化している」という意味ではないことです。
6章で触れたように、TLS 1.3の暗号スイート名に含まれるハッシュ関数は、HKDFやtranscript hashに関係します。
つまり、たとえば TLS_AES_128_GCM_SHA256 は、かなりざっくり言うと次のように読めます。
TLS_AES_128_GCM_SHA256
├─ AES_128_GCM : レコード保護に使うAEAD方式
└─ SHA256 : 鍵導出やハンドシェイク履歴のハッシュに使う関数
💡 豆知識
TLS 1.2以前の暗号スイート名には、鍵交換方式、認証方式、共通鍵暗号、MACなど、いろいろな要素が詰め込まれていました。
一方、TLS 1.3では、鍵交換や署名アルゴリズムは拡張で別に合意され、暗号スイートは主にAEADとハッシュ関数を示す形に整理されています。
そのため、TLS 1.3の暗号スイート名だけを見ても、証明書の署名方式や鍵共有グループまでは分かりません。
7.5 nonceは「レコードごとに変わる使い捨て番号」
AEADを使うときに、とても重要になるのが nonce です。
nonceは、ざっくり言うと「その暗号化処理で使う一度きりの値」です。
同じ鍵と同じnonceの組み合わせを繰り返し使うと、方式によっては安全性が大きく崩れる可能性があります。
TLS 1.3では、レコードごとにシーケンス番号を持ち、それをwrite IVと組み合わせてnonceを作ります。
RFC 8446では、各レコードのnonceは、レコードのシーケンス番号を左側ゼロ埋めし、static IVとXORする形で作ると説明されています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 5.3
この仕組みによって、同じwrite keyを使っていても、レコードごとに異なるnonceを使えます。
ただし、これはTLS実装が正しく管理するべき部分です。
アプリケーション開発者が普段のHTTPS利用でnonceを手作業で管理するわけではありません。
7.6 PythonでRecord保護の雰囲気を確認する
ここでは、TLS 1.3のRecord保護を完全に再現するのではなく、AEADが「暗号化」と「改ざん検知」をまとめて行う雰囲気を確認します。
以下のコードは、TLS 1.3風に次の処理を行う小さな例です。
-
write_keyとwrite_ivを用意する - レコードごとのシーケンス番号からnonceを作る
- HTTPリクエスト風のデータを
TLSInnerPlaintext風に組み立てる - AES-GCMで暗号化する
- 1バイト改ざんすると復号に失敗することを確認する
注意: このコードは学習用です。TLS 1.3の完全な実装ではありません。
実際の通信では、Pythonのsslモジュール、OpenSSL、ブラウザ、Webサーバーなど、十分に検証されたTLS実装を使ってください。
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
import os
# TLS 1.3のapplication_dataを表す値です。
# 実際のTLS 1.3では、暗号化後の外側のContentTypeも多くの場合 application_data として扱われます。
CONTENT_TYPE_APPLICATION_DATA = 0x17
# TLS 1.3のレコードヘッダで使われるlegacy_record_versionです。
# TLS 1.3でも互換性のために 0x0303 が使われます。
LEGACY_RECORD_VERSION = b"\x03\x03"
def make_tls13_nonce(write_iv: bytes, sequence_number: int) -> bytes:
"""TLS 1.3風に、write IVとシーケンス番号からnonceを作る。
RFC 8446では、シーケンス番号をIV長に合わせて左側ゼロ埋めし、
static IVとXORしてAEAD nonceを作ります。
"""
seq_bytes = sequence_number.to_bytes(len(write_iv), "big")
return bytes(iv_byte ^ seq_byte for iv_byte, seq_byte in zip(write_iv, seq_bytes))
def build_record_header(ciphertext_length: int) -> bytes:
"""TLSCiphertextのヘッダに相当する追加認証データを作る。
このヘッダ自体は暗号化されませんが、AEADのadditional dataとして認証対象に含めます。
つまり、ヘッダが途中で書き換えられると復号時の検証に失敗します。
"""
outer_content_type = bytes([CONTENT_TYPE_APPLICATION_DATA])
length = ciphertext_length.to_bytes(2, "big")
return outer_content_type + LEGACY_RECORD_VERSION + length
# 学習用のランダムな鍵とIVです。
# 実際のTLS 1.3では、これらはHKDFから導出されます。
write_key = os.urandom(16) # AES-128-GCM用の128bit鍵
write_iv = os.urandom(12) # AES-GCMで一般的に使われる96bit nonce用IV
# 送信するHTTPリクエスト風のデータです。
http_request = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
# TLSInnerPlaintext風に、末尾へ本来のContentTypeを付けます。
# ここではpaddingは省略しています。
inner_plaintext = http_request + bytes([CONTENT_TYPE_APPLICATION_DATA])
# 1つ目のレコードなので、シーケンス番号を0とします。
sequence_number = 0
nonce = make_tls13_nonce(write_iv, sequence_number)
# AES-GCMでは認証タグが16バイト付くため、暗号文長を先に見積もります。
# cryptographyのAESGCM.encryptは「暗号文 + 認証タグ」を返します。
tag_length = 16
expected_ciphertext_length = len(inner_plaintext) + tag_length
# TLSレコードヘッダ風の追加認証データを作ります。
additional_data = build_record_header(expected_ciphertext_length)
# AEADで暗号化します。
# additional_dataは暗号化されませんが、改ざん検知の対象になります。
aesgcm = AESGCM(write_key)
ciphertext = aesgcm.encrypt(nonce, inner_plaintext, additional_data)
print("ciphertext:", ciphertext.hex())
# 正しい鍵、nonce、additional_dataであれば復号できます。
decrypted = aesgcm.decrypt(nonce, ciphertext, additional_data)
print("decrypted:", decrypted)
# 暗号文を1バイトだけ改ざんしてみます。
tampered = bytearray(ciphertext)
tampered[0] ^= 0x01
try:
# 改ざん後の暗号文は、認証タグの検証に失敗します。
aesgcm.decrypt(nonce, bytes(tampered), additional_data)
except InvalidTag:
print("改ざんを検知しました")
このコードで見てほしいのは、暗号文を1バイトだけ変えただけでも、復号時に検証が失敗することです。
AEADでは、暗号文の復号と認証タグの検証がセットになっています。
つまり、受信側は「読めたからOK」ではなく、「正しい鍵・nonce・追加認証データに対して、改ざんされていない形で届いたか」まで確認します。
7.7 追加認証データを書き換えるとどうなるか
AEADでは、暗号文だけでなく、additional dataも認証対象になります。
そのため、暗号化されていないヘッダ部分が途中で書き換えられた場合も、検証に失敗します。
先ほどのコードに、次のような処理を追加すると確認できます。
# additional_data、つまりTLSレコードヘッダ風の情報を改ざんします。
# 中身の暗号文は変えていなくても、認証対象のヘッダが変わると検証に失敗します。
tampered_additional_data = bytearray(additional_data)
tampered_additional_data[-1] ^= 0x01
try:
aesgcm.decrypt(nonce, ciphertext, bytes(tampered_additional_data))
except InvalidTag:
print("追加認証データの改ざんを検知しました")
この例から、AEADのadditional dataが何のためにあるのかが少し見えてきます。
暗号化しない部分であっても、勝手に変えられると困る情報は、認証対象に含めることができます。
TLS 1.3では、レコードヘッダなどがAEADのadditional dataとして扱われます。
これにより、受信側は暗号文だけでなく、レコードの外側の情報との整合性も確認できます。
7.8 Record保護で見える情報・見えにくくなる情報
TLS 1.3でレコードが暗号化されると、通信内容の多くは第三者から読めなくなります。
ただし、すべての情報が完全に隠れるわけではありません。
| 観点 | TLS 1.3でどう見えるか |
|---|---|
| HTTPの本文 | 暗号化されるため、基本的に第三者には読めない |
| HTTPヘッダ | アプリケーションデータとして暗号化される |
| 内側のContentType | TLS 1.3では暗号化された内側に入る |
| TLSレコードの長さ | 完全には隠れない |
| 通信タイミング | 完全には隠れない |
| IPアドレス | TLSだけでは隠れない |
ここは過信しやすいポイントです。
TLS 1.3は、通信内容を守るうえで非常に重要な仕組みです。
しかし、通信量やタイミングのようなメタ情報まで完全に隠すものではありません。
また、TLS 1.3は「ブラウザとサーバーの間の通信路」を守る仕組みです。
ブラウザに表示されたページそのものが詐欺ページである場合や、端末がマルウェアに感染している場合、アプリケーションに脆弱性がある場合は、別の対策が必要になります。
7.9 KeyUpdate:通信中に鍵を更新する仕組み
TLS 1.3には、接続中に通信鍵を更新する KeyUpdate という仕組みもあります。
長く続く通信では、同じtraffic keyをずっと使い続けるよりも、途中で新しい鍵に更新できる方が望ましい場合があります。
TLS 1.3では、既存のapplication traffic secretから、次のapplication traffic secretを導出し、そこから新しい鍵とIVを作れます。
RFC 8446では、KeyUpdateメッセージによって送信側のapplication traffic secretを更新できることが説明されています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 4.6.3
この記事ではKeyUpdateの詳細な状態管理までは扱いません。
ここでは、「TLS 1.3では通信開始時に鍵を作って終わりではなく、必要に応じて通信中に鍵を更新する仕組みもある」と押さえておけば十分です。
7.10 この章のまとめ
この章では、TLS 1.3のRecord ProtocolとAEADを整理しました。
ポイントは次のとおりです。
- TLS 1.3では、実際の通信データをrecordという単位に分けて保護する
- AEADは、暗号化と改ざん検知をまとめて扱う仕組みである
- TLS 1.3のレコード保護では、鍵、nonce、平文、追加認証データが関係する
- nonceはwrite IVとシーケンス番号から作られ、レコードごとに変わる
- 暗号文だけでなく、additional dataの改ざんも検知できる
- TLS 1.3の暗号スイートは、主にAEAD方式とHKDFで使うハッシュ関数を示す
- TLS 1.3は通信内容を守るが、通信量、タイミング、IPアドレスなどのメタ情報まですべて隠すわけではない
ここまでで、TLS 1.3の中で、証明書、署名、鍵共有、HKDF、AEADがどのようにつながるのかが一通り見えてきました。
次の章では、TLS 1.2以前と比べて、TLS 1.3で何が整理され、なぜ現在の形になったのかを見ていきます。
8. TLS 1.2から何が変わったのか
この章では、TLS 1.3で何が変わったのかを、固い歴史の暗記ではなく「なぜその設計になったのか」という視点で整理します。
TLS 1.3は、TLS 1.2以前を単純に少し強くしたものというより、複雑になっていた選択肢を整理し、安全に使いやすい形へ寄せたプロトコルとして見ると理解しやすいです。
ここまで、TLS 1.3の中で、証明書、デジタル署名、鍵共有、HKDF、AEADがどのように組み合わされるのかを見てきました。
では、TLS 1.3はTLS 1.2以前と比べて、何が変わったのでしょうか。
ここで注意したいのは、TLS 1.3を「新しいから強い」とだけ覚えないことです。
もちろん、TLS 1.3では安全性や性能の面で多くの改善が行われています。
しかし、より本質的には、TLS 1.2以前で複雑になっていた選択肢や、現在では避けたい古い方式を整理し、より安全に使いやすい形にした点が重要です。
RFC 8446では、TLS 1.2からTLS 1.3への主な機能差分として、古い対称暗号方式の整理、AEAD方式への統一、静的RSAや静的Diffie-Hellman暗号スイートの削除、ServerHello以降のハンドシェイクメッセージの暗号化、HKDFを使った鍵導出の再設計、0-RTTの追加などが挙げられています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3, Section 1.2
8.1 まず全体像を見る
TLS 1.2からTLS 1.3への変化を、ざっくり整理すると次のようになります。
| 観点 | TLS 1.2以前の見方 | TLS 1.3での見方 |
|---|---|---|
| 対称暗号 | CBCモードなど複数の選択肢があった | 残る方式はAEAD方式に整理された |
| 暗号スイート | 鍵交換・認証・暗号化・MACまでまとめて表す | 主にAEAD方式とHKDF用ハッシュを表す |
| 鍵交換 | 静的RSAや静的DHも存在した | 静的RSA・静的DHは削除され、公開鍵ベースの鍵交換はForward Secrecyを提供する形になった |
| ハンドシェイク | 平文で見える部分が比較的多かった | ServerHello以降のハンドシェイクメッセージが暗号化される |
| 鍵導出 | TLS 1.2までのPRFを使う | HKDFベースのKey Scheduleに整理された |
| 往復回数 | フルハンドシェイクでは通常2-RTT | フルハンドシェイクは基本的に1-RTT |
| 0-RTT | 基本仕様にはない | 再接続時に早期データを送れるが、リプレイに注意が必要 |
図にすると、TLS 1.3での整理は次のように見ることができます。
ここからは、特に記事で押さえておきたい変更点を順番に見ていきます。
8.2 古い対称暗号方式が整理され、AEAD中心になった
TLS 1.3の大きな変更点の1つは、対称暗号方式の整理です。
TLS 1.2以前では、暗号スイートの選択肢が多く、CBCモードを使う方式なども存在していました。
一方、TLS 1.3では、残されている対称暗号方式はすべて AEAD 方式です。
AEADは、7章で見たように、暗号化と改ざん検知をまとめて扱う仕組みです。
たとえば、TLS 1.3で定義されている代表的な暗号スイートには、次のようなものがあります。
| TLS 1.3の暗号スイート | レコード保護で使うAEAD | HKDFなどで使うハッシュ |
|---|---|---|
TLS_AES_128_GCM_SHA256 |
AES-128-GCM | SHA-256 |
TLS_AES_256_GCM_SHA384 |
AES-256-GCM | SHA-384 |
TLS_CHACHA20_POLY1305_SHA256 |
ChaCha20-Poly1305 | SHA-256 |
ここで大切なのは、TLS 1.3の暗号スイート名を見たときに、「これだけで鍵交換や認証方式まで全部決まる」と考えないことです。
TLS 1.3では、暗号スイートの考え方がTLS 1.2以前から変わっています。
💡 豆知識
TLS 1.2以前の暗号スイート名は、かなり長くなりがちでした。
たとえばTLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256のように、鍵交換、認証、暗号化、ハッシュが1つの名前に詰め込まれていました。
TLS 1.3では、鍵交換や認証の選択と、レコード保護の暗号スイートを分けて考えるようになっています。
8.3 暗号スイート名の意味が変わった
TLS 1.2以前を学んだことがある人ほど、TLS 1.3の暗号スイート名で少し混乱するかもしれません。
たとえば、次の2つを比べてみます。
| 例 | ざっくり含まれる情報 |
|---|---|
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
ECDHEで鍵共有し、RSA証明書で認証し、AES-128-GCMで保護し、SHA-256を使う |
TLS_AES_128_GCM_SHA256 |
AES-128-GCMでRecordを保護し、SHA-256をHKDFなどで使う |
TLS 1.3の暗号スイート名には、ECDHEやRSAのような鍵交換・認証方式が入っていません。
これらは、supported_groups、key_share、signature_algorithms などの拡張や証明書の内容によって別に合意・選択されます。
この違いを、簡単なコードで雰囲気だけ確認してみます。
# TLS 1.2以前風の暗号スイート名と、TLS 1.3の暗号スイート名を見比べる例です。
# 実際のTLS実装で暗号スイートを解析するコードではなく、名前の違いを理解するための簡単な確認用です。
cipher_suites = [
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_AES_128_GCM_SHA256",
]
for name in cipher_suites:
print("暗号スイート名:", name)
if "_WITH_" in name:
# TLS 1.2以前の暗号スイート名では、WITHより前に鍵交換・認証方式が含まれることがあります。
before, after = name.split("_WITH_", 1)
print(" TLS 1.2以前風の読み方")
print(" 鍵交換・認証に関係する部分:", before.replace("TLS_", ""))
print(" レコード保護などに関係する部分:", after)
else:
# TLS 1.3の暗号スイート名は、主にRecord保護のAEAD方式とHKDF用ハッシュを表します。
print(" TLS 1.3風の読み方")
print(" 主にRecord保護方式とHKDF用ハッシュを表す")
print()
このコードはあくまで名前の読み方をつかむためのものです。
実際のTLS通信でどの鍵交換方式や署名方式が使われたかを確認するには、TLSライブラリやパケット解析ツールでハンドシェイク全体を見る必要があります。
8.4 静的RSA・静的DHによる鍵交換が削除された
TLS 1.3では、静的RSAや静的Diffie-Hellmanを使う暗号スイートが削除されました。
RFC 8446では、静的RSAとDiffie-Hellman暗号スイートが削除され、公開鍵ベースの鍵交換方式はForward Secrecyを提供するようになったと説明されています。
参考: RFC 8446 - Section 1.2
ここでいう静的RSAによる鍵交換は、かなりざっくり言うと、サーバーの長期秘密鍵に大きく依存して通信鍵の材料を守る方式です。
このような設計では、もしサーバーの長期秘密鍵が後から漏えいした場合、過去に記録されていた通信の安全性に影響が出る可能性があります。
一方、TLS 1.3で基本となるECDHEでは、通信ごとに一時的な鍵材料を使います。
これにより、サーバー証明書の秘密鍵が後で漏えいしたとしても、過去の通信内容をすぐに復号できるとは限らない設計になります。
これが5章で説明した Forward Secrecy の考え方です。
💡 豆知識
「公開鍵暗号を使っているから安全」と言いたくなりますが、TLSでは“どのように鍵を準備するか”がとても重要です。
TLS 1.3では、単に公開鍵暗号を使うのではなく、過去の通信を守りやすい鍵交換の形に整理されています。
8.5 ServerHello以降のハンドシェイクメッセージが暗号化される
TLS 1.3では、ServerHello以降のハンドシェイクメッセージが暗号化されます。
RFC 8446では、ServerHello以降のすべてのハンドシェイクメッセージが暗号化され、新しく導入されたEncryptedExtensionsによって、以前はServerHelloで平文送信されていた拡張情報にも機密性保護を与えられると説明されています。
参考: RFC 8446 - Section 1.2
これは、通信内容だけでなく、ハンドシェイク中にやり取りされる情報の見え方にも関係します。
もちろん、ClientHelloなど、接続を始めるためにどうしても見える情報は残ります。
しかしTLS 1.3では、TLS 1.2以前よりも早い段階で暗号化された状態に移行するため、ハンドシェイク中に外から見える情報を減らす方向に整理されています。
この点は、TLS 1.3を「通信本文だけを暗号化する仕組み」と考えていると見落としやすいポイントです。
8.6 鍵導出がHKDFベースに整理された
6章で見たように、TLS 1.3ではHKDFを使ったKey Scheduleによって、用途ごとの秘密や鍵を導出します。
TLS 1.3では、鍵導出の設計が見直され、鍵分離の性質を分析しやすい形になっています。
RFC 8446でも、鍵導出関数が再設計され、HMAC-based Extract-and-Expand Key Derivation Function、つまりHKDFが基礎部品として使われると説明されています。
参考: RFC 8446 - Section 1.2
この変更は、初学者向けには次のように理解すると分かりやすいです。
- 鍵共有で得た秘密を、そのまま全部の用途に使い回さない
- ハンドシェイク用、アプリケーションデータ用、再開用などに分ける
- transcript hashを取り込み、「この接続の会話内容」と鍵を結びつける
- 用途ごとの鍵分離により、設計を見通しやすくする
たとえるなら、1つの材料をそのまま全部の料理に入れるのではなく、スープ用、メイン料理用、ソース用に分けて下ごしらえするようなものです。
8.7 ハンドシェイクが短く、整理された
TLS 1.3では、フルハンドシェイクが基本的に1-RTTで完了するように整理されています。
3章で見たように、ClientHelloでkey_shareを送ることで、サーバーはServerHelloの時点で共有秘密の計算に必要な情報を返せます。
TLS 1.2以前では、ハンドシェイクの流れや選択肢が複雑になりやすく、ChangeCipherSpecのようなメッセージも登場しました。
TLS 1.3では、ハンドシェイク状態機械が整理され、不要なメッセージが削減されています。RFC 8446でも、ハンドシェイク状態機械が大きく再構成され、ChangeCipherSpecのような余分なメッセージが削除されたと説明されています。ただし、middlebox compatibilityのために例外的に使われる場合があります。
参考: RFC 8446 - Section 1.2
ここで注意したいのは、1-RTT化は「安全性を犠牲にして速くした」という単純な話ではないことです。
TLS 1.3では、鍵交換、認証、鍵導出、レコード保護の設計を整理することで、より短い流れで安全な通信路を作れるようになっています。
8.8 0-RTTが追加されたが、注意も増えた
TLS 1.3では、再接続時に一部のアプリケーションデータを早く送れる 0-RTT が追加されました。
これは便利な仕組みですが、通常の1-RTTデータと同じ安全性を持つわけではありません。
RFC 8446でも、0-RTTは接続開始時の往復を節約できる一方で、一部のセキュリティ性質を犠牲にすると説明されています。
参考: RFC 8446 - Section 1.2
0-RTTの詳しい話は次章で扱います。
ここでは、TLS 1.3の変更点として「速くするための選択肢が追加されたが、アプリケーション側の設計注意も必要になった」と押さえておけば十分です。
8.9 TLS 1.2はすぐに全部禁止、という意味ではない
TLS 1.3を説明するときに、もう1つ注意したいことがあります。
それは、TLS 1.3があるからTLS 1.2はすぐに全部危険、という単純な話ではない という点です。
RFC 9325では、TLS 1.0とTLS 1.1はネゴシエートしてはならないとされています。
一方で、TLS 1.2は実装が広く展開されており、推奨に従って既知の攻撃を緩和する場合、TLS 1.2の利用も安全に扱えると説明されています。
また、TLS 1.3をサポートする実装では、TLS 1.3を以前のバージョンより優先してネゴシエートする必要があるとされています。
参考: RFC 9325 - Recommendations for Secure Use of TLS and DTLS, Section 3.1.1
さらに、RFC 8996ではTLS 1.0とTLS 1.1が非推奨化され、実装はTLS 1.0やTLS 1.1をネゴシエートしてはならないとされています。
参考: RFC 8996 - Deprecating TLS 1.0 and TLS 1.1
つまり、実務での見方としては次のようになります。
| バージョン | 現在の見方 |
|---|---|
| TLS 1.0 / TLS 1.1 | 使わない。ネゴシエートしない |
| TLS 1.2 | 適切な設定・推奨に従えば利用される場面がある |
| TLS 1.3 | サポートするなら優先して使うべき |
ここは、記事としても誤解を避けたいポイントです。
TLS 1.3を推奨することと、TLS 1.2を雑に「全部危険」と断定することは別です。
💡 豆知識
セキュリティでは、「新しいものだけが正義」「古いものは全部危険」と単純に言い切れないことがあります。
大切なのは、どのバージョンを、どの設定で、どの用途に使っているかを確認することです。
ただし、TLS 1.0やTLS 1.1のように明確に非推奨化されたものは、互換性だけを理由に残し続けるとリスクになります。
8.10 この章のまとめ
この章では、TLS 1.2からTLS 1.3への主な変更点を整理しました。
ポイントは次のとおりです。
- TLS 1.3では、古い対称暗号方式が整理され、残る方式はAEADになった
- 暗号スイートの意味が変わり、鍵交換・認証とRecord保護を分けて考えるようになった
- 静的RSAや静的DHによる鍵交換が削除され、Forward Secrecyを重視する設計になった
- ServerHello以降のハンドシェイクメッセージが暗号化されるようになった
- HKDFベースのKey Scheduleにより、用途ごとの鍵分離が整理された
- フルハンドシェイクが基本的に1-RTTになり、接続開始の流れが短くなった
- 0-RTTが追加されたが、リプレイ攻撃への注意が必要になった
- TLS 1.0 / TLS 1.1は使うべきではなく、TLS 1.3をサポートする場合はTLS 1.2以前より優先して使うべきである
TLS 1.3は、「TLS 1.2に少し機能を足したもの」というより、過去の複雑さや古い選択肢を整理し、安全に使いやすい形に設計し直したものと見ると理解しやすくなります。
次の章では、ここで少し触れた0-RTTについて、便利さと注意点をもう少し詳しく見ていきます。
9. 0-RTTは便利だが注意も必要
この章では、TLS 1.3の高速化機能である 0-RTT を、便利さと注意点の両方から整理します。
結論から言うと、0-RTTは「前に接続したことがある相手に、再接続時の一部データを早く送る仕組み」です。
ただし、通常の1-RTTデータと同じ安全性を持つわけではなく、特にリプレイ攻撃への注意が必要です。
前の章では、TLS 1.3がTLS 1.2以前と比べて、より短い流れで安全な通信を始められるように整理されたことを見ました。
その中でも、少し特殊な機能が 0-RTT です。
名前だけ見ると、「通信が一瞬で安全になるすごい機能」のように見えるかもしれません。
しかし、0-RTTは便利な一方で、使い方を間違えると危険な場面があります。
そのため、この章では「速いから便利」という面だけでなく、「どのようなデータなら送ってよいのか」という注意点まで整理します。
9.1 まずRTTとは何か
0-RTTを理解するために、まず RTT という言葉を確認します。
RTTは Round Trip Time の略で、ざっくり言うと「こちらから送ったものが相手に届き、相手から返事が戻ってくるまでの往復時間」です。
たとえば、友達にメッセージを送って、返事が返ってくるまでの時間をイメージすると分かりやすいです。
ネットワークでも同じように、クライアントとサーバーの間で何度かやり取りが必要になるほど、通信開始までの待ち時間が増えます。
| 用語 | やさしく言うと | TLSでのイメージ |
|---|---|---|
| 1-RTT | 1往復してからデータを送る | 通常のTLS 1.3ハンドシェイク後にアプリケーションデータを送る |
| 0-RTT | 往復を待たずにデータを送る | 再接続時にClientHelloと一緒に早期データを送る |
TLS 1.3の通常のフルハンドシェイクでは、基本的に1-RTTで通信を始められます。
これはTLS 1.2以前と比べて、通信開始までの流れを短くする大きな改善点です。
一方、0-RTTでは、以前接続したことがあるサーバーに対して、再接続時に最初のClientHelloと一緒に一部のアプリケーションデータを送れます。
ここでのポイントは、0-RTTは「初めて接続する相手」に使うものではないことです。
以前の接続で得たPSKなどの情報を使い、再接続を速くするための仕組みです。
RFC 8446でも、0-RTTデータはPSKを使う接続で送られる早期データとして扱われています。
参考: RFC 8446 - Section 2.3: 0-RTT Data
9.2 0-RTTを身近なたとえで考える
0-RTTは、飲食店の常連客をイメージすると分かりやすいです。
初めて入るお店では、まず席に案内され、メニューを見て、注文を伝える必要があります。
これは通常のハンドシェイクに近いです。
一方、何度も行っているお店で、店員さんもあなたのことを覚えているとします。
この場合、「いつものお願いします」と先に伝えて、受付の細かいやり取りが終わる前に準備を始めてもらえるかもしれません。
これが0-RTTのイメージです。
ただし、このたとえには注意点があります。
「いつもの注文票」を先に出せるとしても、その注文票を誰かがコピーして何度も出したら困ります。
もし「水をください」のような注文なら大きな問題になりにくいかもしれません。
しかし、「同じ商品を購入する」「同じ送金を実行する」のような操作が何度も実行されると、大きな問題になります。
この「コピーされてもう一度使われる」リスクが、0-RTTで特に注意したい リプレイ攻撃 です。
9.3 0-RTTの何が便利なのか
0-RTTのメリットは、再接続時の待ち時間を減らせることです。
Webサービスでは、ユーザーがページを開くたびに、ブラウザやアプリがサーバーと接続を作ることがあります。
通信の往復が1回減るだけでも、ユーザー体験に影響する場合があります。
特に、次のような場面では、待ち時間の短縮が重要になります。
| 場面 | 0-RTTが役立つ可能性 |
|---|---|
| 何度も同じサービスへアクセスする | 再接続時の待ち時間を減らせる |
| モバイル回線など遅延が大きい環境 | 1往復分の遅延削減が体感に影響しやすい |
| CDNや大規模Webサービス | 多数の接続で少しずつ遅延を減らせる可能性がある |
ただし、ここで重要なのは、0-RTTは 安全性を何も変えずに速くする魔法ではない という点です。
RFC 8446では、0-RTTデータの安全性は他のTLSデータより弱く、具体的にはForward Secrecyがなく、接続間のnon-replay保証もないと説明されています。
参考: RFC 8446 - Section 2.3: 0-RTT Data
9.4 0-RTTではリプレイ攻撃に注意する
リプレイ攻撃とは、過去に送られた正しいデータを攻撃者がコピーし、もう一度送る攻撃です。
ここで大切なのは、リプレイ攻撃では、データの中身を解読できなくても問題が起きる場合があることです。
攻撃者は暗号を破って内容を読むのではなく、暗号化されたデータのかたまりをそのまま再送します。
たとえば、次のような操作を考えます。
| 操作 | 再送されるとどうなるか | 0-RTTに向いているか |
|---|---|---|
| 商品一覧を見る | 同じ一覧が表示されるだけなら影響が小さいことが多い | 比較的向いている場合がある |
| 検索する | 同じ検索が再実行されるだけなら影響が小さいことが多い | 比較的向いている場合がある |
| 商品を購入する | 同じ購入が複数回行われる可能性がある | 向いていない |
| 送金する | 同じ送金が複数回実行される可能性がある | 向いていない |
| パスワード変更 | 意図しない状態変更につながる可能性がある | 向いていない |
図にすると、0-RTTにおけるリプレイの問題は次のように見えます。
もちろん、実際のシステムでは、サーバー側で再送を検出したり、0-RTTを受け入れない設定にしたりできます。
しかし、0-RTTはTLS層だけで完全に安全にできるものではありません。
RFC 8446でも、TLSは0-RTTデータに対して本質的なリプレイ保護を提供しないため、アプリケーションは再送されても安全だと判断できるearly dataだけを送る必要があると説明されています。
参考: RFC 8446 - Section 8: 0-RTT and Anti-Replay
9.5 「再送されても安全な操作」とは何か
0-RTTで大切なのは、その操作が再送されても困らないか を考えることです。
Webの文脈では、よく「安全なHTTPメソッド」と「安全でないHTTPメソッド」という考え方が出てきます。
ざっくり言うと、GETのように情報を取得するだけの操作は比較的扱いやすく、POSTのように状態を変える可能性がある操作は慎重に扱う必要があります。
ただし、ここでも単純に「GETなら全部安全」と覚えるのは危険です。
実装によってはGETで状態を変更してしまう設計もあり得るためです。
| 観点 | 0-RTTで考えること |
|---|---|
| 状態を変えるか | データベース更新、購入、送金、予約確定などは避ける |
| 何度実行されても同じ結果か | 何度実行しても副作用がない操作の方が扱いやすい |
| サーバー側で重複検出できるか | リクエストIDやnonceで二重処理を防げるか確認する |
| 0-RTTが拒否された場合 | 通常の1-RTTで再送してよいか設計する |
RFC 8470では、HTTPでEarly Dataを使う場合、クライアントは安全なHTTPメソッドをearly dataとして送ってよい場合がある一方、安全でないメソッドや安全性が不明なメソッドをearly dataで送ってはならないと説明されています。
参考: RFC 8470 - Section 4: Using Early Data in HTTP Clients
9.6 Python風の疑似コードで考える:0-RTTで処理してよいか
0-RTTの注意点は、実装コードを見ると少しイメージしやすくなります。
以下は、実際のTLS実装ではありません。
0-RTTで受け取ったHTTPリクエストを、アプリケーション側でどう扱うかを考えるための疑似コードです。
from dataclasses import dataclass
@dataclass
class Request:
method: str
path: str
request_id: str | None = None
# 0-RTTで受け付けてもよいと判断する読み取り専用のパス例
READ_ONLY_PATHS = {
"/products",
"/search",
"/news",
}
# 処理済みのリクエストIDを保存する例
# 実際の本番環境では、複数サーバー間で共有できるストレージなどを検討する必要があります。
processed_request_ids: set[str] = set()
def can_process_as_early_data(request: Request) -> bool:
"""0-RTTで処理してよいリクエストかを判定する。
この関数はTLSそのものを実装するものではありません。
0-RTTではリプレイされても困らない操作だけを許可する、という考え方を示すための例です。
"""
# GET以外は状態を変える可能性があるため、0-RTTでは受け付けない
if request.method != "GET":
return False
# GETでも、読み取り専用として明確に扱うパスだけを許可する
if request.path not in READ_ONLY_PATHS:
return False
return True
def process_request(request: Request, is_early_data: bool) -> str:
"""リクエストを処理する例。
is_early_data=True の場合、リプレイされても困らない操作だけを処理します。
"""
if is_early_data:
# 0-RTTでは、アプリケーション側が安全に処理できるリクエストだけを許可する
if not can_process_as_early_data(request):
return "425 Too Early: 通常の1-RTT接続で再送してください"
# request_id がある場合は、二重処理を避けるために確認する
if request.request_id is not None:
if request.request_id in processed_request_ids:
return "409 Conflict: 同じリクエストIDはすでに処理済みです"
processed_request_ids.add(request.request_id)
return "200 OK: リクエストを処理しました"
# 読み取り専用のGETは0-RTTで処理してもよい例
print(process_request(Request(method="GET", path="/products"), is_early_data=True))
# 購入処理のようなPOSTは0-RTTでは拒否する例
print(process_request(Request(method="POST", path="/purchase", request_id="order-001"), is_early_data=True))
このコードで示したいのは、0-RTTを使う場合、TLSライブラリに任せるだけではなく、アプリケーション側でも「何を0-RTTで受け付けるか」を考える必要があるという点です。
特に、購入、送金、予約確定、パスワード変更のような操作は、再送されると副作用が大きくなります。
そのため、0-RTTでは受け付けず、通常のハンドシェイク完了後に処理する方が安全です。
⚠️ 注意
上のコードは、0-RTTの考え方を説明するための簡略例です。
実際のTLS処理、HTTPサーバー、分散環境での重複排除、リクエストIDの設計をそのまま実装するものではありません。
9.7 HTTPでは 425 Too Early が使われることがある
HTTPで0-RTTを扱う場合、サーバーが「このリクエストを再送される可能性がある状態で処理するのは危ない」と判断することがあります。
そのため、RFC 8470では 425 Too Early というステータスコードが定義されています。
これは、サーバーが「リプレイされる可能性のあるリクエストを処理するリスクを取りたくない」ことを示すためのものです。
参考: RFC 8470 - Section 5.2: The 425 Too Early Status Code
かなり簡単に言うと、次のような流れです。
また、RFC 8470では、Early-Dataヘッダーも定義されています。
これは、リクエストがearly dataとして運ばれた可能性があることを、プロキシやゲートウェイを含むHTTPの経路で伝えるための仕組みです。
参考: RFC 8470 - Section 5.1: The Early-Data Header Field
💡 豆知識
HTTPステータスコードには、よく見る200 OKや404 Not Found以外にも、かなり細かい用途のものがあります。
425 Too Earlyは、まさにTLS 1.3の0-RTTとHTTPの関係から生まれたステータスコードです。
「早すぎるから、もう少し待って通常の接続で送ってください」というニュアンスで捉えると分かりやすいです。
9.8 サーバー側の対策だけで終わらない
RFC 8446では、0-RTTのリプレイ対策として、単回利用チケット、ClientHelloの記録、freshness checkなどが説明されています。
これらは、0-RTTデータが何度も受け入れられるリスクを減らすための仕組みです。
参考: RFC 8446 - Section 8: 0-RTT and Anti-Replay
ただし、仕様書でも示されているように、0-RTTのリプレイ対策はTLS層だけで完全に完結するものではありません。
特に、複数のサーバーやCDN、ロードバランサを使う構成では、どのサーバーがどの0-RTTデータを受け入れたかを一貫して管理する必要があります。
| 対策 | ざっくりした内容 | 注意点 |
|---|---|---|
| Single-Use Tickets | セッションチケットを一度だけ使えるようにする | 分散環境ではチケット状態の共有が必要 |
| ClientHello Recording | 最近受け取ったClientHelloを記録して重複を拒否する | 記録範囲や保存方法の設計が必要 |
| Freshness Checks | 古いClientHelloを拒否する | 時刻ずれや受付時間幅を考える必要がある |
| アプリケーション側の制御 | 0-RTTで処理してよい操作を限定する | 状態変更操作を避ける判断が必要 |
つまり、0-RTTを安全に使うには、TLSの設定だけでなく、アプリケーションの設計、HTTPの扱い、サーバー構成まで含めて考える必要があります。
9.9 この章のまとめ
この章では、TLS 1.3の0-RTTについて整理しました。
0-RTTは、以前接続したことがあるサーバーに対して、再接続時に一部のアプリケーションデータを早く送る仕組みです。
うまく使えば、通信開始までの待ち時間を減らせる可能性があります。
一方で、0-RTTデータは通常の1-RTTデータと同じ安全性を持つわけではありません。
RFC 8446でも、0-RTTデータはForward Secrecyを持たず、接続間のnon-replay保証もないと説明されています。
そのため、0-RTTでは次のような考え方が重要になります。
- 再送されても困らないデータだけを0-RTTで送る
- 購入、送金、予約確定、パスワード変更のような状態変更操作は避ける
- サーバー側でリプレイ対策を行う
- HTTPでは425 Too EarlyやEarly-Dataヘッダーの意味を理解する
- TLS層だけでなく、アプリケーション側の設計まで含めて考える
0-RTTは、TLS 1.3の中でも「便利さ」と「安全性のトレードオフ」が見えやすい機能です。
TLS 1.3を学ぶときは、単に速くなった点だけでなく、どのような前提で使うべきかまで意識することが大切です。
次の章では、TLS 1.3全体を少し引いて見ながら、「TLS 1.3で守れること」と「TLS 1.3だけでは守れないこと」を整理します。
10. TLS 1.3で守れること・守れないこと
この章では、TLS 1.3を過信しないための注意点を整理します。
TLS 1.3は通信経路を守る重要な仕組みですが、サービス全体の安全をすべて保証するものではありません。
ここまで、TLS 1.3の中で証明書、デジタル署名、鍵共有、HKDF、AEADなどがどのように組み合わされるのかを見てきました。
ここまで読むと、TLS 1.3はかなり頼もしい仕組みに見えると思います。
実際、TLS 1.3はインターネット上で安全な通信路を作るために重要なプロトコルです。
ただし、ここで一度立ち止まりたいポイントがあります。
TLS 1.3は、通信経路を守る仕組みであって、サービス全体を丸ごと安全にする魔法ではない という点です。
たとえば、鍵マークの付いたサイトにアクセスしていても、そのサイト自体が偽サイトであれば、利用者は自分から偽サイトに情報を入力してしまうかもしれません。
また、通信経路が安全でも、サーバー側のアプリケーションに脆弱性があれば、別の経路から情報が漏れる可能性もあります。
この章では、TLS 1.3を正しく評価するために、守れること と 守れないこと を分けて整理します。
10.1 まず全体像を見る
TLS 1.3で守れる範囲と、TLS 1.3だけでは守れない範囲をざっくり分けると、次のようになります。
| 観点 | TLS 1.3で主に守れること | TLS 1.3だけでは守れないこと |
|---|---|---|
| 通信経路 | 盗聴、改ざん、通信相手のなりすましを防ぎやすくする | サーバーや端末の中で起きる問題 |
| 接続先確認 | 証明書に基づいて、接続先がそのドメインのサーバーか確認する | ドメイン名自体が紛らわしい偽サイトであること |
| 通信内容 | 通信中のHTTPデータを暗号化し、改ざんを検知する | サーバーに届いた後のデータの扱い |
| 鍵管理 | 通信ごとの鍵を安全に準備し、用途別に導出する | 秘密鍵の漏えい、証明書運用ミスを自動で解決すること |
| アプリケーション | 通信路を安全にする | 認可ミス、SQLインジェクション、XSS、脆弱な依存関係など |
| 利用者行動 | 正しい接続先との通信を守る | 利用者が偽サイトに情報を入力すること |
図にすると、TLS 1.3が主に守るのは、次の赤枠のような 通信経路 の部分です。
TLS 1.3は、ネットワーク上の攻撃者に対して強い防御を提供します。
一方で、アプリケーションの設計ミスや、利用者が偽サイトに情報を入力してしまう問題は、TLSとは別の対策が必要になります。
10.2 TLS 1.3で守れること
RFC 8446では、TLSの主な目的は、通信する2者の間に安全な通信路を提供することだと説明されています。
具体的には、サーバー認証、通信内容の機密性、通信内容の完全性が挙げられています。
参考: RFC 8446 - Section 1: Introduction
ここでのポイントは、TLS 1.3が ネットワーク上の攻撃者 を強く意識していることです。
たとえば、攻撃者が通信経路を完全に観察したり、途中のデータを書き換えようとしたりしても、通信内容を読んだり、気づかれずに改ざんしたりすることを難しくします。
TLS 1.3で主に守れることを整理すると、次のようになります。
| TLS 1.3で守れること | どの章で扱ったか | 関係する主な仕組み |
|---|---|---|
| 接続先サーバーが本物か確認する | 4章 | 証明書、証明書チェーン、CertificateVerify |
| 通信用の秘密を安全に準備する | 5章 | ECDHE、PSK、Forward Secrecy |
| 用途別の鍵を安全に導出する | 6章 | HKDF、Key Schedule、transcript hash |
| 通信内容を第三者に読まれにくくする | 7章 | AEAD、Record Protocol |
| 通信内容の改ざんを検知する | 7章 | AEAD、additional data、認証タグ |
| 古い方式による危険を減らす | 8章 | AEAD中心化、静的RSA/静的DHの削除 |
RFC 9325でも、TLSを使って実現したいセキュリティサービスとして、機密性、データ完全性、認証が整理されています。
参考: RFC 9325 - Section 5.1: Security Services
つまり、TLS 1.3は 通信中のデータを守るための土台 として非常に重要です。
10.3 鍵マークは「そのサイト全体が安全」という意味ではない
ブラウザで鍵マークを見ると、「このサイトは安全」と感じるかもしれません。
しかし、鍵マークが示していることは、もう少し限定的です。
RFC 9110では、https URIにおける “secured” とは、対象の authority に対してサーバーが認証され、HTTP通信が機密性と完全性の保護を受けていることを意味すると説明されています。
参考: RFC 9110 - Section 4.2.2: https URI Scheme
ここで大切なのは、証明書は「そのドメインに対する接続」を確認するものであり、そのサイトの内容や運営者の善悪を保証するものではない という点です。
たとえば、次のようなサイトを考えます。
| URLの例 | 注意点 |
|---|---|
https://example-bank.com |
本物の銀行サイトかもしれない |
https://example-bank-login.example |
見た目は似ていても、別ドメインの可能性がある |
https://example-bank.com.evil.example |
example-bank.com を含んでいても、実際の親ドメインは evil.example
|
https://xn--... |
国際化ドメイン名により、見た目が似た文字が使われる可能性がある |
TLSは、接続しているドメインに対して証明書を検証します。
しかし、利用者が紛らわしいドメインを本物だと思い込んでしまった場合、TLSはその勘違いまでは直してくれません。
URLを見るときは、https:// だけではなく、本当に意図したドメイン名か も確認する必要があります。
from urllib.parse import urlparse
urls = [
"https://example-bank.com/login",
"https://example-bank.com.evil.example/login",
"http://example-bank.com/login",
]
expected_host = "example-bank.com"
for url in urls:
parsed = urlparse(url)
# scheme は通信方式を表す。
# https であれば、HTTP通信をTLSで保護する前提になる。
uses_https = parsed.scheme == "https"
# hostname は実際に接続しようとしているホスト名を表す。
# 文字列に期待ドメインが含まれるかではなく、完全一致で確認する。
host_matches = parsed.hostname == expected_host
print(url)
print(" httpsを使っているか:", uses_https)
print(" 期待したホスト名か:", host_matches)
このコードは、URL確認の考え方を示すための簡単な例です。
実際のブラウザやアプリでは、証明書検証、リダイレクト、HSTS、Cookieのスコープ、国際化ドメイン名など、さらに多くの要素が関係します。
💡 豆知識
https://example-bank.com.evil.exampleのようなURLは、途中にexample-bank.comが入っていても、実際の登録ドメインは別物です。
「それっぽい文字列が含まれているか」ではなく、「どのホストに接続しているか」を見ることが大切です。
10.4 サーバーや端末が侵害されている場合は守れない
TLS 1.3は、通信経路上の盗聴や改ざんに対して強い防御を提供します。
しかし、通信の両端であるクライアントやサーバー自体が侵害されている場合、TLSだけでは守れません。
たとえば、次のようなケースです。
| 状況 | TLS 1.3でどうなるか | 追加で必要になる対策 |
|---|---|---|
| 利用者の端末がマルウェアに感染している | 暗号化前・復号後の情報を盗まれる可能性がある | 端末保護、EDR、OS更新、権限管理 |
| サーバーが侵害されている | サーバーに届いた後のデータが盗まれる可能性がある | サーバー防御、監視、アクセス制御、ログ分析 |
| アプリが入力値を危険に処理している | 通信経路は安全でも、アプリ内部で攻撃が成立する可能性がある | セキュアコーディング、入力検証、WAF、脆弱性診断 |
| 秘密鍵が漏えいしている | 証明書や鍵の信頼性に影響が出る | 鍵管理、証明書失効、鍵ローテーション |
たとえるなら、TLS 1.3は「配送中の荷物を守る頑丈な箱」です。
配送中に中身を見られたり、すり替えられたりしにくくします。
しかし、送り主の家や受け取り先の倉庫がすでに侵入されていれば、箱が頑丈でも中身は守れません。
TLSが守るのは、主に 配送中 の部分です。
10.5 アプリケーションの脆弱性は別の問題
TLS 1.3を正しく使っていても、Webアプリケーションそのものに脆弱性があると、情報漏えいや不正操作につながる可能性があります。
OWASP Top 10は、Webアプリケーションにおける重要なセキュリティリスクを整理した文書です。
2025年版では、Broken Access Control、Security Misconfiguration、Software Supply Chain Failures、Cryptographic Failures、Injection、Insecure Design、Authentication Failuresなどが挙げられています。
参考: OWASP Top 10:2025
これらは、TLS 1.3だけでは解決できない問題です。
| アプリケーション側の問題 | ざっくりした説明 | TLS 1.3で解決できるか |
|---|---|---|
| アクセス制御の不備 | 他人のデータを見られる、管理者機能を使える | 解決できない |
| インジェクション | 入力値を悪用してDBやコマンドを操作される | 解決できない |
| 認証処理の不備 | パスワードリセットやセッション管理が弱い | 解決できない |
| 設定ミス | デバッグ情報の公開、不要な機能の有効化 | 解決できない |
| 脆弱な依存関係 | 古いライブラリやサプライチェーンの問題 | 解決できない |
| 暗号の使い方のミス | 鍵管理不備、弱い方式、証明書検証の省略 | 一部関係するが、運用・実装側の問題 |
ここで大切なのは、TLS 1.3とアプリケーションセキュリティを対立させて考えないことです。
TLS 1.3は通信を守るために必要ですが、それだけでWebサービス全体が安全になるわけではありません。
10.6 証明書や設定の運用も重要
TLS 1.3の設計が安全でも、設定や運用を間違えると安全性は下がります。
たとえば、次のような点は実運用で重要になります。
| 運用上の観点 | 注意したいこと |
|---|---|
| 証明書の有効期限 | 期限切れになると、利用者が警告を受ける |
| 秘密鍵の保護 | 秘密鍵が漏れると、なりすましや復号リスクにつながる |
| 古いTLSバージョン | TLS 1.0 / TLS 1.1は使わない |
| 暗号スイート | 推奨される方式を使い、古い方式を避ける |
| 証明書チェーン | 中間証明書の設定ミスで検証に失敗することがある |
| ライブラリ更新 | TLSライブラリやサーバーソフトウェアの脆弱性に対応する |
| 鍵の更新 | 長期運用では鍵更新や証明書更新の手順を整える |
RFC 9325では、TLS 1.0とTLS 1.1をネゴシエートしてはならないこと、TLS 1.3をサポートする場合はTLS 1.2以前より優先してネゴシエートすべきことが示されています。
参考: RFC 9325 - Section 3.1: Protocol Versions
TLSは一度設定したら終わりではありません。
証明書の更新、ライブラリ更新、暗号方式の移行、設定の見直しを続ける必要があります。
💡 豆知識
TLSは「暗号技術」だけでなく「運用技術」でもあります。
どれだけ強い暗号方式を使っていても、証明書の期限切れ、秘密鍵の漏えい、古い設定の放置があると、実際の安全性は下がります。
10.7 メタデータや通信量まですべて隠せるわけではない
TLS 1.3は通信内容を暗号化しますが、通信に関する情報をすべて隠すわけではありません。
RFC 8446では、TLSは送信するデータの長さを隠さない一方、TLSレコードにpaddingを入れることで長さを分かりにくくできると説明されています。
参考: RFC 8446 - Section 1: Introduction
たとえば、次のような情報は、TLSだけでは完全には隠れない場合があります。
| 情報 | TLS 1.3でどうなるか |
|---|---|
| 接続先IPアドレス | 通信のためにネットワーク上で見える |
| 通信量 | 暗号化されても、おおよそのサイズやタイミングは観測される可能性がある |
| 通信タイミング | いつ通信したかは観測される可能性がある |
| DNS問い合わせ | 通常のDNSでは別途見える可能性がある |
| SNI | 多くの環境では接続先ホスト名の情報が見える場合がある |
このような情報は、メタデータ と呼ばれることがあります。
メタデータは本文そのものではありませんが、「いつ、どこに、どのくらい通信したか」という手がかりになります。
TLS 1.3は本文の保護には強力ですが、通信パターンまで完全に隠すものではないことを押さえておくと、より正確に理解できます。
10.8 TLS 1.3を正しく評価するための見方
ここまでの内容を踏まえると、TLS 1.3は次のように評価すると分かりやすいです。
| 見方 | 内容 |
|---|---|
| TLS 1.3は必要か | 通信経路を守るために非常に重要 |
| TLS 1.3だけで十分か | それだけでは不十分 |
| 何と組み合わせるべきか | アプリケーション防御、認証設計、アクセス制御、ログ監視、鍵管理、端末保護 |
| 何を過信しないべきか | 鍵マーク、https://、証明書、暗号化済みという表示 |
| 何を確認すべきか | 正しいドメインか、証明書警告が出ていないか、サービス自体が信頼できるか |
TLS 1.3は、インターネット上の通信を安全にするためのとても重要な仕組みです。
しかし、TLS 1.3を正しく理解するには、「何を守るのか」と同じくらい、「何は守れないのか」を知ることも大切です。
10.9 この章のまとめ
この章では、TLS 1.3で守れることと、TLS 1.3だけでは守れないことを整理しました。
TLS 1.3は、通信経路上の盗聴、改ざん、なりすましに対して強い防御を提供します。
証明書、デジタル署名、鍵共有、HKDF、AEADなどを組み合わせることで、安全な通信路を作ります。
一方で、TLS 1.3だけでは、次のような問題までは解決できません。
- フィッシングサイトに利用者が情報を入力してしまうこと
- サーバーや端末が侵害されていること
- アプリケーションの認可ミスやインジェクション脆弱性
- 証明書や秘密鍵の運用ミス
- メタデータや通信量の観測
- パスワードの使い回しや弱い認証設計
TLS 1.3は、サービス全体の安全を支える重要な土台です。
ただし、土台だけで家全体が完成するわけではありません。
安全なWebサービスを作るには、TLS 1.3に加えて、アプリケーション設計、認証・認可、鍵管理、ログ監視、脆弱性対応、利用者への注意喚起などを組み合わせて考える必要があります。
次の章では、ここまで見てきた内容を振り返り、TLS 1.3を「暗号技術の組み合わせ」として整理します。
11. まとめ
最後に、TLS 1.3を「暗号技術の組み合わせ」という視点で振り返ります。
この記事で見てきた内容を、今後ほかの暗号技術を学ぶときにも使える形に整理します。
この記事では、ネットショッピング、スマホアプリ、カフェや大学のWi-FiからのWebアクセスといった身近な場面から出発し、TLS 1.3の中で暗号技術がどのように組み合わされているのかを整理しました。
TLS 1.3は、よく「通信を暗号化する仕組み」と説明されます。
もちろんそれは間違いではありません。
ただ、この記事で見てきたように、TLS 1.3は暗号化だけで成り立っているわけではありません。
実際には、サーバーの認証、通信用の鍵の準備、用途別の鍵導出、通信データの暗号化、改ざん検知など、複数の処理が順番に組み合わさっています。
図にすると、この記事全体の流れは次のように整理できます。
11.1 TLS 1.3は「暗号化だけ」ではない
TLS 1.3を理解するときに、まず大切なのは 暗号化だけに注目しすぎないこと です。
通信内容を第三者に読まれないようにする機密性は、とても重要です。
しかし、通信内容を隠すだけでは、安全な通信とは言い切れません。
たとえば、次のような確認も必要になります。
| 確認したいこと | TLS 1.3で関係する仕組み | やさしく言うと |
|---|---|---|
| 接続先は本物か | 証明書、証明書チェーン、CertificateVerify | 本人確認をする |
| 通信用の鍵を安全に準備できるか | ECDHE、PSK | 合鍵を安全に作る |
| 鍵を用途ごとに分けられるか | HKDF、Key Schedule | 1つの材料から用途別の鍵を作る |
| 通信内容を読まれないか | AEAD、Record Protocol | 中身を隠す |
| 通信内容が書き換えられていないか | AEAD、Finished、HMAC | 封印が破られていないか確認する |
TLS 1.3の仕様であるRFC 8446でも、TLSは安全な通信路を提供するプロトコルとして、認証、機密性、完全性を扱っています。
参考: RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3
つまり、TLS 1.3は「通信を暗号化する技術」というより、安全な通信路を作るために暗号技術を組み合わせるプロトコル と見る方が自然です。
11.2 それぞれの暗号技術には役割がある
この記事では、証明書、デジタル署名、ECDHE、PSK、HKDF、AEADなどを順番に見てきました。
最初は用語が多くて難しく感じるかもしれません。
ただし、1つずつ役割で分けると、かなり見通しがよくなります。
| 技術・仕組み | TLS 1.3での主な役割 |
|---|---|
| 証明書 | サーバーの公開鍵とドメイン名などを結びつける |
| 証明書チェーン | 信頼できる認証局からサーバー証明書までのつながりを確認する |
| デジタル署名 | サーバーが秘密鍵を持っていることや、証明書の正当性を確認する |
| ECDHE | 通信ごとに一時的な共有秘密を作る |
| PSK | 再接続時に以前の接続情報を使って効率化する |
| HKDF | 共有秘密から用途別の秘密や鍵を導出する |
| AEAD | 通信内容の暗号化と改ざん検知をまとめて行う |
| Record Protocol | アプリケーションデータをレコード単位で保護して送る |
ここで大切なのは、どれか1つの技術だけでTLS 1.3が成り立っているわけではないことです。
証明書だけでは通信内容は隠せません。
鍵共有だけでは、接続先が本物かまでは確認できません。
暗号化だけでは、使い方によっては改ざん検知が十分でない場合があります。
TLS 1.3では、それぞれの技術が得意な役割を担当し、全体として安全な通信路を作っています。
💡 豆知識
TLS 1.3の暗号スイート名には、TLS 1.2以前のように鍵交換や認証方式まで全部は入りません。
たとえばTLS_AES_128_GCM_SHA256は、主にRecord保護で使うAEAD方式と、HKDFなどで使うハッシュ関数を表しています。
つまり、TLS 1.3では暗号スイート名の読み方も少し整理されています。
11.3 TLS 1.3で特に押さえたい改善点
TLS 1.3は、TLS 1.2以前を単純に少し改良しただけではありません。
古くなった選択肢を整理し、より安全に使いやすい形に設計し直されています。
特に押さえておきたい改善点は、次のあたりです。
| 観点 | TLS 1.3での整理 |
|---|---|
| 古い暗号方式 | 古い対称暗号方式が整理され、残る方式はAEADになった |
| 鍵交換 | 静的RSAや静的DHによる鍵交換が削除された |
| Forward Secrecy | 公開鍵ベースの鍵交換はForward Secrecyを提供する形になった |
| 鍵導出 | HKDFベースのKey Scheduleに整理された |
| ハンドシェイク | 多くの場合、1-RTTで接続を始められるようになった |
| ハンドシェイクの保護 | ServerHello以降の多くのハンドシェイクメッセージが暗号化される |
| 再接続 | 0-RTTにより早期データを送れる場面があるが、リプレイに注意が必要 |
RFC 8446では、TLS 1.2以前との差分として、静的RSA・静的DH暗号スイートの削除、古い対称暗号方式の削除、HKDFの採用、0-RTTの追加などが整理されています。
参考: RFC 8446 - Appendix D. TLS 1.2 Compatibility
ただし、ここで注意したいのは、TLS 1.3を「新しいから何でも安全」と覚えないことです。
大切なのは、安全でない選択肢を減らし、暗号技術の役割分担をより整理した設計になっている と理解することです。
11.4 0-RTTは便利だが、使いどころを選ぶ
TLS 1.3の特徴として、0-RTTも紹介しました。
0-RTTは、以前接続したことがあるサーバーに対して、再接続時に一部のデータを早く送れる仕組みです。
遅延を減らせるため、Webサービスの体感速度に関係する場面があります。
ただし、0-RTTは通常の1-RTTデータと同じ安全性を持つわけではありません。
特に、リプレイ攻撃に注意が必要です。
たとえば、商品一覧の取得のように、同じリクエストが複数回処理されても大きな問題になりにくい処理であれば、0-RTTを検討できる場合があります。
一方で、購入、送金、パスワード変更のように、同じ操作が繰り返されると困る処理には向きません。
HTTPでEarly Dataを扱うRFC 8470でも、サーバーが早期データとして処理したくないリクエストを拒否するための 425 Too Early が定義されています。
参考: RFC 8470 - Using Early Data in HTTP
ここから分かるのは、TLS 1.3の機能は「あるから使う」のではなく、アプリケーションの性質に合わせて使う必要がある ということです。
11.5 TLS 1.3にも守れない範囲がある
TLS 1.3は、通信経路を守るためにとても重要です。
しかし、TLS 1.3だけでサービス全体が安全になるわけではありません。
たとえば、TLS 1.3では次のような問題までは自動的に解決できません。
| TLS 1.3だけでは解決できないこと | 理由 |
|---|---|
| フィッシングサイト | 通信先がTLSを使っていても、サイト自体が偽物の可能性がある |
| サーバー侵害 | 復号後のデータはサーバー上で処理されるため |
| 端末のマルウェア感染 | 暗号化前・復号後のデータを端末側で見られる可能性がある |
| アプリケーション脆弱性 | 認可ミス、インジェクション、設定不備などは別途対策が必要 |
| パスワードの使い回し | 通信路が安全でも、認証情報そのものの扱いに問題がある場合がある |
| 鍵管理の失敗 | 秘密鍵の漏えいや証明書運用ミスはTLSの安全性に影響する |
ブラウザの鍵マークや https:// は大切な確認材料です。
しかし、それだけで「このサイト全体が安全」と判断するのは危険です。
RFC 9110でも、https URIにおける “secured” は、対象サーバーの認証とHTTP通信の機密性・完全性保護に関係するものとして説明されています。
参考: RFC 9110 - HTTP Semantics: https URI Scheme
TLS 1.3は、あくまで安全なWebサービスを支える土台の1つです。
その上に、認証・認可、入力検証、ログ監視、脆弱性対応、鍵管理、利用者への注意喚起などを重ねていく必要があります。
11.6 この記事の位置づけ
この記事では、TLS 1.3の仕様をすべて細かく説明することは目的にしていません。
RFC 8446を完全に読み解くには、ハンドシェイクメッセージ、拡張、Key Schedule、Record Protocol、エラー処理、互換性、セキュリティ性質など、かなり多くの前提知識が必要になります。
そのため、本記事ではまず次の視点に絞りました。
- TLS 1.3は何を守るのか
- どの暗号技術が登場するのか
- それぞれの暗号技術はどこで使われるのか
- TLS 1.2以前から何が整理されたのか
- TLS 1.3を過信しないために何を知るべきか
この見方を持っておくと、今後より詳しい仕様や実装を読むときに、迷子になりにくくなります。
たとえば、RFC 8446で CertificateVerify を読んだときは、「サーバーが証明書に対応する秘密鍵を持っていることを示す部分」と考えられます。
HKDF-Expand-Label を読んだときは、「用途ごとの秘密を作るためのTLS 1.3専用のラベル付き鍵導出」と考えられます。
TLSCiphertext を読んだときは、「Record ProtocolでAEAD保護された通信データ」と考えられます。
細かい仕様名をすべて暗記するよりも、何のための処理なのか を先に押さえる方が、理解しやすくなります。
11.7 暗号技術全体の地図に戻る
TLS 1.3を学ぶと、暗号技術は単体で使うものではなく、目的に応じて組み合わせるものだと分かります。
これは、暗号技術全体を学ぶ上でも大切な視点です。
- 共通鍵暗号は、大量のデータを効率よく保護する
- 公開鍵暗号系の仕組みは、認証や鍵共有の土台になる
- ハッシュ関数は、データの指紋や鍵導出などに関係する
- MACやAEADは、改ざん検知や認証に関係する
- デジタル署名は、公開鍵で検証できる本人確認や改ざん検知に使われる
- 鍵共有やKEMは、安全に通信を始めるための準備になる
TLS 1.3は、これらの技術が実際の通信でどう協力しているのかを学ぶ、とても良い題材です。
暗号技術全体の地図をもう一度確認したい場合は、以下の記事で、共通鍵暗号、公開鍵暗号、ハッシュ関数、MAC、デジタル署名、鍵共有などの関係を整理しています。
- 関連記事: スマホ決済の裏側から見る、情報セキュリティを支える暗号技術の全体像
また、この記事からさらに深掘りするなら、次のようなテーマにつなげやすいです。
| 深掘りテーマ | この記事とのつながり |
|---|---|
| 証明書とPKI | サーバー認証の仕組みを詳しく理解する |
| デジタル署名 | CertificateVerifyや証明書署名の意味を深掘りする |
| ECDHE | Forward Secrecyを支える鍵共有を理解する |
| HKDF | TLS 1.3のKey Scheduleをより詳しく読む |
| AEAD / AES-GCM | Record Protocolで使われる暗号化と改ざん検知を深掘りする |
| 0-RTT | 高速化とリプレイ攻撃のトレードオフを理解する |
| TLS運用 | 安全な設定、証明書管理、バージョン移行を学ぶ |
11.8 最後に
TLS 1.3は、普段のWeb利用の裏側で動いているため、利用者としてはあまり意識することがありません。
しかし中身を見ていくと、証明書、デジタル署名、鍵共有、HKDF、AEADなど、暗号技術の基本がきれいに組み合わされていることが分かります。
暗号技術は、1つの方式だけを覚えてもなかなか実感が湧きにくい分野です。
一方で、TLS 1.3のような実際のプロトコルを題材にすると、「この技術はここで使われているのか」と具体的に理解しやすくなります。
この記事が、TLS 1.3そのものの理解だけでなく、暗号技術全体を学ぶための足がかりになればうれしいです。