2. 后量子密码部署排障:ML-KEM 密钥封装参数不匹配导致解密失败

故障现象与定位思路

在应用层用 ML-KEM 做密钥封装时,典型报错是解封装返回失败或返回的共享密钥与封装方不一致。ML-KEM 的密文长度由参数集唯一决定:ML-KEM-512 为 768 字节,ML-KEM-768 为 1088 字节,ML-KEM-1024 为 1568 字节。只要双方参数集不同,或密文在传输/存储中被截断,解封装就会失败。排查顺序应为:先确认双方参数集,再核对密文实际长度,最后检查调用代码是否把长度写死或按错误类型转换。

ML-KEM 解封装失败的两类根因与判定 封装方(发送端) 参数集:ML-KEM-768 密文长度:1088 B 共享密钥 K₁ 传输 / 存储通道 可能截断、编码转换 Base64 / 定长缓冲区 解封装方(接收端) 参数集:ML-KEM-512(不一致) 期望密文长度:768 B 解封装失败 / K₂ ≠ K₁ 判定一:参数集是否一致 检查双方 kem_alg 名称: · 封装方 OQS_KEM_alg_ml_kem_768 · 解封装方 OQS_KEM_alg_ml_kem_512 → 名称不同即参数集不匹配 修复:统一为同一参数集, 并同步更新密钥对与密文长度常量 判定二:密文长度是否被截断 读取实际密文长度 len: · 期望 1088 B(ML-KEM-768) · 实际 1024 B → 被截断 → len ≠ OQS_KEM_length_ciphertext 修复:按算法查询密文长度, 避免定长缓冲区与 Base64 丢字节

用 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-51276880032
ML-KEM-7681088118432
ML-KEM-10241568156832
排查口诀:先比算法名,再比密文长;长度不符不解封,长度相符再查密钥对是否同源。共享密钥长度三者均为 32 字节,因此不能用共享密钥长度反推参数集,必须看密文长度或算法名。