SSL/TLS 协议

SSL/TLS 是基于 TCP 之上,HTTP 之下的协议。从技术角度上看,HTTP/2 作为新一代的协议,虽然协议文本中并未强制要求加密,但主要的浏览器(Firefox、Chrome、Safari、Opera、IE、Edge)已共同宣布,他们只支持实现基于 TLS 的 HTTP/2,也就是说加密将是下一代协议的强制事实标准。

网络通信存在三个风险:

  1. 窃听风险(eavesdropping):机密性,需要机密性,用公钥分发"对话密钥”,并使用它对称加密数据;
  2. 篡改风险(tampering):完整性,对数据的篡改,数字签名,需要权威的 CA 机构证明;
  3. 冒充风险(pretending):身份验证,对公钥的冒充,数字签名,需要权威的 CA 机构证明;

TLS 便是要解决通信中存在的安全风险,并提供减少延迟可用方案。对于安全风险的处理方式,为了便于理解,可以看这一张简化的流程图: SSL/TLS 通信流程

TLS 是一个分层协议,具体的文档可以查阅《RFC 5246》。

TLS/SSL格式协议 协议->消息类型->子消息类型(如果有的话) 其中握手协议的子消息类型 HandshakeType(十六进制)常规有以下这些: hello_request(0x00), client_hello(0x01), server_hello(0x02),certificate(0x0b), server_key_exchange (0x0c),certificate_request(0x0d), server_hello_done(0x0e),certificate_verify(0x0f), client_key_exchange(0x10),finished(0x14), (0xff) 扩展消息还增加了一些子消息类型,后面涉及时再介绍。需要注意的是,对于 TLS 协议来说,多个子协议可以合并到一个 TLS 协议包中,减少延迟;

下面主要以 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 密钥套件为例来分析 TLS。

1   记录层协议

主要有四部分的逻辑处理: (握手完成,从应用层接收数据)

  • 数据分块:每个块的大小小于2^14字节(16KB);生成 TLSPlaintext 结构;
  • 压缩:由于存在一些安全问题,一般不启用压缩;TLSPlaintext 结构转换为 TLSCompressed 结构,如果不压缩,可以认为 TLSPlaintext 和 TLSCompressed 一致,其实是 TLSPlaintext 和 TLSCompressed.fragment 一致;
  • 加密和完整性保护:TLSCompressed 结构转换为 TLSCiphertext 结构,对 TLSCiphertext 解压后 其实就是 TLSCompressed.fragment,
  • 添加记录层消息头:看格式协议图; (交付给 TCP 层传输)

接下来单独解释一下加密和完整性保护,这里以 AEAD 为例,AEAD 函数将 TLSCompressed.fragment 结构转换/逆转换为 AEAD TLSCiphertext.fragment 结构。

AEAD 模式逻辑图

additional_data = seq_num + TLSCompressed.type +
                        TLSCompressed.version + TLSCompressed.length;

(1)加密

AEADEncrypted = AEAD-Encrypt(write_key, nonce, plaintext, additional_data)

其中 nonce 是明文传输的;plaintext 为 TLSCompressed.fragment;这里的 AEAD 函数校验数据完整性是通过 additional_data 这个数据,所以 Mac 密钥已经不需要了。下面这段引自 rfc5246

AEAD ciphers take as input a single key, a nonce, a plaintext, and “additional data” to be included in the authentication check, as described in Section 2.1 of [AEAD]. The key is either the client_write_key or the server_write_key. No MAC key is used.

(2)解密

TLSCompressed.fragment = AEAD-Decrypt(write_key, nonce, AEADEncrypted, additional_data)

2   握手协议

完整的握手消息流.

      Client                                               Server

      ClientHello                  -------->
                                                      ServerHello
                                                     Certificate*
                                               ServerKeyExchange*
                                              CertificateRequest*
                                   <--------      ServerHelloDone
      Certificate*
      ClientKeyExchange
      CertificateVerify*
      [ChangeCipherSpec]
      Finished                     -------->
                                               [ChangeCipherSpec]
                                   <--------             Finished
      Application Data             <------->     Application Data

2.1   密码套件协商

  • client_hello: 密码套件支持,曲线支持(ecc_curve)等。
  • server_hello: 协商出密码套件。

