feat(sha3): implement synchronous Keccak-f[1600] core with G/H/J modes
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()
This commit is contained in:
195
test_framework/modules/sha3/gen_vectors.py
Normal file
195
test_framework/modules/sha3/gen_vectors.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""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')
|
||||
31
test_framework/modules/sha3/test_plan.json
Normal file
31
test_framework/modules/sha3/test_plan.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"module": "sha3",
|
||||
"rtl_top": "sync_rtl/sha3/sha3_top.v",
|
||||
"rtl_deps": ["sync_rtl/sha3/keccak_core.v", "sync_rtl/sha3/keccak_round.v"],
|
||||
"tb_cpp": "sync_rtl/sha3/TB/tb_sha3.cpp",
|
||||
"simulator": "verilator",
|
||||
"timeout_s": 120,
|
||||
"cases": [
|
||||
{
|
||||
"id": "g_basic",
|
||||
"description": "SHA3-512 G(d||k): 64B input → 64B output, verified against Python reference",
|
||||
"params": {"mode": "G", "k": 2},
|
||||
"num_vectors": 10,
|
||||
"tolerance": "bit_exact"
|
||||
},
|
||||
{
|
||||
"id": "h_basic",
|
||||
"description": "SHA3-256 H(ek): 32B+ output, single-block",
|
||||
"params": {"mode": "H", "k": 2},
|
||||
"num_vectors": 10,
|
||||
"tolerance": "bit_exact"
|
||||
},
|
||||
{
|
||||
"id": "j_basic",
|
||||
"description": "SHAKE-256 J(z||c): variable input → 32B output",
|
||||
"params": {"mode": "J"},
|
||||
"num_vectors": 5,
|
||||
"tolerance": "bit_exact"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user