阅读前提:具备密码学基础,并了解TLS1.2的工作细节
主要内容来源:RFC6347
DTLS设计的目标是为了构建TLS over UDP,但是TLS 依赖传输层提供有序可靠性保证,因此不能直接运行在UDP之上,需要对协议本身依赖传输层有序可靠的地方做一定修改。DTLS旨在以最小的改动做到最大程度上匹配TLS。
TLS依赖传输层有序的地方有:
针对TLS record协议的依赖问题(问题2),DTLS主要通过下面几个措施:
针对TLS handshake协议依赖问题(问题1),DTLS则通过以下措施来解决:
由于传输层UDP,DTLS又引入了几个新的问题:
首先从Record协议上,DTLS的Record 协议新增了两个字段,epoch和sequence_number,其他字段和TLS Record协议一致
struct {
ContentType type;
ProtocolVersion version;
uint16 epoch; // New field
uint48 sequence_number; // New field
uint16 length;
opaque fragment[DTLSPlaintext.length];
} DTLSPlaintext;
DTLS的record层不支持分片(和UDP一样),需要上层来保证产生的record能fit within一个UDP包。因此,协议中规定了几个DTLS实现应当做的事:
其实这点属于Record 数据保护的一部分,但是内容值得拿出来单独写写。
首先一点,DTLS 在record中显式传输sequence_number导致record不在像TLS那样能在Mac校验时做到防重放了。
在TLS over TCP中,replay detection靠双端的seq number 同步做到,由于tls record的Mac计算涉及到seq number的参与,因此任何试图replay的消息都会由于Mac和当前双端内存中的seq number不匹配而导致Mac校验失败,从而做到replay detection。
到 DTLS这里,seq number需要作为header和数据一起显示传给对端,此时攻击者去replay消息,接收端收到后由于所有header都对,消息也都合法,MAC校验会通过,因此需要额外根据显示的seq number来去重做到防重放。在实现上维护一个收到records的滑动窗口,seq number太老或者已经在window里的record直接丢弃。协议规定滑动窗口的最小至少为32,最好一64为默认值。
最后,需要说明的是在DTLS里防重放是可选的,理由是基于UDP重放不一定是代表被攻击。上层可以选择不开启防重放,从而自己根据应用情况来实现。
DTLS handshake过程基本上和TLS1.2 handshake一致,但是有下面几个重要的改动:
UDP非常易受各种DOS攻击,DTLS协议主要关注两种攻击
为了应对这些攻击,DTLS引入的Photuris 、IKE等协议中使用的stateless cookie exchange机制,具体来说是:client发起ClientHello后,server回应一个HelloVerifyRequest消息,这个消息里包含了一个server根据Client的地址、ClientHello消息里的内容等计算的一个MAC(cookie),client收到HelloVerifyRequest消息后需要重发ClientHello消息,消息里需要带上这个cookie,server 收到后会验证这个ClientHello消息是否和cookie匹配(校验MAC),验证成功后server才进行后续的密码学握手。
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie)
[Rest of handshake]
显然这个机制可以防止放大反射攻击,但是不能有效防止针对DTLS server本身的从大量真实地址发起的DOS攻击,此外攻击者还可以通过事先收集真实的cookies, 做到复用ClientHello + cookie,形成和之前等效的DOS。
消息体如下:
struct {
ProtocolVersion server_version;
opaque cookie<0..2^8-1>;
} HelloVerifyRequest;
DTLS给ClientHello新增了cookie字段,用于client将收到HelloVerifyRequest消息后在新的ClientHello里带上cookie,其他字段必须要和初始的ClientHello一致
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以及HelloVerifyRequest不算做handshake transcipt的一部分,在CertificateVerify和Finished消息中不用考虑这部分
DTSL Handshake协议为了保证有序性、并且支持分片,在协议层加了三个字段:
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;
DTLS Handshake协议采用简单的超时重传机制来保证消息可靠性,但是并没有引入TCP、QUIC等协议中的常见的显式ACK机制来进行loss detect,而是利用握手过程的握手消息交互机制做到隐式ACK。
具体来说,DTLS将握手消息按照flight分组,下面是full handshake时双方的握手过程交互:
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 /
对于session-resuming的握手过程,情况有些不同,最后一个fight是client发出的。
Client Server
------ ------
ClientHello --------> Flight 1
ServerHello \
[ChangeCipherSpec] Flight 2
<-------- Finished /
[ChangeCipherSpec] \Flight 3
Finished --------> /
总的来说:一方发出一个flight后,如果握手过程还没结束,另一方需要去回应一个flight。如果一方在一段时间内没有收到对方的下一个flight(很可能对方的下一个flight有丢失),或者收到上对方重传的上一个flight(很可能自己刚发的flight有丢失,导致对方的超时重传)。那么他都需要重传这个flight的所有handshake消息。基于这个原则,DTLS将握手过程的超时重传抽象了一个握手状态机:
+-----------+
| PREPARING |
+---> | | <--------------------+
| | | |
| +-----------+ |
| | |
| | Buffer next flight |
| | |
| \|/ |
| +-----------+ |
| | | |
| | SENDING |<------------------+ |
| | | | | Send
| +-----------+ | | HelloRequest
Receive | | | |
next | | Send flight | | or
flight | +--------+ | |
| | | Set retransmit timer | | Receive
| | \|/ | | HelloRequest
| | +-----------+ | | Send
| | | | | | ClientHello
+--)--| WAITING |-------------------+ |
| | | | Timer expires | |
| | +-----------+ | |
| | | | |
| | | | |
| | +------------------------+ |
| | Read retransmit |
Receive | | |
last | | |
flight | | |
| | |
\|/\|/ |
|
+-----------+ |
| | |
| FINISHED | -------------------------------+
| |
+-----------+
| /|\
| |
| |
+---+
Read retransmit
Retransmit last flight
看起来这样虽然简单但是效率不高,一个flight中任何消息的任何分片丢失都将导致整个flight重传。
最后,重传的时间和次数取决于实现,协议建议从1s开始指数退避到60s截断。
内容源自[I-D.ietf-tls-dtls-connection-id],目前还是草案,主要内容是在DTLS 1.2中引入cid机制,用来标识连接,而非采用至之前的IP + 端口 标识连接。理由是DTLS常被用于IOT通信,对于IOT设备而言往往会因为低功耗要求导致连接保活策略比较保守,NAT rebinding发生的会比较频繁,也就导致DTLS连接失效。CID机制的引入就是为了DTLS连接能发生NAT rebind后依旧能够存活下来,QUIC也有类似的机制。
改动主要包括几个部分:
struct {
opaque cid<0..2^8-1>;
} ConnectionId;
当DTLS连接启用了CID,那么在握手完成后的应用数据将采用新的Record协议进行通信,新的 Record协议采用和TLS1.3类似的做法,先用一个DTLSInnerPlaintext结构wrap下DTLSPlaintext结构:
struct {
opaque content[length];
ContentType real_type;
uint8 zeros[length_of_padding];
} DTLSInnerPlaintext;
有了DTLSInnerPlaintext后,对其整个DTLSInnerPlaintext结构进行加密,并构建新的带CID的DTLSCiphertext:
struct {
ContentType outer_type = tls12_cid;
ProtocolVersion version;
uint16 epoch;
uint48 sequence_number;
opaque cid[cid_length]; // New field
uint16 length;
opaque enc_content[DTLSCiphertext.length];
} DTLSCiphertext;
当收到一个带CID、并且地址和该CID表示的连接之前采用的地址不同时,可能表示对端发生了地址迁移,也可能是攻击者伪造的迁移。因此,协议规定了接收方在什么情况下需要进行地址替换。
最后,CID的引入也造成了一定的隐私泄漏风险。尤其是DTLS1.2的CID缺少更新机制。