Contents

TLS详解(三)

Warning
本文最后更新于 January 16, 2021,文中内容可能已过时,请谨慎使用。

TLS 1.3现已于2018年8月发布。相比与TLS1.2,TLS1.3 在速度和安全性上做出了更大的性能提升,其最大的特点是支持了 Zero Round Trip Time (0-RTT). 在安全性方面,TLS1.3 摒弃了绝大多数不安全的加密套件,只支持几个AEAD的加密认证方式。

TLS1.3 Feature

  • Speed Benefits of TLS 1.3

TLS1.3 可以使用1RTT建立握手,比1.2版本能节约一个网络来回。

  • Improved Security With TLS 1.3

TLS1.3 移除了RC4、DES、MD5 等诸多脆弱不安全的算法,目前仅保持了支持AEAD的ECDH类等算法。

  • 1.3 Browser Support

从 Chrome65 开始,Google公司就可以支持 draft version of TLS 1.3 , 2018年10月的 Chrome70 就完全支持了TLS1.3。同样Firefox63也在同年10月支持了TLS1.3。Microsoft Edge version 76 及 Safari 12.1 on macOS 10.14.4. 也都支持了TLS1.3 。

The TLS handshake

总体握手流程如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func (hs *serverHandshakeStateTLS13) handshake() error {
	c := hs.c

	// For an overview of the TLS 1.3 handshake, see RFC 8446, Section 2.
	if err := hs.processClientHello(); err != nil { return err }
	if err := hs.checkForResumption(); err != nil { return err }
	if err := hs.pickCertificate(); err != nil { return err }
	c.buffering = true
	if err := hs.sendServerParameters(); err != nil { return err }
	if err := hs.sendServerCertificate(); err != nil { return err }
	if err := hs.sendServerFinished(); err != nil { return err }
	// Note that at this point we could start sending application data without
	// waiting for the client's second flight, but the application might not
	// expect the lack of replay protection of the ClientHello parameters.
	if _, err := c.flush(); err != nil { return err  }
	if err := hs.readClientCertificate(); err != nil {  return err  }
	if err := hs.readClientFinished(); err != nil { return err }

	atomic.StoreUint32(&c.handshakeStatus, 1)
	return nil
}

Client Hello

由于TLS1.2已经在互联网上存在了10年。网络中大量的网络中间设备都十分老旧,这些网络设备会识别中间的TLS握手头部,所以TLS1.3的出现如果引入了未知的TLS Version 必然会存在大量的握手失败,为了解决这一点,TLS1.3 的握手头部默认是TLS1.2。

如果客户端支持TLS1.3 则在 Client Hello 发出时在Extensions中携带 supported_versions 并标明客户端是支持TLS1.3的,同样为了1RTT快速握手,会将客户端Key_share 发送给服务端。Key_Share是客户端提前生成好的公钥信息。其密钥派生过程依赖于密码套件的 HKDF Extract 和 HKDF Expand 函数以及 Hash函数。

在密钥交换之前,客户端和服务端使用HKDF生成密钥。(它取代了基于HMAC的伪随机密钥生成函数PRF。

下面用代码过一遍客户端的Client Hello流程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
...
var params ecdheParameters
if hello.supportedVersions[0] == VersionTLS13 {
	hello.cipherSuites = append(hello.cipherSuites, defaultCipherSuitesTLS13()...)

	curveID := config.curvePreferences()[0]
	if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok {
		return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
	}
	params, err = generateECDHEParameters(config.rand(), curveID)
	if err != nil {
		return nil, nil, err
	}
	hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}}
}
}

这里,如果客户端是支持  VersionTLS13, 则在创建 Client Hello 时,添加TLS1.3支持的秘钥套件,并使用 x25519 曲线和随机数生成 PublickKey 放入 Client Hello Extension 中的 KeyShares 中。

Server Hello

服务端的TLS Version仍为TLS1.2(实际上后续的TLS版本均为1.2),如果服务端支持TLS1.3,则会在  supported_versions 中的携带TLS1.3,这样后续的会话便均在TLS1.3下通信。

服务端会在 Server Hello 中的 key_share 中携带公钥信息。

下面是完整的握手过程,BTW 虽然0RTT是各大博客都吹嘘的TLS1.3亮点,但是0RTT 当前大多数的官方库都还没有实现(Nginx似乎是支持了) ,比如看这里

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	if hs.clientHello.earlyData {
		// See RFC 8446, Section 4.2.10 for the complicated behavior required
		// here. The scenario is that a different server at our address offered
		// to accept early data in the past, which we can't handle. For now, all
		// 0-RTT enabled session tickets need to expire before a Go server can
		// replace a server or join a pool. That's the same requirement that
		// applies to mixing or replacing with any TLS 1.2 server.
		c.sendAlert(alertUnsupportedExtension)
		return errors.New("tls: client sent unexpected early data")
	}

