tls证书与ssl证书(HTTPS温故知新五)
tls证书与ssl证书(HTTPS温故知新五)早期 TLS 协议,verify_data 的长度是 12 字节,对于 TLS 1.2 协议来说,verify_data 的长度取决于密钥套件,如果密码套件没有指定 verify_data_length,则默认长度也是 12 字节。handshake_messages 中只包含握手子消息,不包括 ChangeCipherSpec 子消息、 Alert 子消息、HelloRequest 消息。会话密钥(密钥块)是由主密钥、SecurityParameters.server_random 和 SecurityParameters.client_random 数通过 PRF 函数来生成,会话密钥里面包含对称加密密钥、消息认证和 CBC 模式的初始化向量,对于非 CBC 模式的加密算法来说,就没有用到这个初始化向量。在 TLS 1.2 握手的最后,会发送 Finished 子消息,这条消息是加密的
本篇文章我们来对比对比 TLS 1.2 和 TLS 1.3 中的密钥计算。
一. TLS 1.2 中的密钥在 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 模式的加密算法来说,就没有用到这个初始化向量。
四. TLS 1.2 Finished 校验在 TLS 1.2 握手的最后,会发送 Finished 子消息,这条消息是加密的第一条消息,Finished 消息的接收者必须要验证这条消息的内容是否正确。验证的内容是通过 PRF 算法计算出来的。
verify_data = PRF(master_secret finished_label Hash(handshake_messages)) [0..verify_data_length-1];
在计算 verify_data 的时候,PRF(secret label seed) 中 secret 是主密钥,label 是 finished_label,Client 是 "client finished",Server 是 "server finished",seed 是所有握手消息的 hash 值。对于 Client 来说,handshake_messages 内容包含所有发送的消息和接收的消息,但是不包括自己发送的 Finished 消息。对于 Server 来说,handshake_messages 内容包含从 ClientHello 消息开始截止到 Finished 消息之前的所有消息,也包括 Client 的 Finished 子消息。
handshake_messages 中只包含握手子消息,不包括 ChangeCipherSpec 子消息、 Alert 子消息、HelloRequest 消息。
早期 TLS 协议,verify_data 的长度是 12 字节,对于 TLS 1.2 协议来说,verify_data 的长度取决于密钥套件,如果密码套件没有指定 verify_data_length,则默认长度也是 12 字节。
五. TLS 1.2 的无密钥交换如果 CDN 厂商想支持 HTTPS,那么需要做哪些改动呢?国内的厂商的做法是:将自己 HTTPS 网站的私钥上传到 CDN 厂商提供的服务器上。某些对安全性要求非常高的客户(比如银行)想要使用第三方的 CDN,想加快自家网站的访问速度,但是出于安全考虑,不能把私钥交给 CDN 服务商。读者如果已经看懂了上面 TLS 的密钥计算的方法,完全没有必要把私钥上传到第三方 CDN 服务器上。CloudFlare 很早就提供了 Keyless 服务,即你把网站放到它们的 CDN 上,不用提供自己证书的私钥,也能使用 TLS/SSL 加密链接。
在握手阶段,主要是协商出了 3 个随机数。这 3 个随机数产生了 TLS 记录层需要的会话密钥(密钥块)。握手完成以后,之后的加密都是对称加密。唯一需要用到非对称加密中的私钥。如果是 RSA 密钥协商,私钥的作用是解密 Client 传过来的预备主密钥。非对称加密中的公钥用来加密发给 Client 的密钥协商参数。但是 Server 的公钥可以从证书中获取。所以 CDN 唯一不能解决的问题是解密 Client 发过来的预备主密钥。如果是 ECDHE 密钥协商,私钥的作用是对 DH 参数做签名的。
解决办法比较简单:
如果是 RSA 密钥协商,在 CDN 厂商的服务器收到 Client 发来的预备主密钥的时候,把这个加密过的预备主密钥发给用户自己的 key server,让用户用自己的私钥解密预备主密钥,再发还给 CDN 厂商的服务器,这样 CDN 厂商就有解密之后的预备主密钥了,进而可以继续计算主密钥和会话密钥(密钥块)了。流程如下:
如果是 DH 密钥协商算法,预备主密钥可以由 Server 和 Client 共同计算出来,但是 DH 相关的参数需要双方协商出来。Server 将 DH 相关参数发给 Client 的时候,需要用到证书的私钥。CDN 厂商会把 Client 随机数,Server 随机数和 DH 参数三者的 hash 发给用户的 key server,key server 就它们签名以后,发还给 CDN 厂商服务器。CDN 厂商将签名后的消息发给 Client。这样也就完成了密钥协商。CDN 和 Client 相互算出预备主密钥和主密钥还有会话密钥。流程如下:
在 TLS 1.3 中,不再使用 PRF 这种算法了,而是采用更标准的 HKDF 算法来进行密钥的推导。而且在 TLS 1.3 中对密钥进行了更细粒度的优化,每个阶段或者方向的加密都不是使用同一个密钥。TLS 1.3 在 ServerHello 消息之后的数据都是加密的,握手期间 Server 给 Client 发送的消息用 server_handshake_traffic_secret 通过 HKDF 算法导出的密钥加密的,Client 发送给 Server 的握手消息是用 client_handshake_traffic_secret 通过 HKDF 算法导出的密钥加密的。这两个密钥是通过 Handshake Secret 密钥来导出的,而 Handshake Secret 密钥又是由 PreMasterSecret 和 Early Secret 密钥导出,然后通过 Handshake Secret 密钥导出主密钥 Master Secret。
再由主密钥 Master Secret 导出这几个密钥:
client_application_traffic_secret:用来导出客户端发送给服务器应用数据的对称加密密钥。
server_application_traffic_secret:用来导出服务器发送给客户端应用数据的对称加密密钥。
resumption_master_secret:用来生成 PSK。
最终 server_handshake_traffic_secret、client_handshake_traffic_secret、client_application_traffic_secret、server_application_traffic_secret 这 4 个密钥会分别生成 4 套 write_key 和 write_IV 用于对称加密。
如果用到 early_data,还需要 client_early_traffic_secret,它也会生成 1 套 write_key 和 write_IV 用于加密和解密 0-RTT 数据。
七. TLS 1.3 中的 HMAC 和伪随机函数Key Derivation Function (KDF) 是密码学系统中必要的组件。它的目的是把一个 key 拓展成多个从密码学角度来上说是安全的 key。TLS 1.3 使用的是 HMAC-based extract-and-Expand Key Derivation Function (HKDF),HKDF 根据 extract-then-expand 设计模式,即 KDF 有 2 大模块。第一个阶段是将输入的 key material 进行 "extracts",得到固定长度的 key,然后第二阶段将这个 key "expands" 成多个附加的伪随机的 key,输出的 key 的长度和个数,取决于指定的加密算法。由于 extract 流程不是必须的,所以 expand 流程可以独立的使用。
HMAC 的两个参数,第一个是 key,第二个是 data。data 可以由好几个元素组成,我们一般用 | 来表示,例如:
HMAC(K elem1 | elem2 | elem3)
1. Extract
HKDF-Extract(salt IKM) -> PRK
- 变量:
- Hash 是 hash 函数; HashLen 表示这个 hash 函数的输出字节数。
- 输入:
- salt 是可选的值,如果没有指定,则使用 HashLen 个 0 代替。
- IKM 是输入的 keying material,IKM 是 Input Keying Material 的缩写。
- 输出:
- PRK 是一个 pseudorandom 伪随机的 key (HashLen 字节大小),PSK 是 PseudoRandom Key 的缩写。
PRK 的计算方法如下:
PRK = HMAC-Hash(salt IKM)
HKDF 的定义允许使用有随机值 salt 和不带随机值 salt 的操作。这是为了兼容没有 salt 的应用程序。但是强烈建议使用 salt 能够显著加强 HKDF 算法的强度。并且确保了哈希函数的不同用途之间的独立性,支持 "源独立" extraction,并加强了支持 HKDF 设计的分析结果。
随机 salt 在两个方面与初始密钥材料 IKM 的根本不同是:它随机 salt 是非加密的,可以重复使用。因此,随机 salt 值可用于许多应用。例如,通过将 HKDF 应用于可再生的熵池(例如,采样系统事件)而连续产生输出的伪随机数发生器(PRNG)可以确定盐值并将其用于 HKDF 的多个应用而无需保护其 salt 的秘密性。在不同的应用程序域中,从 Diffie-Hellman 交换中导出加密密钥的密钥协商协议可以从通信方之间交换和验证的公共 nonce 中获取 salt 值,并把这种做法作为密钥协议的一部分(这是 IKEv2 中采用的方法)
理想情况下,salt 值是长度为 HashLen 的随机(或伪随机)字符串。然而,即使质量较低的 salt 值(较短的尺寸或有限的熵)仍然可能对输出密钥材料的安全性做出重大贡献;因此,如果应用程序可以获得这些值,鼓励应用程序设计者向 HKDF 提供 salt 值。
值得注意的是,虽然不是典型的情况,但某些应用甚至可能具有可供使用的加密 salt 值。在这种情况下,HKDF 提供更强大的安全保障。这种应用的一个例子是 IKEv1 在其“公钥加密模式”中,其中提取器的 salt 是从加密的 nonce 计算的。类似地,IKEv1 的预共享模式使用从预共享密钥导出的加密的 salt。
2. Expand
HKDF-Expand(PRK info L) -> OKM
- 变量:
- Hash 是 hash 函数; HashLen 表示这个 hash 函数的输出字节数。
- 输入:
- PRK 是至少 HashLen 字节长度的 pseudorandom key (通常由 extract 流程导出)。
- info 是可选的值,可以是""。
- L 是期望输出的字节数(长度 <= 255 * HashLen)。
- 输出:
- OKM 是输出的 keying material (L 字节),OKM 是 Output Keying Material 的缩写。
OKM 的计算方法如下:
N = ceil(L/HashLen) T = T(1) | T(2) | T(3) | ... | T(N) OKM = first L octets of T where: T(0) = empty string (zero length) T(1) = HMAC-Hash(PRK T(0) | info | 0x01) T(2) = HMAC-Hash(PRK T(1) | info | 0x02) T(3) = HMAC-Hash(PRK T(2) | info | 0x03) ...
虽然 info 值在 HKDF 的定义中是可选的,但它在应用程序中通常非常重要。其主要目标是将派生的密钥材料绑定到特定于应用程序和上下文的信息。例如,info 可以包含协议号,算法标识符,用户身份等。特别地,它可以防止针对不同的上下文导出相同的密钥材料(当在不同背景下使用相同的输入密钥材料(IKM)时)。如果需要,它还可以容纳对密钥扩展部分的附加输入(例如,应用程序可能想要将密钥材料绑定到其长度 L,从而使得 info 字段扩充至 L 长度)。info 有一个技术要求:它应该独立于输入密钥材料 IKM 的值。
对比 TLS 1.2 中的 PRF 计算方法:
PRF(secret label seed) = P_<hash>(secret label seed) P_hash(secret seed) = HMAC_hash(secret A(1) seed) HMAC_hash(secret A(2) seed) HMAC_hash(secret A(3) seed) ... where: A(0) = seed A(i) = HMAC_hash(secret A(i-1)) ...
可以看到这两个算法的区别。
在一些应用中,输入密钥材料 IKM 可能已经作为密码强密钥的存在(例如,TLS RSA 密码套件中的预主密钥将是伪随机字符串,除了前两个字节)。在这种情况下,可以跳过 extract 提取部分并在 expand 扩展步骤中直接使用 IKM 作为 HMAC 的入参。另一方面,为了与一般情况兼容,应用程序仍然可以使用 extract 提取部分。特别是,如果 IKM 是随机(或伪随机)但长于 HMAC 密钥,则 extract 提取步骤可用于输出合适的 HMAC 密钥(在 HMAC 的情况下,通过 extractor 提取器的进行缩短不是严格必要的,因为 HMAC 也需要长度达到一定程度才能工作)。但是请注意,如果 IKM 是 Diffie-Hellman值,就像使用 Diffie-Hellman 的 TLS 一样,则不应跳过 extract 提取部分。这样做会导致使用 Diffie-Hellman 值 g ^ {xy} 本身(不是均匀随机或伪随机字符串)作为 HMAC 的关键PRK。相反,HKDF 应该先将 g ^ {xy} 进行 extract 提取步骤(优选具有 salt 值的),并把所得的 PRK 作为 HMAC expansion 部分的关键部分。
在所需的密钥位数 L 不大于 HashLen 的情况下,可以直接使用 PRK 作为 OKM。但是,这不是推荐的,特别是因为它会省略使用 info 作为推导过程的一部分(并且不建议在 extract 提取步骤中添加 info 作为输入 - 参见 HKDF-paper)
在 TLS 1.3 的密钥派生过程使用 HMAC-based Extract-and-Expand Key Derivation Function (HKDF) [RFC5869] 定义的 HKDF-Extract 和 HKDF-Expand 函数,以及下面定义的函数:
HKDF-Expand-Label(Secret Label Context Length) = HKDF-Expand(Secret HkdfLabel Length) Where HkdfLabel is specified as: struct { uint16 length = Length; opaque label<7..255> = "tls13 " Label; opaque context<0..255> = Context; } HkdfLabel; Derive-Secret(Secret Label Messages) = HKDF-Expand-Label(Secret Label Transcript-Hash(Messages) Hash.length)
Transcript-Hash 和 HKDF 使用的 Hash 函数是密码套件哈希算法。Hash.length 是其输出长度(以字节为单位)。消息是表示的握手消息的串联,包括握手消息类型和长度字段,但不包括记录层头。请注意,在某些情况下,零长度 context(由 "" 表示)传递给 HKDF-Expand-Label。labels 都是 ASCII 字符串,不包括尾随 NUL 字节。
由上面的函数调用关系,可以得到下面的结论:
Derive-Secret(Secret Label Messages) = HKDF-Expand(Secret HkdfLabel Length)
HKDF-Extract(salt IKM) 就是 TLS 1.3 中 HKDF 的 Extract 过程;Derive-Secret(Secret Label Messages) 就是 TLS 1.3 中 HKDF 的 Expand 过程。
3. Transcript-Hash最后再来谈谈 Transcript-Hash 函数。TLS 中的许多加密计算都使用了哈希副本。这个值是通过级联每个包含的握手消息的方式进来哈希计算的,它包含握手消息头部携带的握手消息类型和长度字段,但是不包括记录层的头部。例如:
Transcript-Hash(M1 M2 ... Mn) = Hash(M1 || M2 || ... || Mn)
作为此一般规则的例外,当 Server 用一条 HelloRetryRequest 消息来响应一条 ClientHello 消息时,ClientHello1 的值替换为包含 Hash(ClientHello1)的握手类型为 "message_hash" 的特殊合成握手消息。例如:
Transcript-Hash(ClientHello1 HelloRetryRequest ... Mn) = Hash(message_hash || /* Handshake type */ 00 00 Hash.length || /* Handshake message length (bytes) */ Hash(ClientHello1) || /* Hash of ClientHello1 */ HelloRetryRequest || ... || Mn)
设计这种结构的原因是允许 Server 通过在 cookie 中仅存储 ClientHello1 的哈希值来执行无状态 HelloRetryRequest,而不是要求它导出整个中间哈希状态。
具体而言,哈希副本始终取自于下列握手消息序列,从第一个 ClientHello 开始,仅包括已发送的消息:ClientHello HelloRetryRequest ClientHello ServerHello EncryptedExtensions server CertificateRequest server Certificate server CertificateVerify server Finished EndOfEarlyData client Certificate client CertificateVerify client Finished。
通常上,实现方可以下面的方法来实现哈希副本:根据协商的哈希来维持一个动态的哈希副本。请注意,随后的握手后认证不会相互包含,只是通过主握手结束的消息。
八. TLS 1.3 中的密钥计算经过密钥协商得出来的密钥材料的随机性可能不够,协商的过程能被攻击者获知,需要使用一种密钥导出函数来从初始密钥材料(PSK 或者 DH 密钥协商计算出来的 key)中获得安全性更强的密钥。HKDF 正是 TLS 1.3 中所使用的这样一个算法,使用协商出来的密钥材料和握手阶段报文的哈希值作为输入,可以输出安全性更强的新密钥。
从上一章中,我们知道,HKDF 包括 extract_then_expand 的两阶段过程。extract 过程增加密钥材料的随机性,在 TLS 1.2 中使用的密钥导出函数 PRF 实际上只实现了 HKDF 的 expand 部分,并没有经过 extract,而直接假设密钥材料的随机性已经符合要求。
这一章中,让我们来看看 TLS 1.3 是如何对密钥材料进行 extract_then_expand 的。这一章也展示了 TLS 1.3 比 TLS 1.2 在安全性上更上一层楼的原因。
TLS 1.3 中的所有密钥都是由 HKDF-Extract(salt IKM) 和 Derive-Secret(Secret Label Messages) 联合导出的。其中 Salt 是当前的 secret 状态,输入密钥材料(IKM)是要添加的新 secret 。在 TLS 1.3 中,两个输入的 IKM 是:
- PSK(外部建立的预共享密钥,或从先前连接的 resumption_master_secret 值派生的)
- (EC)DHE 共享 secret
TLS 1.3 完整的密钥导出流程图如下:
0 | v PSK -> HKDF-Extract = Early Secret | -----> Derive-Secret(. "ext binder" | "res binder" "") | = binder_key | -----> Derive-Secret(. "c e traffic" ClientHello) | = client_early_traffic_secret | -----> Derive-Secret(. "e exp master" ClientHello) | = early_exporter_master_secret v Derive-Secret(. "derived" "") | v (EC)DHE -> HKDF-Extract = Handshake Secret | -----> Derive-Secret(. "c hs traffic" | ClientHello...ServerHello) | = client_handshake_traffic_secret | -----> Derive-Secret(. "s hs traffic" | ClientHello...ServerHello) | = server_handshake_traffic_secret v Derive-Secret(. "derived" "") | v 0 -> HKDF-Extract = Master Secret | -----> Derive-Secret(. "c ap traffic" | ClientHello...server Finished) | = client_application_traffic_secret_0 | -----> Derive-Secret(. "s ap traffic" | ClientHello...server Finished) | = server_application_traffic_secret_0 | -----> Derive-Secret(. "exp master" | ClientHello...server Finished) | = exporter_master_secret | -----> Derive-Secret(. "res master" ClientHello...client Finished) = resumption_master_secret
几点说明:
- HKDF-Extract 画在图上,它为从顶部获取 Salt 参数,从左侧获取 IKM 参数,它的输出是底部,和右侧输出的名称。
- Derive-Secret 的 Secret 参数由传入的箭头指示。例如,Early Secret 是生成 client_early_traffic_secret 的 Secret。
- "0" 表示将 Hash.length 字节的字符串设置为零。
如果给定的 secret 不可用,则使用由设置为零的 Hash.length 字节串组成的 0 值。请注意,这并不意味着要跳过轮次,因此如果 PSK 未被使用,Early Secret 仍将是 HKDF-Extract(0 0)。对于 binder_key 的计算,label 是外部 PSK(在 TLS 之外提供的那些)的 "ext binder" 和用于恢复 PSK 的 "res binder"(提供为先前握手的恢复主密钥的那些)。不同的 labels 阻止了一种 PSK 替代另一种 PSK。
这存在有多个潜在的 Early Secret 值,具体取决于 Server 最终选择的 PSK。Client 需要为每个潜在的 PSK 都计算一个值;如果没有选择 PSK,则需要计算对应于零 PSK 的 Early Secret。
一旦计算出了从给定 secret 派生出的所有值,就应该删除该 secret。
TLS 1.3 中涉及到了 3 个 Secret 计算方法如下:
Early Secret = HKDF-Extract(salt IKM) = HKDF-Extract(0 PSK) Handshake Secret = HKDF-Extract(salt IKM) = HKDF-Extract(Derive-Secret(Early Secret "derived" "") (EC)DHE) Master Secret = HKDF-Extract(salt IKM) = HKDF-Extract(Derive-Secret(Handshake Secret "derived" "") 0)
TLS 1.3 中涉及到了 8 个密钥计算方法如下:
client_early_traffic_secret = Derive-Secret(Early Secret "c e traffic" ClientHello) early_exporter_master_secret = Derive-Secret(Early Secret "e exp master" ClientHello) client_handshake_traffic_secret = Derive-Secret(Handshake Secret "c hs traffic" ClientHello...ServerHello) server_handshake_traffic_secret = Derive-Secret(Handshake Secret "s hs traffic" ClientHello...ServerHello) client_application_traffic_secret_0 = Derive-Secret(Master Secret "c ap traffic" ClientHello...server Finished) server_application_traffic_secret_0 = Derive-Secret(Master Secret "s ap traffic" ClientHello...server Finished) exporter_master_secret = Derive-Secret(Master Secret "exp master" ClientHello...server Finished) resumption_master_secret = Derive-Secret(Master Secret "res master" ClientHello...client Finished)
例如:
CLIENT_EARLY_TRAFFIC_SECRET edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 5a0d40c3afa57cbb5aa427456f8dc21b9c4c17bfb731600f93e35358f5b581cb EARLY_EXPORTER_SECRET edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 274e61024f88d0952898889a54211200a76456434d8e546cd6450f8313412df5 CLIENT_HANDSHAKE_TRAFFIC_SECRET edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 c041776dc29543e87e3442111be79f289062eef7603ec566f28f5b05b15c9718 SERVER_HANDSHAKE_TRAFFIC_SECRET edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 68e19a5d69dfdf8ca701a370cfd7c21e98b1bd933c03ee9dd72738e60147e8db CLIENT_TRAFFIC_SECRET_0 edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 b866b25bc12f5272dbc6d27471edce47d04f496362b56800d5f95e0760d044ee SERVER_TRAFFIC_SECRET_0 edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 8f07b32b6191019bac664d5071dd961e92ff2060db629d4e3eb3689a43cc71d3 EXPORTER_SECRET edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 c7a1fb9092f245a8b92cd7a481eb0bd6d255b4d06c6d05096ef8a8bf3face22e
EXPORTER_SECRET 是导出密钥,用于用户自定义的其他用途。
上面得到的 8 个密钥除去 2 个用户自定义需要的导出密钥,和会话恢复的 resumption_master_secret,剩下的 5 个密钥虽然是经过一次 HKDF 的 Expand 过程,但是这 5 个密钥仍然只是“中间变量”,生成最后的加密参数还需要一次 Expand 过程:
[sender]_write_key = HKDF-Expand-Label(Secret "key" "" key_length) [sender]_write_iv = HKDF-Expand-Label(Secret "iv" "" iv_length)
[sender] 表示发送方。每种记录类型的 Secret 值显示在下表中:
------------------- --------------------------------------- | Record Type | Secret | ------------------- --------------------------------------- | 0-RTT Application | client_early_traffic_secret | | | | | Handshake | [sender]_handshake_traffic_secret | | | | | Application Data | [sender]_application_traffic_secret_N | ------------------- ---------------------------------------
每当底层 Secret 更改时(例如,从握手更改为应用数据密钥或密钥更新时),将重新计算所有流量密钥材料。
resumption_master_secret 密钥是为了会话恢复导出 PSK 的,计算方法如下:
PskIdentity.identity = ticket = HKDF-Expand-Label(resumption_master_secret "resumption" ticket_nonce Hash.length)
Server 在 NewSessionTicket 中把 ticket 发送到 Client,Client 利用 ticket 生成 PskIdentity。再计算 PskBinderEntry:
PskBinderEntry = HMAC(binder_key Transcript-Hash(Truncate(ClientHello1))) = HMAC(Derive-Secret(HKDF-Extract(0 PSK) "ext binder" | "res binder" "") Transcript-Hash(Truncate(ClientHello1))) 其中 binder_key = Derive-Secret(HKDF-Extract(0 PSK) "ext binder" | "res binder" "")
Client 将 PskIdentity 和 PskBinderEntry 结合成 PSK,在需要会话恢复的时候把 PSK 作为 ClientHello 的扩展发给 Server。PSK 作为 Early Secret 的输入密钥材料 IKM。
Early Secret = HKDF-Extract(salt IKM) = HKDF-Extract(0 PSK) client_early_traffic_secret = Derive-Secret(Early Secret "c e traffic" ClientHello)
由 client_early_traffic_secret 生成的 write_key 和 write_iv 最终用于 0-RTT 的加密和解密。
TLS 1.3 0-RTT 密钥计算流程如下:
TLS 1.3 中的 Finished 并不算是整个握手中的第一条加密消息,作用和 TLS 1.2 是相同的,它对提供握手和计算密钥的身份验证起了至关重要的作用。
在 TLS 1.3 中 Authentication 消息的计算统一采用以下的输入方式:
- 要使用证书和签名密钥
- 握手上下文由哈希副本中的一段消息集组成
- Base key 用于计算 MAC 密钥
Finished 子消息根据 Transcript-Hash(Handshake Context Certificate CertificateVerify) 的值得出的 MAC 。使用从 Base key 派生出来的 MAC key 计算的 MAC 值。
对于每个场景,下表定义了握手上下文和 MAC Base Key
----------- ------------------------- ----------------------------- | Mode | Handshake Context | Base Key | ----------- ------------------------- ----------------------------- | Server | ClientHello ... later | server_handshake_traffic_ | | | of EncryptedExtensions/ | secret | | | CertificateRequest | | | | | | | Client | ClientHello ... later | client_handshake_traffic_ | | | of server | secret | | | Finished/EndOfEarlyData | | | | | | | Post- | ClientHello ... client | client_application_traffic_ | | Handshake | Finished | secret_N | | | CertificateRequest | | ----------- ------------------------- -----------------------------
用于计算 Finished 消息的密钥是使用 HKDF,Base Key 是 server_handshake_traffic_ secret 和 client_handshake_traffic_secret。特别的:
finished_key = HKDF-Expand-Label(BaseKey "finished" "" Hash.length)
这条消息的数据结构是:
struct { opaque verify_data[Hash.length]; } Finished;
verify_data 按照如下方法计算:
verify_data = HMAC(finished_key Transcript-Hash(Handshake Context Certificate* CertificateVerify*)) * Only included if present.
HMAC [RFC2104] 使用哈希算法进行握手。如上所述,HMAC 输入通常是通过动态的哈希实现的,即,此时仅是握手的哈希。
在以前版本的 TLS 中,verify_data 的长度总是 12 个八位字节。在 TLS 1.3 中,它是用来表示握手的哈希的 HMAC 输出的大小。
注意:警报和任何其他非握手记录类型不是握手消息,并且不包含在哈希计算中。
Finished 消息之后的任何记录 Post-Handshake 都必须在适当的 client_application_traffic_secret_N 下加密。特别是,这包括 Server 为了响应 Client 的 Certificate 消息和 CertificateVerify 消息而发送的任何 alert。
十. TLS 1.3 KeyUpdate看到这里读者可能会问,为什么在文章最后还会再讨论 TLS 1.3 的 KeyUpdate 消息?因为这条消息会触发 TLS 1.3 重新计算密钥。所以需要细究一下这条消息。
研究表明 如果使用同一个密钥加密大量的数据,攻击者有几率可以通过记录所有密文并找出特征,逆推出对称加密密钥。因此需要引进一个密钥同步更新的机制,该机制同时也使用 HKDF 算法,在旧密钥的基础上衍生出新一轮的密钥。
当加密的报文达到一定长度后,双方也需要发送 KeyUpdate 报文重新计算加密密钥。
KeyUpdate 握手消息用于表示发送方正在更新其自己的发送加密密钥。任何对等方在发送 Finished 消息后都可以发送此消息。在接收 Finished 消息之前接收 KeyUpdate 消息的,实现方必须使用 "unexpected_message" alert 消息终止连接。发送 KeyUpdate 消息后,发送方应使用新一代的密钥发送其所有流量。收到 KeyUpdate 后,接收方必须更新其接收密钥。
enum { update_not_requested(0) update_requested(1) (255) } KeyUpdateRequest; struct { KeyUpdateRequest request_update; } KeyUpdate;
- request_update:
- 这个字段表示 KeyUpdate 的收件人是否应使用自己的 KeyUpdate 进行响应。 如果实现接收到任何其他的值,则必须使用 "illegal_parameter" alert 消息终止连接。
如果 request_update 字段设置为 "update_requested",则接收方必须在发送其下一个应用数据记录之前发送自己的 KeyUpdate,其中 request_update 设置为 "update_not_requested"。此机制允许任何一方强制更新整个连接,但会导致一个实现方接收多个 KeyUpdates,并且它还是静默的响应单个更新。请注意,实现方可能在发送 KeyUpdate (把 request_update 设置为 "update_requested") 与接收对等方的 KeyUpdate 之间接收任意数量的消息,因为这些消息可能早就已经在传输中了。但是,由于发送和接收密钥是从独立的流量密钥中导出的,因此保留接收流量密钥并不会影响到发送方更改密钥之前发送的数据的前向保密性。
如果实现方独立地发送它们自己的 KeyUpdates,其 request_update 设置为 "update_requested" 并且它们的消息都是传输中,结果是双方都会响应,双方都会更新密钥。
发送方和接收方都必须使用旧密钥加密其 KeyUpdate 消息。另外,在接受使用新密钥加密的任何消息之前,双方必须强制接收带有旧密钥的 KeyUpdate。如果不这样做,可能会引起消息截断攻击。
下一代流量密钥的计算方法是,从 client_ / server_application_traffic_secret_N 生成出 client_ / server_application_traffic_secret_N 1,然后按上一节所述方法重新导出流量密钥。
下一代 application_traffic_secret 计算方法如下:
application_traffic_secret_N 1 = HKDF-Expand-Label(application_traffic_secret_N "traffic upd" "" Hash.length)
一旦计算了 client_ / server_application_traffic_secret_N 1 及其关联的流量密钥,实现方应该删除 client_ / server_application_traffic_secret_N 及其关联的流量密钥。
十一. TLS 1.3 中的密钥导出在 TLS 1.3 中,有 2 个导出密钥 exporter:
early_exporter_master_secret = Derive-Secret(Early Secret "e exp master" ClientHello) exporter_master_secret = Derive-Secret(Master Secret "exp master" ClientHello...server Finished)
RFC5705 根据 TLS 伪随机函数(PRF)定义 TLS 的密钥材料 exporter。TLS 1.3 用 HKDF 取代 PRF,因此需要新的结构。exporter 的接口保持不变。
exporter 的值计算方法如下:
TLS-Exporter(label context_value key_length) = HKDF-Expand-Label(Derive-Secret(Secret label "") "exporter" Hash(context_value) key_length)
Secret 可以是 early_exporter_master_secret 或 exporter_master_secret。除非应用程序明确指定,否则实现方必须使用 exporter_master_secret。early_exporter_master_secret 被定义用来在 0-RTT 数据需要 exporter 的设置这种情况中使用。建议为 early exporter 提供单独的接口;这可以避免 exporter 用户在需要常规 exporter 时意外使用 early exporter,反之亦然。
如果未提供上下文,则 context_value 为零长度。因此,不提供上下文计算与提供空上下文得到的结果都是相同的。这是对以前版本的 TLS 的更改,以前的 TLS 版本中,空的上下文产生的输出与不提供的上下文的结果不同。截至 TLS 1.3,无论是否使用上下文,都不会使用已分配的 exporter 标签。未来的规范绝不能定义允许空上下文和没有相同标签的上下文的 exporter 的使用。exporter 的新用法应该是在所有 exporter 计算中提供上下文,尽管值可能为空。
exporter 标签格式的要求在 [RFC5705] 第4节 中定义。
Reference:
RFC 5246
RFC 8466
Keyless SSL: The Nitty Gritty Technical Details
Cryptographic Extraction and Key Derivation:
The HKDF Scheme
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub
Source: https://halfrost.com/HTTPS-key-cipher/