理解SSL/TLS系列 (五)握手协议

 一、概述

由于当前TLS1.2使用最为广泛,本文描述的内容都基于TLS1.2

握手是 TLS 协议中最精密复杂的部分。在这个过程中,通信双方协商连接参数,并且完成身份验证。根据使用的功能的不同,整个过程通常需要交换 6~10 条消息。根据配置和支持的协议扩展的不同,交换过程可能有许多变种。在使用中经常可以观察到以下三种流程:(1) 完整的握手, 对服务器进行身份验证;(2) 恢复之前的会话采用的简短握手;(3) 对客户端和服务器都进行身份验证的握手。

握手协议消息的标头信息包含消息类型(1 字节)和长度(3 字节),余下的信息则取决于消息类型:

struct {
  HandshakeType msg_type;
  uint24 length;
  HandshakeMessage message;
} Handshake;

握手协议包含以下四种子协议。

  • 握手协议的职责是生成通信过程所需的共享密钥和进行身份认证。这部分使用无密码套件,为防止数据被窃听,通过公钥密码或 Diffie-Hellman 密钥交换技术通信。
  • 密码规格变更协议,用于密码切换的同步,是在握手协议之后的协议。握手协议过程中使用的协议是“不加密”这一密码套件,握手协议完成后则使用协商好的密码套件。
  • 警告协议,当发生错误时使用该协议通知通信对方,如握手过程中发生异常、消息认证码错误、数据无法解压缩等。
  • 应用数据协议,通信双方真正进行应用数据传输的协议。

二、 完整握手

每一个 TLS 连接都会以握手开始。如果客户端此前并未与服务器建立会话,那么双方会执行一次完整的握手流程来协商TLS 会话。握手过程中,客户端和服务器将进行以下四个主要步骤:

  • 交换各自支持的功能,对需要的连接参数达成一致
  • 验证出示的证书,或使用其他方式进行身份验证
  • 对将用于保护会话的共享主密钥达成一致
  • 验证握手消息并未被第三方团体修改

下面介绍最常见的握手规则,一种不需要验证客户端身份但需要验证服务器身份的握手:

理解SSL/TLS系列 (五)握手协议_第1张图片

  1. 客户端开始新的握手,并将自身支持的功能提交给服务器。
  2. 服务器选择连接参数。
  3. 服务器发送其证书链(仅当需要服务器身份验证时)。
  4. 根据选择的密钥交换方式,服务器发送生成主密钥的额外信息。
  5. 服务器通知自己完成了协商过程。
  6. 客户端发送生成主密钥所需的额外信息
  7. 客户端切换加密方式并通知服务器。
  8. 客户端计算发送和接收到的握手消息的MAC并发送。
  9. 服务器切换加密方式并通知客户端。
  10. 服务器计算发送和接收到的握手消息的MAC并发送。

假设没有出现错误,到这一步,连接就建立起来了,可以开始发送应用数据。现在让我们了解一下这些握手消息的更多细节。

ClientHello

这条消息将客户端的功能和首选项传送给服务器。

理解SSL/TLS系列 (五)握手协议_第2张图片

  • Version: 协议版本(protocol version)指示客户端支持的最佳协议版本
  • Random: 一个 32 字节数据,28 字节是随机生成的 (图中的 Random Bytes);剩余的 4 字节包含额外的信息,与客户端时钟有关 。在握手时,客户端和服务器都会提供随机数,客户端的暂记作 random_C (用于后续的密钥的生成)。这种随机性对每次握手都是独一无二的,在身份验证中起着举足轻重的作用。它可以防止 重放攻击,并确认初始数据交换的完整性。
  • Session ID: 在第一次连接时,会话 ID(session ID)字段是空的,这表示客户端并不希望恢复某个已存在的会话。典型的会话 ID 包含 32 字节随机生成的数据,一般由服务端生成通过 ServerHello 返回给客户端。
  • Cipher Suites: 密码套件(cipher suite)块是由客户端支持的所有密码套件组成的列表,该列表是按优先级顺序排列的
  • Compression: 客户端可以提交一个或多个支持压缩的方法。默认的压缩方法是 null,代表没有压缩
  • Extensions: 扩展(extension)块由任意数量的扩展组成。这些扩展会携带额外数据

ServerHello

是将服务器选择的连接参数传回客户端。

 理解SSL/TLS系列 (五)握手协议_第3张图片