服务端选择和客户端同样支持的 CurveID (代码中的 selectedGroup,并且是Client支持的Key Share)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	params, err := generateECDHEParameters(c.config.rand(), selectedGroup)
	if err != nil {
		c.sendAlert(alertInternalError)
		return err
	}
	hs.hello.serverShare = keyShare{group: selectedGroup, data: params.PublicKey()}
	hs.sharedKey = params.SharedKey(clientKeyShare.data)
	if hs.sharedKey == nil {
		c.sendAlert(alertIllegalParameter)
		return errors.New("tls: invalid client key share")
	}

这样以来,客户端和服务端便直接完成了 ECDHE 密钥交换

  • 客户端生成随机数x,确定了曲线类型如Golang TLS SDK只支持的 x25519曲线 即可得方程系数a、b,再调用generateECDHEParameters 获得 PublicKey Q1 。客户端将 Q1 、a、 b、 P 传给服务端。
  • 服务端生成随机数y,解析客户端传来的曲线和 Key_Share 对,得到曲线类型 x25519 既得方程系数a、b,再使用 selectedGroup 和 y 调用generateECDHEParameters 生成 PublicKey Q2 ,传给客户端
  • 这时客户端和服务端可以计算出一个公共的值 K

如下图

PSK (Pre-Shared Key)

这里在接着解析代码之前,先插播一个 TLS1.3 的feature 0RTT是如何实现的。这里介绍一下实现的原理 – PSK

一旦一次握手完成,server 就能给 client 发送一个与一个独特密钥对应的 PSK 密钥,这个密钥来自初次握手。然后 client 能够使用这个 PSK 密钥在将来的握手中协商相关 PSK 的使用。如果 server 接受它,新连接的安全上下文在密码学上就与初始连接关联在一起,从初次握手中得到的密钥就会用于装载密码状态来替代完整的握手。在 TLS 1.2 以及更低的版本中,这个功能由 “session IDs” 和 “session tickets” [RFC5077]来提供。这两个机制在 TLS 1.3 中都被废除了。

PSK 可以与 (EC)DHE 密钥交换算法一同使用以便使共享密钥具备前向安全,或者 PSK 可以被单独使用,这样是以丢失了应用数据的前向安全为代价。

下图显示了两次握手,第一次建立了一个 PSK,第二次时使用它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
          Client                                               Server

   Initial Handshake:
          ClientHello
          + key_share               -------->
                                                          ServerHello
                                                          + key_share
                                                {EncryptedExtensions}
                                                {CertificateRequest*}
                                                       {Certificate*}
                                                 {CertificateVerify*}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Certificate*}
          {CertificateVerify*}
          {Finished}                -------->
                                    <--------      [NewSessionTicket]
          [Application Data]        <------->      [Application Data]


   Subsequent Handshake:
          ClientHello
          + key_share*
          + pre_shared_key          -------->
                                                          ServerHello
                                                     + pre_shared_key
                                                         + key_share*
                                                {EncryptedExtensions}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Finished}                -------->
          [Application Data]        <------->      [Application Data]

当 server 通过一个 PSK 进行认证时,它不会发送一个 Certificate 或一个 CertificateVerify 消息。当一个 client 通过 PSK 想恢复会话的时候,它也应当提供一个 “key_share” 给 server,以允许 server 拒绝恢复会话的时候降级到重新回答一个完整的握手流程中。Server 响应 “pre_shared_key” 扩展,使用 PSK 密钥协商建立连接,同时响应 “key_share” 扩展来进行 (EC)DHE 密钥建立,由此提供前向安全。

当 PKS 在带外提供时,PSK 密钥和与 PSK 一起使用的 KDF hash 算法也必须被提供。

0 RTT

当 client 和 server 共享一个 PSK(从外部获得或通过一个以前的握手获得)时,TLS 1.3 允许 client 在第一个发送出去的消息中携带数据(“application data”)。Client 使用这个 PSK 来认证 server 并加密 early data 信息,最终实现Application数据的0RTT发送。

如下图所示,0-RTT 数据在第一个发送的消息中被加入到 1-RTT 握手过程中。握手的其余消息与带 PSK 会话恢复的 1-RTT 握手消息相同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
         Client                                               Server

         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]

上图是 0-RTT 的信息流

0-RTT 数组安全性比其他类型的 TLS 数据要弱一些,特别是:

  1. 0-RTT 的数据是没有前向安全性的,它使用的是被提供的 PSK 中导出的密钥进行加密的。
  2. 在多个连接之间不能保证不存在重放攻击。普通的 TLS 1.3 1-RTT 数据为了防止重放攻击的保护方法是使用 server 下发的随机数,现在 0-RTT 不依赖于 ServerHello 消息,因此保护措施更差。如果数据与 TLS client 认证或与应用协议里一起验证,这一点安全性的考虑尤其重要。这个警告适用于任何使用 early_exporter_master_secret 的情况。

