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)
This commit is contained in:
165
sync_rtl/top/TB/gen_vectors.py
Normal file
165
sync_rtl/top/TB/gen_vectors.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user