这个消息的结构与 ClientHello 类似,只是每个字段只包含一个选项,其中包含服务端的 random_S 参数 (用于后续的密钥协商)。服务器无需支持客户端支持的最佳版本。如果服务器不支持与客户端相同的版本,可以提供某个其他版本以期待客户端能够接受

图中的 Cipher Suite 是后续密钥协商和身份验证要用的加密套件,此处选择的密钥交换与签名算法是 ECDHE_RSA,对称加密算法是 AES-GCM

还有一点默认情况下 TLS 压缩都是关闭的,因为 CRIME 攻击会利用 TLS 压缩恢复加密认证 cookie,实现会话劫持,而且一般配置 gzip 等内容压缩后再压缩 TLS 分片效益不大又额外占用资源,所以一般都关闭 TLS 压缩

Certificate

典型的 Certificate 消息用于携带服务器 X.509 证书链。 服务器必须保证它发送的证书与选择的算法套件一致。比方说,公钥算法与套件中使用的必须匹配。除此以外,一些密钥交换算法依赖嵌入证书的特定数据,而且要求证书必须以客户端支持的算法签名。所有这些都表明服务器需要配置多个证书(每个证书可能会配备不同的证书链)。

理解SSL/TLS系列 (五)握手协议_第4张图片 Certificate 消息是可选的,因为并非所有套件都使用身份验证,也并非所有身份验证方法都需要证书。更进一步说,虽然消息默认使用 X.509 证书,但是也可以携带其他形式的标志;一些套件就依赖 PGP 密钥

ServerKeyExchange

携带密钥交换需要的额外数据。ServerKeyExchange 是可选的,消息内容对于不同的协商算法套件会存在差异。部分场景下,比如使用 RSA 算法时,服务器不需要发送此消息。ServerKeyExchange 仅在服务器证书消息(也就是上述 Certificate 消息)不包含足够的数据以允许客户端交换预主密钥(premaster secret)时才由服务器发送。比如基于 DH 算法的握手过程中,需要单独发送一条 ServerKeyExchange 消息带上 DH 参数:

理解SSL/TLS系列 (五)握手协议_第5张图片

 ServerHelloDone

表明服务器已经将所有预计的握手消息发送完毕。在此之后,服务器会等待客户端发送消息。

理解SSL/TLS系列 (五)握手协议_第6张图片

 verify certificate

客户端验证证书的合法性,如果验证通过才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证内容包括如下:

  • 证书链的可信性 ;
  • 证书是否吊销 revocation,有两类方式 - 离线 CRL 与在线 OCSP,不同的客户端行为会不同;
  • 有效期 expiry date,证书是否在有效时间范围;
  • 域名 domain,核查证书域名是否与当前的访问域名匹配;

PKI体系的内容可知,对端发来的证书签名是 CA 私钥加密的,接收到证书后,先读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后利用对应 CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性;然后去查询证书的吊销情况等

 ClientKeyExchange

合法性验证通过之后,客户端计算产生预主密钥(Pre-master),并用证书公钥加密,发送给服务器并携带客户端为密钥交换提供的所有信息。这个消息受协商的密码套件的影响,内容随着不同的协商密码套件而不同。

此时客户端已经获取全部的计算协商密钥需要的信息: 两个明文随机数 random_C 和 random_S 与自己计算产生的 Pre-master,然后得到协商密钥(用于之后的消息加密)

enc_key = PRF(Pre_master, "master secret", random_C + random_S)

理解SSL/TLS系列 (五)握手协议_第7张图片

图中使用的是 ECDHE 算法,ClientKeyExchange 传递的是 DH 算法的客户端参数,如果使用的是 RSA 算法则此处应该传递加密的预主密钥。

ChangeCipherSpec

通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信

注意 ChangeCipherSpec 不属于握手消息,它是另一种协议,只有一条消息,作为它的子协议进行实现。

Finished (Encrypted Handshake Message)

Finished 消息意味着握手已经完成。消息内容将加密,以便双方可以安全地交换验证整个握手完整性所需的数据。

这个消息包含 verify_data 字段,它的值是握手过程中所有消息的散列值。这些消息在连接两端都按照各自所见的顺序排列,并以协商得到的主密钥 (enc_key) 计算散列。这个过程是通过一个伪随机函数(pseudorandom function,PRF)来完成的,这个函数可以生成任意数量的伪随机数据。 两端的计算方法一致,但会使用不同的标签(finished_label):客户端使用 client finished,而服务器则使用 server finished。

