Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,10 @@ function prepareAsymmetricKey(key, ctx, name = 'key') {
return { data, format: kKeyFormatJWK };
} else if (format === 'raw-public' || format === 'raw-private' ||
format === 'raw-seed') {
if ((ctx === kConsumePrivate || ctx === kCreatePrivate) &&
format === 'raw-public') {
throw new ERR_INVALID_ARG_VALUE(`${name}.format`, format);
}
if (!isArrayBufferView(data) && !isAnyArrayBuffer(data)) {
throw new ERR_INVALID_ARG_TYPE(
`${name}.key`,
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/crypto/ml_kem.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const {
KEMDecapsulateJob,
KEMEncapsulateJob,
kKeyFormatDER,
kKeyFormatRawPrivate,
kKeyFormatRawPublic,
kKeyFormatRawSeed,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatSPKI,
Expand Down Expand Up @@ -178,7 +178,7 @@ function mlKemImportKey(
case 'raw-seed': {
const isPublic = format === 'raw-public';
verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet);
handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name);
handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name);
break;
}
case 'jwk': {
Expand Down
121 changes: 85 additions & 36 deletions src/crypto/crypto_keys.cc
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,85 @@ int GetNidFromName(const char* name) {
}
return NID_undef;
}

bool IsUnavailablePqcKeyType(Environment* env, Local<String> key_type) {
return key_type->StringEquals(env->crypto_ml_dsa_44_string()) ||
key_type->StringEquals(env->crypto_ml_dsa_65_string()) ||
key_type->StringEquals(env->crypto_ml_dsa_87_string()) ||
key_type->StringEquals(env->crypto_ml_kem_512_string()) ||
key_type->StringEquals(env->crypto_ml_kem_768_string()) ||
key_type->StringEquals(env->crypto_ml_kem_1024_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_128f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_128s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_192f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_192s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_256f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_256s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_128f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_128s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_192f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_192s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_256f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_256s_string());
}

bool IsUnsupportedRawKeyType(Environment* env, Local<String> key_type) {
return key_type->StringEquals(env->crypto_rsa_string()) ||
key_type->StringEquals(env->crypto_rsa_pss_string()) ||
key_type->StringEquals(env->crypto_dsa_string()) ||
key_type->StringEquals(env->crypto_dh_string());
}

void ValidateRawKeyImportFormat(Environment* env,
Local<String> key_type,
const char* key_type_name,
int id,
EVPKeyPointer::PKFormatType format) {
auto validate_raw_format =
[&](EVPKeyPointer::PKFormatType expected_private_format) {
if (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC ||
format == expected_private_format) {
return;
}
THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
};

if (key_type->StringEquals(env->crypto_ec_string())) {
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE);
}

switch (id) {
case EVP_PKEY_X25519:
case EVP_PKEY_X448:
case EVP_PKEY_ED25519:
case EVP_PKEY_ED448:
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE);
default:
break;
}

#if OPENSSL_WITH_PQC
if (IsPqcSeedKeyId(id)) {
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_SEED);
}
if (IsPqcRawPrivateKeyId(id)) {
return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE);
}
#endif

if (IsUnavailablePqcKeyType(env, key_type)) {
THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type");
return;
}

if (IsUnsupportedRawKeyType(env, key_type)) {
THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
return;
}

THROW_ERR_INVALID_ARG_VALUE(
env, "Invalid asymmetricKeyType: %s", key_type_name);
}
} // namespace

bool KeyObjectData::ToEncodedPublicKey(
Expand Down Expand Up @@ -585,6 +664,12 @@ static KeyObjectData ImportRawKey(Environment* env,
}
};

const int id = GetNidFromName(key_type_name);
ValidateRawKeyImportFormat(env, key_type, key_type_name, id, format);
if (env->isolate()->HasPendingException()) {
return {};
}

// EC keys
if (key_type->StringEquals(env->crypto_ec_string())) {
int curve_nid = ncrypto::Ec::GetCurveIdFromName(named_curve);
Expand Down Expand Up @@ -642,8 +727,6 @@ static KeyObjectData ImportRawKey(Environment* env,
return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey));
}

int id = GetNidFromName(key_type_name);

typedef EVPKeyPointer (*new_key_fn)(
int, const ncrypto::Buffer<const unsigned char>&);
new_key_fn fn = nullptr;
Expand Down Expand Up @@ -698,40 +781,6 @@ static KeyObjectData ImportRawKey(Environment* env,
return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey));
}

if (key_type->StringEquals(env->crypto_rsa_string()) ||
key_type->StringEquals(env->crypto_rsa_pss_string()) ||
key_type->StringEquals(env->crypto_dsa_string()) ||
key_type->StringEquals(env->crypto_dh_string())) {
THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
return {};
}

#if !OPENSSL_WITH_PQC
if (key_type->StringEquals(env->crypto_ml_dsa_44_string()) ||
key_type->StringEquals(env->crypto_ml_dsa_65_string()) ||
key_type->StringEquals(env->crypto_ml_dsa_87_string()) ||
key_type->StringEquals(env->crypto_ml_kem_512_string()) ||
key_type->StringEquals(env->crypto_ml_kem_768_string()) ||
key_type->StringEquals(env->crypto_ml_kem_1024_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_128f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_128s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_192f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_192s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_256f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_sha2_256s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_128f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_128s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_192f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_192s_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_256f_string()) ||
key_type->StringEquals(env->crypto_slh_dsa_shake_256s_string())) {
THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type");
return {};
}
#endif

THROW_ERR_INVALID_ARG_VALUE(
env, "Invalid asymmetricKeyType: %s", key_type_name);
return {};
}

