在密码学库的安全记录中,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) 都会通过验证,尽管它不在主子群中。
影响范围与风险评估
这个漏洞的影响范围相对有限,但需要仔细评估:
受影响的情况
-
直接使用低级别函数的自定义协议:如果应用程序直接调用
crypto_core_ed25519_is_valid_point()来验证来自不可信来源的点,并且依赖此验证结果进行后续操作,则可能受到影响。 -
自定义密码学方案:实现基于 Edwards25519 曲线的自定义密码学方案(非标准 Ed25519 签名)的开发者,如果使用该函数进行点验证,需要检查其实现。
-
版本依赖:使用 libsodium 1.0.20 及更早版本,或在 2025 年 12 月 30 日之前发布的任何版本。
不受影响的情况
-
标准 Ed25519 签名:高等级 API 如
crypto_sign_*系列函数完全不受影响,因为它们根本不使用crypto_core_ed25519_is_valid_point()函数。 -
密钥生成:通过
crypto_sign_keypair()和crypto_sign_seed_keypair()生成的公钥保证位于正确的子群中。 -
标量乘法:即使公钥不在主子群中,使用
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 的优势
-
简化验证:在 Ristretto255 中,如果一个点能够解码,它就是安全的。不需要额外的子群验证。
-
性能提升:低级别操作在 Ristretto255 上比在 Edwards25519 上运行得更快。
-
消除歧义:Ristretto255 提供了从椭圆曲线群到素数阶群的唯一编码,完全避免了 cofactor 问题。
迁移建议
对于实现自定义密码学方案并进行有限域群算术的开发者,强烈建议:
-
优先使用 Ristretto255:新项目应直接基于 Ristretto255 构建。
-
现有代码评估:评估现有 Edwards25519 实现中是否包含类似的验证不完整问题。
-
测试覆盖:确保测试套件包含边缘情况,特别是混合阶子群点的验证。
密码学库安全审计的启示
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 点验证漏洞虽然影响有限,但提供了宝贵的安全工程教训:
-
对于 libsodium 用户:如果使用高等级 API,无需立即行动;如果使用低级别 Edwards25519 函数,应升级到修复版本或实施工作函数。
-
对于密码学开发者:优先使用 Ristretto255 而非 Edwards25519 进行新开发;审查现有代码中的类似验证不完整问题。
-
对于安全工程师:将密码学库的低级别 API 纳入安全审计范围;建立跨语言实现的一致性检查流程。
-
对于开源维护者:考虑实施更严格的 API 稳定性保证;建立可持续的维护模式。
密码学安全是一个持续的过程,而不是一次性的成就。libsodium 在 13 年后发现的这个漏洞提醒我们,即使是最受信任的密码学库也需要持续的审查、测试和维护。通过从这些事件中学习,我们可以构建更安全、更可靠的密码学基础设施。
资料来源:
- Frank Denis. "A vulnerability in libsodium." 00f.net, December 30, 2025. https://00f.net/2025/12/30/libsodium-vulnerability/
- libsodium GitHub 提交记录. https://github.com/jedisct1/libsodium/commit/f2da4cd8cb26599a0285a6ab0c02948e361a674a