Skip to content

Commit 03c121c

Browse files
committed
crypto: reject invalid raw key imports
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 18f938d commit 03c121c

7 files changed

Lines changed: 181 additions & 39 deletions

File tree

lib/internal/crypto/keys.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,10 @@ function prepareAsymmetricKey(key, ctx, name = 'key') {
666666
return { data, format: kKeyFormatJWK };
667667
} else if (format === 'raw-public' || format === 'raw-private' ||
668668
format === 'raw-seed') {
669+
if ((ctx === kConsumePrivate || ctx === kCreatePrivate) &&
670+
format === 'raw-public') {
671+
throw new ERR_INVALID_ARG_VALUE(`${name}.format`, format);
672+
}
669673
if (!isArrayBufferView(data) && !isAnyArrayBuffer(data)) {
670674
throw new ERR_INVALID_ARG_TYPE(
671675
`${name}.key`,

lib/internal/crypto/ml_kem.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const {
1313
KEMDecapsulateJob,
1414
KEMEncapsulateJob,
1515
kKeyFormatDER,
16-
kKeyFormatRawPrivate,
1716
kKeyFormatRawPublic,
17+
kKeyFormatRawSeed,
1818
kWebCryptoKeyFormatPKCS8,
1919
kWebCryptoKeyFormatRaw,
2020
kWebCryptoKeyFormatSPKI,
@@ -178,7 +178,7 @@ function mlKemImportKey(
178178
case 'raw-seed': {
179179
const isPublic = format === 'raw-public';
180180
verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet);
181-
handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name);
181+
handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name);
182182
break;
183183
}
184184
case 'jwk': {

src/crypto/crypto_keys.cc

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,85 @@ int GetNidFromName(const char* name) {
336336
}
337337
return NID_undef;
338338
}
339+
340+
bool IsUnavailablePqcKeyType(Environment* env, Local<String> key_type) {
341+
return key_type->StringEquals(env->crypto_ml_dsa_44_string()) ||
342+
key_type->StringEquals(env->crypto_ml_dsa_65_string()) ||
343+
key_type->StringEquals(env->crypto_ml_dsa_87_string()) ||
344+
key_type->StringEquals(env->crypto_ml_kem_512_string()) ||
345+
key_type->StringEquals(env->crypto_ml_kem_768_string()) ||
346+
key_type->StringEquals(env->crypto_ml_kem_1024_string()) ||
347+
key_type->StringEquals(env->crypto_slh_dsa_sha2_128f_string()) ||
348+
key_type->StringEquals(env->crypto_slh_dsa_sha2_128s_string()) ||
349+
key_type->StringEquals(env->crypto_slh_dsa_sha2_192f_string()) ||
350+
key_type->StringEquals(env->crypto_slh_dsa_sha2_192s_string()) ||
351+
key_type->StringEquals(env->crypto_slh_dsa_sha2_256f_string()) ||
352+
key_type->StringEquals(env->crypto_slh_dsa_sha2_256s_string()) ||
353+
key_type->StringEquals(env->crypto_slh_dsa_shake_128f_string()) ||
354+
key_type->StringEquals(env->crypto_slh_dsa_shake_128s_string()) ||
355+
key_type->StringEquals(env->crypto_slh_dsa_shake_192f_string()) ||
356+
key_type->StringEquals(env->crypto_slh_dsa_shake_192s_string()) ||
357+
key_type->StringEquals(env->crypto_slh_dsa_shake_256f_string()) ||
358+
key_type->StringEquals(env->crypto_slh_dsa_shake_256s_string());
359+
}
360+
361+
bool IsUnsupportedRawKeyType(Environment* env, Local<String> key_type) {
362+
return key_type->StringEquals(env->crypto_rsa_string()) ||
363+
key_type->StringEquals(env->crypto_rsa_pss_string()) ||
364+
key_type->StringEquals(env->crypto_dsa_string()) ||
365+
key_type->StringEquals(env->crypto_dh_string());
366+
}
367+
368+
void ValidateRawKeyImportFormat(Environment* env,
369+
Local<String> key_type,
370+
const char* key_type_name,
371+
int id,
372+
EVPKeyPointer::PKFormatType format) {
373+
auto validate_raw_format =
374+
[&](EVPKeyPointer::PKFormatType expected_private_format) {
375+
if (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC ||
376+
format == expected_private_format) {
377+
return;
378+
}
379+
THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
380+
};
381+
382+
if (key_type->StringEquals(env->crypto_ec_string())) {
383+
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE);
384+
}
385+
386+
switch (id) {
387+
case EVP_PKEY_X25519:
388+
case EVP_PKEY_X448:
389+
case EVP_PKEY_ED25519:
390+
case EVP_PKEY_ED448:
391+
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE);
392+
default:
393+
break;
394+
}
395+
396+
#if OPENSSL_WITH_PQC
397+
if (IsPqcSeedKeyId(id)) {
398+
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_SEED);
399+
}
400+
if (IsPqcRawPrivateKeyId(id)) {
401+
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE);
402+
}
403+
#endif
404+
405+
if (IsUnavailablePqcKeyType(env, key_type)) {
406+
THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type");
407+
return;
408+
}
409+
410+
if (IsUnsupportedRawKeyType(env, key_type)) {
411+
THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
412+
return;
413+
}
414+
415+
THROW_ERR_INVALID_ARG_VALUE(
416+
env, "Invalid asymmetricKeyType: %s", key_type_name);
417+
}
339418
} // namespace
340419

