添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
旅途中的西装  ·  Dart ...·  1 年前    · 
逼格高的伤痕  ·  c++ stringstream move ...·  1 年前    · 
刚毅的啤酒  ·  typescript - Angular ...·  1 年前    · 

TLS 全称为 Transport Layer Security(传输层安全),其前身是 SSL,全称为 Secure Sockets Layer(安全套接字层),它的作用是为上层的应用协议提供安全的通信,比如众所周知的 HTTP + TLS = HTTPS。

SSL 2.0 是该协议的第一个公开发布的版本,由于其存在的安全问题很快被升级到了 SSL 3.0,并且在 1999 年,IETF 小组将该协议标准化,因此 TLS 1.0 诞生了。

本文将介绍 TLS 1.0 到 TLS 1.3 的握手流程,你只需要一点点的 SSL\TLS 或 HTTPS 的前置知识与了解过简单的密码学上的概念,如对称加密、非对称加密、哈希算法等名词。

我们先从一个最简化的 TLS 握手流程开始:

  • Client 向 Server 请求建立连接。
  • Server 将自己的证书发送给 Client。
  • Client 验证证书,然后使用证书中的公钥加密接下来要用来通信的密钥,将加密结果发送给 Server。
  • Server 收到后进行响应,且将用该密钥来对需要发送或接收的上层数据进行加解密。
  • 自此 TLS 握手完成,接下来开始使用密钥进行通信。
  • 以下为简单的流程图:

    在上面中,我们使用的是 RSA 算法来进行密钥的交换。

    RSA 应该是最多人了解的非对称加密算法,其在这里用于密钥协商(交换)和证书验证,这分别用到了它的两个特性:

  • 公钥加密,对应私钥才能解密
  • 私钥加密,对应公钥可以解密
  • 证书机制用到了第二个机制。

    证书实际上就是一段符合指定标准的文本,其大致上记录了:

  • 证书所属的域名
  • 域名所有者的公钥
  • 使用的加密算法
  • CA(证书颁发者)会对这些内容进行使用哈希函数导出一个值(简单来讲就是用 md5 类似的算法计算一下),该值将被 CA 的私钥进行加密(这样的操作也叫做"签名"),同时签名也将被记录到证书上。

    而"证书验证"这个行为,当然指的就是使用证书颁发者公布的公钥,来对被加密的值进行解密,然后对证书使用同样的哈希函数进行计算,并使用计算出的值与解密出的值进行对比,如果相等则说明没有被篡改过,可以放心的使用证书上的公钥进行通信。

    而在上文的握手过程中,由于通信密钥在发送给 Server 时被公钥所加密了,而用来解密的私钥自始自终都只被证书申请者持有,所以即使有人截取了所有通信的信息,都不能得到通信的密钥(当然这是在假定密钥永远不会泄露的情况下)。

    这样,我们就建立了一个安全的"信道"。

    嗯...到这里还算是比较常规,接下来就是实际上的 TLS 协议了,不过规范定义中的流程和上面的版本有一点点变化

    TLS 1.0

    Client 向 Server 发送一个随机数、客户端可使用的加密组件、压缩算法和可能的 Session ID。

    Server 接收到消息后返回证书、Session ID、准备使用的加密组件和可能使用的压缩方法,同时还会生成一个随机数并返回。

    Client 收到后进行证书的验证,然后根据密钥协商协议向 Server 发送规定的密钥交换信息。

    Server 和 Client 一起使用 \(pre\ master\ secret\) 与之前的随机数,通过协商好的加密算法生成 \(master\ secret\)

    然后,Server 和 Client 将开始使用 \(master\ secret\) 作为通信密钥进行通信。

    紧接着,Client 和 Server 互相向对方发送本次握手中所有报文生成的 摘要 (Hash 算法的计算结果),同时也是生成了 \(master\ secret\) 后发送的第一条加密信息。

    当 Client 收到 Server 发来的摘要后,就可以正式开始发送应用数据。

    如果你是第一次了解到以上的握手流程,那在看完后多半会发出一个“?”,所以我们接下来将详细的介绍各个步骤。

    RSA 加密组件

    首先需要介绍的是加密组件,TLS 1.0 中的非对称加密的算法包含 RSA 和 DH/DHE,在密钥协商阶段我们可以任意选用一种,我们先来了解使用我们最熟悉的 RSA 作为密钥协商算法的情况。

    Client 向 Server 发送一个随机数、自己支持的加密组件、压缩算法和可能的 Session ID。

    这一步很好理解,Client 需要告知 Server 自己支持的加密算法,让 Server 进行选择,接下来才能进行通信。同时我们可以告知 Server 我们希望使用的压缩算法,如果 Server 也支持,那在之后传输上层的应用数据时将会使用该压缩算法进行解压缩。

    需要提醒的是,压缩功能 TLS 不建议启用,甚至在 TLS 1.3 时相关功能被直接禁用。

    这一方面是由于压缩功能并不是 TLS 协议的本职,同时也是因为压缩功能带来了安全上的问题。

    "可能的 Session ID"则是在 Client 在重连 Server 时减少握手成本的一个机制,即在重连时并不需要重新走一次握手流程,只需要带着 Session ID,就能重新连接,具体的使用我们会在后面再详细介绍。

    在 Server 收到 Client 的连接建立的请求后,将返回自己的证书;并且如果启用了 Session ID 的话则会分配一个 Session ID 并返回,这样的话当下次建立连接时,Client 将会带上 Session ID,就不需要走完整的握手过程。

    当然 Session ID 也可以为空,这样的话代表 Server 不希望缓存会话。

    并且还会根据自身的情况返回接下来将使用的加密套件名称和压缩算法的名称,同时还会生成一个随机数并返回。

    Client 在收到证书后,将进行"验证证书",证书合法才会继续下一步。

    然后,Client 将生成 \(pre\ master\ secret\) ,具体是啥我们看一下它的结构就知道了:

    struct {
      ProtocolVersion client_version;
      opaque random[46];
    } PreMasterSecret;
    

    第一个字段是客户端版本,用来检测 降级攻击;第二个字段则是一个随机数。

    降级攻击(downgrade attack)

    攻击者故意欺骗 Server 和 Client 使用旧的、不安全的协议,即使它们都支持最新的协议。常用于中间人攻击中,由于握手阶段是明文的,所以攻击者可以任意的篡改,故需要防止这种攻击。

    比如美国在早期时候禁止超过指定长度的算法出口,所以 TLS 带有了一些不安全的算法。攻击者可以尝试修改明文报文来欺骗双方使用不安全的 TLS 版本(比如现在正在介绍的 TLS 1.0),以此可以更简单的破解中间使用的不安全的算法加密的应用数据。

    但由于 TLS 在这里加上了 client_version 字段,并且本条数据将会被加密发送,所以实际上攻击者无法篡改。

    \(pre\ master\ secret\) 是用来生成 \(master\ secret\) 的重要参数,所以接下来将使用证书上的公钥对 \(pre\ master\ secret\) 加密,并发送给 Server

    接下来,Client 和 Server 双方都将使用(客户端随机数)+(服务器端随机数)+( \(pre\ master\ secret\) )来生成 \(master\ secret\)

    \(master\ secret\) 将被作为通信的密钥。不过,我们为什么要这么做呢?

    为什么不直接使用公钥加密 \(master\ secret\) 的方式来做密钥交换?

    仔细一想,这不是挺好的吗?虽然握手流程一次也少不了,但至少少了好几个"莫名其妙"的参数?

    不过实际上,如果不考虑其他密钥协商算法的话,这确实是一个不错的选择。但是别忘了,在我们思考的上面的密钥协商的流程中,使用的加密套件是 RSA,而对于 TLS 协议来讲,还具有其他密钥协商算法,例如在 TLS 1.0 中就具有 RSA 和 DH/DHE 两种,所以这其实是一个协议设计上的问题。

    TLS 协议加入了 \(pre\ master\ secret\) 来生成 \(master\ secret\) 的流程,最主要还是为了使得协议能更好的模块化(应该算是一种"模板方法"的思想?)

    为什么要使用到这么多随机数?

    对于 \(master\ secret\) ,TLS 1.0 中的生成公式为:

    \[\begin{aligned} master\_secret=PRF(&pre\_master\_secret,\\ &\text{"master secret"},\\ &ClientHello.random + ServerHello.random) [0..47]; \end{aligned} \]

    其中 \(PRF\) 我们可以简单理解为一个 Hash 函数。然后我们首先来考虑 \(pre\ master\ secret\) ,这是可以去掉的随机数吗?

    应该不是,对于 RSA ,它是属于由客户端生成的随机数;但对于 DH/DHE 来讲,其是通过密钥协商得到的密钥(具体会在后面介绍),所以不属于随机数,不能够被去掉。

    那服务器端随机数呢?对于服务器端随机数,它最大的用途就是用来防止 重放攻击

    重放攻击(replay attack)

    该攻击指的是在通信的时候,攻击者截取通信的一部分,然后在日后的某时重新向服务器发送该段被阶段的报文。

    最重要的是,即使通信的信道是保密的(通信信息被加密),攻击者也可以使用该方法来进行捣蛋,即使它并不能理解通信的信息。

    比如在下单付款时,攻击者截取了你的从下单到付款的 HTTPS 的流量,然后进行重放,使得你多下单付款了好几个订单。(当然这个例子不够恰当,因为对于付款这种流程几乎都是要多因素身份验证(MF)的,比如使用支付宝付款时手机至少得接收个验证码吧,更何况支付的时候肯定是有对应的幂等性方案的)

    所以,由于我们在通信密钥的生成中加入了来自 Server 的随机数,所以即使攻击者收集了 Client 发向 Server 的报文,但由于每次的服务端随机数都不一样,所以两次握手协商出的 \(master\ secret\) 必然是不一样的,这样的话攻击者最后会在发送第一次消息的阶段就会露馅。

    最后再看看客户端随机数,同样的这里的随机数也可以用来防止攻击者收集 Server 发向 Client 的数据进行重放(不过这样做大多数情况下意义不大)。并且客户端随机数还可以为最后的 \(master\ secret\) 的生成贡献"熵"(相当于盐(salt)一样)。

    在得到了 \(master\ secrrt\) 后,双方会计算出本次握手中的包的摘要,并直接发送给对方(已被加密),当一方收到来自另一方的发来摘要后,将正式进行上层应用数据的通信。

    其流程图大概是这样:

    在介绍 DH 加密组件的握手流程之前,我们先了解下 DH 算法具体是干啥的

    DH 算法介绍

    DH 算法也属于非对称加密算法,其在此用于密钥协商(和 RSA 相比,RSA 更像是一个密钥交换算法),其目的是在不安全的信道下协商出一个密钥以加密未来的通信信息使得建立一个安全的信道(不可监听、不可篡改)。

    对于 DH 算法的具体内容,由于我们要讨论的不是算法的实现,所以为了避免陷入过多的数学细节,我们将其具体算法抽象为一个函数(你也可以点击这里了解 DH 算法的具体实现):

    \[f(p,g,n)=N \]

    该函数中,\(p\)\(g\) 在一次密钥的协商的过程中是不变的,我们只需要关心 \(n\)

    该函数的 \(n\) 我们称为私钥,计算出的 \(N\) 称为公钥,且根据 \(N\) 难以推出 \(n\) (就像是哈希函数一样);而对于使用同一个 \(p\)\(g\) 计算出的多个密钥对(\(a\), \(A\))、(\(b\), \(B\)),具有一个特殊的性质:

    \[p(a,B)=K=p(b,A) \]

    其中 \(K\) 为协商出来的密钥。但这又有什么用呢?我们再来重新回顾下握手流程,这次使用 DH 作为密钥协商算法。

    第一步与使用的密码套件无关,因此客户端的行为是相同的。

    我们具体来看第二步,这一步中主要的不同在于返回的证书,这里的证书分两种,一种是在 RSA 中见到的,记录的是 RSA 的公钥,而另一种记录的是 DH 算法中的 \(p\)\(g\)\(B\)\(B\) 是由 Server 的私钥 \(b\) 通过 \(f(p,g,n)\) 函数计算得到的公钥)。

    在 DH 密码组件中,DHE 算法会使用前者,而 DH 算法会使用后者,这两者之间的区别在于是否具有"前向安全性"(后文会详细介绍)。

    在第二步中,如果使用的是 DH 算法,将会直接把证书发送过去;而如果是 DHE,则除了发送证书外,还会使用证书上的 RSA 密钥对来对新生成的 \(p\)\(g\)\(B\) 进行签名,并和这几个参数一起向 Client 发送。

    在 TLS 1.2 时,除了对 \(p\)\(g\)\(B\) 签名,还会对客户端随机数和服务器端随机数进行签名。

    在第三步中,当 Client 验证完证书后,接着会向 Server 发送进行密钥协商所需要的信息。对于 RSA 来讲,这一步要向 Server 发送 \(pre\ master\ secret\) ,而对于 DH 算法,则需要发送的是 \(A\)\(A\) 是由客户端生成的私钥 \(a\) 通过 \(p\)\(g\) 计算所得出的公钥。

    其中客户端得到的 \(p\)\(g\) 来自于证书或为直接收到的参数。

    接下来 \(A\) 被发送给 Server 后,Client 接着使用 \(p(a,B)\) 计算出 \(K\) 作为 \(pre\ master\ secret\) ,参与最终的 \(master\ secert\) 的计算。

    而对于 Server,也可以使用 \(p(b,A)\) 生成 \(K\) 作为 \(pre\ master\ secret\) ,由于我们在上面介绍的特殊的性质,这两个 \(K\) 一定是相等的。

    之后的过程则是和使用其他密钥组件相同。

    以下为简单的流程图:

    我们再来思考一下,这种方法安全吗?

    首先考虑我们作为中间人能获得到哪些信息,在整个握手的流程中,我们可以截获 \(A\)\(B\)\(p\)\(g\) 、证书和两个随机数,但是参与 \(K\) 的计算的 \(a\)\(b\) 都没有被泄露,且不可能根据 \(A\) 计算出 \(a\) 或根据 \(B\) 计算出 \(b\) ,所以最后用来通信的 \(master\ secert\) 也不可能被知道。

    那中间人攻击呢?如果我们能将 \(B\)\(b\) 替换为作为攻击者的我们的 \(C\)\(c\) ,那么也是可以得到 \(master\ secret\) 的。但是, \(B\) 要么是直接被证书所保护(DH 算法),要么是被证书上的 RSA 密钥对签名(DHE算法),所以篡改也变得不可能了。

    当然以上只是简单的推理,实际上在密码学中会进行严格的证明

    到这里,你发现了 TLS 1.0 的几个缺点了吗?

    最显著的,就是"慢",每建立一次 TLS 连接都至少需要 \(2\cdot RTT\) ,如果 http 使用 TLS 进行保护,那么如果每次加载一个资源都建立一条连接,那不是每个资源都会比普通的 http 慢上 \(2\cdot RTT\)

    别担心,这里只是举个例子,http 1.1 时就默认开启了持久连接,http 2.0 时启用了二进制分帧,一般不会为单独一个资源建立一条连接的,因此其实只会在初次连接时慢。

    实际上 TLS 也是有对应的解决方案的,使用的当然就是万能的缓存,还记得在一开始提到的 Session ID 吗?

    Session ID

    Session ID 为第一次握手时候发送的 Session ID 字段的值,服务器端会维护对于该 Session ID 对应的通信密钥,当 Client 再次连接的时候,只需要在头部中加上 Session ID,即可通知 Server 重新使用上次协商过的通信密钥进行通信,这样就不需要每一次都重新协商来浪费资源,同时由于不需要完整的握手过程,所以握手时间也减少了 \(1\cdot RTT\)

    但这样的缺点也是十分明显的,首先是单机问题,一个池子只能在单台机器上使用。并且当连接数较大时效果可能会降低,这是因为当我们的缓存池过小会起不到作用,因为连接数大时之前的 Session ID 会很快的失效;而缓存池大时,Server 则需要更多的内存资源去维护 Session ID 到通信密钥的映射。

    Session Ticket

    不过除了 Session ID 外,还有一个叫做 Session Ticket 的东西。和 Session ID 相同,Session Ticket 也是一种无需重新协商密钥的方案。

    当 Client 明确支持 Session Ticket 时,Server 会在握手结束后,向 Client 发送被加密的恢复连接所需要的数据,同时 Server 无需保存任何信息,且由于此时握手已经完成,所以信道是安全的。

    在下次建立 TLS 连接时,Client 只需要在第一次握手时发送上次收到的"被加密的恢复连接所需的数据",Server 则可以通过只有自己知道的密钥解密该握手数据,并重新使用上次的通信密钥。

    一个更简单的理解:Session ID 相当于 HTTP 中的 Session,Session Ticket 相当于 HTTP 中的 Cookies

    以下为使用 Session ID 或 Session Ticket 时的握手流程:

    需要注意的是,Client 在第一次握手时仍然携带了较为完整的握手报文,这是为了当 Server 发现 Session ID 或 Session Ticket 过期的时候能够方便的退化为普通的握手流程。

    前向安全性

    前向安全性(Forward Secrecy)指的是过去的通信的安全性不会受到未来的密钥泄露事件的影响。RSA 和 DH 的密钥协商方式在 TLS 1.3 中被删除就是因为不具备前向安全性。

    拿 RSA 举例,如果我们一直使用 RSA 的密钥协商方式进行通信,虽然攻击人不能立刻的解密出通信密钥,但是可以持续的收集这些被加密的通信内容。直到某一天你的服务器终于被攻破,证书对应的私钥被攻击人拿到,那么他将可以使用私钥解密你在之前的所有连接建立时被加密的 \(pre\ master\ secret\) ,并生成 \(master\ secret\) ,然后可以获得使用该密钥加密的所有会话数据。

    除了服务器被攻破外,社会工程学和"FBI open the door"时的情况是一样的,它们都属于密钥泄露事件

    在这种情况下,由于被攻破的时间点前的被加密的通信数据会受到影响。所以不具备前向安全性。

    同样的,静态的 DH 算法由于公钥是固定在证书上的,所以也不具备前向安全性。

    那 DHE 呢?由于它的私钥 \(n\) 每次都会重新生成,其证书只用于保证公钥 \(N\) 没有被篡改,在一次通信完成后私钥就会被丢弃,所以攻击者无法取得。中间人即使获得了证书的私钥,也只能使用中间人攻击获得在那之后的通信数据,而在那之前的通信数据则无法取得,所以说 DHE 具备前向安全性。

    TLS 1.2

    False Start

    TLS 的握手流程在 1.1 和 1.2 的标准发布时都没有发生较大的变化,不过在 RFC7918 中对于 TLS 握手增加了一个叫做 False Start (抢跑)的小优化。

    该优化在于 Client 无需等待收到来自 Server 的 finish 响应就可以开始发送被加密的业务数据,就是说减少了一个 \(RTT\)

    不过为了安全(由于是在 finish 消息前就开始发送业务数据,所以报文有被篡改的风险),这个小优化需要使用的密钥协商算法具有前向安全性(例如 DHE 或 ECDHE)。

    当启用 False Start 时的 DHE/ECDHE 算法的握手流程如下:

    TLS 1.3

    TLS 的握手流程从 1.0 发布(1999)到 1.3 正式发布(2018),终于迎来了较大的变动(在这之前的 1.1 和 1.2 大多只是对于加密算法的更新和安全漏洞的修复)。

    首先,之前我们介绍过的 Session Ticket 机制和 Session ID 机制都被废弃了,其使用了新的机制来代替,并且改变了密钥派生的机制,废弃了一大堆已经不安全的算法,同时直接用 RSA 来交换密钥的方式和 DH 密码套件被废弃,现在使用的是 DHE/ECDHE,并且握手信息也被部分的进行了加密。

    现在的连接建立的方式大体上使用的是 DHE/ECDHE 或 PSK。

    其中对于握手流程来讲,最大的变化是之前介绍的 \(2\cdot RTT\) 变成了 \(1\cdot RTT\) 甚至 \(0\cdot RTT\) 。而之所以能做到 \(1\cdot RTT\) ,是因为 TLS 1.3 中将 Client 进行密钥协商的时机提前了。

    具体怎么做的呢?我们先来简单回忆一下 Client 的密钥在 1.3 前的 DH(E) 算法下是怎么进行协商的:

  • Client -> Server:请求建立连接
  • Server -> Client:生成 \(b\) 并计算出 \(B\) 发向对方
  • Client -> Server:生成 \(a\) 并计算出 \(A\) 发向对方
  • 双方根据 \(a\cdot B\)\(b\cdot A\) 计算出密钥 \(K\)
  • 在这里我们可以看出 Client 向 Server 发送密钥是在收到 Server 的响应后的,那为什么要这么晚才发送呢?这是因为函数 \(f(p,g,n)=N\) 的计算需要 \(p\)\(g\) ,而他们又在 Server 的证书里或被证书所签名,所以需要等到收到 Server 证书后才能计算。也就是说,只要我们能够提前得知 \(p\)\(g\) ,就能在第一个握手时就发送 \(A\)

    TLS 1.3 就是采用的这样的做法,它直接在协议内部固定了 \(p\)\(g\) 的值,将其限定在几个值内,因此就能够提前的计算出 \(A\)

    因此 Client 将可以提前生成好 \(a\) 并通过多个固定的 \(p\)\(g\) 直接计算出 \(A\) 并发送。

    具体我们直接来看 TLS 1.3 的握手流程:

    Client 生成 \(key\_share\) 向 Server 发送

    \(key\_share\) 的结构为一个列表,其中具有多个 <密钥组名,通过 \(a_n\) 和对应组使用的 \(p\)\(g\) 生成的 \(A_n\) > 的数据项。

    为了安全起见,每一个数据项中的 \(a\) (Client 私钥) 都是不一样的

    以上包含的多个组中并不只有 DHE 类型的组,还有 ECDHE 类型,其值中的加密组件所需参数不同

    Server 从多个密钥组中选择自己能够接受的密钥组;如果没有,则会直接响应自己能够接受的密钥组的组名,让 Client 重新生成对应的 \(A\) 并发送(此时 \(1\cdot RTT\) 会退化为 \(2\cdot RTT\) )。

    在选定密钥组后,Server 发送决定使用的密钥组的名称,同时使用该组的 \(p\)\(g\) 和自己的 \(b\) 计算出 \(B\) ,紧接着根据密钥交换原理通过收到的 \(A\) 计算出 \(K\)

    在将"服务器端随机数"和 \(B\) 发送出去后,接下来,将通过 \(K\) 导出以下三个密钥:

  • \(master\ secret\) :主密钥
  • 客户端握手密钥:用来加密 Client -> Server 的握手信息
  • 服务器端握手密钥:用来加密 Server -> Client 的握手信息
  • 导出密钥后,接下的所有要发送的信息都将被对应的密钥进行加密。

    需要注意的是,尽管以上描述的结果是正确的的,但是"导出"的过程被省略了,实际上对于每个被导出的密钥,所参与的参数与过程都存在不同,且该过程中会使用到 [客户端随机数] 和 [服务器端随机数],这两参数的交换过程也被省略。如果你对于"导出"的过程感兴趣,建议阅读 RFC8446

    导出的操作可理解为使用单向散列函数,以上导出的每个密钥中,参与导出的参数并不完全相同,所以导出的密钥的值也并不同的。

    Client 收到来自 Server 的服务器端随机数和公钥 \(B\) 后,也进行以上的计算过程,并根据得出的"服务器端握手密钥"解密接下来收到的握手信息。

    在解密并验证完被加密的证书后,就能确保服务器的身份。同时仍会验证发过来的握手阶段的报文的摘要,确认握手过程未被篡改。

    在保证通信安全后,接下来会和 TLS 1.3 前一样发送握手阶段报文的摘要(当然,这会被"客户端握手密钥"加密)。

    在两边做完自己的工作后,双方将从 \(master\ secret\) 中导出以下四个密钥:

    客户端通信密钥:用来加密 Client -> Server 的通信信息

    (上文的两个用于握手报文加密的密钥将会被丢弃)

    服务器端通信密钥:用来加密 Server -> Client 的通信信息

    恢复密钥:用来参与 PSK 的计算

    导出密钥:用来参与默认的密钥导出计算

    介绍完 TLS 1.3 后,我猜你的第一个问题是:

    为什么使用了这么多不同的密钥?

    在 TLS 1.3 中,其密钥的导出函数叫做 HKDF,为一个基于 HMAC 的密钥导出函数(KDF),它执行的主要方法是 "extract-then-expand",也就是说,先从输入密钥与参数中提取一个固定长度的密钥,然后拓展为多个额外的密钥。重点在于,额外导出的密钥在密码学上是安全的,并且即使其中一个密钥被泄露,也不会导致其他由相同的密钥材料导出的密钥存在风险。

    简单来讲就是,"不要把鸡蛋放在同一个篮子里"。

    Pre-share key(PSK)

    PSK 是 TLS 1.3 实现 \(0\cdot RTT\) 的重要功能,它的全称是 \(pre\ share\ key\) ,从名称你应该已经知道了,它是用于会话恢复的一个机制,同时 Session ID 和 Session Ticket 机制在 TLS 1.3 中被废弃也是因为其作用被该功能所替代了。

    Zero-RTT

    我们首先来关心 PSK 从何而来

    在握手流程结束后,Server 可以发送任意数量的 ticket 给 Client,由于此时的信道被各自的通信密钥所保护,所以直接的发送是安全的。和 ticket 一起发送的还有该 ticket 的过期时间。

    我们都知道 PSK 可以用来恢复通信,所以泄露可能会导致一定程度的危险,于是为了最大程度减少风险,加上过期时间是必要的。

    然后,双方根据 ticket 的序号与由 \(master\ sectet\) 导出的恢复密钥导出得到 PSK。

    Client 在导出 PSK 后会将其以及相关的信息后存到该网站下的本地缓存中,它将会在下次进行 TLS 握手的时候使用。

    当第二次想和网站进行 TLS 握手时,在发送的时候除了发送 \(key\ share\) ,还会发送 PSK 对应的 ticket 和它的签名。同时还会通过 PSK 导出以下三个密钥:

  • binder 密钥:用来对发送的 ticket 进行签名(防止篡改)
  • 早期数据加密密钥:用来对启用 \(0\cdot RTT\) 时想要发送的应用数据进行加密的密钥
  • 早期导出密钥:在计算出"导出密钥"前代替它的作用
  • 而此时如果需要 \(0\cdot RTT\) ,则可以使用"早期数据加密密钥"来对需要发送的数据进行加密。而 Server 则可以根据 ticket 来验证并得到 PSK 以解密应用数据,同时由于身份已经被 PSK 机制所验证,因此不需要再次发送证书,此时只需要走简单的密钥协商的流程即可。

    以下为简单的流程图:

    Ticket

    我们再回过头来看在会话恢复流程中以明文传输的 ticket 到底是什么。

    我们说过 PSK 机制替代了 Session ID 和 Session Ticket ,这也就意味着 PSK 两种角色都可以扮演。也就是说,ticket 的值可以像是 Session ID 一样为一个在数据库中进行查找的键,也可以是像 Session Ticket 为一个自加密、自验证的值。

    所以 Server 在验证的时候会根据使用的 ticket 不同来具体的验证行为。比如说可以像 Session ID 一样,检查本地缓存是否存在对应的键,存在则取出,而它的值可能是上一次握手协商得到的 PSK 值,此时可以直接取出并也导出对应的三个密钥,并对数据进行验证和解密。又或者和 Session Ticket 一样,使用本地的密钥对该 ticket 进行解密,而解密出的可能是 PSK 的值,此时同样会导出密钥以进行验证与解密。

    如果你的足够敏锐的话,此时应该已经发现了 PSK 的一个缺点:在使用 PSK 发送早期数据时,此时的加密早期数据的密钥派生于 PSK,而 PSK 是不具备前向安全性的!也就是说如果某时的攻击者获得了 PSK ,那么使用该 PSK 导出的密钥加密的早期数据将会被解密。(当然,这不包括在建立连接后的应用数据,因为它们使用的是具有前向安全的 DHE/ECDHE 算法协商出的密钥)

    一种简单的解决方式就是尽量的使用较短有效期的 PSK,TLS 1.3 在设计的时候就想到了这点,所以添加了有效期字段,如果在乎前向安全的话,一般设置一个较短的有效期即可解决。

    在其他的问题上,攻击者还可以截取这部分的早期数据进行重放攻击,所以对于早期应用数据,幂等性也是必要的。

    RFC 2246

    RFC 4346

    RFC 5246

    RFC 8446

    RFC 9001

    《Web性能权威指南》

    Source:https://www.cnblogs.com/enoc/p/tls-handshake.html