TLS1.3---密钥的计算

TLS1.2中的PRF(Pseudo-random Function)

通过对TLS1.2的学习,我了解到其中具有三种密钥:预主密钥、主密钥和会话密钥。
在其握手过程中,客户端和服务器都会根据密码套件生成自己的临时公私钥对,并且将公钥发送给对方,当客户端接收到服务器的公钥时,会生成预主密钥。
之后客户端会使用PRF函数计算出主密钥发送给服务器,计算时需要三个输入:

  • 预主密钥
  • 客户端生成的随机数
  • 服务器生成的随机数

如果使用的是RSA,那么预主密钥的结构如下:

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

        struct {
             uint8 major;
             uint8 minor;
         } ProtocolVersion;   

会话密钥也是通过PRF函数生成的,需要输入:

  • 主密钥
  • 客户端生成的随机数
  • 服务器生成的随机数

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

P_HASH算法

它使用一个 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))

"+"表示连接,HMAC_hash的运算次数取决于想要结果的长度。
在计算不同类型的密钥时,对应的secret值也是不同的

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

label必须是ASCII字符串,不包含结尾的’\0’。

HKDF(HMAC-key derivation function)

通过前面的介绍,我们已经大概了解了TLS1.2中的密钥生成实现过程,在TLS1.3中主要使用的是HKDF中的 HKDF-Extract 和 HKDF-Expand 函数,HKDF的主要的目标是获取一些初始的密钥材料,并从中派生出一个或多个安全强度很大的密钥。主要分为两步:

提取

HKDF-Extract(salt, IKM) -> PRK

使用协商好的哈希算法将输入的密钥尽量的伪随机化。

  • Salt:输入,如果不提供则全部初始化为0的字符串,长度则为所采用哈希函数的散列值长度。
  • IKM:输入,密钥材料,在TLS1.3中比较复杂。
  • PRK:输出,伪随机化后的密钥,长度则为所采用哈希函数的散列值长度。

扩展

通过一系列的哈希运算,将密钥扩展到我们所需要的长度,有点类似TLS1.2中的P_HASH。

HKDF-Expand(PRK,info,L)-> OKM 
  • info:可选上下文和应用程序特定信息(可以是零长度字符串),我们可以理解为要计算密钥的名称。
  • L:所需要的长度。
  • OKM:计算得出的密钥结果

