Extend mlkem_top with a runtime op_i select (0=KeyGen, 1=Encaps) and the
first Encaps stages, reusing the shared keccak_core and the ST_H multi-block
SHA3-256 machinery:
ST_ENC_H: H(ek) over preloaded ek_bram (same FSM as KeyGen ST_H)
ST_ENC_G: (K,r) = G(m||H(ek)) via new 64-byte single-block SHA3-512
- sha3_top_shared: add mode=2'b11 = SHA3-512 over a full 512-bit message
(g512_pad). Standalone tb_sha3_g512 confirms it byte-exact.
- mlkem_top: new ports op_i, msg_i, ek_in_{we,addr,byte} (ek preload), ss_o,
dbg_ct_*, dbg_r_o/dbg_hek_o. st widened 4->5 bits; ST_ENC_* states added.
Renamed message port to msg_i to avoid collision with ST_M counter m_i.
- TB tb_mlkem_enc_katK + gen_encaps_vectors.py (per-byte ek/m/ct/ss vectors).
Verified ss==KAT.ss for K=2/3/4, cases 0-2 (all PASS). KeyGen unaffected
(K=2 c0 still ek==pk, dk==sk byte-exact).
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""gen_encaps_vectors.py - Emit per-byte Encaps KAT vectors for the XSIM TB.
|
|
|
|
For ML-KEM-512/768/1024, parse the NIST .rsp and write, per case:
|
|
enc_k{K}_c{N}_ek.hex : ek (=pk) bytes, one hex byte per line, byte 0 first
|
|
enc_k{K}_c{N}_m.hex : msg bytes, one per line, byte 0 first
|
|
enc_k{K}_c{N}_ct.hex : ct bytes, one per line, byte 0 first (golden)
|
|
enc_k{K}_c{N}_ss.hex : ss bytes, one per line, byte 0 first (golden)
|
|
|
|
Per-byte layout removes all endianness ambiguity: the TB preloads ek_bram with
|
|
addr i = line i, builds m_i[8*i +: 8] = m byte i, and compares ct/ss byte-wise.
|
|
|
|
Run: python3 gen_encaps_vectors.py [num_cases]
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ML_KEM_R = os.environ.get("ML_KEM_R", os.path.expanduser("~/Dev/ml-kem-r"))
|
|
OUT_DIR = os.path.join(os.path.dirname(__file__), "vectors")
|
|
KATS = {2: "kat_MLKEM_512.rsp", 3: "kat_MLKEM_768.rsp", 4: "kat_MLKEM_1024.rsp"}
|
|
|
|
|
|
def parse_kat(path, n):
|
|
"""Return list of dicts {count, msg, pk, ct, ss} (hex strings) for first n."""
|
|
vecs, cur = [], {}
|
|
with open(path) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
m = re.match(r"^count\s*=\s*(\d+)$", line)
|
|
if m:
|
|
if cur:
|
|
vecs.append(cur)
|
|
if len(vecs) >= n:
|
|
break
|
|
cur = {"count": int(m.group(1))}
|
|
continue
|
|
# exact-key match so "ct =" doesn't catch "ct_n ="
|
|
m = re.match(r"^(msg|pk|ct|ss)\s*=\s*([0-9a-fA-F]+)$", line)
|
|
if m and cur:
|
|
cur[m.group(1)] = m.group(2).lower()
|
|
if cur and len(vecs) < n:
|
|
vecs.append(cur)
|
|
return vecs
|
|
|
|
|
|
def write_bytes(path, hexstr):
|
|
"""Write hex string as one byte per line (byte 0 = first 2 hex chars)."""
|
|
with open(path, "w") as f:
|
|
for i in range(0, len(hexstr), 2):
|
|
f.write(hexstr[i:i + 2] + "\n")
|
|
|
|
|
|
def main():
|
|
ncases = int(sys.argv[1]) if len(sys.argv) > 1 else 3
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
for k, fname in KATS.items():
|
|
path = os.path.join(ML_KEM_R, "test_data", fname)
|
|
if not os.path.exists(path):
|
|
print(f"skip K={k}: {path} not found", file=sys.stderr)
|
|
continue
|
|
vecs = parse_kat(path, ncases)
|
|
for v in vecs:
|
|
n = v["count"]
|
|
base = os.path.join(OUT_DIR, f"enc_k{k}_c{n}")
|
|
write_bytes(f"{base}_ek.hex", v["pk"])
|
|
write_bytes(f"{base}_m.hex", v["msg"])
|
|
write_bytes(f"{base}_ct.hex", v["ct"])
|
|
write_bytes(f"{base}_ss.hex", v["ss"])
|
|
print(f"K={k}: wrote {len(vecs)} cases "
|
|
f"(ek={len(vecs[0]['pk'])//2}B ct={len(vecs[0]['ct'])//2}B)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|