Skip to content

Commit 313829d

Browse files
committed
crypto: reject small-order EdDSA points during verify
Return false for Ed25519 and Ed448 one-shot verification when the public key or signature R component is a known low-order point. This keeps key import behavior unchanged while making WebCrypto verification match WPT expectations across OpenSSL variants. Remove the stale WPT expected-failure entry and add focused regression coverage for both curves. Closes: #54572 Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 81e93df commit 313829d

3 files changed

Lines changed: 184 additions & 20 deletions

File tree

src/crypto/crypto_sig.cc

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,147 @@ bool ApplyRSAOptions(const EVPKeyPointer& pkey,
7878
return true;
7979
}
8080

81+
constexpr size_t kEd25519PointSize = 32;
82+
constexpr size_t kEd448PointSize = 57;
83+
84+
// Ed25519 has cofactor 8, so the first eight entries are the full
85+
// canonical small-order subgroup: identity, one point of order 2,
86+
// two points of order 4, and four points of order 8.
87+
constexpr unsigned char kEd25519SmallOrderPoints[][kEd25519PointSize] = {
88+
// Identity.
89+
{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
90+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
91+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
92+
// Order 2.
93+
{0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
94+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
95+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
96+
// Order 4.
97+
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
98+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
99+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
100+
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
101+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
102+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
103+
// Order 8.
104+
{0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b,
105+
0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39,
106+
0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a},
107+
{0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b,
108+
0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39,
109+
0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa},
110+
{0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4,
111+
0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6,
112+
0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05},
113+
{0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4,
114+
0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6,
115+
0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85},
116+
// Non-canonical encodings of the same small-order points.
117+
{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
118+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
119+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
120+
{0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
121+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
122+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
123+
{0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
124+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
125+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
126+
{0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
127+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
128+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
129+
{0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
130+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
131+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
132+
{0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
133+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
134+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
135+
};
136+
137+
// Ed448 has cofactor 4, so these four entries are the full canonical
138+
// small-order subgroup: identity, one point of order 2, and two points
139+
// of order 4.
140+
constexpr unsigned char kEd448SmallOrderPoints[][kEd448PointSize] = {
141+
// Identity.
142+
{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
143+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
144+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
145+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
146+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
147+
// Order 2.
148+
{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
149+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
150+
0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
151+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
152+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00},
153+
// Order 4.
154+
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
155+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
156+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
157+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
158+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
159+
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
160+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
161+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
162+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
163+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
164+
};
165+
166+
template <size_t PointSize, size_t Count>
167+
bool ContainsPoint(const unsigned char* candidate,
168+
const unsigned char (&points)[Count][PointSize]) {
169+
for (const auto& point : points) {
170+
if (memcmp(candidate, point, PointSize) == 0) return true;
171+
}
172+
return false;
173+
}
174+
175+
bool IsSmallOrderEdDsaPoint(int id,
176+
const unsigned char* candidate,
177+
size_t size) {
178+
switch (id) {
179+
case EVP_PKEY_ED25519:
180+
return size == kEd25519PointSize &&
181+
ContainsPoint(candidate, kEd25519SmallOrderPoints);
182+
case EVP_PKEY_ED448:
183+
return size == kEd448PointSize &&
184+
ContainsPoint(candidate, kEd448SmallOrderPoints);
185+
default:
186+
return false;
187+
}
188+
}
189+
190+
bool HasSmallOrderEdDsaPoint(const EVPKeyPointer& key,
191+
const ByteSource& signature) {
192+
const int id = key.id();
193+
size_t point_size;
194+
195+
switch (id) {
196+
case EVP_PKEY_ED25519:
197+
point_size = kEd25519PointSize;
198+
break;
199+
case EVP_PKEY_ED448:
200+
point_size = kEd448PointSize;
201+
break;
202+
default:
203+
return false;
204+
}
205+
206+
if (signature.size() != point_size * 2) return false;
207+
208+
if (IsSmallOrderEdDsaPoint(id, signature.data<unsigned char>(), point_size)) {
209+
return true;
210+
}
211+
212+
unsigned char raw_public_key[kEd448PointSize];
213+
size_t raw_public_key_size = point_size;
214+
if (EVP_PKEY_get_raw_public_key(
215+
key.get(), raw_public_key, &raw_public_key_size) != 1) {
216+
return false;
217+
}
218+
219+
return IsSmallOrderEdDsaPoint(id, raw_public_key, raw_public_key_size);
220+
}
221+
81222
std::unique_ptr<BackingStore> Node_SignFinal(Environment* env,
82223
EVPMDCtxPointer&& mdctx,
83224
const EVPKeyPointer& pkey,
@@ -754,7 +895,8 @@ bool SignTraits::DeriveBits(Environment* env,
754895
case SignConfiguration::Mode::Verify: {
755896
auto buf = DataPointer::Alloc(1);
756897
static_cast<char*>(buf.get())[0] = 0;
757-
if (context.verify(params.data, params.signature)) {
898+
if (context.verify(params.data, params.signature) &&
899+
!HasSmallOrderEdDsaPoint(key, params.signature)) {
758900
static_cast<char*>(buf.get())[0] = 1;
759901
}
760902
*out = ByteSource::Allocated(buf.release());

test/parallel/test-webcrypto-sign-verify-eddsa.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,44 @@ const vectors = require('../fixtures/crypto/eddsa')();
1515

1616
const supportsContext = hasOpenSSL(3, 2);
1717

18+
const smallOrderVerifyVectors = [
19+
{
20+
name: 'Ed25519',
21+
publicKey: Buffer.from(
22+
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
23+
'hex'),
24+
signature: Buffer.from(
25+
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a' +
26+
'0000000000000000000000000000000000000000000000000000000000000000',
27+
'hex'),
28+
data: Buffer.from(
29+
'8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6',
30+
'hex'),
31+
},
32+
];
33+
34+
if (!process.features.openssl_is_boringssl) {
35+
smallOrderVerifyVectors.push({
36+
name: 'Ed448',
37+
publicKey: Buffer.concat([Buffer.from([1]), Buffer.alloc(56)]),
38+
signature: Buffer.concat([Buffer.from([1]), Buffer.alloc(113)]),
39+
data: Buffer.from([1, 2, 3]),
40+
});
41+
}
42+
43+
async function testSmallOrderVerify({ name, publicKey, signature, data }) {
44+
const key = await subtle.importKey(
45+
'raw',
46+
publicKey,
47+
{ name },
48+
false,
49+
['verify']);
50+
51+
assert.strictEqual(
52+
await subtle.verify({ name }, key, signature, data),
53+
false);
54+
}
55+
1856
async function testVerify({ name,
1957
context,
2058
publicKeyBuffer,
@@ -260,6 +298,9 @@ async function testSign({ name,
260298
variations.push(testVerify(vector));
261299
variations.push(testSign(vector));
262300
});
301+
smallOrderVerifyVectors.forEach((vector) => {
302+
variations.push(testSmallOrderVerify(vector));
303+
});
263304

264305
await Promise.all(variations);
265306
})().then(common.mustCall());

test/wpt/status/WebCryptoAPI.cjs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
'use strict';
22

3-
const os = require('node:os');
4-
53
const { hasOpenSSL } = require('../../common/crypto.js');
64

7-
const s390x = os.arch() === 's390x';
8-
95
const conditionalFileSkips = {};
106
const conditionalSubtestSkips = {};
117

@@ -115,19 +111,4 @@ module.exports = {
115111
'historical.any.js': {
116112
'skip': 'Not relevant in Node.js context',
117113
},
118-
'sign_verify/eddsa_small_order_points.https.any.js': {
119-
'fail': {
120-
'note': 'see https://github.com/nodejs/node/issues/54572',
121-
'expected': [
122-
'Ed25519 Verification checks with small-order key of order - Test 1',
123-
'Ed25519 Verification checks with small-order key of order - Test 2',
124-
'Ed25519 Verification checks with small-order key of order - Test 12',
125-
'Ed25519 Verification checks with small-order key of order - Test 13',
126-
...(s390x ? [] : [
127-
'Ed25519 Verification checks with small-order key of order - Test 0',
128-
'Ed25519 Verification checks with small-order key of order - Test 11',
129-
]),
130-
],
131-
},
132-
},
133114
};

0 commit comments

Comments
 (0)