密码学 10:两端如何得到共同秘密,为什么仍怕中间人
浏览器与 HTTPS 端点第一次见面,却能协商供双方使用的流量密钥。是不是把这把密钥直接从网络上发过去了?不是。09 用小整数说明:双方交换各自算出的公开值,再用自己没有外发的私有输入,最终可以得到同一个结果。问题在于:没有人核对交换来的公开值到底属于谁,攻击者也能和两边分别完成运算。本篇将“共同秘密已算出”和“对端身份已确认”拆成两个各自可失败的断言。
这里会用成熟库执行真实 X25519 椭圆曲线密钥交换,但不运行 TLS 端点。真实库结果仅证明在本次输入下原语计算正确;服务名、证书、握手摘要与通信保护要在 13–16 章继续组合。
正常路线:线上传的不是最终秘密
仍用 09 的手算:公开模数 23、生成元 5,甲有私有指数 6,乙有私有指数 15。甲发公开 share 8,乙发公开 share 19;两人将对方公开 share 与自己的私有指数结合,分别算到结果 2。
1 | |
从公式可以看出,诚实双方确实同意一个数字。数字 2 在如此小的空间毫无安全性,旁观者随手即可穷举。真正协议采用专门选择的组或曲线和足够强的参数,并借助密钥派生器把共同输入变成分用途、分方向的密钥。即便用到真实参数,DH 的数学关系也只能证明“我与我刚才使用的那个公有值相配的私有值持有人共享了一个秘密”,不能证明这个公有值来自预期的 HTTPS 网站。
这一点不靠术语即可检验:如果交换的公开值可被替换,密钥交换仍会成功,只不过成功发生在错误的人之间。
中间人拆开一次交换
攻击者能截下甲、乙的公开 share,自己为两边分别准备指数 7 和 11。对甲声称“这是乙的 share 17”,对乙声称“这是甲的 share 22”。下面所有数字都是公开的小参数教学值,不是抓包或真实私人密钥。
1 | |
攻击者知道两条会话各自的共同结果,若没有之后的身份认证和握手绑定,可一边解读消息、一边转给另一端,双方各自误以为已有安全通道。这是“DH 不认证对端”最直接的反例。**删掉谁的身份绑定会出事?**如果客户端没检查服务端公钥与可信身份的关系,冒充者自己的公开值可以作为“服务端 share”进入运算;仅要求双方各自完成运算挡不住它。
在 TLS 1.3 的证书认证握手中,协议还会把握手摘要、证书身份、服务端对相应握手上下文的认证,以及双方对派生秘密的确认结合起来。证书里的公钥一般不是直接“加密所有应用订单”;双方派生得到流量密钥后用认证加密保护记录。具体 CertificateVerify、Finished 的密钥与对象各不相同,15–16 才逐条核对 RFC 9846;本篇不把一句“加个签名”冒充完整握手说明。
真实曲线原语并不会自动确认身份
examples/cryptography/10_x25519.mjs 用 Node 22.23.2 的 node:crypto 创建四组临时 X25519 密钥:甲、乙以及中间人的两个独立会话。诚实甲乙的派生结果相等;给甲换成攻击者左侧公开值,甲与攻击者左侧派生结果相等;给乙换成攻击者右侧公开值,乙与攻击者右侧结果相等;两侧的结果不同。实际私钥和共享字节只在内存中使用,公开输出只显示长度和比较布尔值。
为了不让“用同一个库对两边算,结果相等”冒充算法正确性,测试还用 RFC 7748 §6.1 的公开 Alice 私钥样例与 Bob 公钥样例,核对 Alice 公开值和最终共享值与规范给出的完整十六进制字节一致。这些字节是标准公布的测试向量,绝非现实服务的敏感私钥。Node KeyObject 的测试封装使用 RFC 8410 对 X25519 的 PKCS#8/SPKI 编码;不手写曲线标量乘法。
1 | |
2026-10-06 UTC,Python 3.12.3 下两个小参数测试通过、脚本退出 0;Node 22.23.2 下两个测试通过、真实 X25519 原语脚本退出 0。小模型输出甲乙正常 2/2、攻击者两侧 12/12 与 22/22;Node 输出诚实与各局部对端相等为真、两侧共用同一秘密为假,派生长度为 32 字节。具体的随机私有值和派生秘密没有保存在公开证据中;再次运行只能复现拓扑、断言和规范固定向量,不能复现随机那轮的秘密值。
再加入哪一步才可能解决冒充
需要把预期身份(例如网站域名)、受信公钥来源、这次会话的 key share 与握手内容绑定到一起,而且验证者确实检查绑定结果。浏览器的证书链能提供一个信任起点,但如果客户端允许任意自签证书、关掉服务名检查,依然可能连接到错误人。SSH 的主机公钥和 WebAuthn 的 RP 身份又是不同绑定体系;相同 DH 运算在不同协议中不能代替对端认证规则。
即使最终身份已正确确认,共同原始结果也还不是“直接拿来当两把流量密钥”的完整设计。应在 11 使用带上下文的 KDF 分出不同阶段、不同方向与用途的密钥。KDF 不负责识别对端,也不能修复一个被中间人成功替换却未校验身份的 key share;它只加工已有秘密和明确上下文。
两道带答案的练习
画图题。 在图里给出三对实际共享者,以及甲、乙分别“以为自己在和谁通信”。若服务端只检查“这次有共同秘密”,为什么不能拒绝中间人?
可核对答案: 正常交换里共享的是甲—乙;被替换时共享的是甲—攻击者左侧和乙—攻击者右侧,两边值分别 12 与 22。甲、乙会错误地以为对方是直接会话伙伴;本地计算只见“与收到的 share 持有人有共同结果”,并没有关于对方真实身份的独立可信输入,因此不能凭此拒绝。
变更题。 在 10_dh_mitm.py 中把 attacker_left_secret 改成 9,不改乙侧攻击者指数。重新运行脚本和测试,哪些值会改变?若只验证“双方各自能算出某个值”,原攻击是否会消失?
可核对答案: 左侧假 share、甲—攻击者共同结果随新指数改变;乙侧 22/22 仍按原输入计算。写死 12/12 的断言将失败,这是测试准确捕捉输入变化,不是 MITM 的消失:攻击者仍控制给甲看的公有值,并能计算和甲相同的结果。没有真实身份绑定,修改一组指数不能修补协议。
资料与导航
- RFC 7748,§5–6.1:X25519 密钥交换与官方公开测试向量;RFC 8410:测试中 KeyObject 的编码。
- RFC 9846,TLS 1.3:认证握手的后续研究入口;本章没有抓到 TLS 真实握手。
- 09 模运算、群与困难问题 · 07 AEAD 保护内容与上下文 · 11 一个秘密为什么派生多把密钥。






