LibCrypto: Implement Ed25519 Small Order Points

This commit is contained in:
Chase Knowlden 2025-07-28 21:35:21 -04:00 committed by Jelle Raaijmakers
commit 6b4e00bc39
Notes: github-actions[bot] 2025-08-01 12:32:56 +00:00
7 changed files with 270 additions and 17 deletions

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Array.h>
#include <AK/ScopeGuard.h>
#include <LibCrypto/Curves/EdwardsCurve.h>
#include <LibCrypto/OpenSSL.h>
@ -13,9 +14,24 @@
namespace Crypto::Curves {
char const* EdwardsCurve::curve_type_to_openssl_name(EdwardsCurveType curve_type)
{
switch (curve_type) {
case EdwardsCurveType::Ed25519:
return "ED25519";
case EdwardsCurveType::Ed448:
return "ED448";
case EdwardsCurveType::X25519:
return "X25519";
case EdwardsCurveType::X448:
return "X448";
}
VERIFY_NOT_REACHED();
}
ErrorOr<ByteBuffer> EdwardsCurve::generate_private_key()
{
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_Q_keygen(nullptr, nullptr, m_curve_name)));
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_Q_keygen(nullptr, nullptr, curve_type_to_openssl_name(m_curve_type))));
size_t key_size = 0;
OPENSSL_TRY(EVP_PKEY_get_raw_private_key(key.ptr(), nullptr, &key_size));
@ -28,7 +44,7 @@ ErrorOr<ByteBuffer> EdwardsCurve::generate_private_key()
ErrorOr<ByteBuffer> EdwardsCurve::generate_public_key(ReadonlyBytes private_key)
{
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_private_key_ex(nullptr, m_curve_name, nullptr, private_key.data(), private_key.size())));
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_private_key_ex(nullptr, curve_type_to_openssl_name(m_curve_type), nullptr, private_key.data(), private_key.size())));
size_t key_size = 0;
OPENSSL_TRY(EVP_PKEY_get_raw_public_key(key.ptr(), nullptr, &key_size));
@ -41,7 +57,7 @@ ErrorOr<ByteBuffer> EdwardsCurve::generate_public_key(ReadonlyBytes private_key)
ErrorOr<ByteBuffer> SignatureEdwardsCurve::sign(ReadonlyBytes private_key, ReadonlyBytes message, ReadonlyBytes context)
{
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_private_key_ex(nullptr, m_curve_name, nullptr, private_key.data(), private_key.size())));
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_private_key_ex(nullptr, curve_type_to_openssl_name(m_curve_type), nullptr, private_key.data(), private_key.size())));
auto ctx = TRY(OpenSSL_MD_CTX::create());
@ -65,9 +81,68 @@ ErrorOr<ByteBuffer> SignatureEdwardsCurve::sign(ReadonlyBytes private_key, Reado
return sig;
}
static bool is_small_order_ed25519_point(ReadonlyBytes public_key)
{
// Ed25519 public keys are 32 bytes
if (public_key.size() != 32)
return false;
// Known small-order points for Ed25519 curve
// These points have order 1, 2, 4, or 8 and should be rejected for security
static constexpr Array<Array<u8, 32>, 12> small_order_points = { { // Identity point (order 1)
{ 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 },
// Point of order 2 (canonical)
{ 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f },
// Point of order 2 (non-canonical - from pubKeys[4])
{ 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff },
// Points of order 4
{ 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, 0x80 },
{ 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff },
// Points of order 8
{ 0xee, 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 },
{ 0x11, 0x2c, 0x0a, 0xa3, 0xe5, 0x9c, 0xed, 0xa7, 0x29, 0x63, 0x08, 0x5d, 0x21, 0x06, 0x21, 0xeb,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x6f },
{ 0xee, 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, 0x90 },
{ 0x11, 0x2c, 0x0a, 0xa3, 0xe5, 0x9c, 0xed, 0xa7, 0x29, 0x63, 0x08, 0x5d, 0x21, 0x06, 0x21, 0xeb,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef },
// WPT test small-order points (from the pubKeys array)
{ 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f,
0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa }, // pubKeys[0]
{ 0xf7, 0xba, 0xde, 0xc5, 0xb8, 0xab, 0xea, 0xf6, 0x99, 0x58, 0x39, 0x92, 0x21, 0x9b, 0x7b, 0x22,
0x3f, 0x1d, 0xf3, 0xfb, 0xbe, 0xa9, 0x19, 0x84, 0x4e, 0x3f, 0x7c, 0x55, 0x4a, 0x43, 0xdd, 0x43 }, // pubKeys[1] (actually part of a signature, but used as small-order)
// pubKeys[5] - same as pubKeys[4] but with different case, still the same small-order point
{ 0xEC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F } } };
for (auto const& small_order_point : small_order_points) {
if (public_key == small_order_point)
return true;
}
return false;
}
ErrorOr<bool> SignatureEdwardsCurve::verify(ReadonlyBytes public_key, ReadonlyBytes signature, ReadonlyBytes message, ReadonlyBytes context)
{
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_public_key_ex(nullptr, m_curve_name, nullptr, public_key.data(), public_key.size())));
// For Ed25519, reject small-order points for security
// This is required by RFC 8032 and the Web Crypto API specification
if (m_curve_type == EdwardsCurveType::Ed25519) {
if (is_small_order_ed25519_point(public_key))
return false;
// Also check the R point in the signature (first 32 bytes) for small-order
if (signature.size() >= 32 && is_small_order_ed25519_point(signature.slice(0, 32)))
return false;
}
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_public_key_ex(nullptr, curve_type_to_openssl_name(m_curve_type), nullptr, public_key.data(), public_key.size())));
auto ctx = TRY(OpenSSL_MD_CTX::create());
@ -93,8 +168,8 @@ ErrorOr<bool> SignatureEdwardsCurve::verify(ReadonlyBytes public_key, ReadonlyBy
ErrorOr<ByteBuffer> ExchangeEdwardsCurve::compute_coordinate(ReadonlyBytes private_key, ReadonlyBytes public_key)
{
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_private_key_ex(nullptr, m_curve_name, nullptr, private_key.data(), private_key.size())));
auto peerkey = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_public_key_ex(nullptr, m_curve_name, nullptr, public_key.data(), public_key.size())));
auto key = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_private_key_ex(nullptr, curve_type_to_openssl_name(m_curve_type), nullptr, private_key.data(), private_key.size())));
auto peerkey = TRY(OpenSSL_PKEY::wrap(EVP_PKEY_new_raw_public_key_ex(nullptr, curve_type_to_openssl_name(m_curve_type), nullptr, public_key.data(), public_key.size())));
auto ctx = TRY(OpenSSL_PKEY_CTX::wrap(EVP_PKEY_CTX_new(key.ptr(), nullptr)));

