写 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 这块还有啥不明白的直接告诉我就成了
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.