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

12 KiB
Raw Blame History

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 jK)∘ 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 在 jK,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。