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

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