密码套件提供了身份认证、数据机密性和数据完整性,是 TLS/SSL 的核心,可以说只有充分理解密码套件才能真正理解 TLS/SSL 协议。以 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(IANA 表示法,编号 0xC02F,OpenSSL 表示为 ECDHE_RSA_AES128_GCM_SHA256) 密钥套件为例,其总体上由三部分组成:(1)Key Exchange(ECDHE_RSA)、(2)Cipher(AES_128_GCM)、(3)Mac(SHA256)。

  1. 使用 ECDHE_RSA 进行身份验证和密钥协商,协商预备主密钥;在密码套件中,身份验证和密码协商是一起理解的(也包含了证书的信息),他们可以是同一个算法。 ECDHE 和 ECHD 密码套件差不多,但是不包含匿名的密码套件;
  2. 使用 AES_128_GCM 算法和密钥块进行会话保护;因为在数据加密过程中,是需要保证机密性和完整性的,所以需要对称加密算法 + Mac 算法(注意这里的 Mac 没有使用 HMac 算法),亦即实现 AD 或者 AEAD 的加密模式。
  3. 使用 SHA256 算法的 PRF 和 HMAC 算法推导出密钥块。在 TLSv1.2 以前版本采用的 Hash 算法是硬编码,TLSv1.2 及以后的版本取决于 TLS 版本和密码套件,如果密码套件指定的 Mac 算法安全级高于SHA256,则采用 SHA384。

ECDHE_RSA 密码套件中的证书包含一个 RSA 服务器的公钥(注意 CA 公钥算法是在证书里指定的,不需要在密码套件中指定),对 DH_XXX、ECDH_XXX、DHE_XXX、ECDHE_XXX 套件来说,套件的后半部分对应的公钥不会用来加密或者数字签名(如果要用于数字签名,前提是证书 Key Usage 扩展必须置为 digitalSignature),也不限制 CA 机构签发证书所用的数字签名,所以服务器的公钥可以看作普通的消息,服务器的非对称加密没有存在的必要性。ECDHE_RSA 这些名字的存在仅仅是历史原因。

2.2   身份认证

  • certificate:一般只是证明服务端的身份,银行大额转账则要求双向认证,这时候就需要用到银行发的 Ukey。

身份认证就是密码学中数字签名的实际应用例子,由权威的 CA 机构签名背书给服务器发放数字证书(类似现实生活中身份证,驾驶证),身份认证就是对数字证书的身份求证,除了 TLS 使用的数字证书,银行 Ukey 里的数字证书都是一样的原理。SSL/TLS 通信流程图结合密码学的数字签名就可以讲清楚身份认证的过程。

数字证书解决了服务器身份认证和公钥传输问题,服务器实体并不能自己证明自己,所以需要通过 CA 机构来进行认证,对服务端来说 CA 机构的私钥用来给服务器颁发证书,对客户端来说浏览器集成了 CA 机构的公钥可以验证服务器证书。**理解数字证书就要理解一次对话中需要两对非对称加密:CA 机构的公钥私钥和服务器的公钥私钥,且这两套的算法没有必然的联系。**目前密钥协商已不再使用 RSA 算法,所以服务器的公钥和私钥已经限于身份认证功能了。

2.3   密钥协商

  • server_key_exchange: 一般 HTTPS 网站会部署 ECDHE_RSA、DHE_RSA、ECDHE_ECDSA、RSA 这四种密码套件,这些套件中,前三个一定需要发送 server_key_exchange 子消息才有足够的信息进行密钥协商,最后一个不允许服务器发送 server_key_exchange 子消息,因为单靠 RSA 已经足以进行密钥协商(密钥传输),除 RSA 之外 DH_DSS、DH_RAS 都不需要该子消息。 对 DH_RSA 来说,ServerDhParams 里包含了 dh_p、dh_g、dh_Ys,即 p 大质数、g 生成元、Ys 服务器 DH 公钥。 对 ECDHE_RSA 来说,ServerECDHParams 里包含了 curve_params、public,即椭圆曲线(一般是命名曲线,需要双方都支持)、ECC 公钥。 另外该消息会对发送的 DH/ECDH 参数和公钥进行签名,不过这不是重点。
  • server_hello_done: 其实就是一条空消息,表示服务端发送了足够的消息,接下来可以和客户端一起协商出预备主密钥。
  • client_key_exchanges: 收到 server_hello_done 后客户端立刻发送该消息。以 ECDHE_RSA 为例,把客户端 ECDH 公钥(ecdh_Yc)发送给服务端。注意的是,client_key_exchange 一定会发的,而 server_key_exchange 不一定会发,如果理解了密钥协商的要素,也就理解是否需要发送了。

