RFC 4347 (DTLS) について調べてみた

AES-CTRの時に出てきたDTLSについて調べてみた。DTLSはRFC4347になっているようなので、それを読んでみた時の各章ごとのメモ(意訳?)です。

RFC 4347 Datagram Transport Layer Security

1. Introduction

TLSは広く普及しているけどTCPのような信頼性があるチャンネル上でしか使えない。しかし最近ではSIPやゲームのようにUDPを使うことも多くなってきた。このようなケースでは今一つな選択肢しかない。一つ目はIPsecだが、これは使える用途が限られている。二つ目は自分で作ることだが、セキュリティのことを考えると大変でありTLSのように楽ではない。

そのためdatagramでもTLSのようなものがあるのが望ましい。ここではDTLSというTLSに似せて作ったプロトコルについて記載する。

2. Usage Model

カーネルの変更なしにユーザモードで動作するように設計した。datagramでは信頼性や順序保証は必要ないためDTLSではこの性質を踏襲した。
電話やゲームのように遅延が重要なアプリケーションがdatagramを使う。
DTLSを使ったセキュアな通信では、DTLSは再送制御や並べ替えといったことはしないので、アプリケーションの動作を変更する必要はない。

3. Overview of DTLS

TLSがdatagramで使えない理由はパケットが消えたり順番が変わったりするから。この問題をTLSから少しだけ変更することで解決してみた。

TLSをdatagramで使うときの問題点をまとめると次のとおりになる。

  1. TLSではレコードを個別に複合できない。つまりN番目のレコードがなければN+1番目は複合できない。
  2. TLSのhandshakeは必ず届くことを前提にしているので、届かないと破綻してしまう。
3.1. Loss-Insensitive Messaging

TLSのレコードを個別に扱うことができない理由はこの二つ。

  1. 暗号がCBCのようにレコード間の連鎖を利用しているから。
  2. レコード本体には含まれていない暗黙的なシーケンス番号を含むMACを使ってリプレイや順序保証をしているから。

単純な解決方法はIPsec ESPのようにレコードに明示的な状態情報を持たせること。既にTLS 1.1で明示的なCBC情報をレコードに持たせているので*1、DTLSはさらにシーケンス番号も追加する。。

3.2. Providing Reliability for Handshake

TLSのhandshakeは決まった順番にメッセージ交換をしないとダメなので、パケットロスや順序間違いがあるとエラーになってしまう。
handshakeメッセージはdatagramの最大値を超える可能があるので、分割されてしまうかもしれない。

3.2.1. Packet Loss

DTLSでは再送タイマーを使ってパケットロスに対処する。

  1. clientからserverにClientHelloを送る。
  2. serverからclientにHelloVerifyRequestを返す。
  3. clientはしばらく待って返事がないのでダメだったことが分かる。
  4. clientからserverにもう一度ClientHellotを送る。
  5. serverはClientHelloをもう一度受け取ったのでHelloVerifyRequestが届かなかったことが分かる。
  6. serverからclientにもう一度HelloVerifyRequestを送る。

今のはclientの例だったけれど、serverの方も再送タイマーを持っていて、一定時間経ったら再送を行う。
注:HelloVerifyRequestについては再送タイマーは適用されない。なぜならサーバ上に状態を作成することを要求してしまうから*2

3.2.2. Reordering

handshakeでは期待されるメッセージの順番が存在しているので、それを利用して順序制御をする。
順番から外れて先に来てしまったメッセージは、キューに入れておいて後で処理する。

3.2.3. Message Size

handshakeメッセージは理論上では2^24-1バイトまで大きくなる(実際は数キロバイト程度だけど)。UDPは1500バイトを超えるとフラグメント化される可能性がある。DTLSではメッセージにフラグメントの大きさとオフセットを持たせることで、受信側で再構築できるようにする。

3.3. Replay Detection

DTLSではIPsec AH/ESPと同じ方法を使ってリプレイを検出できる*3

リプレイ発生は攻撃の場合が多いけど、他の理由でも発生することなので、アプリケーション側で検出して何とかするべきである。そのためリプレイ検出はoptionalになっている。

4. Differences from TLS

DTLSはTLSに似せてあるのでTLS 1.1との差分だけ記載する。それ以外で何も書かれていない部分はTLS 1.1と同じ。

4.1. Record Layer

DTLSのレコード層がTLS1.1と違うのはシーケンス番号が明示されていること。明示されたシーケンス番号は受信側でMACの検証に使う。

レコードの各変数の意味

type
TLS 1.1と同じ
version
DTLSでは、TLSと衝突しないように0xFFFFからデクリメントしていく形になっている。例えば1.0=(254, 255), 1.1=(254.254), 2.0=(253.255)となる。
epoch
ChangeCipherSpecのたびにインクリメントする値。初期値は0。
sequence_number
このレコードのシーケンス番号。TLSでは暗黙に両者が共有していたが、DTLSではレコードに明示して送信する。
length
TLS1.1と同じ
fragment
TLS1.1と同じ

