Files
mlkem-sync/.claude/plans/decaps_plan.md
FallenSigh 2b70431923 feat(dec): Decaps D7 - implicit-reject compare + end-to-end KAT
Final FO step (FIPS 203 Alg 17 steps 9-11): compare c' to c and select the
shared secret with implicit rejection. Completes full ML-KEM Decaps.

- ST_DEC_CMP walks all ct bytes (no early-out: constant work, matching the
  constant-time spec intent), reading c' (ct_bram) and c (c_in_bram) in
  parallel, OR-ing every byte difference into cmp_neq; the final byte latches
  dec_reject.
- ss_o = dec_reject ? kbar_r (K-bar) : ss_r (K'). KeyGen/Encaps leave
  dec_reject=0 so ss_r passes through unchanged.
- ST_ENC_C2 terminal branches on op_r: Decaps -> ST_DEC_CMP, Encaps -> DONE.

The dec TB now runs end-to-end twice per case (dk loaded once):
  - valid ct  : ss_o == KAT ss   (c'==c -> K'),            dec_reject==0
  - corrupt ct: ss_o == KAT ss_n (c'!=c -> K-bar=J(z||ct_n)), dec_reject==1
exercising both the accept and implicit-reject paths against the KAT's own
ct_n/ss_n reject vectors.

Verified: dec D7 K=2/3/4 all cases PASS (accept + reject); KeyGen + Encaps
unregressed. Full ML-KEM (KeyGen + Encaps + Decaps w/ implicit rejection) now
works in hardware across all three parameter sets.
2026-06-29 21:56:07 +08:00

90 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ML-KEM Decaps 顶层集成 — 实现计划
> 在 `mlkem_top`(KeyGen + Encaps 均 KAT 通过)基础上扩展 Decaps(FIPS 203 Alg 18 + K-PKE.Decrypt Alg 15)。
> 决策(已与用户确认):**(1) 实现完整隐式拒绝路径(J + 常量时间 c'==c 比较 + K̄/K' mux),并用 corrupted-ct(KAT ct_n/ss_n)验证拒绝路径;(2) 逐级 dbg tap 对拍 ml-kem-r golden(m'/w/u_hat 等)+ 端到端 ss==KAT.ss。**
## 算法(Decaps_internal,全 K)
输入:dk(=KAT.sk,768K+96 B)、c(=KAT.ct,32(du·K+dv) B)。输出:K'=ss(32 B)。
1. 解析 dk:`dk_pke = dk[0:384K]`(s_hat)、`ek_pke = dk[384K:768K+32]``h = dk[768K+32:768K+64]``z = dk[768K+64:768K+96]`
2. `m' = K-PKE.Decrypt(dk_pke, c)`(**唯一全新数据通路**,见下)。
3. `(K', r') = G(m' ‖ h)` = SHA3-512(64 B 单块)。**与 Encaps G 完全相同**(mode=11)。
4. `K̄ = J(z ‖ c)` = SHAKE-256(z‖c, 32 B 输出)。**多块,块数 6/9/12 = 与 H(ek) 相同**。
5. `c' = K-PKE.Encrypt(ek_pke, m', r')`。**这就是 Encaps E1E7**(A 再生 + 采样 y/e1/e2(seed=r') + NTT + u + v + 压缩成 ct')。**已 KAT 通过,零改动复用**。
6. 常量时间比较 `c' == c`(逐字节 XOR 累加)。
7. `K' = (c'==c) ? K' : K̄`(隐式拒绝 mux)。返回 K'=ss。
### K-PKE.Decrypt(Alg 15,全新)
- 解析 c:`c1 = c[0:32·du·K]``c2 = c[32·du·K : +32·dv]`
- `u'[i] = Decompress_du(byteDecode_du(c1[i]))`,i=0..K-1(**新:通用 byteDecode_d 解包器 + comp_decomp mode=1 解压**)。
- `v' = Decompress_dv(byteDecode_dv(c2))`(1 poly)。
- `s_hat[i] = byteDecode12(dk_pke[i·384..])`,i=0..K-1(**复用 Encaps TDEC 机器**,5-cyc/triple)。
- `u_hat[i] = NTT(u'[i])`(mode=0,K polys,**复用 ST_ENC_N 的 ntt_core**)。
- `w = v' INTT(Σⱼ s_hat[j]∘u_hat[j])` mod Q(**复用 Encaps V 机器(MAC+INTT),ADD 改 SUB,加数 v' 替 e2+mu**)。
- `m' = byteEncode₁(Compress₁(w))` = 32 B(**复用 C1/C2 打包器,d=1**)。
## 复用与新增
### 直接复用(零或极小改动)
- **整个 Encaps E1E7**(c' = Encrypt):A 再生、CBD(seed=r')、NTT、U、V、C1、C2 → ct_bram。完全复用,只是 seed 来自 r'(已是 r_r 路径)、ek 来自 ek_pke(已在 ek_bram)、m 来自 m'(新:m' 寄存器替 m_r)。
- `u_sha3` G(mode=11,m'‖h):与 Encaps G 同,只是 enc_g_data 高半改 h(来自 dk 而非 H(ek) 重算)。
- `u_keccak` 共享核 + J 多块:复用 H 的 mb 路径,仅末块 pad 常量 0x06→0x1F(SHAKE)。
- `ntt_core`:u_hat fwd NTT(mode=0,复用 ST_ENC_N);decrypt 的 INTT(mode=1,复用 V 的 u_intt 路径)。
- `poly_mul` / 3 银行 / comp_decomp:加 Decaps phase mux。
- Encaps TDEC(byteDecode12 → bank_a):decrypt 的 s_hat 解码复用(改落 dk_pke 源 + 目标 bank)。
### 新增 RTL
- **`byteDecode_d` 通用解包器**(d∈{4,5,10,11}):c 字节流 → d-bit 系数,LSB-first(byteEncode_d 的逆)。新写,流式读 ct/c_bram,写银行。
- **comp_decomp mode=1 解压**:实例已有(E5/E7 用 mode=0);Decaps decode-decompress 用 mode=1,d=du/dv。加 phase。
- **dk 载入路径**:`dk_in_*`(或复用 ek_in_* 加宽地址),Decaps 前流入 dk_bram。顶层解析:dk_pke→bank/解码、ek_pke→ek_bram、h/z→寄存器。
- **c 载入路径**:`c_in_*` 流入 ct_bram 的「输入 c」区(注意 c' 也写 ct_bram → 需独立 c_in_bram 或分区,见存储编排)。
- **J 多块组装**:z(reg)‖c(c_in_bram)→ 136B 块,末块 0x1F pad。新地址逻辑(类 H 的 h_g_addr)。
- **w = v' INTT(...)**:V 机器 ADD 子相改 SUB(`(v' psum) mod Q`,负则 +Q),加数源 v'(银行)替 e2(bank_a)+mu。
- **m' Compress₁+byteEncode₁**:打包器加 d=1 路径(每系数 1 bit,256 bit = 32 B)。m' 落 32-bit 寄存器(供 G 与 c'-Encrypt)。
- **常量时间比较 c'==c**:逐字节读 ct_bram(c')与 c_in_bram(c),XOR 累加进 1-bit `ct_ne_r`(全程扫完,不早退)。
- **隐式拒绝 mux**:`ss_r = ct_ne_r ? k_bar_r : kprime_r`
- **op_i 加宽 2-bit**:00=KeyGen,01=Encaps,10=Decaps。新增 ST_DEC_* 状态。
## 存储编排(关键)
- **c 输入 vs c' 输出冲突**:Decaps 既要保留输入 c(给 J 和最终比较),又要算 c'(写 ct_bram)。**解法:c 输入存独立 `c_in_bram`(sd_bram W=8 D=2048);c' 仍写 ct_bram。** 比较阶段两个 bram 各一读口,无冲突。J 从 c_in_bram 读。
- **Decrypt 阶段银行**:s_hat(K)、u'(K)、u_hat(=NTT(u') 就地 K)、v'(1)、psum/w(1)。
- u' → bank_se rel 0..K-1;NTT 就地得 u_hat。
- s_hat → bank_a slot j·K(复用 TDEC 落点 + V-MAC 寻址)。
- v' → bank_t rel 0(或 bank_a 空 slot)。
- psum(Σ s∘u_hat)→ bank_t[UPSUM];INTT 就地;SUB 读 v'(bank_t rel 0)+ psum(bank_t UPSUM)→ 单口冲突 → v' 改存 bank_a 某 slot(类 e2 搬迁)或 bank_se 空区。**bring-up 定稿,dbg 验证。**
- m' 算完 → 32-bit 寄存器。Decrypt 阶段结束,银行清空。
- **Encrypt(c')阶段**:Decrypt 已出 m'(reg),银行重新被 Encaps E1E7 占用,无并发。
## 顶层接口新增
- `op_i [1:0]`:00/01/10。start_i 锁存 op_r[1:0]。
- `dk_in_*`(we/addr/byte):dk 流入。`c_in_*`(we/addr/byte):c 流入。
- `ss_o`(复用):Decaps 输出 K'。
- dbg taps:m'(`dbg_mprime_o[255:0]`)、w/u_hat 经现有 dbg_coeff_o、k_bar(`dbg_kbar_o`)。
## 实现阶段(逐阶段 dbg/KAT 对拍)
- **D0 — 脚手架 + dk/c 载入 + 解析** ✅:op_i 加宽 2-bit(00 KG/01 Enc/10 Dec),ST_DEC_LOAD(D0 暂直接→DONE)。dk 流入按 region 路由:dk_pke→dkp_bram、ek_pke→ek_bram、h→hek_r、z→z_r;ct→c_in_bram(独立于 ct_bram)。dbg 验证 h/z/ek_pke/dk_pke。**踩坑1:载入路由用 k_r 但 k_r 在 start_i 才锁存 → 预载期 region 边界全 0,路由全错。改用 LIVE k_i 边界(dkp_bytes_ld 等)。踩坑2:旧 KG/Enc TB 未接新端口(dk_in_*/c_in_*/dbg_*)→ X 漂入 write mux,KeyGen/Encaps 超时回归。补 tie-off 0。** runner = `./run_tb.sh dec [K] [CASE]`。K=2/3/4 D0 全过,KG/Enc 回归通过。
- **D1 — byteDecode_d + Decompress → u'/v'** ✅:复用 comp_decomp(改 mode 可选:Encaps C1/C2 compress mode=0,Decaps DECOMP mode=1)。ST_DEC_DECOMP 内联 byteDecode 走子机:逐字节读 c_in_bram,LSB-first 累进 bit buffer,凑够 d 位抽符号→comp_decomp 解压→写 bank。c1(K 多项式,d=du)→u'[i] bank_se rel i;c2(1 多项式,d=dv)→v' bank_t rel DEC_VSLOT=2(避开 UPSUM=1)。dbg_slot_i 加宽 4→6 bit(K=4 v' 在 slot 26)。dump_decaps.rs(ml-kem-r 工作树)产 u'/v'/s_hat/u_hat/w/m' golden 到 vectors/decgold/。**踩坑:dbg coeff 读回延迟 = bank(1)+dbg_coeff_r(1),TB rdcoeff 等 2 拍少一拍 → 数据整体错位一格;改 3 拍修正。** K=2/3/4 全过,KG/Enc 回归通过。
- **D2 — s_hat 解码 + u_hat = NTT(u')** ✅:复用 Encaps TDEC 机(ST_DEC_SDEC),字节源从 ek 切到 dkp_bram(td_byte mux + dkp_rd_addr 在 SDEC 走 td_ekaddr),s_hat 写 bank_a slot j*K(与 t_hat 同布局,D3 MAC 可直接读)。复用前向 NTT 机(ST_DEC_NTT,n_slot_max=k_r)对 bank_se rel 0..K-1 的 u' 原地变换成 u_hat。**踩坑:NTT 原地覆盖 u' → verify_d1 复查 u' 必失败;改为 verify_d1 只查 v'(bank_t 未动),u' 正确性由 u_hat==NTT(u') golden 传递性证明。** K=2/3/4 全过,KG/Enc 回归通过。
- **D3 — w = v' INTT(Σ s∘u_hat)** ✅:复用 Encaps V 机(ST_DEC_W,u_row=0 单多项式)。MAC s_hat[j](bank_a slot j*K)∘ u_hat[j](bank_se rel j)→psum bank_t[UPSUM],与 V MAC 完全同址,免改。INTT 原地。SUB:w = v' psum,(v'psum) 负则 +Q。**关键:v'/psum 读口冲突 → D1 把 v' 落到 bank_a slot DEC_VASLOT=1(s_hat 在 j*K,slot 1 恒空 K≥2),SUB 时 psum(bank_t)+v'(bank_a)并行读,正如 V-ADD 并读 psum+e2。** K=2/3/4 w 全过。
- **D4 — m' = byteEncode₁(Compress₁(w))** ✅:ST_DEC_MENC,逐系数读 bank_t[UPSUM] 的 w,Compress₁(w)=1 iff 832<w≤2496(Q=3329),LSB-first 打包进 mprime_r[255:0],经 dbg_mprime_o 暴露。TB verify_d4 对 32 字节 golden(== KAT 解密的 m'==原始 m)。K=2/3/4 全过,KG/Enc 回归通过。K-PKE.Decrypt 硬件全链路打通。
- **D5 — G(m'‖h) → (K',r') + J(z‖c) → K̄** ✅:G 复用单块 SHA3-512 路径(ST_DEC_G,mode=11,dec_g_data={hek_r,mprime_r}),输出 K'→ss_r(候选 ss,ss_o),r'→r_r(D6 PRF 种子,dbg_r_o)。J 复用多块吸收口(ST_DEC_J,mb_en=1),仿照 H(ek) 机器组装 136B/块,字节源 g<32 取 z_r、32≤g<msglen 取 c_in_bram、否则 SHAKE256 pad(0x1F 后缀,末字节|0x80,与 H 的 0x06 唯一区别)。mb_* 端口按 ST_DEC_J 在 H/J 间多路选择。K̄→kbar_r(dbg_kbar_o)。**踩坑:c_in_bram 读经 cin_rd_addr_r 寄存器 + BRAM 寄存 = 2 拍延迟,而组装/写回流水只容 1 拍 → z→c 边界首个 c 字节(byte32)读到 X,毒化整个 keccak。改为 ST_DEC_J 时 cin_rd_addr 组合直接取 dj_c_idx(去掉寄存器级),数据次拍到达正好对齐写回。** K=2/3/4(密文 768/1088/1568B → 6/9/12 块)全过;K'==KAT ss(有效密文)验证 FO 正确;KG/Enc 回归通过。
- **D6 — c' = Encrypt(ek_pke,m',r')** ✅:整条 Encaps E1E7 流水直接复用——ST_DEC_J 完成后跳 ST_ENC_LOAD(rho 从 ek_bram 的 ek_pke 区载入,与 Encaps 同 offset 384k),经 A→C→N→U→C1→TDEC→E2MV→V→C2 跑完写 ct_bram。复用前提全部就位:r'(CBD 种子)在 r_r、ek_pke 在 ek_bram(D0 load 时 dk_ld_ekpke 写入)。**关键改动:D4 把 m' 直接打包进 m_r(而非独立 mprime_r),因为 Encaps V 的 mu=m_r[idx]、dbg_mprime_o 也改指 m_r;省一个寄存器且让重加密天然读到 m'。** ST_ENC_LOAD 的 arming 扩展为从 ST_ENC_G 或 ST_DEC_J 进入皆触发。**注意:重加密覆盖 bank_a/se/t,故 D1D3 的 bank 检查在 run 末已失效;TB 改为只校验存活的寄存器/BRAM 工件(D0 解析、m'、K'/r'/K̄、c'),bank 阶段正确性由早期分阶段构建 + c' 传递性保证。** K=2/3/4 c'==golden(==有效密文 c)全过,KG/Enc 回归通过。
- **D7 — 比较 + 拒绝 mux + 端到端 KAT** ✅:ST_DEC_CMP 逐字节走遍整个密文(无早退,恒定工作量),ct_bram(c')与 c_in_bram(c)同址并行读,差异 OR 进 cmp_neq;末字节后锁存 dec_reject。ss_o = dec_reject ? kbar_r(K̄) : ss_r(K'),KeyGen/Encaps 因 dec_reject=0 原样透传。ST_ENC_C2 终态按 op_r 分流(Decaps→CMP,Encaps→DONE)。TB 单次载 dk 后跑两遍:
- 有效 ct(KAT.ct):ss==KAT.ss(c'==c → K'),reject=0 ✓。
- 损坏 ct(KAT ct_n / ss_n):ss==KAT.ss_n(c'≠c → K̄=J(z‖ct_n)),reject=1 ✓。
- K=2/3/4 各 3 case 全过;KG/Enc 回归通过。**至此 ML-KEM(KeyGen+Encaps+Decaps,含隐式拒绝)硬件全功能打通。**
## 验证
-`dump_decaps.rs`(ml-kem-r examples,工作树):出 D1D6 中间量(u'/v'/s_hat/u_hat/w/m'/K'/K̄)256-coeff / 32B golden。
- 新 TB `tb_mlkem_dec_katK_xsim.v`:从 KAT 取 sk(→dk)、ct、ss、ct_n、ss_n,载入 dk/c,跑 Decaps,比 ss。
- runner:`./run_tb.sh dec [K] [CASE]`(并入 run_tb,复用 top tcl + dec TB)。
- XSIM 环境同前:`source settings64.sh; export LD_PRELOAD=libtinfo.so.5; rm -rf xsim.dir .Xil`
## 风险 / 注意
- **c 输入 / c' 输出共存**:必须分两个 bram(c_in_bram + ct_bram),否则 c' 覆盖 c 后无法比较 / 算 J。最易错,D0 定。
- **w 的 SUB**:`(v' psum) mod Q`,结果可能负 → +Q 修正(类 mod_sub)。v'/psum 读口冲突 → v' 搬到不同 bank。D3 dbg 验证。
- **J SHAKE pad**:末块 0x1F(非 0x06);单字节差异,复用 H mb 机器加 phase 选择 pad 常量。
- **op_i 加宽**:1-bit→2-bit,改 IDLE 分发、reset、所有 op_r 判断。回归 KeyGen/Encaps 不破。
- **m' 双用**:既喂 G(m'‖h)又喂 c'-Encrypt(替 m_r)。确保 Encrypt 路径读 m'_r 而非 m_r(加 mux 或 Decaps 时把 m'_r 写入 m_r)。
- **隐式拒绝常量时间**:比较全程扫完不早退(XOR 累加),硬件天然如此;但 mux 不可短路。
- **ek_pke 来自 dk**:Decaps 的 Encrypt 用 dk 内嵌的 ek_pke(解析到 ek_bram),不是外部 ek。