Files
mlkem-sync/sync_rtl/top/TB/xcheck_mlkemr.py
FallenSigh c4669480d1 test(top): add ml-kem-r cross-validation script (xcheck_mlkemr.py)
Cross-checks the hardware KAT vectors against the ml-kem-r Rust reference's
NIST .rsp files, accounting for the d/z byte-order convention: the hardware
hashes G(reverse(d)||K), so its vector files store d/z reversed vs the NIST
.rsp. The script applies that reversal for d/z and compares ek==pk, dk==sk
directly; 11/11 cases consistent.

ml-kem-r path via $ML_KEM_R (default ~/Dev/ml-kem-r); friendly errors on
missing files. Documents the convention that tripped up logging.rs (which
pasted hardware-order d into the literal-order Rust reference).
2026-06-28 23:54:08 +08:00

119 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
# xcheck_mlkemr.py - Cross-validate the hardware ML-KEM KeyGen KAT vectors
# against the ml-kem-r Rust reference's NIST .rsp files.
#
# WHY THIS SCRIPT EXISTS (byte-order convention):
# The hardware feeds G(reverse(d) || K), i.e. it byte-reverses the d/z seeds
# before hashing. Consequently the hardware vector files
# sync_rtl/top/TB/vectors/kat_k{K}_c{C}_{d,z}.hex
# store d/z in the REVERSE byte order of the NIST .rsp `d`/`z` fields. Both
# conventions hash to the SAME pk/sk (reverse(reverse(d)) == d), so both the
# hardware KAT and ml-kem-r's own KAT pass -- they just disagree on how d/z
# are written down. A naive byte-for-byte compare of d/z therefore "fails"
# even though the implementations are equivalent. This script compares with
# the reversal applied, and compares ek/dk (== pk/sk) directly.
#
# See also: the TB d/z dump in tb_mlkem_kg_katK_xsim.v and the keygen G-hash
# reversal in sync_rtl/top/mlkem_top.v.
#
# Usage:
# python3 sync_rtl/top/TB/xcheck_mlkemr.py
# ML_KEM_R=/path/to/ml-kem-r python3 sync_rtl/top/TB/xcheck_mlkemr.py
#
# Exit code 0 = all consistent, 1 = discrepancy / missing files.
import os
import sys
# Hardware vector dir (relative to repo root, where this script's ../../.. is).
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HW_DIR = SCRIPT_DIR # vectors live under SCRIPT_DIR/vectors
# ml-kem-r location: env override, else common sibling path.
ML_KEM_R = os.environ.get("ML_KEM_R", os.path.expanduser("~/Dev/ml-kem-r"))
RSP = {
2: "test_data/kat_MLKEM_512.rsp",
3: "test_data/kat_MLKEM_768.rsp",
4: "test_data/kat_MLKEM_1024.rsp",
}
# Cases present in the hardware vector set per K (K=2 has 0..4, K=3/4 have 0..2).
CASES = {2: range(5), 3: range(3), 4: range(3)}
def die(msg):
print(f"ERROR: {msg}", file=sys.stderr)
sys.exit(1)
def rsp_cases(path):
"""Parse a NIST .rsp into a list of dicts keyed by count."""
if not os.path.isfile(path):
die(f"ml-kem-r .rsp not found: {path}\n"
f" set ML_KEM_R=/path/to/ml-kem-r (current: {ML_KEM_R})")
cur, out = None, []
with open(path) as f:
for ln in f:
ln = ln.strip()
if ln.startswith("count = "):
if cur is not None:
out.append(cur)
cur = {"count": int(ln[8:])}
elif " = " in ln and cur is not None:
k, v = ln.split(" = ", 1)
cur[k] = v.strip().lower()
if cur is not None:
out.append(cur)
return out
def hwcat(k, c, which):
"""Concatenate a hardware .hex vector file into one lowercase hex string."""
path = os.path.join(HW_DIR, "vectors", f"kat_k{k}_c{c}_{which}.hex")
if not os.path.isfile(path):
die(f"hardware vector not found: {path}")
with open(path) as f:
return "".join(f.read().split()).lower()
def revhex(h):
"""Byte-reverse a hex string."""
return bytes.fromhex(h)[::-1].hex()
def main():
print(f"ml-kem-r: {ML_KEM_R}")
print(f"hardware: {os.path.join(HW_DIR, 'vectors')}")
print("convention: hardware d/z == reverse(NIST d/z); ek==pk, dk==sk\n")
all_ok = True
total = 0
for k in (2, 3, 4):
cases = rsp_cases(os.path.join(ML_KEM_R, RSP[k]))
bycount = {c["count"]: c for c in cases}
for c in CASES[k]:
total += 1
hd, hz = hwcat(k, c, "d"), hwcat(k, c, "z")
hek, hdk = hwcat(k, c, "ek"), hwcat(k, c, "dk")
rc = bycount.get(c)
if rc is None:
die(f"ml-kem-r .rsp for K={k} has no count={c}")
d_ok = revhex(hd) == rc["d"]
z_ok = revhex(hz) == rc["z"]
ek_ok = hek == rc["pk"]
dk_ok = hdk == rc["sk"]
case_ok = d_ok and z_ok and ek_ok and dk_ok
all_ok = all_ok and case_ok
print(f"K={k} c{c}: d(rev)={'Y' if d_ok else 'N'} "
f"z(rev)={'Y' if z_ok else 'N'} "
f"ek={'Y' if ek_ok else 'N'} "
f"dk={'Y' if dk_ok else 'N'} -> "
f"{'OK' if case_ok else 'MISMATCH'}")
print(f"\n{total} cases checked: "
f"{'ALL CONSISTENT' if all_ok else 'DISCREPANCY FOUND'}")
sys.exit(0 if all_ok else 1)
if __name__ == "__main__":
main()