写 TLS1.3 的协议解析自动机的时候,不单实现了 cavium 卡的流程,还得实现软实现。所以硬生生对着 HKDF 的 RFC 和 OPENSSL 的源代码吃了一遍。做的时候还有些疑问,不知道有没有本科生看,希望我写的能直接给本科生看。
做密钥衍生的时候常常需要根据初始密钥材料( initial keying material )产生符合特定长度要求,密码学安全标准的新密钥的需求。因此 RFC5889 定义了一种基于 HMAC 的密钥衍生函数( KDF ),合起来就是 HKDF ( KDF 前面加一个 H )。HKDF-Extract 与 HKDF-Expand 就是一枚硬币的两面,一体双生,两者结合才能产生安全的新密钥。
HKDF-Extract 从初始密钥材料中“拽( extract )”出固定长度的伪随机密钥 K(K 就是个代号),
HKDF-Expand 过程,负责将伪随机密钥 K“拉( expand )”也就是拓展为多份附加伪随机密钥,也就是 KDF 的输出。
RFC 定义的非常简单了。
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(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)
代码实现的基础是实现 HMAC-hash,这个代码不多讲,属于基础知识。以后单独摘出来说。下面的代码直接抄的 openssl 的,我司的代码和 openssl 非常相似(废话,一样的做法必然相似啊)
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 是存储结果的。
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”那章,那么这个实现可以说是非常简单了。
在看 RFC 的时候,主要由两个疑问
写到这里差不多就可以结束了,TLS 这块还有啥不明白的直接告诉我就成了
![]() | 1 hxndg OP 卧槽,竟然 V 站的帖子能在 GOOGLE 搜到,我的博客搜不到!这么蛋疼吗 |