HKDF-Extract 与 HKDF-Expand

2020-08-28 18:48:27 +08:00
 hxndg

HKDF-Extract 与 HKDF-Expand

前言

写 TLS1.3 的协议解析自动机的时候,不单实现了 cavium 卡的流程,还得实现软实现。所以硬生生对着 HKDF 的 RFC 和 OPENSSL 的源代码吃了一遍。做的时候还有些疑问,不知道有没有本科生看,希望我写的能直接给本科生看。

HKDF-Extract 与 HKDF-Expand 的作用

做密钥衍生的时候常常需要根据初始密钥材料( initial keying material )产生符合特定长度要求,密码学安全标准的新密钥的需求。因此 RFC5889 定义了一种基于 HMAC 的密钥衍生函数( KDF ),合起来就是 HKDF ( KDF 前面加一个 H )。HKDF-Extract 与 HKDF-Expand 就是一枚硬币的两面,一体双生,两者结合才能产生安全的新密钥。

HKDF-Extract

HKDF-Extract 从初始密钥材料中“拽( extract )”出固定长度的伪随机密钥 K(K 就是个代号),

HKDF-Expand

HKDF-Expand 过程,负责将伪随机密钥 K“拉( expand )”也就是拓展为多份附加伪随机密钥,也就是 KDF 的输出。

HKDF-Extract 与 HKDF-Expand 的 RFC

RFC 定义的非常简单了。

HKDF-Extract

HKDF-Extract(salt, IKM) -> PRK

   Options:
      Hash     a hash function; HashLen denotes the length of the
               hash function output in octets

   Inputs:
      salt     optional salt value (a non-secret random value);
               if not provided, it is set to a string of HashLen zeros.
      IKM      input keying material

   Output:
      PRK      a pseudorandom key (of HashLen octets)

   The output PRK is calculated as follows:

   PRK = HMAC-Hash(salt, IKM)

可以看到,HKDF-Extract 的过程非常简单,本质上就是初始密钥材料加盐做一次 Hmac-Hash 。如果没有提供盐的话,就是遗传长度为 hashLen 的 0 字符串。

HKDF-Expand

HKDF-Expand(PRK, info, L) -> OKM

   Options:
      Hash     a hash function; HashLen denotes the length of the
               hash function output in octets
			   
   Inputs:
      PRK      a pseudorandom key of at least HashLen octets
               (usually, the output from the extract step)
      info     optional context and application specific information
               (can be a zero-length string)
      L        length of output keying material in octets
               (<= 255*HashLen)

   Output:
      OKM      output keying material (of L octets)

   The output OKM is calculated as follows:

   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)
   ...

“拉”的过程略显复杂,我们下面按假设来做操作:

T(N) = HMAC-Hash(PRK, T(N-1) | info | N)

HKDF-Extract 与 HKDF-Expand 的代码实现

代码实现的基础是实现 HMAC-hash,这个代码不多讲,属于基础知识。以后单独摘出来说。下面的代码直接抄的 openssl 的,我司的代码和 openssl 非常相似(废话,一样的做法必然相似啊)

HKDF-Extract

static unsigned char *HKDF_Extract(const EVP_MD *evp_md,
                                   const unsigned char *salt, size_t salt_len,
                                   const unsigned char *key, size_t key_len,
                                   unsigned char *prk, size_t *prk_len)
{
        unsigned int tmp_len;

        if (!HMAC(evp_md, salt, salt_len, key, key_len, prk, &tmp_len)) {
				return NULL;
		}
        

        *prk_len = tmp_len;
        return prk;
}

简单说说,salt 就是参与计算的盐,salt_len 为盐长度,如果盐为 NULL 或者盐长度为 0,就会被初始化为空字符串即static const unsigned char dummy_key[1] = {'\0'};,key 就是输入的初始密钥材料,利用它计算出来伪随机密钥( PRK )。这里唯一注意的就是 prk 和 prk_len 是存储结果的。

HKDF-Expand

static unsigned char *HKDF_Expand(const EVP_MD *evp_md,
                                  const unsigned char *prk, size_t prk_len,
                                  const unsigned char *info, size_t info_len,
                                  unsigned char *okm, size_t okm_len)
{
    HMAC_CTX *hmac;
    unsigned char *ret = NULL;

    unsigned int i;

    unsigned char prev[EVP_MAX_MD_SIZE];

    size_t done_len = 0, dig_len = EVP_MD_size(evp_md);

    size_t n = okm_len / dig_len;  //计算需要产生几块 T(x),如果像输出的结果不是 hashLen 的整数倍,需要向上取整。
    if (okm_len % dig_len)
        n++;

    if (n > 255 || okm == NULL)		//如果输出的地址或者长度太长,直接就当失败了。
        return NULL;

    if ((hmac = HMAC_CTX_new()) == NULL) //初始化 HMAC 失败那就算了,“毁灭吧,赶紧的”
        return NULL;

    if (!HMAC_Init_ex(hmac, prk, prk_len, evp_md, NULL)) //初始化下 hmac 使用的函数
        goto err;

    for (i = 1; i <= n; i++) {
        size_t copy_len;
        const unsigned char ctr = i;

        if (i > 1) {
            if (!HMAC_Init_ex(hmac, NULL, 0, NULL, NULL))
                goto err;

            if (!HMAC_Update(hmac, prev, dig_len)) //如果不是第一次计算,也就是由 T(0)计算 T(1),那么需要把 T(N-1)作为数据拼接到计算中,失败就直接算了
                goto err;
        }

        if (!HMAC_Update(hmac, info, info_len))		//可以拼接上 info 信息了,失败就直接算了
            goto err;

        if (!HMAC_Update(hmac, &ctr, 1))		//可以拼接上计数器序号了,失败就直接算了
            goto err;

        if (!HMAC_Final(hmac, prev, NULL))		//好,算一次 hmac,然后就从 T(N-1)得到了 T(N)了。
            goto err;
												//下面的结果就是不断把 T(x)拼接起来的过程,边拷贝边叠加长度
        copy_len = (done_len + dig_len > okm_len) ?
                       okm_len - done_len :
                       dig_len;

        memcpy(okm + done_len, prev, copy_len);

        done_len += copy_len;
    }
    ret = okm;									//这就是输出的结果

 err:
    OPENSSL_cleanse(prev, sizeof(prev));
    HMAC_CTX_free(hmac);
    return ret;
}

具体流程我就不提了,如果你看懂了“HKDF-Extract 与 HKDF-Expand 的 RFC”那章,那么这个实现可以说是非常简单了。

HKDF-Extract 与 HKDF-Expand 的一些疑问

在看 RFC 的时候,主要由两个疑问

结尾

写到这里差不多就可以结束了,TLS 这块还有啥不明白的直接告诉我就成了

2149 次点击
所在节点    SSL
1 条回复
hxndg
2021-02-22 18:59:10 +08:00
卧槽,竟然 V 站的帖子能在 GOOGLE 搜到,我的博客搜不到!这么蛋疼吗

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/702282

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX