test: support 1000 KAT Vectors

This commit is contained in:
2026-07-11 19:48:17 +08:00
parent ba160cca34
commit 961f25f50e
5 changed files with 1661 additions and 1202 deletions

View File

@@ -1,34 +1,22 @@
#!/usr/bin/env python3
"""gen_vectors.py - Test vector generator for sample_ntt_sync module.
"""Generate FIPS 203 SampleNTT test vectors for sample_ntt_sync.
Generates random rho+k+i+j test vectors, computes expected coefficients using
Keccak-p[1600, 24] matching the RTL's per-permutation squeeze pattern.
The reference oracle is hashlib.shake_128 (FIPS 202). It samples
SHAKE128(rho || j || i) by reading three bytes at a time and accepting
d1 = b0 + 256 * (b1 mod 16) and d2 = floor(b1 / 16) + 16 * b2 when d < Q.
The RTL sample_ntt_sync absorbs rho || j || i, then repeatedly permutes
the keccak state, extracting d1,d2 from bits [23:0] of each permuted state
with rejection sampling (d < Q=3329).
Algorithm (matching RTL exactly):
1. Build absorb state: pad10*1 padding for SHAKE-128 (rate=1344) on msg(272b)
2. state = keccak_p(absorb_state)
3. Extract d1 = (state[11:8] << 8) | state[7:0]
d2 = (state[23:16] << 4) | state[15:12]
4. If d1 < Q: output d1 (12-bit unsigned)
5. If d2 < Q: output d2
6. state = keccak_p(state)
7. Repeat 3-6 until 256 coefficients collected
Bit ordering (FIPS 202 / RTL match):
The RTL feeds rho_i[0] as the first message bit into SHA3.
$readmemh stores hex MSB-first → rho_i[255:0] in MSB-first order.
Python: input bytes = reversed(bytes.fromhex(rho_hex)) + bytes([j, i])
The RTL feeds rho_i[0] as the first message byte. $readmemh loads
rho_i[255:0] MSB-first, so Python passes
reversed(bytes.fromhex(rho_hex)) + bytes([j, i]) to SHAKE-128.
Usage:
python3 gen_vectors.py # Generate vectors
python3 gen_vectors.py --verify # Verify results against expected
python3 gen_vectors.py --selftest # Run Keccak-p self-test
python3 gen_vectors.py --refresh-expected # Recompute checked-in oracle
python3 gen_vectors.py --selftest # Run Keccak-p self-test
"""
import hashlib
import os
import random
import sys
@@ -36,6 +24,15 @@ import sys
Q = 3329
N_COEFFS = 256
# This vector accepts its 256th coefficient as d2 from group 168, the final
# group of a SHAKE-128 rate block. It guards the no-extra-permutation boundary.
RATE_BOUNDARY_VECTOR = {
"rho_hex": "7484937160DBF5D93F7D69AEA2DF7C7C77B3A6B02D6B4D579CF975E6F8562097",
"k": 2,
"i": 1,
"j": 1,
}
# ======================================================================
# Keccak-p[1600, 24] implementation (stdlib only, matches RTL exactly)
# ======================================================================
@@ -269,10 +266,7 @@ def extract_d1_d2(state):
def sample_ntt_compute(rho_hex, k, i, j):
"""Compute expected coefficients matching RTL sample_ntt_sync.
The RTL absorbs rho || j || i, then repeatedly permutes,
extracting d1,d2 from each permuted state with rejection sampling.
"""Compute the FIPS 203 SampleNTT output for one matrix polynomial.
Args:
rho_hex: 64-char hex string.
@@ -283,22 +277,29 @@ def sample_ntt_compute(rho_hex, k, i, j):
Returns:
list of 256 integers in [0, Q-1].
"""
state = build_absorb_state(rho_hex, j, i)
state = keccak_p(state)
del k
msg = bytes.fromhex(rho_hex)[::-1] + bytes([j & 0xFF, i & 0xFF])
coeffs = []
out_len = 168
processed = 0
while len(coeffs) < N_COEFFS:
d1, d2 = extract_d1_d2(state)
if d1 < Q:
coeffs.append(d1)
if len(coeffs) == N_COEFFS:
break
if d2 < Q:
coeffs.append(d2)
if len(coeffs) == N_COEFFS:
break
# Re-permute for next squeeze
state = keccak_p(state)
stream = hashlib.shake_128(msg).digest(out_len)
for off in range(processed, len(stream) - 2, 3):
b0, b1, b2 = stream[off:off + 3]
d1 = b0 + 256 * (b1 & 0x0F)
d2 = (b1 >> 4) + 16 * b2
if d1 < Q:
coeffs.append(d1)
if len(coeffs) == N_COEFFS:
break
if d2 < Q:
coeffs.append(d2)
if len(coeffs) == N_COEFFS:
break
processed = out_len
out_len += 168
return coeffs
@@ -367,9 +368,7 @@ def write_expected_hex(vectors, filepath):
"""Write expected coefficients: one 12-bit hex value per line."""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w") as f:
for idx, v in enumerate(vectors):
f.write(f"# VECTOR_{idx} k={v['k']} i={v['i']} j={v['j']} "
f"rho={v['rho_hex']}\n")
for v in vectors:
for c in v["coeffs"]:
f.write(coeff_to_hex(c) + "\n")
@@ -424,7 +423,7 @@ def main():
if "--selftest" in sys.argv:
keccak_p_selftest()
print("Running quick smoke test on sample_ntt computation...")
print("Running FIPS SampleNTT reference self-test...")
rho = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"
coeffs = sample_ntt_compute(rho, k=4, i=0, j=0)
print(f" rho={rho[:16]}...: {len(coeffs)} coefficients")
@@ -467,14 +466,33 @@ def main():
else:
print("VERIFICATION FAILED")
sys.exit(1)
elif "--refresh-expected" in sys.argv:
if not os.path.exists(input_file):
print(f"ERROR: Input file not found: {input_file}")
sys.exit(1)
vectors = []
with open(input_file, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
if len(line) != 66:
raise ValueError(f"invalid packed input vector: {line}")
header = int(line[:2], 16)
vectors.append({
"rho_hex": line[2:],
"k": header & 0x7,
"i": (header >> 3) & 0x3,
"j": (header >> 5) & 0x3,
})
for v in vectors:
v["coeffs"] = sample_ntt_compute(v["rho_hex"], v["k"], v["i"], v["j"])
write_expected_hex(vectors, expected_file)
print(f"Wrote {len(vectors)} FIPS SampleNTT expected vectors to {expected_file}")
else:
# Generate mode — only input vectors (no expected outputs).
# The RTL uses per-permutation Keccak-p extraction that may not
# match standard SHAKE-128 bit-stream output. The TB performs a
# smoke test: 256 coefficients, each in [0, Q-1].
vector_count = 4
print(f"Generating {vector_count} random input vectors...")
print(f"(Expected outputs are NOT computed — TB uses smoke-test only.)")
vectors = []
for idx in range(vector_count):
@@ -490,8 +508,15 @@ def main():
})
print(f" Vector {idx}: k={k}, i={i}, j={j}, rho={rho_hex[:8]}...")
vectors.append(RATE_BOUNDARY_VECTOR.copy())
print(" Boundary vector: k=2, i=1, j=1 (256th coefficient is group 168 d2)")
write_input_hex(vectors, input_file)
for v in vectors:
v["coeffs"] = sample_ntt_compute(v["rho_hex"], v["k"], v["i"], v["j"])
write_expected_hex(vectors, expected_file)
print(f"Wrote {len(vectors)} vectors to {input_file}")
print(f"Wrote FIPS SampleNTT expected values to {expected_file}")
if __name__ == "__main__":