到此密钥协商过程就算结束,客户端和服务端都计算出了预备主密钥,接下来双方就各自推导出密钥块。密钥块是通过密钥衍生算法计算出来的,而且需要经过两次运算,计算主密钥和密钥块是两个不同的过程。 首先我们先来解释一下 TLS 采用的密钥衍生算法(KDF)——PRF(scret, label, seed),其中 scret 是输入值,label 是标识符(对固定的运算有固定的标识符),seed 其实就是 salt(随机数),如果输入值和 salt 一样,则输出也是一样的,为了保证输出结果一致,客户端和服务端持有的 salt 值是相同的,一般是客户端随机数加客户端随机数。《RFC 5246》——5. HMAC and the Pseudorandom Function 部分能更进一步理解 PRF,从下面的描述可以看出,PRF 就用到了密码套件的 Mac 算法

5.  HMAC and the Pseudorandom Function

   The TLS record layer uses a keyed Message Authentication Code (MAC)
   to protect message integrity.  The cipher suites defined in this
   document use a construction known as HMAC, described in [HMAC], which
   is based on a hash function.  Other cipher suites MAY define their
   own MAC constructions, if needed.

   In addition, a construction is required to do expansion of secrets
   into blocks of data for the purposes of key generation or validation.
   This pseudorandom function (PRF) takes as input a secret, a seed, and
   an identifying label and produces an output of arbitrary length.

   In this section, we define one PRF, based on HMAC.  This PRF with the
   SHA-256 hash function is used for all cipher suites defined in this
   document and in TLS documents published prior to this document when
   TLS 1.2 is negotiated.  New cipher suites MUST explicitly specify a
   PRF and, in general, SHOULD use the TLS PRF with SHA-256 or a
   stronger standard hash function.

   First, we define a data expansion function, P_hash(secret, data),
   that uses a single hash function to expand a secret and seed into an
   arbitrary quantity of output:






Dierks & Rescorla           Standards Track                    [Page 14]
 
RFC 5246                          TLS                        August 2008


      P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
                             HMAC_hash(secret, A(2) + seed) +
                             HMAC_hash(secret, A(3) + seed) + ...

   where + indicates concatenation.

   A() is defined as:

      A(0) = seed
      A(i) = HMAC_hash(secret, A(i-1))

   P_hash can be iterated as many times as necessary to produce the
   required quantity of data.  For example, if P_SHA256 is being used to
   create 80 bytes of data, it will have to be iterated three times
   (through A(3)), creating 96 bytes of output data; the last 16 bytes
   of the final iteration will then be discarded, leaving 80 bytes of
   output data.

   TLS's PRF is created by applying P_hash to the secret as:

      PRF(secret, label, seed) = P_<hash>(secret, label + seed)

   The label is an ASCII string.  It should be included in the exact
   form it is given without a length byte or trailing null character.
   For example, the label "slithy toves" would be processed by hashing
   the following bytes:

      73 6C 69 74 68 79 20 74 6F 76 65 73

(1)计算主密钥:一旦客户端和服务端协商出预备主密钥,就会立刻计算主密钥,在 Change Cipher Spec 协议发送之前,客户端和服务端计算出主密钥就行。通过 PRF 函数将预备主密钥转换为主密钥后,客户端和服务端应该立刻从内存中删除预备主密钥,避免被窃取。计算公式如下:

 master_secret = PRF(pre_master_secret, "master secret",
                 ClientHello.random + ServerHello.random)
                 [0..47];

其中,客户端和服务端的随机数组合起来就是 seed;主密钥的长度固定是48字节,而预备主密钥的长度取决于密码协商算法,对 RSA 密钥协商其预备主密钥的长度是48字节,对 DH/ECDH 密钥协商其预备主密钥长度取决于 DH/ECDH 公钥。 (2)计算密钥块: 客户端和服务端计算出主密钥后,立刻计算密钥块(key_block),TLS 记录层协议(实际上是记录层协议上的应用协议)需要使用这些密钥块进行密码学机密性和完整性保护。 主密钥的长度固定是48字节,而密钥块的个数和长度取决于密钥协商算法。计算如下:

key_block = PRF(SecurityParameters.master_secret,
            "key expansion",
            SecurityParameters.server_random +
            SecurityParameters.client_random);

2.4   完成握手

  • Change Cipher Spec 协议:该协议并不是握手协议的一部分,但是握手过程依赖于该协议,所以 为了避免握手流程理解中断得把它放在握手过程中。它是可以和握手协议的组成一个 TLS 包一起发送的。客户端和服务端计算出预备主密钥、主密钥和密钥块后,接下来通知对端后续的消息都需要 TLS 记录协议加密和完整性保护了,连接状态由待读、待写状态切换为可读、可写状态。该协议只有一个字节
  • finished:在 Wireshark 中显示的是 encrypted_handshake_message。理论上只要发了 Change Cipher Spec 就可以对数据进行加密保护了,但因为握手协议过程中的包都没有加密和完整性保护,为了避免消息篡改,客户端和服务端需要校验对方发送的 Finished 子消息确保握手消息没有被篡改。该消息一定是在 Change Cipher Spec 子消息之后,否则会产生一个致命的错误。一旦客户端和服务端都校验了对方的 Finished 子消息,那么接下来就可以立刻加密保护应用层数据了。

