DTLSにおけるシーケンス番号
前回はDTLSのハンドシェイクについて説明しましたが、今回はシーケンス番号まわりの説明をしたいと思います。
本記事では特に断らなければDTLSとはDTLS1.2を指すものとします。
TLSではRecordプロトコルでデータのやりとりをします。DTLSでもこれは変わりません。ただし、DTLSのRecordヘッダーにepoch(エポック)とsequence_number(シーケンス番号)が追加になっています(図1)。

エポックとシーケンス番号
エポックとシーケンス番号の簡単な説明をします。
エポックは暗号状態が変わる度に増加するカウントとなります。例えば、最初のハンドシェイク時は0ですが、鍵交換が終わりChange Cipher Specが送信されて通信が暗号化されるようになると1になります。
シーケンス番号はDTLSの各レコードに付与される連番です。レコード送信される度に1ずつ増えていきます。0から始まり、エポックが変わった場合はシーケンス番号はまた0から始まります。
エポックとシーケンス番号は通信するお互いノードでそれぞれ管理され、送信時にDTLS Recordレイヤーのフィールドに設定されます。
DTLSではトランスポート層にUDPを使うことからレコードの順番が入れ替わる可能性があるため、古いエポック(古い暗号状態)のレコードが来た場合に、エポックをチェックすることで、それをDTLS層で破棄できるようになっています。
また、シーケンス番号はリプレイ攻撃(*1)を防ぐために使われます(これは後述)。
ちなみに、DTLS1.3のRecordレイヤーではヘッダーが可変長になっていたり、暗号化されたRecordではシーケンス番号も暗号化されるようになっていたりと、ヘッダーまわりだけでも大きく変わっています。
なぜRecordヘッダーにエポックとシーケンス番号が追加されたか
シーケンス番号自体はDTLSで新たに追加されたものではなく、もともとTLSにも存在しています。ただし、通信するお互いのノードでそれぞれカウントされるだけでパケット上に現れることはありません。シーケンス番号は送受信用にそれぞれあり、TLSレコードを送受信すると対応するシーケンス番号がインクリメントされます(図2)。このようにして、パケットでシーケンス番号をやりとりしなくてもお互いのノードで同じシーケンス番号を保持するようになっています。そしてこれから説明するように、ノード間でシーケンス番号を共有できていることがメッセージの完全性チェック(MAC検証)に必要になります。

