Audit round: fair OMA reference, dense grids, covariance-attack checks

Resource-match the OMA reference in the key-length sweep (oma_ser_keylen),
which gives it the L/16 combining gain the longer frame allows. The
proposal now passes a resource-matched OMA by 1.27x at L=64 rather than
the 4.3x reported against a fixed-d reference.

Densify the JSR, sensitivity, and brute-force grids so the curves are
smooth, give the index cipher its channel floor instead of error-free
reception, and add the permutation-key known-plaintext attack
(exp_permkpa) so Fig. 7 carries a conventional linear scheme.

Add check_cov_attack.py and check_cov_ceiling.py: a referee raised a
ciphertext-only second-order attack; the exact-population test shows the
received covariance leaks only a sparse rank-deficient subset of the key
Gram and leaves the eavesdropper at the random-guess level.

Dump verify_math.csv, move the superseded V=256 pilot CSVs to data/pilot.
This commit is contained in:
KiHoLee
2026-08-16 23:48:39 +09:00
parent fbbad154b2
commit 3529ab1918
28 changed files with 755 additions and 123 deletions
+82
View File
@@ -0,0 +1,82 @@
"""Decisive noiseless population test of the covariance attack (audit M1).
If the second-order statistics leak the key Gram, they leak it best in
the noiseless infinite-sample limit. This computes the EXACT per-period
second moment E[y_k y_l] over the uniform index distribution with no
channel and no noise, then runs the same recovery, and asks whether the
keys come out. If they do not come out even here, no finite noisy attack
can do better and the leak is not exploitable against this codebook.
"""
from __future__ import annotations
import itertools
import math
import numpy as np
import torch
from sse_lib import DEVICE
from exp_full import get_model, hadamard, eval_ser_eve
def main():
U, L = 4, 16
K0 = torch.tensor(hadamard(L)[1:U + 1], dtype=torch.float32)
m = get_model(iters=4000, freeze_W=K0)
m.eval()
Bn = m.unit_codebook().to(DEVICE) # (Vu,L)
true_m = m.masks().to(DEVICE) # (U,L)
c = m.c
# exact population second moment of one period, indices uniform
# y_k = (1/c) sum_u e_{s_u,k} m_{u,k}, s_u iid uniform over Vu
mu = Bn.mean(0) # codebook column mean
R = (Bn.T @ Bn) / Bn.shape[0] # E[e_k e_l], (L,L)
G_true = true_m.T @ true_m # (L,L) key Gram, the target
# E[y_k y_l] = (1/c^2)[ R_kl (M^TM)_kl + (mu_k mu_l)(rowsum_k rowsum_l
# - diag correction) ]; assemble exactly
rs = true_m.sum(0) # sum_u m_{u,k}
cross = torch.outer(rs, rs) - G_true # sum_{u!=v} m_uk m_vl
S = (R * G_true + torch.outer(mu, mu) * cross) / (c * c)
print(f"codebook column mean |mu|_max = {mu.abs().max():.4f}")
offdiag = R - torch.diag(torch.diagonal(R))
print(f"codebook R off-diagonal: max|R_kl| = {offdiag.abs().max():.4f}, "
f"mean|R_kl| = {offdiag.abs().mean():.4f}")
# recover G from S using the public R (exactly the attack)
C = R
keep = C.abs() > 1e-2
Ghat = torch.zeros(L, L, device=DEVICE)
Ghat[keep] = S[keep] * (c * c) / C[keep]
# how well does the off-diagonal of Ghat match the true key Gram?
od = ~torch.eye(L, dtype=torch.bool, device=DEVICE)
usable = keep & od
if usable.any():
err = (Ghat[usable] - G_true[usable]).abs().mean()
rng = G_true[od].abs().mean()
print(f"usable off-diagonal entries: {int(usable.sum())} of {L*(L-1)}")
print(f"recovered-Gram error on usable entries: {err:.4f} "
f"(true off-diag scale {rng:.4f})")
else:
print("no usable off-diagonal entries: R is diagonal, zero leak")
# try to factor and decode from the exact-population Ghat
Ghat[~keep] = 0.0
Ghat.fill_diagonal_(float(U))
Ghat = 0.5 * (Ghat + Ghat.T)
ev, evec = torch.linalg.eigh(Ghat)
idx = torch.argsort(ev, descending=True)[:U]
root = (evec[:, idx] * ev[idx].clamp_min(0).sqrt()).T
cand = torch.sign(root)
cand[cand == 0] = 1.0
best = 1.0
for perm in itertools.permutations(range(U)):
for sg in itertools.product([1.0, -1.0], repeat=U):
Mh = cand[list(perm)] * torch.tensor(sg, device=DEVICE)[:, None]
best = min(best, eval_ser_eve(m, Mh.cpu(), [10.0],
frames=20_000, seed=13)[0])
print(f"best eavesdropper SER from EXACT population covariance: {best:.4f}")
print("chance 0.99998, legitimate ~0.276")
if __name__ == "__main__":
main()