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.
This commit is contained in:
2026-06-27 17:23:28 +08:00
parent 5d86000231
commit 4d7ce69405
12 changed files with 318 additions and 295 deletions

17
.gitignore vendored
View File

@@ -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

View File

@@ -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 |

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,7 @@
615E530C77D5D834311E922DB99D5B7F1D57C7B08F029FD829914D3F4035FB730350FD00A852A5CCE9CFC79CF8C61384FEA115030E41750A6AE2CFE055D7976D
5A7DEC7ECB7A1217993059A6CBAF7754BF774311A936963A10568900EEE6DB3BDAB89C84A5650189B0E8B2526BE916FDA070A0AB2E33399590B87D315C55D5CB
BE71BF8A954846FB70D52DE11DCC189E25B6A931E63BD2A3BB546E4C02A8929500F1AE56BFAF3D61ACF85ECFA2B5D8E02778CBBED34D22552A371B4DE938ED6B
0000000000000000000000000000000000000000000000000000000000000000A63A98E0A2FB57548A6906708813766DA0F1E9ACF3831A6E77C4A56227E9473E
0000000000000000000000000000000000000000000000000000000000000000CFB62681BD3A993605FB6C80D93E8A308C5494EF0641F92C18F83002453A8FA2
0000000000000000000000000000000000000000000000000000000000000000B09665ECDF8E2934BC244DF46E36AF3B3DB2C18524300A896BE8837558BEE367
0000000000000000000000000000000000000000000000000000000000000000EB253D3B38BBF839B448BBC0149ED56E12E4EE72D31DE12D881994CA16AA0C2A

View File

@@ -0,0 +1,7 @@
00000000000000000000000000000000000000000000000000000000000000007DBC2AC0D13D719B37B4E2D4691951FF890A97854EF5D3A8957EF67A54978E26C9
0000000000000000000000000000000000000000000000000000000000000000C247885BF4E257CF39645D34C593047B7570D6ABBA300599D96171A950BB5027D5
0000000000000000000000000000000000000000000000000000000000000000B038CB3C2F0F7398474A3D68C4C90B777F42FA4C12B1FC8F70E1DAADF20755473C
010000000000000000000000000000000000000000000000000000000000000000F9D94DE0F1EF4D2CB73DCA0F437D2334320494E5D0F728D73F5275E342572FF9
0100000000000000000000000000000000000000000000000000000000000000008A1733CED3BC820A5D410AD00B0E4ACB0C260A8EBCD1B70FECF85B59256609CB
02CC2F7B1D60813526FB623E09F10D22635605046A66888277667DACCA02B420BBE795DC9A6DA171DA1DE082111568D55A8A27FE9B009275761098B44B484B3684
02E94BA9958BE80EDB47A9C83C07DBE09EA32CF4B1A59AD014938117DA74B87E9840E880E1A6F0C9E69C5E78D61A357D30ECC92FE5890DF046D93D94E87EA4ECB0

View File

@@ -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": {

View File

@@ -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')

View File

@@ -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"
}]
}