大多数人在 VPN 配置里遇到 nonce 这个词,就以为它是又一个需要在节点之间复制的共享密钥。并非如此。nonce 是一个在同一密钥下绝不能重复的值,而在大多数基于 nonce 的加密中,它是明文传输的。这种误解本身无害,直到有人手动修改它,真正的损坏正是从这里开始。

nonce 究竟从哪里来

认证加密要求,在同一个密钥下加密的每条消息都使用不同的 nonce。协议用不同的方式满足这一要求:单调递增的计数器、与数据包标识符结合的盐值,或者足够长的随机值。IKEv2 在初始交换中交换 nonce,它们是密钥派生的公开输入,而不是秘密。

唯一性可以是结构性的,也可以是概率性的。计数器在回绕之前不会重复,因此协议会在这之前很久就定义重新生成密钥的节点。随机值依赖足够的比特,使碰撞在消息上限内不可能发生,这是另一种论证,也有另一种失效方式。协议文档会说明适用哪种方法,两者都只有在遵守所述上限时才安全。

DesignWhere uniqueness comes fromExhaustion is handled by
AES-GCM recordsAn IV, typically a counterRekey before the counter wraps
ChaCha20-Poly1305A 96-bit nonce from a counter or generatorA message limit per key
WireGuardA per-key 64-bit counterRekey at a message cap
OpenVPN AEADPacket identifier plus implicit key materialRekey on the packet-id limit
IKEv2Fresh nonces per exchangeA new security association

重复使用究竟要付出什么代价

在同一个密钥下重复使用 nonce 不是一个小效率问题。使用 GCM 时,两条共享 IV 的消息会让观察者恢复认证密钥,并计算出两段明文的异或值。基于流的构造以同样的方式重复使用密钥流,把密文变成一道谜题,而不是一个秘密。

这就是为什么实现会在计数器回绕之前重新生成密钥,也是为什么当状态不确定时它们会停下而不是继续。当对端仍持有旧密钥时重启并重置计数器,或者从会话中途的快照恢复虚拟机,正是这些上限要防止的情形。不需要攻击者,常规操作就足够了。

为什么手动修改是错误的修复方式

计数器、盐值和数据包标识符是两端共享的状态机。改动一侧会让另一侧失去同步,隧道的故障看起来却像路由问题。受支持的手段是重新进行一次密钥交换,它会同时重置两端的状态。

密码和 nonce 也扮演不同的角色。密码是密钥派生的秘密输入;nonce 是每条消息的公开输入。一个标记为 nonce、却要求填入长秘密的配置字段,通常是换了个名字的静态密钥或预共享密钥,所以应当查阅该字段的文档,而不是想当然。

当安全公告涉及 nonce 的处理时,修复方式是维护者发布的修复版本,而不是换一种密码或缩短密钥。为了避开 nonce 这个词而更换密码,只是把问题挪走,并没有解决。

不靠猜测去读警告

关于计数器或 nonce 的日志行通常描述的是状态丢失,而不是攻击。检查是否有一端重启过、是否有克隆的虚拟机复用了同一身份,以及两端是否对重新生成密钥的设置达成一致。然后建立新会话,观察该消息是否再次出现。

把诊断与无关的故障分开。页面打不开,或者凭据提示反复循环,并不是 nonce 重复使用的证据,把它当成证据会让你钻进密码学层,而问题其实在它上面。

对于普通使用,安全的规则很短:让实现去管理 nonce,让两端都停留在受支持的版本上,并把任何状态重置都当作重新生成密钥的理由,而不是去修改某个值。如果配置字段不在厂商文档里,就不要为它编造一个值。修好隧道,首先要弄清自己看的是哪一层。

相关文章