TLS 1.3 协议详解

TLS 1.3 握手流程详解

我的TLS实现(支持TLS1.3和国密SSL),大家可以学习参考:https://github.com/mrpre/atls/

需要的背景知识:
(1):对 TLS 1.2 协议有一定程度的了解,包括秘钥交换、会话复用等。

第一节 TLS 1.3 的握手概述

协议分析的第一步就是看报文。TLS 1.3的报文,有个特点,就是通过抓包发现,只能看到明文的Client HelloServer Hello,其余的握手报文均被加密。

1-RTT

如图:

图1
这里写图片描述

条件:无条件(如果client发送的keyshare类型是server不支持,那就不是1-RTT了,此处忽略)
换句话说,TLS 1.3 正常情况下,第一次握手就是1-RTT的。(而TLS 1.3之前的协议只有第二次及其之后的握手且session resume成功后,才有1-RTT)。其次1-RTT又分为两种情况,一种是完整的握手(新的握手,就是上面图1所示),还有一种是不发送early data的PSK握手(可以理解为session 复用),如下图:

图2
这里写图片描述
可见,图2 相对 图1,server少发送了诸多报文(熟悉TLS协议的肯定能猜到,至少是少发了server的证书,好几k的数据)。话句话说,TLS 1.3,PSK handshake和FULL handshake都是1-RTT,而所谓的0-RTT是建立在PSK handshake基础上的,下面就说说0-RTT。

0-RTT

如图3:
图3
TLS 1.3 协议详解_第1张图片
上面第一行,Application Data就是应用数据了,换句话说0-rtt就是在client-hello时就把应用数据捎带发给server(当然,server会根据自己的设置选择相信这个数据,也可以拒绝这个数据)
条件:
(1)之前已经有一次握手,并且server发送了session ticket。且session ticket中存在max_early_data_size拓展表示愿意接受early data。很好理解,0-rtt是基于session 复用的,所以肯定之前已经完成一次握手,且那次握手携带了session ticket以方便下一次握手时完成复用。

(2)第二次握手时,client 发送了psk(其实就是session ticket外加一些检验的东西),server处理session ticket 会话复用成功,即server从client发送的psk中提恢复出了session。

(3)第二次握手时,client 配置了发送 early data,表象就是如上图,Client Hello后面紧跟ccsccs后面紧跟application data(ccs不是必须的),且Client Hello 中的拓展中early data拓展表示会发送early data

(3)第二次握手时,server配置了允许读取early data,且client行为如(2)。表象就是server的Encrypted Extensions中携带了early data拓展表示自己会读取early data。 如果 server没有配置读取early data的选项,那么server就会忽略client发送的Application data。client 需要在应用层主动ssl_write应用数据(通常库函数肯定会告诉你early data有没有被处理)。

综上所述,0-RTT的要求还是蛮严格的,首先第一次握手时,server表示愿意读取early data,其次第二次握手时,会话复用成功,接着客户端表示会发early data且server表示自己会读取early data

第二节 TLS 1.3 1-RTT 流程

第一节中,我们已经看到了TLS 1.3的报文,可惜的是,握手报文是被加密的,看不出什么东西,不知道发了什么东西。本节将描述TLS 1.3下的诸多改进,以及详细的TLS 1.3 的报文。

1-RTT 概述

(1)TLS 1.3 相较于之前的版本,多了许多握手类型报文,比如Encrypted Extensions End of early data等。
(2)除了Client HelloServer Hello其余报文全部都被加密,这就导致了你无法从wireshark的抓包结果中学习。
(3)server 发送 certificate verify。之前协议中,只有客户端认证时,client才会发送certificate verify

上述这些都是比较明显的区别,不明显的区别后文会一一阐述。

接下来先回顾老的协议,再说新的协议。

TLS 1.3之前的握手流程

首先回忆一下,TLS 1.3 之前是如何秘钥交换的。

这里简单的描述下 TLS 1.3 之前协议的秘钥交换流程,以及其缺点

