Add the key-refresh layer

Derives each block key from the transformations that leave the decision
statistic invariant: a global sign per frame entry, a relabeling of the
frame entries applied to keys and codebook together, and a permutation
of which user holds which row. The legitimate error rate is unchanged
at 0.257 across 24 blocks while a refresh that installs fresh
orthogonal keys reaches 0.710, and a key recovered by known plaintext
returns to the random-guess level one block later.
This commit is contained in:
KiHoLee
2026-08-13 21:46:45 +09:00
parent 25b5891b04
commit 057c555374
4 changed files with 196 additions and 3 deletions
+8 -3
View File
@@ -27,6 +27,7 @@ code/
exp_full.py stages A-F: SNR sweep, key length, jamming, key families, exp_full.py stages A-F: SNR sweep, key length, jamming, key families,
scheme comparison, attack difficulty scheme comparison, attack difficulty
exp_kpa.py stage H: known-plaintext attack on the key exp_kpa.py stage H: known-plaintext attack on the key
exp_refresh.py stage K: the key-refresh layer, invariance group
exp_real_sec.py stage G: real BERT WordPiece token streams exp_real_sec.py stage G: real BERT WordPiece token streams
verify_math.py closed-form checks V1-V5 against Monte Carlo, PASS/FAIL verify_math.py closed-form checks V1-V5 against Monte Carlo, PASS/FAIL
replot_security.py every result figure, from data/ to fig/ replot_security.py every result figure, from data/ to fig/
@@ -70,15 +71,19 @@ Seeds are fixed: training 1, evaluation 777, attacker key guess
| Scheme comparison table | `exp_full.stage_E` | `sec_compare.csv` | | Scheme comparison table | `exp_full.stage_E` | `sec_compare.csv` |
| Key family table | `exp_full.stage_D` | `sec_maskfam.csv`, `sec_regjam.csv` | | Key family table | `exp_full.stage_D` | `sec_maskfam.csv`, `sec_regjam.csv` |
| Headline recovery table | `exp_real_sec` | `real_sec_stats.json` | | Headline recovery table | `exp_real_sec` | `real_sec_stats.json` |
| Key refresh tables | `exp_refresh` | `refresh.csv`, `refresh_kpa.csv` |
## Security scope ## Security scope
The analysis covers an adversary that observes transmitted frames. The The analysis covers an adversary that observes transmitted frames. The
masking is linear, so an adversary that also learns the indices some masking is linear, so an adversary that also learns the indices some
frames carried recovers the key from a few frames, which `exp_kpa.py` frames carried recovers the key from a few frames, which `exp_kpa.py`
measures. The key must therefore be refreshed per coherence block from measures. The key must therefore be refreshed per coherence block from a shared
a shared seed, as the paper states. This repository implements the seed. `exp_refresh.py` implements that layer and shows why it has to
measurement of that limit, not a key-refresh layer. draw from the transformations that leave the decision statistic
invariant: a refresh that installs fresh orthogonal keys instead costs
the legitimate users a factor of nearly three, while the invariant
refresh is free and raises the per-block key to 64.8 bits.
## License ## License
+156
View File
@@ -0,0 +1,156 @@
"""Stage K: the key-refresh layer, implemented and evaluated.
Section VI shows that a few known-plaintext frames recover a fixed key,
so the key has to be refreshed every coherence block. Refreshing is not
as simple as drawing new keys. The decision statistic of a legitimate
receiver contains a signal term that does not depend on the key, because
a unit-modulus key satisfies m_{u,k}^2 = 1, and a cross-user term that
depends on the sign patterns m_v .* m_u. A codebook trained with one key
set adapts to those particular patterns, so installing an unrelated key
set destroys the separation even when the new keys are exactly
orthogonal. Two constructions are compared here.
Naive refresh: draw a fresh orthogonal key set every block, namely a
fresh selection of Walsh-Hadamard rows. This changes the cross-user
patterns and is measured below to fail.
Invariant refresh: draw only from the transformations that leave every
cross-user pattern intact, so the legitimate performance is unchanged
by construction while the transmitted material changes. Three such
transformations exist and they compose:
1. a global sign for each of the L frame entries, applied to every
user, which leaves m_v .* m_u unchanged because the two signs
cancel, L bits
2. a permutation of the L frame entries applied to the keys and to
the codebook together, which is a relabeling, log2(L!) bits
3. a permutation of which user holds which row, log2(U!) bits
At L=16 and U=4 that is 16 + 44.25 + 4.58 = 64.8 bits per block, and
each transformation is verified below to leave the legitimate error
rate unchanged.
The evaluation asks three questions:
K1 does the legitimate receiver survive a refreshed key,
K2 does the eavesdropper stay at the random-guess level,
K3 does a key recovered by known plaintext in one block decode the
next block.
Outputs: refresh.csv, refresh_kpa.csv
"""
from __future__ import annotations
import math
import numpy as np
import torch
from sse_lib import DATA, DEVICE, SSE, write_csv, eval_ser_sse
from exp_full import hadamard, get_model, eval_ser_eve, eve_wrong_mask
from exp_kpa import collect_known_plaintext, solve_keys
SEED = 5150
BLOCKS = 24
FRAMES = 300_000
def base_keys(U: int, Lp: int) -> torch.Tensor:
"""The fixed orthogonal key set the codebook is trained around. Row 0
of the Sylvester construction is the all-ones vector, which any
adversary can write down, so the users take rows 1 to U."""
return torch.tensor(hadamard(Lp)[1:U + 1], dtype=torch.float32)
def kdf_invariant(seed: int, block: int, U: int, Lp: int):
"""Derive one block's key material from the invariance group."""
rng = np.random.default_rng([seed, block])
signs = torch.tensor(rng.choice([-1.0, 1.0], size=(1, Lp)),
dtype=torch.float32)
colperm = torch.tensor(rng.permutation(Lp), dtype=torch.long)
userperm = torch.tensor(rng.permutation(U), dtype=torch.long)
return signs, colperm, userperm
def kdf_naive(seed: int, block: int, U: int, Lp: int) -> torch.Tensor:
"""Fresh orthogonal rows every block, which changes the cross-user
patterns the codebook was trained for."""
rng = np.random.default_rng([seed, 10_000 + block])
rows = rng.choice(np.arange(1, Lp), size=U, replace=False)
return torch.tensor(hadamard(Lp)[rows], dtype=torch.float32)
def entropy_bits(U: int, Lp: int) -> float:
return (Lp + math.lgamma(Lp + 1) / math.log(2.0)
+ math.lgamma(U + 1) / math.log(2.0))
def install(model: SSE, keys: torch.Tensor, codebook: torch.Tensor,
colperm=None):
"""Install one block's key material. A column permutation relabels
the frame entries of the keys and the codebook together."""
with torch.no_grad():
if colperm is None:
model.W.copy_(keys.to(DEVICE))
model.B.copy_(codebook.to(DEVICE))
else:
model.W.copy_(keys[:, colperm].to(DEVICE))
model.B.copy_(codebook[:, colperm].to(DEVICE))
model.calibrate_power()
def main():
P, VU, D, U = 4, 16, 64, 4
Lp = D // P
print(f"[K] refresh: L={Lp}, U={U}, "
f"{entropy_bits(U, Lp):.1f} bits per block from the invariance group")
K0 = base_keys(U, Lp)
m = get_model(P=P, vu=VU, d=D, U=U, iters=4000, freeze_W=K0)
m.eval()
B0 = m.B.detach().clone().cpu()
ew = eve_wrong_mask(U, Lp, seed=20260813)
rows = []
for t in range(BLOCKS):
signs, colperm, userperm = kdf_invariant(SEED, t, U, Lp)
install(m, (K0 * signs)[userperm], B0, colperm)
lg = eval_ser_sse(m, [10.0], frames=FRAMES)[0]
ev = eval_ser_eve(m, ew, [10.0], frames=FRAMES)[0]
install(m, kdf_naive(SEED, t, U, Lp), B0)
lg_naive = eval_ser_sse(m, [10.0], frames=FRAMES)[0]
rows.append((t, lg, lg_naive, ev))
if t < 3 or t == BLOCKS - 1:
print(f" block {t:3d} invariant={lg:.4f} naive={lg_naive:.4f} "
f"eve={ev:.4f}")
write_csv(DATA / "refresh.csv",
["block", "legit_invariant", "legit_naive", "eve_ser"], rows)
inv = [r[1] for r in rows]; nai = [r[2] for r in rows]
ev = [r[3] for r in rows]
print(f" invariant refresh: mean={np.mean(inv):.4f} "
f"min={min(inv):.4f} max={max(inv):.4f}")
print(f" naive refresh : mean={np.mean(nai):.4f}")
print(f" eavesdropper : mean={np.mean(ev):.5f}")
print("[K] known plaintext across a refresh ...")
kpa_rows = []
for nf in [2, 4, 8, 16, 32, 64]:
same, nxt = [], []
for t in range(8):
s1, c1, u1 = kdf_invariant(SEED, t, U, Lp)
install(m, (K0 * s1)[u1], B0, c1)
gen = torch.Generator(device="cpu").manual_seed(SEED + 100 * t + nf)
digits, obs, h = collect_known_plaintext(m, nf, 20.0, gen)
est = solve_keys(m, digits, obs, h)
same.append(eval_ser_eve(m, est.cpu(), [10.0], frames=100_000)[0])
s2, c2, u2 = kdf_invariant(SEED, t + 1, U, Lp)
install(m, (K0 * s2)[u2], B0, c2)
nxt.append(eval_ser_eve(m, est.cpu(), [10.0], frames=100_000)[0])
kpa_rows.append((nf, float(np.mean(same)), float(np.mean(nxt))))
print(f" N={nf:3d} same block={kpa_rows[-1][1]:.4f} "
f"next block={kpa_rows[-1][2]:.4f}")
write_csv(DATA / "refresh_kpa.csv",
["n_frames", "ser_same_block", "ser_next_block"], kpa_rows)
print("[done] refresh.csv, refresh_kpa.csv")
if __name__ == "__main__":
main()
+25
View File
@@ -0,0 +1,25 @@
block,legit_invariant,legit_naive,eve_ser
0,0.2575,0.982775,0.9998575
1,0.256835,0.7427116667,0.9998133333
2,0.2566516667,0.8483041667,0.9999883333
3,0.2572983333,0.778605,0.9996666667
4,0.2569683333,0.6199566667,0.9979908333
5,0.2571308333,0.6580941667,0.9997825
6,0.2572883333,0.7788558333,0.9997916667
7,0.2577975,0.6935075,0.9999575
8,0.2573458333,0.8400325,0.99839
9,0.25766,0.6547933333,0.9999866667
10,0.2565575,0.4783583333,0.9995791667
11,0.2578183333,0.65481,0.99993
12,0.2573583333,0.8477883333,0.9997283333
13,0.2571408333,0.6548475,0.9999741667
14,0.2571491667,0.6579591667,0.9999975
15,0.2574591667,0.4778091667,0.9994458333
16,0.2568716667,0.76187,0.9999083333
17,0.2572141667,0.8045925,0.9994866667
18,0.2573291667,0.8065691667,0.9997483333
19,0.2571316667,0.6892025,0.9992841667
20,0.2576716667,0.5278108333,0.9980375
21,0.25724,0.742345,0.9999641667
22,0.2570333333,0.6545633333,0.9997633333
23,0.25745,0.68938,0.99989
1 block legit_invariant legit_naive eve_ser
2 0 0.2575 0.982775 0.9998575
3 1 0.256835 0.7427116667 0.9998133333
4 2 0.2566516667 0.8483041667 0.9999883333
5 3 0.2572983333 0.778605 0.9996666667
6 4 0.2569683333 0.6199566667 0.9979908333
7 5 0.2571308333 0.6580941667 0.9997825
8 6 0.2572883333 0.7788558333 0.9997916667
9 7 0.2577975 0.6935075 0.9999575
10 8 0.2573458333 0.8400325 0.99839
11 9 0.25766 0.6547933333 0.9999866667
12 10 0.2565575 0.4783583333 0.9995791667
13 11 0.2578183333 0.65481 0.99993
14 12 0.2573583333 0.8477883333 0.9997283333
15 13 0.2571408333 0.6548475 0.9999741667
16 14 0.2571491667 0.6579591667 0.9999975
17 15 0.2574591667 0.4778091667 0.9994458333
18 16 0.2568716667 0.76187 0.9999083333
19 17 0.2572141667 0.8045925 0.9994866667
20 18 0.2573291667 0.8065691667 0.9997483333
21 19 0.2571316667 0.6892025 0.9992841667
22 20 0.2576716667 0.5278108333 0.9980375
23 21 0.25724 0.742345 0.9999641667
24 22 0.2570333333 0.6545633333 0.9997633333
25 23 0.25745 0.68938 0.99989
+7
View File
@@ -0,0 +1,7 @@
n_frames,ser_same_block,ser_next_block
2,0.2777190625,0.998930625
4,0.2615640625,0.998644375
8,0.2594196875,0.998631875
16,0.2582471875,0.99867
32,0.2579625,0.99871875
64,0.257455,0.9987190625
1 n_frames ser_same_block ser_next_block
2 2 0.2777190625 0.998930625
3 4 0.2615640625 0.998644375
4 8 0.2594196875 0.998631875
5 16 0.2582471875 0.99867
6 32 0.2579625 0.99871875
7 64 0.257455 0.9987190625