朋友不会让朋友重用Nonce - Trail of Bits博客
Joe Doyle
2024年9月13日
密码学
如果你接触过密码学软件,可能听过"切勿重复使用nonce"的建议——事实上,nonce(number used once)这个词的本意就是"只用一次的数字"。根据使用的密码学方案,重复使用nonce可能泄露加密信息,甚至泄漏你的密钥!但常识可能无法覆盖所有意外重用nonce的情况。有时,那些本应防止nonce重用的技术存在微妙缺陷。
本文讲述了一个警示故事:在实现相对基础的密码学类型(如双向加密通道,例如加密语音通话或加密聊天)时可能出现的问题。我们将探讨在具有多个加密通道的网络中如何出现这类更微妙的问题,并描述我们在客户阈值签名方案中发现的一个漏洞。在该实现中,所有参与方都从未重复使用过同一个nonce。然而,由于他们使用了相同的nonce值序列,两个不同的发送方可能意外地使用了相同的nonce。攻击者可能利用此问题篡改消息,或使诚实方显得恶意。
加密通道的构建原理
加密消息——即使第三方完全访问消息内容也能隐藏其含义——可能是我们认可的最古老的"密码学"活动。当今消息加密的核心结构至少可追溯到1500年代的多字母替换密码,基本流程如下:
加密过程:
- 获取秘密消息并将其分割为规则大小的部分(或"块")
- 根据密钥、消息位置以及可能的前序符号,将每个符号替换为不同符号
- 发送加密后的消息
解密过程:
- 获取加密消息并分割为块
- 使用加密过程的逆操作替换每个符号,同样使用密钥、位置和可能的前序符号
- 读取解密后的消息
该方案的安全性依赖于第三方无法仅通过查看加密数据推断符号替换过程的信息。
历史上,许多密码通过观察单个加密消息中的模式被破解(艾伦·图灵破解纳粹海军Enigma加密的Banburismus技术就是著名例子)。现代密码在设计上如果正确使用,可以完全消除这些消息内模式。
首先,我们的替换字母表要大得多——两种常用流密码AES-CTR和ChaCha20分别使用128和256位的块大小,意味着字母表分别有2¹²⁸和2²⁵⁶个符号。其次,有规则确保消息中的每个符号获得不同的替换表。如果你以相同方式处理每个符号,就可能像经典ECB企鹅那样暴露底层消息的模式!
图2:原始图像(来源)
图3:ECB模式加密后的图像(来源)
最后,也是本文最重要的:需要确保每条消息都被不同处理——这就是nonce的用武之地。
数字,但只能用一次
AES-CTR和ChaCha20流密码都是"计数器模式"流密码。计数器模式密码使用非常简单的替换表:将第i个块的值x_i映射到x_i XOR F(i),其中F是从密钥派生的所谓"伪随机函数"。
为展示其工作原理,让我们再次使用可靠的Tux图像和从AES-CTR伪随机函数生成的图像:
图4:再次展示原始图像
图5:从AES-CTR伪随机函数生成的图像
当我们将伪随机图像与Tux进行XOR运算时,Tux消失在噪声中:
图6:伪随机图像与Tux的XOR结果
可能不明显这实际上仍包含Tux——但如果你仔细观察下面的动画,可以在从原始噪声切换到加密版Tux时看到Tux的轮廓:
图7:Tux与AES-CTR输出混合的动画;注意Tux的可见轮廓
如果我们再次与噪声进行XOR,Tux又回来了!
图8:XOR后再次可见的Tux
只要我们知道用于生成伪随机数据的函数F,这让我们既能加密也能解密数据。
但如果不小心,我们可能泄露太多信息。让我们从不同的图像开始,但使用相同的噪声:
图9:不同示例:Beastie(来源)
图10:相同噪声
当我们将图像和噪声进行XOR时,Beastie像Tux一样消失:
图11:Beastie消失在噪声中
但如果我们现在将这两条加密消息进行XOR,突然可以分辨出它们原本是什么!
图12:两条加密消息XOR后Beastie和Tux重现
哪里出错了?我们在每条加密消息中使用了完全相同的噪声。在实际加密通道中,用于生成噪声的伪随机函数F有一个额外参数,称为"nonce"或"只用一次的数字"。顾名思义,该数字对每条消息应该是唯一的。如果重复使用nonce,看到两条加密消息的第三方可以获知明文的XOR结果。然而,只要从不重复使用nonce,良好的伪随机函数将在给定两个不同nonce时生成完全不同的噪声。
通过调整上述实验,对Tux使用nonce 1,对Beastie使用nonce 2,两条消息的XOR仍然是不可理解的噪声:
图13:加密的Tux
图14:加密的Beastie
图15:前两张图像的XOR结果
这就引出了我们要讨论的漏洞。
漏洞详情
我们的客户正在实现一个阈值签名方案。阈值签名方案中的签名过程需要所有参与方之间进行大量通信。有些通信是广播式的,有些是对等式的。为了安全,对等通信需要既私密又防篡改,因此实现使用了称为ChaCha20-Poly1305的认证加密方案,该方案结合了ChaCha20流密码和Poly1305多项式消息认证码。
考虑一个包含Alice、Bob和Carol的三方示例。为创建对等通道,Alice分别通过Diffie-Hellman密钥交换与Bob和Carol建立两个不同的共享秘密s_B和s_C。然后,Alice设置一个全局"nonce计数器":每次Alice发送消息时,她使用计数器的当前值发送,然后递增计数器。这样她绝对永远不会在不同通道上发送两条具有相同nonce的消息!
不幸的是,所有参与方都将计数器初始化为相同的值(0),以相同的速率递增,并按相同的顺序发送消息。因此在第一步中,当Alice向Bob发送消息,而Bob向Alice发送消息时,他们都使用秘密s_B和nonce 0!所以拦截这两条消息的窃听者可以获知它们的XOR内容。同样,Bob和Carol将使用nonce 1相互发送消息,然后在下一轮中Alice和Bob都将使用nonce 2。然而,Alice和Carol总是使用不同的nonce——Alice是Carol的第一个接收方,而Carol是Alice的第二个接收方——所以Alice到Carol的nonce将始终为奇数,而Carol到Alice的nonce将始终为偶数。
在发生此漏洞的实际系统中,使用相同nonce的消息恰好具有高度结构性,且被XOR的重要字段本身是伪随机的。这意味着窃听者无法获得足够信息来利用这些消息进行直接攻击。然而,这种特定的nonce重用确实泄漏了消息认证密钥,并允许中间人篡改某些消息,导致其他参与者将诚实方视为潜在恶意方。
修复方案
每当有通信通道时,正确管理涉及的nonce以确保从不重复任何nonce极其重要。一个快速但不完美的方法是在各方之间划分nonce空间。在上面的示例中,Alice和Carol巧合地总是具有不同的nonce奇偶性,你可以有意这样做:在每个通道中,以某种方式指定一方为"奇数"方,另一方为"偶数"方,然后发送消息时,如果你是偶数方实际使用2n,如果是奇数方使用2n+1。
然而,更好的方案是为每个方向使用完全独立的密钥:换句话说,Alice使用秘密s_AB加密发送给Bob的消息,使用s_BA解密来自Bob的消息。同样,Bob使用s_BA加密,使用s_AB解密。[Noise协议框架]就是这样做的,它要求为发送和接收使用不同的CipherState对象。有几种方法可以从单个共享秘密派生这些"方向密钥",但通常我们建议使用经过充分验证的现有实现,如Noise协议框架。许多此类问题在这些实现中已得到主动处理。
切勿重用Nonce!
归根结底,仔细评估密码系统的每个假设和限制非常重要,并确保所有缓解措施真正应对实际威胁。对Nonce重用的一个简单心理简化是"不要用相同nonce发送两条消息"——在该简化模型中,全局nonce计数器有效!然而,nonce重用的实际威胁不在乎谁发送消息——如果任何人使用相同密钥和nonce发送消息,你就面临风险。
大多数知名加密通道库都安全处理了此问题,但如果你发现需要实现此类解决方案,请考虑联系我们进行密码学审查。
-
尽管这是对计数器模式加密的忠实描述,但许多称为"伪随机"的函数完全不适合加密使用。尽可能使用经过充分验证的流密码并遵循行业最佳实践。
-
一些加密方案有各种超出避免nonce重用之外的限制——在某些方案中,过长的消息可能导致类似nonce重用的问题。一些方案根据你是随机生成nonce还是使用计数器有不同的建议。通常,请使用经过充分验证的加密实现,并确保遵循相关规范或标准中的所有建议。
-
这需要将有效nonce大小减少1位,因此通常我们不推荐此方法!
如果你喜欢这篇文章,请分享: Twitter • LinkedIn • GitHub • Mastodon • Hacker News
页面内容
如何构建加密通道 • 数字,但只能用一次 • 漏洞详情 • 修复方案 • 切勿重用Nonce!
最近文章
使用Deptective调查依赖项 • 做好准备,Buttercup,AIxCC评分轮正在进行中!• 使智能合约超越私钥风险的成熟方法 • Go解析器中意外的安全隐患 • 我们审查Silence Laboratories首批DKLs23库的收获
© 2025 Trail of Bits.
使用Hugo和Mainroad主题生成。