3. ML-KEM参数集与数据结构

ML-KEM(FIPS 203)定义了三个参数集:ML-KEM-512、ML-KEM-768 和 ML-KEM-1024。三者的算法流程完全一致,差异集中在多项式环的维度 k、模数 q、噪声参数以及由此决定的密钥与密文长度上。理解这些差异,是正确选择参数集、正确分配缓冲区、正确实现序列化的前提。本节先梳理参数差异,再深入密钥与密文的字节级编码格式,最后给出序列化与反序列化的实现要点。

三个参数集的核心差异

ML-KEM 工作在环 R_q = Z_q[X]/(X^256+1) 上,模数固定为 q = 3329,多项式次数固定为 256。参数集之间的区别在于向量维度 k:ML-KEM-512 取 k=2,ML-KEM-768 取 k=3,ML-KEM-1024 取 k=4。维度直接决定了公钥向量和密文向量的分量个数,从而决定了所有编码长度。噪声参数 η₁ 与 η₂ 也随参数集变化:512 使用 η₁=3, η₂=2,768 与 1024 使用 η₁=2, η₂=2。压缩参数 d_u 与 d_v 则分别为 10 和 4,三个参数集相同。

下表给出三个参数集的关键数值,这些数值直接对应实现中的常量定义:

参数 ML-KEM-512 ML-KEM-768 ML-KEM-1024
k(向量维度)234
η₁ / η₂3 / 22 / 22 / 2
d_u / d_v10 / 410 / 410 / 4
公钥 ek 长度(字节)80011841568
私钥 dk 长度(字节)163224003168
密文 ct 长度(字节)76810881568
共享密钥长度(字节)323232

这些长度不是随意设定的,而是由编码规则精确推导出来的。下面逐一说明。

公钥 ek 的编码格式

公钥 ek 由两部分拼接而成:向量 t̂ 的编码,后接种子 ρ。向量 t̂ 含 k 个多项式,每个多项式有 256 个系数,每个系数是 Z_q 中的元素(0 到 3328)。编码时每个系数占 12 位,即 1.5 字节,因此单个多项式占 256 × 12 / 8 = 384 字节。整个向量占 384k 字节。种子 ρ 固定 32 字节。于是:

ek 长度 = 384k + 32
k=2 → 384×2 + 32 = 800
k=3 → 384×3 + 32 = 1184
k=4 → 384×4 + 32 = 1568

12 位系数的打包规则是:每两个系数(共 24 位)写入 3 个字节。设两个系数为 a 和 b,则三个字节依次为 a & 0xFF、(a >> 8) | ((b & 0x0F) << 4)、b >> 4。反序列化时按相反顺序拆解。实现中必须严格按此位序,否则与标准测试向量不一致。

私钥 dk 的编码格式

私钥 dk 的结构更复杂,由四段拼接:dk = dkPKE ‖ ek ‖ H(ek) ‖ z。其中 dkPKE 是底层 PKE 的私钥,即向量 ŝ 的编码,长度同样为 384k 字节;ek 是公钥,长度 384k+32;H(ek) 是公钥的 SHA3-256 哈希,32 字节;z 是隐式拒绝用的随机种子,32 字节。因此:

dk 长度 = 384k + (384k + 32) + 32 + 32 = 768k + 96
k=2 → 768×2 + 96 = 1632
k=3 → 768×3 + 96 = 2400
k=4 → 768×4 + 96 = 3168

注意 dk 中内嵌了完整的 ek 和 H(ek),这是为了在解封装时能够重新计算并校验,也是隐式拒绝机制的数据基础。实现时不能只存 dkPKE,否则解封装会失败。

密文 ct 的编码格式

密文由压缩后的向量 u 和压缩后的多项式 v 拼接而成。压缩参数 d_u=10 表示 u 的每个系数压缩到 10 位,d_v=4 表示 v 的每个系数压缩到 4 位。向量 u 含 k 个多项式,每个多项式 256 个系数,每个系数 10 位,故单个多项式占 256 × 10 / 8 = 320 字节,向量占 320k 字节。多项式 v 的每个系数 4 位,256 个系数占 256 × 4 / 8 = 128 字节。于是:

