#!/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 = → invalid ciphertext (ignored) ss_n = → 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()