aere-research/kat/mlkem768_reference.py
Aere Network 6cb0140fae Republished from a clean root: the compiled artifact is gone from history, and the local line of work joins the sanitized public line
The public history carried kat/__pycache__/mlkem768_reference.cpython-314.pyc,
a compiled Python artifact embedding the operator's absolute local path. Text
secret scanners do not read compiled binaries, which is exactly how it slipped
through, and removing it from the tip would have left it reachable through the
old root commits. So this repository is republished from a single clean root.

This root also carries, from the previously unpublished line of work:
- corrected LICENSE year, LICENSING.md, VERIFY-POLICY.md, and
  CITATIONS-UNRESOLVED.md remeasured 2026-08-11 (101 paths, README aligned)
- O-018: run_consensus_verification.py ran 19 of 29 models and reported PASS;
  it now runs all 29, and computemarket_smt.py gains resolveByTimeout /
  reclaimUnsettled cases plus a negative control
- O-006: the word 'audited' removed from next to Bouncy Castle, twice, after a
  concurrent edit resurrected it
- O-014: prior art named and dated - Algorand's native falcon_verify shipped
  about ten months before AERE's precompiles; the primacy claim is withdrawn
  where it was implied
- bench/ scripts parametrized so they actually run for an outsider (the
  earlier textual sanitization left $STAGING unexpanded inside Python strings)
- AIP-2/AIP-3 errata with measured figures, spec remeasurements at 2026-08-01,
  and the spec-zk-stack retractions (owner is an operational key, not the
  Foundation; 'maximally sound' withdrawn; aggregator V1 deprecated)
The redacted bench-host environment files from the sanitized line are kept
exactly as published; the unredacted local variants are not carried.
2026-08-15 13:52:14 +03:00

321 lines
11 KiB
Python

