kaspterio code as a hacker

09 January 2022

阅读前提:具备密码学基础,并了解TLS1.2、DTLS1.2、TLS1.3的工作细节

主要内容来源:draft-ietf-tls-dtls13-43

1. Overview of DTLS1.3

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的协议分片设计变得没有意义)

2. DTLS 1.3 Record Layer

协议格式

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)  |
    +-+-+-+-+-+-+-+-+ 
  • fixed bits: 标志位的前三个bits是固定的001
  • C: 0x10位标识header中是否包含CID。
  • S: 0x08位标识header中seq number的长度,0表示8-bit,1表示16-bit
  • L: 0x04位标识header中是否包含length字段,有时候DTLS record独占一个UDP包,则不需要length来表示包长度
  • E: 0x03 两位是epoch值的最后两个bits,epoch在DTL1.3中的重要性得到提升,后面会说到。
  • Connection ID: 可变长度的CID,长度在握手过程协商好,所以header里不需要传CID的length。
  • Seq number: 是当前seq number值的低8/16 bits
  • Length: 和TLS1.3一样

在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

AEAD 认证加密保护

和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;

协议并没有规定具体什么做,只是给出了建议。

Epoch 含义

TLS1.3中连接的不同阶段采用的加密等级/加密secret不同(TLS1.2则只有加密和不加密两种),此外握手完成后TLS1.3还会进行key udpate更新secret。这种加密等级的同步在TLS中由于基于可靠传输,加密等级状态在双端是自动(隐式)同步的。在DTLS中则需要显式地标识当前包的加密等级/状态,使得从DTLS1.2中就引入的epoch字段重要性得以加强。

  • epoch 0 用于未加密的消息:ClientHello、ServerHello、HelloRetryRequest
  • epoch 1用于使用client_early_traffic_secret保护的消息,即client的0-rtt包
  • epoch 2 用于使用[sender]_handshake_traffic_secret保护的消息,包括:EncryptedExtensions, CertificateRequest, Certificate, CertificateVerify, and Finished
  • epoch 3用于使用[sender]_application_traffic_secret_0 保护的应用数据消息以及post-handshake消息
  • epoch 4 - 2^16-1 用于[sender]_application_traffic_secret_N 保护的消息。

Seq Number加密

DTLS1.3中对seq number进行加密,过程和QUIC的header encrption非常类似。都是从 ciphertext采样16个字节,通过AES-ECB/ ChaCha20再加密计算出一个Mask,然后将seq_number和这个mask进行XOR。

注意Seq number加密只针对DTLSCiphertext生效。

其他record layer层改动

和DTLS1.2一样,需要考虑PMTU问题。另外需要对Record进行防重放(1.3协议中没有提到防重放是可选的了),做法也都类似。

对于防重放时一个需要注意的地方是实现上需要防止timing side channel攻击,和QUIC一样,对任何packet需要先header deprotection → packet deprotection → replay protection

3. DTLS 1.3 Handshake Protocol

DTLS1.3的握手过程和TLS1.3的握手过程基本一致,不同的地方基本也和DTLS1.2一样:

  • 为了防止DOS而引入的stateless cookie exchange机制,由于TLS1.3定义了HelloRetryRequest和cookie扩展,所以不再需要DTLS1.2 中的HelloVerifyRequest消息了,并且ClientHello中的cookie字段也被废弃了,但是为了做前向兼容还是保留了位置。这里需要说明一点,HelloRetryRequest消息在TLS1.3中主要是为了让server在无法和client成功协商出一致的密钥的时候让client有次重试的机会,而在DTLS中HelloRetryRequest则为了server能够验证client的地址有效性。
  • 为了保证Handshake消息的有序性在Handshake header里引入message_seq字段(和 DTLS1.2一样)
  • 为了避免IP 分片,Handshake层需要支持分片,因此Header里引入分片offset和len (和DTLS1.2一样)
  • 为了保证 Handshake消息的可靠性,引入简单的超时重传机制,区别在于引入了ACK进行细粒度的重传,解决之前一个分片丢失,整个flight所有消息都需要重传的问题。这个后面详细介绍下。

DTLS ClientHello

为了前向兼容, 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;

DTLS Full Handshake

   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]

DTLS Resumption and PSK Handshake

    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]

DTLS 0-rtt Handshake

   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]

DTLS state machine

                             +-----------+
                             | 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状态下的动作和状态切换

  • SENDING状态下如果收到对端的对刚发送消息的ACK,说明对端接受到了消息,可以从这个消息从buffer里删掉
  • WAITING状态下如果收到了对端的ACK,此时分为两种情况:
    • 收到的是对flight部分消息的ACK,那么需要重传本次flight未ACK的那些消息,状态切换至SENDING、重传消息、重置定时器、切换至WAITING
    • 收到的是对flight所有修消息的ACK,说明此时对端收到所有消息,继续在WAITING状态上等着对端的回应flight 就好,如果本次flight不需要回应,则直接切入FINISHED状态
  • WAITING状态下如果收到了对端flight的部分消息,实现可以在此时发送一个ACK给对端

4. ACK机制

ACK消息虽然作用于Handshake阶段,但是这里独立作为一个section,一方面是因为DTLS ACK消息不属于Handshake消息的一种,而是和Handshake、application、alert同一层的消息类型。ACK消息的结构很简单,只需要给出那些已经收到的records的record_number。

    struct {
        RecordNumber record_numbers<0..2^16-1>;
    } ACK;

发送ACK行为

什么时候应该发送ACK,协议并给出了一些实现上的建议/规定:

  • 当收到flight的部分消息时,可以设置一个定时器,超时时间设置为重传时间的1/4,超时后发送ACK,有点delay ack的意思
  • 当收到flight的所有消息,并且自己不需要再回应消息时(比如收到Client的最后一个flight消息、收到Server的NewSessionTicket、以及KeyUpdate消息时),协议要求必须要发送ACK。

协议也给出几个什么时候不发送ACK的建议/规定:

  • 如果收到了乱序的消息或者分片时选择丢弃这个消息/分片,那么一定不能去ACK这个丢弃了的消息/分片
  • 当收到了flight的所有消息,并且自己需要回应一个flight握手消息时,应该不要去发送ACK,回应的消息已经隐式ACK了
  • 不能ACK那些暂时不能deprotect的消息

Key Upadate & 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。

5. Connection ID更新

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;