341420
bool KeyObjectData::ToEncodedPublicKey(
@@ -585,6 +664,12 @@ static KeyObjectData ImportRawKey(Environment* env,
585664
}
586665
};
587666

667+
const int id = GetNidFromName(key_type_name);
668+
ValidateRawKeyImportFormat(env, key_type, key_type_name, id, format);
669+
if (env->isolate()->HasPendingException()) {
670+
return {};
671+
}
672+
588673
// EC keys
589674
if (key_type->StringEquals(env->crypto_ec_string())) {
590675
int curve_nid = ncrypto::Ec::GetCurveIdFromName(named_curve);
@@ -642,8 +727,6 @@ static KeyObjectData ImportRawKey(Environment* env,
642727
return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey));
643728
}
644729

645-
int id = GetNidFromName(key_type_name);
646-
647730
typedef EVPKeyPointer (*new_key_fn)(
648731
int, const ncrypto::Buffer<const unsigned char>&);
649732
new_key_fn fn = nullptr;
@@ -698,40 +781,6 @@ static KeyObjectData ImportRawKey(Environment* env,
698781
return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey));
699782
}
700783

701-
if (key_type->StringEquals(env->crypto_rsa_string()) ||
702-
key_type->StringEquals(env->crypto_rsa_pss_string()) ||
703-
key_type->StringEquals(env->crypto_dsa_string()) ||
704-
key_type->StringEquals(env->crypto_dh_string())) {
705-
THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
706-
return {};
707-
}
708-
709-
#if !OPENSSL_WITH_PQC
710-
if (key_type->StringEquals(env->crypto_ml_dsa_44_string()) ||
711-
key_type->StringEquals(env->crypto_ml_dsa_65_string()) ||
712-
key_type->StringEquals(env->crypto_ml_dsa_87_string()) ||
713-
key_type->StringEquals(env->crypto_ml_kem_512_string()) ||
714-
key_type->StringEquals(env->crypto_ml_kem_768_string()) ||
715-
key_type->StringEquals(env->crypto_ml_kem_1024_string()) ||
716-
key_type->StringEquals(env->crypto_slh_dsa_sha2_128f_string()) ||
717-
key_type->StringEquals(env->crypto_slh_dsa_sha2_128s_string()) ||
718-
key_type->StringEquals(env->crypto_slh_dsa_sha2_192f_string()) ||
719-
key_type->StringEquals(env->crypto_slh_dsa_sha2_192s_string()) ||
720-
key_type->StringEquals(env->crypto_slh_dsa_sha2_256f_string()) ||
721-
key_type->StringEquals(env->crypto_slh_dsa_sha2_256s_string()) ||
722-
key_type->StringEquals(env->crypto_slh_dsa_shake_128f_string()) ||
723-
key_type->StringEquals(env->crypto_slh_dsa_shake_128s_string()) ||
724-
key_type->StringEquals(env->crypto_slh_dsa_shake_192f_string()) ||
725-
key_type->StringEquals(env->crypto_slh_dsa_shake_192s_string()) ||
726-
key_type->StringEquals(env->crypto_slh_dsa_shake_256f_string()) ||
727-
key_type->StringEquals(env->crypto_slh_dsa_shake_256s_string())) {
728-
THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type");
729-
return {};
730-
}
731-
#endif
732-
733-
THROW_ERR_INVALID_ARG_VALUE(
734-
env, "Invalid asymmetricKeyType: %s", key_type_name);
735784
return {};
736785
}
737786