下面看一下OKM的计算方式:

   N = ceil(L / HashLen)
   T = T(1| T(2| T(3| ... | T(N)
   OKM = T的前L个八位位组,

   其中:
   T(0= 空字符串(零长度)
   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... 

   (其中,连接到每个T(n)末尾的常数是一个
   八位位组。)

n为0-255的整数,L除以所用哈希函数输出摘要的长度,再向上取整,我们将其记为n。
然后将T(n)串接起来,而我们想要获得的OKM正是取T的前L个字节组成。这要我们就得到了我们想要的密钥材料。

TLS1.3密钥计算

 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 是其输出长度(以字节为单位)。消息是表示的握手消息的串联,包括握手消息类型和长度字段,但不包括记录层头,labels 都是 ASCII 字符串。
我们可以看到由于HKDF-Expand-Label(Secret, Label, Context, Length) = HKDF-Expand(Secret, HkdfLabel, Length)所以Derive-Secret相当于HKDF中的HKDF-Expand函数,所以TLS1.3中整个的密钥生成过程主要还是由HKDF中的两个函数计算得来。
其中的HkdfLabel:

HkdfLabel = Hash.length(2 字节) + label_length(1字节) + "tls13 " + Label + Hash.length(1字节) + HASH(Messages)

Transcript-Hash

TLS 中的许多加密计算都使用了哈希副本。这个值是通过级联每个包含的握手消息的方式进来哈希计算的,它包含握手消息头部携带的握手消息类型和长度字段,但是不包括记录层的头部。我的理解是不包括ContenType、legacyRecordVersion、length字段的数据。示例:

Transcript-Hash(M1, M2, ... Mn) = Hash(M1 || M2 || ... || Mn)

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

从上面开始分析,PSK和0通过HKDF-Extract计算得到Early Secret,然后右侧的第一个是使用Derive-Secret函数生成binder_key,这是什么呢?我们再分析一下之前理解的不是很清楚的PreSharedKey

struct {
          opaque identity<1..2^16-1>;
          uint32 obfuscated_ticket_age;
      } PskIdentity;

      opaque PskBinderEntry<32..255>;

      struct {
          PskIdentity identities<7..2^16-1>;
          PskBinderEntry binders<33..2^16-1>;
      } OfferedPsks;

      struct {
          select (Handshake.msg_type) {
              case client_hello: OfferedPsks;
              case server_hello: uint16 selected_identity;
          };
      } PreSharedKeyExtension;

PSK binders

那么我们主要来说一下PSK binders,也就是PskBinderEntry是怎样计算的:
它的计算过程与 Finished 消息一致,只不过其中的Basekey是通过PSK派生的,也就是binder_key

The PskBinderEntry is computed in the same way as the Finished message (Section 4.4.4) but with the BaseKey being the binder_key.
  • 首先就是就是计算binder_key,前面已经说过计算过程了
  • 接着我们看一下Finished计算过程:
    HMAC(Transcript-Hash(all_handshake), finished_key)所以我们还需要计算finished_key
  • 计算finished_key,先看公式:finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)
  • 其中的BaseKey正常计算的话,这是在client中计算的,应该是在密钥导出流程图中的client_handshake_traffic_secret
  • 然而此时是binder_key,所以已经可以计算得出,我们只需要计算的是:Transcript-Hash(all_handshake)
  • 计算当前clienthello的Transcript-Hash,其中不包括PSK BindersPSK Binders length,然后和finished_key一起计算HMAC值,即可得出最终结果。

那我们接着往下看,如果给定的 secret 不可用,则使用由设置为零的 Hash.length 字节串组成的 0 值。它不会跳过本轮次,所示当不适用PSK时,Early Secret 仍将是 HKDF-Extract(0,0)。

至于client_early_traffic_secretearly_exporter_master_secret的作用是需要使用会话恢复时,生成的相关密钥。
然后再往下看,Early Secret经过Derive-Secret计算的结果作为下面计算Handshake Secretsalt(EC)DHE作为IKM(EC)DHE是server和client协商出的公钥。然后又扩展成xxx_handshake_traffic_secret,其中都是把clienthello和serverhello中的信息作为Messages来计算的。

继续往下的话,依然是这样的原理,后面的Messages组成部分有Finished。说一下密钥导出吧,我也还不是很理解这是啥意思,慢慢理解吧,先写了:

TLS-Exporter(label, context_value, key_length) =
       HKDF-Expand-Label(Derive-Secret(Secret, label, ""),
                         "exporter", Hash(context_value), key_length)

其中的Secret是右侧计算出的exporter_master_secretearly_exporter_master_secret

最后再来说一下,右侧生成的xxx_traffic_secret都叫做Basekey,应用于不同的场景,并且这些密钥也不会用来加密数据,最终还需要:

[sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
[sender]_write_iv  = HKDF-Expand-Label(Secret, "iv" , "", iv_length)

我们可以把它们理解为中间变量,再需要一次HKDF-Expand计算就可以得到对应的加密密钥了。
[sender] 表示发送方。每种记录类型的 Secret 值显示在下表中:

  	   +-------------------+---------------------------------------+
       | Record Type       | Secret                                |
       +-------------------+---------------------------------------+
       | 0-RTT Application | client_early_traffic_secret           |
       |                   |                                       |
       | Handshake         | [sender]_handshake_traffic_secret     |
       |                   |                                       |
       | Application Data  | [sender]_application_traffic_secret_N |
       +-------------------+---------------------------------------+

client_early_traffic_secret生成的 write_keywrite_iv 最终用于 0-RTT 的加密和解密。

猜你喜欢

转载自blog.csdn.net/qq_35324057/article/details/105792293