- 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)
166 lines
5.3 KiB
Python
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()
|