20Alertプロトコルの続きです。
暗号に関する計算
ここでは暗号鍵の生成方法について説明しています。前置きに、「プロトコルを安全にするには強固なアルゴリズムと強力な暗号鍵が必要です。」 と説明があります。
暗号鍵はハンドシェイクの段階で生成されます。
暗号学においては「1つの鍵は1つの用途にしか使わない」というのが原則です。このためTLS 1.3の処理では利用可能な情報があるばあい、常にセキュリティが確保されるように複数の暗号鍵が規定されています。
TLS 1.3の暗号鍵
| 鍵の用途 | 生成される鍵の数 | 説明 |
|---|---|---|
| binder_key (クライアント) | 1 | 事前共有鍵を保持していることの証明に使う鍵 |
| client_early_traffic_secret | 1 | early dataの暗号化に使う鍵(クライアントのみ) |
| early_exporter_master_secret | 1 | 鍵素材をTLSの外部で利用するためのエクスポーター用の鍵(0-RTTの場合) |
| server_handshake_traffic_secret client_handshake_traffic_secret | 2 | ハンドシェイクのトラフィック暗号化に使う鍵。クライアント、サーバーの其々に生成 |
| server_application_traffic_secret_N client_application_traffic_secret_N | 2以上 | アプリケーションのトラフィック暗号化に使用する鍵。クライアント、サーバーの其々で生成。最初に2つ生成し、それを同一の接続で1回の通信毎に変化させる仕組みも利用可能 |
| exporter_master_secret | 1 | 鍵素材をTLSの外部で利用するためのエクスポーター用の鍵 |
| resumption_master_secret | 1 | session resumption で使われる事前共有鍵を生成する鍵 |
暗号に使用する鍵の生成 : 鍵導出(key derivation)
暗号に使用する鍵の生成は 鍵導出(key derivation) と呼ばれ、通常、特定の鍵導出関数(KDF: Key Derivation Function)として実装されます。
KDFの目的は相応にエントロピーがある値が入力された際、その値を鍵として利用できるようにする事にあります。
一般にはKDFに入力した値は、暗号に利用可能なだけの暗号学的な特性を備えていないことがある、不十分な例としてブルートフォース攻撃に弱かったり攻撃者がそもそも一部を知っていたりするパスワードが挙げられます。(類似は個人パスワードの生年月日、みたいなものでしょうか。)
TLS 1.3では鍵導出としてHKDF(HMAC-based key Derivation Function)が採用されています。
HKDF はRFC 5869で規定されています。TLS旧バージョンでは独自のKDFでした。この独自KDFが安全でない、という事ではなく標準化された周知の要素技術を採用した、という事です。
HKDFでは以下の2段階で鍵を導出します。
ステップ1 抽出:extraction 入力から利用可能なエントロピーを取り出す。強力な疑似乱数の鍵が1つ生成される。HKDFではオプションでソルト(salt)も指定できます。
HKDF-Extract(salt, input) -> extracted_key
ステップ2 伸張:expansion ステップ1抽出の出力結果から任意の長さの疑似乱数の鍵を任意の個数導出する。複数の鍵が必要な場合はcontextを指定する。
HKDF-Expand(extracted_key, context, length) -> expanded_key
鍵スケジュール key Schedule
安全な通信のためにアリスとボブに求められることは敵対者に知られていない十分に強力な秘密の情報を1つ共有する事だkです。これまでやり取りが無い相手との間でこれを実現する方法として一般には、DH(Diffie-Hellman)鍵交換という仕組みを使います。
鍵交換を適切な相手と確実に実行するためには認証が必要になりますが、TLS 1.3ではメジャーなサーバー証明書に加えて、session resumptionやパスワード認証もサポートしています。
HKDF-Expand-Labelラッパー関数
TLS 1.3ではHKDFの抽出 extraction のための関数はそのまま使用されますが、伸張 expansion についてはラッパー関数が複数定義されておりこれらを使う事が多いようです。
伸張 expansion のラッパー関数例として HKDF-Expand-Labelがあります。この関数は同一の入力から別々の用途で使う鍵を複数個生成できます。鍵はラベル labelによって区別されます。
HKDF-Expand-Labelラッパー関数の例
# HKDFの入力用の構造
## コンテキストにラベルをつけて一位の鍵を生成
struct {
uint16 length = length;
opaque label<7..255> = "tls1.3" + label;
opaque context<0..255> = context;
} HkdfLabel;
# コンテキストとラベルをパッケージ化してHKDFを起動するラッパー関数
HKDF-Expand_Label(Secret, Label, Context, Length) =
HKDF-Expand(Secret, HkdfLabel, Length)
Derive-Secret ラッパー関数
Derive-Secretラッパー関数の役割は鍵導出の処理を鍵が生成される個々のハンドシェイクに結びつける事です。具体的にはハンドシェイクでやり取りされるメッセージいたいするハッシュ(トランスクリプトハッシュ transcript hash)を使用します。
Derve-Secret(Secret, Label, Messages) =
HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash, Length)
クライアントとサーバーはハンドシェイク時に強力な乱数を用意します。この乱数を使用して1回1回のハンドシェイクを一意にします。別な言い方をすると全く同じ入力があっても乱数により別な値に出来ます。
同様にこの乱数によって鍵導出のラッパー関数も秘密の鍵の元になるインプットが同一でも生成される暗号鍵は一意に保たれます。
実際の鍵の導出は通信の?ブロックごとに鍵の属を生成するような操作として設計されています。
鍵スケジュールの例
最初のブロック
# 抽出extract
事前共有鍵 --> HKDF-Extract --> Early Secret ----①
↑
0(ソルト)
①-- Early Secret--↓
Derive-Secret(Eary Secret, "ext binder" または "res binder","") --> binder_key
①-- Early Secret--↓
Derive-Secret(Eary Secret, "c e traffic", ClientHello) --> client_eary_traffic_secret
①-- Early Secret--↓
Derive-Secret(Eary Secret, "e exp master", ClientHello) --> client_eary_traffic_secret
①-- Early Secret--↓
Derive-Secret(Eary Secret, "derived","")
↓
(Secret Stete)
鍵スケジュールにおける先頭ブロックでは、
事前共有鍵がある場合(以前のセッションが中断した場合等):事前共有鍵を秘密の情報として使用。
事前共有鍵が無い場合:ソルトと同様に空の文字列を事前共有鍵の代わりに使用。
抽出 extraction の段階では、Early Secretという中間の値を生成し、個々から実際に利用する3種類の鍵をそれぞれ伸張 expansion を行います。
最後の状態を再利用されることを防ぐため、さらに(ダミーで)伸張 expansion を1回実行してブロックを終了します。
2つ目のブロック
ECDHE(or DHE)鍵交換の出力値 --> HKDF-Extract --> Handshake Secret ----②
↑
0(ソルト)
②-- Handshake Secret--↓
Derive-Secret(Handshake Secret, "c hs traffic" ClientHello...ServerHello) --> client_handshake_traffic_secret
①-- Handshake Secret--↓
Derive-Secret(Handshak Secret, "c hs traffic" ClientHello...ServerHello) --> Server_handshake_traffic_secret
①-- Handshake Secret--↓
Derive-Secret(Handshak Secret, "derived","")
↓
(Secret State)
2つ目のブロックにおける秘密の情報としては、利用可能な場合はECDHE鍵交換(または)DHE鍵交換の出力値を使用します。利用可能なものが無い場合は、長さゼロの文字列を使います。
もう1つの入力であるHKDFに対するソルトとしては1つ目のブロックの最後の状態(Secret State)を使います。
抽出 extraction の段階ではハンドシェイクシークレット(Handshake Secret)という値を生成し、そこから実際に使用する2種類の鍵の伸張 expansionを行います。
最後の3つ目のブロック
0 --> HKDF-Extract --> Master Secret ----③
↑
0(ソルト)
③-- Master Secret--↓
Derive-Secret(Master Secret, "c ap traffic" ClientHello...サーバーのFinished--> client_application_traffic_secret_0
③-- Master Secret--↓
Derive-Secret(Master Secret, "s ap traffic" ClientHello....サーバーのFinished--> server_application_traffic_secret_0
③-- Master Secret--↓
Derive-Secret(Master Secret, "exp master" ClientHello...サーバーのFinished--> exporter_master_secret
③-- Master Secret--↓
Derive-Secret(Master Secret, "res master" ClientHello...クライアントのFinished--> resumption_master_secret
最後の3つ目のブロックでは秘密の情報は使わず、前のブロックの出力をHKDFへのソルトとして使用してマスターシークレット Master Secret を生成し、そこから実際に使う鍵を必要に応じて伸張 expansion します。
Derive-Secret監修の出力はトランスクリプトハッシュに依存します。上記説明中ではトランスクリプトハッシュは、完遂したハンドシェイクを表すように見えますが、実際の鍵導出はハンドシェイクの進行中に実行されるのでそれぞれの鍵の伸張 expansionに使われるトランスクリプトハッシュの値は同一とは限りしません。
上記でアプリケーションが使う鍵 client_application_traffic_secret_0, server_application_traffic_secret_0 は1つ目に送信されたアプリケーション用の暗号鍵を示しています。この暗号鍵はサーバー・クライアントの双方がKeyUpdateメッセージを使って自身の送信する暗号鍵を更新できます。子の更新は最初のハンドシェイク完遂後ならいつでも実行可能です。次の更新で使う暗号鍵の生成にはHKDF-Expand-Label関数を使用します。
application_traffic_secret_N+1 =
HKDF-Expand-Label(application_traffic_secret_N,
"traffic upd", "", Hash.length)
このほかに必要となる鍵としては、ハンドシェイクの完全性を検証する Finishedメッセージで使用する finished_keyがあります。この鍵は現在のトラフィックの鍵(Basekeyと呼ばれます)からHKDFの伸張関数で生成されます。
finished_key = HKDF-Expand-Label(Basekey, "finished", "", Hash.length)