4. DTLS协议入门

聊CoAP安全,DTLS是绕不开的核心。很多人一上来就问我:DTLS和TLS到底啥关系?是不是就是在TLS外面包了一层?嗯,这么说对了一半。今天我就把这层窗户纸捅破,从协议设计到实际坑点,一次性讲清楚。

4.1 DTLS与TLS:不是简单的“套壳”

DTLS(Datagram Transport Layer Security)说白了就是TLS在UDP上的变种。TLS依赖TCP的可靠传输和有序到达,但UDP不保证这些。所以DTLS不能直接复用TLS的代码,得在记录层和握手层做大量改造。

我当年第一次接触DTLS时,天真地以为改个传输层接口就行。结果调试时发现握手永远卡在第三步——因为丢包了。后来才明白,DTLS自己得实现重传和乱序重组。

特性 TLS DTLS
传输层 TCP(可靠、有序) UDP(不可靠、无序)
丢包处理 依赖TCP重传 自身实现超时重传
乱序处理 TCP保证有序 记录层加序列号,接收端重组
握手状态 状态机简单 状态机复杂,需处理重传和分片
适用场景 Web、文件传输 IoT、VoIP、实时通信

核心结论:DTLS不是TLS的“UDP包装”,而是重新设计了记录层和握手层,以适配UDP的无连接特性。两者共享密码套件和证书体系,但传输逻辑完全不同。

4.2 DTLS记录层:在不可靠信道上建“可靠通道”

DTLS记录层是协议的基础。每个记录都是一个独立的数据报,包含类型、版本、序列号、负载长度和负载内容。关键设计点有两个:序列号和防重放。

4.2.1 序列号:解决乱序和丢包

每个DTLS记录都有一个8字节的序列号(epoch + sequence_number)。epoch表示握手阶段(0表示当前连接,1表示第一次握手完成,以此类推),sequence_number是epoch内的递增计数器。

接收端根据序列号做两件事:

  • 去重:如果收到相同序列号的记录,直接丢弃。
  • 重组:如果序列号不连续,暂存到缓冲区,等缺失的包到达后再按序提交给上层。

我在项目中遇到过一个问题:设备A发送了记录1、2、3,但网络延迟导致接收端先收到3,再收到1和2。如果没有序列号,解密后数据顺序就乱了。DTLS记录层通过序列号窗口机制,保证应用层看到的数据是连续的。

4.2.2 防重放攻击

UDP天然容易被重放攻击。DTLS在记录层维护一个滑动窗口(默认64位),记录已收到的序列号。如果收到一个序列号在窗口左侧(太旧)或窗口内但已标记为已收,直接丢弃。

避坑指南:我曾经在低功耗设备上关闭了防重放窗口以节省内存,结果被攻击者用重放报文打满了CPU。后来我加了一条规则:窗口大小可以缩小到16位,但绝不能关闭。这是安全底线。

4.3 DTLS握手协议:比TLS多出的“三把锁”

DTLS握手协议在TLS握手的基础上增加了三个机制:重传、分片和HelloVerifyRequest。我画了一张流程图,帮你直观理解整个握手过程。

DTLS 1.2 握手流程 DTLS 1.2 握手流程(含重传与分片) 客户端 (Client) 服务端 (Server) ① ClientHello ② HelloVerifyRequest ③ ClientHello (含Cookie) ④ ServerHello + Certificate ⑤ ClientKeyExchange + ChangeCipherSpec ⑥ ChangeCipherSpec + Finished ✅ 安全连接建立,开始应用数据传输 说明:步骤②(HelloVerifyRequest)是DTLS独有的防放大攻击机制。步骤③中客户端必须携带服务端下发的Cookie才能继续握手。

4.3.1 重传机制

DTLS握手消息可能丢失。客户端发送ClientHello后,启动一个定时器(初始值1秒)。如果超时未收到响应,就重传ClientHello。每次重传超时时间翻倍(指数退避),直到达到上限(默认60秒)。

服务端也一样。发送ServerHello后,如果收不到ClientKeyExchange,就重传ServerHello。注意:重传的不是整个握手消息,而是当前状态下的最新消息。

注意:重传次数和超时时间需要根据网络环境调整。我在NB-IoT项目中将初始超时设为3秒,最大重传次数设为5次,因为窄带网络延迟高、丢包率大。如果按默认的1秒/7次重传,设备电池撑不过一天。

4.3.2 分片与重组

UDP数据报有长度限制(通常1500字节以内)。如果握手消息(比如证书)超过这个限制,DTLS会将其分片成多个记录发送。每个分片包含相同的消息序列号,但分片偏移和分片长度不同。

