Hotdry.
ai-security

Libsodium Ed25519点验证漏洞:技术分析与工程修复

分析libsodium中crypto_core_ed25519_is_valid_point()函数的技术缺陷,该漏洞导致混合阶子群点被错误接受。探讨Edwards25519曲线子群结构、修复方案,以及密码学库安全审计的工程实践。

在密码学库的安全记录中,libsodium 一直保持着令人瞩目的成绩 ——13 年零 CVE。然而,2025 年 12 月 30 日,libsodium 维护者 Frank Denis 披露了一个存在于低级别函数crypto_core_ed25519_is_valid_point()中的验证漏洞。这个漏洞虽然不影响大多数标准使用场景,却揭示了密码学库在低级别 API 设计、验证逻辑完整性以及安全审计实践中的重要教训。

漏洞的技术本质:不完整的点验证

crypto_core_ed25519_is_valid_point()函数的设计目的是验证给定的椭圆曲线点是否位于 Edwards25519 曲线的主子群(main subgroup)中。在椭圆曲线密码学中,Edwards25519 曲线包含多个不同阶的子群:

  • 阶 1:仅包含恒等点 (0, 1)
  • 阶 2:恒等点 + 点 (0, -1)
  • 阶 4:4 个点
  • 阶 8:8 个点
  • 阶 L:主子群(约 2^252 个点),所有密码学操作应在此进行
  • 阶 2L、4L、8L:非常大的非素数阶子群

验证点是否在主子群中的标准方法是:将点乘以群阶 L,然后检查结果是否为恒等点。如果点在主子群中(阶为 L),乘以 L 后应得到恒等点。

漏洞的核心在于实现的不完整性。在 libsodium 的原始实现中,函数只检查了 X 坐标是否为 0,而忘记了验证 Y 坐标是否等于 Z 坐标。在 Edwards25519 的射影坐标表示中,恒等点的正确表示是 X=0 且 Y=Z(Z 可以是任意值,取决于之前的操作)。

// 有漏洞的旧代码
return fe25519_iszero(pl.X);

这意味着,对于某些不在主子群中的点,如果它们乘以 L 后得到 X=0 但 Y≠Z 的点,就会被错误地接受为有效点。具体来说,取任何主子群点 Q,加上阶 2 点 (0, -1)(或等价地取两个坐标的负值),得到的点 Q + (0, -1) 都会通过验证,尽管它不在主子群中。

影响范围与风险评估

这个漏洞的影响范围相对有限,但需要仔细评估:

受影响的情况

  1. 直接使用低级别函数的自定义协议:如果应用程序直接调用crypto_core_ed25519_is_valid_point()来验证来自不可信来源的点,并且依赖此验证结果进行后续操作,则可能受到影响。

  2. 自定义密码学方案:实现基于 Edwards25519 曲线的自定义密码学方案(非标准 Ed25519 签名)的开发者,如果使用该函数进行点验证,需要检查其实现。

  3. 版本依赖:使用 libsodium 1.0.20 及更早版本,或在 2025 年 12 月 30 日之前发布的任何版本。

不受影响的情况

  1. 标准 Ed25519 签名:高等级 API 如crypto_sign_*系列函数完全不受影响,因为它们根本不使用crypto_core_ed25519_is_valid_point()函数。

  2. 密钥生成:通过crypto_sign_keypair()crypto_sign_seed_keypair()生成的公钥保证位于正确的子群中。

  3. 标量乘法:即使公钥不在主子群中,使用crypto_scalarmult_ed25519进行标量乘法也不会泄露任何信息。

Frank Denis 在披露中强调:"大多数用户不受影响。不要恐慌。" 这种谨慎的表述反映了维护者对漏洞影响的准确评估。

修复方案与工程实现

核心修复

修复方案简洁而有效,在 GitHub 提交f2da4cd8cb26599a0285a6ab0c02948e361a674a中实现:

// 修复后的代码
fe25519_sub(t, pl.Y, pl.Z);
return fe25519_iszero(pl.X) & fe25519_iszero(t);

现在函数正确验证两个条件:X 必须为 0,且 Y 必须等于 Z。这个修复已包含在 2025 年 12 月 30 日之后发布的所有稳定包中。

临时解决方案

对于无法立即更新 libsodium 的系统,Frank Denis 提供了一个应用层的工作函数:

int is_on_main_subgroup(const unsigned char p[crypto_core_ed25519_BYTES])
{
    /* l - 1 (group order minus 1) */
    static const unsigned char L_1[crypto_core_ed25519_SCALARBYTES] = {
        0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
        0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
    };
    /* Identity point encoding: (x=0, y=1) */
    static const unsigned char ID[crypto_core_ed25519_BYTES] = {
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    unsigned char t[crypto_core_ed25519_BYTES];
    unsigned char r[crypto_core_ed25519_BYTES];
    if (crypto_scalarmult_ed25519_noclamp(t, L_1, p) != 0 ||
        crypto_core_ed25519_add(r, t, p) != 0) {
        return 0;
    }
    return sodium_memcmp(r, ID, sizeof ID) == 0;
}

这个工作函数实现了完整的验证逻辑,可以作为临时解决方案。

工程实践:从 Ristretto255 中学习

这个漏洞的根本原因与 Edwards25519 曲线的 cofactor(余因子)问题相关。实际上,libsodium 在 2019 年就引入了 Ristretto255 组的支持,专门解决这类 cofactor 相关问题。

Ristretto255 的优势

  1. 简化验证:在 Ristretto255 中,如果一个点能够解码,它就是安全的。不需要额外的子群验证。

  2. 性能提升:低级别操作在 Ristretto255 上比在 Edwards25519 上运行得更快。

  3. 消除歧义:Ristretto255 提供了从椭圆曲线群到素数阶群的唯一编码,完全避免了 cofactor 问题。

迁移建议

对于实现自定义密码学方案并进行有限域群算术的开发者,强烈建议:

  1. 优先使用 Ristretto255:新项目应直接基于 Ristretto255 构建。

  2. 现有代码评估:评估现有 Edwards25519 实现中是否包含类似的验证不完整问题。

  3. 测试覆盖:确保测试套件包含边缘情况,特别是混合阶子群点的验证。

密码学库安全审计的启示

1. 低级别 API 的风险

libsodium 的设计哲学是提供高级别、安全的 API,让用户无需了解底层算法细节。然而,开发者社区逐渐开始直接使用低级别函数,将其视为算法工具箱。这种使用模式与库的设计初衷相悖,却反映了实际需求。

工程实践:密码学库应明确区分 "稳定 API" 和 "实验性 / 低级 API",并在文档中清晰标注风险等级。libsodium 通过--enable-minimal构建标志来标识稳定 API,这是一个值得借鉴的模式。

2. 验证逻辑的完整性

这个漏洞揭示了验证逻辑中一个常见的陷阱:部分验证可能看起来足够,但实际上存在隐蔽的缺陷。在密码学中,"足够好" 往往意味着 "不够安全"。

审计要点

  • 所有验证函数应有完整的数学证明支持
  • 测试套件应包含所有理论上的边缘情况
  • 代码审查应特别关注验证逻辑的完整性

3. 跨语言实现的一致性

漏洞是在与 Zig 语言实现的比较中发现的。Zig 版本包含了正确的检查,而 C 版本遗漏了。这强调了跨语言实现一致性检查的重要性。

监控策略

  • 建立跨语言实现的定期对比测试
  • 使用形式化验证工具检查关键函数的一致性
  • 实施差异驱动的测试生成

4. 维护者可持续性

Frank Denis 在披露中坦诚地提到,libsodium 由一个人维护,时间有限。这反映了开源密码学库面临的普遍挑战:关键基础设施依赖于志愿者的有限时间。

支持机制

  • 企业用户应考虑赞助关键密码学库
  • 建立维护者轮换或备份机制
  • 开发自动化测试和发布流水线减轻维护负担

应急响应与修复部署

libsodium 的应急响应展示了良好的安全实践:

及时修复

  • 漏洞发现后立即修复
  • 修复提交到主分支
  • 所有发布渠道同步更新

透明披露

  • 详细的技术分析
  • 清晰的影响范围说明
  • 提供临时解决方案
  • 完整的修复时间线

包管理协调

修复已部署到所有主要分发渠道:

  • 官方 tarball
  • Visual Studio 和 MingW 二进制文件
  • NuGet 包(包括 Android 架构)
  • swift-sodium xcframework
  • Rust libsodium-sys-stable
  • libsodium.js

结论与建议

libsodium 的 Ed25519 点验证漏洞虽然影响有限,但提供了宝贵的安全工程教训:

  1. 对于 libsodium 用户:如果使用高等级 API,无需立即行动;如果使用低级别 Edwards25519 函数,应升级到修复版本或实施工作函数。

  2. 对于密码学开发者:优先使用 Ristretto255 而非 Edwards25519 进行新开发;审查现有代码中的类似验证不完整问题。

  3. 对于安全工程师:将密码学库的低级别 API 纳入安全审计范围;建立跨语言实现的一致性检查流程。

  4. 对于开源维护者:考虑实施更严格的 API 稳定性保证;建立可持续的维护模式。

密码学安全是一个持续的过程,而不是一次性的成就。libsodium 在 13 年后发现的这个漏洞提醒我们,即使是最受信任的密码学库也需要持续的审查、测试和维护。通过从这些事件中学习,我们可以构建更安全、更可靠的密码学基础设施。


资料来源

  1. Frank Denis. "A vulnerability in libsodium." 00f.net, December 30, 2025. https://00f.net/2025/12/30/libsodium-vulnerability/
  2. libsodium GitHub 提交记录. https://github.com/jedisct1/libsodium/commit/f2da4cd8cb26599a0285a6ab0c02948e361a674a
查看归档