3   会话恢复

上面讲的是完整的握手过程,接下来讲解通过会话恢复实现简短的握手过程。 会话恢复有两种形式,分别是基于 Session ID 的会话恢复和基于 Session ticker 的会话恢复。

3.1   基于 Session ID 的会话恢复

《RFC 5246》文档对这一内容有描述。 基于 Session ID 的会话恢复是由服务端存储会话信息,TLS 协议只是规定 Session Cache 的存储方式,没有考虑如何实现 Session Cache,对分布式服务来说增加了复杂性,目前 Nginx 官方就没有支持分布式服务器 Session Cache。所以一般不采用。

  • 基于 Session ID 进行完整的握手(会话建立):Client Hello 的 Session ID 值为空,服务端创建新的 Session ID。建立完成后: (1) 客户端:仅保存 Session ID 值在内存中。 (2) 服务端:将会话信息保存 Session Cache 中,键值就是 Session ID,键值对应的内容就是会话信息。一个完整的会话信息包括: session id、证书(对端的,一般为空)、压缩算法(一般不启用)、密码套件、主密钥、会话可恢复标识(表示某个会话是否可恢复)。

  • 基于 Session ID 进行简短的握手(会话恢复):Client Hello 的 Session ID 值不为空。服务端检查是否可以恢复会话,如不能则进行完整的握手协议,同时生成一个新的 Session ID。如能恢复连接,则直接发送 ChangeCipherSpec 和 Finished,而不进行协商(主密钥存在 Session Cache 中)。

3.2   基于 SessionTicket 的会话恢复

详见文档《RFC 5077》——没有服务器端状态的会话恢复,它属于扩展,是 HTTPS 的一个重要知识点。 服务器将会话信息加密后以 ticket 的方式发送给客户端,服务端本身不存储会话信息,客户端收到 ticket 后存储在内存中,客户端只是存储和传输 ticket,不涉及 ticket 的解密。 添加了一个 HandshakeType 子消息类型 session_ticket(0x04),如果服务端 ServerHello 消息包含 SessionTicket TLS 扩展,则必须发送该子消息;如果服务端 ServerHello 不包含 SessionTicket TLS 扩展,则不能发送该子消息。

  • 服务器返回 NewSessionTicket 完整握手的消息流
Client                                               Server

         ClientHello
         (SessionTicket extension) -------->
                                                         ServerHello
                                     (empty SessionTicket extension)
                                                        Certificate*
                                                  ServerKeyExchange*
                                                 CertificateRequest*
                                  <--------          ServerHelloDone
         Certificate*
         ClientKeyExchange
         CertificateVerify*
         [ChangeCipherSpec]
         Finished                 -------->
                                                    NewSessionTicket
                                                  [ChangeCipherSpec]
                                  <--------                 Finished
         Application Data         <------->         Application Data
  • 使用新 NewSessionTicket 进行简短握手的消息流
         Client                                                Server
         ClientHello
         (SessionTicket extension)      -------->
                                                          ServerHello
                                      (empty SessionTicket extension)
                                                     NewSessionTicket
                                                   [ChangeCipherSpec]
                                       <--------             Finished
         [ChangeCipherSpec]
         Finished                      -------->
         Application Data              <------->     Application Data

注意,发送 NewSessionTicket 子消息来更新 ticket,ticket 也是有有效期的。

4   总结

TLS 为了更安全必然造成一些损耗,但其的设计上已经做了优化,比如采用对称加密。如果采用 HTTP/2 协议传输,那么带来的性能提升完全可以抵消 TLS 带来的性能损耗。下面这些诀窍可供借鉴。

  • 保持连接开启尽可能长的时间。TLS 成本最高的地方就是连接时的握手过程。如果连接时间能保持足够长,握手次数就可以减少。
  • 使用基于 SessionTicket 的会话恢复,允许客户端复用上次的握手直接重连服务器。
  • 使用内置加解密支持的芯片。

参考文献 [1] 虞卫东. 深入浅出 HTTPS 从原理一实战. 版次:2018年6月第1版 [2] 阮一峰. HTTPS 升级指南 http://www.ruanyifeng.com/blog/2016/08/migrate-from-http-to-https.html. 2016年8月19日