接收端根据消息序列号收集所有分片,按偏移量重组完整消息后再处理。如果某个分片丢失,整个消息都无法处理,必须等待重传。

4.3.3 HelloVerifyRequest:防放大攻击

这是DTLS独有的步骤。服务端收到ClientHello后,不直接回复ServerHello,而是先发一个HelloVerifyRequest,里面带一个Cookie。客户端必须重新发送ClientHello并带上这个Cookie,服务端才继续握手。

为什么要这么做?防止攻击者用伪造源IP发送大量ClientHello,让服务端回复大量ServerHello,从而放大流量攻击受害者。Cookie验证确保客户端能收到UDP包,从而确认源IP不是伪造的。

4.4 DTLS 1.2 vs 1.3:差异对比

DTLS 1.3(基于TLS 1.3)在2018年发布,相比1.2做了大量简化。我整理了一张对比表,方便你快速把握核心差异。

维度 DTLS 1.2 DTLS 1.3
握手轮次 2-RTT(含HelloVerifyRequest为3-RTT) 1-RTT(含HelloVerifyRequest为2-RTT)
0-RTT 不支持 支持(PSK模式下可发送早期数据)
密码套件 支持多种(RSA、DH、ECDH等) 仅支持AEAD(如AES-GCM、ChaCha20-Poly1305)
握手消息 明文传输证书链 证书加密传输(更隐私)
会话恢复 Session ID / Session Ticket PSK(预共享密钥)
记录层 序列号+epoch 序列号+epoch,但移除压缩和CBC模式
兼容性 广泛支持 较新,部分旧设备不支持

4.4.1 握手速度:1.3明显更快

DTLS 1.2需要2次往返(加上HelloVerifyRequest是3次)才能建立连接。DTLS 1.3只需要1次往返(加上HelloVerifyRequest是2次)。对于IoT设备来说,每次往返都意味着几十到几百毫秒的延迟,1.3的优势非常明显。

更关键的是0-RTT模式。如果客户端之前和服务端通信过,可以缓存PSK。下次连接时,客户端直接在第一个数据报中发送应用数据(0-RTT early data)。这在传感器上报场景中特别有用——设备唤醒后立即发送数据,不需要等待握手完成。

我的建议:如果设备支持DTLS 1.3,优先使用1.3。我在智能照明项目中将协议从1.2升级到1.3后,设备平均入网时间从2.3秒降到了0.8秒。但要注意0-RTT存在重放风险,需要应用层做幂等处理。

4.4.2 安全性:1.3更“瘦”也更安全

DTLS 1.3移除了所有不安全或过时的特性:

  • 移除压缩:CRIME攻击的根源,1.3直接砍掉。
  • 移除CBC模式:Lucky13攻击的温床,1.3只允许AEAD。
  • 移除RSA密钥交换:不支持前向安全性,1.3只用DH/ECDH。
  • 移除静态RSA证书:所有证书必须支持签名算法。

说白了,1.3把那些“虽然能用但容易出问题”的选项全删了。攻击面小了,配置也简单了。我见过太多1.2的配置错误——有人为了兼容老设备,开了TLS_RSA_WITH_AES_128_CBC_SHA,结果被中间人攻击打穿。1.3不会给你这种犯错的机会。

4.4.3 迁移成本:1.3不是免费的午餐

虽然1.3好处多,但迁移有代价:

  • 库支持:mbedTLS 2.x不支持1.3,需要升级到3.x。OpenSSL 1.1.1开始支持1.3,但部分旧设备用的还是1.0.2。
  • 硬件加速:1.3强制AEAD,如果设备没有AES-GCM硬件加速,纯软件实现会消耗更多CPU。
  • 0-RTT风险:早期数据可能被重放,应用层需要设计防重放逻辑(比如时间戳+nonce)。

避坑指南:我曾经在一个水表项目中尝试用DTLS 1.3的0-RTT,结果发现服务端收到重复的抄表数据。原因是设备在弱网环境下重发了ClientHello,服务端处理了两次。后来我在应用层加了消息去重(基于消息ID和序列号),才解决这个问题。

4.5 小结:选型建议

如果你现在要选DTLS版本,我的建议很简单:

  • 新项目:直接上DTLS 1.3。性能更好,安全性更高,配置更简单。
  • 存量项目:如果设备库不支持1.3,用1.2也没问题,但务必关闭弱密码套件(如TLS_RSA_WITH_AES_128_CBC_SHA)。
  • 极端低功耗场景:考虑0-RTT + PSK,但一定要在应用层做防重放。