RFC 6347 - 4.2. DTLS ハンドシェイクプロトコル
4.2. The DTLS Handshake Protocol
DTLS は TLS と同じハンドシェイクメッセージとフローを用いるが, 主に次の 3 点が異なる:
-
拒否サービス攻撃を防ぐためのステートレス cookie 交換が追加された.
-
メッセージ損失, 並べ替え, DTLS メッセージの分断 (IP 分断回避) に対応するためハンドシェイクヘッダが変更された.
-
メッセージ損失に対処する再送タイマーが用いられる.
これらを除き, DTLS のメッセージ形式, フロー, 論理は TLS 1.2 と同じである.
4.2.1. Denial-of-Service Countermeasures
データグラムセキュリティプロトコルは様々な DoS 攻撃に極めて弱い. 特に注意すべき攻撃が 2 つある:
-
攻撃者が一連のハンドシェイク開始要求を送りサーバ資源を過剰消費し, 状態割当てや高コストな暗号演算を誘発しうる.
-
攻撃者が被害者を偽った送信元で接続開始メッセージを送りサーバを増幅器として使う. サーバは次のメッセージ (DTLS では大きくなりうる Certificate) を被害者へ送り洪水攻撃となる.
両攻撃に対抗するため, DTLS は Photuris [PHOTURIS] と IKE [IKEv2] のステートレス cookie 手法を借用する. クライアントが ClientHello を送ると, サーバは HelloVerifyRequest を返してよい (MAY). これには [PHOTURIS] の手法で生成したステートレス cookie が含まれる. クライアントは cookie を付けた ClientHello を再送しなければならない (MUST). サーバは cookie を検証し, 有効な場合のみハンドシェイクを続ける. これにより攻撃者/クライアントは cookie を受信できなければならず, IP 偽装による DoS を困難にする. 正当な IP からの DoS には防御しない.
交換を以下に示す:
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie)
[Rest of handshake]
したがって DTLS は ClientHello に cookie 値を追加する.
struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
opaque cookie<0..2^8-1>; // New field
CipherSuite cipher_suites<2..2^16-1>;
CompressionMethod compression_methods<1..2^8-1>;
} ClientHello;
最初の ClientHello 送信時はクライアントはまだ cookie を持たない. この場合 Cookie フィールドは空 (長さ 0) にする.
HelloVerifyRequest の定義は次のとおり:
struct {
ProtocolVersion server_version;
opaque cookie<0..2^8-1>;
} HelloVerifyRequest;
HelloVerifyRequest のメッセージ型は hello_verify_request(3) である.
server_version は TLS と同じ構文だが, 初期ハンドシェイクで版交渉を要しないようにするため, DTLS 1.2 サーバ実装は交渉見込みの TLS 版に関わらず DTLS 1.0 を使うべき (SHOULD) である. DTLS 1.2/1.0 クライアントは版をパケット形式表示 (1.2 と 1.0 で同じ) にのみ用い, 版交渉の一部としては使ってはならない (MUST). 特に DTLS 1.2 クライアントは, HelloVerifyRequest が 1.0 だからといってサーバが DTLS 1.2 でないと推定したり, 最終的に 1.2 ではなく 1.0 に交渉すると仮定してはならない (MUST NOT).
HelloVerifyRequest への応答では, クライアントは元の ClientHello と同じパラメータ (version, random, session_id, cipher_suites, compression_method) を使わなければならない (MUST). サーバは cookie 生成と受信時検証にそれらを使うべき (SHOULD) である. HelloVerifyRequest の版番号は ServerHello を送る場合と同じでなければならない (MUST). ServerHello 受信後, クライアントはサーバの版値が一致することを検証しなければならない (MUST). 複数 HelloVerifyRequest によるシーケンス重複を避けるため, サーバは ClientHello のレコードシーケンス番号を HelloVerifyRequest のレコードシーケンス番号に使わなければならない (MUST).
注: 本仕様は将来の柔軟性のため cookie 上限を 255 バイトに拡大する. 以前の DTLS では 32 のままである.
DTLS サーバはサーバ上にクライアントごとの状態を残さず検証できるよう cookie を生成すべき (SHOULD) である. 乱数秘密を持ち次のように生成する手法がある:
Cookie = HMAC(Secret, Client-IP, Client-Parameters)
2 番目の ClientHello を受信するとサーバは Cookie が有効でその IP でパケットを受信できることを検証できる. 複数 cookie 交換での重複を避けるため, サーバは ClientHello のレコードシーケンス番号を初期 ServerHello のレコードシーケンス番号に使わなければならない (MUST). 以降の ServerHello は状態作成後にのみ送られ, 通常どおり増分しなければならない (MUST).
攻撃者が複数アドレスから cookie を集め再利用する攻撃がありうる. サーバは Secret を頻繁に変えて無効化できる. 正当クライアントが移行中もハンドシェイクできるようにするなら (例: Secret 1 の cookie を受け取った後にサーバが Secret 2 に変わってから 2 番目の ClientHello を送る), 両秘密をしばらく受け入れる窓を設けてよい.[IKEv2] は検出のため cookie に版番号を付けることを提案する. 両方で検証を試すだけでもよい.
DTLS サーバは新規ハンドシェイクのたびに cookie 交換を行うべき (SHOULD) である. 増幅が問題にならない環境では交換を行わない設定にしてよい (MAY). デフォルトは行うべき (SHOULD) である. またセッション再開時は交換しなくてよい (MAY). クライアントは毎回のハンドシェイクで cookie 交換に備えなければならない (MUST).
HelloVerifyRequest を使う場合, 最初の ClientHello と HelloVerifyRequest は handshake_messages (CertificateVerify 用) と verify_data (Finished 用) の計算に含めない.
無効な cookie の ClientHello を受けたサーバは, cookie なしの ClientHello と同様に扱うべき (SHOULD) である. クライアントが誤った cookie を得た場合 (サーバが署名鍵を変えたなど) の競合/デッドロックを避ける.
実装者へ: 異なる cookie の複数 HelloVerifyRequest がクライアントに届くことがある. 新しい HelloVerifyRequest に応じて cookie 付きの新 ClientHello を送るべき (SHOULD) である.
4.2.2. Handshake Message Format
メッセージ損失, 並べ替え, 分断を支えるため, DTLS は TLS 1.2 のハンドシェイクヘッダを変更する:
struct {
HandshakeType msg_type;
uint24 length;
uint16 message_seq; // New field
uint24 fragment_offset; // New field
uint24 fragment_length; // New field
select (HandshakeType) {
case hello_request: HelloRequest;
case client_hello: ClientHello;
case hello_verify_request: HelloVerifyRequest; // New type
case server_hello: ServerHello;
case certificate:Certificate;
case server_key_exchange: ServerKeyExchange;
case certificate_request: CertificateRequest;
case server_hello_done:ServerHelloDone;
case certificate_verify: CertificateVerify;
case client_key_exchange: ClientKeyExchange;
case finished: Finished;
} body;
} Handshake;
各側が各ハンドシェイクで最初に送るメッセージは常に message_seq = 0 である. 新メッセージごとに message_seq を 1 増やす. 再ハンドシェイクでは HelloRequest が message_seq = 0, ServerHello が message_seq = 1 となる. 再送では同じ message_seq を使う. 例:
Client Server
------ ------
ClientHello (seq=0) ------>
X<-- HelloVerifyRequest (seq=0)
(lost)
[Timer Expires]
ClientHello (seq=0) ------>
(retransmit)
<------ HelloVerifyRequest (seq=0)
ClientHello (seq=1) ------>
(with cookie)
<------ ServerHello (seq=1)
<------ Certificate (seq=2)
<------ ServerHelloDone (seq=3)
[Rest of handshake]
ただし DTLS レコード層から見ると再送は新レコードであり, 新しい DTLSPlaintext.sequence_number を持つ.
DTLS 実装は (少なくとも概念的に) next_receive_seq カウンタを維持する. 初期値は 0. 受信メッセージのシーケンスが next_receive_seq と一致すれば増分して処理する. 小さければ破棄しなければならない (MUST). 大きければキューに入れるべき (SHOULD) が破棄してもよい (MAY) (空間/帯域のトレードオフ).
4.2.3. Handshake Message Fragmentation and Reassembly
セクション 4.1.1 のとおり, 各 DTLS メッセージは 1 つのトランスポート層データグラムに収めなければならない (MUST). しかしハンドシェイクは最大レコードより大きくなりうる. したがって複数レコードに分けて別々に送り IP 分断を避ける機構を提供する.
送信側はメッセージを N 個の連続範囲に分割する. 範囲は最大ハンドシェイクフラグメントを超えてはならず (MUST NOT), 全体を覆わなければならない (MUST). 重複しないべき (SHOULD NOT). 同一 message_seq で N 個のハンドシェイクメッセージを作る. 各々に fragment_offset (先行フラグメントのバイト数) と fragment_length を付す. 全メッセージの length は元と同じ. 未分断は fragment_offset=0, fragment_length=length の退化ケース.
フラグメントを受けた実装は全体が揃うまでバッファしなければならない (MUST). 重複するフラグメント範囲を扱えなければならない (MUST). PMTU 変化時に小さいフラグメントで再送できる.
TLS と同様, 同一 flight で空間があれば複数ハンドシェイクを同一 DTLS レコードに入れてよい. 同一データグラムに詰める方法は同一レコードか別レコードの 2 通り.
4.2.4. Timeout and Retransmission
DTLS メッセージは下図のように複数の flight に分かれる. 各 flight は複数メッセージから成りうるが, タイムアウトと再送の目的では単一体として扱う.
Client Server
------ ------
ClientHello --------> Flight 1
<------- HelloVerifyRequest Flight 2
ClientHello --------> Flight 3
ServerHello \
Certificate* \
ServerKeyExchange* Flight 4
CertificateRequest* /
<-------- ServerHelloDone /
Certificate* \
ClientKeyExchange \
CertificateVerify* Flight 5
[ChangeCipherSpec] /
Finished --------> /
[ChangeCipherSpec] \ Flight 6
<-------- Finished /
Figure 1. Message Flights for Full Handshake
Client Server
------ ------
ClientHello --------> Flight 1
ServerHello \
[ChangeCipherSpec] Flight 2
<-------- Finished /
[ChangeCipherSpec] \Flight 3
Finished --------> /
Figure 2. Message Flights for Session-Resuming Handshake
(No Cookie Exchange)
DTLS は次の状態機械による単純なタイムアウトと再送を用いる. クライアントが最初に ClientHello を送るため PREPARING から始まる. サーバは WAITING だがバッファは空で再送タイマーはない.
+-----------+
| PREPARING |
+---> | | <--------------------+
| | | |
| +-----------+ |
| | |
| | Buffer next flight |
| | |
| \|/ |
| +-----------+ |
| | | |
| | SENDING |<------------------+ |
| | | | | Send
Receive | | | | HelloRequest
next | | Send flight | |
flight | +--------+ | | or
| | | Set retransmit timer | | Receive
| | \|/ | | HelloRequest
| | +-----------+ | | Send
| | | | | | ClientHello
+--)--| WAITING |-------------------+ |
| | | | Timer expires | |
| | +-----------+ | |
| | | | |
| | | | |
| | +------------------------+ |
| | Read retransmit |
Receive | | |
last | | |
flight | | |
| | |
\|/\|/ |
|
+-----------+ |
| | |
| FINISHED | -------------------------------+
| |
+-----------+
| /|\
| |
| |
+---+
Read retransmit
Retransmit last flight
Figure 3. DTLS Timeout and Retransmission State Machine
状態機械には 3 つの基本状態がある.
PREPARING では次の flight を準備する計算を行い, バッファを空にしてから送信用にバッファし SENDING に入る.
SENDING ではバッファした flight を送る. 送り終えたら, これが最終 flight なら FINISHED へ. まだ受信が続くなら再送タイマーを設定し WAITING へ.
WAITING を抜ける経路は 3 つ:
-
再送タイマー期限: SENDING に移り flight を再送しタイマーをリセットし WAITING に戻る.
-
ピアから再送された flight を読む: SENDING に移り同様に再送しタイマーをリセットし WAITING に戻る. 重複受信は相手のタイムアウトが原因であることが多く, 自分の前 flight の一部損失を示唆する.
-
次の flight を受信: 最終なら FINISHED. 新 flight を送る必要があれば PREPARING. 部分読取りでは状態遷移やタイマーリセットはしない.
クライアントは PREPARING, サーバは空バッファで WAITING から始まる (前述).
サーバが再ハンドシェイクを望めば FINISHED から PREPARING へ移り HelloRequest を送る. クライアントは HelloRequest で FINISHED から PREPARING へ移り ClientHello を送る.
さらに [TCP] のデフォルト MSL の少なくとも 2 倍の間, FINISHED で最後の flight を送った側 (通常ハンドシェイクではサーバ, 再開ではクライアント) は, ピア最終 flight の再送に対し最終 flight を再送しなければならない (MUST). 最終 flight 損失のデッドロックを防ぐ. DTLS 1.0 にも適用され, [DTLS1] に明示はないが状態機械に必須であった. 例: サーバ Finished 損失でサーバは完了と思うが実際は未完了. クライアントは Finished を待ちタイマーで自身の Finished を再送し, サーバが応じて握手完了. 再開時も同様.
損失により一方が他方の Finished を受け取る前にアプリデータを送りうる. 実装はその epoch の Finished を受けるまで新 epoch のアプリデータをすべて破棄するかバッファしなければならない (MUST). 対応 Finished 前に新 epoch のアプリデータを受けたら並べ替え/損失の証拠として最終 flight を直ちに再送してもよい (MAY).
4.2.4.1. Timer Values
タイマー値は実装次第だが誤処理は深刻な輻輳を招く. 初期値は 1 秒 (RFC 6298 [RFC6298] 最小) を使い, 再送ごとに倍々で RFC 6298 最大 60 秒以上まで伸ばすべき (SHOULD). 遅延敏感アプリのため 3 秒デフォルトより 1 秒を推奨. DTLS はデータフローでは再送しないため輻輳影響は小さいはず.
損失のない送信が起きるまで現在値を維持し, その後初期値に戻してよい. 長期アイドル後, 現在値の 10 倍以上で初期化してもよい (大量転送後の再ハンドシェイクなど).
4.2.5. ChangeCipherSpec
TLS と同様 ChangeCipherSpec は技術的にはハンドシェイクメッセージではないが, タイムアウトと再送の目的では対応する Finished と同一 flight として扱わなければならない (MUST). 損失時に ChangeCipherSpec とハンドシェイクメッセージの順序が曖昧になりうる.
現行 TLS モードでは ChangeCipherSpec 前の期待メッセージ集合が状態から予測できるため問題にならない. 将来のモードは曖昧を作らないよう注意しなければならない (MUST).
4.2.6. CertificateVerify and Finished Messages
形式は TLS と同じ. ハッシュは message_seq, fragment_offset, fragment_length を含む全体を対象とする. ただし分断への感度を除くため Finished MAC は各メッセージが単一フラグメントだったかのように計算しなければならない (MUST). cookie 交換を使う場合, 最初の ClientHello と HelloVerifyRequest は CertificateVerify/Finished MAC に含めてはならない (MUST NOT).
4.2.7. Alert Messages
Alert はハンドシェイク中でも再送しない. ただし通常アラートを出す実装は, 問題レコードを再び受けたら新しいアラートを生成すべき (SHOULD) である. ピアが継続的に不正メッセージを送る場合は検知し, 悪挙動後にローカル状態を終了すべき (SHOULD) である.
4.2.8. Establishing New Associations with Existing Parameters
同一ホスト/ポート四元組で繰り返し接続する設定では, クライアントが接続を黙って捨て同じパラメータで再接続 (再起動後など) しうる. サーバには epoch=0 の新ハンドシェイクに見える. 既存関連付けがあると信じるサーバが epoch=0 の ClientHello を受けたら, 新ハンドシェイクを進めるべき (SHOULD) だが, cookie 交換完了または検証可能な Finished を含む完全ハンドシェイクで到達可能性が示されるまで既存関連付けを破壊してはならない (MUST NOT). 正しい Finished 後は重複 epoch を避けるため旧関連付けを破棄しなければならない (MUST). 到達性要件は偽 ClientHello だけで関連付けを壊すオフパス攻撃を防ぐ.