2. 后量子密码部署排障:ML-KEM 密钥封装参数不匹配导致解密失败
故障现象与定位思路
在应用层用 ML-KEM 做密钥封装时,典型报错是解封装返回失败或返回的共享密钥与封装方不一致。ML-KEM 的密文长度由参数集唯一决定:ML-KEM-512 为 768 字节,ML-KEM-768 为 1088 字节,ML-KEM-1024 为 1568 字节。只要双方参数集不同,或密文在传输/存储中被截断,解封装就会失败。排查顺序应为:先确认双方参数集,再核对密文实际长度,最后检查调用代码是否把长度写死或按错误类型转换。
用 liboqs 复现并定位参数不匹配
下面用 liboqs 的 C 接口构造一个最小复现:封装方使用 ML-KEM-768,解封装方误用 ML-KEM-512 的密钥对与算法对象。关键点是解封装前先校验密文长度是否等于该算法声明的 length_ciphertext,长度不符直接判定为参数或截断问题,而不是继续调用解封装。
#include <stdio.h>
#include <string.h>
#include <oqs/oqs.h>
int main(void) {
OQS_KEM *kem_enc = OQS_KEM_new(OQS_KEM_alg_ml_kem_768);
OQS_KEM *kem_dec = OQS_KEM_new(OQS_KEM_alg_ml_kem_512);
if (!kem_enc || !kem_dec) { printf("算法不可用\n"); return 1; }
uint8_t *pk = malloc(kem_enc->length_public_key);
uint8_t *sk = malloc(kem_enc->length_secret_key);
uint8_t *ct = malloc(kem_enc->length_ciphertext);
uint8_t *ss_enc = malloc(kem_enc->length_shared_secret);
uint8_t *ss_dec = malloc(kem_dec->length_shared_secret);
OQS_KEM_keypair(kem_enc, pk, sk);
OQS_KEM_encaps(kem_enc, ct, ss_enc, pk);
/* 关键校验:密文长度必须与解封装方算法声明一致 */
printf("封装方密文长度 = %zu\n", kem_enc->length_ciphertext);
printf("解封装方期望长度 = %zu\n", kem_dec->length_ciphertext);
if (kem_enc->length_ciphertext != kem_dec->length_ciphertext) {
printf("参数集不匹配:密文长度不一致,停止解封装\n");
return 2;
}
OQS_STATUS rc = OQS_KEM_decaps(kem_dec, ss_dec, ct, sk);
printf("decaps 返回 = %d, 共享密钥一致 = %d\n",
rc, memcmp(ss_enc, ss_dec, kem_enc->length_shared_secret) == 0);
free(pk); free(sk); free(ct); free(ss_enc); free(ss_dec);
OQS_KEM_free(kem_enc); OQS_KEM_free(kem_dec);
return 0;
}
预期输出中,封装方密文长度为 1088,解封装方期望长度为 768,程序在长度校验处返回 2 并打印“参数集不匹配”,不会进入 OQS_KEM_decaps。这正是把“解密失败”从模糊报错转化为可判定条件的关键一步。
密文截断的典型来源与修正
参数集一致但仍失败时,重点查密文长度。常见来源有三类:一是接收缓冲区按固定 1024 字节分配,ML-KEM-768 的 1088 字节被截断;二是把二进制密文当字符串处理,遇到 0x00 提前终止;三是 Base64 解码后未校验解码长度。修正方式是始终用算法对象查询长度,并在解封装前做长度断言。
size_t expect = kem_dec->length_ciphertext;
size_t actual = received_len; /* 从报文或存储中读到的真实长度 */
if (actual != expect) {
fprintf(stderr, "密文长度异常: 期望 %zu, 实际 %zu\n", expect, actual);
return -1; /* 截断或参数不匹配,禁止继续解封装 */
}
OQS_STATUS rc = OQS_KEM_decaps(kem_dec, ss_dec, ct, sk);
if (rc != OQS_SUCCESS) {
fprintf(stderr, "解封装失败,检查密钥对与参数集\n");
return -1;
}
strlen 或 strcpy 处理 ML-KEM 密文与共享密钥,它们是二进制数据,可能包含 0x00。传输时使用长度前缀或定长字段,并在解码后再次核对长度。OpenSSL 3.x 接口下的对应检查
若使用 OpenSSL 的 ML-KEM 提供者接口,同样以算法名和密文长度作为判定依据。通过 EVP_PKEY_CTX 获取算法后,封装得到的密文长度应与解封装方所用算法一致;不一致时 EVP_PKEY_decapsulate 会失败。排查时打印双方算法名与 outlen,即可快速区分是参数集问题还是缓冲区长度问题。
| 参数集 | 密文长度(字节) | 公钥长度(字节) | 共享密钥长度(字节) |
|---|---|---|---|
| ML-KEM-512 | 768 | 800 | 32 |
| ML-KEM-768 | 1088 | 1184 | 32 |
| ML-KEM-1024 | 1568 | 1568 | 32 |