Files
mlkem-sync/sync_rtl/top/TB/gen_vectors.py
FallenSigh 0e6798beb5 feat(tb): add KAT testbench for mlkem_top (ML-KEM-512)
- gen_vectors.py: parse kat_MLKEM_512.rsp, generate hex vectors
- tb_mlkem_top_xsim.v: force-inject d/msg/z for KAT testing
- mlkem_top_input.hex: 5 vectors (d + msg + z)
- mlkem_top_expected.hex: 5 vectors (pk + sk + ct + ss)
- xsim_run.tcl: full dependency chain compilation

Known issue: mlkem_top FSM has combinational race on rng_valid_i
- rng_valid_i driven by state_r (registered) causes rng_sync
  to miss valid_i pulse when state transitions at posedge
- Fix: change rng_valid_i to use state_next pattern
  (same as sha3_top uses state_next for kc_valid_i)
2026-06-27 01:07:34 +08:00

166 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""gen_vectors.py - Parse FIPS 203 KAT test vectors for ML-KEM-512.
Reads kat_MLKEM_512.rsp and generates hex vector files for the
mlkem_top XSIM testbench.
Output files (written to vectors/ subdirectory):
mlkem_top_input.hex - d (256b) + msg (256b) + z (256b) per line
mlkem_top_expected.hex - pk (6400b) + sk (13056b) + ct (6144b) + ss (256b)
KAT file format (per test vector):
count = N
z = <64 hex chars> → 32 bytes, implicit rejection seed
d = <64 hex chars> → 32 bytes, KeyGen randomness
msg = <64 hex chars> → 32 bytes, Encaps message
seed = <128 hex chars> → DRBG seed (ignored)
pk = <1600 hex chars> → 800 bytes, public key
sk = <3264 hex chars> → 1632 bytes, secret key
ct_n = <hex> → invalid ciphertext (ignored)
ss_n = <hex> → shared secret from invalid ct (ignored)
ct = <1536 hex chars> → 768 bytes, ciphertext
ss = <64 hex chars> → 32 bytes, shared secret
"""
import os
import re
import sys
KAT_PATH = "/home/fallensigh/Dev/ml-kem-r/test_data/kat_MLKEM_512.rsp"
OUT_DIR = os.path.join(os.path.dirname(__file__), "vectors")
NUM_VECTORS = 5
def parse_kat(filepath: str, num_vectors: int = NUM_VECTORS) -> list[dict]:
"""Parse KAT .rsp file and return list of test vector dicts."""
vectors: list[dict] = []
current: dict = {}
with open(filepath, "r") as f:
for line in f:
line = line.strip()
# New vector starts with "count = N"
m = re.match(r"^count\s*=\s*(\d+)$", line)
if m:
if current and "count" in current:
vectors.append(current)
current = {}
idx = int(m.group(1))
if idx >= num_vectors:
break
current = {"count": idx}
continue
# Parse key = value lines
m = re.match(r"^(\w+)\s*=\s*([0-9a-fA-F]+)$", line)
if m and current:
key = m.group(1)
value = m.group(2).lower()
current[key] = value
continue
# Add last vector if present
if current and "count" in current:
vectors.append(current)
# Validate each vector has required fields
required = {"d", "z", "msg", "pk", "sk", "ct", "ss"}
for v in vectors:
missing = required - set(v.keys())
if missing:
print(f"WARNING: count={v['count']} missing fields: {missing}",
file=sys.stderr)
return vectors
def verify_lengths(vectors: list[dict]) -> bool:
"""Verify hex field lengths match FIPS 203 expected sizes."""
expected = {
"d": 64, # 32 bytes
"z": 64, # 32 bytes
"msg": 64, # 32 bytes
"pk": 1600, # 800 bytes
"sk": 3264, # 1632 bytes
"ct": 1536, # 768 bytes
"ss": 64, # 32 bytes
}
ok = True
for v in vectors:
for field, elen in expected.items():
if field not in v:
continue
actual = len(v[field])
if actual != elen:
print(f"WARNING: count={v['count']} {field}: "
f"expected {elen} hex chars, got {actual}",
file=sys.stderr)
ok = False
return ok
def write_input_hex(vectors: list[dict], out_dir: str) -> str:
"""Write input vectors: d || msg || z (768 bits = 192 hex chars each)."""
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, "mlkem_top_input.hex")
with open(out_path, "w") as f:
for v in vectors:
d = v.get("d", "0" * 64)
msg = v.get("msg", "0" * 64)
z = v.get("z", "0" * 64)
# Concatenate: [d:255:0][msg:255:0][z:255:0]
f.write(f"{d}{msg}{z}\n")
print(f"Wrote {len(vectors)} input vectors to {out_path}")
return out_path
def write_expected_hex(vectors: list[dict], out_dir: str) -> str:
"""Write expected output vectors: pk || sk || ct || ss.
pk: 800 bytes = 1600 hex chars
sk: 1632 bytes = 3264 hex chars
ct: 768 bytes = 1536 hex chars
ss: 32 bytes = 64 hex chars
Total: 3232 bytes = 6464 hex chars per line
"""
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, "mlkem_top_expected.hex")
with open(out_path, "w") as f:
for v in vectors:
pk = v.get("pk", "0" * 1600)
sk = v.get("sk", "0" * 3264)
ct = v.get("ct", "0" * 1536)
ss = v.get("ss", "0" * 64)
f.write(f"{pk}{sk}{ct}{ss}\n")
print(f"Wrote {len(vectors)} expected vectors to {out_path}")
return out_path
def main():
vectors = parse_kat(KAT_PATH, NUM_VECTORS)
print(f"Parsed {len(vectors)} test vectors from KAT file")
if len(vectors) == 0:
print("ERROR: No vectors parsed!", file=sys.stderr)
sys.exit(1)
verify_lengths(vectors)
# Print summary
for v in vectors:
print(f" count={v['count']}: "
f"d={v.get('d', 'N/A')[:8]}... "
f"msg={v.get('msg', 'N/A')[:8]}... "
f"z={v.get('z', 'N/A')[:8]}... "
f"ss={v.get('ss', 'N/A')[:8]}...")
write_input_hex(vectors, OUT_DIR)
write_expected_hex(vectors, OUT_DIR)
print("Done.")
if __name__ == "__main__":
main()