sequence_numberはChangeCipherSpecの際に0にリセットされる。epochは複数回handshakeした場合に、同じシーケンス番号なのに異なる暗号化をされているものを識別するために存在する。
epochとsequenceの組は、TCP maximum segment lifetimeの2倍の時間の間はユニークでなければならない。ただTLS実装での経験からは、滅多にhandshakeは行わなかったので問題はないと思われる。

4.1.1. Transport Layer Mapping

個々のDTLSレコードは一つのデータグラムに収める必要がある。そのために実装はMTUを検出してMTUより小さいレコードを送信するべきである。また実装はアプリケーションにPMTUの値(datagramの最大サイズ)を知らせるべきである。もしMTUより大きいサイズのデータを送ろうとした場合は、フラグメント化されたパケットを送らないように、エラーを生成するべきである。

IPsecとは異なりレコードはassociation identifierを含んでいない。そのため、アプリケーションはassociation間での多重化を調整しなければならない。これはUDPを使う場合は、hostとport番号の組を使えばおそらく問題ない*4

一つのデータグラムに複数のDTLSレコードが入ることがある。DTLSのレコードはこれらの境界を認識するのに十分な構造を持っている。しかし、データグラムのペイロードの最初のバイトは、レコードの先頭部分である必要がある*5

DCCPのような独自のシーケンス番号を持っているトランスポートを使って伝送する場合、DTLSとトランスポートの二つのシーケンス番号が提供されることになる。これは少し効率が悪いけれど、それぞれ用途が違うため、シンプルにするために両方を使うことにする。将来的には、DTLSの拡張として一つのシーケンス番号だけを使うのを許可する方法が記述されるかもしれない。

DCCPのようなトランスポートではcongestionコントロールを提供する。congenstion windowが十分に狭い場合、DTLSのhandshakeの再送がすぐに行われる状態になり、タイムアウトと誤った再送が行われる可能性がある。
DTLSをこのようなトランスポート上で使う場合、congenstion windowsを超えることがないようにする必要がある。将来的には、DTLS-DCCPの場合の最適化された振る舞いが記述されるかもしれない。

4.1.1.1. PMTU Discovery

一般に、DTLSの哲学はPMTUの問題を避けることである。一般的な戦略として、保守的なMTUで始めて、その後必要な時に値を更新する。

PMTUはインターフェイスのMTUを使って初期化されるだろう。もしfragmentation neededとDFがセットされたICMP Destination Unreachable を受け取った場合、ICMPメッセージから計算した値にPMTUの値を減少させる。

アプリケーションがPMTU discoveryできるように、DFビットの制御とPMTUのリセットを許可するべき。

一つの特別な場合として、handshakeメッセージはDFビットがセットされるかもしれない。なぜなら、ファイアウォールがICMPメッセージを遮断する場合、パケットロスとPMTUが大き過ぎる倍を区別することは難しい。このような状況で接続できるようにするために、実装は大きいパケットを複数回再送した後は、フラグメント化することを選択するかもしれない。一般的には、保守的なMTUの初期値を使うことでこの問題を避けることができる。

4.1.2. Record Payload Protection

TLSのように、DTLSは一連の保護されたレコードを送信する。このセクションの残りでは、フォーマットの詳細について記述する。

4.1.2.1. MAC

DTLSのMACはTLS1.1と同じだが、暗黙のシーケンス番号の変わりに、伝送されてくるepochとシーケンス番号の結合された64ビットの値を使う。DTLSのecpohとシーケンス番号の組はTLSのシーケンス番号と同じ長さになっている。

4.1.2.2. Null or Standard Stream Cipher

DTLSのNULL cipherはTLS1.1と完全に同じ。
TLS1.1ではストリーム暗号はランダムアクセスできないRC4だけだったが、DTLSでは使ってはいけない*6

4.1.2.3. Block Cipher

ブロック暗号はTLS1.1と完全に同じ。

4.1.2.4. New Cipher Suites

よく分からないが、新しいcipher suiteが登録されたらDTLSに使えるかどうか示す必要があると言ってるようだ*7

4.1.2.5. Anti-replay

DTLSのレコードはシーケンス番号を含むことで再送から保護されている。シーケンス番号はスライディングウィンドウ(RFC 2402の 3.4.3から拝借)を使って行われるべきである。

セッションの確立時に受信パケットカウンターを0に初期化する。個々の受信レコードごとに、レコードのシーケンス番号を検証する。セッションでシーケンス番号はユニークなので、重複したレコードは破棄する。

