From 4d7ce69405d18a4e2fa43e1d53ace8be5daec196 Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Sat, 27 Jun 2026 17:23:28 +0800 Subject: [PATCH] fix(sample_ntt,sha3): FIPS-203 SHAKE-128 squeeze + self-checking sha3 TBs sample_ntt was non-conformant: both RTL and the test reference re-ran keccak_p after every 3-byte squeeze instead of consuming the full 1344-bit SHAKE-128 rate. Only coeff[0] matched a standard sampler, so the generated A matrix would not interoperate with any compliant ML-KEM. - sample_ntt_sync{,_shared}.v: walk all 56 groups of the rate block via grp_ptr_r; re-permute only when the block is exhausted. Verified 256/256 against ml-kem-r Rust sample_ntt on two seeds, and 1536/1536 in the Verilator framework (runtime ~128x faster per poly). - gen_vectors.py: use a self-contained hashlib.shake_128 oracle. sha3 testbench fixes (all now self-check hash_o against verified vectors, cross-checked with hashlib and ml-kem-r mlkem_G): - tb_sha3_xsim_simple.v: test G/H/J modes, not just G. - tb_keccak_core_xsim.v: correct the wrong EXPECTED_STATE constant (RTL was correct; lane0 = 0xf1258f7940e1dde7 per FIPS 202). - tb_sha3_xsim.v: read expected file and self-check per vector; add vectors/g_basic_{input,expected}.hex (3 G / 2 H / 2 J). Remove stale sha3_chain test (its RTL was deleted in 1cace51) and its README references. Extend .gitignore for XSIM artifacts and result dumps. --- .gitignore | 17 ++ README.md | 6 +- sync_rtl/sample_ntt/sample_ntt_sync.v | 77 ++++++-- sync_rtl/sample_ntt/sample_ntt_sync_shared.v | 75 ++++++-- sync_rtl/sha3/TB/tb_keccak_core_xsim.v | 6 +- sync_rtl/sha3/TB/tb_sha3_xsim.v | 33 +++- sync_rtl/sha3/TB/tb_sha3_xsim_simple.v | 182 +++++++++--------- sync_rtl/sha3/TB/vectors/g_basic_expected.hex | 7 + sync_rtl/sha3/TB/vectors/g_basic_input.hex | 7 + .../modules/sample_ntt/gen_vectors.py | 63 +++--- .../modules/sha3_chain/gen_vectors.py | 126 ------------ .../modules/sha3_chain/test_plan.json | 14 -- 12 files changed, 318 insertions(+), 295 deletions(-) create mode 100644 sync_rtl/sha3/TB/vectors/g_basic_expected.hex create mode 100644 sync_rtl/sha3/TB/vectors/g_basic_input.hex delete mode 100644 test_framework/modules/sha3_chain/gen_vectors.py delete mode 100644 test_framework/modules/sha3_chain/test_plan.json diff --git a/.gitignore b/.gitignore index 1dd4f2a..7c58397 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,23 @@ __pycache__/ test_framework/reports/report_*.html test_framework/modules/*/vectors/*.hex +# XSIM testbench result dumps (regenerated by simulation) +sync_rtl/**/TB/vectors/*_result.hex + +# Vivado XSIM simulation artifacts +xsim.dir/ +*.jou +*.log +*.pb +*.wdb +*.backup.jou +*.backup.log +.Xil/ +webtalk*.jou +webtalk*.log +xelab.pb +xvlog.pb + # OS .DS_Store Thumbs.db diff --git a/README.md b/README.md index 50cd420..b769cf1 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,6 @@ mlkem/ │ │ ├── keccak_round.v # Single Keccak-f round (θ,ρ,π,χ,ι) │ │ ├── keccak_core.v # 24-round sequential Keccak-f[1600] core │ │ └── sha3_top.v # SHA3-512(G)/SHA3-256(H)/SHAKE-256(J) wrapper -│ ├── sha3_chain/ # G function for key generation -│ │ └── sha3_chain_top.v # SHA3-512 chain: G(d||k=2) → rho, sigma │ ├── rng/ # Pseudorandom number generator │ │ └── rng_sync.v # 256-bit Galois LFSR (taps: 255,253,252,247,0) │ ├── ntt/ # Number Theoretic Transform @@ -157,7 +155,7 @@ export LD_PRELOAD=/usr/lib64/libtinfo.so.5 # ncurses fix for 2019.2 on modern L # Run all modules for m in mod_add rng poly_arith comp_decomp storage \ - sha3_chain ntt poly_mul sample_cbd sample_ntt; do + ntt poly_mul sample_cbd sample_ntt; do ./run_tb.sh "$m" done ``` @@ -236,7 +234,6 @@ A single Keccak-f[1600] permutation engine is shared across all SHA-3/SHAKE modu | `poly_mul_sync` | 512×coeff → 256×coeff | ~300 cycles | NTT-domain poly multiply | | `comp_decomp_sync` | coeff_in[11:0], d[4:0] → coeff_out | 1 cycle | Compress/Decompress | | `sha3_top` | data_i[511:0], mode → hash_o[511:0] | ~24 cycles | SHA3/SHAKE | -| `sha3_chain_top` | d_in[255:0], start → rho, sigma | ~24 cycles | G function | | `sample_cbd_sync` | seed[255:0], nonce, eta → 256×coeff | ~300 cycles | CBD sampling | | `sample_ntt_sync` | rho[255:0], k,i,j → 256×coeff | ~4000 cycles | SampleNTT | | `s_bram` | rd/wr addr, data | 1 cycle | Single-port BRAM | @@ -248,7 +245,6 @@ A single Keccak-f[1600] permutation engine is shared across all SHA-3/SHAKE modu |--------|:---:|:---:|:---:| | sha3_top | ✅ | ✅ | PASS | | keccak_core | — | ✅ | PASS | -| sha3_chain_top | ✅ | ✅ | PASS | | rng_sync | ✅ | ✅ | PASS | | mod_add_sync | ✅ | ✅ | PASS | | ntt_core | ✅ | ✅ | PASS | diff --git a/sync_rtl/sample_ntt/sample_ntt_sync.v b/sync_rtl/sample_ntt/sample_ntt_sync.v index 7be48e1..97621a0 100644 --- a/sync_rtl/sample_ntt/sample_ntt_sync.v +++ b/sync_rtl/sample_ntt/sample_ntt_sync.v @@ -3,12 +3,14 @@ // Generates one k×k polynomial (256 coefficients) via SHAKE-128 XOF // rejection sampling from seed rho || j || i. // -// Matches Python reference (sample.py/SHA_3.py) bit-exactly: +// FIPS 202/203 conformant SHAKE-128 squeeze: // - Absorb: S = Keccak-p(padded(rho || j || i)) -// - For each squeeze: take S[23:0] (3 bytes), extract d1[11:0], d2[23:12] +// - Squeeze the full 1344-bit (168-byte) rate as 56 groups of 3 bytes, +// each group g read from S[24*g +: 24]; extract d1[11:0], d2[23:12] // - Accept d if d < Q=3329 -// - S = Keccak-p(S) (permute between every 3-byte squeeze) -// - Repeat until 256 coefficients collected +// - Only after all 56 groups of the block are consumed: S = Keccak-p(S) +// (re-permute once per rate block, NOT per 3-byte group) +// - Repeat until 256 coefficients collected (~3 blocks) // // Parameters: // K = 4 (ML-KEM parameter) @@ -52,6 +54,10 @@ module sample_ntt_sync #(parameter K = 4) ( // ================================================================ localparam Q = `Q; // 3329 + // SHAKE-128 rate = 1344 bits = 168 bytes = 56 groups of 3 bytes. + // After consuming all 56 groups of a block, re-permute the state. + localparam GRP_MAX = 6'd55; + // ================================================================ // FSM state encoding // ================================================================ @@ -84,6 +90,13 @@ module sample_ntt_sync #(parameter K = 4) ( // ================================================================ reg [1599:0] squeeze_state_r; + // ================================================================ + // Group pointer: which 3-byte group within the 1344-bit rate + // block is currently being consumed (0..GRP_MAX). Re-permute when + // it would exceed GRP_MAX. + // ================================================================ + reg [5:0] grp_ptr_r; + // ================================================================ // Registered d1, d2 and acceptance flags // ================================================================ @@ -121,15 +134,21 @@ module sample_ntt_sync #(parameter K = 4) ( }; // ================================================================ - // Comb: extract d1,d2 from squeeze state + // Comb: extract d1,d2 from the current 3-byte group of squeeze state // ================================================================ - // squeeze_state_r[7:0]=c0, [15:8]=c1, [23:16]=c2 + // group g occupies bits [24*g +: 24]: c0=byte0, c1=byte1, c2=byte2 // d1 = {c1[3:0], c0} // d2 = {c2, c1[7:4]} + wire [10:0] grp_bit_off; + assign grp_bit_off = grp_ptr_r * 11'd24; // 0..1320, +24 ≤ 1344 (rate) + + wire [23:0] grp_bits; + assign grp_bits = squeeze_state_r[ grp_bit_off +: 24 ]; + wire [7:0] c0, c1, c2; - assign c0 = squeeze_state_r[7:0]; - assign c1 = squeeze_state_r[15:8]; - assign c2 = squeeze_state_r[23:16]; + assign c0 = grp_bits[7:0]; + assign c1 = grp_bits[15:8]; + assign c2 = grp_bits[23:16]; wire [11:0] d1_comb, d2_comb; assign d1_comb = {c1[3:0], c0}; @@ -161,10 +180,10 @@ module sample_ntt_sync #(parameter K = 4) ( .ready_i (1'b1) ); - // kc_valid_i: asserted during ABSORB and first phase of SQUEEZE. - // Keccak captures it on the transition (when ready_o=1). + // kc_valid_i: asserted on the ABSORB load, and for one cycle when the + // squeeze block is exhausted (SQUEEZE → WAIT) to re-permute the state. assign kc_valid_i = (state_next == ST_ABSORB) || - (state_r == ST_SQUEEZE && sq_phase_r == 2'd0); + (state_r == ST_SQUEEZE && state_next == ST_WAIT); // kc_state_i: absorb_state in ABSORB, squeeze_state_r otherwise assign kc_state_i = (state_next == ST_ABSORB) ? absorb_state : squeeze_state_r; @@ -186,6 +205,11 @@ module sample_ntt_sync #(parameter K = 4) ( assign ready_o = (state_r == ST_IDLE); wire need_more = (coeff_cnt_r < 9'd256); + // grp_done: current 3-byte group fully consumed (phase 2 resolved): + // d2 rejected, no longer need coefficients, or d2 was just accepted out. + wire grp_done = (state_r == ST_SQUEEZE) && (sq_phase_r == 2'd2) && + (!d2_acc_r || !need_more || (valid_o_r && ready_i)); + // ================================================================ // FSM: state_next (combinational) // ================================================================ @@ -205,11 +229,18 @@ module sample_ntt_sync #(parameter K = 4) ( end ST_SQUEEZE: begin - // Sub-phase transitions managed in sequential logic. - // Only transitions to ST_WAIT from phase 2 when done. - if (sq_phase_r == 2'd2 && - (!d2_acc_r || !need_more || (valid_o_r && ready_i))) - state_next = ST_WAIT; + // A group is fully consumed once phase 2 resolves (d2 output, + // rejected, or no longer needed). Then either advance to the + // next group in this block, re-permute (block exhausted), or + // finish. + if (grp_done) begin + if (!need_more) + state_next = ST_DONE; + else if (grp_ptr_r < GRP_MAX) + state_next = ST_SQUEEZE; // next group, no re-permute + else + state_next = ST_WAIT; // block exhausted: re-permute + end end ST_WAIT: begin @@ -239,6 +270,7 @@ module sample_ntt_sync #(parameter K = 4) ( sq_phase_r <= 2'd0; coeff_cnt_r <= 9'd0; squeeze_state_r <= 1600'd0; + grp_ptr_r <= 6'd0; d1_r <= 12'd0; d2_r <= 12'd0; d1_acc_r <= 1'b0; @@ -265,10 +297,12 @@ module sample_ntt_sync #(parameter K = 4) ( end // --------------------------------------------------------- - // Latch keccak output when valid_o fires + // Latch keccak output when valid_o fires. A fresh block + // starts at group 0 (ABSORB load or WAIT re-permute result). // --------------------------------------------------------- if (kc_valid_o) begin squeeze_state_r <= kc_state_o; + grp_ptr_r <= 6'd0; end // --------------------------------------------------------- @@ -347,6 +381,13 @@ module sample_ntt_sync #(parameter K = 4) ( valid_o_r <= 1'b0; end endcase + + // Group consumed but block not exhausted: advance to the + // next 3-byte group within the same block (no re-permute). + if (grp_done && need_more && grp_ptr_r < GRP_MAX) begin + grp_ptr_r <= grp_ptr_r + 6'd1; + sq_phase_r <= 2'd0; + end end else if (state_r != ST_SQUEEZE && state_next == ST_SQUEEZE) begin // About to enter SQUEEZE: reset phase and output sq_phase_r <= 2'd0; diff --git a/sync_rtl/sample_ntt/sample_ntt_sync_shared.v b/sync_rtl/sample_ntt/sample_ntt_sync_shared.v index 5e9be67..5339a4b 100644 --- a/sync_rtl/sample_ntt/sample_ntt_sync_shared.v +++ b/sync_rtl/sample_ntt/sample_ntt_sync_shared.v @@ -8,12 +8,14 @@ // Generates one k×k polynomial (256 coefficients) via SHAKE-128 XOF // rejection sampling from seed rho || j || i. // -// Matches Python reference (sample.py/SHA_3.py) bit-exactly: +// FIPS 202/203 conformant SHAKE-128 squeeze: // - Absorb: S = Keccak-p(padded(rho || j || i)) -// - For each squeeze: take S[23:0] (3 bytes), extract d1[11:0], d2[23:12] +// - Squeeze the full 1344-bit (168-byte) rate as 56 groups of 3 bytes, +// each group g read from S[24*g +: 24]; extract d1[11:0], d2[23:12] // - Accept d if d < Q=3329 -// - S = Keccak-p(S) (permute between every 3-byte squeeze) -// - Repeat until 256 coefficients collected +// - Only after all 56 groups of the block are consumed: S = Keccak-p(S) +// (re-permute once per rate block, NOT per 3-byte group) +// - Repeat until 256 coefficients collected (~3 blocks) // // Parameters: // K = 4 (ML-KEM parameter) @@ -75,6 +77,10 @@ module sample_ntt_sync_shared #(parameter K = 4) ( // ================================================================ localparam Q = `Q; // 3329 + // SHAKE-128 rate = 1344 bits = 168 bytes = 56 groups of 3 bytes. + // After consuming all 56 groups of a block, re-permute the state. + localparam GRP_MAX = 6'd55; + // ================================================================ // FSM state encoding // ================================================================ @@ -107,6 +113,12 @@ module sample_ntt_sync_shared #(parameter K = 4) ( // ================================================================ reg [1599:0] squeeze_state_r; + // ================================================================ + // Group pointer: which 3-byte group within the 1344-bit rate + // block is currently being consumed (0..GRP_MAX). + // ================================================================ + reg [5:0] grp_ptr_r; + // ================================================================ // Registered d1, d2 and acceptance flags // ================================================================ @@ -144,15 +156,21 @@ module sample_ntt_sync_shared #(parameter K = 4) ( }; // ================================================================ - // Comb: extract d1,d2 from squeeze state + // Comb: extract d1,d2 from the current 3-byte group of squeeze state // ================================================================ - // squeeze_state_r[7:0]=c0, [15:8]=c1, [23:16]=c2 + // group g occupies bits [24*g +: 24]: c0=byte0, c1=byte1, c2=byte2 // d1 = {c1[3:0], c0} // d2 = {c2, c1[7:4]} + wire [10:0] grp_bit_off; + assign grp_bit_off = grp_ptr_r * 11'd24; // 0..1320, +24 ≤ 1344 (rate) + + wire [23:0] grp_bits; + assign grp_bits = squeeze_state_r[ grp_bit_off +: 24 ]; + wire [7:0] c0, c1, c2; - assign c0 = squeeze_state_r[7:0]; - assign c1 = squeeze_state_r[15:8]; - assign c2 = squeeze_state_r[23:16]; + assign c0 = grp_bits[7:0]; + assign c1 = grp_bits[15:8]; + assign c2 = grp_bits[23:16]; wire [11:0] d1_comb, d2_comb; assign d1_comb = {c1[3:0], c0}; @@ -176,9 +194,10 @@ module sample_ntt_sync_shared #(parameter K = 4) ( // kc_ready_i: always ready to accept keccak output assign kc_ready_i = 1'b1; - // kc_valid_i: asserted during ABSORB and first phase of SQUEEZE. + // kc_valid_i: asserted on the ABSORB load, and for one cycle when the + // squeeze block is exhausted (SQUEEZE → WAIT) to re-permute the state. assign kc_valid_i = (state_next == ST_ABSORB) || - (state_r == ST_SQUEEZE && sq_phase_r == 2'd0); + (state_r == ST_SQUEEZE && state_next == ST_WAIT); // kc_state_i: absorb_state in ABSORB, squeeze_state_r otherwise assign kc_state_i = (state_next == ST_ABSORB) ? absorb_state : squeeze_state_r; @@ -200,6 +219,11 @@ module sample_ntt_sync_shared #(parameter K = 4) ( assign ready_o = (state_r == ST_IDLE); wire need_more = (coeff_cnt_r < 9'd256); + // grp_done: current 3-byte group fully consumed (phase 2 resolved): + // d2 rejected, no longer need coefficients, or d2 was just accepted out. + wire grp_done = (state_r == ST_SQUEEZE) && (sq_phase_r == 2'd2) && + (!d2_acc_r || !need_more || (valid_o_r && ready_i)); + // ================================================================ // FSM: state_next (combinational) // ================================================================ @@ -219,11 +243,18 @@ module sample_ntt_sync_shared #(parameter K = 4) ( end ST_SQUEEZE: begin - // Sub-phase transitions managed in sequential logic. - // Only transitions to ST_WAIT from phase 2 when done. - if (sq_phase_r == 2'd2 && - (!d2_acc_r || !need_more || (valid_o_r && ready_i))) - state_next = ST_WAIT; + // A group is fully consumed once phase 2 resolves (d2 output, + // rejected, or no longer needed). Then either advance to the + // next group in this block, re-permute (block exhausted), or + // finish. + if (grp_done) begin + if (!need_more) + state_next = ST_DONE; + else if (grp_ptr_r < GRP_MAX) + state_next = ST_SQUEEZE; // next group, no re-permute + else + state_next = ST_WAIT; // block exhausted: re-permute + end end ST_WAIT: begin @@ -253,6 +284,7 @@ module sample_ntt_sync_shared #(parameter K = 4) ( sq_phase_r <= 2'd0; coeff_cnt_r <= 9'd0; squeeze_state_r <= 1600'd0; + grp_ptr_r <= 6'd0; d1_r <= 12'd0; d2_r <= 12'd0; d1_acc_r <= 1'b0; @@ -279,10 +311,12 @@ module sample_ntt_sync_shared #(parameter K = 4) ( end // --------------------------------------------------------- - // Latch keccak output when valid_o fires + // Latch keccak output when valid_o fires. A fresh block + // starts at group 0 (ABSORB load or WAIT re-permute result). // --------------------------------------------------------- if (kc_valid_o) begin squeeze_state_r <= kc_state_o; + grp_ptr_r <= 6'd0; end // --------------------------------------------------------- @@ -361,6 +395,13 @@ module sample_ntt_sync_shared #(parameter K = 4) ( valid_o_r <= 1'b0; end endcase + + // Group consumed but block not exhausted: advance to the + // next 3-byte group within the same block (no re-permute). + if (grp_done && need_more && grp_ptr_r < GRP_MAX) begin + grp_ptr_r <= grp_ptr_r + 6'd1; + sq_phase_r <= 2'd0; + end end else if (state_r != ST_SQUEEZE && state_next == ST_SQUEEZE) begin // About to enter SQUEEZE: reset phase and output sq_phase_r <= 2'd0; diff --git a/sync_rtl/sha3/TB/tb_keccak_core_xsim.v b/sync_rtl/sha3/TB/tb_keccak_core_xsim.v index afcb309..1874e35 100644 --- a/sync_rtl/sha3/TB/tb_keccak_core_xsim.v +++ b/sync_rtl/sha3/TB/tb_keccak_core_xsim.v @@ -53,9 +53,11 @@ module tb_keccak_core_xsim; // // Keccak-f[1600](0) after 24 rounds, flat 1600-bit output. // Verilog packing: {lane24, lane23, ..., lane1, lane0} - // where lane[i] = A[i%5][i/5] at bits [i*64+63 : i*64] + // where lane[i] = A[i%5][i/5] at bits [i*64+63 : i*64]. + // lane0 = 0xf1258f7940e1dde7 is the well-known FIPS 202 test value; + // independently recomputed and confirmed against the RTL output. // ================================================================ - parameter [1599:0] EXPECTED_STATE = 1600'hbfb7e1b9bd5d5750a1ad89d6b16dd89e76c82a7b8b784ff1a8f71cbe511a4b37593f83e2476e446a9607dab51d2284543912af66d5169a42b000c95e3f38a1d14df070f6937de8028965308c22a1d7ed39beddcf42fc7c09f566afaa29ffc221e0bedc7fe0a51684906ebd59a992e4c2e88479b3c8e88c45bfe624f5737b96a5d0656897dda87cafe0f3909e35059e7a83831c8c135d1f2ac3c03f088c216b4c4a445d8c512ddea81c7cc6d86579ec7d3ca0d28f00c7d66020fb5a92a1a94488625f47811fa2dc9d; + parameter [1599:0] EXPECTED_STATE = 1600'heaf1ff7b5ceca24975f644e97f30a13b16f53526e70465c21841f924a2c509e4940c7922ae3a26148c3ee88a1ccf32c8b87c5a554fd00ecb613670957bc4661164befef28cc970f205e5635a21d9ae6101f22f1a11a5569f43b831cd0347c82681a57c16dbcf555fa9a6e6260d712103eb5aa93f2317d63530935ab7d08ffc64ad30a6f71b19059c8c5bda0cd6192e7690fee5a0a44647c4ff97a42d7f8e6fd48b284e056253d057bd1547306f80494dd598261ea65aa9ee84d5ccf933c0478af1258f7940e1dde7; // ================================================================ // Test sequence diff --git a/sync_rtl/sha3/TB/tb_sha3_xsim.v b/sync_rtl/sha3/TB/tb_sha3_xsim.v index bdc5c55..bfbcb9a 100644 --- a/sync_rtl/sha3/TB/tb_sha3_xsim.v +++ b/sync_rtl/sha3/TB/tb_sha3_xsim.v @@ -26,6 +26,7 @@ module tb_sha3_xsim; // Parameters // ================================================================ parameter VECTOR_FILE = "sync_rtl/sha3/TB/vectors/g_basic_input.hex"; + parameter EXPECTED_FILE = "sync_rtl/sha3/TB/vectors/g_basic_expected.hex"; parameter RESULT_FILE = "sync_rtl/sha3/TB/vectors/g_basic_result.hex"; parameter MAX_VECTORS = 256; parameter TIMEOUT_CYCLES = 1000; @@ -69,6 +70,7 @@ module tb_sha3_xsim; // 520 bits per word: bits[519:512]=padding+mode, bits[511:0]=data_i // ================================================================ reg [519:0] vector_mem [0:MAX_VECTORS-1]; + reg [511:0] expected_mem [0:MAX_VECTORS-1]; // expected hash per vector integer vec_count; integer idx; integer cycle_count; @@ -100,6 +102,8 @@ module tb_sha3_xsim; // Load vectors from hex file $readmemh(VECTOR_FILE, vector_mem); + // Load expected hashes (one 512-bit hex per line, MSB-first) + $readmemh(EXPECTED_FILE, expected_mem); // Count non-zero entries to determine actual vector count // (XSim leaves unloaded entries as 520'hX) @@ -180,9 +184,29 @@ module tb_sha3_xsim; $display("ERROR: Timeout waiting for valid_o on vector %0d", idx); fail_count = fail_count + 1; end else begin - // Capture hash output + // Capture hash output and self-check against expected. + // G (mode 0) uses all 512 bits; H/J use the low 256 bits. captured_hash = hash_o; - pass_count = pass_count + 1; + + begin + reg [511:0] exp_hash; + reg match; + exp_hash = expected_mem[idx]; + if (vec_mode == 2'd0) + match = (captured_hash === exp_hash); + else + match = (captured_hash[255:0] === exp_hash[255:0]); + + if (match) begin + pass_count = pass_count + 1; + $display("PASS: Vector %0d (mode=%0d)", idx, vec_mode); + end else begin + fail_count = fail_count + 1; + $display("FAIL: Vector %0d (mode=%0d) hash mismatch", idx, vec_mode); + $display(" got = %0h", (vec_mode==2'd0) ? captured_hash : {256'd0, captured_hash[255:0]}); + $display(" exp = %0h", (vec_mode==2'd0) ? exp_hash : {256'd0, exp_hash[255:0]}); + end + end // Write result to output file // Format: "RESULT: MODE HASH_HEX" @@ -216,6 +240,11 @@ module tb_sha3_xsim; $display(" Failed: %0d", fail_count); $display(" Results written to: %s", RESULT_FILE); $display("========================================"); + if (fail_count == 0) + $display("ALL TESTS PASSED (%0d/%0d)", pass_count, vec_count); + else + $display("TESTS FAILED: %0d of %0d", fail_count, vec_count); + $display("========================================"); $finish; end diff --git a/sync_rtl/sha3/TB/tb_sha3_xsim_simple.v b/sync_rtl/sha3/TB/tb_sha3_xsim_simple.v index a6c8a19..7d41209 100644 --- a/sync_rtl/sha3/TB/tb_sha3_xsim_simple.v +++ b/sync_rtl/sha3/TB/tb_sha3_xsim_simple.v @@ -1,12 +1,14 @@ -// tb_sha3_xsim_simple.v - Simple self-checking testbench for sha3_top +// tb_sha3_xsim_simple.v - Self-checking testbench for sha3_top (G/H/J modes) // -// Tests sha3_top in G mode (SHA3-512) with a hardcoded all-zero input. -// Verifies the output hash against an expected value. -// Uses $display for output and $error for mismatches. -// Self-checking: pass/fail determined by $error count at $finish. +// Drives sha3_top in all three modes and checks hash_o against expected +// values computed by the verified Python reference (server_code SHA_3, +// KAT-validated). Vectors are deterministic (gen_vectors seed=20260627). // -// NOTE: This testbench uses the RTL's actual padding (suffix "10"). -// The expected hash was pre-computed using the same algorithm as the RTL. +// G (mode=00, SHA3-512): data_i[263:0] = d||k, output 512 bits +// H (mode=01, SHA3-256): data_i[255:0] = ek, output low 256 bits +// J (mode=10, SHAKE-256): data_i[511:0] = z||c, output low 256 bits +// +// data_i / hash_o use MSB-first hex packing (hex[511:0] literal == data_i). // // Usage: // xvlog -sv sha3_top.v tb_sha3_xsim_simple.v @@ -52,111 +54,119 @@ module tb_sha3_xsim_simple; always #5 clk = ~clk; // ================================================================ - // Expected hash value for G mode with all-zero input - // - // Input: data_i[263:0] = 264'd0 (all zeros) - // mode = 2'b00 (G mode, SHA3-512) - // - // RTL g_pad = {1'b1, 308'b0, 1'b1, 2'b10, data_i[263:0]} - // absorb_state = {1024'b0, g_pad} - // - // Expected hash_o = Keccak-f[1600](absorb_state) lower 512 bits + // Expected vectors (verified Python reference, seed=20260627) // ================================================================ - parameter [511:0] G_EXPECTED_HASH = 512'h93d50514dbf28b7f2b6aa4f34bc6bd53368a9a20c6568940dc8eb3ce0a8e357f8608c63ce7b579f6916c69ca3f196527ccc92b87c515edc12e159e0f3092e1d9; + // G: data_i[263:0] = {k=2, d}; output 512-bit SHA3-512 + localparam [511:0] G_DATA = 512'h000000000000000000000000000000000000000000000000000000000000007DBC2AC0D13D719B37B4E2D4691951FF890A97854EF5D3A8957EF67A54978E26C9; + localparam [511:0] G_EXP = 512'h615E530C77D5D834311E922DB99D5B7F1D57C7B08F029FD829914D3F4035FB730350FD00A852A5CCE9CFC79CF8C61384FEA115030E41750A6AE2CFE055D7976D; + + // H: data_i[255:0] = ek; output 256-bit SHA3-256 (in hash_o[255:0]) + localparam [511:0] H_DATA = 512'h000000000000000000000000000000000000000000000000000000000000000047885BF4E257CF39645D34C593047B7570D6ABBA300599D96171A950BB5027D5; + localparam [255:0] H_EXP = 256'h6B44867E24B29A9231570DF5E1D6D14DB0C29EBD7A40AE98606EF66B8244C308; + + // J: data_i[511:0] = {c, z}; output 256-bit SHAKE-256 (in hash_o[255:0]) + localparam [511:0] J_DATA = 512'hB73DCA0F437D2334320494E5D0F728D73F5275E342572FF9B0219DC338CB3C2F0F7398474A3D68C4C90B777F42FA4C12B1FC8F70E1DAADF20755473CC2653D3C; + localparam [255:0] J_EXP = 256'hECEC4DAD11DFF42D911925DB83F13D119209AB4EE182E9E9BA0F29F5524B240E; - // ================================================================ - // Test sequence - // ================================================================ - reg [511:0] captured_hash; integer error_count; integer cycle_count; parameter TIMEOUT = 200; + // ================================================================ + // Drive one mode and self-check against expected output + // ================================================================ + task run_mode(input [1:0] m, input [511:0] din, + input [511:0] exp, input integer out_bits, + input [127:0] label); + reg [511:0] got; + begin + @(posedge clk); + mode <= m; + data_i <= din; + valid_i <= 1'b1; + @(posedge clk); + valid_i <= 1'b0; + + cycle_count = 0; + while (!valid_o && cycle_count < TIMEOUT) begin + @(posedge clk); + cycle_count = cycle_count + 1; + end + + if (cycle_count >= TIMEOUT) begin + $display("FAIL [%0s]: TIMEOUT, valid_o not asserted", label); + error_count = error_count + 1; + end else begin + got = hash_o; + if (out_bits == 512) begin + if (got !== exp) begin + $display("FAIL [%0s]: hash mismatch", label); + $display(" got = 512'h%0h", got); + $display(" exp = 512'h%0h", exp); + error_count = error_count + 1; + end else begin + $display("PASS [%0s]: hash_o = 512'h%0h", label, got); + end + end else begin + // Compare low 256 bits only + if (got[255:0] !== exp[255:0]) begin + $display("FAIL [%0s]: hash mismatch", label); + $display(" got = 256'h%0h", got[255:0]); + $display(" exp = 256'h%0h", exp[255:0]); + error_count = error_count + 1; + end else begin + $display("PASS [%0s]: hash_o = 256'h%0h", label, got[255:0]); + end + end + end + + // Wait for DUT to return to IDLE + @(posedge clk); + while (!ready_o) @(posedge clk); + end + endtask + + // ================================================================ + // Test sequence + // ================================================================ initial begin error_count = 0; $display("========================================"); - $display(" SHA3 Top Simple Self-Checking Testbench"); - $display(" Mode: G (SHA3-512)"); - $display(" Input: data_i = 512'd0"); + $display(" SHA3 Top Testbench (G / H / J modes)"); $display("========================================"); - // Initialize - mode = 2'd0; // G mode - data_i = 512'd0; - valid_i = 1'b0; - ready_i = 1'b1; // always ready + mode <= 2'd0; + data_i <= 512'd0; + valid_i <= 1'b0; + ready_i <= 1'b1; - // Reset: rst_n low for 3 cycles - rst_n = 1'b0; + rst_n <= 1'b0; repeat (3) @(posedge clk); - rst_n = 1'b1; + rst_n <= 1'b1; @(posedge clk); - $display("INFO: Reset complete. Starting test..."); + $display("INFO: Reset complete. Running G/H/J..."); - // Drive test vector - mode = 2'd0; - data_i = 512'd0; - valid_i = 1'b1; - @(posedge clk); - valid_i = 1'b0; + run_mode(2'd0, G_DATA, G_EXP, 512, "G SHA3-512"); + run_mode(2'd1, H_DATA, {256'd0,H_EXP},256, "H SHA3-256"); + run_mode(2'd2, J_DATA, {256'd0,J_EXP},256, "J SHAKE-256"); - $display("INFO: Vector driven (mode=G, data=0). Waiting for valid_o..."); - - // Wait for valid_o - cycle_count = 0; - while (!valid_o && cycle_count < TIMEOUT) begin - @(posedge clk); - cycle_count = cycle_count + 1; - end - - if (cycle_count >= TIMEOUT) begin - $error("TIMEOUT: valid_o not asserted within %0d cycles", TIMEOUT); - error_count = error_count + 1; - end else begin - captured_hash = hash_o; - - $display("INFO: valid_o asserted after %0d cycles", cycle_count + 1); - $display("INFO: hash_o = 512'h%0h", captured_hash); - - // Check against expected - if (captured_hash !== G_EXPECTED_HASH) begin - $error("MISMATCH!"); - $display(" Expected: 512'h%0h", G_EXPECTED_HASH); - $display(" Got: 512'h%0h", captured_hash); - error_count = error_count + 1; - end else begin - $display("PASS: hash_o matches expected value."); - end - end - - // One extra cycle - @(posedge clk); - - // ============================================================ - // Summary - // ============================================================ $display("========================================"); - if (error_count == 0) begin - $display("ALL TESTS PASSED"); - end else begin - $display("TESTS FAILED: %0d error(s)", error_count); - end - $display("========================================"); - - // Vivado xsim: $finish with error code - if (error_count > 0) - $finish; + if (error_count == 0) + $display("ALL TESTS PASSED (3/3 modes)"); else - $finish; + $display("TESTS FAILED: %0d error(s)", error_count); + $display("========================================"); + + $finish; end // ================================================================ // Timeout watchdog // ================================================================ initial begin - #(TIMEOUT * 10 * 10); // TIMEOUT * 10ns * extra margin + #(TIMEOUT * 10 * 100); $display("FATAL: Global simulation timeout"); $finish; end diff --git a/sync_rtl/sha3/TB/vectors/g_basic_expected.hex b/sync_rtl/sha3/TB/vectors/g_basic_expected.hex new file mode 100644 index 0000000..3dfdd8c --- /dev/null +++ b/sync_rtl/sha3/TB/vectors/g_basic_expected.hex @@ -0,0 +1,7 @@ +615E530C77D5D834311E922DB99D5B7F1D57C7B08F029FD829914D3F4035FB730350FD00A852A5CCE9CFC79CF8C61384FEA115030E41750A6AE2CFE055D7976D +5A7DEC7ECB7A1217993059A6CBAF7754BF774311A936963A10568900EEE6DB3BDAB89C84A5650189B0E8B2526BE916FDA070A0AB2E33399590B87D315C55D5CB +BE71BF8A954846FB70D52DE11DCC189E25B6A931E63BD2A3BB546E4C02A8929500F1AE56BFAF3D61ACF85ECFA2B5D8E02778CBBED34D22552A371B4DE938ED6B +0000000000000000000000000000000000000000000000000000000000000000A63A98E0A2FB57548A6906708813766DA0F1E9ACF3831A6E77C4A56227E9473E +0000000000000000000000000000000000000000000000000000000000000000CFB62681BD3A993605FB6C80D93E8A308C5494EF0641F92C18F83002453A8FA2 +0000000000000000000000000000000000000000000000000000000000000000B09665ECDF8E2934BC244DF46E36AF3B3DB2C18524300A896BE8837558BEE367 +0000000000000000000000000000000000000000000000000000000000000000EB253D3B38BBF839B448BBC0149ED56E12E4EE72D31DE12D881994CA16AA0C2A diff --git a/sync_rtl/sha3/TB/vectors/g_basic_input.hex b/sync_rtl/sha3/TB/vectors/g_basic_input.hex new file mode 100644 index 0000000..f7d9819 --- /dev/null +++ b/sync_rtl/sha3/TB/vectors/g_basic_input.hex @@ -0,0 +1,7 @@ +00000000000000000000000000000000000000000000000000000000000000007DBC2AC0D13D719B37B4E2D4691951FF890A97854EF5D3A8957EF67A54978E26C9 +0000000000000000000000000000000000000000000000000000000000000000C247885BF4E257CF39645D34C593047B7570D6ABBA300599D96171A950BB5027D5 +0000000000000000000000000000000000000000000000000000000000000000B038CB3C2F0F7398474A3D68C4C90B777F42FA4C12B1FC8F70E1DAADF20755473C +010000000000000000000000000000000000000000000000000000000000000000F9D94DE0F1EF4D2CB73DCA0F437D2334320494E5D0F728D73F5275E342572FF9 +0100000000000000000000000000000000000000000000000000000000000000008A1733CED3BC820A5D410AD00B0E4ACB0C260A8EBCD1B70FECF85B59256609CB +02CC2F7B1D60813526FB623E09F10D22635605046A66888277667DACCA02B420BBE795DC9A6DA171DA1DE082111568D55A8A27FE9B009275761098B44B484B3684 +02E94BA9958BE80EDB47A9C83C07DBE09EA32CF4B1A59AD014938117DA74B87E9840E880E1A6F0C9E69C5E78D61A357D30ECC92FE5890DF046D93D94E87EA4ECB0 diff --git a/test_framework/modules/sample_ntt/gen_vectors.py b/test_framework/modules/sample_ntt/gen_vectors.py index 62fe6aa..5d74106 100644 --- a/test_framework/modules/sample_ntt/gen_vectors.py +++ b/test_framework/modules/sample_ntt/gen_vectors.py @@ -1,9 +1,11 @@ """gen_vectors.py - Test vector generator for sample_ntt module. -Generates random rho seeds, calls the Python reference sampleNTT to compute -expected coefficients, and writes hex files for Verilator simulation. +Generates random rho seeds, computes expected coefficients with a standard +FIPS 202 SHAKE-128 rejection sampler (hashlib), and writes hex files for +Verilator simulation. -Matches the Python reference (sample.py / SHA_3.py) bit-exactly. +The expected coefficients are FIPS-203-conformant (Algorithm 7 SampleNTT): +SHAKE-128(rho || j || i), consume the full rate, 12-bit rejection sampling. """ import os @@ -11,21 +13,36 @@ import random import sys import hashlib -# Add the Python reference implementation to path -_REF_DIR = os.path.expanduser( - "~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0" -) -if _REF_DIR not in sys.path: - sys.path.insert(0, _REF_DIR) - -import utils -import sample as sample_ref - # Add test_framework/lib to path for VectorGenerator base class sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "lib")) from vector_gen import VectorGenerator +def _sample_ntt_ref(seed: bytes) -> list[int]: + """FIPS 203 Algorithm 7 SampleNTT using standard SHAKE-128. + + Args: + seed: 34-byte input = rho(32) || j(1) || i(1). + + Returns: + List of 256 coefficients in [0, 3329). + """ + # 1344 bytes = 8 full SHAKE-128 rate blocks; ample for 256 accepts. + stream = hashlib.shake_128(seed).digest(1344) + coeffs = [] + off = 0 + while len(coeffs) < 256: + c0, c1, c2 = stream[off], stream[off + 1], stream[off + 2] + off += 3 + d1 = c0 | ((c1 & 0x0F) << 8) + d2 = (c1 >> 4) | (c2 << 4) + if d1 < 3329: + coeffs.append(d1) + if d2 < 3329 and len(coeffs) < 256: + coeffs.append(d2) + return coeffs + + class SampleNTTVectorGenerator(VectorGenerator): """Generates test vectors for the sample_ntt_sync module.""" @@ -40,25 +57,21 @@ class SampleNTTVectorGenerator(VectorGenerator): """ k = params.get("k", 2) - # Generate random 32-byte rho (as binary string, matching the reference) - rho_bin = utils.random_Generator(8 * 32) # 256-bit binary string + # Random 32-byte rho + rho = bytes(random.randint(0, 255) for _ in range(32)) # Choose random (i, j) indices within [0, k-1] i = random.randint(0, k - 1) j = random.randint(0, k - 1) - # Build the 34-byte input for sampleNTT: rho || j || i - # Each component is a binary string (LSB at index 0) - s_j_bin = utils.dec_to_binary_little_endian(j) # 8-bit binary string - s_i_bin = utils.dec_to_binary_little_endian(i) # 8-bit binary string - B = rho_bin + s_j_bin + s_i_bin # 272-bit binary string + # 34-byte SampleNTT input: rho || j || i (FIPS 203 Alg 13/7) + seed = rho + bytes([j]) + bytes([i]) - # Compute expected coefficients using Python reference - coeffs = sample_ref.sampleNTT(B) # numpy array of 256 ints + # Expected coefficients from a standard SHAKE-128 sampler + coeffs = _sample_ntt_ref(seed) - # Convert rho to hex for the input file - # binary_to_hex_little_endian produces MSB-first hex per byte - rho_hex = utils.binary_to_hex_little_endian(rho_bin) + # rho hex: byte 0 first (matches TB parse_rho byte ordering) + rho_hex = rho.hex().upper() return { "input": { diff --git a/test_framework/modules/sha3_chain/gen_vectors.py b/test_framework/modules/sha3_chain/gen_vectors.py deleted file mode 100644 index 19412a3..0000000 --- a/test_framework/modules/sha3_chain/gen_vectors.py +++ /dev/null @@ -1,126 +0,0 @@ -"""gen_vectors.py - Test vector generator for sha3_chain module. - -Generates random 256-bit d, computes G(d||k=2) using Python reference, -and outputs rho (first 256 bits) and sigma (next 256 bits). -""" - -import os -import random -import sys - -# Add test_framework/lib to path for VectorGenerator base class -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib')) - -# Add Python reference path -_REF_PATH = os.path.expanduser( - "~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0") -sys.path.insert(0, _REF_PATH) - -import SHA_3 - -from vector_gen import VectorGenerator - - -def _bits_to_hex(bits_str, num_bits): - """Convert binary string (LSB-first: bits_str[0] = bit 0) to hex. - - Returns hex with MSB-nibble first (matching Verilog %X format). - Each group of 4 bits forms a nibble: bit[i+3:i] → two hex chars. - """ - result = [] - for i in range(0, num_bits, 4): - nib = 0 - for j in range(4): - pos = i + j - if pos < len(bits_str) and bits_str[pos] == '1': - nib |= (1 << j) - result.insert(0, '0123456789ABCDEF'[nib]) - return ''.join(result) - - -def _random_bits(length): - """Generate a random binary string of given length (LSB-first).""" - val = random.getrandbits(length) - bits = '' - for i in range(length): - bits += '1' if (val & (1 << i)) else '0' - return bits - - -def _k_bits(k_val): - """Convert integer k to 8-bit LSB-first binary string. - - k=2 → "01000000" (bit 0=0, bit 1=1) - """ - bits = '' - for i in range(8): - bits += '1' if (k_val & (1 << i)) else '0' - return bits - - -class Sha3ChainVectorGenerator(VectorGenerator): - """Generates test vectors for sha3_chain_top module.""" - - def generate_one(self, params: dict) -> dict: - """Generate a single test vector. - - Args: - params: dict with 'k' key (default 2 for ML-KEM-512). - - Returns: - dict with 'input' and 'expected' keys. - """ - k_val = params.get('k', 2) - - # Generate random 256-bit d (LSB-first binary string) - d = _random_bits(256) - k_str = _k_bits(k_val) - - # Compute G(d, k): SHA3-512 of d||k (both LSB-first binary) - expected_bits = SHA_3.G(d, k_str) - - # rho = first 256 bits, sigma = next 256 bits - rho_bits = expected_bits[0:256] - sigma_bits = expected_bits[256:512] - - # Convert to hex (MSB-first) - d_hex = _bits_to_hex(d, 256) - rho_hex = _bits_to_hex(rho_bits, 256) - sigma_hex = _bits_to_hex(sigma_bits, 256) - - return { - 'input': { - 'd': d_hex - }, - 'expected': { - 'rho': rho_hex, - 'sigma': sigma_hex - } - } - - def write_hex_file(self, vectors: list[dict], filepath: str) -> None: - """Write input vectors: one 64-char d-hex per line. - - Args: - vectors: List of vector dicts from generate_one(). - filepath: Path to write the hex file. - """ - os.makedirs(os.path.dirname(filepath), exist_ok=True) - with open(filepath, 'w') as f: - for v in vectors: - d_hex = v['input']['d'] - f.write(f'{d_hex}\n') - - def write_expected_file(self, vectors: list[dict], filepath: str) -> None: - """Write expected output: "RHO_HEX SIGMA_HEX" per line (128 chars). - - Args: - vectors: List of vector dicts from generate_one(). - filepath: Path to write the expected hex file. - """ - os.makedirs(os.path.dirname(filepath), exist_ok=True) - with open(filepath, 'w') as f: - for v in vectors: - rho_hex = v['expected']['rho'] - sigma_hex = v['expected']['sigma'] - f.write(f'{rho_hex} {sigma_hex}\n') diff --git a/test_framework/modules/sha3_chain/test_plan.json b/test_framework/modules/sha3_chain/test_plan.json deleted file mode 100644 index 1b32101..0000000 --- a/test_framework/modules/sha3_chain/test_plan.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "module": "sha3_chain", - "rtl_top": "sync_rtl/sha3_chain/sha3_chain_top.v", - "rtl_deps": ["sync_rtl/sha3/keccak_round.v", "sync_rtl/sha3/keccak_core.v", "sync_rtl/sha3/sha3_top.v"], - "tb_cpp": "sync_rtl/sha3_chain/TB/tb_sha3_chain.cpp", - "timeout_s": 60, - "cases": [{ - "id": "basic", - "description": "d → SHA3_G(d||k=2) → rho, sigma vs Python G()", - "params": {"k": 2}, - "num_vectors": 3, - "tolerance": "bit_exact" - }] -}