TLSで通信を行う場合、一般的に、MAC(Message Authentication Code)によって、メッセージに改竄がないか完全性のチェックが行われます。単なるMACによるチェックだけではメッセージの抜き取り、リプレイなどを検出することはできませんが、TLSでは、MACの計算にシーケンス番号を含めていることで、抜き取り、リプレイを検出できるようになっています(*2)。
[参考] TLS1.2/1.3のRFCの関連箇所
RFC 5246 6.2.3. Record Payload Protection
The MAC of the record also includes a sequence number so that missing, extra, or repeated messages are detectable.
レコードのMACはシーケンス番号を含むので、メッセージの消失、挿入、再送信を検出できる。
RFC 8446 5.3 Per-Record Nonce
レコードごとのNonceの作成にシーケンス番号を含めることが記述されている。
RFC 8446 E.2. Record Layer
Non-replayability is provided by using a separate nonce for each record, with the nonce being derived from the record sequence number
各レコードで個別Nonceを使いリプレイ不可能性を提供する。Nonceはシーケンス番号から導出される。
このようにTLSにおいてもシーケンス番号は存在し、メッセージの抜き取りやリプレイ攻撃への対策に使われていました。
DTLSに話を戻しますが、DTLSではトランスポート層がUDPであるため、レコードの順番が入れ替わることがあります。
TLSではトランスポート層にTCPを使うため、(TCP層へ攻撃された場合を除き)メッセージが消えたり、順序が入れ替わることはありません。このため、パケット上で明示的にシーケンス番号をやりとりしなくても、お互いのノードでシーケンス番号を共有することができました。
一方、DTLSではレコードの順番が入れ替わることがあるので、TLSのようなそれぞれのノード内でシーケンス番号を個別に管理する方式では受信レコードのシーケンス番号を正しく認識することができません。つまり、メッセージの完全性(MAC)をチェックすることができません(*3)。このため、DTLSではRecordヘッダーにエポックとシーケンス番号が埋め込まれるようになりました。
なお、DTLSにおいてもMACの処理はTLSと同じ手順を踏みます(シーケンス番号を含める)。シーケンス番号はTLSでは各ノードが個別に管理している値を使っていましたが、DTLS Record ヘッダーに付与されているものを使う点が異なります。
ここまでのまとめ:
DTLSではレコードの順番が入れ替わりうる特性上、シーケンス番号をヘッダーに載せて明示する必要があることを説明しました。
DTLSにおけるリプレイ攻撃対策
TLSにおいてはシーケンス番号とMACを使ってリプレイ攻撃を防いでいることは先に説明しましたが、DTLSにおいても方法は異なりますが、シーケンス番号を使ってリプレイ攻撃を防いでいます。DTLSでは、受信時にレコードのシーケンス番号をチェックおよび記録して、受信済みのRecordは破棄することでリプレイ攻撃への対策を行っています。
ただし、過去の受信データ(シーケンス番号の一覧)をすべて記録するわけにはいかないのでSliding Windowという方法で管理されています。Sliding Windowでは、もっとも新しい(シーケンス番号が大きい)受信レコードのシーケンス番号を先頭として一定幅のWindowを用意し、Windowの範囲内で受信記録が保持されます。Windowサイズを越えた古いレコードは受信しても廃棄されます。Windowサイズは64が推奨値となっています(*4)。
OpenSSLでの実装をもとにもう少し具体的に説明してみましょう。
OpenSSLではWindowをbitmapで管理しています(図3)。そして、受信したなかでもっともシーケンス番号の大きいもの(新しいもの)をmax_seq_numに保持しています。
bitmapのLSB(一番右側)のbitが、シーケンス番号がmax_seq_numのレコードの受信フラグになっています(つまりbit0は常に1)。レコード受信時はヘッダ上のシーケンス番号からこのbitmapをチェックして、受信済みであれば廃棄するようになっています。
より新しいシーケンス番号のレコードを受信した場合は、max_seq_numを更新して、bitmapはシーケンス番号が進んだ分だけ左にbit shiftするだけとなっており効率的に処理されています。

OpenSSLではbitmapのサイズをunsigned longで確保しているため32bit環境では32、64bit環境では64のWindowサイズを持つことになります。
ここまでのまとめ:
DTLSにおいても、シーケンス番号を使ってリプレイ攻撃対策をしていることを説明しました。
ちなみにヘッダー上のシーケンス番号は暗号化されていないので、中間者攻撃で書き換えることができそうですが、その場合、受信側のMAC検証で失敗する(*5)ので、そのレコードは廃棄されます。
まとめ
この記事では以下を解説しました。
- DTLS 1.2のRecordヘッダーにエポックとシーケンス番号が追加されていること
- ヘッダーにこれらが追加された理由(レコードの順番が入れ替わりうるので、シーケンス番号を明示する必要がある)
- それらがTLSと同じくリプレイ攻撃対策に使われていること
(*1) 過去に送信されたレコードを保存しておき、後から再送信してくる攻撃。攻撃者が自分でレコードを作成せず、元々正しくやりとりされたレコードを再送信するだけなので、暗号化に必要な鍵を持っていなくても、一見正しいメッセージを送信することができる。
(*2) 抜き取りやリプレイがあった場合、ノードで管理している受信シーケンス番号と受信パケットのシーケンス番号にずれが生じるため、MACの検証で失敗する。
(*3) MACの生成や検証にシーケンス番号を使うが、そのシーケンス番号が不明なため。
(*4) RFC 6347 4.1.2.6. Anti-Replay
(*5) MACの生成時に使われたシーケンス番号と検証時に使われるヘッダー上のシーケンス番号が異なるため。
投稿日:2023/02/13 19:02