src/crypto/crypto_pqc.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ KeyObjectData ImportJWKPqcKey(Environment* env, Local<Object> jwk) {
175175

176176
return KeyObjectData::CreateAsymmetric(type, std::move(pkey));
177177
}
178+
179+
bool IsPqcRawPrivateKeyId(int id) {
180+
const PqcAlgorithm* alg = FindPqcAlgorithmById(id);
181+
return alg != nullptr && !alg->use_seed;
182+
}
183+
184+
bool IsPqcSeedKeyId(int id) {
185+
const PqcAlgorithm* alg = FindPqcAlgorithmById(id);
186+
return alg != nullptr && alg->use_seed;
187+
}
178188
#endif
179189
} // namespace crypto
180190
} // namespace node

src/crypto/crypto_pqc.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ bool ExportJwkPqcKey(Environment* env,
1515
v8::Local<v8::Object> target);
1616

1717
KeyObjectData ImportJWKPqcKey(Environment* env, v8::Local<v8::Object> jwk);
18+
19+
// Returns true for PQC algorithms that support raw private key export/import.
20+
bool IsPqcRawPrivateKeyId(int id);
21+
// Returns true for PQC algorithms that carry the private key as a seed
22+
// (ML-DSA, ML-KEM). Returns false for algorithms that use the expanded
23+
// private key (SLH-DSA), or for non-PQC ids.
24+
bool IsPqcSeedKeyId(int id);
1825
#endif
1926
} // namespace crypto
2027
} // namespace node

test/parallel/test-crypto-key-objects-raw.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,47 @@ const { hasOpenSSL } = require('../common/crypto');
5959
}
6060
}
6161