ct 长度 = 320k + 128
k=2 → 640 + 128 = 768
k=3 → 960 + 128 = 1088
k=4 → 1280 + 128 = 1568

10 位压缩的打包规则是:每 4 个系数(共 40 位)写入 5 个字节。4 位压缩则是每 2 个系数(共 8 位)写入 1 个字节,高 4 位放第一个系数,低 4 位放第二个系数。压缩本身是 Compress_d(x) = round((2^d / q) · x) mod 2^d,解压缩是 Decompress_d(y) = round((q / 2^d) · y)。这两步的舍入方向必须与标准一致,否则解封装得到的共享密钥会不匹配。

ML-KEM 密钥与密文字节布局(按参数集) 12位系数向量 种子 ρ / z / 哈希 10位压缩向量 u 4位压缩多项式 v ML-KEM-512(k=2) ek 800B t̂:2×384 = 768B ρ 32B dk 1632B ŝ:768B ek:800B H(ek) z 32B ct 768B u:2×320 = 640B v 128B ML-KEM-768(k=3) ek 1184B t̂:3×384 = 1152B ρ 32B dk 2400B ŝ:1152B ek:1184B H(ek) z ct 1088B u:3×320 = 960B v 128B ML-KEM-1024(k=4) ek 1568B t̂:4×384 = 1536B ρ 32B dk 3168B ŝ:1536B ek 1568B H,z ct 1568B u:4×320 = 1280B v 128B 注:dk 中 H(ek) 与 z 各 32B;图中长度按比例示意,非严格等比

序列化与反序列化的实现要点

实现中最容易出错的地方有三处。第一是 12 位打包的位序。标准规定按小端序将系数写入字节流,两个系数共享 3 个字节,若实现时把高低位弄反,公钥虽然能生成,但对方解析出的 t̂ 会完全错误,导致封装失败。第二是压缩与解压缩的舍入。压缩时用 round 而非截断,解压缩时同样用 round,且中间计算要用足够宽的整数类型避免溢出。第三是私钥中 H(ek) 的位置。解封装时需要从 dk 中取出 ek 重新计算哈希并与存储值比对,若偏移量算错,校验必然失败。

下面给出一个参数化的长度计算与偏移定义示例,可直接作为实现中的常量表使用:

// 以 ML-KEM-768 为例(k=3)
const int K = 3;
const int EK_LEN = 384 * K + 32;        // 1184
const int DK_LEN = 768 * K + 96;        // 2400
const int CT_LEN = 320 * K + 128;       // 1088

// dk 内部偏移
const int OFF_DK_PKE = 0;               // ŝ 起点
const int OFF_EK     = 384 * K;         // ek 起点 = 1152
const int OFF_H_EK   = OFF_EK + EK_LEN; // H(ek) 起点 = 2336
const int OFF_Z      = OFF_H_EK + 32;   // z 起点 = 2368

// ct 内部偏移
const int OFF_U = 0;                    // u 起点
const int OFF_V = 320 * K;              // v 起点 = 960

验证序列化是否正确,最直接的方法是用标准测试向量做往返测试:将给定的 ek、dk、ct 字节串反序列化为内部多项式表示,再重新序列化,比较字节是否完全一致。若往返一致,说明打包位序和偏移都正确;若不一致,应优先检查 12 位打包和 10 位打包的边界处理,尤其是最后一个系数的填充位是否被正确置零。

选择参数集时,ML-KEM-512 适合资源受限的嵌入式场景,ML-KEM-768 是通用推荐级别(对应 NIST 第 3 类安全强度),ML-KEM-1024 用于高安全需求。三者的编码规则完全相同,仅长度常量随 k 变化,因此实现时应把长度全部写成 k 的函数,避免为每个参数集维护三套硬编码。

总结来说,ML-KEM 的数据结构可以归纳为一条清晰的长度公式链:公钥 384k+32,私钥 768k+96,密文 320k+128,共享密钥恒为 32 字节。掌握这些公式与对应的打包位序,就掌握了 ML-KEM 字节级实现的骨架,后续的封装、解封装与测试向量验证都建立在这一基础之上。