TLS Keying Material Export(密钥材料导出)是 RFC 5705 定义的一个重要功能,允许从 TLS 会话中导出密钥材料,用于构建更上层的安全协议。本文介绍其原理及 OpenSSL 命令行实战。

什么是 Keying Material Export?

RFC 5705 定义了从 TLS 握手或连接中导出密钥材料的机制。导出的密钥材料可以用于:

  • 额外密钥派生:为应用程序特定用途生成额外的加密密钥
  • Channel Binding:绑定 TLS 通道到上层协议(如 TLS-SRP)
  • API 认证:某些 API 使用导出的密钥材料进行身份验证

导出公式如下:

1
2
3
4
5
6
exporter_label:导出标签(如 "client finished")
context_value:上下文值(可选)
length:导出长度

ExporterMaterial(ExporterLabel, ContextValue, Length) =
    HKDF-Expand-Label(DerivedSecret, ExporterLabel, ContextValue, Length)

使用 OpenSSL s_client 导出

OpenSSL s_client 提供了 -keymatexport-keymatexportlen 两个参数:

1
2
3
openssl s_client -connect example.com:443 \
    -keymatexport "my-app-label" \
    -keymatexportlen 32

参数说明

参数 说明
-keymatexport 导出使用的标签(label),字符串类型
-keymatexportlen 导出密钥材料的长度(字节),默认 20

完整示例

1
2
3
4
5
6
7
8
9
# 连接到测试服务器并导出 32 字节密钥材料
$ echo "Q" | openssl s_client -connect cloudflare.com:443 \
    -keymatexport "application-key" \
    -keymatexportlen 32 2>/dev/null | grep -A3 "Keying material"

Keying material exporter:
    Label: 'application-key'
    Length: 32 bytes
    Keying material: 529152F31C7576568F7946568F1032BCE53AC1D52ED2699A3D4791AE57755748

本地测试服务器

也可以使用 OpenSSL s_server 创建本地测试服务器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 1. 生成测试证书
openssl req -x509 -newkey rsa:2048 -keyout /tmp/test.key -out /tmp/test.crt -days 1 -nodes -subj "/CN=test"

# 2. 启动测试服务器
openssl s_server -key /tmp/test.key -cert /tmp/test.crt -www -accept 4433 &

# 3. 客户端连接并导出密钥材料
echo "Q" | openssl s_client -connect localhost:4433 \
    -keymatexport "test-label" \
    -keymatexportlen 32

输出示例:

1
2
3
4
Keying material exporter:
    Label: 'test-label'
    Length: 32 bytes
    Keying material: A1B2C3D4E5F6...(32字节十六进制)

在程序中使用 EVP API

如果需要在 C/C++ 程序中使用 Keying Material Export,OpenSSL 3.x 提供了 EVP API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <openssl/ssl.h>
#include <openssl/kdf.h>

int export_keying_material(SSL *ssl, const char *label, 
                           unsigned char *out, size_t outlen) {
    EVP_KDF *kdf = EVP_KDF_fetch(NULL, "TLS13-KDF", NULL);
    if (!kdf) return -1;

    EVP_KDF_CTX *ctx = EVP_KDF_CTX_new(kdf);
    EVP_KDF_free(kdf);
    if (!ctx) return -1;

    OSSL_PARAM params[] = {
        OSSL_PARAM_construct_utf8_string("mode", "exporter", 0),
        OSSL_PARAM_construct_utf8_string("label", label, 0),
        OSSL_PARAM_construct_octet_string("secret", 
            SSL_get_master_key(ssl, NULL), SSL_get_master_key_length(ssl)),
        OSSL_PARAM_construct_size_t("length", &outlen),
        OSSL_PARAM_construct_octet_string("output", out, outlen),
        OSSL_PARAM_END
    };

    int ret = EVP_KDF_CTX_set_params(ctx, params);
    EVP_KDF_CTX_free(ctx);
    return ret == 1 ? 0 : -1;
}

TLS 1.2 与 TLS 1.3 的差异

TLS 1.2 和 TLS 1.3 在 Keying Material Export 上有区别:

特性 TLS 1.2 TLS 1.3
导出时机 握手完成后 握手完成后
基础密钥 master_secret derived_secret
API 兼容性 SSL_export_keying_material() TLS 1.3 HKDF

TLS 1.3 中,导出使用 HKDF-Expand-Label,公式更简洁。

安全注意事项

  1. 标签唯一性:使用应用专属的标签,避免与标准标签(如 “client finished”)冲突
  2. 上下文值:可使用上下文值进一步区分导出内容
  3. 密钥长度:根据实际需求选择合适长度,AES-256 需要 32 字节
  4. TLS 版本:TLS 1.3 优先,其 HKDF 设计更安全

参考来源


本文示例基于 OpenSSL 3.0.20 测试。