RFC 6347 - 4.2. DTLS 握手协议
4.2. The DTLS Handshake Protocol (DTLS 握手协议)
DTLS 使用与 TLS 相同的全部握手消息与流程, 有三处主要变化:
-
增加无状态 cookie 交换以防拒绝服务攻击。
-
修改握手头部以处理消息丢失、重排与 DTLS 消息分片 (以避免 IP 分片)。
-
使用重传定时器处理消息丢失。
除上述例外, DTLS 的消息格式、流程与逻辑与 TLS 1.2 相同。
4.2.1. Denial-of-Service Countermeasures (拒绝服务对策)
数据报安全协议极易遭受多种 DoS 攻击。两类攻击尤其值得关注:
-
攻击者可通过发送一系列握手发起请求消耗服务器过量资源, 导致服务器分配状态并可能执行昂贵密码运算。
-
攻击者可将受害者地址伪造为源发送连接发起消息, 把服务器用作放大器。服务器随后将其下一条消息 (在 DTLS 中为可能很大的 Certificate 消息) 发往受害者机器, 从而淹没受害者。
为同时抵御这两类攻击, DTLS 借用 Photuris [PHOTURIS] 与 IKE [IKEv2] 使用的无状态 cookie 技术。客户端向服务器发送 ClientHello 时, 服务器可以 (MAY) 以 HelloVerifyRequest 消息响应。该消息包含用 [PHOTURIS] 技术生成的无状态 cookie。客户端必须 (MUST) 重传带有该 cookie 的 ClientHello。服务器随后验证 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 字段留空 (零长度)。
HelloVerifyRequest 定义如下:
struct {
ProtocolVersion server_version;
opaque cookie<0..2^8-1>;
} HelloVerifyRequest;
HelloVerifyRequest 消息类型为 hello_verify_request(3)。
server_version 字段语法与 TLS 中相同。然而, 为避免在初始握手中进行版本协商的要求, DTLS 1.2 服务器实现应当 (SHOULD) 使用 DTLS 1.0 版, 而不论预期协商的 TLS 版本如何。DTLS 1.2 与 1.0 客户端必须 (MUST) 仅将版本用于指示分组格式 (在 DTLS 1.2 与 1.0 中相同), 而不作为版本协商的一部分。特别地, DTLS 1.2 客户端禁止 (MUST NOT) 因服务器在 HelloVerifyRequest 中使用 1.0 版就假定服务器不是 DTLS 1.2 或最终会协商 DTLS 1.0 而非 DTLS 1.2。
响应 HelloVerifyRequest 时, 客户端必须 (MUST) 使用与原始 ClientHello 相同的参数值 (version, random, session_id, cipher_suites, compression_method)。服务器应当 (SHOULD) 使用这些值生成 cookie 并在收到 cookie 时验证其正确性。服务器必须 (MUST) 在 HelloVerifyRequest 中使用与发送 ServerHello 时相同的版本号。收到 ServerHello 后, 客户端必须 (MUST) 验证服务器版本值匹配。为避免多次 HelloVerifyRequest 导致序列号重复, 服务器必须 (MUST) 将 ClientHello 中的记录序列号用作 HelloVerifyRequest 中的记录序列号。
注意: 本规范将 cookie 大小上限提高到 255 字节以获得更大未来灵活性。先前 DTLS 版本上限仍为 32。
DTLS 服务器应当 (SHOULD) 以可在服务器上不保留任何每客户端状态的方式生成 cookie。一种技术是使用随机生成的秘密并按如下生成 cookie:
Cookie = HMAC(Secret, Client-IP, Client-Parameters)
收到第二个 ClientHello 时, 服务器可验证 Cookie 有效且客户端可在给定 IP 地址接收分组。为避免多次 cookie 交换导致序列号重复, 服务器必须 (MUST) 将 ClientHello 中的记录序列号用作其初始 ServerHello 中的记录序列号。后续 ServerHello 仅在服务器创建状态后发送且必须 (MUST) 正常递增。
对该方案的一种潜在攻击是攻击者从不同地址收集若干 cookie 然后复用以攻击服务器。服务器可通过频繁更改 Secret 值来防御, 从而使那些 cookie 失效。若服务器希望合法客户端能在转换期间完成握手 (例如, 它们在服务器切换到 Secret 2 之前收到 Secret 1 的 cookie 然后发送第二个 ClientHello), 服务器可在一段时间窗口内同时接受两个秘密。[IKEv2] 建议在 cookie 中加入版本号以检测该情形。另一种做法是简单地尝试用两个秘密分别验证。
DTLS 服务器应当 (SHOULD) 在执行新握手时进行 cookie 交换。若服务器运行环境中放大不是问题, 服务器可以 (MAY) 配置为不进行 cookie 交换。然而默认应当 (SHOULD) 执行交换。此外, 服务器可以 (MAY) 在恢复会话时不进行 cookie 交换。客户端必须 (MUST) 准备好每次握手都进行 cookie 交换。
若使用 HelloVerifyRequest, 初始 ClientHello 与 HelloVerifyRequest 不参与 handshake_messages (用于 CertificateVerify 消息) 与 verify_data (用于 Finished 消息) 的计算。
若服务器收到带无效 cookie 的 ClientHello, 它应当 (SHOULD) 与无 cookie 的 ClientHello 同等处理。这可避免客户端以某种方式获得错误 cookie (例如因服务器更改 cookie 签名密钥) 时的竞态/死锁。
对实现者的说明: 这可能导致客户端收到带不同 cookie 的多个 HelloVerifyRequest。客户端应当 (SHOULD) 通过响应新的 HelloVerifyRequest 发送带 cookie 的新 ClientHello 来处理。
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 加一。注意在重握手情形下, 这意味着 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 计数器。该计数器初始为零。收到消息时, 若其序列号与 next_receive_seq 匹配, 则递增 next_receive_seq 并处理消息。若序列号小于 next_receive_seq, 必须 (MUST) 丢弃消息。若序列号大于 next_receive_seq, 实现应当 (SHOULD) 排队消息但可以 (MAY) 丢弃。(这是简单的空间/带宽权衡。)
4.2.3. Handshake Message Fragmentation and Reassembly (握手消息分片与重组)
如第 4.1.1 节所述, 每条 DTLS 消息必须 (MUST) 适配单个传输层数据报。然而, 握手消息可能大于最大记录尺寸。因此, DTLS 提供将一条握手消息分片到若干记录上的机制, 每条可分别传输, 从而避免 IP 分片。
发送握手消息时, 发送方将其划分为 N 个连续数据范围。这些范围禁止 (MUST NOT) 大于最大握手分片大小, 且必须 (MUST) 共同覆盖整条握手消息。范围不应 (SHOULD NOT) 重叠。发送方随后创建 N 条握手消息, 均与原始握手消息具有相同 message_seq。每条新消息标记 fragment_offset (先前分片包含的字节数) 与 fragment_length (本分片长度)。所有消息中的 length 字段与原始消息的 length 相同。未分片消息是 fragment_offset=0 且 fragment_length=length 的退化情形。
DTLS 实现收到握手消息分片时, 必须 (MUST) 缓冲直至握有整条消息。DTLS 实现必须 (MUST) 能够处理重叠的分片范围。这允许发送方在 PMTU 估计变化时用更小分片尺寸重传握手消息。
注意与 TLS 一样, 多条握手消息可置于同一条 DTLS 记录中, 只要有空间且属于同一 flight (航班)。因此, 将两条 DTLS 消息装入同一数据报有两种可接受方式: 同一条记录或不同记录。
4.2.4. Timeout and Retransmission (超时与重传)
DTLS 消息按下图分为一系列消息 flight。尽管每个 flight 可能由多条消息组成, 就超时与重传而言应视为整体。
(图 1 全握手、图 2 会话恢复、图 3 状态机与英文版相同, 此处保留 ASCII 图与英文标签以保持与实现一致。)
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 使用简单的超时与重传方案及下述状态机。因 DTLS 客户端发送第一条消息 (ClientHello), 它们从 PREPARING 状态开始。DTLS 服务器从 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
图 3. DTLS 超时与重传状态机
状态机有三个基本状态。
在 PREPARING 状态, 实现执行准备下一 flight 消息所需的计算。随后将其缓冲以供传输 (先清空缓冲区) 并进入 SENDING 状态。
在 SENDING 状态, 实现发送缓冲的 flight。消息发送后, 若这是握手中最后一 flight, 则进入 FINISHED 状态。若实现预期收到更多消息, 则设置重传定时器并进入 WAITING 状态。
有三种方式离开 WAITING 状态:
-
重传定时器到期: 实现转到 SENDING 状态, 重传该 flight, 重置重传定时器, 并返回 WAITING 状态。
-
实现从对等端读到重传的 flight: 实现转到 SENDING 状态, 重传该 flight, 重置重传定时器, 并返回 WAITING 状态。理由是收到重复消息很可能是对等端定时器到期的结果, 因而表明己方先前 flight 的一部分丢失。
-
实现收到下一 flight 消息: 若这是最后一条 flight, 转到 FINISHED。若需要发送新 flight, 转到 PREPARING 状态。部分读取 (无论是部分消息或 flight 中仅部分消息) 不引起状态转换或定时器重置。
因 DTLS 客户端发送第一条消息 (ClientHello), 它们从 PREPARING 开始。DTLS 服务器从 WAITING 开始, 但缓冲区为空且无重传定时器。
当服务器希望重握手时, 从 FINISHED 转到 PREPARING 以发送 HelloRequest。客户端收到 HelloRequest 时, 从 FINISHED 转到 PREPARING 以发送 ClientHello。
此外, 在 FINISHED 状态中至少 [TCP] 定义的两倍默认 MSL 时间内, 发送最后一 flight 的节点 (普通握手中的服务器或恢复握手中的客户端) 必须 (MUST) 以对等端最后一 flight 的重传回应其重传。这可避免最后一 flight 丢失时的死锁。该要求同样适用于 DTLS 1.0, 虽在 [DTLS1] 中未明示, 但状态机正确运行一直需要。为理解必要性, 考虑普通握手中服务器 Finished 丢失的情形: 服务器认为握手完成但实际未完成。客户端等待 Finished, 其重传定时器将触发并重传客户端 Finished, 从而促使服务器以其 Finished 响应, 完成握手。恢复握手在服务器侧同理。
注意由于分组丢失, 一方可能在另一方尚未收到己方 Finished 消息的情况下发送应用数据。实现必须 (MUST) 丢弃或缓冲新 epoch 的全部应用数据分组, 直至收到该 epoch 的 Finished。实现可以 (MAY) 将在收到对应 Finished 之前收到带新 epoch 的应用数据视为重排或分组丢失的证据, 并立即重传其最后 flight, 绕过重传定时器。
4.2.4.1. Timer Values (定时器取值)
定时器取值由实现选择, 但错误处理可能导致严重拥塞问题; 例如, 若许多 DTLS 实例过早超时并在拥塞链路上过快重传。实现应当 (SHOULD) 使用 1 秒初始定时器值 (RFC 6298 [RFC6298] 定义的最小值), 每次重传加倍, 直至不低于 RFC 6298 最大值 60 秒。注意我们推荐 1 秒定时器而非 RFC 6298 默认 3 秒, 以改善时延敏感应用的延迟。由于 DTLS 仅对手握使用重传而非数据流, 对拥塞的影响应很小。
实现应当 (SHOULD) 保留当前定时器值直至发生无丢失的传输, 此时可重置为初始值。长时间空闲后, 不少于当前定时器值的 10 倍, 实现可将定时器重置为初始值。一种可能情形是在大量数据传输后使用重握手。
4.2.5. ChangeCipherSpec
与 TLS 一样, ChangeCipherSpec 消息在技术上不是握手消息, 但就超时与重传而言必须 (MUST) 视为与相关 Finished 消息同一 flight 的一部分。这在消息丢失时造成潜在歧义, 因为无法无歧义地确定 ChangeCipherSpec 相对握手消息的顺序。
这对当前任何 TLS 模式都不是问题, 因为预期在 ChangeCipherSpec 之前的握手消息集合可从握手其余状态预测。然而, 未来模式必须 (MUST) 注意避免产生歧义。
4.2.6. CertificateVerify and Finished Messages
CertificateVerify 与 Finished 消息格式与 TLS 相同。散列计算包含完整握手消息, 包括 DTLS 特有字段: message_seq, fragment_offset 与 fragment_length。然而, 为消除对握手消息分片的敏感性, Finished MAC 必须 (MUST) 按每条握手消息以单一分片发送的方式计算。注意在使用 cookie 交换的情形下, 初始 ClientHello 与 HelloVerifyRequest 禁止 (MUST NOT) 纳入 CertificateVerify 或 Finished MAC 计算。
4.2.7. Alert Messages
注意 Alert 消息完全不重传, 即便发生在握手上下文中。然而, 通常会发出告警的 DTLS 实现应当在 (SHOULD) 再次收到问题记录时 (例如作为重传的握手消息) 生成新告警。实现应当 (SHOULD) 检测对等端持续发送错误消息的情形, 并在检测到此类行为后终止本地连接状态。
4.2.8. Establishing New Associations with Existing Parameters (用现有参数建立新关联)
若某 DTLS 客户端-服务器对配置为在同一主机/端口四元组上反复连接, 则客户端可能静默放弃一条连接然后用相同参数发起另一条 (例如重启后)。这在服务器看来将是带 epoch=0 的新握手。若服务器认为在给定主机/端口四元组上已有关联并收到 epoch=0 的 ClientHello, 它应当 (SHOULD) 继续进行新握手, 但禁止 (MUST NOT) 销毁现有关联, 直至客户端通过完成 cookie 交换或完成包含可验证 Finished 消息的完整握手证明可达性。收到正确的 Finished 后, 服务器必须 (MUST) 放弃先前关联, 以避免两个有效关联的 epoch 重叠造成混淆。可达性要求防止离径/盲攻击者仅凭伪造 ClientHello 就销毁关联。