Nym 安全审查报告(Nym Security Review)
Nym 核心组件安全审计
背景
2021年7月,Nym 接受了知名安全专家让-菲利普·奥马松(Jean-Philippe Aumasson)的安全审计。 本次审计旨在找出 Nym 代码库中潜在的漏洞和缺陷,重点关注密码学、代码安全以及协议安全。 本次审查旨在确保系统的完整性与鲁棒性,涵盖了其架构和实现的关键方面。 您可以在此处查看完整的审计报告。
审计摘要
审计工作于 2021 年夏季展开。 本次审计的范围涵盖了 Nym 项目的多个代码库,包括:
- Nym 主代码库:nymtech/nym,实现了混合节点(mixnode)、网关(gateway)和验证者(validator)服务。
- Sphinx 混合网数据包格式:nymtech/sphinx,供混合节点(mixnodes)使用。
- Coconut 门限盲签名协议:nymtech/coconut,用于凭证发放。
审计工作对开发分支的最新代码进行了审查。由于该项目演进迅速,该分支的代码会定期进行刷新更新。 Nym 团队向让-菲利普·奥马松(JP Aumasson)开放了代码库的全量访问权限,并提供了详细的项目规范文档、研究论文及相关配套文档。
审计的重点领域
本次审计重点关注三大核心领域:代码安全、密码学以及协议设计。 审计人员对 Nym 的代码库进行了彻底审查,以识别潜在漏洞,重点排查了不安全的代码模式和错误处理不当等常见问题。 审计工作特别关注了内存安全性,并着重避免了诸如整数溢出之类的潜在陷阱。
密码学审查涵盖了 Nym 采用的一系列核心原语(Core Primitives),例如 AES-128-CTR、BLAKE2b、ChaCha、Ed25519 等。 其目标是确保这些算法得到正确实现,并验证它们对侧信道攻击、参数误用以及随机数生成缺陷的防御能力。
除了审查各个独立组件外,本次审计还深入评估了定义 Nym 核心功能的各项协议。 这涉及对 Sphinx 数据包格式以及 Coconut 门限盲签名协议的审查,以识别可能损害隐私或安全性的任何潜在漏洞。 审查重点关注了威胁用户匿名性的潜在信息泄露问题,以及 MAC 验证绕过、小子群攻击(Small Subgroup Attacks),以及 BLS12-381 算术与零知识证明等密码学操作中的漏洞。
调查结果概述
审计期间共发现了 9 个安全漏洞。 其中无严重漏洞,2 个高危漏洞,1 个中危漏洞,以及 6 个低危漏洞。 此外,审计还提出了 17 项审查意见(Observations),为进一步增强 Nym 网络的安全性提供了建议。 下文对针对所有已识别安全漏洞所实施的修复措施进行了概述。
S-SPHX-01:缺失密钥校验(低危)
在 nymtech/sphinx 中识别出的关于私钥和公钥校验不足的问题,已通过迁移至 x25519 库得到解决。该库在设计上就原生强制保证了密钥的有效性。 x25519 的实现确保了私钥会被正确地“夹紧”(Clamped),这意味着私钥会被自动限制在标量(Scalars)的有效子集中,从而消除了使用无效私钥的风险。 此外,虽然 x25519 不会显式地对公钥进行校验,但其蒙哥马利梯子(Montgomery Ladder)算法实现在设计上阻止了某些无效曲线点的使用。 这降低了遇到无穷远点(Points at Infinity)或其他畸形公钥的风险。 此外,密钥交换过程确保了在必要时能够检测并拒绝全零的共享密钥(All-Zero Shared Secret)。 通过使用 x25519 ,我们大大改进了密钥验证,减少了系统内使用无效或格式不当的密钥的风险。 这有效地解决了审计中提出的关于密钥校验的疑虑。
S-COCO-01:哈希转标量存在偏置分布(低危)
在 BLS12-381 域中导出标量的哈希过程,先前涉及将哈希函数的输出直接映射为标量域元素。 然而,这种方法由于模还原操作(mod p)引入了偏置;当哈希输出未能与域的质数模数完美整除匹配时,会导致生成非均匀的概率分布。 为了解决这个问题,我们用来自 bls12_381 的hash_to_field 函数替换了现有的哈希转标量(hash-to-scalar)函数。 该函数遵循标准化哈希转域规范,确保了哈希输出到域元素之间均匀且无偏置的映射。 此外,替代了 Coconut 协议的 zk-Nym 协议同样依赖该函数进行哈希操作,从而确保了密码学计算中的一致性与正确性。
S-COCO-02:keygen-cli 密钥文件权限设置缺陷(低危)
审计人员指出,keygen-cli 生成的密钥文件使用的是默认访问权限,但为了更高的安全性,应当对其设置更严格的权限控制。 该问题无需进行任何修改,因为 keygen-cli 仅在我们 Coconut 实现的早期阶段被使用。 它已不再属于 Nym 的主代码库,且不再被使用。 由于该工具已被废弃,因此无需针对其文件权限采取进一步措施。
S-CRYP-01:流密码初始化向量(IV)可能存在复用风险(高危)
该问题涉及一个使用初始化向量(IV)对数据进行加密的函数。 如果未提供初始化向量,该函数会默认使用全零初始化向量,这可能会导致初始化向量复用。 该问题已通过在注册阶段引入 AES-GCM-SIV 协议得以解决。 为了防止降级攻击,我们移除了 在客户端与网关通信中使用传统 AES-128-CTR 密钥的选项。 只要客户端和网关都已更新,它们就始终会生成随机且非零的初始化向量。
S-PROT-01:凭证潜在的双重支出风险(高危)
审计人员指出,受审版的 Coconut 缺乏双花检测机制,这可能会导致多个网关同时将同一个凭证错误地验证为未使用状态。 然而,Coconut 带宽合约随后进行了更新,通过在链上存储状态来跟踪凭证的使用情况,从而解决了这个问题。 此外,Coconut 此后已被 zk-nyms 协议取代,该协议内置了双花检测功能。
S-NYM-01:依赖项存在已知漏洞(中危)
审计人员发现,Nym 项目中的多个组件使用了包含已知安全漏洞的旧版本依赖项。 例如,Sphinx 仓库使用了存在安全漏洞的 generic-array crate 版本,Nym 的核心仓库依赖了过期的 tokio,而 Nym 客户端包含多个存在已知安全问题的 crate,其中部分问题在 Nym 的具体使用场景下似乎是可以被利用的。 我们遵循审计人员的建议,对依赖项进行了定期更新。 我们现在通过 Dependabot 对依赖项进行主动管理,它能持续监控并自动检测我们仓库中过时或存在漏洞的依赖包。 请查看我们仓库中部署的 Dependabot 程序。
S-NYM-02:解密失败未作处理(低危)
在代码中发现的该问题发生在 nym/common/nymsphinx/acknowledgments/identifier.rs 的 recover_identifier 函数中,该函数未对无效参数等解密失败情况进行妥善处理。 这可能会导致程序崩溃(Panic)或其他不安全的状态。 类似的隐患也存在于 nym/common/nymsphinx/src/receiver.rs 的 recover_plaintext 函数中。 未能有效管理解密错误可能会导致系统进入不稳定或不安全的状态。 在仔细审查相关函数后,我们得出结论:第一个提到的例子(即 recover_identifier 中存在的问题)实际上并不构成问题。 由于所有内容均使用 AckEncryptionAlgorithm 进行参数化(链接),传错参数是不可能的,这意味着任何无效值都会在编译时被捕获。
他们提到的第二个问题(即 recover_plaintext)现已不复存在。 不过,取代它的代码 recover_plaintext_from_regular_packet 确实包含一种理论上可能的失败情况,前提是我们将其中的某些参数修改为了异常值。 我们通过添加适当的错误处理机制解决了这个问题,以确保系统即使在极端情况下也能保持稳定。
S-NYM-03:分片 ID 生成时出现程序崩溃(低风险)
用于生成新分片 ID(fragment IDs)的代码会随机选择一个 i32 类型的值,并取其绝对值。 然而,这种方法在随机选中的值为 i32::MIN(i32 的最小值)时会导致程序崩溃,因为它的绝对值超出了 i32 的表示范围。 这会导致“尝试进行取反溢出”(attempt to negate with overflow)错误,原因在于补码(2-complement)编码的表达限制。 我们同意将此问题的严重性等级定为“低”(Low),因为遇到这种情况的概率约为 40 亿分之一。 不过,我们已经通过实现审计员推荐的修复方案解决了该问题,以防止潜在的 Panic 和意外崩溃——尤其是在数百万用户生成数千个分片 ID 的场景下。
S-NYM-04:助记词未进行内存清零(低风险)
该问题指出,用于连接 nymd 服务的 BIP39 助记词在使用后未进行内存清零,导致内存中留存了该助记词的多份副本。 这增加了泄露风险,因为与原始密码学密钥相比,助记词在内存中更容易被识别出来。 为了解决这一问题,我们已采取了多项缓解措施。 在 Rust 组件中,我们集成了 zeroize 组件,以确保任何包含助记词的内存空间在其离开作用域(out of scope)时立刻被安全擦除。 鉴于 JavaScript 是一门带垃圾回收(garbage-collected)的语言,无法直接控制内存的释放,我们在 JavaScript 运行时环境中采取了不同的方法来降低泄露风险。 具体而言,我们对系统进行了修改:一旦助记词被传递给 Rust 层,JavaScript 运行时就会立刻终止。 这确保了在运行时关闭时,JavaScript 内存中所有的助记词实例都会被清除。 助记词会被尽可能早地传递给 Rust 层,从而减少其在 JavaScript 环境中的暴露时间。 一旦进入 Rust 层,zeroize 就能确保所有助记词实例在不再需要时立即被安全擦除。 通过这些修改,我们大幅减少了助记词在内存中的驻留,降低了泄露风险,并确保敏感数据得到安全管理与擦除。
致谢
我们要感谢 JP Aumasson 在整个审计过程中展现的专业素养与敬业精神。 我们对贵方在本次审计的筹备规划与具体实施阶段所展现出的高度协同与卓越专业性表示由衷赞赏。 保障系统安全是我们贯穿始终的重中之重。展望未来,我们将继续深化与业内顶尖安全团队的合作,共同为生态系统的稳健发展夯实最高标准的安全防线。