# ML-KEM 硬件实现(FIPS 203) 基于 SystemVerilog 的 **ML-KEM**(Module-Lattice-based Key Encapsulation Mechanism,NIST 后量子密码标准,源自 Kyber)同步流水线硬件实现。面向 FPGA,使用 Vivado XSIM 与 Verilator 进行仿真验证。 本文档重点介绍顶层密钥生成模块 **`mlkem_top`** 的工作流程及其测试流程。各底层算子(SHA-3、NTT、CBD 采样等)已独立验证,本文不再展开。 ## 概述 ML-KEM 是 NIST 在 FIPS 203 中标准化的后量子密钥封装机制,其安全性基于多项式环 Z_q[x]/(x²⁵⁶+1) 上的 Module-LWE 难题。 `mlkem_top` 实现 **FIPS 203 算法 16(KeyGen_internal)** 的完整密钥生成数据通路:给定种子 `d` 与 `z`,输出封装密钥 `ek`(公钥)和解封装密钥 `dk`(私钥)。模块运行于 **100 MHz**(10 ns 周期),低电平复位。 ### 运行时参数选择 **ML-KEM 的安全等级 K 在运行时通过输入端口 `k_i` 选择**,而非编译期参数。存储按最坏情况(ML-KEM-1024,KMAX=4)静态分配,`k_i` 在 `start_i` 时被采样到内部寄存器 `k_r`,据此选取激活的子区间。 | k_i | 方案 | 模块秩 K | η₁ | ek 字节数 | dk 字节数 | KeyGen 周期数 | |:---:|:---|:---:|:---:|:---:|:---:|:---:| | 2 | ML-KEM-512 | 2 | 3 | 800 | 1632 | 21 403 | | 3 | ML-KEM-768 | 3 | 2 | 1184 | 2400 | 36 207 | | 4 | ML-KEM-1024 | 4 | 2 | 1568 | 3168 | 54 005 | > 注:`k_i` 仅在 `start_i` 时采样,且假定取值 ∈ {2,3,4};越界值(0/1/5–7)当前不做保护,会产生错误的尺寸计算。 固定参数:**q = 3329**(素数模)、**n = 256**(多项式次数)。du/dv 仅用于封装/解封装,KeyGen 不涉及。 ## `mlkem_top` 接口 ``` module mlkem_top #(parameter KMAX = 4) ( input clk, rst_n, input [2:0] k_i, // 运行时方案选择:2/3/4 input [255:0] d_i, // KeyGen 种子 d(byte0 在 d_i[7:0]) input [255:0] z_i, // 隐式拒绝种子 z input start_i, // 启动脉冲 output busy_o, // 运行中拉高 output done_o, // ek/dk 就绪时拉高 // 调试回读端口(供 TB 逐字节核对,无需宽总线) input [3:0] dbg_slot_i, input [7:0] dbg_idx_i, output [11:0] dbg_coeff_o, input dbg_byte_sel_i, input [10:0] dbg_byte_idx_i, output [7:0] dbg_byte_o, input [11:0] dbg_dk_idx_i, output [7:0] dbg_dk_o, output [255:0] dbg_rho_o, dbg_sigma_o ); ``` `busy_o`/`done_o` 提供握手;`dbg_*` 端口为只读调试抽头,让 TB 可以逐系数 / 逐字节读出中间结果与最终的 ek/dk,而无需引出整条数据总线。 ## 工作流程 `mlkem_top` 复用了已独立验证的叶子模块(每个模块自带 keccak_core,无共享仲裁器):`sha3_top`、`sample_ntt_sync`、`sample_cbd_sync`、`ntt_core`、`poly_mul_sync`。顶层是一个 8 状态 FSM,串行驱动这些算子,并把所有中间多项式存放在统一的系数寄存器阵列 `polymem` 中。 ### KeyGen 算法(FIPS 203 算法 16) ``` (ρ, σ) = G(d ‖ K) // SHA3-512 Â[i][j] = SampleNTT(ρ ‖ j ‖ i) i,j ∈ 0..K-1 // SHAKE-128 拒绝采样 s[i] = CBD_η1(PRF(σ, i)) i ∈ 0..K-1 // SHAKE-256 e[i] = CBD_η1(PRF(σ, K+i)) i ∈ 0..K-1 ŝ[i] = NTT(s[i]), ê[i] = NTT(e[i]) t̂[i] = ê[i] + Σⱼ Â[i][j] ∘ ŝ[j] // NTT 域逐点乘 + 累加 ek = byteEncode₁₂(t̂[0..K-1]) ‖ ρ dk = byteEncode₁₂(ŝ[0..K-1]) ‖ ek ‖ H(ek) ‖ z ``` ### FSM 状态机 ``` start_i ST_IDLE ─────────────────► ST_G ▲ │ G(d‖K),捕获 ρ/σ │ ▼ │ ST_A 生成 Â[i][j](K² 个多项式,SampleNTT) │ │ │ ▼ │ ST_C 采样 s[i], e[i](2K 个多项式,CBD) │ │ │ ▼ │ ST_N 原地前向 NTT:ŝ[i], ê[i](2K 次) │ │ │ ▼ │ ST_M t̂[i] = ê[i] + Σⱼ Â[i][j]∘ŝ[j] │ │ │ ▼ │ ST_E byteEncode₁₂ → ek_mem / dkp_mem,ek 尾接 ρ │ │ │ ▼ │ ST_H H(ek):多块 SHA3-256 │ done_o │ └────────── ST_DONE ◄───────┘ ``` | 状态 | 名称 | 动作 | 使用的算子 | |:---:|:---|:---|:---| | ST_G | 哈希 G | `(ρ,σ)=G(d‖K)`,`data_i={K_byte, d}` | `sha3_top`(SHA3-512) | | ST_A | 矩阵采样 | 逐个生成 Â[i][j],行主序写入 slot `i*K+j`,每个 256 系数 | `sample_ntt_sync` | | ST_C | CBD 采样 | s[0..K-1](nonce 0..K-1)、e[0..K-1](nonce K..2K-1);有符号→模 q | `sample_cbd_sync` | | ST_N | 前向 NTT | 对 ŝ、ê 共 2K 个多项式逐个原地变换 | `ntt_core`(mode=0) | | ST_M | 矩阵乘累加 | 对每个 (i,j):流入 256 对 (Â,ŝ) 做逐点乘,累加进 t̂[i](j=0 时以 ê[i] 初始化) | `poly_mul_sync` | | ST_E | 字节编码 | byteEncode₁₂:t̂→ek_mem,ŝ→dkp_mem;末尾拷入 ρ 的 32 字节 | — | | ST_H | 哈希 H | 对 ek 做多块 SHA3-256,得到 H(ek);调用方预填充末块(0x06…0x80) | `sha3_top`(多块模式) | 各状态之间以 valid/ready 握手串接:FSM 拉高对应算子的 `valid_i`,在 `ready_o` 时认为请求被接收,再逐拍收集 `valid_o` 输出,直到 `last_o`/`done_o`。 ### 存储布局 所有多项式存于 `polymem`(`NUM_SLOTS×256` 个 12-bit 系数,`NUM_SLOTS = KMAX²+3·KMAX = 28`)。每个 slot 256 系数,槽基址在运行时由 `k_r` 推导: ``` slot 0 .. K²-1 : Â[i][j] (下标 i*K + j) slot_s_rt = K² : ŝ[i] (ST_N 原地覆盖 s[i]) slot_e_rt = K² + K : ê[i] slot_t_rt = K² + 2K : t̂[i] ``` 字节输出存于 `ek_mem`(KMAX 最大 1568B)与 `dkp_mem`(最大 1536B)。byteEncode₁₂ 规则:每 2 个系数打包成 3 字节,LSB 优先 12-bit: ``` b0 = c0[7:0] b1 = {c1[3:0], c0[11:8]} b2 = c1[11:4] ``` 完整私钥 dk 的字节布局(与 NIST KAT 的 sk 对齐): ``` dk = dk_pke(384·K) ‖ ek(384·K+32) ‖ H(ek)(32) ‖ z(32) ``` H(ek) 阶段采用预填充多块吸收:调用方逐块组装 136 字节速率块,在 `ek_bytes` 位置填 `0x06`、末块最后字节或上 `0x80`;分块数 `h_nblk_rt` 为 6/9/12(对应 K=2/3/4)。 ## 测试流程 `mlkem_top` 的验证策略是:**对全部三种安全等级,把硬件产出的 `ek`/`dk` 与 NIST KAT 标准答案逐字节比对**。验证素材、参数化 TB、运行脚本三者配合完成。 ### 1. 黄金向量(NIST KAT) 测试向量来自 NIST FIPS 203 的 KAT 响应文件,经 `sync_rtl/top/TB/gen_vectors.py` 解析后生成每个用例的独立 hex 文件,存于 `sync_rtl/top/TB/vectors/`: | 文件 | 内容 | 字节长度(按 K) | |:---|:---|:---| | `kat_k_c_d.hex` | KeyGen 种子 d | 32 | | `kat_k_c_z.hex` | 隐式拒绝种子 z | 32 | | `kat_k_c_ek.hex` | 期望公钥 pk(== ek) | 384·K+32 | | `kat_k_c_dk.hex` | 期望私钥 sk(== dk) | 768·K+96 | 其中 `K ∈ {2,3,4}`、`n` 为用例号。当前覆盖:**K=2 共 5 个用例(c0–c4),K=3 / K=4 各 3 个用例(c0–c2)**,合计 11 个用例。 向量采用 “byte0 在低位” 约定:256-bit 值满足 `bit[8m+:8] = byte m`。 ### 2. 参数化测试平台 `sync_rtl/top/TB/tb_mlkem_kg_katK_xsim.v` 是参数化自检 TB: - 通过 `parameter KP`(由 `xelab -generic_top KP=2|3|4` 设定)选择安全等级; - 通过 `+CASE=n` plusarg 选择用例号,据此加载对应的 `kat_k_c_*.hex`; - **将 `KP` 驱动到 DUT 的运行时输入 `k_i`**(不再用参数覆盖),KMAX 取默认 4; - 复位 → 加载 d/z 与 k_i → 拉 `start_i` 一拍 → 轮询 `done_o`(超时上限 200 万周期); - 完成后通过 `dbg_byte_o`(读 ek,0..EKB-1)与 `dbg_dk_o`(读完整 dk,0..DKB-1)逐字节回读,与黄金向量比对; - 全部相符打印 `K= CASE PASS`,否则打印前 8 个不匹配字节并报 `FAIL`。 ### 3. 运行测试 测试经由统一脚本 `run_tb.sh` 分发(自动 source Vivado 环境并设置 `LD_PRELOAD`): ```bash ./run_tb.sh top ``` 该命令执行 `sync_rtl/top/TB/xsim_run.tcl`,其流程为: 1. **编译**(`xvlog`):全部叶子算子 RTL + `mlkem_top.v` + 参数化 TB; 2. **细化**(`xelab`):为每种 K 生成一个快照 —— `KP=2→mlkem_kg_k2`、`KP=3→mlkem_kg_k3`、`KP=4→mlkem_kg_k4`; 3. **仿真**(`xsim -R -testplusarg CASE=n`):依次跑完每种 K 的全部用例。 整体测试矩阵: ``` K=2 (ML-KEM-512) : CASE 0,1,2,3,4 → ek 800B, dk 1632B K=3 (ML-KEM-768) : CASE 0,1,2 → ek 1184B, dk 2400B K=4 (ML-KEM-1024): CASE 0,1,2 → ek 1568B, dk 3168B ``` ### 4. 预期结果 11 个用例全部 PASS,每个用例确认 `ek == pk` 且 `dk == sk` 逐字节相等: ``` === ML-KEM K=2 KAT case 0: KeyGen done in 21403 cyc === K=2 CASE 0 PASS: ek (800B)==pk, dk (1632B)==sk ... K=4 CASE 2 PASS: ek (1568B)==pk, dk (3168B)==sk ``` ### 验证注意事项 - **干净重跑**:每轮仿真前清理 `xsim.dir`、`.Xil`,避免旧快照污染(`rm -rf xsim.dir .Xil`)。 - **`$readmemh` 缺文件只是 WARNING**:文件名拼错时数据读为 X,不会报错,极易造成假 PASS。务必确认日志中无 `cannot be opened` 警告。 - **以日志文件为准**:将每个 `xsim` 调用重定向到独立日志后再 grep `PASS|FAIL|cannot be opened`,不要只看终端滚屏的模糊输出。 ## 手动 XSIM 命令 ```bash source /opt/Xilinx/Vivado/2019.2/settings64.sh export LD_PRELOAD=/usr/lib64/libtinfo.so.5 # Vivado 2019.2 的 ncurses 兼容修复 rm -rf xsim.dir .Xil # 1) 编译(叶子算子 + 顶层 + TB) xvlog -sv --relax -i . \ sync_rtl/sha3/keccak_round.v sync_rtl/sha3/keccak_core.v sync_rtl/sha3/sha3_top.v \ sync_rtl/sample_ntt/sample_ntt_sync.v sync_rtl/sample_cbd/sample_cbd_sync.v \ sync_rtl/ntt/barrett_mul.v sync_rtl/ntt/zeta_rom.v sync_rtl/ntt/butterfly_unit.v sync_rtl/ntt/ntt_core.v \ sync_rtl/poly_mul/basecase_mul.v sync_rtl/poly_mul/poly_mul_zeta_rom.v sync_rtl/poly_mul/poly_mul_sync.v \ sync_rtl/top/mlkem_top.v \ sync_rtl/top/TB/tb_mlkem_kg_katK_xsim.v # 2) 细化某一种 K xelab tb_mlkem_kg_katK_xsim -generic_top KP=4 -s mlkem_kg_k4 --timescale 1ns/1ps # 3) 跑某个用例 xsim mlkem_kg_k4 -R -testplusarg CASE=0 ``` ## 先决条件 - **Vivado 2019.2+**(XSIM 仿真):`/opt/Xilinx/Vivado/2019.2/` - **Verilator 5.046**(底层算子 C++ 验证) - **Python 3.10+**(向量生成,仅用标准库) ## Vivado 2019.2 兼容性说明 在 Fedora 44 上经实测的必要 workaround: ```bash export LD_PRELOAD=/usr/lib64/libtinfo.so.5 # 必需:ncurses 兼容库 xvlog -sv --relax -i . .v # 用 -i(非 -include_dirs)指定包含目录;--relax 放宽严格 SV 检查 xelab -s --timescale 1ns/1ps # xelab 需显式 --timescale ``` ## 参考 - [FIPS 203: ML-KEM](https://csrc.nist.gov/pubs/fips/203/final) —— NIST 标准(算法 16 KeyGen_internal) - [FIPS 202: SHA-3 / SHAKE](https://csrc.nist.gov/pubs/fips/202/final) —— Keccak 哈希族 - [CRYSTALS-Kyber](https://pq-crystals.org/kyber/) —— 原始提案