重複したレコードはスライディングウィンドウを使って破棄する。(ウィンドウをどのように実装するかはローカルな問題だけど、以下では実装が必要とする機能を記述する)
最小のウィンドウサイズは32だが、64がデフォルトとしてお勧めである。最初の値よりも大きな値であれば他の値を選んでもよい(受信者側が選択したウィンドウサイズは送信者には通知されない)。

ウィンドウの右端はシーケンス番号の最大値となる。レコードがウィンドウの左端の値よりも小さなシーケンス番号を持っていた場合は破棄する。

パケットがウィンドウ内にあるか、ウィンドウのどの位置にあるかを効率よくチェックする方法として、ビットマスクをベースとした方法(RFC 2401のAppendix C)がある。

受信したレコードがウィンドウ内にあるか、ウィンドウのより右の場合、MACを検証する。MAC検証に失敗した場合はレコードを破棄する。受信ウィンドウはMAC検証が成功した場合だけ更新する。

4.2. The DTLS Handshake Protocol

3つの変更点以外はTLSと同じhandshakeメッセージを使う。

  • DoS攻撃に対処するためにstateless cookieの交換が追加されている。
  • メッセージの紛失、並べ替え、フラグメンテーションを扱うためにヘッダが変更されている。
  • メッセージの紛失を扱うために再送タイマーがある。

これらの例外を除いて、メッセージフォーマットや順序、ロジックはTLS1.1と同じ。

4.2.1. Denial of Service Countermeasures

データグラムを使ったセキュリティプロトコルはDoS攻撃の影響を受けやすい。特に以下の二つが問題となる。

  • handshakeの開始リクエストを送信することで、サーバ上のリソースを過剰に消費させる。サーバは状態を確保して、高価な暗号関係の処理を行う可能性がある。
  • 送信元を犠牲者に偽装したコネクション開始メッセージを送ることで、サーバを増幅器として使う。サーバは次のメッセージ(DTLSではCertificateのようなかなり大きなもの)を犠牲者のマシンに送ってしまう。

これらの攻撃に対抗するために、DTLSはPhoturisとIKEで使われているstateless cookieというテクニックを拝借する。

クライアントはClientHelloを送ると、サーバはHelloVerifyRequestを返信する。このメッセージはstateless cookie (PHOTURISのテクニックを用いて生成された)を含んでいる。クライアントはこのcookieを追加したClientHelloを再度送る必要がある。サーバはこのcookieをチェックして正しかった場合だけhandshakeを行う。

この構造は攻撃者にcookieを受け取ることを強制するため、偽装したIPアドレスを利用したDoS攻撃は困難になる。ただし、この構造は正しいIPアドレスからのDoS攻撃に対する防御は提供しない。

最初にClientHelloを送る時はcookieがないので長さ0で送る。
HelloVerifyRequestに返信する時は、最初のClientHelloと同じパラメータのものを送る。

DTLSサーバはこのような方法(cookie = hmac(Secret, Client-IP, Client-Parameters))でcookieを生成することで、クライアントに関する状態を持たなくてよくなっている。

一つの攻撃の可能性として、異なるアドレスからの複数のcookieを収集し、サーバーを攻撃するためにそれらを再利用する方法がある。サーバーはSecretを周期的に変えてcookieを無効にすることで、この攻撃に対抗することができる。

この方法では正常な相手との通信でも、最初のClientHelloと2回目のClientHelloの間でSecretが変わってしまう可能性がある。IKEv2ではこの問題を検出するためにcookieにバージョン番号をつけている。別の方法として、両方のsecretを試してみるという方法がある。

新しいhandshakeを行う時はcookie exchangeを行うべき。もしサーバが増幅器となってしまうことが問題ではない環境にあるなら、cookie exchangeを行わない設定にしてもよい。しかしながら、デフォルトではcookie exchangeを行うようにするべき。追加でセッションをレジュームするときはcookie exchangeをしなくてもよい。

HelloVerifyRequestが使われた場合、最初のClientHelloとHelloVerifyRequestはFinishedメッセージの計算時には使われない。

4.2.2. Handshake Message Format

メッセージ紛失、並べ替え、フラグメンテーションに対応するため、handshakeヘッダーが変更されている。

最初のメッセージではmesseage_seqは0になっている。新しいメッセージを生成するたびに値をインクリメントする。再送する場合、message_seqの値は同じ値のままとなる。しかしレコードレイヤーでは再送は新しいレコードとなるため、レコードのシーケンス番号は新しい値となる。

DTLSの実装はnext_receive_seqカウンターを保持する。このカウンターの初期値は0となる。メッセージを受信して、シーケンス番号がnext_receive_seqと一致すると、next_receive_seqをインクリメントし、メッセージを処理する。