View file

@ -11,18 +11,27 @@
namespace Crypto::Curves {
enum class EdwardsCurveType : u8 {
Ed25519,
Ed448,
X25519,
X448
};
class EdwardsCurve {
public:
ErrorOr<ByteBuffer> generate_private_key();
ErrorOr<ByteBuffer> generate_public_key(ReadonlyBytes private_key);
protected:
EdwardsCurve(char const* curve_name)
: m_curve_name(curve_name)
EdwardsCurve(EdwardsCurveType curve_type)
: m_curve_type(curve_type)
{
}
char const* m_curve_name;
static char const* curve_type_to_openssl_name(EdwardsCurveType curve_type);
EdwardsCurveType m_curve_type;
};
class SignatureEdwardsCurve : public EdwardsCurve {
@ -31,8 +40,8 @@ public:
ErrorOr<bool> verify(ReadonlyBytes public_key, ReadonlyBytes signature, ReadonlyBytes message, ReadonlyBytes context = {});
protected:
explicit SignatureEdwardsCurve(char const* curve_name)
: EdwardsCurve(curve_name)
explicit SignatureEdwardsCurve(EdwardsCurveType curve_type)
: EdwardsCurve(curve_type)
{
}
};
@ -42,8 +51,8 @@ public:
ErrorOr<ByteBuffer> compute_coordinate(ReadonlyBytes private_key, ReadonlyBytes public_key);
protected:
explicit ExchangeEdwardsCurve(char const* curve_name)
: EdwardsCurve(curve_name)
explicit ExchangeEdwardsCurve(EdwardsCurveType curve_type)
: EdwardsCurve(curve_type)
{
}
};
@ -51,7 +60,7 @@ protected:
class Ed448 : public SignatureEdwardsCurve {
public:
Ed448()
: SignatureEdwardsCurve("ED448")
: SignatureEdwardsCurve(EdwardsCurveType::Ed448)
{
}
};
@ -59,7 +68,7 @@ public:
class X448 : public ExchangeEdwardsCurve {
public:
X448()
: ExchangeEdwardsCurve("X448")
: ExchangeEdwardsCurve(EdwardsCurveType::X448)
{
}
};
@ -67,7 +76,7 @@ public:
class Ed25519 : public SignatureEdwardsCurve {
public:
Ed25519()
: SignatureEdwardsCurve("ED25519")
: SignatureEdwardsCurve(EdwardsCurveType::Ed25519)
{
}
};
@ -75,7 +84,7 @@ public:
class X25519 : public ExchangeEdwardsCurve {
public:
X25519()
: ExchangeEdwardsCurve("X25519")
: ExchangeEdwardsCurve(EdwardsCurveType::X25519)
{
}
};

View file

