#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Replay the VM's BLAKE2s-style compression core for xfalcon.""" from __future__ import annotations import glob import re import struct import sys from pathlib import Path MASK32 = 0xFFFFFFFF SIGMA = [ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], ] SAMPLE_V0 = [ 0xA82057D6, 0xA9A24FF4, 0x9138D3FD, 0x2A2193F3, 0x2ADEF3F4, 0x9876EF16, 0x9ABED34F, 0x9103DE12, 0xA92157F6, 0xA9A24FF4, 0x9138D3FD, 0x2A2193F3, 0x2ADEF3D5, 0x9876EF16, 0x65412CB0, 0x9103DE12, ] SAMPLE_M = [ 0x0C5B0466, 0x04580152, 0x51500555, 0x00565451, 0x36753729, 0x5B72176D, 0x070A1F60, 0x7C662341, 0x682F4A37, 0x26562F2D, 0x47573177, 0x540A115A, 0x715D5A79, 0x061C581D, 0x6D0E0D1C, 0x0F230801, ] SAMPLE_V_FINAL = [ 0x296C7D05, 0xA2E09447, 0x62AB2823, 0xB3FD4E92, 0xF96DE833, 0xA5517BB8, 0xBEBB2C06, 0xFAD21B26, 0x2929349D, 0xF418B33C, 0x7325D323, 0xB446F83B, 0x04D647B0, 0x11FB17F9, 0x0CD935BC, 0x858F3D9F, ] SAMPLE_DIGEST = [ 0xA8651E4E, 0xFF5A688F, 0x80B628FD, 0x2D9A255A, 0xD7655C77, 0x2CDC8357, 0x28DCCAF5, 0xEE5EF8AB, ] def ror32(value: int, count: int) -> int: value &= MASK32 count &= 31 return ((value >> count) | ((value << (32 - count)) & MASK32)) & MASK32 def compress(v0: list[int], m: list[int]) -> list[int]: v = [x & MASK32 for x in v0] def g(a: int, b: int, c: int, d: int, x: int, y: int) -> None: v[a] = (v[a] + v[b] + x) & MASK32 v[d] = ror32(v[d] ^ v[a], 16) v[c] = (v[c] + v[d]) & MASK32 v[b] = ror32(v[b] ^ v[c], 12) v[a] = (v[a] + v[b] + y) & MASK32 v[d] = ror32(v[d] ^ v[a], 8) v[c] = (v[c] + v[d]) & MASK32 v[b] = ror32(v[b] ^ v[c], 7) for s in SIGMA: g(0, 4, 8, 12, m[s[0]], m[s[1]]) g(1, 5, 9, 13, m[s[2]], m[s[3]]) g(2, 6, 10, 14, m[s[4]], m[s[5]]) g(3, 7, 11, 15, m[s[6]], m[s[7]]) g(0, 5, 10, 15, m[s[8]], m[s[9]]) g(1, 6, 11, 12, m[s[10]], m[s[11]]) g(2, 7, 8, 13, m[s[12]], m[s[13]]) g(3, 4, 9, 14, m[s[14]], m[s[15]]) return v def finalize_digest(v0: list[int], v_final: list[int]) -> list[int]: return [(v0[i] ^ v_final[i] ^ v_final[i + 8]) & MASK32 for i in range(8)] def words_hex(words: list[int]) -> str: return " ".join(f"{x & MASK32:08x}" for x in words) def digest_hex(words: list[int]) -> str: return "".join((x & MASK32).to_bytes(4, "big").hex() for x in words) def newest_log() -> Path: logs = sorted(glob.glob("out/xfalcon_blake_params_*.log")) if not logs: raise SystemExit("no xfalcon_blake_params log found") return Path(logs[-1]) def collect_dump(text: str, name: str, base_rel: int) -> bytearray: buf = bytearray() pat = rf"\[BLK\]\[dump\.{re.escape(name)}\] rel=0x([0-9a-f]+).*? hex=([0-9a-f]+)" for m in re.finditer(pat, text): rel = int(m.group(1), 16) off = rel - base_rel data = bytes.fromhex(m.group(2)) if off < 0: continue if len(buf) < off + len(data): buf.extend(b"\x00" * (off + len(data) - len(buf))) buf[off : off + len(data)] = data return buf def qwords_low32(buf: bytes, off: int, count: int) -> list[int]: return [struct.unpack_from(" tuple[list[int], list[int], list[int], list[int]]: text = path.read_text(encoding="utf-8", errors="replace") work = collect_dump(text, "work_2e00_at_compress", 0x2E00) if len(work) < 0x228: raise SystemExit("log missing work_2e00_at_compress dump") v0 = qwords_low32(work, 0x2E28 - 0x2E00, 16) m_words = qwords_low32(work, 0x2FA8 - 0x2E00, 16) table: dict[int, int] = {} for sm in re.finditer(r"\[BLK\]\[store\].*?rel=0x([0-9a-f]+).*?val=0x([0-9a-f]+)", text): rel = int(sm.group(1), 16) if 0x2E28 <= rel < 0x2EA8 and (rel - 0x2E28) % 8 == 0: table[(rel - 0x2E28) // 8] = int(sm.group(2), 16) & MASK32 if len(table) < 16: raise SystemExit(f"log missing final table stores: got {len(table)}") v_final = [table[i] for i in range(16)] digest: dict[int, int] = {} for sm in re.finditer(r"\[BLK\]\[store\].*?rel=0x([0-9a-f]+).*?val=0x([0-9a-f]+)", text): rel = int(sm.group(1), 16) if 0x103A68 <= rel < 0x103AA8 and (rel - 0x103A68) % 8 == 0: digest[(rel - 0x103A68) // 8] = int(sm.group(2), 16) & MASK32 digest_words = [digest[i] for i in range(8)] if len(digest) >= 8 else [] return v0, m_words, v_final, digest_words def main() -> int: if len(sys.argv) > 1: path = Path(sys.argv[1]) else: path = newest_log() if path.exists(): v0, m_words, expected_v, expected_digest = parse_log(path) print(f"log={path}") else: v0, m_words, expected_v, expected_digest = ( SAMPLE_V0, SAMPLE_M, SAMPLE_V_FINAL, SAMPLE_DIGEST, ) print("using built-in sample") got_v = compress(v0, m_words) got_digest = finalize_digest(v0, got_v) print("v0: " + words_hex(v0)) print("m: " + words_hex(m_words)) print("v_calc: " + words_hex(got_v)) print("v_expect:" + words_hex(expected_v)) print("digest: " + words_hex(got_digest)) print("hex: " + digest_hex(got_digest)) ok_v = got_v == expected_v ok_d = not expected_digest or got_digest == expected_digest if ok_v and ok_d: print("[OK] xfalcon blake core replay verified") return 0 print(f"[FAIL] v_ok={ok_v} digest_ok={ok_d}") return 1 if __name__ == "__main__": raise SystemExit(main())