4. 开发环境搭建与测试向量获取
在进入 ML-KEM 的算法实现之前,先把“能编译、能跑测试、能对照官方向量”这条链路打通,是后续所有开发与验证工作的前提。本节围绕三件事展开:Rust 与 C 两套工具链的配置要点、NIST 官方 KAT 测试向量的下载与解析方法、以及如何把单元测试框架与向量驱动测试集成到工程中。三者构成一个闭环——工具链负责产出可执行代码,KAT 向量提供权威的期望值,测试框架负责自动比对并给出可复现的结论。
一、Rust 与 C 工具链配置
ML-KEM 的实现通常需要同时兼顾安全性与可移植性,因此课程采用 Rust 作为主实现语言、C 作为交叉验证与性能对照语言。Rust 侧建议使用 rustup 管理工具链,固定一个稳定版本以保证构建可复现:
rustup toolchain install 1.79.0
rustup override set 1.79.0
cargo new mlkem-dev --lib
cd mlkem-dev
cargo add rand_core --features getrandom
在 Cargo.toml 中显式声明 edition = "2021",并建议开启 [profile.test] 的 opt-level = 2,因为 ML-KEM 的 NTT 与采样运算在 debug 模式下会慢一个数量级,向量测试动辄上千组,优化等级直接影响迭代效率。若需要 no_std 目标(嵌入式场景),应关闭默认特性并自行提供随机源。
C 侧推荐 clang 或 gcc 配合 CMake,关键是打开告警并固定标准:
cmake_minimum_required(VERSION 3.20)
project(mlkem_c C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror -O2")
add_library(mlkem STATIC src/ntt.c src/poly.c src/indcpa.c src/kem.c)
enable_testing()
add_executable(test_kat tests/test_kat.c)
target_link_libraries(test_kat mlkem)
两套工具链都要注意同一件事:ML-KEM 涉及大量定长整数与字节序转换,务必在工程内统一“小端序”约定,并在头文件或模块文档中写明,避免 Rust 与 C 交叉比对时因序列化差异产生假失败。
二、NIST 官方 KAT 测试向量的获取与解析
ML-KEM 已由 NIST 标准化为 FIPS 203,其官方测试向量随提交包与 ACVP 测试系统发布。获取渠道主要有两类:一是 NIST PQC 项目页面提供的 KAT 响应文件(.rsp),二是 ACVP 服务器导出的 JSON 向量。前者格式简单、适合入门;后者字段更完整、适合工程化回归。下载后应先校验文件哈希,再放入工程的 tests/vectors/ 目录并纳入版本管理。
.rsp 文件采用“段头 + 键值对”的文本结构,每个测试组以 count 分隔。以 ML-KEM-768 的封装测试为例,典型片段如下(示例格式,具体数值以官方文件为准):
# ML-KEM-768
count = 0
ek = 1A2B...(1184 字节公钥)
m = 3C4D...(32 字节随机消息)
c = 5E6F...(1088 字节密文)
K = 7A8B...(32 字节共享密钥)
count = 1
...
解析时要抓住三个要点。第一,字段是十六进制字符串,需按字节还原为 Vec<u8> 或 uint8_t[],长度必须与 FIPS 203 规定的参数集严格一致:ML-KEM-512 的 ek 为 800 字节、c 为 768 字节;768 对应 1184/1088;1024 对应 1568/1568。第二,封装测试的输入是 ek 与 m,输出是 c 与 K;解封装测试的输入是 dk 与 c,输出是 K,两类测试不能混用字段。第三,count 是组内序号而非全局序号,跨参数集合并解析时要带上参数集标识作为主键。
.rsp 转成内存中的结构体数组,并附带“字段长度断言”。这样一旦向量文件被误替换或截断,测试会在解析阶段就报错,而不是在密码运算深处产生难以定位的失败。三、单元测试框架集成与向量驱动测试
Rust 内置的 #[test] 足以承担向量驱动测试。核心思路是把解析出的每组向量映射为一个测试用例,或在单个测试中循环遍历并逐组断言。后者更常用,因为可以在失败信息中打印 count 与参数集,便于定位:
#[test]
fn kat_encaps_768() {
let groups = parse_rsp("tests/vectors/ML-KEM-768.rsp");
for g in groups {
let (c, k) = encaps(&g.ek, &g.m);
assert_eq!(c, g.c, "count={} 密文不匹配", g.count);
assert_eq!(k, g.k, "count={} 共享密钥不匹配", g.count);
}
}
C 侧可用 CMake 的 CTest 注册用例,配合一个极简断言宏即可,无需引入重型框架:
#define CHECK_EQ_BYTES(a, b, n, msg) \
do { if (memcmp((a),(b),(n)) != 0) { \
fprintf(stderr, "FAIL %s\n", msg); return 1; } } while (0)
集成时有三条实践建议。其一,把“确定性随机”作为可注入参数:KAT 向量中的 m 与解封装中的隐式拒绝随机值都必须可控,否则无法复现。其二,为负向测试单独建组,例如篡改密文一个字节后验证解封装输出为隐式拒绝的伪随机密钥而非报错,这是 ML-KEM 抗选择密文攻击特性的关键验证点。其三,在 CI 中同时跑 Rust 与 C 两套实现,并增加一条“交叉比对”测试:同一组向量分别喂给两种实现,比较输出是否一致,可有效发现字节序与边界处理错误。
上图把本节的关键结构串成一条纵向链路:工具链层产出两套可执行实现,测试向量层提供权威期望值,解析模块把文本向量转成带长度校验的结构化数据,最终在比对层完成“密文与共享密钥一致”的判定。实际工程中,这条链路应被 CI 完整覆盖,任何一次提交都能自动回答一个问题:当前实现是否仍与 NIST 官方向量逐字节一致。
.rsp 中的十六进制字符串按字符而非字节处理,导致长度翻倍;二是解封装负向测试中误把“隐式拒绝”当成错误返回,从而漏测抗 CCA 特性;三是 Rust 与 C 的优化等级不一致,在涉及未定义行为的边界代码上出现结果分歧。建议在测试入口统一打印参数集、count 与失败字段名,把定位成本降到最低。完成本节后,工程应具备这样的能力:一条命令即可编译两套实现、解析全部 KAT 向量、逐组比对并输出通过率;新增一组向量只需放入目录而无需改代码。这条基础设施一旦稳定,后续的 NTT 优化、采样实现与侧信道防护才有可靠的回归基线。