参考 TLS 1.3 Introduction – Halfrost’s Field | 冰霜之地

checkForResumption – Go

上面2个小节其实就是在介绍 checkForResumption() 这个函数的作用。

在Client Hello 包的扩展里如果有 psk_key_exchange_modes 和  pre_shared_key 就表示客户端想要会话复用,即类似TLS1.2的 Session Ticket or Session Id 的概念。

如下所示:

psk_key_exchange_modes是 psk 密钥交互模式选择. 此处的PSK模式为(EC)DHE下的PSK,客户端和服务器必须提供KeyShare, 如果是仅PSK模式,则服务器不需要提供KeyShare。

pre_shared_key 是预共享密钥认证机制,相当于session ticket再加一些检验的东西. Identity中包含的是客户端愿意进行协商的服务器身份列表。PSK binder表示已经构建当前PSK与当前握手之间的绑定。

下面函数中,服务端会将 identity 解析成 plaintextplaintext中包含TLS版本、证书、复用秘钥、超时时间 等多个信息,如果unmarshal成功,即可以会话复用,继续向下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	for i, identity := range hs.clientHello.pskIdentities {
		if i >= maxClientPSKIdentities { break }
		plaintext, _ := c.decryptTicket(identity.label)
		if plaintext == nil { continue }
		sessionState := new(sessionStateTLS13)
        
		if ok := sessionState.unmarshal(plaintext); !ok { continue }
        
        createdAt := time.Unix(int64(sessionState.createdAt), 0)
		if c.config.time().Sub(createdAt) > maxSessionTicketLifetime { continue }

		// We don't check the obfuscated ticket age because it's affected by
		// clock skew and it's only a freshness signal useful for shrinking the
		// window for replay attacks, which don't affect us as we don't do 0-RTT.

		pskSuite := cipherSuiteTLS13ByID(sessionState.cipherSuite)
		if pskSuite == nil || pskSuite.hash != hs.suite.hash { continue }
		...
		psk := hs.suite.expandLabel(sessionState.resumptionSecret, "resumption",
			nil, hs.suite.hash.Size())
		hs.earlySecret = hs.suite.extract(psk, nil)
		binderKey := hs.suite.deriveSecret(hs.earlySecret, resumptionBinderLabel, nil)
		...
	}
    

Change Cipher Space

发送一个 ChangeCipherSpec record 报文,之后的加密方式将会改变。详见 See RFC 8446, Appendix D.4.

EncryptedExtensions

随后 Server 会发来建立 EncryptedExtensions Server 参数: 对 ClientHello 扩展的响应,不需要确定加密参数,而不是特定于各个证书的加密参数。一般ALPN会在这里添加。

Certificate && Certificate Verify && Finished

最后,Client 和 Server 交换认证消息。TLS 在每次基于证书的认证时使用相同的消息集,(基于 PSK 的认证是密钥交换中的一个副作用)特别是:

  • Certificate: 终端的证书和每个证书的扩展。 服务器如果不通过证书进行身份验证,并且如果服务器没有发送CertificateRequest(由此指示客户端不应该使用证书进行身份验证),客户端将忽略此消息。 请注意,如果使用原始公钥 [RFC7250] 或缓存信息扩展 [RFC7924],则此消息将不包含证书,而是包含与服务器长期密钥相对应的其他值。

  • CertificateVerify: 使用与证书消息中的公钥配对的私钥对整个握手消息进行签名。如果终端没有使用证书进行验证则此消息会被忽略。

  • Finished: 对整个握手消息的 MAC(消息认证码)。这个消息提供了密钥确认,将终端身份与交换的密钥绑定在一起,这样在 PSK 模式下也能认证握手。

接收到 Server 的消息之后,Client 会响应它的认证消息,即 Certificate,CertificateVerify (如果需要), 和 Finished。

这时握手已经完成,client 和 server 会提取出密钥用于记录层交换应用层数据,这些数据需要通过认证的加密来保护。应用层数据不能在 Finished 消息之前发送数据,必须等到记录层开始使用加密密钥之后才可以发送。需要注意的是 server 可以在收到 client 的认证消息之前发送应用数据,任何在这个时间点发送的数据,当然都是在发送给一个未被认证的对端。

New Session Ticket

实际等同于发送 PSK 数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (hs *serverHandshakeStateTLS13) shouldSendSessionTickets() bool {
	if hs.c.config.SessionTicketsDisabled {
		return false
	}

	// Don't send tickets the client wouldn't use. See RFC 8446, Section 4.2.9.
	for _, pskMode := range hs.clientHello.pskModes {
		if pskMode == pskModeDHE {
			return true
		}
	}
	return false
}