跳到主要内容

RFC 6347 - 4.2. DTLS 握手协议

4.2. The DTLS Handshake Protocol (DTLS 握手协议)​

DTLS 使用与 TLS 相同的全部握手消息与流程, 有三处主要变化:

  1. 增加无状态 cookie 交换以防拒绝服务攻击.

  2. 修改握手头部以处理消息丢失、重排与 DTLS 消息分片 (以避免 IP 分片).

  3. 使用重传定时器处理消息丢失.

除上述例外, DTLS 的消息格式、流程与逻辑与 TLS 1.2 相同.

4.2.1. Denial-of-Service Countermeasures (拒绝服务对策)​

数据报安全协议极易遭受多种 DoS 攻击.两类攻击尤其值得关注:

  1. 攻击者可通过发送一系列握手发起请求消耗服务器过量资源, 导致服务器分配状态并可能执行昂贵密码运算.

  2. 攻击者可将受害者地址伪造为源发送连接发起消息, 把服务器用作放大器.服务器随后将其下一条消息 (在 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 状态:

  1. 重传定时器到期: 实现转到 SENDING 状态, 重传该 flight, 重置重传定时器, 并返回 WAITING 状态.

  2. 实现从对等端读到重传的 flight: 实现转到 SENDING 状态, 重传该 flight, 重置重传定时器, 并返回 WAITING 状态.理由是收到重复消息很可能是对等端定时器到期的结果, 因而表明己方先前 flight 的一部分丢失.

  3. 实现收到下一 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 就销毁关联.