verify_data = PRF(master_secret, finished_label, Hash(handshake_messages))

因为 Finished 消息是加密的,并且它们的完整性由协商 MAC 算法保证,所以主动网络攻击者不能改变握手消息并对 vertify_data 的值造假。在 TLS 1.2 版本中,Finished 消息的长度默认是 12 字节(96 位),并且允许密码套件使用更长的长度。在此之前的版本,除了 SSL 3 使用 36 字节的定长消息,其他版本都使用 12 字节的定长消息。

handshake_messages 中只包含握手子消息,不包括 ChangeCipherSpec 子消息、 Alert 子消息、HelloRequest 消息。

Server

服务器用私钥解密加密的 Pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,同样计算得到协商密钥:

enc_key = PRF(Pre_master, "master secret", random_C + random_S);

同样计算之前所有收发信息的 hash 值,然后用协商密钥解密客户端发送的 verify_data_C,验证消息正确性;

change_cipher_spec

服务端验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信(图中多了一步 New Session Ticket,此为会话票证,会在会话恢复中解释);

Finished (Encrypted Handshake Message)

服务器也结合所有当前的通信参数信息生成一段数据 (verify_data_S) 并采用协商密钥 session secret (enc_key) 与算法加密并发送到客户端;

握手结束

客户端计算所有接收信息的 hash 值,并采用协商密钥解密 verify_data_S,验证服务器发送的数据和密钥,验证通过则握手完成;

加密通信

开始使用协商密钥与算法进行加密通信。

理解SSL/TLS系列 (五)握手协议_第8张图片

RSA 密钥交换和 DH 密钥交换的区别

RSA 历史悠久,支持度好,但不支持 完美前向安全 - PFS(Perfect Forward Secrecy);而 ECDHE 是使用了 ECC(椭圆曲线)的 DH(Diffie-Hellman)算法,计算速度快,且支持 PFS。

在 RSA 密钥交换中,浏览器使用证书提供的 RSA 公钥加密相关信息,如果服务端能解密,意味着服务端拥有与公钥对应的私钥,同时也能算出对称加密所需密钥。密钥交换和服务端认证合并在一起

在 ECDH 密钥交换中,服务端使用私钥 (RSA 或 ECDSA) 对相关信息进行签名,如果浏览器能用证书公钥验证签名,就说明服务端确实拥有对应私钥,从而完成了服务端认证。密钥交换则是各自发送 DH 参数完成的,密钥交换和服务端认证是完全分开的。

可用于 ECDHE 数字签名的算法主要有 RSA 和 ECDSA - 椭圆曲线数字签名算法,也就是目前密钥交换 + 签名有三种主流选择:

  • RSA - RSA 密钥交换(无需签名)
  • ECDHE_RSA - ECDHE 密钥交换、RSA 签名
  • ECDHE_ECDSA - ECDHE 密钥交换、ECDSA 签名

使用 RSA 进行密钥交换的握手过程与前面说明的基本一致,只是没有 ServerKeyExchange 消息,其中协商密钥涉及到三个参数 (客户端随机数 random_C、服务端随机数 random_S、预主密钥 Premaster secret), 其中前两个随机数和协商使用的算法是明文的很容易获取,最后一个 Premaster secret 会用服务器提供的公钥( 一般已经放置在证书中了)加密后传输给服务器 (密钥交换),如果这个预主密钥被截取并破解则协商密钥也可以被破解。而使用 DH(Diffie-Hellman) 算法 进行密钥交换,双方只要交换各自的 DH 参数(在 ServerKeyExchange 发送 Server params,在 ClientKeyExchange 发送 Client params),不需要传递 Premaster secret,就可以各自算出这个预主密钥

DH 的握手过程如下,大致过程与 RSA 类似,图中只表达如何生成预主密钥:

 理解SSL/TLS系列 (五)握手协议_第9张图片

 

服务器通过私钥将客户端随机数 random_C,服务端随机数 random_S,服务端 DH 参数 Server params 签名生成 signature,然后在 ServerKeyExchange 消息中发送服务端 DH 参数和该签名;

