Phase 1.1 of ML-KEM sync rewrite. - keccak_round.v: combinational theta/rho/pi/chi/iota - keccak_core.v: 24-round pipeline, valid/ready - sha3_top.v: sponge FSM, modes G(SHA3-512)/H(SHA3-256)/J(SHAKE-256) - Verilator C++ TB + Python vector gen against reference - Verified: 25/25 vectors bit-exact vs Python G()/H()/J()
196 lines
6.0 KiB
Python
196 lines
6.0 KiB
Python
"""gen_vectors.py - Test vector generator for sha3 module.
|
|
|
|
Generates vectors for G (SHA3-512), H (SHA3-256), J (SHAKE-256) modes
|
|
and computes expected outputs using the Python reference implementation.
|
|
"""
|
|
|
|
import os
|
|
import random
|
|
import sys
|
|
|
|
# Add test_framework/lib to path for VectorGenerator base class
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib'))
|
|
|
|
# Add Python reference path
|
|
_REF_PATH = os.path.expanduser(
|
|
"~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0")
|
|
_REF_PATH = os.path.expanduser(_REF_PATH)
|
|
sys.path.insert(0, _REF_PATH)
|
|
|
|
import SHA_3
|
|
|
|
from vector_gen import VectorGenerator
|
|
|
|
|
|
def _bits_to_hex(bits_str, num_bits):
|
|
"""Convert binary string (LSB-first: bits_str[0] = bit 0) to hex.
|
|
|
|
Returns hex with MSB-nibble first (matching Verilog %%X format).
|
|
Each group of 4 bits forms a nibble: bit[i+3:i] → two hex chars.
|
|
"""
|
|
result = []
|
|
for i in range(0, num_bits, 4):
|
|
nib = 0
|
|
for j in range(4):
|
|
pos = i + j
|
|
if pos < len(bits_str) and bits_str[pos] == '1':
|
|
nib |= (1 << j)
|
|
result.insert(0, '0123456789ABCDEF'[nib])
|
|
return ''.join(result)
|
|
|
|
|
|
def _hex_to_bits(hex_str, num_bits):
|
|
"""Convert hex string (MSB-first) to binary string (LSB-first).
|
|
|
|
Returns a binary string where result[0] = bit 0 (LSB).
|
|
"""
|
|
hex_val = int(hex_str, 16)
|
|
bits = ''
|
|
for i in range(num_bits):
|
|
bits += '1' if (hex_val & (1 << i)) else '0'
|
|
return bits
|
|
|
|
|
|
def _random_bits(length):
|
|
"""Generate a random binary string of given length."""
|
|
val = random.getrandbits(length)
|
|
bits = ''
|
|
for i in range(length):
|
|
bits += '1' if (val & (1 << i)) else '0'
|
|
return bits
|
|
|
|
|
|
class Sha3VectorGenerator(VectorGenerator):
|
|
"""Generates test vectors for the sha3_top module."""
|
|
|
|
def generate_one(self, params: dict) -> dict:
|
|
"""Generate a single test vector.
|
|
|
|
For G mode:
|
|
- d: 256-bit random binary string
|
|
- k: 8-bit random binary string
|
|
- Input to RTL: d||k packed into 512-bit value (data_i[263:0])
|
|
- Expected: G(d, k) = 512-bit binary string
|
|
|
|
For H mode:
|
|
- ek: 256-bit random binary string (single-block)
|
|
- Input to RTL: ek in data_i[255:0]
|
|
- Expected: H(ek) = 256-bit binary string
|
|
|
|
For J mode:
|
|
- z: 256-bit random binary string
|
|
- c: 256-bit random binary string
|
|
- Input to RTL: z||c in data_i[511:0]
|
|
- Expected: J(z, c) = 256-bit binary string
|
|
|
|
Args:
|
|
params: dict with 'mode' key ('G', 'H', 'J').
|
|
|
|
Returns:
|
|
dict with 'input' and 'expected' keys.
|
|
"""
|
|
mode = params.get('mode', 'G')
|
|
|
|
if mode == 'G':
|
|
# d: 256 bits, k: 8 bits
|
|
d = _random_bits(256)
|
|
k = _random_bits(8)
|
|
msg = d + k # 264 bits, LSB first
|
|
|
|
# Pack into 512-bit hex value: data_i[511:0]
|
|
# pad msg to 512 bits with zeros (upper bits = 0)
|
|
msg_512 = msg + '0' * (512 - len(msg))
|
|
input_hex = _bits_to_hex(msg_512, 512)
|
|
|
|
# Expected: G(d, k) returns binary string
|
|
expected_bits = SHA_3.G(d, k)
|
|
expected_hex = _bits_to_hex(expected_bits, 512)
|
|
|
|
return {
|
|
'input': {
|
|
'mode': 0,
|
|
'data': input_hex
|
|
},
|
|
'expected': {
|
|
'hash': expected_hex
|
|
}
|
|
}
|
|
|
|
elif mode == 'H':
|
|
# ek: 256-bit random message (single-block test)
|
|
ek = _random_bits(256)
|
|
|
|
# Pack into 512-bit hex: data_i[511:0]
|
|
msg_512 = ek + '0' * (512 - len(ek))
|
|
input_hex = _bits_to_hex(msg_512, 512)
|
|
|
|
# Expected: H(ek) returns 256-bit binary string
|
|
expected_bits = SHA_3.H(ek)
|
|
expected_hex = _bits_to_hex(expected_bits, 256)
|
|
|
|
return {
|
|
'input': {
|
|
'mode': 1,
|
|
'data': input_hex
|
|
},
|
|
'expected': {
|
|
'hash': expected_hex
|
|
}
|
|
}
|
|
|
|
elif mode == 'J':
|
|
# z: 256 bits, c: 256 bits
|
|
z = _random_bits(256)
|
|
c = _random_bits(256)
|
|
msg = z + c # 512 bits
|
|
|
|
# Pack into 512-bit hex
|
|
input_hex = _bits_to_hex(msg, 512)
|
|
|
|
# Expected: J(z, c) returns 256-bit binary string
|
|
expected_bits = SHA_3.J(z, c)
|
|
expected_hex = _bits_to_hex(expected_bits, 256)
|
|
|
|
return {
|
|
'input': {
|
|
'mode': 2,
|
|
'data': input_hex
|
|
},
|
|
'expected': {
|
|
'hash': expected_hex
|
|
}
|
|
}
|
|
|
|
else:
|
|
raise ValueError(f"Unknown mode: {mode}")
|
|
|
|
def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
|
|
"""Write input vectors as "MM DDDD..." hex format (mode, data).
|
|
|
|
Each line: "MM DDDD..." where MM is hex mode (00/01/10) and DDDD...
|
|
is the hex-encoded 512-bit message.
|
|
|
|
Args:
|
|
vectors: List of vector dicts from generate_one().
|
|
filepath: Path to write the hex file.
|
|
"""
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
|
with open(filepath, 'w') as f:
|
|
for v in vectors:
|
|
mode_val = v['input']['mode']
|
|
data_hex = v['input']['data']
|
|
f.write(f'{mode_val:02X} {data_hex}\n')
|
|
|
|
def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
|
|
"""Write expected output as hex strings, one per line.
|
|
|
|
Args:
|
|
vectors: List of vector dicts from generate_one().
|
|
filepath: Path to write the expected hex file.
|
|
"""
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
|
with open(filepath, 'w') as f:
|
|
for v in vectors:
|
|
expected_hex = v['expected']['hash']
|
|
f.write(f'{expected_hex}\n')
|