2. 后量子密码算法基础与OpenSSL支持现状
要理解 OpenSSL 为什么在 3.x 系列中反复调整 PQC 接口,先要弄清 NIST 后量子标准化选出的几类算法在数学结构、密钥与密文规模、安全模型上的差异。这些差异直接决定了 API 的形态:有的算法需要封装/解封装语义,有的只需要签名/验签,有的签名体积大到必须重新考虑缓冲区管理。
NIST PQC 标准算法的核心特性
NIST 后量子标准化进程在 2022 年公布首批入选算法,2024 年正式发布 FIPS 203、FIPS 204、FIPS 205 三项标准。它们分别对应密钥封装、数字签名和基于哈希的签名三条技术路线。
ML-KEM(FIPS 203,原 Kyber)是基于模格上带误差学习问题(Module-LWE)的密钥封装机制(KEM)。它的使用模式与 RSA 密钥交换不同:发送方用接收方公钥封装出一个共享密钥并产生密文,接收方用私钥解封装恢复同一共享密钥。ML-KEM 定义了 512、768、1024 三个参数集,安全强度大致对应 AES-128、AES-192、AES-256 级别。典型规模上,ML-KEM-768 的公钥约 1184 字节、密文约 1088 字节,远大于传统 ECDH 的 32~65 字节,这对 TLS 握手报文长度和分片策略有实际影响。
ML-DSA(FIPS 204,原 Dilithium)同样基于模格问题,但用于数字签名。它采用 Fiat-Shamir with Aborts 范式,签名过程可能需要多次拒绝采样,因此签名时间存在一定波动。ML-DSA 提供 44、65、87 三个参数集,签名体积在 2.4~4.6 KB 量级,公钥约 1.3~2.6 KB。与 Ed25519 的 64 字节签名相比,证书链和握手消息会显著膨胀。
SLH-DSA(FIPS 205,原 SPHINCS+)是唯一不依赖格问题的标准算法,安全性仅建立在哈希函数抗碰撞与抗原像假设上,属于“保守派”选择。代价是签名极大(约 7~50 KB)、签名速度慢,但验签相对快,且安全假设最简单。它适合对长期安全性要求极高、对体积和性能不敏感的场景,例如固件签名根、代码签名根证书。
OpenSSL 各版本的 PQC 支持矩阵
OpenSSL 对 PQC 的支持是逐步演进的,且早期版本大量依赖第三方分支(如 OQS-OpenSSL)。理解这条演进线,能避免在错误版本上调用不存在的接口。
| 版本 | PQC 支持情况 | 接口形态 |
|---|---|---|
| 1.1.1 及更早 | 无原生 PQC | 需借助 OQS-OpenSSL 等分支,接口非标准 |
| 3.0 / 3.1 | 无原生标准 PQC | 依赖 provider 机制加载第三方实现 |
| 3.2 | 引入部分 PQC 实验性支持 | 通过 provider 暴露 KEM 与签名能力 |
| 3.3 | 开始提供 ML-KEM、ML-DSA 等实现 | EVP_PKEY 体系 + provider 算法名 |
| 3.5 | PQC 支持趋于完整,默认 provider 覆盖更广 | 统一走 EVP 高层接口,支持 TLS 集成 |
需要强调:上表是能力演进的方向性描述,具体算法名、参数集和默认启用状态会随小版本变化。工程中应以目标版本的 openssl list -kem-algorithms 与 openssl list -signature-algorithms 实际输出为准,而不是凭记忆写死算法名。
接口演进:从低层结构到 EVP 统一抽象
OpenSSL 3.x 的核心变化是把算法实现下沉到 provider,上层统一通过 EVP 接口访问。对 PQC 而言,这意味着两件事:第一,KEM 不再走“密钥交换”老路,而是有了独立的封装/解封装语义;第二,签名算法无论经典还是后量子,都收敛到 EVP_PKEY 的 sign/verify 流程。
下面这段示例演示 ML-KEM 的封装与解封装调用骨架。算法名与参数集需按实际版本确认,此处以 ML-KEM-768 为例:
/* 生成 ML-KEM 密钥对(示意,需目标版本支持该算法名) */
EVP_PKEY *pkey = NULL;
EVP_PKEY_CTX *gctx = EVP_PKEY_CTX_new_from_name(NULL, "ML-KEM-768", NULL);
EVP_PKEY_keygen_init(gctx);
EVP_PKEY_keygen(gctx, &pkey);
/* 发送方:封装,得到共享密钥与密文 */
EVP_PKEY_CTX *ectx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
unsigned char secret[32], ct[1200];
size_t secretlen = sizeof(secret), ctlen = sizeof(ct);
EVP_PKEY_encapsulate_init(ectx, NULL);
EVP_PKEY_encapsulate(ectx, ct, &ctlen, secret, &secretlen);
/* 接收方:解封装,恢复同一共享密钥 */
EVP_PKEY_decapsulate_init(dctx, NULL);
EVP_PKEY_decapsulate(dctx, secret2, &secret2len, ct, ctlen);
这段代码的关键不在语法,而在两个判断点:一是 EVP_PKEY_CTX_new_from_name 返回 NULL 说明当前 provider 不提供该算法,应立即回退或报错;二是 EVP_PKEY_encapsulate 的密文缓冲区必须足够大,建议先以 NULL 输出参数查询所需长度,再分配内存,避免因 ML-KEM 密文上千字节而溢出。
签名侧则完全复用既有 EVP 流程,只是算法名换成 ML-DSA-65 或 SLH-DSA-*。真正的工程难点在于签名和公钥体积变大后,证书、CSR、TLS 握手缓冲区的容量规划,以及验签失败时如何区分“算法不支持”和“签名确实无效”。
工程落地时的三个判断点
第一,先探测再编码。不同发行版打包的 OpenSSL 版本和 provider 配置不同,同一份代码在开发机可用、在目标机报“unknown algorithm”是常见问题。启动时用 openssl list -kem-algorithms 和 openssl list -signature-algorithms 确认算法可见性,比在代码里硬编码更可靠。
第二,按体积重新规划缓冲区。ML-KEM 密文、ML-DSA 签名都达到千字节级,TLS 记录层、证书解析、消息队列都要预留足够空间。建议封装前先用输出参数为 NULL 的调用查询长度,再分配。
第三,区分安全模型再选型。若系统只信任哈希假设,选 SLH-DSA;若追求性能与体积平衡,选 ML-KEM + ML-DSA 组合;若需要与经典算法混合过渡,则采用混合密钥交换与双证书策略。选型不是“越新越好”,而是安全假设、性能预算与互操作性的三方权衡。