#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# mlkem768_reference.py
#
# Independent, dependency-free ML-KEM-768 (FIPS 203) ENCAPSULATION oracle,
# written directly from FIPS 203 and using only Python's standard-library
# hashlib (SHA3-256, SHA3-512, SHAKE128, SHAKE256). It shares no code with
# Bouncy Castle, which is what the 0x0AE6 precompile wraps, so agreement
# between the two is real cross-implementation evidence rather than a tautology.
#
# Purpose: the ML-KEM-768 ACVP vectors in ../vectors/mlkem768_acvp.txt were
# previously only checkable by running MlKemAcvpKat.java against a patched Besu
# build. This script makes them checkable from a clean checkout with no build
# step, so an outside researcher can reproduce the claim.
#
# python mlkem768_reference.py ../vectors/mlkem768_acvp.txt [out.json]
#
# Vector line format: ek_hex|m_hex|c_hex|k_hex
# ML-KEM.Encaps_internal(ek, m) must reproduce BOTH the ciphertext c and the
# shared secret k. Checking c alone would not catch a wrong shared secret.
#
# NOTE ON SCOPE: 0x0AE6 is TESTNET-ONLY. It is NOT live on Aere mainnet 2800.
# ---------------------------------------------------------------------------
import hashlib
import json
import sys
N = 256
Q = 3329
K = 3 # ML-KEM-768
ETA1 = 2
ETA2 = 2
DU = 10
DV = 4
# zeta^BitRev7(i) mod q, the standard FIPS 203 NTT constant table.
ZETAS = [
1, 1729, 2580, 3289, 2642, 630, 1897, 848, 1062, 1919, 193, 797, 2786,
3260, 569, 1746, 296, 2447, 1339, 1476, 3046, 56, 2240, 1333, 1426, 2094,
535, 2882, 2393, 2879, 1974, 821, 289, 331, 3253, 1756, 1197, 2304, 2277,
2055, 650, 1977, 2513, 632, 2865, 33, 1320, 1915, 2319, 1435, 807, 452,
1438, 2868, 1534, 2402, 2647, 2617, 1481, 648, 2474, 3110, 1227, 910, 17,
2761, 583, 2649, 1637, 723, 2288, 1100, 1409, 2662, 3281, 233, 756, 2156,
3015, 3050, 1703, 1651, 2789, 1789, 1847, 952, 1461, 2687, 939, 2308, 2437,
2388, 733, 2337, 268, 641, 1584, 2298, 2037, 3220, 375, 2549, 2090, 1645,
1063, 319, 2773, 757, 2099, 561, 2466, 2594, 2804, 1092, 403, 1026, 1143,
2150, 2775, 886, 1722, 1212, 1874, 1029, 2110, 2935, 885, 2154,
]
# zeta^(2*BitRev7(i)+1) for the degree-2 base multiplications.
GAMMAS = [
17, 3312, 2761, 568, 583, 2746, 2649, 680, 1637, 1692, 723, 2606, 2288,
1041, 1100, 2229, 1409, 1920, 2662, 667, 3281, 48, 233, 3096, 756, 2573,
2156, 1173, 3015, 314, 3050, 279, 1703, 1626, 1651, 1678, 2789, 540, 1789,
1540, 1847, 1482, 952, 2377, 1461, 1868, 2687, 642, 939, 2390, 2308, 1021,
2437, 892, 2388, 941, 733, 2596, 2337, 992, 268, 3061, 641, 2688, 1584,
1745, 2298, 1031, 2037, 1292, 3220, 109, 375, 2954, 2549, 780, 2090, 1239,
1645, 1684, 1063, 2266, 319, 3010, 2773, 556, 757, 2572, 2099, 1230, 561,
2768, 2466, 863, 2594, 735, 2804, 525, 1092, 2237, 403, 2926, 1026, 2303,
1143, 2186, 2150, 1179, 2775, 554, 886, 2443, 1722, 1607, 1212, 2117, 1874,
1455, 1029, 2300, 2110, 1219, 2935, 394, 885, 2444, 2154, 1175,
]
# --- hashes (FIPS 203 section 4.1) -----------------------------------------
def _H(b):
return hashlib.sha3_256(b).digest()
def _G(b):
d = hashlib.sha3_512(b).digest()
return d[:32], d[32:]
class _Squeezer:
"""Incremental reader over a SHAKE128 stream.
hashlib's SHAKE objects have no incremental read, so squeeze a block and
grow it if the rejection sampler exhausts it. digest(n) is a prefix of
digest(n+m) for a fixed absorbed input, so growing is safe.
"""
def __init__(self, xof):
self._xof = xof
self._buf = xof.digest(1024)
self._pos = 0
def read(self, n):
if self._pos + n > len(self._buf):
self._buf = self._xof.digest(len(self._buf) * 2)
out = self._buf[self._pos:self._pos + n]
self._pos += n
return out
def _XOF(rho, i, j):
return _Squeezer(hashlib.shake_128(rho + bytes([i, j])))
def _PRF(eta, s, b):
return hashlib.shake_256(s + bytes([b])).digest(64 * eta)
# --- bit / byte conversion (Algorithms 3-6) --------------------------------
def _bytes_to_bits(bs):
out = []
for byte in bs:
for i in range(8):
out.append((byte >> i) & 1)
return out
def _byte_encode(f, d):
bits = []
for x in f:
for i in range(d):
bits.append((x >> i) & 1)
out = bytearray(len(bits) // 8)
for i, bit in enumerate(bits):
out[i // 8] |= bit << (i % 8)
return bytes(out)
def _byte_decode(bs, d):
bits = _bytes_to_bits(bs)
m = Q if d == 12 else (1 << d)
return [
sum(bits[i * d + j] << j for j in range(d)) % m
for i in range(len(bits) // d)
]
# --- sampling (Algorithms 7-8) ---------------------------------------------
def _sample_ntt(xof):
a = []
while len(a) < N:
c = xof.read(3)
d1 = c[0] + 256 * (c[1] % 16)
d2 = (c[1] // 16) + 16 * c[2]
if d1 < Q:
a.append(d1)
if d2 < Q and len(a) < N:
a.append(d2)
return a
def _sample_poly_cbd(bs, eta):
bits = _bytes_to_bits(bs)
f = []
for i in range(N):
x = sum(bits[2 * i * eta + j] for j in range(eta))
y = sum(bits[2 * i * eta + eta + j] for j in range(eta))
f.append((x - y) % Q)
return f
# --- NTT (Algorithms 9-11) --------------------------------------------------
def _ntt(f):
f = list(f)
i = 1
length = 128
while length >= 2:
start = 0
while start < N:
z = ZETAS[i]
i += 1
for j in range(start, start + length):
t = (z * f[j + length]) % Q
f[j + length] = (f[j] - t) % Q
f[j] = (f[j] + t) % Q
start += 2 * length
length //= 2
return f
def _ntt_inv(f):
f = list(f)
i = 127
length = 2
while length <= 128:
start = 0
while start < N:
z = ZETAS[i]
i -= 1
for j in range(start, start + length):
t = f[j]
f[j] = (t + f[j + length]) % Q
f[j + length] = (z * (f[j + length] - t)) % Q
start += 2 * length
length *= 2
return [(x * 3303) % Q for x in f]
def _multiply_ntts(f, g):
h = [0] * N
for i in range(N // 2):
a0, a1 = f[2 * i], f[2 * i + 1]
b0, b1 = g[2 * i], g[2 * i + 1]
gm = GAMMAS[i]
h[2 * i] = (a0 * b0 + a1 * b1 % Q * gm) % Q
h[2 * i + 1] = (a0 * b1 + a1 * b0) % Q
return h
def _add(f, g):
return [(a + b) % Q for a, b in zip(f, g)]
# --- compression (section 4.2.1) -------------------------------------------
def _compress(f, d):
return [(((x << d) + Q // 2) // Q) % (1 << d) for x in f]
def _decompress(f, d):
return [((x * Q) + (1 << (d - 1))) >> d for x in f]
# --- K-PKE.Encrypt (Algorithm 14) ------------------------------------------
def _kpke_encrypt(ek, m, r):
t_hat = [_byte_decode(ek[384 * i:384 * (i + 1)], 12) for i in range(K)]
rho = ek[384 * K:384 * K + 32]
# FIPS 203 absorbs rho || j || i to produce A_hat[i][j], so the array built
# here from _XOF(rho, i, j) is already A_hat TRANSPOSED. Encrypt wants
# A_hat^T . y, so it is indexed [i][j] directly below with no second
# transpose. Getting this backwards still yields a correct shared secret K
# and a correct v, and corrupts only c1, which is why the shared-secret
# check alone is not sufficient evidence.
a_hat_t = [[_sample_ntt(_XOF(rho, i, j)) for j in range(K)] for i in range(K)]
y = [_ntt(_sample_poly_cbd(_PRF(ETA1, r, i), ETA1)) for i in range(K)]
e1 = [_sample_poly_cbd(_PRF(ETA2, r, K + i), ETA2) for i in range(K)]
e2 = _sample_poly_cbd(_PRF(ETA2, r, 2 * K), ETA2)
# u = NTT^-1(A^T . y) + e1
u = []
for i in range(K):
acc = [0] * N
for j in range(K):
acc = _add(acc, _multiply_ntts(a_hat_t[i][j], y[j]))
u.append(_add(_ntt_inv(acc), e1[i]))
mu = _decompress(_byte_decode(m, 1), 1)
acc = [0] * N
for i in range(K):
acc = _add(acc, _multiply_ntts(t_hat[i], y[i]))
v = _add(_add(_ntt_inv(acc), e2), mu)
c1 = b"".join(_byte_encode(_compress(u[i], DU), DU) for i in range(K))
c2 = _byte_encode(_compress(v, DV), DV)
return c1 + c2
def encaps_internal(ek, m):
"""ML-KEM.Encaps_internal (Algorithm 17). Returns (shared_secret_K, ciphertext_c)."""
k_bar, r = _G(m + _H(ek))
c = _kpke_encrypt(ek, m, r)
return k_bar, c
def main():
vectors = sys.argv[1] if len(sys.argv) > 1 else "../vectors/mlkem768_acvp.txt"
outpath = sys.argv[2] if len(sys.argv) > 2 else "kat-results-mlkem-reference.json"
passed = failed = total = 0
results = []
with open(vectors) as f:
for raw in f:
line = raw.strip()
if not line or line.startswith("#"):
continue
p = line.split("|")
if len(p) != 5:
continue
total += 1
tc, ek_hex, m_hex, c_hex, k_hex = p
ek = bytes.fromhex(ek_hex)
m = bytes.fromhex(m_hex)
c_want = bytes.fromhex(c_hex)
k_want = bytes.fromhex(k_hex)
k_got, c_got = encaps_internal(ek, m)
ok = (k_got == k_want) and (c_got == c_want)
if ok:
passed += 1
else:
failed += 1
results.append({
"tcId": tc,
"ekLen": len(ek),
"ctLen": len(c_got),
"ciphertextMatch": c_got == c_want,
"sharedSecretMatch": k_got == k_want,
"status": "PASS" if ok else "FAIL",
})
print(f"ML-KEM-768 tc{tc} ekLen={len(ek)} ctLen={len(c_got)} "
f"c={'OK' if c_got == c_want else 'MISMATCH'} "
f"k={'OK' if k_got == k_want else 'MISMATCH'} -> "
f"{'PASS' if ok else 'FAIL'}")
report = {
"precompile": "ML-KEM-768 (FIPS 203) deterministic encapsulate",
"address": "0x0000000000000000000000000000000000000ae6",
"liveOnMainnet2800": False,
"note": "TESTNET-ONLY precompile. Verified here against an independent "
"pure-Python FIPS 203 oracle (stdlib hashlib only), not Bouncy Castle.",
"oracle": "mlkem768_reference.py (this file)",
"passed": passed, "failed": failed, "total": total, "results": results,
}
with open(outpath, "w") as f:
json.dump(report, f, indent=2)
print(f"ML-KEM-768 KAT: passed={passed} failed={failed} total={total}")
if failed != 0 or total == 0:
print("MLKEM_KAT_FAILED")
sys.exit(3)
print("MLKEM_KAT_OK")
if __name__ == "__main__":
main()