Expand Down
10 changes: 10 additions & 0 deletions src/crypto/crypto_pqc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,16 @@ KeyObjectData ImportJWKPqcKey(Environment* env, Local<Object> jwk) {

return KeyObjectData::CreateAsymmetric(type, std::move(pkey));
}

bool IsPqcRawPrivateKeyId(int id) {
const PqcAlgorithm* alg = FindPqcAlgorithmById(id);
return alg != nullptr && !alg->use_seed;
}

bool IsPqcSeedKeyId(int id) {
const PqcAlgorithm* alg = FindPqcAlgorithmById(id);
return alg != nullptr && alg->use_seed;
}
#endif
} // namespace crypto
} // namespace node
7 changes: 7 additions & 0 deletions src/crypto/crypto_pqc.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ bool ExportJwkPqcKey(Environment* env,
v8::Local<v8::Object> target);

KeyObjectData ImportJWKPqcKey(Environment* env, v8::Local<v8::Object> jwk);

// Returns true for PQC algorithms that support raw private key export/import.
bool IsPqcRawPrivateKeyId(int id);
// Returns true for PQC algorithms that carry the private key as a seed
// (ML-DSA, ML-KEM). Returns false for algorithms that use the expanded
// private key (SLH-DSA), or for non-PQC ids.
bool IsPqcSeedKeyId(int id);
#endif
} // namespace crypto
} // namespace node
Expand Down
68 changes: 67 additions & 1 deletion test/parallel/test-crypto-key-objects-raw.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,47 @@ const { hasOpenSSL } = require('../common/crypto');
}
}

// Raw public keys cannot be imported as private keys.
{
const rawPublicKeys = [
['ec', 'ec_p256_public.pem', { namedCurve: 'P-256' }],
['ed25519', 'ed25519_public.pem'],
['x25519', 'x25519_public.pem'],
];

if (!process.features.openssl_is_boringssl) {
rawPublicKeys.push(
['ed448', 'ed448_public.pem'],
['x448', 'x448_public.pem'],
);
} else {
common.printSkipMessage('Skipping unsupported ed448/x448 test cases');
}

if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) {
rawPublicKeys.push(
['ml-dsa-44', 'ml_dsa_44_public.pem'],
['ml-kem-768', 'ml_kem_768_public.pem'],
);
}

if (hasOpenSSL(3, 5)) {
rawPublicKeys.push(
['slh-dsa-sha2-128f', 'slh_dsa_sha2_128f_public.pem'],
);
}

for (const [asymmetricKeyType, fixture, options = {}] of rawPublicKeys) {
const publicKey = crypto.createPublicKey(fixtures.readKey(fixture, 'ascii'));
assert.throws(() => crypto.createPrivateKey({
key: publicKey.export({ format: 'raw-public' }),
format: 'raw-public',
asymmetricKeyType,
...options,
}), { code: 'ERR_INVALID_ARG_VALUE' });
}
}

// Raw seed imports do not support strings.
if (hasOpenSSL(3, 5)) {
const privKeyObj = crypto.createPrivateKey(
Expand Down Expand Up @@ -113,7 +154,11 @@ if (hasOpenSSL(3, 5)) {
assert.throws(() => privKeyObj.export({ format: 'raw-private' }),
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });

for (const format of ['raw-public', 'raw-private', 'raw-seed']) {
assert.throws(() => crypto.createPrivateKey({
key: Buffer.alloc(32), format: 'raw-public', asymmetricKeyType: 'dh',
}), { code: 'ERR_INVALID_ARG_VALUE' });

for (const format of ['raw-private', 'raw-seed']) {
assert.throws(() => crypto.createPrivateKey({
key: Buffer.alloc(32), format, asymmetricKeyType: 'dh',
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
Expand Down Expand Up @@ -274,6 +319,12 @@ if (hasOpenSSL(3, 5)) {
fixtures.readKey('ec_p256_private.pem', 'ascii'));
assert.throws(() => ecPriv.export({ format: 'raw-seed' }),
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
assert.throws(() => crypto.createPrivateKey({
key: ecPriv.export({ format: 'raw-private' }),
format: 'raw-seed',
asymmetricKeyType: 'ec',
namedCurve: 'P-256',
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });

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

if (hasOpenSSL(3, 5)) {
const slhPriv = crypto.createPrivateKey(
fixtures.readKey('slh_dsa_sha2_128f_private.pem', 'ascii'));
assert.throws(() => slhPriv.export({ format: 'raw-seed' }),
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
assert.throws(() => crypto.createPrivateKey({
key: slhPriv.export({ format: 'raw-private' }),
format: 'raw-seed',
asymmetricKeyType: 'slh-dsa-sha2-128f',
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
}
}

Expand All @@ -302,6 +363,11 @@ if (hasOpenSSL(3, 5)) {
fixtures.readKey(`${type.replaceAll('-', '_')}_private.pem`, 'ascii'));
assert.throws(() => priv.export({ format: 'raw-private' }),
{ code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
assert.throws(() => crypto.createPrivateKey({
key: priv.export({ format: 'raw-seed' }),
format: 'raw-private',
asymmetricKeyType: type,
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
}
}

Expand Down
6 changes: 6 additions & 0 deletions test/parallel/test-crypto-pqc-key-objects-slh-dsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ for (const asymmetricKeyType of [
key: rawPriv, format: 'raw-private', asymmetricKeyType,
});
assert.strictEqual(importedPriv.equals(key), true);
assert.throws(() => createPrivateKey({
key: rawPriv, format: 'raw-seed', asymmetricKeyType,
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
assert.throws(() => createPublicKey({
key: rawPriv, format: 'raw-seed', asymmetricKeyType,
}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
}

if (!hasOpenSSL(3, 5)) {
Expand Down
Loading