TLS1.3 密钥衍生计算方法和功能
前言
Tls1.3 的 RFC ( RFC8446 )中规定了各种密钥的算法和用途,虽然我打算在不久的将来翻译 TLS1.3 的 RFC,出于方便的考虑,还是简单讲讲 TLS1.3 中的各种密钥计算方法。因为 RFC 的密钥衍生流程中没有画出 PSK 的计算过程,我也会在本文附加 PSK 的计算流程如果文章帮到了你 /或者依然存在一些问题,欢迎给我一些反馈。
我本来是写到了博客里的把这文章,想了想还是在 V 站再发一遍吧,我平时有 V 站检索资料的习惯。就顺便发出来了。
密钥衍生流程
密钥衍生流程中一些计算方法
密钥衍生的流程依赖于 RFC5869 定义的 HKDF-Extract 和 HKDF-Expand 操作流程,这两个流程结合到一起才算是从密钥原始材料( key material)衍生出安全的符合长度需求的密钥。TLS1.3 定义了一个 HKDF-Expand-Label 即带衍生标签的 HKDF-Expand 操作,这个操作的流程是:
Derive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(Secret, Label,Transcript-Hash(Messages), Hash.length) = HKDF-Expand(Secret, HkdfLabel, Hash.length)
如果你知道 HKDF-Expand 的操作(我会不久翻译 RFC5869 并且讲解 HKDF 的两个操作),问题就变成了 HkdfLabel 是如何拼接的。按照 RFC,HkdfLabel 的拼接方式如下:
struct { uint16 length = Length; opaque label<7..255> = “tls13 ” + Label; opaque context<0..255> = Context; } HkdfLabel;
首先拼接一个 uint16_t 的“长度”,也就是两个字节,该“长度”的数值等于当前算法使用的 hash 算法的长度,比方说你使用 ciphersuite 是 TLS_AES_256_GCM_SHA384,所以该“长度的数值”等于 SHA384 计算结果的长度,也就是 384/8=48 字节。之后先拼接上一个 uint8_t 的“字符串长度”,这个长度用来标识整个字符串的长度,包括”tls1.3″+Label 的长度,然后附加上字符串”tls13″(我这里附加的时候可不包括两个双引号啊),再拼接上 Label,比方说你此时在计算复用主密钥(resumption_master_secret),那么这时候的 Label 就是”res master”。最后拼接一个 uint8_t 的 Context 长度,再拼接 Context 。Context 一般是对消息做 hash 计算得出的。
密钥衍生流程及每个密钥的作用
这部分我是直接粘贴的 TLS1.3 RFC 的内容,见下,由于翻译可能产生争议,这部分我用的是原图。 密钥衍生流程中有两个用于计算的基础密钥,分别是 PSK 和 ECDHE secret 。PSK 通过上次连接由复用主密钥计算得出 /带外数据传输建立,(EC)DHE 密钥则是完整握手时协商产生。这两个密钥并不一定每次都存在,比方说完整握手的情况下,计算初期密钥( Early Secret )的时候可能不存在 PSK,那么 PSK 就由一串长度等同于当前 ciphersuite 使用的 hash 算法结果的长度的 0x00 代替,比方说 ciphersuite 是 TLS_AES_256_GCM_SHA384,那么 PSK 就用长度等于 48 的 0x00 内容替代。我在写 TLS1.3 自动机的时候就直接写了个
static const unsigned char default_zeros[EVP_MAX_MD_SIZE];
来替代。
流程当中 HKDF-Extract 顶部的数据充当“盐”的角色,左侧是原始密钥材料( IKM ),其输出结果为衍生出来的密钥。我们熟悉了 HKDF-Extract 和 Derive-Secret 之后,就会发现 TLS1.3 中是结合了 HKDF-Extract 和 HKDF-Expand 一起计算出来各种密钥的,这点比 TLS1.2 更符合 RFC 的要求,更安全。
还有一点值得注意,整个密钥衍生流程是不可以跳过任何一步的,每一步都必须进行(这里的必须进行只针对需要的数据,比方说你不开复用,那么就没必要计算 PSK,复用主密钥之类的)。举个简单的例子,完整握手的情况下,仍然需要计算初期密钥,计算的方式就是 HKDF-Extract(0, 0)。
最后一个需要注意的是,我们计算出来的密钥并不直接参与到会话 /握手当中去,是要经过一论演算的。也就是说从 secret 到 key 是有一个过程的,举个例子说,客户端握手密钥( client_handshake_traffic_secret )计算到客户端会话 key 是有个过程的,如下:
client_handshake_write_key = HKDF-Expand-Label(client_handshake_traffic_secret, “key”, “”, key_length) client_handshake_write_iv = HKDF-Expand-Label(client_handshake_traffic_secret, “iv”, “”, iv_length)
我这里之所以写 client_handshake_write_key 的时候加了 write 是因为客户端发送握手消息使用的 key 和客户端接受握手消息(服务端发送握手消息)的 key 是不一样的。这一点在 TLS1.2 中也是如此。
PSK 的计算过程
这次只简单写写 PSK 的计算过程,复用时如何计算 binder value,恢复会话的操作以后单独写。 PSK 由复用主密钥( resumption_master_secret )计算得出,公式如下:
PSK = HKDF-Expand-Label(resumption_master_secret,”resumption”, ticket_nonce, Hash.length)
这里复用主密钥不需要多说,而 ticket_nonce 需要简单说说:ticket_nonce 每个连接只有一个,其值应当每个都不一样。ticket_nonce 需要明文发给客户端,让客户端计算出来 PSK 。包含 ticket_nonce 的报文就是 new session ticket 报文,简写为 NST 报文。
直接对照“密钥衍生流程中一些计算方法”就可以清晰的计算出来 PSK,实际上从 PSK 到 HMAC key,再到 binder key 的计算过程非常简单,完全可以一次算完。为了节省复用时的时间,可以直接计算存储 binder key 。但是由于涉及到操作都是 hmac/hash,所以本质上时间减少的很少。这东西我第一个想出来以后被我司拿去申请了专利,还给了点奖金。
密钥衍生中的一些细节
这里写的东西是我当初踩到的一些坑,有的我注意到避开了,有的没注意到踩了进去。 1 计算 HKDF-Extract 和 HKDF-Extract 时,需要注意,对于空消息的 hash 结果并不为空。其结果时有值的。因为忘了 hash 计算的流程,就容易踩这个坑。目前 tls1.3 的 cipher 只有两种 hash 长度一个是 sha384 的 48,另一个时 sha256 的 32 。直接把当时写的结果贴上来了,各位也不用自己算了。
uint8_t zero_sha384_hash[TLSV13_SHA384_HASH_LEN] = {0x38, 0xb0, 0x60, 0xa7,0x51, 0xac, 0x96, 0x38, 0x4c, 0xd9, 0x32, 0x7e,0xb1, 0xb1, 0xe3, 0x6a, 0x21, 0xfd, 0xb7, 0x11,0x14, 0xbe, 0x07, 0x43, 0x4c, 0x0c, 0xc7, 0xbf,0x63, 0xf6, 0xe1, 0xda, 0x27, 0x4e, 0xde, 0xbf,0xe7, 0x6f, 0x65, 0xfb, 0xd5, 0x1a, 0xd2, 0xf1,0x48, 0x98, 0xb9, 0x5b}; uint8_t zero_sha256_hash[TLSV13_SHA256_HASH_LEN] = {0xe3, 0xb0, 0xc4, 0x42,0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8,0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4,0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,0x78, 0x52, 0xb8, 0x55};
2 对于每个 TLS1.3 的连接,需要自己保存 ticket_nonce 。踩这个坑是因为写的自动机时异步,计算 PSK 时候用的 ticket_nonce 和写 NST 报文的时候的 ticket_nonce 可能会变化。因此需要每个连接自己保存自己的 ticket_nonce 。
结尾
写到这里差不多就可以结束了,TLS 这块还有啥不明白的直接告诉我就成了