4、DTLS协议入门:DTLS与TLS的关系、DTLS记录层、DTLS握手协议、DTLS1.2与1.3差异
聊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。我画了一张流程图,帮你直观理解整个握手过程。
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,但一定要在应用层做防重放。