シーケンス番号がnext_receive_seqより小さい値の場合、メッセージは破棄される。もしシーケンス番号がnext_receive_seqより大きい場合、実装はメッセージをキューするべきだが、破棄してもよい。(これは単純な空間/帯域のトレードオフとなる)

4.2.3. Message Fragmentation and Reassembly

4.1.1で記載したように、DTLSメッセージは一つのdatagramに収める必要がある。しかしながら、handshakeメッセージは最大レコードサイズより大きくなる可能性がある。そのため、DTLSはhandhsakeメッセージを複数のレコードにわたって分割する仕組みを提供する。

handshakeメッセージを送信するとき、送信者はn個の連続するデータに分割する。データはmaximum handshake fragmentサイズより大きくなってはいけない。そして完全なhandshakeメッセージとしなければならない。個々のデータの範囲は重なってはいけない。

送信者がN個のhandshakeメッセージを作成した場合、それらは全てオリジナルのhandshakeメッセージと同じmesssage_seqを持っていなければならない。個々の新しいメッセージは fragment_offset (以前のフラグメントが持つバイト数)とfragment_lengthを持つ。lengthは全てのメッセージでオリジナルのメッセージと同じになる。フラグメントされていないメッセージは fragment_offset=0, fragment_length=lengthとなる。

DTLSの実装はフラグメントされたhandshakeメッセージを受け取った場合、完全なhandshakeメッセージになるまでバッファしなければならない。これは送信者にpath MTU discoveryの間、小さなサイズのフラグメントによるhandhshakeメッセージの再送を許可する。

TLSのように、複数のhandshakeメッセージを同じレコードに収めることができる。

4.2.4. Timeout and Retransmission

DTLSはステートマシンによるシンプルなタイムアウトと再送を使う。(図の説明部分は省略)

4.2.4.1. Timer Values

タイマーの値は実装によって選択されるが、タイマーの誤った扱いは深刻なcongestionの問題を引き起こす。
実装は初期値として1秒を使うべきである。そして個々の再送のたびに最大60秒まで値を倍にしていく。
時間に敏感なアプリケーションの遅延の改善のためにRFC2988の3秒タイマーより1秒タイマーを推奨する。DTLSでは再送はhandshakeだけで、データには使われないので、congestionの影響は最小となる。

実装は現在のタイマー値を保持するべきで、ロスなく送信が行われた時は初期値にリセットしてよい。長いアイドルの後、現在のタイマー値が10秒より小さくなければ、実装はタイマーを初期値にリセットしてよい。これが起こる一つの状況は、多量のデータ転送の後にreshandshakeが行われた時に起こる。

4.2.5. ChangeCipherSpec

TLSと同じく厳密にはhandshakeメッセージではない。しかし関連付けられたFinishedメッセージの一部として扱わなければならない。

4.2.6. Finished Messages

FinishedメッセージはTLSと同じフォーマット。ただフラグメンテーションを気にしなくても良いように、個々のhandshakeメッセージは単一のfragmentとして送信されたとして計算しなければならない。
cookie exchangeが使われた場合、ClientHelloとHelloVerifyRequestはMACの計算に含めない。

4.2.7. Alert Messages

Alertメッセージは再送しない。しかし、問題があるレコード(再送されたhandshakeメッセージのような)を再び受信した場合は新しいalertメッセージを生成するべきである。
相手側が永続的に間違ったメッセージを送ってくることを検出した場合、ローカルの接続状態を終了にするべきである。

4.3. Summary of new syntax

TLS 1.1とDTLSの間で変更されたデータ構造のまとめ。

5. Security Considerations

TLS1.1を変形させたものなので、多くの部分はTLS1.1と同じ。

DTLSの主要なセキュリティの懸念はDoSとなる。DTLSではDoSから守るためにcookie exchangeの仕組みを含んでいる。しかしcookie exchangeを使わない実装はDoSに対して脆弱なままである。cookie exchangeを使わないDTLSサーバは、もし自身がDoSを受けなかったとしても、攻撃の増幅器として使われてしまう。増幅器になりたくなかったらDTLSサーバはcookie exchangeを実装するべき。またクライアントは毎回のhandshakeでcookie exchangeの準備をしておくこと。

*1:AES-CTRの時に節約できると言っていた16バイトはこれのことだったようです。

*2:DoS攻撃に対処するために、後で出てくるcookieが確認できるまではステートを持たないということだと思います。

*3:以前に受信したレコードを記録しておいて、それと一致したら捨てるという感じみたいだけど詳細は分からないです。

*4:同じホスト間で複数のコネクションを作る場合、それらを区別する方法はアプリケーションが考えろと言っているのだと思います。

*5:データグラムに余計なものを含めるなということかな。

*6:AES-CTRを使えってことかな?

*7:どこに誰が示すんだろう?