客户端收到后用服务器给的公钥解密验证签名,并在 ClientKeyExchange 消息中发送客户端 DH 参数 Client params;

服务端收到后,双方都有这两个参数,再各自使用这两个参数生成预主密钥 Premaster secret,之后的协商密钥等步骤与 RSA 基本一致。

基于 RSA 算法与 DH 算法的握手最大的区别就在于密钥交换与身份认证。前者客户端使用公钥加密预主密钥并发送给服务端完成密钥交换,服务端利用私钥解密完成身份认证。后者利用各自发送的 DH 参数完成密钥交换,服务器私钥签名数据,客户端公钥验签完成身份认证。

RSA算法原理参考 http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html

DH算法原理参考 Wiki 和 Ephemeral Diffie-Hellman handshake

关于 椭圆曲线密码学 - ECC,推荐看下 A Primer on Elliptic Curve Cryptography - 原文 - 译文

三、客户端身份验证

尽管可以选择对任意一端进行身份验证,但人们几乎都启用了对服务器的身份验证。如果服务器选择的套件不是匿名的,那么就需要在 Certificate 消息中跟上自己的证书。

理解SSL/TLS系列 (五)握手协议_第10张图片

 

相比之下,服务器通过发送 CertificateRequest 消息请求对客户端进行身份验证。消息中列出所有可接受的客户端证书。作为响应,客户端发送自己的 Certificate 消息(使用与服务器发送证书相同的格式),并附上证书。此后,客户端发送 CertificateVerify 消息,证明自己拥有对应的私钥。

只有已经过身份验证的服务器才被允许请求客户端身份验证。基于这个原因,这个选项也被称为相互身份验证(mutual authentication)。

CertificateRequest

在 ServerHello 的过程中发出,请求对客户端进行身份验证,并将其接受的证书的公钥和签名算法传送给客户端。

它也可以选择发送一份自己接受的证书颁发机构列表,这些机构都用其可分辨名称来表示:

struct {
  ClientCertificateType certificate_types;
  SignatureAndHashAlgorithm supported_signature_algorithms;
  DistinguishedName certificate_authorities;
} CertificateRequest;

CertificateVerify

在 ClientKeyExchange 的过程中发出,证明自己拥有的私钥与之前发送的客户端证书中的公钥匹配。消息中包含一条到这一步为止的所有握手消息的签名:

struct {
  Signature handshake_messages_signature;
} CertificateVerify;

四、会话恢复

最初的会话恢复机制是,在一次完整协商的连接断开时,客户端和服务器都会将会话的安全参数保存一段时间。希望使用会话恢复的服务器为会话指定唯一的标识,称为会话 ID(Session ID)。服务器在 ServerHello 消息中将会话 ID 发回客户端。

希望恢复早先会话的客户端将适当的 Session ID 放入 ClientHello 消息,然后提交。服务器如果同意恢复会话,就将相同的 Session ID 放入 ServerHello 消息返回,接着使用之前协商的主密钥生成一套新的密钥,再切换到加密模式,发送 Finished 消息。 客户端收到会话已恢复的消息以后,也进行相同的操作。这样的结果是握手只需要一次网络往返。

Session ID 由服务器端支持,协议中的标准字段,因此基本所有服务器都支持,服务器端保存会话 ID 以及协商的通信信息,占用服务器资源较多。

理解SSL/TLS系列 (五)握手协议_第11张图片

用来替代服务器会话缓存和恢复的方案是使用会话票证(Session ticket)。使用这种方式,除了所有的状态都保存在客户端(与 HTTP Cookie 的原理类似)之外,其消息流与服务器会话缓存是一样的。

其思想是服务器取出它的所有会话数据(状态)并进行加密 (密钥只有服务器知道),再以票证的方式发回客户端。在接下来的连接中,客户端恢复会话时在 ClientHello 的扩展字段 session_ticket 中携带加密信息将票证提交回服务器,由服务器检查票证的完整性,解密其内容,再使用其中的信息恢复会话。

这种方法有可能使扩展服务器集群更为简单,因为如果不使用这种方式,就需要在服务集群的各个节点之间同步会话。 Session ticket 需要服务器和客户端都支持,属于一个扩展字段,占用服务器资源很少。

警告 会话票证破坏了 TLS 安全模型。它使用票证密钥加密的会话状态并将其暴露在线路上。有些实现中的票证密钥可能会比连接使用的密码要弱。如果票证密钥被暴露,就可以解密连接上的全部数据。因此,使用会话票证时,票证密钥需要频繁轮换。