62+
// Raw public keys cannot be imported as private keys.
63+
{
64+
const rawPublicKeys = [
65+
['ec', 'ec_p256_public.pem', { namedCurve: 'P-256' }],
66+
['ed25519', 'ed25519_public.pem'],
67+
['x25519', 'x25519_public.pem'],
68+
];
69+
70+
if (!process.features.openssl_is_boringssl) {
71+
rawPublicKeys.push(
72+
['ed448', 'ed448_public.pem'],
73+
['x448', 'x448_public.pem'],
74+
);
75+
} else {
76+
common.printSkipMessage('Skipping unsupported ed448/x448 test cases');
77+
}
78+
79+
if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) {
80+
rawPublicKeys.push(
81+
['ml-dsa-44', 'ml_dsa_44_public.pem'],
82+
['ml-kem-768', 'ml_kem_768_public.pem'],
83+
);
84+
}
85+
86+
if (hasOpenSSL(3, 5)) {
87+
rawPublicKeys.push(
88+
['slh-dsa-sha2-128f', 'slh_dsa_sha2_128f_public.pem'],
89+
);
90+
}
91+
92+
for (const [asymmetricKeyType, fixture, options = {}] of rawPublicKeys) {
93+
const publicKey = crypto.createPublicKey(fixtures.readKey(fixture, 'ascii'));
94+
assert.throws(() => crypto.createPrivateKey({
95+
key: publicKey.export({ format: 'raw-public' }),
96+
format: 'raw-public',
97+
asymmetricKeyType,
98+
...options,
99+
}), { code: 'ERR_INVALID_ARG_VALUE' });
100+
}
101+
}
102+
62103
// Raw seed imports do not support strings.
63104
if (hasOpenSSL(3, 5)) {
64105
const privKeyObj = crypto.createPrivateKey(
@@ -113,7 +154,11 @@ if (hasOpenSSL(3, 5)) {
113154
assert.throws(() => privKeyObj.export({ format: 'raw-private' }),
114155
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
115156

116-
for (const format of ['raw-public', 'raw-private', 'raw-seed']) {
157+
assert.throws(() => crypto.createPrivateKey({
158+
key: Buffer.alloc(32), format: 'raw-public', asymmetricKeyType: 'dh',
159+
}), { code: 'ERR_INVALID_ARG_VALUE' });
160+
161+
for (const format of ['raw-private', 'raw-seed']) {
117162
assert.throws(() => crypto.createPrivateKey({
118163
key: Buffer.alloc(32), format, asymmetricKeyType: 'dh',
119164
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
@@ -274,6 +319,12 @@ if (hasOpenSSL(3, 5)) {
274319
fixtures.readKey('ec_p256_private.pem', 'ascii'));
275320
assert.throws(() => ecPriv.export({ format: 'raw-seed' }),
276321
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
322+
assert.throws(() => crypto.createPrivateKey({
323+
key: ecPriv.export({ format: 'raw-private' }),
324+
format: 'raw-seed',
325+
asymmetricKeyType: 'ec',
326+
namedCurve: 'P-256',
327+
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
277328

278329
if (process.features.openssl_is_boringssl) {
279330
common.printSkipMessage('Skipping unsupported ed448/x448 test cases');
@@ -285,13 +336,23 @@ if (hasOpenSSL(3, 5)) {
285336
fixtures.readKey(`${type}_private.pem`, 'ascii'));
286337
assert.throws(() => priv.export({ format: 'raw-seed' }),
287338
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
339+
assert.throws(() => crypto.createPrivateKey({
340+
key: priv.export({ format: 'raw-private' }),
341+
format: 'raw-seed',
342+
asymmetricKeyType: type,
343+
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
288344
}
289345

290346
if (hasOpenSSL(3, 5)) {
291347
const slhPriv = crypto.createPrivateKey(
292348
fixtures.readKey('slh_dsa_sha2_128f_private.pem', 'ascii'));
293349
assert.throws(() => slhPriv.export({ format: 'raw-seed' }),
294350
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
351+
assert.throws(() => crypto.createPrivateKey({
352+
key: slhPriv.export({ format: 'raw-private' }),
353+
format: 'raw-seed',
354+
asymmetricKeyType: 'slh-dsa-sha2-128f',
355+
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
295356
}
296357
}
297358

@@ -302,6 +363,11 @@ if (hasOpenSSL(3, 5)) {
302363
fixtures.readKey(`${type.replaceAll('-', '_')}_private.pem`, 'ascii'));
303364
assert.throws(() => priv.export({ format: 'raw-private' }),
304365
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
366+
assert.throws(() => crypto.createPrivateKey({
367+
key: priv.export({ format: 'raw-seed' }),
368+
format: 'raw-private',
369+
asymmetricKeyType: type,
370+
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
305371
}
306372
}
307373

test/parallel/test-crypto-pqc-key-objects-slh-dsa.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ for (const asymmetricKeyType of [
9191
key: rawPriv, format: 'raw-private', asymmetricKeyType,
9292
});
9393
assert.strictEqual(importedPriv.equals(key), true);
94+
assert.throws(() => createPrivateKey({
95+
key: rawPriv, format: 'raw-seed', asymmetricKeyType,
96+
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
97+
assert.throws(() => createPublicKey({
98+
key: rawPriv, format: 'raw-seed', asymmetricKeyType,
99+
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
94100
}
95101

96102
if (!hasOpenSSL(3, 5)) {

0 commit comments

Comments
 (0)