Files
mlkem-sync/test_framework/modules/sample_ntt/gen_vectors.py
FallenSigh 4d7ce69405 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.
2026-06-27 17:23:28 +08:00

169 lines
5.5 KiB
Python

"""gen_vectors.py - Test vector generator for sample_ntt module.
Generates random rho seeds, computes expected coefficients with a standard
FIPS 202 SHAKE-128 rejection sampler (hashlib), and writes hex files for
Verilator simulation.
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
import random
import sys
import hashlib
# 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."""
def generate_one(self, params: dict) -> dict:
"""Generate a single test vector.
Args:
params: dict with 'k' key (ML-KEM parameter, 2/3/4).
Returns:
dict with 'input' and 'expected' keys.
"""
k = params.get("k", 2)
# 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)
# 34-byte SampleNTT input: rho || j || i (FIPS 203 Alg 13/7)
seed = rho + bytes([j]) + bytes([i])
# Expected coefficients from a standard SHAKE-128 sampler
coeffs = _sample_ntt_ref(seed)
# rho hex: byte 0 first (matches TB parse_rho byte ordering)
rho_hex = rho.hex().upper()
return {
"input": {
"rho_hex": rho_hex,
"k": k,
"i": i,
"j": j,
},
"expected": {
"coeffs": list(coeffs), # 256 coefficients (0 <= c < 3329)
},
}
def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
"""Write input vectors as "RHO_HEX K_HEX I_HEX J_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:
inp = v["input"]
rho_hex = inp["rho_hex"]
k_hex = format(inp["k"], "X")
i_hex = format(inp["i"], "X")
j_hex = format(inp["j"], "X")
f.write(f"{rho_hex} {k_hex} {i_hex} {j_hex}\n")
def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
"""Write expected outputs: one coefficient per line (12-bit hex).
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:
coeffs = v["expected"]["coeffs"]
for c in coeffs:
f.write(f"{c:03X}\n")
def compare_results(self, got: list[str], expected_file: str) -> bool:
"""Compare RTL output against expected values.
Args:
got: List of hex result strings from simulation ("RESULT: XXX").
expected_file: Path to expected hex file.
Returns:
bool: True if all results match.
"""
with open(expected_file, "r") as f:
expected = [
line.strip()
for line in f
if line.strip() and not line.startswith("#")
]
if len(got) != len(expected):
print(
f" MISMATCH: got {len(got)} results, expected {len(expected)}"
)
return False
all_ok = True
for i, (g, e) in enumerate(zip(got, expected)):
if g.upper() != e.upper():
print(f" MISMATCH[{i}]: got={g}, expected={e}")
all_ok = False
if i >= 9: # Stop after 10 mismatches
print(f" ... too many mismatches, stopping comparison")
break
return all_ok
if __name__ == "__main__":
# Quick smoke test when run directly
gen = SampleNTTVectorGenerator()
for k_val in [2, 4]:
vec = gen.generate_one({"k": k_val})
rho = vec["input"]["rho_hex"]
i = vec["input"]["i"]
j = vec["input"]["j"]
coeffs = vec["expected"]["coeffs"]
print(f"k={k_val}, i={i}, j={j}: {len(coeffs)} coefficients")
print(f" rho[0:8]={rho[:8]}...")
print(f" coeffs[0:4]={coeffs[:4]}")
# Verify all coefficients < Q
assert all(0 <= c < 3329 for c in coeffs), "Coefficient out of range!"
print("Smoke test PASSED")