做物理层安全研究这几年,我的一个体会是:越是宣称“信息论安全”的协议,越容易在随机源这个环节翻车。基尔霍夫-洛-约翰逊噪声(KLJN)安全密钥交换协议就是典型——它绕开RSA、ECC那套大整数分解和离散对数难题,直接用电阻热噪声和基尔霍夫定律搞密钥协商,理论上连量子计算机都拿它没办法。可一旦落到工程实现,Alice和Bob手里的随机数生成器只要有一点统计上的偏置,整个安全边界就会从“信息论安全”滑向“统计意义上的可破解”。
这篇文章我会以攻击者视角,完整走一遍针对KLJN协议的统计随机数生成器攻击:先拆KLJN协议里随机性到底扮演什么角色,再推导统计攻击的数学模型,然后给出我调试好的Matlab仿真代码,从信道建模、攻击实施到结果分析全程可复现,最后聊一聊怎么从攻击反推防御。适合做物理层安全、密码协议实现、以及用Matlab做安全仿真验证的朋友,新手也能照着代码跑一遍。
1. KLJN在保护什么:协议原理与三重随机性假设
1.1 一轮比特交换是如何完成的
KLJN协议的基本动作很简洁。Alice和Bob预先约定两个电阻值,比如Rlo=1kΩ和Rhi=10kΩ,然后每一轮协商中,双方各自随机从这两个值里挑一个,通过传输线连接起来。电阻本身会发热,产生热噪声,也就是约翰逊噪声,噪声的功率谱密度(PSD)和电阻值、温度严格挂钩。
窃听者Eve在信道两端测到的电压PSD可以写成这样一个简洁公式:
S_meas = 4kB T * (Ra * Rb) / (Ra + Rb)
其中kB是玻尔兹曼常数,T是绝对温度,Ra和Rb分别是Alice和Bob本轮的电阻选择。这个公式里的RaRb/(Ra+Rb)其实就是两个电阻并联后的等效电阻。
当Alice和Bob选相同时,PSD落在“匹配值”;选不同时,PSD落在“不匹配值”。因为Rlo和Rhi差了一个数量级,三种情况——Rlo/Rlo匹配、Rhi/Rhi匹配、Rlo/Rhi不匹配——的PSD数值拉开得很明显。我给个具体感觉:T=300K时,按上面公式算,lo匹配大约5.5e-16 W/Hz,不匹配大约1.0e-15 W/Hz,hi匹配大约5.5e-15 W/Hz。量级差异足够让Eve通过功率测量分辨出信道当前处于哪种状态。
1.2 窃听者能看到什么、看不到什么
Eve能测到的东西有两类。第一是信道PSD,她因此知道这一轮是匹配还是不匹配;第二是双方通过公开信道发出的确认消息,比如“匹配,保留”或者“不匹配,丢弃”。但她有一个关键盲区:当信道处于不匹配状态时,她只知道Alice和Bob选择不同,却无法分辨到底是“Alice选了Rlo、Bob选了Rhi”还是“Alice选了Rhi、Bob选了Rlo”。这个左右对称性就是KLJN保密性的原始来源。
匹配状态下,PSD确实直接暴露了匹配于哪个电阻,但协议后续通常还会叠加去模糊化、隐私放大之类的处理,把暴露信息压到可忽略。我想强调一点:KLJN的安全性不是“Eve啥都看不到”,而是“Eve看到的东西在有限轮内仍然不足以恢复完整密钥”。这个假设在理论模型里成立,前提是——所有随机选择都来自理想随机源。
1.3 协议依赖的随机性角色
如果你把KLJN协议从头捋一遍,会发现随机性至少在三个环节起作用:
- Alice侧电阻选择:她要随机决定选Rlo还是Rhi,这会直接影响最终密钥候选比特;
- Bob侧电阻选择:他要随机决定自己的电阻,用来和Alice碰撞或错开;
- 后续隐私放大中的随机化参数:包括盐值、哈希种子等辅助随机量。
每一处都要求随机数生成器的输出在统计上不可区分于理想随机序列。统计随机数生成器攻击的意思就是:不去和物理定律硬碰硬,而是攻击这三个随机性角色中最弱的那一个。只要Alice侧的RNG输出有偏置、有线性模式、有周期,Eve就能把信道观测多轮累积起来,把偏置还原成对密钥的后验优势。
打个比方,KLJN协议就像一个保险柜,外壳是热噪声和物理定律,锤子砸不动。但柜子的锁芯用的是普通弹簧锁,钥匙齿形有点规律——统计攻击不砸外壳,只专心研究锁芯。
2. 统计随机数生成器攻击的数学内核
2.1 Eve的观测模型
为了让攻击逻辑更清晰,我在仿真中采用一个文献里常用的简化工作模型:
- 每个协商周期里,Alice以概率p选择Rhi(对应候选密钥位1),以概率1-p选择Rlo(对应0);
- Bob用理想均匀RNG随机选择电阻;
- 匹配轮被保留,Eve通过信道PSD观测判断匹配状态;
- 假设信道测量噪声有限,Eve不是全知,但能获得一个有噪声的状态分类结果。
Eve在整个攻击过程中收集的数据就是每轮的状态观测和公开公告。她不需要破解热噪声的物理机理,只要把几百轮的观测结果放进统计检验器里跑一圈,就能发现Alice侧RNG的偏置程度。
这里遵循的还是Kerckhoffs原则:攻击者知道协议的全部设计细节,唯一不知道的就是随机源内部状态和密钥本身。在这个前提下,任何统计偏差都是可利用的信息。
2.2 贝叶斯偏置推断:p为什么能直接从“hi匹配”事件里估出来
这里有个特别重要的洞察:Eve在匹配轮里看到“hi匹配”(即PSD等于Rhi/Rhi匹配值)这件事,等价于她知道了Alice这一轮选了Rhi。因为只有双方都选Rhi时才会出现hi匹配。
于是问题变成了一个最基本的概率估计:在匹配轮里,hi匹配事件占多大比例?真实比例为:
P(hi匹配 | 匹配) = P(Alice选Rhi且Bob选Rhi) / P(匹配) = (p * 0.5) / [p*0.5 + (1-p)*0.5] = p
同理,P(lo匹配 | 匹配) = 1-p。这就意味着,匹配轮里hi匹配的频率就是Alice偏置p的无偏估计。这不是什么高深数学,就是一个条件概率化简,但攻击的含义很重:随机源偏置会直接转化成信道观测里的频率偏差,而频率是最好估的统计量。
我测试过,当协议轮数到几百轮时,这个估计的标准误差已经降到可用的范围。轮数越多,Eve对p的估计越准,后面对密钥的猜解优势就越稳定。
2.3 卡方检验:如何判定“随机源已经不安全”
光估计偏置还不够,攻击者还需要一个形式化判据:什么时候能“实锤”这个随机源不满足均匀性要求?标准做法是卡方检验。对匹配轮的hi/lo计数做单因素卡方检验,零假设是p=0.5,统计量是:
χ² = (Ohi - E)²/E + (Olo - E)²/E,其中E = (Ohi + Olo)/2
查卡方分布自由度1,如果p值小于显著性水平(比如0.01),Eve就以超过99%的置信水平认定RNG存在偏置。这个检验在Matlab里可以用chi2gof,也可以像我后面代码那样直接用chi2cdf手动算。
不过卡方检验有个适用前提:样本量不能太少。当你只有几十个匹配轮时,即使真实p已经偏到0.6,检验结果也可能是不显著。在实际攻击脚本里,我会同时输出“偏置估计值”和“检验p值”,前者告诉你偏多少,后者告诉你这个偏置是否统计上站得住。
2.4 熵损失的量化:从每比特熵到密钥强度
统计攻击能直接把随机源偏置转化为密钥熵损失。假设Eve估计出p,那么她对密钥每个比特的后验分布就是(P(1)=p, P(0)=1-p),每比特熵为:
H = -p log2 p - (1-p) log2 (1-p)
p越偏离0.5,H越小。我整理了一张对照表:
| Alice侧偏置p | 每比特熵(bit) | 熵损失(bit) | 单比特猜中率 |
|---|---|---|---|
| 0.50 | 1.000 | 0.000 | 0.500 |
| 0.55 | 0.994 | 0.006 | 0.550 |
| 0.60 | 0.971 | 0.029 | 0.600 |
| 0.65 | 0.934 | 0.066 | 0.650 |
| 0.70 | 0.881 | 0.119 | 0.700 |
| 0.80 | 0.722 | 0.278 | 0.800 |
| 0.90 | 0.469 | 0.531 | 0.900 |
128位密钥在p=0.7时,单比特猜中率70%,这个数字单看好像“不高”,但问题要看系统性:攻击者不是随机猜,而是每一比特都有稳定优势。熵从128 bit掉到约113 bit,等于白白丢掉了15 bit的安全强度。如果随机源不只是简单偏置,而是有线性可预测结构,攻击者甚至可以做到逐位还原,那就彻底没有安全性可言。
3. Matlab攻击实验全流程:从协议复现到密钥恢复
3.1 仿真环境与参数建立
我用的环境是MATLAB R2023a,核心代码只有一个主脚本加几个函数,没有额外工具箱依赖,Statistics and Machine Learning Toolbox里的chi2cdf就够用。固定随机种子rng(20240401),保证每次跑出来的曲线和文章里一致。
物理层参数这样设:
- Rlo = 1 kΩ,Rhi = 10 kΩ,T = 300 K;
- 总协议轮数800轮;
- 测量噪声0.3 dB(对数域标准差);
- Alice侧真实偏置p=0.66。
这个0.66不是随手写的。它比0.5高但不算离谱,刚好卡在一个“工程上可能出现的坏RNG”和“明显离谱”的中间带,能看出攻击的层次感。
3.2 三方角色建模:Alice、Bob与Eve
代码里角色的划分很清晰,先看Alice有偏随机源、Bob理想随机源以及真实PSD的计算:
%% KLJN信道仿真 + 统计攻击主程序 clear; clc; close all; rng(20240401); % 固定种子便于复现 % ---- 物理层参数 ---- Rlo = 1e3; % 低阻值 (Ohm) Rhi = 1e4; % 高阻值 (Ohm) T = 300; % 噪声温度 (K) kB = 1.380649e-23; rounds = 800; % 协议总轮数 % 三种状态的理论PSD PSD_lo_match = 4*kB*T*(Rlo*Rlo)/(2*Rlo); PSD_hi_match = 4*kB*T*(Rhi*Rhi)/(2*Rhi); PSD_mismatch = 4*kB*T*(Rlo*Rhi)/(Rlo+Rhi); % ---- Alice: 有偏随机源 ---- p_alice = 0.66; alice_hi = rand(rounds,1) < p_alice; % ---- Bob: 理想均匀随机源 ---- bob_hi = rand(rounds,1) < 0.5; % ---- 每轮真实PSD ---- match_round = (alice_hi == bob_hi); real_psd = zeros(rounds,1); real_psd(match_round & ~alice_hi) = PSD_lo_match; real_psd(match_round & alice_hi) = PSD_hi_match; real_psd(~match_round) = PSD_mismatch; % ---- Eve的观测: 对数域加噪声 ---- sigma_db = 0.3; obs_psd_db = 10*log10(real_psd) + sigma_db*randn(rounds,1); obs_psd = 10.^(obs_psd_db/10);这里有一个值得强调的工程细节:对PSD加噪声要在对数域加,而不是线性域。我第一次仿真时直接在real_psd上加高斯噪声,结果出现了负PSD,后面的log计算直接NaN。物理上PSD是正定功率量纲,线性域加高斯噪声会带偏估计,对数域加才是符合实际测量设备的建模方式。
3.3 三状态分类与统计攻击器实现
Eve拿到带噪观测后,需要把连续PSD值分类成三类状态。我用几何均值做阈值,因为三类PSD在对数域近似等距,几何阈值比算术阈值稳定得多:
% ---- Eve的三状态分类 ---- thr1 = sqrt(PSD_lo_match * PSD_mismatch); thr2 = sqrt(PSD_mismatch * PSD_hi_match); est_state = zeros(rounds,1); est_state(obs_psd < thr1) = 1; % 估计: lo匹配 est_state(obs_psd > thr2) = 3; % 估计: hi匹配 est_state(obs_psd >= thr1 & ... obs_psd <= thr2) = 2; % 估计: 不匹配 % 真实状态 true_state = zeros(rounds,1); true_state(match_round & ~alice_hi) = 1; true_state(match_round & alice_hi) = 3; true_state(~match_round) = 2; classify_acc = mean(est_state == true_state); fprintf('三状态分类准确率: %.2f%%\n', classify_acc*100);按上面参数跑,分类准确率一般能到95%以上。这说明对Eve来说,KLJN信道上的状态信息是确实可观测的,物理层本身没有挡住她。接下来是核心的攻击逻辑——从匹配轮估计偏置、做卡方检验、算贝叶斯猜中率:
%% ---- 统计攻击核心部分 ---- hi_match = (est_state == 3); lo_match = (est_state == 1); n_hi = sum(hi_match); n_lo = sum(lo_match); % 偏置估计: P(Alice选Rhi | 匹配) p_est = n_hi / (n_hi + n_lo); % 卡方检验, H0: p=0.5 exp_cnt = (n_hi + n_lo) / 2; chi2_stat = (n_hi - exp_cnt)^2 / exp_cnt + ... (n_lo - exp_cnt)^2 / exp_cnt; p_chi2 = 1 - chi2cdf(chi2_stat, 1); reject_uniform = (p_chi2 < 0.01); % 贝叶斯后验与猜中率 guess_acc = max(p_est, 1 - p_est); per_bit_entropy = -p_est*log2(p_est) - (1-p_est)*log2(1-p_est); entropy_loss = 1 - per_bit_entropy; fprintf('=========== 统计攻击结果 ===========\n'); fprintf('估计偏置 p = %.4f (真实 p=%.4f)\n', p_est, p_alice); fprintf('卡方统计量 = %.4f, p值=%.4f\n', chi2_stat, p_chi2); fprintf('拒绝均匀性(H0)? = %d\n', reject_uniform); fprintf('单比特猜中率 = %.4f\n', guess_acc); fprintf('每比特熵 = %.4f bit\n', per_bit_entropy); fprintf('熵损失 = %.4f bit\n', entropy_loss);这一段是整个攻击的核心。Eve不需要任何私密信息,只需要统计匹配轮中的hi/lo比例,就能成功把密钥每比特熵从1.0 bit拉低到0.9 bit左右。当p=0.66时,128位密钥的总熵从128降到约115 bit,这对协议设计者来说是必须正视的警报。
3.4 偏置扫描:攻击强度的全景曲线
单一偏置值不够直观,我补了一段扫描实验,把Alice侧偏置从0.5扫到0.9,同时计算攻击者的单比特猜中率和密钥熵,画成曲线:
%% ---- 偏置扫描 ---- p_list = 0.5:0.02:0.9; guess_curve = zeros(size(p_list)); entropy_curve = zeros(size(p_list)); for k = 1:length(p_list) p = p_list(k); pHiGivenMatch = (p*0.5) / (p*0.5 + (1-p)*0.5); guess_curve(k) = max(pHiGivenMatch, 1-pHiGivenMatch); entropy_curve(k) = -pHiGivenMatch*log2(pHiGivenMatch) - ... (1-pHiGivenMatch)*log2(1-pHiGivenMatch); end figure('Color','w','Position',[100 100 700 500]); subplot(2,1,1); plot(p_list, guess_curve, 'b-o', 'LineWidth',1.5); xlabel('Alice RNG 偏置 p'); ylabel('单比特猜中率'); grid on; ylim([0.5 1]); title('统计攻击效果随偏置变化'); subplot(2,1,2); plot(p_list, entropy_curve, 'r-s', 'LineWidth',1.5); xlabel('Alice RNG 偏置 p'); ylabel('密钥每比特熵 (bit)'); grid on; ylim([0 1]); title('密钥熵随偏置变化');从曲线能很直观看到:p=0.5时,猜中率严格0.5,熵1.0 bit;p=0.66时猜中率0.66,熵约0.93;p到了0.8,熵已经掉到0.72以下。随机源偏置的杀伤力在曲线后半段几乎是垂直下坠,这也是我一直在强调“不能把RNG偏置当小概率事件”的原因。
3.5 进阶攻击演示:线性同余RNG的完全预测
如果Alice的“随机”不是物理熵源,而是软件里常见的线性同余生成器(LCG),统计攻击可以直接升级为模式恢复攻击。下面给一个小模数演示代码,便于跑通:
%% ---- LCG模式恢复(演示版) ---- % 假设Alice用了一个朴素的16位LCG,参数公开 a = 1103515245; c = 12345; m = 2^16; % 小模数便于枚举 seed_true = 42; x = zeros(1,10); x(1) = seed_true; for i = 2:10 x(i) = mod(a*x(i-1)+c, m); end % 映射: 状态大于m/2则选Rhi observed = (x(1:3) > m/2); % Eve已知a,c,m,枚举种子恢复后续输出 found = []; for s = 0:m-1 xs = s; ok = true; for i = 1:3 xs = mod(a*xs+c, m); if (xs > m/2) ~= observed(i) ok = false; break; end end if ok found = [found, s]; end end if ~isempty(found) fprintf('LCG种子枚举结束,候选种子数: %d\n', length(found)); xs = found(1); predicted = false(1, 4); for i = 1:4 xs = mod(a*xs+c, m); predicted(i) = xs > m/2; end true_next = (x(4:7) > m/2); fprintf('预测后续4位: %d %d %d %d\n', predicted); fprintf('真实后续4位: %d %d %d %d\n', true_next); end这段代码演示了一个很残酷的事实:只要攻击者拿到RNG的部分输出,并且知道RNG的算法参数,整个密钥序列都是可预测的。在KLJN场景里,匹配轮状态就是RNG的部分输出,拿到几十轮匹配观测,LCG的状态空间就被压缩到极少数候选,后面的密钥几乎等于公开。这也是KLJN实现指南里反复要求使用真随机数生成器(TRNG)、而不是普通PRNG代替的原因。
4. 攻击实验暴露的工程问题:熵源检测与协议加固实践
4.1 熵源检测:上线前的RNG质量体检
仿真攻击跑完,一个很自然的工程追问是:怎么在部署KLJN前就知道自己的随机源有没有这类问题?我建议至少做三件事:
- 输出级统计检测:对RNG的原始比特序列做频数检验和游程检验。频数检验看0/1比例是否接近一半,游程检验看有没有不该出现的连续块规律。
- 时域相关性检测:算自相关系数,检查滞后1到32位的相关性是否显著。
- 熵估计:用分块频率统计估计每比特熵,低于0.997基本可以直接判不合格。
下面给一个最基础的双检验函数,可以直接落到Matlab里复用:
function pass = entropy_source_check(bits, alpha) % 简单RNG质量检测: 频数检验 + 游程检验 % bits: 0/1行向量 if nargin < 2, alpha = 0.01; end n = length(bits); if n < 100 warning('样本太少, 检测结果仅供参考'); end % 1) 频数检验 n1 = sum(bits); stat1 = (n1 - n/2)^2 / (n/2) + ((n-n1) - n/2)^2 / (n/2); pass1 = chi2cdf(stat1, 1) > alpha; % 不拒绝均匀 % 2) 游程检验 runs = 1 + sum(abs(diff(bits))); mu = (n-1)/2 + 1; % 近似期望游程数 var_ = (n-1)*(n-3)/(4*(n-2)); % 近似方差 z = (runs - mu) / sqrt(var_); pass2 = abs(z) <= norminv(1 - alpha/2); pass = pass1 && pass2; end注意,游程检验样本数太小时方差公式会失效,至少采集几千比特再测。我在实际项目里一般要求原始比特不少于100k,才做最终判定。
4.2 协议层加固:去偏置、隐私放大与协商轮次控制
检测发现问题之后,最省钱的办法是在协议层做去偏置。经典做法是冯诺依曼去偏置:把输出比特两两分组,01映射为0,10映射为1,00和11直接丢弃。这样无论输入偏置多大,输出都是严格均匀独立的。代价是吞吐率下降,当输入接近均匀时约损失一半。
function debiased = von_neumann_debias(bits) % 冯诺依曼去偏置: 01->0, 10->1, 00/11丢弃 n = 2 * floor(length(bits)/2); pair = reshape(bits(1:n), 2, [])'; keep = xor(pair(:,1), pair(:,2)); debiased = pair(keep, 1); end去偏置之后还要做隐私放大。什么叫隐私放大?就是双方已经通过KLJN协商出一串“部分保密”的原始密钥矩阵,再通过一个2-universal哈希函数(例如Toeplitz矩阵乘法)把原始矩阵压缩成更短的最终密钥。哪怕Eve对原始密钥每个比特的偏置知识只有ε,隐私放大也能把Eve对最终密钥的信息量压到2^-s量级。我建议在KLJN实现里必须保留这一层,不能省。
另外,协商轮次控制也很重要。轮数越多,Eve对偏置的估计越准,攻击优势越稳定。所以要动态评估:如果检测到随机源质量恶化,就减少每轮密钥生成长度、强制重新种子化,或者切换熵源。这相当于把“被动挨打”变成“主动熔断”。
4.3 我在这套仿真里踩过的坑与调参经验
最后说几个实操中真实的坑,都是我在跑这套仿真时反复折腾出来的。
第一个坑是PSD阈值。一开始我用线性域的固定阈值,比如取PSD_lo_match和PSD_mismatch的算术平均。结果sigma_db偏大时,不匹配状态经常被误判成lo匹配。后来我把阈值改成几何平均,分类准确率立刻上来。原因是三类PSD在对数域上近似等距,几何阈值等效于对数域的中点,符合高斯加性噪声的最大似然分界。
第二个坑是卡方检验的小样本失效。仿真里如果p_alice很接近0.5,匹配轮总数可能只有三百多轮,其中lo匹配和hi匹配各一百多,卡方统计量的p值会抖动得很厉害。我后来在攻击器里加了一条规则:匹配轮样本数低于200时不做“拒绝均匀性”的硬判据,只报告偏置估计和熵损失,避免小样本误报。
第三个坑是固定随机种子。为了做攻击效果评估,我前前后后跑了很多版本,不同随机种子下分类准确率和p估计差几个百分点是常事。如果你要复现文章数字,记得用rng(20240401);如果你要评估攻击的统计稳定性,建议跑100次蒙特卡洛,看p估计的均值和方差,而不是单次结果。
第四个坑是关于隐私放大的一个误解。有些人以为“反正最后有隐私放大,RNG偏置的影响会被抹掉”,这句话只对了一半。隐私放大能把Eve的已知信息压到指数级小,但它不能凭空增加原始密钥的熵——如果原始密钥在Alice侧产生阶段就只有0.6 bit/比特的熵,隐私放大压缩后的熵也不会超过总量上限。所以要在源头把随机源质量治理好,别把隐私放大当万能灭火器。
这套仿真跑下来,我对KLJN这类物理层安全协议的态度变成四个字:理论真好,工程真脆。信息论安全的前提是每个熵源都理想,而工程世界里理想熵源几乎是奢侈品。做协议设计的朋友,建议把RNG质量检测和去偏置模块当成协议的一部分来设计,别当外挂;做攻击研究的朋友,这套统计方法也可以迁移到其他依赖随机性的物理层协议上,思路是通用的。