五、密钥计算

在 TLS 1.2 中,有 3 种密钥:预备主密钥、主密钥和会话密钥(密钥块),这几个密钥都是有联系的。

         struct {
             uint32 gmt_unix_time;
             opaque random_bytes[28];
         } Random;
         
        struct {
             ProtocolVersion client_version;
             opaque random[46];
         } PreMasterSecret;  

        struct {
             uint8 major;
             uint8 minor;
         } ProtocolVersion;       

对于 RSA 握手协商算法来说,Client 会生成的一个 48 字节的预备主密钥,其中前 2 个字节是 ProtocolVersion,后 46 字节是随机数,用 Server 的公钥加密之后通过 Client Key Exchange 子消息发给 Server,Server 用私钥来解密。对于 (EC)DHE 来说,预备主密钥是双方通过椭圆曲线算法生成的,双方各自生成临时公私钥对,保留私钥,将公钥发给对方,然后就可以用自己的私钥以及对方的公钥通过椭圆曲线算法来生成预备主密钥,预备主密钥长度取决于 DH/ECDH 算法公钥。预备主密钥长度是 48 字节或者 X 字节

主密钥是由预备主密钥、ClientHello random 和 ServerHello random 通过 PRF 函数生成的。主密钥长度是 48 字节。可以看出,只要我们知道预备主密钥或者主密钥便可以解密抓包数据,所以 TLS 1.2 中抓包解密调试只需要一个主密钥即可,SSLKEYLOG 就是将主密钥导出来,在 Wireshark 里面导入就可以解密相应的抓包数据。

会话密钥(密钥块)是由主密钥、SecurityParameters.server_random 和 SecurityParameters.client_random 数通过 PRF 函数来生成,会话密钥里面包含对称加密密钥、消息认证和 CBC 模式的初始化向量,对于非 CBC 模式的加密算法来说,就没有用到这个初始化向量。

Session ID 缓存和 Session Ticket 里面保存的也是主密钥,而不是会话密钥,这样每次会话复用的时候再用双方的随机数和主密钥导出会话密钥,从而实现每次加密通信的会话密钥不一样,即使一个会话的主密钥泄露了或者被破解了也不会影响到另一个会话。

HMAC 和伪随机函数

TLS 记录层使用一个有密钥的信息验证码(MAC)来保护信息的完整性。密码算法族使用了一个被称为HMAC(在[HMAC]中描述)的 MAC 算法,它基于一个 hash 函数。如果必要的话其它密码算法族可以定义它们自己的 MAC 算法。

此外,为了进行密钥生成或验证,需要一个 MAC 算法对数据块进行扩展以增加机密性。这个伪随机函数(PRF)将机密信息(secret),种子和身份标签作为输入,并产生任意长度的输出。

在 TLS 1.2 中,基于 HMAC 定义了一个 PRF 函数。这个使用 SHA-256 hash 函数的 PRF 函数被用于所有的密码算法套件。新的密码算法套件必须显式指定一个 PRF,通常应该使用 SHA-256 或更强的标准 hash 算法与 TLS PRF 一同使用。

首先,我们定义一个数据扩展函数,P_hash(secret, data),它使用一个 hash 函数扩展成一个 secret 和种子,形成任意大小的输出:

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

这里"+"是指级联。A()被定义为:

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

必要时 P_hash 可以被多次迭代,以产生所需数量的数据。例如,如果 P_SHA256 被用于产生 80 字节的数据,它应该被迭代 3 次(通过 A(3)),SHA_256 每次输出 32 字节(256 bit),迭代 3 次才能产生 96 字节的输出数据,最终迭代产生的最后 16 字节会被丢弃,留下 80 字节作为输出数据。 PRF 可以通过将 P_hash 运用与 secret 来实现:

             PRF(secret, label, seed) = P_(secret, label + seed)

label 是一个 ASCII 字符串。它应该以严格地按照它被给出的内容进行处理,不包含一个长度字节或结尾添加的空字符。例如,label "slithy toves" 应该通过 hash 下列字节的方式被处理:

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

上述数据是字符串 "slithy toves" 的十六进制格式。

PRF 使用的 Hash 算法取决于密码套件和 TLS 版本,对应关系如下:

PRF 算法 Hash 算法
prf_tls10 TLS 1.0 和 TLS 1.1 协议,PRF 算法是结合 MD5 和 SHA_1 算法
prf_tls12_sha256 TLS 1.2 协议,默认是 SHA_256 算法(这是能满足最低安全的算法)
prf_tls12_sha384 TLS 1.2 协议,如果加密套件指定的 HMAC 算法安全级别高于 SHA_256,则采用加密基元 SHA_384 算法

计算主密钥

为了开始连接保护,TLS 记录协议要求指定一个算法套件,一个主密钥和 Client 及 Server 端随机数。认证,加密和消息认证码算法由 cipher_suite 确定,cipher_suite 是由 Server 选定并在 ServerHello 消息中表明出来的。压缩算法在 hello 消息里协商出来,随机数也在 hello 消息中交换。所有这些都用于计算主密钥。

对于所有的密钥交换算法,相同的算法都会被用来将 pre_master_secret 转化为 master_secret。一旦 master_secret 计算完毕,pre_master_secret就应当从内存中删除。避免攻击者获取预备主密钥,如果攻击者获取到了预备主密钥,加上 ClientHello.random 和 ServerHello.random 传输过程中是不加密的,也容易获取,那么攻击者就可以合成主密钥并进一步导出会话密钥,这样整个加密过程就被完全破解了

        master_secret = PRF(pre_master_secret, "master secret",
                            ClientHello.random + ServerHello.random)
                           

主密钥的长度一直是 48 字节。预密钥的长度根据密钥交换算法而变

RSA

当RSA被用于身份认证和密钥交换时,Client 会产生一个 48 字节的 pre_master_secret,用 Server 的公钥加密,然后发送给 Server。Server 用它自己的私钥解密 pre_master_secret。然后双方按照前述方法将 pre_master_secret转换为 master_secret。

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

Diffie-Hellman

一个传统的 Diffie-Hellman 计算需要被执行。协商出来的密钥(Z)会被用做pre_master_secret,并按照前述方法将其转换为 master_secret。在被用做pre_master_secret之前,Z 开头所有的 0 位都会被压缩。

注:Diffie-Hellman 参数由 Server 指定,可能是临时的也可能包含在 Server 的证书中。

计算会话密钥

会话密钥(密钥块)用于 TLS 记录层加密。记录协议需要一个算法从握手协议提供的安全参数中生成当前连接状态所需的密钥。

   enum { null(0), (255) } CompressionMethod;

   enum { server, client } ConnectionEnd;

   enum { tls_prf_sha256 } PRFAlgorithm;

   enum { null, rc4, 3des, aes } BulkCipherAlgorithm;

   enum { stream, block, aead } CipherType;

   enum { null, hmac_md5, hmac_sha1, hmac_sha256, hmac_sha384,
     hmac_sha512} MACAlgorithm;

   /* Other values may be added to the algorithms specified in
   CompressionMethod, PRFAlgorithm, BulkCipherAlgorithm, and
   MACAlgorithm. */

   struct {
       ConnectionEnd          entity;
       PRFAlgorithm           prf_algorithm;
       BulkCipherAlgorithm    bulk_cipher_algorithm;
       CipherType             cipher_type;
       uint8                  enc_key_length;
       uint8                  block_length;
       uint8                  fixed_iv_length;
       uint8                  record_iv_length;
       MACAlgorithm           mac_algorithm;
       uint8                  mac_length;
       uint8                  mac_key_length;
       CompressionMethod      compression_algorithm;
       opaque                 master_secret[48];
       opaque                 client_random[32];
       opaque                 server_random[32];
   } SecurityParameters;

主密钥被扩张为一个安全字节序列,它被分割为一个 client_write_MAC_key,一个 server_write_MAC_key,一个 client_write_key,一个 server_write_key。它们中的每一个都是从字节序列中以上述顺序生成。未使用的值是空。一些AEAD加密可能会额外需要一个 client_write_IV 和一个 server_write_IV。生成密钥和 MAC 密钥时,主密钥被用作一个熵源。所以会话密钥(密钥块)的长度和个数取决于协商出来的密码套件,更准确的说是取决于加密参数 SecurityParameters,需要使用 PRF 函数扩展出足够长的密钥块,计算如下:

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