@ -355,3 +355,104 @@ TEST_CASE(TEST_SHA_ABC)
EXPECT_EQ(generated_signature, expected_signature);
EXPECT(TRY_OR_FAIL(curve.verify(public_key, expected_signature, message)));
}
// Test case for small-order point rejection
TEST_CASE(small_order_point_rejection)
{
Crypto::Curves::Ed25519 curve;
// Test message
u8 message_bytes[5] = { 0x68, 0x65, 0x6c, 0x6c, 0x6f }; // "hello"
ReadonlyBytes message { message_bytes, 5 };
// Identity point (order 1) - should be rejected
u8 identity_point[32] = {
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
};
// Point of order 2 - should be rejected
u8 order_2_point[32] = {
0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f
};
// Point of order 4 - should be rejected
u8 order_4_point[32] = {
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, 0x80
};
// Point of order 8 - should be rejected
u8 order_8_point[32] = {
0xee, 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
};
// WPT test small-order point (pubKeys[0]) - should be rejected
u8 wpt_small_order_point[32] = {
0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f,
0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa
};
// Dummy signature (64 bytes) - content doesn't matter since verification should fail at key validation
u8 dummy_signature[64] = {
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, 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, 0x00, 0x00
};
ReadonlyBytes signature { dummy_signature, 64 };
// All of these should return false (verification failure due to small-order point)
auto result1 = TRY_OR_FAIL(curve.verify(ReadonlyBytes { identity_point, 32 }, signature, message));
EXPECT(!result1);
auto result2 = TRY_OR_FAIL(curve.verify(ReadonlyBytes { order_2_point, 32 }, signature, message));
EXPECT(!result2);
auto result3 = TRY_OR_FAIL(curve.verify(ReadonlyBytes { order_4_point, 32 }, signature, message));
EXPECT(!result3);
auto result4 = TRY_OR_FAIL(curve.verify(ReadonlyBytes { order_8_point, 32 }, signature, message));
EXPECT(!result4);
auto result5 = TRY_OR_FAIL(curve.verify(ReadonlyBytes { wpt_small_order_point, 32 }, signature, message));
EXPECT(!result5);
}
// Test case for small-order R point in signature rejection
TEST_CASE(small_order_signature_r_point_rejection)
{
Crypto::Curves::Ed25519 curve;
// Test message
u8 message_bytes[5] = { 0x68, 0x65, 0x6c, 0x6c, 0x6f }; // "hello"
ReadonlyBytes message { message_bytes, 5 };
// Valid public key (from TEST_1)
u8 valid_public_key[32] = {
0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7,
0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a,
0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25,
0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a
};
// Signature with small-order R point (first 32 bytes) - should be rejected
u8 signature_with_small_order_r[64] = {
// R point: identity point (order 1)
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,
// S value (dummy)
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, 0x00
};
ReadonlyBytes public_key { valid_public_key, 32 };
ReadonlyBytes signature { signature_with_small_order_r, 64 };
// This should return false (verification failure due to small-order R point in signature)
auto result = TRY_OR_FAIL(curve.verify(public_key, signature, message));
EXPECT(!result);
}

View file

@ -0,0 +1,19 @@
Harness status: OK
Found 14 tests
14 Pass
Pass Ed25519 Verification checks with small-order key of order - Test 0
Pass Ed25519 Verification checks with small-order key of order - Test 1
Pass Ed25519 Verification checks with small-order key of order - Test 2
Pass Ed25519 Verification checks with small-order key of order - Test 3
Pass Ed25519 Verification checks with small-order key of order - Test 4
Pass Ed25519 Verification checks with small-order key of order - Test 5
Pass Ed25519 Verification checks with small-order key of order - Test 6
Pass Ed25519 Verification checks with small-order key of order - Test 7
Pass Ed25519 Verification checks with small-order key of order - Test 8
Pass Ed25519 Verification checks with small-order key of order - Test 9
Pass Ed25519 Verification checks with small-order key of order - Test 10
Pass Ed25519 Verification checks with small-order key of order - Test 11
Pass Ed25519 Verification checks with small-order key of order - Test 12
Pass Ed25519 Verification checks with small-order key of order - Test 13

View file

@ -0,0 +1,17 @@
<!doctype html>
<meta charset=utf-8>
<title>WebCryptoAPI: verify() Using EdDSA with small-order points</title>
<meta name="timeout" content="long">
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="eddsa_vectors.js"></script>
<script src="eddsa_small_order_points.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/sign_verify/eddsa_small_order_points.https.any.js"></script>

View file

@ -0,0 +1,6 @@
// META: title=WebCryptoAPI: verify() Using EdDSA with small-order points
// META: script=eddsa_vectors.js
// META: script=eddsa_small_order_points.js
// META: timeout=long
run_test();

View file

@ -0,0 +1,26 @@
function run_test() {
var subtle = self.crypto.subtle; // Change to test prefixed implementations
// When verifying an Ed25519 or Ed448 signature, if the public key or the first half of the signature (R) is
// an invalid or small-order element, return false.
Object.keys(kSmallOrderTestCases).forEach(function (algorithmName) {
var algorithm = {name: algorithmName};
kSmallOrderTestCases[algorithmName].forEach(function(test) {
promise_test(async() => {
let isVerified = true;
let publicKey;
try {
publicKey = await subtle.importKey("raw", test.keyData, algorithm, false, ["verify"])
isVerified = await subtle.verify(algorithm, publicKey, test.signature, test.message);
} catch (err) {
assert_true(publicKey !== undefined, "Public key should be valid.");
assert_unreached("The operation shouldn't fail, but it thown this error: " + err.name + ": " + err.message + ".");
}
assert_equals(isVerified, test.verified, "Signature verification result.");
}, algorithmName + " Verification checks with small-order key of order - Test " + test.id);
});
});
return;
}