RSA 秘钥交换
1:client 发起请求(Client Hello
2:server 回复 certificate
3:client使用证书中的公钥,加密预主秘钥,发给 server(Client Key Exchange
4:server 提取出 预主秘钥,计算主秘钥,然后发送对称秘钥加密的finished
5:client 计算主秘钥,验证 finished,验证成功后,就可以发送Application Data了。

缺点:RSA秘钥交换不是前向安全算法(证书对应私钥泄漏后,之前抓包的报文都能被解密)。

所以在 TLS 1.3中 RSA 已经废弃了。

ECDHE秘钥交换
1:client 发送请求(Client Hello),extension携带支持的椭圆曲线类型。
2:server 回复 Server Hellocertificate等;server选择的椭圆曲线参数,然后 生成私钥(BIGNUM),乘以椭圆曲线的base point得到公钥(POINT),顺便签个名表示自己拥有证书,然后将报文发给client,报文就是Server Key Exchange,其包含了server选择的椭圆曲线参数、自己根据这个参数计算的公钥、自己用证书的私钥对当前报文的签名。
3:client 收到 Server Key Exchange,获得椭圆曲线参数,生成私钥(BIGNUM)后计算公钥(POINT),然后把公钥发出去Client Key Exchange。client使用自己的私钥(BIGNUM)和server的公钥(POINT)计算出主秘钥。
4:server 收到 client的公钥(POINT),使用自己的私钥(BIGNUM),计算主秘钥。两端主秘钥是一致。

缺点:
client发送自己支持的椭圆曲线类型,然后等待server选择后,才计算自己的公钥然后发送给server。这个可以优化。

TLS 1.3 中是这样优化握手的:
1:client 发送请求(Client Hello),extension携带支持的椭圆曲线类型。且对每个自己支持的椭圆曲线类型计算公钥(POINT)。公钥放在extension中的keyshare中。
2:server 回复 Server Hellocertificate等;server选择的椭圆曲线参数,然后乘以椭圆曲线的base point得到公钥(POINT)。然后提取Client Hello中的key_share拓展中对应的公钥,计算主秘钥。公钥(POINT)不再和之前的以协议一样放在Server Key Exchange中,而是放在Server Hellokey_share拓展中。client收到server的公钥(POINT)后计算主秘钥。

所以在TLS 1.3 中最显著的变化,也即上文说的无条件的1-RTT就是基于对握手协商的优化生。

所以,Client 相当于计算了多种可能情况下server会使用的ecdhe参数,然后根据这些参数计算公钥提前发给server = =(为了追求rtt,也是绝了),你让那种嵌入式的客户端怎么办。

完整的 TLS 1.3 1-RTT描述

由于TLS 1.3几乎所有的报文都被加密,不好分析这里使用了写手段,将其解密开来,好可以在wireshark中进行直观的分析(修改方法:找一套开源代码,将其加解密流程bypass,然后用这套代码,起一个server和client,例如在OpenSSL中,我将加解密用的ctx全部强制置为NULL,保证了加解密都bypass)。
顺便提一句,一张证书链怎么也得2、3k,这比通常的应用数据都长,原先传输证书是没有加密的,而TLS 1.3是加密的 = = 真把服务器当牛来用
其流程如下图(解密后的):

这里写图片描述

Full handshake

1:client发送Client Hello,携带如下几个重要信息
(1):支持的加密套件(该作用和之前一样)。
(2):supproted_versions 拓展。包含自己支持的TLS协议版本号。(之前协议没有)
(3):supproted_groups 拓展,表示自己支持的椭圆曲线类型。
(4):key_share拓展。包含supprot_groups中各椭圆曲线对应的public key。(当然可以发送空的,然后server会回复hello request,其中会包含server的key_share,可以用来探测,这里不讨论)。key_share中的椭圆曲线类型必须出现在supproted_groups中。(之前协议没有)

2:server 发送Server Hello,携带如下几个重要信息
(1):supproted_versions 拓展。包含自己从client的supproted_versions中选择的TLS协议版本号。(之前协议没有)
(2):key_share拓展。包含自己选中的椭圆曲线,以及自己计算出来的公钥。(之前协议没有)

3:server 发送Change Cipher Spec。(允许不发送)

4:server发送Encrypted Extension。(加密的)
(1:):ServerHello之后必须立刻发送Encrypted Extension。这是第一个被加密的数据数据。显然,放在这里的拓展,是和秘钥协商没关系的拓展。(之前协议没有)

5:server发送Certificate。(加密的)
这个报文和之前的协议没有太大区别,唯一的区别就是,在证书链中的每个证书后面,都有一个extension。(双向认证时也会有区别,有机会再说)。这个extension目前只能是OCSP Status extensionSignedCertificateTimestamps

6:server发送Certificate Verify(加密的)
这个报文并不陌生,但是以前只出现在双向认证(客户端认证)中,以前Certificate Verify生成的逻辑是将当前所有的握手报文解析解析签名(简单的md+非对称加密)。而在TLS 1.3中,这个计算有些变化,但是还是很简单。计算逻辑如下:

    u8 *p = tbs[BUFFER_SIZE];

    memset(p, 0x20, 64);
    p += 64;
    memcpy(p, "TLS 1.3, server CertificateVerify", 33);
    p += 33;
    *p ++ = 0;
    HASH(ALL_HANDSHKE, ALL_SIZE, P);
    p += md_size;

    SIGN(tbs)

而TLS 1.2的计算是这样的:

    SIGN(HASH(ALL_HANDSHKE, ALL_SIZE));

7:server回复Finished(加密的)
这个报文的目的和之前协议一样,检验握手报文的完整性。但是计算方法有变化。
TLS 1.3 的计算方法,先计算md,然后计算hamc,其中finishkey详细如何生成见key_derive。

    buffer = HMAC(HASH(ALL_HANDSHKE, finishkey)
    buffer 前添加握手报文类型+长度
    加密_send(buffer)

TLS 1.2是这样计算的

    buffer = prf("server finished" + HASH(ALL_HANDSHKE))
    buffer 前添加握手报文类型+长度
    加密_send(buffer)

8:client发送Change Cipher Spec。(允许不发送)

9:client发送 Finished(加密的)
同7。

10:server发送New Session Ticket(可选,0-RTT 依赖次报文)
若server表明自己有能力读取0-RTT,则会在New Session Ticket后面添加一个max_early_data_size拓展。

整个流程的目的和TLS 1.2是相似的,就是为了交换椭圆曲线参数。和之前不一样的就是,无非就是提前把所有的公钥计算了一遍,发给server,server再挑选。

PSK handshake

我还是习惯于叫会话复用。其流程如下图(解密后的)

这里写图片描述
1:client发送Client Hello,除了Full handshake中提到的携带的拓展以外还包含如下字段:
(1):psk_key_exchange_modes,目前只有psk_dhe_ke才能跑的通。
(2):pre_shared_key拓展,其实就是New Session Ticket+binders,由于TLS 1.3 中,Server的New Session Ticket可以在握手结束后随时随地的发送,且可能发送多次,这意味着client会缓存多个New Session Ticket,所以pre_shared_key会保存多对New Session Ticket+binders组合。详细pre_shared_key后文会将,为了方便理解PSK handshake和0-RTT握手,此处的pre_shared_key完全理解为 TLS 1.2中的Client Hello中携带的Session_Ticket_TLS拓展。

2:server发送Server Hello,除了Full handshake中提到的携带的拓展以外还包含如下字段:
(1):pre_shared_key。表示自己正常解析了client发送的pre_shared_key,然后指定字节从client中选择的New Session Ticket+binders组合,用数字0,1,2,3…表示选择了第几个。

3:server发送Change Cipher Spec(允许不发送)

4:server发送Encrypted Extension
Full handshake中一样

5:client发送Change Cipher Spec(允许不发送)

6:client发送 Finished(加密的)
Full handshake中一样

7:server发送New Session Ticket(可选)
Full handshake中一样

可见,他和TLS 1.2的session ticket复用没什么区别,无非就是处理session ticket的方式有些改变而已。

TLS 1.3 0-RTT 流程

上面讲了1-RTT的情况,TLS 1.3 默认最多1-RTT(不考虑Hello request),所以从这个角度上来说,TLS 1.3已经相较于 TLS 1.2 有了握手速度的提升,但是最终极的做法是0-RTT。0-RTT必然存在重放攻击,但是在Google的威逼利诱下,TLS 1.3 还是同意支持了0-RTT。后面会说0-RTT下重放攻击。

文章开头TLS 1.3 的握手概述一节中,对0-RTT已经有了一些描述,但是不详细,这里根据报文详细描述下其流程。

RFC中的描述

            ClientHello
            + early_data
            + key_share*
            + psk_key_exchange_modes
            + pre_shared_key
            (Application Data*)     -------->
                                                            ServerHello
                                                       + pre_shared_key
                                                           + key_share*
                                                  {EncryptedExtensions}
                                                          + early_data*
                                                             {Finished}
                                    <--------       [Application Data*]
            (EndOfEarlyData)
            {Finished}              -------->
            [Application Data]      <------->        [Application Data]

完整的TLS 1.3 0-RTT 描述

0-RTT是基于上文中PSK handshake为基础的,所以这里和PSK handshake一样的地方直接一笔带过。

1:client发送Client Hello,除了PSK handshake中提到的携带的拓展以外还包含如下字段:
(1):early_data拓展,表示其在Client Hello后面紧跟着early data

2:client发送Change Cipher Spec。(可以选,允许不发送)

3:client发送application data
这个application data是被加密的应用层数据,例如GET /...,可以发送好几个Application Data

4:server发送Server Hello
PSK Handshake

5:server发送Change Cipher Spec(允许不发送)

6:server发送Encrypted Extension。除了PSK Handshake中提到的携带的拓展以外还包含如下字段:
(1):early_data拓展,表示其愿意接受Client Hello发送的early data

7:server发送Finished

8:client发送完early data后,发送End_Of_Early_Data报文表示client自己发送完了early data
注意,early data并不算在最后的Finished中,其次,End_Of_Early_Data不参与最后application traffic secret的计算。

9:client发送完End_Of_Early_Data后,发送Finished

10:server发送New Session Ticket(可选)。

11:server处理early data,这一步可能是server边收边处理的,所以放在3与4中间也行。

0-RTT 处理顺序

作为client,client发送early data之后,可以一直发early data
作为server,接受Client Hello后,立刻发送自己的数据Server HelloChange Cipher SpecFinished
作为client,当收到server的Finished后,才允许发送End_Of_Early_Data
作为client,在如果server没有接受自己发送的early data,则不发送End_Of_Early_Data

0-RTT 降级到 1-RTT

上文TLS 1.3 的握手概述中描述过,完成0-RTT的条件有如下2个
1:PSK Handshake成功
2:Server配置了接受0-RTT
那么自然,1和2都有可能发生错误(无论主观还是客观),肯定会由0-RTT降级到1-RTT的情况。降级必然需要妥善处理early data

对于server来说,拒绝early data,有2种程度的方式可以拒绝:
(1):拒绝PSK Handshake,即Server Hello中,不加入pre_shared_key拓展。这个常见于session ticket过期等情况。不支持PSK Handshake,自然而然不支持early data
(2):只拒绝early data,但允许PSK Handshake。即Server Hello中,加入pre_shared_key拓展,但是Encrypted Extension报文中不加入early_data拓展。这个常见于server不准备读取early_data的情况。

无论(1)和(2),都会忽略client发送的early data,如何忽略?其实如果server不准备读取early data,其当前阶段的secret导出是handshake secret而不是early secret(即想解密的是握手报文而不是early data),所以当拿这个secret去解密early data必然会出现错误,话句话说,所谓的忽略,就是忽略解密错误

TLS 1.3 中的 New Session Ticket

TLS 1.2 中的New Session Ticket

      struct {
          uint32 ticket_lifetime_hint;
          opaque ticket<0..2^16-1>;
      } NewSessionTicket;

很简单的格式,一个是ticket_lifetime_hint,告知客户端这个ticket的生命周期(老化时间),ticket就是主要包含了主秘钥等其他信息(客户端认证的话可能还包含客户端的证书)。

只有在Client HelloServer Hello中都携带了session ticket拓展,server才能在Change Cipher Spec之前发送New Session Ticket

TLS 1.3 中的New Session Ticket

      struct {
          uint32 ticket_lifetime;
          uint32 ticket_age_add;
          opaque ticket_nonce<0..255>;
          opaque ticket<1..2^16-1>;
          Extension extensions<0..2^16-2>;
      } NewSessionTicket;

ticket_lifetime和TLS 1.2中的ticket_lifetime_hint含义一样,用以告知客户端ticket的老化时间;ticket_age_add作用讲PSK还会再说,这里我们需要知道的是,该值主要是用来混淆PSK中携带的session ticket老化时间。ticket_nonce目前都是是一个counter,,初始值是0,发送一次后,counter++。ticket和TLS 1.2 中的含义类似,只是这里是PSK,而不是master_secretExtension extensions目前只定义了一种拓展:max_early_data_size,该值的作用在上文多次提到过,就是表明server愿意接收多少early data,超过这个值,server会断开连接。
这个PSK,需要着重说一下,RFC中其计算流程如下:

HKDF-Expand-Label(resumption_master_secret,
                "resumption", ticket_nonce, Hash.length) = PSK

入参的核心是,resumption_master_secret,它是由Master Secret进行derive后获得的(详见key derive)。

TLS 1.3 中的 pre_share_key

先看一下RFC中对应的格式:

      struct {
          opaque identity<1..2^16-1>;
          uint32 obfuscated_ticket_age;
      } PskIdentity;

      opaque PskBinderEntry<32..255>;

      struct {
          PskIdentity identities<7..2^16-1>;
          PskBinderEntry binders<33..2^16-1>;
      } OfferedPsks;

      struct {
          select (Handshake.msg_type) {
              case client_hello: OfferedPsks;
              case server_hello: uint16 selected_identity;
          };
      } PreSharedKeyExtension;

上面的意思是说,对于Client Hello而言,发送的是OfferedPsks,也是这节我们关注的。(Server Hello中携带的PSK拓展,上文已经提到过了,也描述了其作用)。我们要关注3个值。
1:Obfuscated Ticket Age
2:Identity
3:PSK binders

Obfuscated Ticket Age

个人理解,首先在TLS 1.2中,client发送的Session_Ticket_TLS拓展,并不携带该ticket在客户端已存在的时间。server收到后,靠ticket里面的内容来判断收到的ticket是否过期(例如server发送的ticket里面增加时间相关信息,收到后校验、或者server会定时更新解密key,解密失败意味着ticket过期),而在TLS 1.3中,特地增加一个值,表示ticket的年龄(即在客户端存在的时间)。但是,Client Hello是明文传输的,这个年龄必然也是明文传输的,所以中间人能够看到(看到有什么坏处呢?我不是很清楚,反正就是这么说的)。为了不让中间人知道这个ticket的年龄,很容易想到的是,对这个值 加盐。这个盐就是server端发送的New Session Ticket中的ticket_age_add,因为New Session Ticket本身就是被加密的,所以这个ticket_age_add只有通信两端才知道。
Obfuscated Ticket Age的计算方法,RFC中也提到了:

The "obfuscated_ticket_age" field of each PskIdentity contains an obfuscated version of the ticket age formed by taking the age in milliseconds and adding the 
"ticket_age_add" value that was included with the ticket (seeSection 4.6.1), modulo 
2^32

obfuscated_ticket_age = (uint32)(jiffies + ticket_age_add)

其次,Clienthello携带这个时间信息还有一个好处就是,Server可以依据这个时间来快速拒绝0-RTT。Server解密客户端发送Session Ticket后,提取出ticket_age_add,根据公式获取到这个Session Ticket在客户端的age ,判断这个ticket时间是否在自己可接受的范围之内。

Identity

其内容就是NewSessionTicket中的ticket部分。server用来恢复session的。这里不赘述了。

PSK binders

先说一下,对于client,PSK binders是怎么计算的:
1、首先计算一个binder_key

                 0
                 |
                 v
   PSK ->  HKDF-Extract = Early Secret
                 |
                 +-----> Derive-Secret(.,
                 |                     "ext binder" |
                 |                     "res binder",
                 |                     "")
                 |                     = binder_key

(1)第一步就是获取PSK
PSK哪里的?其实就是从New Session Ticket中的ticket恢复得到。

(2)第二步就是PSK和0进行HKDF-Extract计算Early Secret
(3)第三步就是计算binder_key

2、计算Finished key

The PskBinderEntry is computed in the same way as the Finished message (Section 4.4.4) but with the BaseKey being the binder_key.

所以我们需要计算Finished key

3、计算当前client hello的摘要md
注意这个不包括PSK BindersPSK Binders length,否则就出现鸡生蛋蛋生鸡的问题了。

4、将md进行hmac计算,hmac的key是Finished key

结尾

无语 搞这么复杂,为了减少 RTT,代价就是增加 client 和 server的性能消耗。

你可能感兴趣的:(TLS,网络协议,SSL/TLS协议详解,TLS)