注意:计算会话密钥和主密钥使用 PRF 的三个入参都不同,PRF(secret, label, seed):主密钥是 (pre_master_secret, "master secret", ClientHello.random + ServerHello.random),会话密钥是 (SecurityParameters.master_secret, "key expansion", SecurityParameters.server_random + SecurityParameters.client_random)seed 顺序有变化,Client 和 Server 随机数的组合顺序会调换

直到产生足够的输出。然后,key_block会按照如下方式分开:

      client_write_MAC_key[SecurityParameters.mac_key_length]
      server_write_MAC_key[SecurityParameters.mac_key_length]
      client_write_key[SecurityParameters.enc_key_length]
      server_write_key[SecurityParameters.enc_key_length]
      client_write_IV[SecurityParameters.fixed_iv_length]
      server_write_IV[SecurityParameters.fixed_iv_length]

client_write_key、server_write_key、client_write_MAC_key 和 server_write_MAC_key 是加密和消息验证码需要的密钥。Client 和 Server 分别拥有自己的一套密钥,使用的密钥是不同的。如果是分组加密方式,还需要初始化向量 client_write_IV 和 server_write_IV。如果是 AEAD 模式,client_write_MAC_key 和 server_write_MAC_key 可以不需要,使用 client_write_IV 和 server_write_IV 作为 nonce(随机值) 。

目前,client_write_IV 和 server_write_IV 只能由 AEAD 的隐式 nonce 技术生成。

当前定义的密码协议套件使用最多的是 AES_256_CBC_SHA256。它需要 2 x 32 字节密钥和 2 x 32 字节 MAC 密钥,它们从 128 字节的密钥数据中产生。

总结 TLS 1.2 密钥计算流程如下:

理解SSL/TLS系列 (五)握手协议_第12张图片

 

六. 无密钥交换

如果 CDN 厂商想支持 HTTPS,那么需要做哪些改动呢?国内的厂商的做法是:将自己 HTTPS 网站的私钥上传到 CDN 厂商提供的服务器上。某些对安全性要求非常高的客户(比如银行)想要使用第三方的 CDN,想加快自家网站的访问速度,但是出于安全考虑,不能把私钥交给 CDN 服务商。一种称为keyless的技术可以解决这个问题,即你把网站放到它们的 CDN 上,不用提供自己证书的私钥,也能使用 TLS/SSL 加密链接。

在握手阶段,主要是协商出了 3 个随机数。这 3 个随机数产生了 TLS 记录层需要的会话密钥(密钥块)。握手完成以后,之后的加密都是对称加密。唯一需要用到非对称加密中的私钥。如果是 RSA 密钥协商,私钥的作用是解密 Client 传过来的预备主密钥。非对称加密中的公钥用来加密发给 Client 的密钥协商参数。但是 Server 的公钥可以从证书中获取。所以 CDN 唯一不能解决的问题是解密 Client 发过来的预备主密钥。如果是 ECDHE 密钥协商,私钥的作用是对 DH 参数做签名的。

解决办法比较简单:

如果是 RSA 密钥协商,在 CDN 厂商的服务器收到 Client 发来的预备主密钥的时候,把这个加密过的预备主密钥发给用户自己的 key server,让用户用自己的私钥解密预备主密钥,再发还给 CDN 厂商的服务器,这样 CDN 厂商就有解密之后的预备主密钥了,进而可以继续计算主密钥和会话密钥(密钥块)了。流程如下:

理解SSL/TLS系列 (五)握手协议_第13张图片

如果是 DH 密钥协商算法,预备主密钥可以由 Server 和 Client 共同计算出来,但是 DH 相关的参数需要双方协商出来。Server 将 DH 相关参数发给 Client 的时候,需要用到证书的私钥。CDN 厂商会把 Client 随机数,Server 随机数和 DH 参数三者的 hash 发给用户的 key server,key server 就它们签名以后,发还给 CDN 厂商服务器。CDN 厂商将签名后的消息发给 Client。这样也就完成了密钥协商。CDN 和 Client 相互算出预备主密钥和主密钥还有会话密钥。流程如下:

理解SSL/TLS系列 (五)握手协议_第14张图片

 参考

《HTTPS权威指南》

密钥计算 https://halfrost.com/https-key-cipher/

握手协议部分图 https://juejin.im/post/5b88a93df265da43231f1451

你可能感兴趣的:(TLS)