Libsodium 中的一个漏洞
A Vulnerability in Libsodium

原始链接: https://00f.net/2025/12/30/libsodium-vulnerability/

## Libsodium 漏洞修复与项目更新 经过13年对简洁性和API稳定性的优先考虑,最近在libsodium的底层`crypto_core_ed25519_is_valid_point()`函数中发现了一个错误。该函数用于验证Edwards25519曲线点,未能完全拒绝无效点,可能影响自定义加密实现。问题源于函数内的不完整检查,现已通过验证X和Y坐标来纠正。 大多数用户不受影响,因为像`crypto_sign_*`这样的高级API不使用此函数。但是,直接使用`crypto_core_ed25519_is_valid_point()`或使用Edwards25519构建自定义密码学的人员应更新到2025年12月30日之后发布的版本。对于新的实现,建议使用Ristretto255群组,因为它避免了这种类型的验证问题。 Libsodium最初是一个简化密码学的项目,专注于高级API并避免不必要地暴露底层算法。尽管最初的意图是一个工具包,但它在13年中保持了强大的安全记录——零个CVE,尽管被广泛使用。该项目由一名开发人员维护,并依赖于社区通过赞助来确保持续开发和稳定性。

## Libsodium 漏洞与资金讨论 最近在 Libsodium(一个流行的密码学库)中发现了一个漏洞,影响了像 PHP 库 `sodium_compat` 这样的实现。开发者 CiPHPerCoder 进行了调查,发现多个库缺少必要的检查,并主动通知了维护者。 讨论迅速转移到资助 Libsodium 唯一维护者 Frank Denis 的挑战上。许多用户表达了对他的工作的感激之情,并探讨了除了通过 Open Collective 等平台进行个人捐赠之外的其他选择,后者可能对大型公司来说难以使用。 建议包括公司赞助、苹果的捐赠匹配计划(尽管存在资格要求),甚至出售高价书籍以方便公司费用审批。 许多评论者强调了大型组织在资助开源维护方面存在的官僚障碍,以及对更简化的解决方案的需求。 最终,这次讨论强调了像 Libsodium 这样重要的开源项目对可持续资助模式的迫切需求。
相关文章

原文

Libsodium is now 13 years old!

I started that project to pursue Dan Bernstein’s desire to make cryptography simple to use. That meant exposing a limited set of high-level functions and parameters, providing a simple API, and writing documentation for users, not cryptographers or developers. Libsodium’s goal was to expose APIs to perform operations, not low-level functions. Users shouldn’t even have to know or care about what algorithms are used internally. This is how I’ve always viewed libsodium.

Never breaking the APIs is also something I’m obsessed with. APIs may not be great, and if I could start over from scratch, I would have made them very different, but as a developer, the best APIs are not the most beautifully designed ones, but the ones that you don’t have to worry about because they don’t change and upgrades don’t require any changes in your application either. Libsodium started from the NaCl API, and still adheres to it.

These APIs exposed high-level functions, but also some lower-level functions that high-level functions wrap or depend on. Over the years, people started using these low-level functions directly. Libsodium started to be used as a toolkit of algorithms and low-level primitives.

That made me sad, especially since it is clearly documented that only APIs from builds with --enable-minimal are guaranteed to be tested and stable. But after all, it makes sense. When building custom protocols, having a single portable library with a consistent interface for different functions is far better than importing multiple dependencies, each with their own APIs and sometimes incompatibilities between them.

That’s a lot of code to maintain. It includes features and target platforms I don’t use but try to support for the community. I also maintain a large number of other open source projects.

Still, the security track record of libsodium is pretty good, with zero CVEs in 13 years even though it has gotten a lot of scrutiny.

However, while recently experimenting with adding support for batch signatures, I noticed inconsistent results with code originally written in Zig. The culprit was a check that was present in a function in Zig, but that I forgot to add in libsodium.

The bug

The function crypto_core_ed25519_is_valid_point(), a low-level function used to check if a given elliptic curve point is valid, was supposed to reject points that aren’t in the main cryptographic group, but some points were slipping through.

Why does this matter?

Edwards25519 is like a special mathematical playground where cryptographic operations happen.

It is used internally for Ed25519 signatures, and includes multiple subgroups of different sizes (order):

  • Order 1: just the identity (0, 1)
  • Order 2: identity + point (0, -1)
  • Order 4: 4 points
  • Order 8: 8 points
  • Order L: the “main subgroup” (L = ~2^252 points) where all operations are expected to happen
  • Order 2L, 4L, 8L: very large, but not prime order subgroups

The validation function was designed to reject points not in the main subgroup. It properly rejected points in the small-order subgroups, but not points in the mixed-order subgroups.

What went wrong technically?

To check if a point is in the main subgroup (the one of order L), the function multiplies it by L. If the order is L, multiplying any point by L gives the identity point (the mathematical equivalent of zero). So, the code does the multiplication and checks that we ended up with the identity point.

Points are represented by coordinates. In the internal representation used here, there are three coordinates: X, Y, and Z. The identity point is represented internally with coordinates where X = 0 and Y = Z. Z can be anything depending on previous operations; it doesn’t have to be 1.

The old code only checked X = 0. It forgot to verify Y = Z. This meant some invalid points (where X = 0 but Y ≠ Z after the multiplication) were incorrectly accepted as valid.

Concretely: take any main-subgroup point Q (for example, the output of crypto_core_ed25519_random) and add the order-2 point (0, -1), or equivalently negate both coordinates. Every such Q + (0, -1) would have passed validation before the fix, even though it’s not in the main subgroup.

The fix

The fix is trivial and adds the missing check:

// OLD:
return fe25519_iszero(pl.X);
// NEW:
fe25519_sub(t, pl.Y, pl.Z);
return fe25519_iszero(pl.X) & fe25519_iszero(t);

Now it properly verifies both conditions: X must be zero and Y must equal Z.

Who is affected?

You may be affected if you:

  • Use a point release <= 1.0.20 or a version of libsodium released before December 30, 2025.
  • Use crypto_core_ed25519_is_valid_point() to validate points from untrusted sources
  • Implement custom cryptography using arithmetic over the Edwards25519 curve

But don’t panic. Most users are not affected.

None of the high-level APIs (crypto_sign_*) are affected; they don’t even use or need that function. Scalar multiplication using crypto_scalarmult_ed25519 won’t leak anything even if the public key is not on the main subgroup. And public keys created with the regular crypto_sign_keypair and crypto_sign_seed_keypair functions are guaranteed to be on the correct subgroup.

Recommendation

Support for the Ristretto255 group was added to libsodium in 2019 specifically to solve cofactor-related issues. With Ristretto255, if a point decodes, it’s safe. No further validation is required.

If you implement custom cryptographic schemes doing arithmetic over a finite field group, using Ristretto255 is recommended. It’s easier to use, and as a bonus, low-level operations will run faster than over Edwards25519.

If you can’t update libsodium and need an application-level workaround, use the following function:

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;
}

Fixed packages

This issue was fixed immediately after discovery. All stable packages released after December 30, 2025 include the fix:

  • official tarballs
  • binaries for Visual Studio
  • binaries for MingW
  • NuGet packages for all architectures including Android
  • swift-sodium xcframework (but swift-sodium doesn’t expose low-level functions anyway)
  • Rust libsodium-sys-stable
  • libsodium.js

A new point release is also going to be tagged.

If libsodium is useful to you, please keep in mind that it is maintained by one person, for free, in time I could spend with my family or on other projects. The best way to help the project would be to consider sponsoring it, which helps me dedicate more time to improving it and making it great for everyone, for many more years to come.

联系我们 contact @ memedata.com