feat(dec): Decaps D5 - (K',r')=G(m'||h) + K-bar=J(z||c)

FO transform hash derivations (FIPS 203 Alg 17 steps 6-7), reusing the
shared SHA3 core:

- ST_DEC_G: (K',r') = G(m'||h) via the single-block SHA3-512 path (mode 11,
  dec_g_data = {hek_r, mprime_r}; h was captured into hek_r at D0 load).
  K' -> ss_r (candidate shared secret, ss_o), r' -> r_r (PRF seed for D6).
- ST_DEC_J: K-bar = J(z||c) via the multi-block absorb port (mb_en=1),
  modeled on the H(ek) machine: assemble 136-byte blocks, byte source is
  z_r (g<32), c_in_bram (32<=g<msglen), or SHAKE256 pad (0x1F suffix, last
  byte |=0x80 -- the only difference from H's 0x06). mb_* inputs muxed
  between H and J by state. K-bar -> kbar_r (dbg_kbar_o).
- FSM: MENC -> G -> J -> DONE.

Bring-up note: c_in_bram read through cin_rd_addr_r (a register) plus the
registered BRAM is 2-cycle latency, but the assemble/writeback pipeline only
budgets 1 -- so the first c byte at the z->c boundary read X and poisoned the
whole sponge. Fixed by driving cin_rd_addr combinationally from dj_c_idx
during ST_DEC_J (dropping the register stage) so data lands the next cycle.

Verified: dec D5 K=2/3/4 all cases PASS (ct 768/1088/1568B -> 6/9/12 J blocks);
K' matches the KAT shared secret for valid ciphertexts; KeyGen + Encaps
unregressed.
This commit is contained in:
2026-06-29 20:37:03 +08:00
parent 7f519fe826
commit 189411e8d1
39 changed files with 11352 additions and 21 deletions

View File

@@ -195,7 +195,11 @@ module mlkem_top #(
.rd_addr(cin_rd_addr), .rd_data(cin_rd_data),
.wr_en(c_in_we), .wr_addr(c_in_addr), .wr_data(c_in_byte)
);
assign cin_rd_addr = cin_rd_addr_r;
// c_in read addr: D5 J presents dj_c_idx combinationally (no extra register
// stage) so the registered BRAM read yields the byte 1 cycle later, matching
// the assemble/writeback pipeline. Otherwise the registered cin_rd_addr_r
// (D1 walker / idle) drives it.
assign cin_rd_addr = (st == ST_DEC_J) ? dj_c_idx[10:0] : cin_rd_addr_r;
// ================================================================
// Polynomial storage, sized for KMAX (worst case). Runtime k uses a
@@ -482,6 +486,8 @@ module mlkem_top #(
localparam ST_DEC_NTT = 5'd23; // D2: u_hat[i] = NTT(u'[i]) in place (bank_se rel i)
localparam ST_DEC_W = 5'd24; // D3: w = v' - INTT(sum_j s_hat[j] o u_hat[j])
localparam ST_DEC_MENC = 5'd25; // D4: m' = byteEncode_1(Compress_1(w))
localparam ST_DEC_G = 5'd26; // D5: (K',r') = G(m' || h), SHA3-512 single block
localparam ST_DEC_J = 5'd27; // D5: K-bar = J(z || c), multi-block SHAKE256
localparam ST_DONE = 5'd31;
reg [4:0] st, st_next;
@@ -524,7 +530,9 @@ module mlkem_top #(
reg sha3_ack; // consumer ready for hash
wire [511:0] kg_g_data = {248'b0, 5'b0, k_r, d_i}; // KeyGen G: [263:256]=k, [255:0]=d
wire [511:0] enc_g_data = {hek_r, m_r}; // Encaps G: m || H(ek), 64 bytes
wire [511:0] g_data = (st == ST_ENC_G) ? enc_g_data : kg_g_data;
wire [511:0] dec_g_data = {hek_r, mprime_r}; // Decaps D5 G: m' || h, 64 bytes
wire [511:0] g_data = (st == ST_ENC_G) ? enc_g_data :
(st == ST_DEC_G) ? dec_g_data : kg_g_data;
// ================================================================
// Shared keccak_core + phase mux (3 consumers -> 1 core)
@@ -551,7 +559,8 @@ module mlkem_top #(
// phase selects (mutually exclusive). Encaps adds ST_ENC_H/G (sha3),
// ST_ENC_A (snt), ST_ENC_C (cbd).
wire sel_sha3 = (st == ST_G) || (st == ST_H) ||
(st == ST_ENC_H) || (st == ST_ENC_G);
(st == ST_ENC_H) || (st == ST_ENC_G) ||
(st == ST_DEC_G) || (st == ST_DEC_J);
wire sel_snt = (st == ST_A) || (st == ST_ENC_A);
wire sel_cbd = (st == ST_C) || (st == ST_ENC_C);
@@ -578,15 +587,17 @@ module mlkem_top #(
.ready_i(1'b1) // consumers always accept (kc_ready_i=1)
);
// ---- single shared sha3_top serving BOTH G and H ----
// G (ST_G) uses single-block mode (mb_en=0); H(ek) (ST_H) uses the
// multi-block absorb path (mb_en=1). These phases are disjoint in the
// top FSM, so one sha3_top (one keccak_core) is sufficient. mb_en and
// ready_i are muxed by phase; data_i/mode only matter while mb_en=0.
wire sha3_mb_en = (st == ST_H) || (st == ST_ENC_H);
// sha3 single-block mode: 2'b00 = G(33B) for KeyGen, 2'b11 = G(64B) for
// Encaps (m||H(ek)). mode only matters when mb_en=0.
wire [1:0] sha3_mode = (st == ST_ENC_G) ? 2'b11 : 2'b00;
// G/H/J share one sha3_top. Single-block (mb_en=0): KeyGen G (ST_G, mode 00),
// Encaps G (ST_ENC_G, mode 11), Decaps D5 G (ST_DEC_G, mode 11). Multi-block
// absorb (mb_en=1): H(ek) (ST_H/ST_ENC_H, SHA3-256 pad) and Decaps D5 J
// (ST_DEC_J, SHAKE256 pad over z||c). mb_* inputs are muxed by phase.
wire sha3_mb_en = (st == ST_H) || (st == ST_ENC_H) || (st == ST_DEC_J);
wire [1:0] sha3_mode = (st == ST_ENC_G || st == ST_DEC_G) ? 2'b11 : 2'b00;
// multi-block feed mux: J (ST_DEC_J) drives dj_*, else H drives h_*.
wire [1087:0] mb_block_mux = (st == ST_DEC_J) ? dj_block_r : h_block_r;
wire mb_valid_mux = (st == ST_DEC_J) ? dj_mbvalid : h_mbvalid;
wire mb_last_mux = (st == ST_DEC_J) ? dj_mblast : h_mblast;
wire mb_ack_mux = (st == ST_DEC_J) ? dj_ack : h_ack;
sha3_top_shared u_sha3 (
.clk(clk), .rst_n(rst_n),
.mode(sha3_mode), // G = SHA3-512 (only used when mb_en=0)
@@ -595,11 +606,11 @@ module mlkem_top #(
.ready_o(sha3_ready),
.hash_o(sha3_hash),
.valid_o(sha3_vo),
.ready_i(sha3_mb_en ? h_ack : sha3_ack),
.ready_i(sha3_mb_en ? mb_ack_mux : sha3_ack),
.mb_en(sha3_mb_en),
.mb_block_i(h_block_r),
.mb_valid_i(h_mbvalid),
.mb_last_i(h_mblast),
.mb_block_i(mb_block_mux),
.mb_valid_i(mb_valid_mux),
.mb_last_i(mb_last_mux),
.mb_ready_o(h_mbready),
// shared keccak_core interface (gated by phase in the mux below)
.kc_state_o(kc_state_o),
@@ -631,6 +642,43 @@ module mlkem_top #(
reg [7:0] h_wb_pad; // pad constant to use if g is out of ek range
reg h_wb_inek; // 1 if g is within ek range (use BRAM data)
// ---- Decaps D5 J(z||c) multi-block SHAKE256 state ----
// Message = z (32 B, from z_r) || c (ct_bytes_rt B, from c_in_bram). Rate=136.
// SHAKE256 pad: byte at msg_len = 0x1F, last byte of last block |= 0x80.
// Reuses the shared u_sha3 mb_* port (muxed by ST_DEC_J above).
reg [1087:0] dj_block_r; // current pre-padded rate block
reg dj_mbvalid;
reg dj_mblast;
reg dj_ack;
reg [3:0] dj_blk; // block index 0..DJ_NBLK-1 (z||c up to ~1600B -> <=12 blks)
reg [7:0] dj_byte; // 0..136 byte being assembled
reg [1:0] dj_phase; // 0=assemble 1=feed 2=wait-perm 3=done
reg dj_done;
// writeback pipeline (c_in_bram registered read, 1-cyc latency)
reg dj_wb_vld;
reg [7:0] dj_wb_idx; // position within 136-byte block
reg dj_wb_inc; // 1 if global byte g is within c range (use BRAM)
reg dj_wb_inz; // 1 if g < 32 (use z_r byte)
reg [7:0] dj_wb_pad; // pad constant if g out of msg range
reg [4:0] dj_wb_zidx; // z byte index (g) when inz
// J message length = 32 (z) + ct_bytes_rt; total padded blocks.
wire [11:0] dj_msglen = 12'd32 + {1'b0, ct_bytes_rt};
wire [11:0] dj_last_byte = (dj_msglen / 12'd136) * 12'd136 + 12'd135; // last byte of final block
wire [3:0] dj_nblk = (dj_msglen / 12'd136) + 4'd1;
// global byte index for the address presented this cycle (assemble)
wire [11:0] dj_g = {4'd0, dj_blk} * 12'd136 + {4'd0, dj_byte};
// c_in_bram byte index (when g>=32): g-32
wire [11:0] dj_c_idx = dj_g - 12'd32;
// SHAKE256 pad constant for global byte g
function [7:0] dj_padconst(input [11:0] g);
begin
if (g == dj_last_byte && g == dj_msglen) dj_padconst = 8'h9f; // 0x1F|0x80
else if (g == dj_msglen) dj_padconst = 8'h1f;
else if (g == dj_last_byte) dj_padconst = 8'h80;
else dj_padconst = 8'h00;
end
endfunction
// h_hash / h_vo are now served by the shared u_sha3 above (mb_en=1 during
// ST_H). They alias the single core's outputs; the H consumer logic below
// already gates on st==ST_H, and u_sha3's valid_o/hash_o are mb-selected.
@@ -1075,7 +1123,10 @@ module mlkem_top #(
// D3: w = v' - INTT(sum_j s_hat[j] o u_hat[j]). Single output poly
// (u_row 0..0); MAC->INTT->SUB, then D4 encodes m'.
ST_DEC_W: if (u_row >= 3'd1) st_next = ST_DEC_MENC;
ST_DEC_MENC: if (men_done) st_next = ST_DONE;
ST_DEC_MENC: if (men_done) st_next = ST_DEC_G;
// D5: (K',r') = G(m'||h) single-block, then K-bar = J(z||c) multi-block.
ST_DEC_G: if (sha3_vo) st_next = ST_DEC_J;
ST_DEC_J: if (dj_done) st_next = ST_DONE;
ST_G: if (sha3_vo) st_next = ST_A;
ST_A: if (a_pair >= kk_rt) st_next = ST_C;
ST_C: if (c_poly >= {1'b0, k_r, 1'b0}) st_next = ST_N;
@@ -1220,6 +1271,15 @@ module mlkem_top #(
men_ph <= 2'd0;
men_done <= 1'b0;
mprime_r <= 256'd0;
dj_blk <= 4'd0;
dj_byte <= 8'd0;
dj_phase <= 2'd0;
dj_mbvalid <= 1'b0;
dj_mblast <= 1'b0;
dj_ack <= 1'b0;
dj_done <= 1'b0;
dj_wb_vld <= 1'b0;
dj_block_r <= 1088'd0;
h_blk <= 3'd0;
h_byte <= 8'd0;
h_phase <= 2'd0;
@@ -1885,6 +1945,93 @@ module mlkem_top #(
sha3_ack <= 1'b0;
end
// Arm Decaps D5 G when m' is ready (ST_DEC_MENC -> ST_DEC_G): fire the
// 64-byte single-block G(m'||h). dec_g_data = {hek_r, mprime_r}.
if (st == ST_DEC_MENC && st_next == ST_DEC_G) begin
sha3_valid <= 1'b1;
sha3_ack <= 1'b1;
end
// Capture (K', r') when D5 G completes. K' = low half (candidate ss),
// r' = high half (PRF seed for D6 re-encrypt). Reuse ss_r / r_r.
if (st == ST_DEC_G && sha3_vo) begin
ss_r <= sha3_hash[255:0]; // K' = G bytes 0..31
r_r <= sha3_hash[511:256]; // r' = G bytes 32..63
sha3_ack <= 1'b0;
end
// Arm D5 J(z||c) when G completes (ST_DEC_G -> ST_DEC_J): assemble the
// first 136-byte block. dj_ack high to consume the final digest.
if (st == ST_DEC_G && st_next == ST_DEC_J) begin
dj_blk <= 4'd0;
dj_byte <= 8'd0;
dj_phase <= 2'd0;
dj_mbvalid<= 1'b0;
dj_mblast <= 1'b0;
dj_ack <= 1'b1;
dj_done <= 1'b0;
dj_wb_vld <= 1'b0;
cin_rd_addr_r <= 11'd0; // present c byte 0 (g=32 -> c_idx 0)
end
// ---- ST_DEC_J: K-bar = J(z||c) multi-block SHAKE256 ----
// Mirror of the H(ek) multi-block machine. Byte source by global g:
// g < 32 -> z_r byte g
// 32 <= g < msglen -> c_in_bram byte (g-32), registered read
// else -> SHAKE256 pad constant
// c_in_bram read is registered: present c_idx for dj_byte this cycle,
// write back the byte that arrived for the addr presented last cycle.
if (st == ST_DEC_J && !dj_done) begin
case (dj_phase)
2'd0: begin
// writeback the byte read for the previous address
if (dj_wb_vld) begin
if (dj_wb_inz)
dj_block_r[dj_wb_idx*8 +: 8] <= z_r[dj_wb_zidx*8 +: 8];
else if (dj_wb_inc)
dj_block_r[dj_wb_idx*8 +: 8] <= cin_rd_data;
else
dj_block_r[dj_wb_idx*8 +: 8] <= dj_wb_pad;
end
if (dj_byte <= 8'd135) begin
dj_wb_vld <= 1'b1;
dj_wb_idx <= dj_byte;
dj_wb_inz <= (dj_g < 12'd32);
dj_wb_inc <= (dj_g >= 12'd32) && (dj_g < dj_msglen);
dj_wb_zidx <= dj_g[4:0];
dj_wb_pad <= dj_padconst(dj_g);
// c_in addr is presented combinationally (cin_rd_addr
// mux uses dj_c_idx during ST_DEC_J); data lands next
// cycle, matching this byte's writeback.
dj_byte <= dj_byte + 8'd1;
end else begin
dj_wb_vld <= 1'b0;
dj_byte <= 8'd0;
dj_mbvalid <= 1'b1;
dj_mblast <= (dj_blk == dj_nblk - 4'd1);
dj_phase <= 2'd1;
end
end
2'd1: begin
if (dj_mbvalid && !h_mbready) begin
dj_mbvalid <= 1'b0;
dj_mblast <= 1'b0;
dj_phase <= 2'd2;
end
end
2'd2: begin
if (sha3_vo) begin
kbar_r <= sha3_hash[255:0];
dj_done <= 1'b1;
dj_phase<= 2'd3;
end else if (h_mbready) begin
dj_blk <= dj_blk + 4'd1;
dj_phase <= 2'd0;
end
end
default: ; // done
endcase
end
// Arm rho-load when entering ST_ENC_LOAD. rho = ek[384k .. 384k+31].
if (st == ST_ENC_G && st_next == ST_ENC_LOAD) begin
rl_idx <= 6'd0;