阅读前提:具备密码学基础,并了解TLS1.2、DTLS1.2、TLS1.3的工作细节
主要内容来源:draft-ietf-tls-dtls13-43
DTLS1.3 总体上和DTLS1.2的设计原则区别不大,主要区别分为两方面:
一方面是为了适配TLS1.3 对TLS1.2的升级,除了和DTLS1.2一样要解决TLS在握手阶段、应用层数据传输阶段的有序性/可靠性依赖问题外,还要解决类似比如TLS1.3中引入了key/IV update支持握手完成后的密钥更新,DTLS1.2中就已经引入的epoch 字段重要性上来了,用于标识key phase;再比如TLS1.3支持0-rtt data,在DTLS1.3使用epoch来标识0-rtt data的开始和结束,而非依赖end of early data。
另一方面是DTL1.3对自身的优化,这里借鉴吸收了不少QUIC的经验,首先是connection ID成为协议原住民了、并且支持ConnectionID的更新(类似QUIC);再比如record的header不再是固定长度了(对应QUIC里的long header & short header);再比如record header里的sequence number也需要加密了(对应QUIC的header encrption); 最后一点很重要,优化了DTLS1.2中粗糙的handshake消息重传机制,引入了ACK进行细粒度的重传,解决之前一个分片丢失,整个flight所有消息都需要重传的问题(个人觉得这基于属于DTLS1.2的设计缺陷了,使得对handshake的协议分片设计变得没有意义)
DTLS1.3的record协议和DTLS1.2相比,最重要的区别是传输明文的DTLSPlaintext和传输密文的DTLSCiphertext结构不再一样,DTLSCiphertext采用可变的header,引入了一个字节的标志位来来标识header中的内容。
DTLSPlaintext和DTLSCiphertext的定义如下:
struct {
ContentType type;
ProtocolVersion legacy_record_version;
uint16 epoch = 0
uint48 sequence_number;
uint16 length;
opaque fragment[DTLSPlaintext.length];
} DTLSPlaintext;
struct {
opaque content[DTLSPlaintext.length];
ContentType type;
uint8 zeros[length_of_padding];
} DTLSInnerPlaintext;
struct {
opaque unified_hdr[variable];
opaque encrypted_record[length];
} DTLSCiphertext;
对于ClientHello、ServerHello消息封装为DTLSPlaintext结构,对于其他消息封装过程为DTLSPlaintext → DTLInnerPlaintext → DTLSCiphertext。
注意到DTLSPlaintext的结构一点没变,这是为了做前向兼容,后面我们还可以看到ClientHello的结构也不会变,虽然有些字段已经不会用了。
DTLSCiphertext的unified_hdr是可变的header,定义如下:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|0|0|1|C|S|L|E E|
+-+-+-+-+-+-+-+-+
| Connection ID | Legend:
| (if any, |
/ length as / C - Connection ID (CID) present
| negotiated) | S - Sequence number length
+-+-+-+-+-+-+-+-+ L - Length present
| 8 or 16 bit | E - Epoch
|Sequence Number|
+-+-+-+-+-+-+-+-+
| 16 bit Length |
| (if present) |
+-+-+-+-+-+-+-+-+
在DTLS1.3中会有三种record header,分别如下
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| Content Type | |0|0|1|1|1|1|E E| |0|0|1|0|0|0|E E|
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| 16 bit | | | |8-bit Seq. No. |
| Version | / Connection ID / +-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+ | | | |
| 16 bit | +-+-+-+-+-+-+-+-+ | Encrypted |
| Epoch | | 16 bit | / Record /
+-+-+-+-+-+-+-+-+ |Sequence Number| | |
| | +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | 16 bit |
| 48 bit | | Length | DTLSCiphertext
|Sequence Number| +-+-+-+-+-+-+-+-+ Structure
| | | | (minimal)
| | | Encrypted |
+-+-+-+-+-+-+-+-+ / Record /
| 16 bit | | |
| Length | +-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+
| | DTLSCiphertext
| | Structure
/ Fragment / (full)
| |
+-+-+-+-+-+-+-+-+
DTLSPlaintext
Structure
和TLS1.3区别不大,不同的地方是additional data 用的不是DTLSCiphertext的on-the-wire的 header,而是完整的header,因此需要从on-the-wire header重建出完整的header,尤其是对于epoch和seq number的重建,需要重建出64bits的RecordNumber。
struct {
uint16 epoch;
uint48 sequence_number;
} RecordNumber;
协议并没有规定具体什么做,只是给出了建议。
TLS1.3中连接的不同阶段采用的加密等级/加密secret不同(TLS1.2则只有加密和不加密两种),此外握手完成后TLS1.3还会进行key udpate更新secret。这种加密等级的同步在TLS中由于基于可靠传输,加密等级状态在双端是自动(隐式)同步的。在DTLS中则需要显式地标识当前包的加密等级/状态,使得从DTLS1.2中就引入的epoch字段重要性得以加强。
DTLS1.3中对seq number进行加密,过程和QUIC的header encrption非常类似。都是从 ciphertext采样16个字节,通过AES-ECB/ ChaCha20再加密计算出一个Mask,然后将seq_number和这个mask进行XOR。
注意Seq number加密只针对DTLSCiphertext生效。
和DTLS1.2一样,需要考虑PMTU问题。另外需要对Record进行防重放(1.3协议中没有提到防重放是可选的了),做法也都类似。
对于防重放时一个需要注意的地方是实现上需要防止timing side channel攻击,和QUIC一样,对任何packet需要先header deprotection → packet deprotection → replay protection
DTLS1.3的握手过程和TLS1.3的握手过程基本一致,不同的地方基本也和DTLS1.2一样:
为了前向兼容, 1.3的 ClientHello和1.2的格式保持一致,和tls1.3一样,不再通过version来进行版本协商,而是通过supported_versions扩展进行和server 的版本协商。
uint16 ProtocolVersion;
opaque Random[32];
uint8 CipherSuite[2]; /* Cryptographic suite selector */
struct {
ProtocolVersion legacy_version = { 254,253 }; // DTLSv1.2
Random random;
opaque legacy_session_id<0..32>;
opaque legacy_cookie<0..2^8-1>; // DTLS
CipherSuite cipher_suites<2..2^16-2>;
opaque legacy_compression_methods<1..2^8-1>;
Extension extensions<8..2^16-1>;
} ClientHello;
Client Server
+--------+
ClientHello | Flight |
--------> +--------+
+--------+
<-------- HelloRetryRequest | Flight |
+ cookie +--------+
+--------+
ClientHello | Flight |
+ cookie --------> +--------+
ServerHello
{EncryptedExtensions} +--------+
{CertificateRequest*} | Flight |
{Certificate*} +--------+
{CertificateVerify*}
{Finished}
<-------- [Application Data*]
{Certificate*} +--------+
{CertificateVerify*} | Flight |
{Finished} --------> +--------+
[Application Data]
+--------+
<-------- [ACK] | Flight |
[Application Data*] +--------+
[Application Data] <-------> [Application Data]
ClientHello +--------+
+ pre_shared_key | Flight |
+ psk_key_exchange_modes +--------+
+ key_share* -------->
ServerHello
+ pre_shared_key +--------+
+ key_share* | Flight |
{EncryptedExtensions} +--------+
<-------- {Finished}
[Application Data*]
+--------+
{Finished} --------> | Flight |
[Application Data*] +--------+
+--------+
<-------- [ACK] | Flight |
[Application Data*] +--------+
[Application Data] <-------> [Application Data]
Client Server
ClientHello
+ early_data
+ psk_key_exchange_modes +--------+
+ key_share* | Flight |
+ pre_shared_key +--------+
(Application Data*) -------->
ServerHello
+ pre_shared_key
+ key_share* +--------+
{EncryptedExtensions} | Flight |
{Finished} +--------+
<-------- [Application Data*]
+--------+
{Finished} --------> | Flight |
[Application Data*] +--------+
+--------+
<-------- [ACK] | Flight |
[Application Data*] +--------+
[Application Data] <-------> [Application Data]
+-----------+
| PREPARING |
+----------> | |
| | |
| +-----------+
| |
| | Buffer next flight
| |
| \|/
| +-----------+
| | |
| | SENDING |<------------------+
| | | |
| +-----------+ |
Receive | | |
next | | Send flight or partial |
flight | | flight |
| | |
| | Set retransmit timer |
| \|/ |
| +-----------+ |
| | | |
+------------| WAITING |-------------------+
| +----->| | Timer expires |
| | +-----------+ |
| | | | | |
| | | | | |
| +----------+ | +--------------------+
| Receive record | Read retransmit or ACK
Receive | (Maybe Send ACK) |
last | |
flight | | Receive ACK
| | for last flight
\|/ |
|
+-----------+ |
| | <---------+
| FINISHED |
| |
+-----------+
| /|\
| |
| |
+---+
Server read retransmit
Retransmit ACK
区别于DTLS1.2主要是引入了ACK导致在SENDING、WAITING状态下的动作和状态切换
ACK消息虽然作用于Handshake阶段,但是这里独立作为一个section,一方面是因为DTLS ACK消息不属于Handshake消息的一种,而是和Handshake、application、alert同一层的消息类型。ACK消息的结构很简单,只需要给出那些已经收到的records的record_number。
struct {
RecordNumber record_numbers<0..2^16-1>;
} ACK;
什么时候应该发送ACK,协议并给出了一些实现上的建议/规定:
协议也给出几个什么时候不发送ACK的建议/规定:
TLS1.3中支持key update消息,key update是有方向的,可以是单边的更新,也可以是双边都更新。对于单边更新的key update,DTLS规定对端必须要进行ACK,另外发起端只有在收到对端的ACK后才能使用新的 secret、以及epoch值。
注意 对key udpate消息的ACK消息可能会丢失,这样发起端需要进行重传。
这个策略看起来比较保守,和QUIC激进的做法不一样,QUIC里不需要显式通过Key update来进行key 更新,只是通过flip key phase bit来在数据packet交互过程中进行隐式的Key update。
DTLS1.2 ConnectionID在ClientHello和ServerHello消息的connection_id扩展里进行协商确认,然后整个连接将保持不变。
DTLS1.3对ConnectionID的变动是使得其可以更新。任意一段可以通过RequestConnectionId消息发起ConnectionID的更新请求,表示希望对端能够多提供几个ConnectionID给自己备用。对端收到ConnectionID的更新请求后可以回应一个NewConnectionId消息,包含生成的ConnectionIDs,也可以在觉得对端请求的太多时忽略更新请求(但是此时需要ACK这个更新请求)。
struct {
uint8 num_cids;
} RequestConnectionId;
enum {
cid_immediate(0), cid_spare(1), (255)
} ConnectionIdUsage;
opaque ConnectionId<0..2^8-1>;
struct {
ConnectionIds cids<0..2^16-1>;
ConnectionIdUsage usage;
} NewConnectionId;