From 03c121c6e17ab4070e853992da0152160fb2d842 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 5 May 2026 20:28:30 +0200 Subject: [PATCH 01/11] crypto: reject invalid raw key imports Signed-off-by: Filip Skokan --- lib/internal/crypto/keys.js | 4 + lib/internal/crypto/ml_kem.js | 4 +- src/crypto/crypto_keys.cc | 121 ++++++++++++------ src/crypto/crypto_pqc.cc | 10 ++ src/crypto/crypto_pqc.h | 7 + test/parallel/test-crypto-key-objects-raw.js | 68 +++++++++- .../test-crypto-pqc-key-objects-slh-dsa.js | 6 + 7 files changed, 181 insertions(+), 39 deletions(-) diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 03ace95df21dca..f9fd873d371e71 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -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`, diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 67f5ddd0ff2499..abb156ee07262d 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -13,8 +13,8 @@ const { KEMDecapsulateJob, KEMEncapsulateJob, kKeyFormatDER, - kKeyFormatRawPrivate, kKeyFormatRawPublic, + kKeyFormatRawSeed, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, @@ -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': { diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 92bb7dbb9714ce..560e13903e88e3 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -336,6 +336,85 @@ int GetNidFromName(const char* name) { } return NID_undef; } + +bool IsUnavailablePqcKeyType(Environment* env, Local 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 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 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( @@ -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); @@ -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&); new_key_fn fn = nullptr; @@ -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 {}; } diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc index cd2024cbe2f05d..bf40052fb6ea1e 100644 --- a/src/crypto/crypto_pqc.cc +++ b/src/crypto/crypto_pqc.cc @@ -175,6 +175,16 @@ KeyObjectData ImportJWKPqcKey(Environment* env, Local 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 diff --git a/src/crypto/crypto_pqc.h b/src/crypto/crypto_pqc.h index 7a805c0e36c6a3..156066097bbfb9 100644 --- a/src/crypto/crypto_pqc.h +++ b/src/crypto/crypto_pqc.h @@ -15,6 +15,13 @@ bool ExportJwkPqcKey(Environment* env, v8::Local target); KeyObjectData ImportJWKPqcKey(Environment* env, v8::Local 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 diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js index 9ef4bd3b9004d1..311659ef004ea2 100644 --- a/test/parallel/test-crypto-key-objects-raw.js +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -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( @@ -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' }); @@ -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'); @@ -285,6 +336,11 @@ 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)) { @@ -292,6 +348,11 @@ if (hasOpenSSL(3, 5)) { 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' }); } } @@ -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' }); } } diff --git a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js index 98af15dc795f8b..eff309468c3117 100644 --- a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js @@ -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)) { From b241e0f12e3f1a72a3f4523cf927e006f94e5dc2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 22 Apr 2026 17:02:28 +0200 Subject: [PATCH 02/11] crypto: wire ML-DSA and ML-KEM for use when using BoringSSL Signed-off-by: Filip Skokan --- benchmark/crypto/create-keyobject.js | 2 + benchmark/crypto/kem.js | 3 + benchmark/crypto/oneshot-sign.js | 2 + benchmark/crypto/oneshot-verify.js | 2 + deps/ncrypto/ncrypto.cc | 119 +++++++++- deps/ncrypto/ncrypto.h | 10 +- lib/internal/crypto/webidl.js | 16 +- src/crypto/crypto_kem.cc | 2 +- src/crypto/crypto_kem.h | 2 +- src/crypto/crypto_keys.cc | 213 +++++------------- src/crypto/crypto_pqc.cc | 67 ++++-- src/crypto/crypto_pqc.h | 7 + src/crypto/crypto_sig.cc | 31 +-- src/crypto/crypto_util.cc | 2 +- src/node_crypto.cc | 7 +- src/node_crypto.h | 4 +- test/fixtures/keys/Makefile | 16 ++ .../keys/ml_dsa_44_private_encrypted.der | Bin 0 -> 166 bytes .../keys/ml_dsa_44_private_encrypted.pem | 6 + .../keys/ml_kem_768_private_encrypted.der | Bin 0 -> 198 bytes .../keys/ml_kem_768_private_encrypted.pem | 7 + .../webcrypto/supports-modern-algorithms.mjs | 56 ++--- test/parallel/test-crypto-encap-decap.js | 20 +- test/parallel/test-crypto-key-objects-raw.js | 45 ++-- .../test-crypto-key-objects-to-crypto-key.js | 2 +- test/parallel/test-crypto-keygen-raw.js | 16 +- .../test-crypto-pqc-encrypted-pkcs8.js | 134 +++++++++++ .../test-crypto-pqc-key-objects-ml-dsa.js | 18 +- .../test-crypto-pqc-key-objects-ml-kem.js | 24 +- .../parallel/test-crypto-pqc-keygen-ml-dsa.js | 2 +- .../parallel/test-crypto-pqc-keygen-ml-kem.js | 17 +- .../test-crypto-pqc-sign-verify-ml-dsa.js | 25 +- .../test-webcrypto-deduplicate-usages.js | 12 +- .../test-webcrypto-encap-decap-ml-kem.js | 12 +- .../test-webcrypto-export-import-ml-dsa.js | 53 +++-- .../test-webcrypto-export-import-ml-kem.js | 70 ++++-- test/parallel/test-webcrypto-keygen.js | 14 +- ...-webcrypto-promise-prototype-pollution.mjs | 2 +- .../test-webcrypto-sign-verify-ml-dsa.js | 4 +- test/parallel/test-webcrypto-sign-verify.js | 2 +- test/parallel/test-webcrypto-wrap-unwrap.js | 2 +- 41 files changed, 673 insertions(+), 375 deletions(-) create mode 100644 test/fixtures/keys/ml_dsa_44_private_encrypted.der create mode 100644 test/fixtures/keys/ml_dsa_44_private_encrypted.pem create mode 100644 test/fixtures/keys/ml_kem_768_private_encrypted.der create mode 100644 test/fixtures/keys/ml_kem_768_private_encrypted.pem create mode 100644 test/parallel/test-crypto-pqc-encrypted-pkcs8.js diff --git a/benchmark/crypto/create-keyobject.js b/benchmark/crypto/create-keyobject.js index 30f8213175df69..7cd6db2d567ad6 100644 --- a/benchmark/crypto/create-keyobject.js +++ b/benchmark/crypto/create-keyobject.js @@ -26,6 +26,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } const bench = common.createBenchmark(main, { diff --git a/benchmark/crypto/kem.js b/benchmark/crypto/kem.js index ffdcac6d7fcb0d..a544fc2124afe9 100644 --- a/benchmark/crypto/kem.js +++ b/benchmark/crypto/kem.js @@ -24,6 +24,9 @@ if (hasOpenSSL(3, 5)) { keyFixtures['ml-kem-512'] = readKeyPair('ml_kem_512_public', 'ml_kem_512_private'); keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private'); keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private_seed_only'); + keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private_seed_only'); } if (hasOpenSSL(3, 2)) { keyFixtures['p-256'] = readKeyPair('ec_p256_public', 'ec_p256_private'); diff --git a/benchmark/crypto/oneshot-sign.js b/benchmark/crypto/oneshot-sign.js index d0abc7b5412e60..72e3726d9a5349 100644 --- a/benchmark/crypto/oneshot-sign.js +++ b/benchmark/crypto/oneshot-sign.js @@ -19,6 +19,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); diff --git a/benchmark/crypto/oneshot-verify.js b/benchmark/crypto/oneshot-verify.js index c6a24f52126eb2..8b397b02dbf285 100644 --- a/benchmark/crypto/oneshot-verify.js +++ b/benchmark/crypto/oneshot-verify.js @@ -26,6 +26,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index b7a0c96ee2ea60..17701b71411d5e 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -7,6 +7,10 @@ #include #include #include +#ifdef OPENSSL_IS_BORINGSSL +#include +#include +#endif #include #include #include @@ -29,9 +33,11 @@ constexpr static PQCMapping pqc_mappings[] = { {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, - {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, + +#ifndef OPENSSL_IS_BORINGSSL + {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, @@ -44,6 +50,7 @@ constexpr static PQCMapping pqc_mappings[] = { {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, +#endif }; #endif @@ -2074,10 +2081,51 @@ EVPKeyPointer EVPKeyPointer::NewRawSeed( int id, const Buffer& data) { if (id == 0) return {}; +#ifdef OPENSSL_IS_BORINGSSL + // BoringSSL exposes seed-based construction via EVP_PKEY_from_private_seed, + // which needs an |EVP_PKEY_ALG*| rather than a NID. + const EVP_PKEY_ALG* alg = nullptr; + switch (id) { + case EVP_PKEY_ML_DSA_44: + alg = EVP_pkey_ml_dsa_44(); + break; + case EVP_PKEY_ML_DSA_65: + alg = EVP_pkey_ml_dsa_65(); + break; + case EVP_PKEY_ML_DSA_87: + alg = EVP_pkey_ml_dsa_87(); + break; + case EVP_PKEY_ML_KEM_768: + alg = EVP_pkey_ml_kem_768(); + break; + case EVP_PKEY_ML_KEM_1024: + alg = EVP_pkey_ml_kem_1024(); + break; + default: + return {}; + } + return EVPKeyPointer(EVP_PKEY_from_private_seed(alg, data.data, data.len)); +#else + // ML-DSA and ML-KEM seeds use distinct OSSL_PARAM keys. + const char* param_name; + switch (id) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + param_name = OSSL_PKEY_PARAM_ML_DSA_SEED; + break; + case EVP_PKEY_ML_KEM_512: + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + param_name = OSSL_PKEY_PARAM_ML_KEM_SEED; + break; + default: + return {}; + } + OSSL_PARAM params[] = { - OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_ML_DSA_SEED, - const_cast(data.data), - data.len), + OSSL_PARAM_construct_octet_string( + param_name, const_cast(data.data), data.len), OSSL_PARAM_END}; EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); @@ -2091,6 +2139,7 @@ EVPKeyPointer EVPKeyPointer::NewRawSeed( } return EVPKeyPointer(pkey); +#endif // OPENSSL_IS_BORINGSSL } #endif @@ -2140,7 +2189,7 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; int type = EVP_PKEY_id(key); -#if OPENSSL_WITH_PQC +#if OPENSSL_WITH_PQC && !defined(OPENSSL_IS_BORINGSSL) // EVP_PKEY_id returns -1 when EVP_PKEY_* is only implemented in a provider // which is the case for all post-quantum NIST algorithms // one suggested way would be to use a chain of `EVP_PKEY_is_a` @@ -2218,6 +2267,30 @@ DataPointer EVPKeyPointer::rawPublicKey() const { DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; +#ifdef OPENSSL_IS_BORINGSSL + size_t seed_len; + switch (id()) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + seed_len = 32; // ML-DSA uses 32-byte seeds + break; + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + seed_len = 64; // ML-KEM uses 64-byte seeds + break; + default: + return {}; + } + + if (auto data = DataPointer::Alloc(seed_len)) { + const Buffer buf = data; + size_t len = data.size(); + if (EVP_PKEY_get_private_seed(get(), buf.data, &len) != 1) return {}; + return data; + } + return {}; +#else // Determine seed length and parameter name based on key type size_t seed_len; const char* param_name; @@ -2249,6 +2322,7 @@ DataPointer EVPKeyPointer::rawSeed() const { return data; } return {}; +#endif // OPENSSL_IS_BORINGSSL } #endif @@ -2287,6 +2361,7 @@ EVPKeyPointer::operator const EC_KEY*() const { } namespace { + EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp, const char* name, auto&& parse) { @@ -2714,6 +2789,7 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: +#ifndef OPENSSL_IS_BORINGSSL case EVP_PKEY_SLH_DSA_SHA2_128F: case EVP_PKEY_SLH_DSA_SHA2_128S: case EVP_PKEY_SLH_DSA_SHA2_192F: @@ -2726,6 +2802,7 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_SLH_DSA_SHAKE_192S: case EVP_PKEY_SLH_DSA_SHAKE_256F: case EVP_PKEY_SLH_DSA_SHAKE_256S: +#endif #endif return true; default: @@ -4369,7 +4446,17 @@ std::optional EVPMDCtxPointer::signInitWithContext( const EVPKeyPointer& key, const Digest& digest, const Buffer& context_string) { -#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING +#ifdef OPENSSL_IS_BORINGSSL + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestSignInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + if (EVP_PKEY_CTX_set1_signature_context_string( + ctx, context_string.data, context_string.len) <= 0) { + return std::nullopt; + } + return ctx; +#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) EVP_PKEY_CTX* ctx = nullptr; #ifdef OSSL_SIGNATURE_PARAM_INSTANCE @@ -4414,7 +4501,17 @@ std::optional EVPMDCtxPointer::verifyInitWithContext( const EVPKeyPointer& key, const Digest& digest, const Buffer& context_string) { -#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING +#ifdef OPENSSL_IS_BORINGSSL + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestVerifyInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + if (EVP_PKEY_CTX_set1_signature_context_string( + ctx, context_string.data, context_string.len) <= 0) { + return std::nullopt; + } + return ctx; +#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) EVP_PKEY_CTX* ctx = nullptr; #ifdef OSSL_SIGNATURE_PARAM_INSTANCE @@ -4815,8 +4912,8 @@ const Digest Digest::FromName(const char* name) { // ============================================================================ // KEM Implementation -#if OPENSSL_VERSION_MAJOR >= 3 -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { const char* operation = nullptr; @@ -4861,7 +4958,7 @@ std::optional KEM::Encapsulate( return std::nullopt; } -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L if (!SetOperationParameter(ctx.get(), public_key)) { return std::nullopt; } @@ -4902,7 +4999,7 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return {}; } -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L if (!SetOperationParameter(ctx.get(), private_key)) { return {}; } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 1f116169f57a27..c430ae9beece75 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -35,6 +35,10 @@ #define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 #define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #include +#elif defined(OPENSSL_IS_BORINGSSL) +#define OPENSSL_WITH_PQC 1 +#define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 +#define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #endif #if OPENSSL_VERSION_MAJOR >= 3 @@ -1654,7 +1658,7 @@ DataPointer argon2(const Buffer& pass, // ============================================================================ // KEM (Key Encapsulation Mechanism) -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) class KEM final { public: @@ -1678,13 +1682,13 @@ class KEM final { const Buffer& ciphertext); private: -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L static bool SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key); #endif }; -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) // ============================================================================ // Version metadata diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index a87edcb411d08e..f4ae6a8191720a 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -569,14 +569,18 @@ converters.ContextParams = createDictionaryConverter( key: 'context', converter: converters.BufferSource, validator(V, dict) { - let { 0: major, 1: minor } = process.versions.openssl.split('.'); - major = NumberParseInt(major, 10); - minor = NumberParseInt(minor, 10); - if (major > 3 || (major === 3 && minor >= 2)) { + if (process.features.openssl_is_boringssl) { this.validator = undefined; } else { - this.validator = validateZeroLength('ContextParams.context'); - this.validator(V, dict); + let { 0: major, 1: minor } = process.versions.openssl.split('.'); + major = NumberParseInt(major, 10); + minor = NumberParseInt(minor, 10); + if (major > 3 || (major === 3 && minor >= 2)) { + this.validator = undefined; + } else { + this.validator = validateZeroLength('ContextParams.context'); + this.validator(V, dict); + } } }, }, diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index dff69f2e18f947..cb062c0e2cdd8b 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -1,6 +1,6 @@ #include "crypto/crypto_kem.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) #include "async_wrap-inl.h" #include "base_object-inl.h" diff --git a/src/crypto/crypto_kem.h b/src/crypto/crypto_kem.h index 2b4671cfc7a0ec..fff3ff03dc2f64 100644 --- a/src/crypto/crypto_kem.h +++ b/src/crypto/crypto_kem.h @@ -10,7 +10,7 @@ #include "memory_tracker.h" #include "node_external_reference.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) namespace node { namespace crypto { diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 560e13903e88e3..ce09131bafc588 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -177,7 +177,11 @@ bool ExportJWKAsymmetricKey(Environment* env, const KeyObjectData& key, Local target, bool handleRsaPss) { - switch (key.GetAsymmetricKey().id()) { + const int id = key.GetAsymmetricKey().id(); +#if OPENSSL_WITH_PQC + if (IsPqcKeyId(id)) return ExportJwkPqcKey(env, key, target); +#endif + switch (id) { case EVP_PKEY_RSA_PSS: { if (handleRsaPss) return ExportJWKRsaKey(env, key, target); break; @@ -187,51 +191,10 @@ bool ExportJWKAsymmetricKey(Environment* env, case EVP_PKEY_EC: return ExportJWKEcKey(env, key, target); case EVP_PKEY_ED25519: - // Fall through case EVP_PKEY_ED448: - // Fall through case EVP_PKEY_X25519: - // Fall through case EVP_PKEY_X448: return ExportJWKEdKey(env, key, target); -#if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - // Fall through - case EVP_PKEY_ML_DSA_65: - // Fall through - case EVP_PKEY_ML_DSA_87: - // Fall through - case EVP_PKEY_SLH_DSA_SHA2_128F: - // Fall through - case EVP_PKEY_SLH_DSA_SHA2_128S: - // Fall through - case EVP_PKEY_SLH_DSA_SHA2_192F: - // Fall through - case EVP_PKEY_SLH_DSA_SHA2_192S: - // Fall through - case EVP_PKEY_SLH_DSA_SHA2_256F: - // Fall through - case EVP_PKEY_SLH_DSA_SHA2_256S: - // Fall through - case EVP_PKEY_SLH_DSA_SHAKE_128F: - // Fall through - case EVP_PKEY_SLH_DSA_SHAKE_128S: - // Fall through - case EVP_PKEY_SLH_DSA_SHAKE_192F: - // Fall through - case EVP_PKEY_SLH_DSA_SHAKE_192S: - // Fall through - case EVP_PKEY_SLH_DSA_SHAKE_256F: - // Fall through - case EVP_PKEY_SLH_DSA_SHAKE_256S: - // Fall through - case EVP_PKEY_ML_KEM_512: - // Fall through - case EVP_PKEY_ML_KEM_768: - // Fall through - case EVP_PKEY_ML_KEM_1024: - return ExportJwkPqcKey(env, key, target); -#endif } THROW_ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE(env); return false; @@ -306,35 +269,19 @@ int GetNidFromName(const char* name) { const char* name; int nid; } kNameToNid[] = { - {"Ed25519", EVP_PKEY_ED25519}, - {"Ed448", EVP_PKEY_ED448}, - {"X25519", EVP_PKEY_X25519}, - {"X448", EVP_PKEY_X448}, -#if OPENSSL_WITH_PQC - {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, - {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, - {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, - {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, - {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, - {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, - {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, - {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, - {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, - {"SLH-DSA-SHA2-192s", EVP_PKEY_SLH_DSA_SHA2_192S}, - {"SLH-DSA-SHA2-256f", EVP_PKEY_SLH_DSA_SHA2_256F}, - {"SLH-DSA-SHA2-256s", EVP_PKEY_SLH_DSA_SHA2_256S}, - {"SLH-DSA-SHAKE-128f", EVP_PKEY_SLH_DSA_SHAKE_128F}, - {"SLH-DSA-SHAKE-128s", EVP_PKEY_SLH_DSA_SHAKE_128S}, - {"SLH-DSA-SHAKE-192f", EVP_PKEY_SLH_DSA_SHAKE_192F}, - {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, - {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, - {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, -#endif + {"Ed25519", EVP_PKEY_ED25519}, + {"Ed448", EVP_PKEY_ED448}, + {"X25519", EVP_PKEY_X25519}, + {"X448", EVP_PKEY_X448}, }; for (const auto& entry : kNameToNid) { if (StringEqualNoCase(name, entry.name)) return entry.nid; } +#if OPENSSL_WITH_PQC + return GetPqcNidFromName(name); +#else return NID_undef; +#endif } bool IsUnavailablePqcKeyType(Environment* env, Local key_type) { @@ -442,35 +389,15 @@ bool KeyObjectData::ToEncodedPublicKey( const auto point = ECKeyPointer::GetPublicKey(ec_key); return ECPointToBuffer(env, group, point, form).ToLocal(out); } - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + is_raw_supported = is_raw_supported || IsPqcKeyId(id); #endif - break; - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!is_raw_supported) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } auto raw_data = pkey.rawPublicKey(); if (!raw_data) { @@ -517,29 +444,18 @@ bool KeyObjectData::ToEncodedPrivateKey( } return Buffer::Copy(env, buf.get(), buf.size()).ToLocal(out); } - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + // SLH-DSA uses raw private key; ML-DSA/ML-KEM use the seed format and are + // handled in the RAW_SEED branch below. + is_raw_supported = + is_raw_supported || (IsPqcKeyId(id) && !IsPqcSeedKeyId(id)); #endif - break; - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!is_raw_supported) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } auto raw_data = pkey.rawPrivateKey(); if (!raw_data) { @@ -551,21 +467,11 @@ bool KeyObjectData::ToEncodedPrivateKey( } else if (config.format == EVPKeyPointer::PKFormatType::RAW_SEED) { Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); - switch (pkey.id()) { #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - break; -#endif - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!IsPqcSeedKeyId(pkey.id())) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } -#if OPENSSL_WITH_PQC auto raw_data = pkey.rawSeed(); if (!raw_data) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed"); @@ -738,33 +644,17 @@ static KeyObjectData ImportRawKey(Environment* env, fn = target_type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate : EVPKeyPointer::NewRawPublic; break; + default: #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - fn = target_type == kKeyTypePrivate ? EVPKeyPointer::NewRawSeed - : EVPKeyPointer::NewRawPublic; - break; - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: - fn = target_type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate - : EVPKeyPointer::NewRawPublic; - break; + if (IsPqcKeyId(id)) { + if (target_type == kKeyTypePrivate) { + fn = IsPqcSeedKeyId(id) ? EVPKeyPointer::NewRawSeed + : EVPKeyPointer::NewRawPrivate; + } else { + fn = EVPKeyPointer::NewRawPublic; + } + } #endif - default: break; } @@ -1383,12 +1273,14 @@ Local KeyObjectHandle::GetAsymmetricKeyType() const { return env()->crypto_ml_dsa_65_string(); case EVP_PKEY_ML_DSA_87: return env()->crypto_ml_dsa_87_string(); - case EVP_PKEY_ML_KEM_512: - return env()->crypto_ml_kem_512_string(); case EVP_PKEY_ML_KEM_768: return env()->crypto_ml_kem_768_string(); case EVP_PKEY_ML_KEM_1024: return env()->crypto_ml_kem_1024_string(); + +#ifndef OPENSSL_IS_BORINGSSL + case EVP_PKEY_ML_KEM_512: + return env()->crypto_ml_kem_512_string(); case EVP_PKEY_SLH_DSA_SHA2_128F: return env()->crypto_slh_dsa_sha2_128f_string(); case EVP_PKEY_SLH_DSA_SHA2_128S: @@ -1413,6 +1305,7 @@ Local KeyObjectHandle::GetAsymmetricKeyType() const { return env()->crypto_slh_dsa_shake_256f_string(); case EVP_PKEY_SLH_DSA_SHAKE_256S: return env()->crypto_slh_dsa_shake_256s_string(); +#endif #endif default: return Undefined(env()->isolate()); @@ -1533,9 +1426,10 @@ void KeyObjectHandle::RawPublicKey( case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: case EVP_PKEY_ML_KEM_768: case EVP_PKEY_ML_KEM_1024: +#ifndef OPENSSL_IS_BORINGSSL + case EVP_PKEY_ML_KEM_512: case EVP_PKEY_SLH_DSA_SHA2_128F: case EVP_PKEY_SLH_DSA_SHA2_128S: case EVP_PKEY_SLH_DSA_SHA2_192F: @@ -1548,6 +1442,7 @@ void KeyObjectHandle::RawPublicKey( case EVP_PKEY_SLH_DSA_SHAKE_192S: case EVP_PKEY_SLH_DSA_SHAKE_256F: case EVP_PKEY_SLH_DSA_SHAKE_256S: +#endif #endif break; default: @@ -1583,6 +1478,7 @@ void KeyObjectHandle::RawPrivateKey( case EVP_PKEY_X25519: case EVP_PKEY_X448: #if OPENSSL_WITH_PQC +#ifndef OPENSSL_IS_BORINGSSL case EVP_PKEY_SLH_DSA_SHA2_128F: case EVP_PKEY_SLH_DSA_SHA2_128S: case EVP_PKEY_SLH_DSA_SHA2_192F: @@ -1595,6 +1491,7 @@ void KeyObjectHandle::RawPrivateKey( case EVP_PKEY_SLH_DSA_SHAKE_192S: case EVP_PKEY_SLH_DSA_SHAKE_256F: case EVP_PKEY_SLH_DSA_SHAKE_256S: +#endif #endif break; default: @@ -1695,7 +1592,9 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: +#ifndef OPENSSL_IS_BORINGSSL case EVP_PKEY_ML_KEM_512: +#endif case EVP_PKEY_ML_KEM_768: case EVP_PKEY_ML_KEM_1024: break; @@ -2141,9 +2040,12 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_44); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_65); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_87); +#ifndef OPENSSL_IS_BORINGSSL NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_512); +#endif NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_768); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_1024); +#ifndef OPENSSL_IS_BORINGSSL NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128F); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128S); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_192F); @@ -2156,6 +2058,7 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_192S); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_256F); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_256S); +#endif #endif NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448); diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc index bf40052fb6ea1e..c6c1dff6d63b62 100644 --- a/src/crypto/crypto_pqc.cc +++ b/src/crypto/crypto_pqc.cc @@ -20,29 +20,39 @@ namespace { struct PqcAlgorithm { int id; const char* name; - bool - use_seed; // true: rawSeed/NewRawSeed, false: rawPrivateKey/NewRawPrivate + // true: rawSeed/NewRawSeed, false: rawPrivateKey/NewRawPrivate + bool use_seed; + // true: signature algorithm (ML-DSA, SLH-DSA), + // false: key encapsulation mechanism (ML-KEM). + bool is_signature; }; +// use_seed is true for algorithms whose private key material is carried as a +// seed (rawSeed/NewRawSeed), false for those that use the expanded private +// key (rawPrivateKey / NewRawPrivate). ML-KEM-512 and SLH-DSA are only +// exposed by OpenSSL and are not available in BoringSSL. constexpr PqcAlgorithm kPqcAlgorithms[] = { - {EVP_PKEY_ML_DSA_44, "ML-DSA-44", true}, - {EVP_PKEY_ML_DSA_65, "ML-DSA-65", true}, - {EVP_PKEY_ML_DSA_87, "ML-DSA-87", true}, - {EVP_PKEY_ML_KEM_512, "ML-KEM-512", true}, - {EVP_PKEY_ML_KEM_768, "ML-KEM-768", true}, - {EVP_PKEY_ML_KEM_1024, "ML-KEM-1024", true}, - {EVP_PKEY_SLH_DSA_SHA2_128F, "SLH-DSA-SHA2-128f", false}, - {EVP_PKEY_SLH_DSA_SHA2_128S, "SLH-DSA-SHA2-128s", false}, - {EVP_PKEY_SLH_DSA_SHA2_192F, "SLH-DSA-SHA2-192f", false}, - {EVP_PKEY_SLH_DSA_SHA2_192S, "SLH-DSA-SHA2-192s", false}, - {EVP_PKEY_SLH_DSA_SHA2_256F, "SLH-DSA-SHA2-256f", false}, - {EVP_PKEY_SLH_DSA_SHA2_256S, "SLH-DSA-SHA2-256s", false}, - {EVP_PKEY_SLH_DSA_SHAKE_128F, "SLH-DSA-SHAKE-128f", false}, - {EVP_PKEY_SLH_DSA_SHAKE_128S, "SLH-DSA-SHAKE-128s", false}, - {EVP_PKEY_SLH_DSA_SHAKE_192F, "SLH-DSA-SHAKE-192f", false}, - {EVP_PKEY_SLH_DSA_SHAKE_192S, "SLH-DSA-SHAKE-192s", false}, - {EVP_PKEY_SLH_DSA_SHAKE_256F, "SLH-DSA-SHAKE-256f", false}, - {EVP_PKEY_SLH_DSA_SHAKE_256S, "SLH-DSA-SHAKE-256s", false}, + {EVP_PKEY_ML_DSA_44, "ML-DSA-44", true, true}, + {EVP_PKEY_ML_DSA_65, "ML-DSA-65", true, true}, + {EVP_PKEY_ML_DSA_87, "ML-DSA-87", true, true}, + {EVP_PKEY_ML_KEM_768, "ML-KEM-768", true, false}, + {EVP_PKEY_ML_KEM_1024, "ML-KEM-1024", true, false}, + +#ifndef OPENSSL_IS_BORINGSSL + {EVP_PKEY_ML_KEM_512, "ML-KEM-512", true, false}, + {EVP_PKEY_SLH_DSA_SHA2_128F, "SLH-DSA-SHA2-128f", false, true}, + {EVP_PKEY_SLH_DSA_SHA2_128S, "SLH-DSA-SHA2-128s", false, true}, + {EVP_PKEY_SLH_DSA_SHA2_192F, "SLH-DSA-SHA2-192f", false, true}, + {EVP_PKEY_SLH_DSA_SHA2_192S, "SLH-DSA-SHA2-192s", false, true}, + {EVP_PKEY_SLH_DSA_SHA2_256F, "SLH-DSA-SHA2-256f", false, true}, + {EVP_PKEY_SLH_DSA_SHA2_256S, "SLH-DSA-SHA2-256s", false, true}, + {EVP_PKEY_SLH_DSA_SHAKE_128F, "SLH-DSA-SHAKE-128f", false, true}, + {EVP_PKEY_SLH_DSA_SHAKE_128S, "SLH-DSA-SHAKE-128s", false, true}, + {EVP_PKEY_SLH_DSA_SHAKE_192F, "SLH-DSA-SHAKE-192f", false, true}, + {EVP_PKEY_SLH_DSA_SHAKE_192S, "SLH-DSA-SHAKE-192s", false, true}, + {EVP_PKEY_SLH_DSA_SHAKE_256F, "SLH-DSA-SHAKE-256f", false, true}, + {EVP_PKEY_SLH_DSA_SHAKE_256S, "SLH-DSA-SHAKE-256s", false, true}, +#endif }; const PqcAlgorithm* FindPqcAlgorithmById(int id) { @@ -176,15 +186,26 @@ KeyObjectData ImportJWKPqcKey(Environment* env, Local jwk) { return KeyObjectData::CreateAsymmetric(type, std::move(pkey)); } -bool IsPqcRawPrivateKeyId(int id) { - const PqcAlgorithm* alg = FindPqcAlgorithmById(id); - return alg != nullptr && !alg->use_seed; +bool IsPqcKeyId(int id) { + return FindPqcAlgorithmById(id) != nullptr; } bool IsPqcSeedKeyId(int id) { const PqcAlgorithm* alg = FindPqcAlgorithmById(id); return alg != nullptr && alg->use_seed; } + +bool IsPqcSignatureKeyId(int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + return alg != nullptr && alg->is_signature; +} + +int GetPqcNidFromName(const char* name) { + for (const auto& alg : kPqcAlgorithms) { + if (StringEqualNoCase(name, alg.name)) return alg.id; + } + return NID_undef; +} #endif } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_pqc.h b/src/crypto/crypto_pqc.h index 156066097bbfb9..bb4c41d0b06f0e 100644 --- a/src/crypto/crypto_pqc.h +++ b/src/crypto/crypto_pqc.h @@ -18,10 +18,17 @@ KeyObjectData ImportJWKPqcKey(Environment* env, v8::Local jwk); // Returns true for PQC algorithms that support raw private key export/import. bool IsPqcRawPrivateKeyId(int id); +// Returns true if the given EVP_PKEY id is a PQC algorithm known to Node. +bool IsPqcKeyId(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); +// Returns true for PQC signature algorithms (ML-DSA, SLH-DSA). Returns false +// for ML-KEM or for non-PQC ids. +bool IsPqcSignatureKeyId(int id); +// Returns the EVP_PKEY id for the given PQC algorithm name, or NID_undef. +int GetPqcNidFromName(const char* name); #endif } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index bd3c9f538c5de5..658c86d45d6936 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -3,6 +3,7 @@ #include "base_object-inl.h" #include "crypto/crypto_ec.h" #include "crypto/crypto_keys.h" +#include "crypto/crypto_pqc.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -237,33 +238,17 @@ bool UseP1363Encoding(const EVPKeyPointer& key, const DSASigEnc dsa_encoding) { } bool SupportsContextString(const EVPKeyPointer& key) { -#if OPENSSL_VERSION_NUMBER < 0x3020000fL +#if OPENSSL_VERSION_NUMBER < 0x3020000fL && !defined(OPENSSL_IS_BORINGSSL) return false; #else - switch (key.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: + const int id = key.id(); #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + if (IsPqcSignatureKeyId(id)) return true; #endif - return true; - default: - return false; - } +#ifndef OPENSSL_IS_BORINGSSL + if (id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448) return true; +#endif + return false; #endif } } // namespace diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 0e743135e8de15..30b60cf1ac77ce 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -139,7 +139,7 @@ void InitCryptoOnce() { OPENSSL_init_ssl(0, settings); -#if OPENSSL_WITH_PQC +#if OPENSSL_WITH_PQC && !defined(OPENSSL_IS_BORINGSSL) // Configure all loaded providers to prefer seed-only format for ML-KEM and // ML-DSA private keys in PKCS#8 export, falling back to priv-only when a // seed is not available. The provider encoder reads these parameters at diff --git a/src/node_crypto.cc b/src/node_crypto.cc index c0869f40e0410d..25a3aa6d6fea51 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -68,11 +68,14 @@ namespace crypto { #endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2 // KEM and KMAC functionality requires OpenSSL 3.0.0 or later -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) #define KEM_NAMESPACE_LIST(V) V(KEM) -#define KMAC_NAMESPACE_LIST(V) V(Kmac) #else #define KEM_NAMESPACE_LIST(V) +#endif +#if OPENSSL_VERSION_MAJOR >= 3 +#define KMAC_NAMESPACE_LIST(V) V(Kmac) +#else #define KMAC_NAMESPACE_LIST(V) #endif diff --git a/src/node_crypto.h b/src/node_crypto.h index 80657431a791db..c2f53f3a395801 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -40,8 +40,10 @@ #include "crypto/crypto_hash.h" #include "crypto/crypto_hkdf.h" #include "crypto/crypto_hmac.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) #include "crypto/crypto_kem.h" +#endif +#if OPENSSL_VERSION_MAJOR >= 3 #include "crypto/crypto_kmac.h" #endif #include "crypto/crypto_keygen.h" diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index d255b5eea80e6a..d5bdd8f46ff573 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -101,6 +101,8 @@ all: \ ml_dsa_44_private.pem \ ml_dsa_44_private_seed_only.pem \ ml_dsa_44_private_priv_only.pem \ + ml_dsa_44_private_encrypted.pem \ + ml_dsa_44_private_encrypted.der \ ml_dsa_44_public.pem \ ml_dsa_65_private.pem \ ml_dsa_65_private_seed_only.pem \ @@ -123,6 +125,8 @@ all: \ ml_kem_768_private.pem \ ml_kem_768_private_seed_only.pem \ ml_kem_768_private_priv_only.pem \ + ml_kem_768_private_encrypted.pem \ + ml_kem_768_private_encrypted.der \ ml_kem_768_public.pem \ ml_kem_1024_private.pem \ ml_kem_1024_private_seed_only.pem \ @@ -1005,6 +1009,12 @@ ml_dsa_44_private_priv_only.pem: ml_dsa_44_private.pem ml_dsa_44_public.pem: ml_dsa_44_private.pem openssl pkey -in ml_dsa_44_private.pem -pubout -out ml_dsa_44_public.pem +ml_dsa_44_private_encrypted.pem: ml_dsa_44_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-dsa.output_formats=seed-only -in ml_dsa_44_private_seed_only.pem -passout 'pass:password' -out ml_dsa_44_private_encrypted.pem + +ml_dsa_44_private_encrypted.der: ml_dsa_44_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-dsa.output_formats=seed-only -in ml_dsa_44_private_seed_only.pem -passout 'pass:password' -outform DER -out ml_dsa_44_private_encrypted.der + ml_dsa_65_private.pem: openssl genpkey -algorithm ml-dsa-65 -out ml_dsa_65_private.pem @@ -1053,6 +1063,12 @@ ml_kem_768_private_priv_only.pem: ml_kem_768_private.pem ml_kem_768_public.pem: ml_kem_768_private.pem openssl pkey -in ml_kem_768_private.pem -pubout -out ml_kem_768_public.pem +ml_kem_768_private_encrypted.pem: ml_kem_768_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-kem.output_formats=seed-only -in ml_kem_768_private_seed_only.pem -passout 'pass:password' -out ml_kem_768_private_encrypted.pem + +ml_kem_768_private_encrypted.der: ml_kem_768_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-kem.output_formats=seed-only -in ml_kem_768_private_seed_only.pem -passout 'pass:password' -outform DER -out ml_kem_768_private_encrypted.der + ml_kem_1024_private.pem: openssl genpkey -algorithm ml-kem-1024 -out ml_kem_1024_private.pem diff --git a/test/fixtures/keys/ml_dsa_44_private_encrypted.der b/test/fixtures/keys/ml_dsa_44_private_encrypted.der new file mode 100644 index 0000000000000000000000000000000000000000..2ae136bc7961e5fbcd3be7c19b4a3394a6ffdf25 GIT binary patch literal 166 zcmXqLTx<}}#;Mij(e|B}k(JjV$iNW6AOlb39Ol4+a)M61>&*ISfpVxQO&6=-`Eprm`SRBGuE%`QezIP0J z1M|8SyCc%b8*tA~?e3`bs$G$oE()}%ymS>mM Kp6{Ay^&0?pj6U@M literal 0 HcmV?d00001 diff --git a/test/fixtures/keys/ml_dsa_44_private_encrypted.pem b/test/fixtures/keys/ml_dsa_44_private_encrypted.pem new file mode 100644 index 00000000000000..e127aa0085bc5f --- /dev/null +++ b/test/fixtures/keys/ml_dsa_44_private_encrypted.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGjMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBD1YJCeuwCAuw/ktX9I +K9g9AgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ7kmeI0FzRNLI2m54 +BMASEgRAWBi1BRsuBBVt2kWVTbz8tQa8K3lV+nNE+iRGlMaOhnF5o5Kx4mQnzE1q +ppIFNbWPGGr+xKHTU6fNfNnMecVXKA== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/keys/ml_kem_768_private_encrypted.der b/test/fixtures/keys/ml_kem_768_private_encrypted.der new file mode 100644 index 0000000000000000000000000000000000000000..4f9097751c2249b118f919cb244ddecbae399726 GIT binary patch literal 198 zcmXqLJZuop#;Mij(e|B}k(JjV$iNW6Bl!re{s>;}1+s91I3L zY#b0hOq{F?2C{6N32h#Xsmv^lS}Xz=8qR66`4mV{dIBhwrvSFNgJ zn!e7*@X8F1H8*~R9Bhi{&(l?$(YO58E0y_f+XW@JE?#VV_u#IU<=d-|JlMv~kz%9r tl$FE(#unzIw->stUevvD$CfwTYxMpc`KDZV^ReOqo0h2yGvl&<004`KP}Kkc literal 0 HcmV?d00001 diff --git a/test/fixtures/keys/ml_kem_768_private_encrypted.pem b/test/fixtures/keys/ml_kem_768_private_encrypted.pem new file mode 100644 index 00000000000000..0e3d54e75a3259 --- /dev/null +++ b/test/fixtures/keys/ml_kem_768_private_encrypted.pem @@ -0,0 +1,7 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHDMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBBSwnAxR1nLC5FZtJyu +lumDAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQyBgMhkKPAMK6jaIc +YxhYcgRg3P97VHfT14YDN024txZznhhzC0mWGNpP6f1EV/mP/YttQp2JTXMKID4V +um3QuQes5my0oOIuiRl3gYIz/BDjKkqLagYBQmUcUUlURgaYJ67Yk3BZg6ULjXmq +EdLYqK5D +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 0572107e9f492e..80c32ff8031a4e 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -2,14 +2,14 @@ import * as crypto from 'node:crypto' import { hasOpenSSL } from '../../common/crypto.js' -const pqc = hasOpenSSL(3, 5); +const boringSSL = process.features.openssl_is_boringssl; +const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2); const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); const chacha = crypto.getCiphers().includes('chacha20-poly1305'); const ocb = hasOpenSSL(3); const kmac = hasOpenSSL(3); -const boringSSL = process.features.openssl_is_boringssl; const { subtle } = globalThis.crypto; const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); @@ -75,7 +75,7 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], @@ -96,7 +96,7 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], @@ -117,7 +117,7 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], @@ -141,7 +141,7 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [false, 'AES-CTR'], @@ -200,37 +200,37 @@ export const vectors = { [false, 'AES-OCB'], ], 'encapsulateBits': [ - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], ], 'encapsulateKey': [ - [pqc, 'ML-KEM-512', 'AES-KW'], - [pqc, 'ML-KEM-512', 'AES-GCM'], - [pqc, 'ML-KEM-512', 'AES-CTR'], - [pqc, 'ML-KEM-512', 'AES-CBC'], - [pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'], - [pqc, 'ML-KEM-512', 'HKDF'], - [pqc, 'ML-KEM-512', 'PBKDF2'], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [pqc && !boringSSL, 'ML-KEM-768', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-GCM'], + [pqc, 'ML-KEM-768', 'AES-CTR'], + [pqc, 'ML-KEM-768', 'AES-CBC'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'HKDF'], + [pqc, 'ML-KEM-768', 'PBKDF2'], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], ], 'decapsulateBits': [ - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], ], 'decapsulateKey': [ - [pqc, 'ML-KEM-512', 'AES-KW'], - [pqc, 'ML-KEM-512', 'AES-GCM'], - [pqc, 'ML-KEM-512', 'AES-CTR'], - [pqc, 'ML-KEM-512', 'AES-CBC'], - [pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'], - [pqc, 'ML-KEM-512', 'HKDF'], - [pqc, 'ML-KEM-512', 'PBKDF2'], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [pqc && !boringSSL, 'ML-KEM-768', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-GCM'], + [pqc, 'ML-KEM-768', 'AES-CTR'], + [pqc, 'ML-KEM-768', 'AES-CBC'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'HKDF'], + [pqc, 'ML-KEM-768', 'PBKDF2'], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], ], }; diff --git a/test/parallel/test-crypto-encap-decap.js b/test/parallel/test-crypto-encap-decap.js index 38e24a7341713a..f2259194a9e15d 100644 --- a/test/parallel/test-crypto-encap-decap.js +++ b/test/parallel/test-crypto-encap-decap.js @@ -9,7 +9,9 @@ const fixtures = require('../common/fixtures'); const { hasOpenSSL } = require('../common/crypto'); const { promisify } = require('util'); -if (!hasOpenSSL(3)) { +const isBoringSSL = process.features.openssl_is_boringssl; + +if (!hasOpenSSL(3) && !isBoringSSL) { assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' }); return; } @@ -79,25 +81,25 @@ const keys = { raw: true, }, 'ml-kem-512': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5), // BoringSSL does not support ML-KEM-512 publicKey: fixtures.readKey('ml_kem_512_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_512_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_512_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 768, raw: true, }, 'ml-kem-768': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5) || isBoringSSL, publicKey: fixtures.readKey('ml_kem_768_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_768_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_768_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1088, raw: true, }, 'ml-kem-1024': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5) || isBoringSSL, publicKey: fixtures.readKey('ml_kem_1024_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_1024_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_1024_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1568, raw: true, @@ -109,7 +111,7 @@ for (const [name, { }] of Object.entries(keys)) { if (!supported) { assert.throws(() => crypto.encapsulate(publicKey), - { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_CRYPTO_OPERATION_FAILED/ }); + { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM|ERR_CRYPTO_OPERATION_FAILED/ }); continue; } @@ -211,7 +213,7 @@ for (const [name, { } else if (name.startsWith('p-')) { wrongPrivateKey = name === 'p-256' ? keys['p-384'].privateKey : keys['p-256'].privateKey; } else if (name.startsWith('ml-')) { - wrongPrivateKey = name === 'ml-kem-512' ? keys['ml-kem-768'].privateKey : keys['ml-kem-512'].privateKey; + wrongPrivateKey = name === 'ml-kem-768' ? keys['ml-kem-1024'].privateKey : keys['ml-kem-768'].privateKey; } else { wrongPrivateKey = keys.x25519.privateKey; } diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js index 311659ef004ea2..024d5f6f199ffc 100644 --- a/test/parallel/test-crypto-key-objects-raw.js +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -170,15 +170,22 @@ if (hasOpenSSL(3, 5)) { // PQC import throws when PQC is not supported if (!hasOpenSSL(3, 5)) { - for (const asymmetricKeyType of [ - 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', - 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', - 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f', - ]) { + const unsupported = process.features.openssl_is_boringssl ? + // BoringSSL supports ML-DSA and ML-KEM-{768,1024}, but not ML-KEM-512 or SLH-DSA. + ['ml-kem-512', 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f'] : + [ + 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', + 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', + 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f', + ]; + for (const asymmetricKeyType of unsupported) { for (const format of ['raw-public', 'raw-private', 'raw-seed']) { assert.throws(() => crypto.createPublicKey({ key: Buffer.alloc(32), format, asymmetricKeyType, - }), { code: 'ERR_INVALID_ARG_VALUE' }); + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /Invalid asymmetricKeyType|Unsupported key type/ + }); } } } @@ -224,27 +231,27 @@ if (!hasOpenSSL(3, 5)) { }), { code: 'ERR_INVALID_ARG_VALUE' }); } -// ML-KEM: -768 and -512 public keys cannot be imported as the other type -if (hasOpenSSL(3, 5)) { - const mlKem512Pub = crypto.createPublicKey( - fixtures.readKey('ml_kem_512_public.pem', 'ascii')); +// ML-KEM: public keys of different type cannot be imported as the other type +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlKem768Pub = crypto.createPublicKey( fixtures.readKey('ml_kem_768_public.pem', 'ascii')); + const mlKem1024Pub = crypto.createPublicKey( + fixtures.readKey('ml_kem_1024_public.pem', 'ascii')); - const mlKem512RawPub = mlKem512Pub.export({ format: 'raw-public' }); const mlKem768RawPub = mlKem768Pub.export({ format: 'raw-public' }); + const mlKem1024RawPub = mlKem1024Pub.export({ format: 'raw-public' }); assert.throws(() => crypto.createPublicKey({ - key: mlKem512RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-768', + key: mlKem768RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-1024', }), { code: 'ERR_INVALID_ARG_VALUE' }); assert.throws(() => crypto.createPublicKey({ - key: mlKem768RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-512', + key: mlKem1024RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-768', }), { code: 'ERR_INVALID_ARG_VALUE' }); } // ML-DSA: -44 and -65 public keys cannot be imported as the other type -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlDsa44Pub = crypto.createPublicKey( fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); const mlDsa65Pub = crypto.createPublicKey( @@ -357,10 +364,10 @@ if (hasOpenSSL(3, 5)) { } // raw-private cannot be used for ml-kem and ml-dsa -if (hasOpenSSL(3, 5)) { - for (const type of ['ml-kem-512', 'ml-dsa-44']) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + for (const type of ['ml-kem-768', 'ml-dsa-44']) { const priv = crypto.createPrivateKey( - fixtures.readKey(`${type.replaceAll('-', '_')}_private.pem`, 'ascii')); + fixtures.readKey(`${type.replaceAll('-', '_')}_private_seed_only.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-private' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); assert.throws(() => crypto.createPrivateKey({ @@ -465,9 +472,9 @@ if (hasOpenSSL(3, 5)) { { code: 'ERR_INVALID_ARG_VALUE' }); // PQC raw-seed -> createPublicKey - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlDsaPriv = crypto.createPrivateKey( - fixtures.readKey('ml_dsa_44_private.pem', 'ascii')); + fixtures.readKey('ml_dsa_44_private_seed_only.pem', 'ascii')); const mlDsaPub = crypto.createPublicKey( fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); const mlDsaRawSeed = mlDsaPriv.export({ format: 'raw-seed' }); diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 54449329cb551a..2b6074474c919f 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -200,7 +200,7 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { } } -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { const { publicKey, privateKey } = generateKeyPairSync(name.toLowerCase()); assert.throws(() => { diff --git a/test/parallel/test-crypto-keygen-raw.js b/test/parallel/test-crypto-keygen-raw.js index 5b7abe3f72d9dd..e55c3f10eed8e3 100644 --- a/test/parallel/test-crypto-keygen-raw.js +++ b/test/parallel/test-crypto-keygen-raw.js @@ -205,7 +205,7 @@ if (!process.features.openssl_is_boringssl) { } // PQC key types -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // Test raw encoding for ML-DSA key types (raw-public + raw-seed only). { for (const type of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { @@ -232,6 +232,10 @@ if (hasOpenSSL(3, 5)) { // Test raw encoding for ML-KEM key types (raw-public + raw-seed only). { for (const type of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { + if (process.features.openssl_is_boringssl && type === 'ml-kem-512') { + common.printSkipMessage(`Skipping unsupported ${type} test case`); + continue; + } const { publicKey, privateKey } = generateKeyPairSync(type, { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-seed' }, @@ -246,7 +250,7 @@ if (hasOpenSSL(3, 5)) { // Test error: raw-private with ML-KEM (not supported). { - assert.throws(() => generateKeyPairSync('ml-kem-512', { + assert.throws(() => generateKeyPairSync('ml-kem-768', { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); @@ -255,6 +259,10 @@ if (hasOpenSSL(3, 5)) { // Test raw encoding for SLH-DSA key types. { for (const type of ['slh-dsa-sha2-128f', 'slh-dsa-shake-128f']) { + if (process.features.openssl_is_boringssl) { + common.printSkipMessage(`Skipping unsupported ${type} test case`); + continue; + } const { publicKey, privateKey } = generateKeyPairSync(type, { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, @@ -266,11 +274,13 @@ if (hasOpenSSL(3, 5)) { } // Test error: raw-seed with SLH-DSA (not supported). - { + if (!process.features.openssl_is_boringssl) { assert.throws(() => generateKeyPairSync('slh-dsa-sha2-128f', { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-seed' }, }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } else { + common.printSkipMessage('Skipping unsupported slh-dsa test case'); } // Test async generateKeyPair with raw encoding for PQC types. diff --git a/test/parallel/test-crypto-pqc-encrypted-pkcs8.js b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js new file mode 100644 index 00000000000000..b4a1b586d21d10 --- /dev/null +++ b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js @@ -0,0 +1,134 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); + +const assert = require('assert'); +const { + createPrivateKey, + generateKeyPairSync, + getCiphers, +} = require('crypto'); + +const algorithms = new Set([ + 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', + 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', +]); +// BoringSSL does not support ML-KEM-512. +if (process.features.openssl_is_boringssl) { + algorithms.delete('ml-kem-512'); +} + +// Exercise each CBC cipher that PBES2 may use. This covers multiple +// EVP_CIPHER_key_length values (16 / 24 / 32) and, for variable-key +// ciphers like RC2, the optional PBKDF2 keyLength INTEGER branch in +// the EncryptedPrivateKeyInfo parser. +const availableCiphers = new Set(getCiphers()); +const ciphers = [ + 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', + 'des-ede3-cbc', 'rc2-cbc', +].filter((c) => availableCiphers.has(c)); + +const passphrase = 'top secret'; +const wrongPassphraseError = + /bad decrypt|DECRYPTION_FAILED|BAD_DECRYPT|bad password|DECODE[ _]ERROR/i; +// A wrong passphrase usually fails during cipher finalization, but CBC output +// can have valid padding by chance. OpenSSL then parses the bad plaintext as +// PKCS#8 and may report ASN.1 or decoder errors from the same failed import. +function assertWrongPassphrase(fn) { + assert.throws(fn, (err) => wrongPassphraseError.test(err.message) || + err.code?.startsWith('ERR_OSSL_ASN1_') || + err.code === 'ERR_OSSL_UNSUPPORTED'); +} + +for (const asymmetricKeyType of algorithms) { + const { privateKey } = generateKeyPairSync(asymmetricKeyType); + assert.strictEqual(privateKey.asymmetricKeyType, asymmetricKeyType); + + const plainDer = privateKey.export({ type: 'pkcs8', format: 'der' }); + + for (const cipher of ciphers) { + for (const format of ['pem', 'der']) { + const encrypted = privateKey.export({ + type: 'pkcs8', + format, + cipher, + passphrase, + }); + + const imported = createPrivateKey({ + key: encrypted, + format, + type: 'pkcs8', + passphrase, + }); + assert.strictEqual(imported.type, 'private'); + assert.strictEqual(imported.asymmetricKeyType, asymmetricKeyType); + assert.deepStrictEqual( + imported.export({ type: 'pkcs8', format: 'der' }), + plainDer, + ); + + assertWrongPassphrase(() => createPrivateKey({ + key: encrypted, + format, + type: 'pkcs8', + passphrase: 'wrong', + })); + } + } +} + +// Cross-implementation compatibility: load encrypted PKCS#8 fixtures that +// were generated by OpenSSL's `openssl pkcs8` from the seed-only PQC +// PrivateKeyInfo fixtures. The inner seed-only form is portable across +// OpenSSL (>=3.5) and BoringSSL, and the matching JWK fixture provides the +// canonical key material used to derive the expected PKCS#8 bytes. +const fixtures = require('../common/fixtures'); +const fixtureCases = [ + { alg: 'ml-dsa-44', jwkFile: 'ml-dsa-44.json', + encBase: 'ml_dsa_44_private_encrypted' }, + { alg: 'ml-kem-768', jwkFile: 'ml-kem-768.json', + encBase: 'ml_kem_768_private_encrypted' }, +]; + +for (const { alg, jwkFile, encBase } of fixtureCases) { + const jwkKey = createPrivateKey({ + key: JSON.parse(fixtures.readKey(jwkFile, 'utf8')), + format: 'jwk', + }); + assert.strictEqual(jwkKey.asymmetricKeyType, alg); + const expectedDer = jwkKey.export({ type: 'pkcs8', format: 'der' }); + + for (const format of ['pem', 'der']) { + const encryptedFixture = fixtures.readKey( + `${encBase}.${format}`, + format === 'pem' ? 'utf8' : null, + ); + + const imported = createPrivateKey({ + key: encryptedFixture, + format, + type: 'pkcs8', + passphrase: 'password', + }); + assert.strictEqual(imported.asymmetricKeyType, alg); + assert.deepStrictEqual( + imported.export({ type: 'pkcs8', format: 'der' }), + expectedDer, + ); + + assertWrongPassphrase(() => createPrivateKey({ + key: encryptedFixture, + format, + type: 'pkcs8', + passphrase: 'wrong', + })); + } +} diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js index f18c555d4653d4..f2a19799c51541 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js @@ -4,10 +4,6 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { - common.skip('Skipping unsupported ML-DSA key tests'); -} - const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); @@ -104,7 +100,7 @@ for (const [asymmetricKeyType, pubLen] of [ } } - if (!hasOpenSSL(3, 5)) { + if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -119,11 +115,15 @@ for (const [asymmetricKeyType, pubLen] of [ assertPublicKey(publicKey); { - for (const [pem, hasSeed] of [ - [keys.private, true], - [keys.private_seed_only, true], - [keys.private_priv_only, false], + for (const [pem, hasSeed, seedOnly] of [ + [keys.private, true, false], + [keys.private_seed_only, true, true], + [keys.private_priv_only, false, false], ]) { + if (process.features.openssl_is_boringssl && !seedOnly) { + common.printSkipMessage('Skipping unsupported private key format test'); + continue; + } const pubFromPriv = createPublicKey(pem); assertPublicKey(pubFromPriv); assertPrivateKey(createPrivateKey(pem), hasSeed); diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js index 0c344ed100c2da..4c2ef216a0f1a7 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js @@ -100,7 +100,7 @@ for (const [asymmetricKeyType, pubLen] of [ } } - if (!hasOpenSSL(3, 5)) { + if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -110,16 +110,28 @@ for (const [asymmetricKeyType, pubLen] of [ code: hasOpenSSL(3) ? 'ERR_OSSL_UNSUPPORTED' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); } + } else if (process.features.openssl_is_boringssl && asymmetricKeyType === 'ml-kem-512') { + // BoringSSL does not support ML-KEM-512 + assert.throws(() => createPublicKey(keys.public), + { code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' }); + for (const pem of [keys.private, keys.private_seed_only, keys.private_priv_only]) { + assert.throws(() => createPrivateKey(pem), + { code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' }); + } } else { const publicKey = createPublicKey(keys.public); assertPublicKey(publicKey); { - for (const [pem, hasSeed] of [ - [keys.private, true], - [keys.private_seed_only, true], - [keys.private_priv_only, false], - ]) { + const entries = process.features.openssl_is_boringssl ? + // BoringSSL only supports the seed-only PKCS#8 private key encoding. + [[keys.private_seed_only, true]] : + [ + [keys.private, true], + [keys.private_seed_only, true], + [keys.private_priv_only, false], + ]; + for (const [pem, hasSeed] of entries) { const pubFromPriv = createPublicKey(pem); assertPublicKey(pubFromPriv); assertPrivateKey(createPrivateKey(pem), hasSeed); diff --git a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js index abad2c15cf01d1..e6534c988c4e2b 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js @@ -11,7 +11,7 @@ const { generateKeyPair, } = require('crypto'); -if (!hasOpenSSL(3, 5)) { +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { for (const asymmetricKeyType of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', diff --git a/test/parallel/test-crypto-pqc-keygen-ml-kem.js b/test/parallel/test-crypto-pqc-keygen-ml-kem.js index ea3ac9f4dde137..620f65c3a8d156 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-kem.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-kem.js @@ -11,7 +11,12 @@ const { generateKeyPair, } = require('crypto'); -if (!hasOpenSSL(3, 5)) { +const algorithms = process.features.openssl_is_boringssl ? + // BoringSSL does not support ML-KEM-512. + ['ml-kem-768', 'ml-kem-1024'] : + ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']; + +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', @@ -19,8 +24,7 @@ if (!hasOpenSSL(3, 5)) { }); } } else { - for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { - + for (const asymmetricKeyType of algorithms) { function assertJwk(jwk) { assert.strictEqual(jwk.kty, 'AKP'); assert.strictEqual(jwk.alg, asymmetricKeyType.toUpperCase()); @@ -67,3 +71,10 @@ if (!hasOpenSSL(3, 5)) { } } } + +if (process.features.openssl_is_boringssl) { + assert.throws(() => generateKeyPair('ml-kem-512', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The argument 'type' must be a supported key type/ + }); +} diff --git a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js index 57d6692ca79b55..535e6a33d5ccb0 100644 --- a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js @@ -6,8 +6,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { @@ -34,7 +34,15 @@ for (const [asymmetricKeyType, sigLen] of [ private_priv_only: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private_priv_only'), 'ascii'), }; - for (const privateKey of [keys.private, keys.private_seed_only, keys.private_priv_only]) { + for (const [privateKey, seedOnly] of [ + [keys.private, false], + [keys.private_seed_only, true], + [keys.private_priv_only, false], + ]) { + if (process.features.openssl_is_boringssl && !seedOnly) { + common.printSkipMessage('Skipping unsupported private key format test'); + continue; + } for (const data of [randomBytes(0), randomBytes(1), randomBytes(32), randomBytes(128), randomBytes(1024)]) { // sync { @@ -44,10 +52,12 @@ for (const [asymmetricKeyType, sigLen] of [ assert.strictEqual(verify(undefined, data, keys.public, Buffer.alloc(sigLen)), false); assert.strictEqual(verify(undefined, data, keys.public, signature), true); assert.strictEqual(verify(undefined, data, privateKey, signature), true); - assert.throws(() => sign('sha256', data, privateKey), { code: 'ERR_OSSL_INVALID_DIGEST' }); + const code = process.features.openssl_is_boringssl ? + 'ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED' : 'ERR_OSSL_INVALID_DIGEST'; + assert.throws(() => sign('sha256', data, privateKey), { code }); assert.throws( () => verify('sha256', data, keys.public, Buffer.alloc(sigLen)), - { code: 'ERR_OSSL_INVALID_DIGEST' }); + { code }); } // async @@ -62,8 +72,9 @@ for (const [asymmetricKeyType, sigLen] of [ })); })); - sign('sha256', data, privateKey, common.expectsError(/invalid digest/)); - verify('sha256', data, keys.public, Buffer.alloc(sigLen), common.expectsError(/invalid digest/)); + const message = process.features.openssl_is_boringssl ? /COMMAND_NOT_SUPPORTED/ : /invalid digest/; + sign('sha256', data, privateKey, common.expectsError(message)); + verify('sha256', data, keys.public, Buffer.alloc(sigLen), common.expectsError(message)); } } } diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index e30dbe7887166e..a908f588a1c28e 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -121,7 +121,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { asymmetric.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], @@ -136,7 +136,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['decapsulateKey', 'decapsulateBits'], }); } else { - common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } for (const { algorithm, usages, publicExpected, privateExpected } of asymmetric) { @@ -310,7 +310,7 @@ function assertSameSet(actual, expected, msg) { assert.deepStrictEqual(imported.usages, ['sign']); })()); - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // ML-DSA JWK roundtrip. tests.push((async () => { const { privateKey } = await subtle.generateKey( @@ -336,7 +336,7 @@ function assertSameSet(actual, expected, msg) { ['decapsulateKey', 'decapsulateBits']); })()); } else { - common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } // Spki import of RSA public key. @@ -523,7 +523,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { jwkPairVectors.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], @@ -538,7 +538,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['decapsulateKey', 'decapsulateBits'], }); } else { - common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } for (const { algorithm, usages, publicExpected, privateExpected } of jwkPairVectors) { diff --git a/test/parallel/test-webcrypto-encap-decap-ml-kem.js b/test/parallel/test-webcrypto-encap-decap-ml-kem.js index 450ba2cefb0a4f..958a4d240db148 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const crypto = require('crypto'); @@ -253,12 +253,16 @@ async function testDecapsulateBits({ name, publicKeyPem, privateKeyPem, results (async function() { const variations = []; - vectors.forEach((vector) => { + for (const vector of vectors) { + if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + common.printSkipMessage(`Skipping unsupported ${vector.name} test`); + continue; + } variations.push(testEncapsulateKey(vector)); variations.push(testEncapsulateBits(vector)); variations.push(testDecapsulateKey(vector)); variations.push(testDecapsulateBits(vector)); - }); + } await Promise.all(variations); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index 63766a7b377c77..20d46870e430f1 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -96,12 +96,23 @@ async function testImportSpki({ name, publicUsages }, extractable) { } async function testImportPkcs8({ name, privateUsages }, extractable) { - const key = await subtle.importKey( - 'pkcs8', - keyData[name].pkcs8, - { name }, - extractable, - privateUsages); + let key; + try { + key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + privateUsages); + } catch (err) { + if (process.features.openssl_is_boringssl) { + assert.strictEqual(err.name, 'DataError'); + assert.strictEqual(err.cause.code, 'ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED'); + common.printSkipMessage('Skipping unsupported private key format test'); + return; + } + throw err; + } assert.strictEqual(key.type, 'private'); assert.strictEqual(key.extractable, extractable); assert.deepStrictEqual(key.usages, privateUsages); @@ -480,14 +491,18 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { }); })().then(common.mustCall()); -(async function() { - for (const { name, privateUsages } of testVectors) { - const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); - const keyObject = createPrivateKey(pem); - const key = keyObject.toCryptoKey({ name }, true, privateUsages); - await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { - assert.strictEqual(err.name, 'OperationError'); - return true; - }); - } -})().then(common.mustCall()); +if (!process.features.openssl_is_boringssl) { + (async function() { + for (const { name, privateUsages } of testVectors) { + const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); + const keyObject = createPrivateKey(pem); + const key = keyObject.toCryptoKey({ name }, true, privateUsages); + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + return true; + }); + } + })().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported private key format test'); +} diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index 332d88d93f69d1..a3b1b3fe773090 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -96,12 +96,26 @@ async function testImportSpki({ name, publicUsages }, extractable) { } async function testImportPkcs8({ name, privateUsages }, extractable) { - const key = await subtle.importKey( - 'pkcs8', - keyData[name].pkcs8, - { name }, - extractable, - privateUsages); + let key; + try { + key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + privateUsages); + } catch (err) { + if (process.features.openssl_is_boringssl) { + assert.strictEqual(err.name, 'DataError'); + // It should really only be ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED + // but BoringSSL is inconsistent between handling ML-KEM and ML-DSA + // Fixed in https://github.com/google/boringssl/commit/94c4c7f9e0eeeff72ea1ac6abf1aed5bd2a82c0c + assert.match(err.cause.code, /ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM|ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED/); + common.printSkipMessage('Skipping unsupported private key format test'); + return; + } + throw err; + } assert.strictEqual(key.type, 'private'); assert.strictEqual(key.extractable, extractable); assert.deepStrictEqual(key.usages, privateUsages); @@ -239,7 +253,7 @@ async function testImportRawPublic({ name, publicUsages }, extractable) { subtle.importKey( 'raw-public', pub, - { name: name === 'ML-KEM-512' ? 'ML-KEM-768' : 'ML-KEM-512' }, + { name: name === 'ML-KEM-768' ? 'ML-KEM-1024' : 'ML-KEM-768' }, extractable, publicUsages), { message: 'Invalid keyData' }); } @@ -415,7 +429,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) privateUsages), // Invalid for a public key { message: /Unsupported key usage/ }); - for (const alg of [undefined, name === 'ML-KEM-512' ? 'ML-KEM-1024' : 'ML-KEM-512']) { + for (const alg of [undefined, name === 'ML-KEM-768' ? 'ML-KEM-1024' : 'ML-KEM-768']) { await assert.rejects( subtle.importKey( 'jwk', @@ -457,6 +471,10 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) (async function() { const tests = []; for (const vector of testVectors) { + if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); + continue; + } for (const extractable of [true, false]) { tests.push(testImportSpki(vector, extractable)); tests.push(testImportPkcs8(vector, extractable)); @@ -472,26 +490,14 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) })().then(common.mustCall()); (async function() { - const alg = 'ML-KEM-512'; + const alg = 'ML-KEM-768'; const pub = Buffer.from(keyData[alg].jwk.pub, 'base64url'); await assert.rejects(subtle.importKey('raw', pub, alg, false, []), { name: 'NotSupportedError', - message: 'Unable to import ML-KEM-512 using raw format', + message: 'Unable to import ML-KEM-768 using raw format', }); })().then(common.mustCall()); -(async function() { - for (const { name, privateUsages } of testVectors) { - const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); - const keyObject = createPrivateKey(pem); - const key = keyObject.toCryptoKey({ name }, true, privateUsages); - await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { - assert.strictEqual(err.name, 'OperationError'); - return true; - }); - } -})().then(common.mustCall()); - // Regression test: JWK `key_ops` validation must recognize ML-KEM operations // (encapsulateKey, encapsulateBits, decapsulateKey, decapsulateBits) so that // duplicate entries are rejected @@ -504,3 +510,19 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name: 'DataError', message: /Duplicate key operation/ }); } })().then(common.mustCall()); + +if (!process.features.openssl_is_boringssl) { + (async function() { + for (const { name, privateUsages } of testVectors) { + const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); + const keyObject = createPrivateKey(pem); + const key = keyObject.toCryptoKey({ name }, true, privateUsages); + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + return true; + }); + } + })().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported private key format test'); +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index e57c34436578ab..ae9b8d2c54d8b1 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -196,7 +196,7 @@ if (hasOpenSSL(3)) { } } -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { vectors[name] = { result: 'CryptoKeyPair', @@ -772,7 +772,7 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); } // Test ML-DSA Key Generation -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test( name, privateUsages, @@ -815,7 +815,7 @@ if (hasOpenSSL(3, 5)) { } // Test ML-KEM Key Generation -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test( name, privateUsages, @@ -850,7 +850,13 @@ if (hasOpenSSL(3, 5)) { assert.strictEqual(publicKey.usages, publicKey.usages); } - const kTests = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']; + const kTests = ['ML-KEM-768', 'ML-KEM-1024']; + + if (!process.features.openssl_is_boringssl) { + kTests.unshift('ML-KEM-512'); + } else { + common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); + } const tests = kTests.map((name) => test(name, ['decapsulateKey', 'decapsulateBits'], diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index 3ea0a961f41b90..d162190a0c53a5 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -80,7 +80,7 @@ const { privateKey } = await subtle.generateKey( await subtle.getPublicKey(privateKey, ['verify']); -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const kemPair = await subtle.generateKey( { name: 'ML-KEM-768' }, false, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index 1ed74c2508f438..b11e65ade79185 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const crypto = require('crypto'); diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 26e66d9aa0fa8b..0a6f5cffe7b934 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -173,7 +173,7 @@ if (!process.features.openssl_is_boringssl) { } // Test Sign/Verify ML-DSA -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test(name, data) { const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 8c57111daebca6..fb0803896e222f 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -215,7 +215,7 @@ async function generateKeysToWrap() { common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); } - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { parameters.push({ algorithm: { name }, From 88f9da97e23d46f773717559ead7325d00c05cba Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 22 Apr 2026 22:39:29 +0200 Subject: [PATCH 03/11] crypto: wire AES-KW in Web Cryptography when using BoringSSL Signed-off-by: Filip Skokan --- lib/internal/crypto/util.js | 1 - src/crypto/crypto_aes.cc | 75 +++++++++++++++++++ src/crypto/crypto_aes.h | 12 +++ test/fixtures/webcrypto/supports-level-2.mjs | 12 +-- .../webcrypto/supports-modern-algorithms.mjs | 10 ++- .../test-crypto-key-objects-to-crypto-key.js | 4 +- .../test-webcrypto-deduplicate-usages.js | 39 +++------- .../test-webcrypto-derivebits-hkdf.js | 4 +- test/parallel/test-webcrypto-keygen.js | 27 +++---- ...-webcrypto-promise-prototype-pollution.mjs | 20 ++--- test/parallel/test-webcrypto-wrap-unwrap.js | 24 +++--- .../test-webcrypto-derivebits-pbkdf2.js | 11 +-- 12 files changed, 142 insertions(+), 97 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index b743f3f93a149e..718954eaebbb7b 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -395,7 +395,6 @@ const kAlgorithmDefinitions = { // Conditionally supported algorithms const conditionalAlgorithms = { - 'AES-KW': !process.features.openssl_is_boringssl, 'AES-OCB': !!hasAesOcbMode, 'Argon2d': !!Argon2Job, 'Argon2i': !!Argon2Job, diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index fa619696ffd5b2..815c972837049a 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -181,6 +181,68 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, return WebCryptoCipherStatus::OK; } +#ifdef OPENSSL_IS_BORINGSSL +// AES Key Wrap using BoringSSL's low-level AES_wrap_key / AES_unwrap_key. +// BoringSSL does not expose EVP_aes_*_wrap via the +// EVP_CIPHER registry, so the EVP-based AES_Cipher path is unusable for +// AES-KW. This matches Chromium's WebCrypto AES-KW implementation. +WebCryptoCipherStatus AES_KW_Cipher(Environment* env, + const KeyObjectData& key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + ByteSource* out) { + CHECK_EQ(key_data.GetKeyType(), kKeyTypeSecret); + + const unsigned key_bits = + static_cast(key_data.GetSymmetricKeySize()) * 8; + const auto key_bytes = + reinterpret_cast(key_data.GetSymmetricKey()); + const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; + + AES_KEY aes_key; + if (encrypt) { + // Input must be a multiple of 8 bytes and at least 16 bytes. + if (in.size() < 16 || in.size() % 8 != 0) { + return WebCryptoCipherStatus::FAILED; + } + if (AES_set_encrypt_key(key_bytes, key_bits, &aes_key) != 0) { + return WebCryptoCipherStatus::FAILED; + } + auto buf = DataPointer::Alloc(in.size() + 8); + int len = AES_wrap_key(&aes_key, + nullptr, + static_cast(buf.get()), + in.data(), + in.size()); + if (len < 0 || static_cast(len) != in.size() + 8) { + return WebCryptoCipherStatus::FAILED; + } + *out = ByteSource::Allocated(buf.release()); + } else { + // Input must be a multiple of 8 bytes and at least 24 bytes. + if (in.size() < 24 || in.size() % 8 != 0) { + return WebCryptoCipherStatus::FAILED; + } + if (AES_set_decrypt_key(key_bytes, key_bits, &aes_key) != 0) { + return WebCryptoCipherStatus::FAILED; + } + auto buf = DataPointer::Alloc(in.size() - 8); + int len = AES_unwrap_key(&aes_key, + nullptr, + static_cast(buf.get()), + in.data(), + in.size()); + if (len < 0 || static_cast(len) != in.size() - 8) { + return WebCryptoCipherStatus::FAILED; + } + *out = ByteSource::Allocated(buf.release()); + } + + return WebCryptoCipherStatus::OK; +} +#endif // OPENSSL_IS_BORINGSSL + // The AES_CTR implementation here takes it's inspiration from the chromium // implementation here: // https://github.com/chromium/chromium/blob/7af6cfd/components/webcrypto/algorithms/aes_ctr.cc @@ -465,6 +527,19 @@ Maybe AESCipherTraits::AdditionalConfig( } #undef V +#ifdef OPENSSL_IS_BORINGSSL + // On BoringSSL the KW variants have no backing EVP_CIPHER; they use + // low-level AES_wrap_key / AES_unwrap_key instead. + const bool is_kw = params->variant == AESKeyVariant::KW_128 || + params->variant == AESKeyVariant::KW_192 || + params->variant == AESKeyVariant::KW_256; + + if (is_kw) { + UseDefaultIV(params); + return JustVoid(); + } +#endif + if (!params->cipher) { THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); return Nothing(); diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h index 5627f9020bad54..3cfd366d0f7a37 100644 --- a/src/crypto/crypto_aes.h +++ b/src/crypto/crypto_aes.h @@ -22,9 +22,21 @@ constexpr unsigned kNoAuthTagLength = static_cast(-1); V(GCM_128, AES_Cipher, ncrypto::Cipher::AES_128_GCM) \ V(GCM_192, AES_Cipher, ncrypto::Cipher::AES_192_GCM) \ V(GCM_256, AES_Cipher, ncrypto::Cipher::AES_256_GCM) \ + VARIANTS_KW(V) + +#ifdef OPENSSL_IS_BORINGSSL +// BoringSSL does not expose EVP_aes_*_wrap via the EVP_CIPHER registry. +// Route AES-KW through low-level AES_wrap_key / AES_unwrap_key instead. +#define VARIANTS_KW(V) \ + V(KW_128, AES_KW_Cipher, static_cast(nullptr)) \ + V(KW_192, AES_KW_Cipher, static_cast(nullptr)) \ + V(KW_256, AES_KW_Cipher, static_cast(nullptr)) +#else +#define VARIANTS_KW(V) \ V(KW_128, AES_Cipher, ncrypto::Cipher::AES_128_KW) \ V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW) \ V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW) +#endif #if OPENSSL_VERSION_MAJOR >= 3 #define VARIANTS_OCB(V) \ diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 196f4588188b48..51a3ff10ac2188 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -74,7 +74,7 @@ export const vectors = { [false, { name: 'AES-CBC', length: 25 }], [true, { name: 'AES-GCM', length: 128 }], [false, { name: 'AES-GCM', length: 25 }], - [!boringSSL, { name: 'AES-KW', length: 128 }], + [true, { name: 'AES-KW', length: 128 }], [false, { name: 'AES-KW', length: 25 }], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], @@ -166,7 +166,7 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [!boringSSL, 'AES-KW'], + [true, 'AES-KW'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], @@ -188,18 +188,18 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [!boringSSL, 'AES-KW'], + [true, 'AES-KW'], [true, 'Ed25519'], [true, 'X25519'], ], 'wrapKey': [ [false, 'AES-KW'], - [!boringSSL, 'AES-KW', 'AES-CTR'], - [!boringSSL, 'AES-KW', 'HMAC'], + [true, 'AES-KW', 'AES-CTR'], + [true, 'AES-KW', 'HMAC'], ], 'unwrapKey': [ [false, 'AES-KW'], - [!boringSSL, 'AES-KW', 'AES-CTR'], + [true, 'AES-KW', 'AES-CTR'], ], 'unsupported operation': [ [false, ''], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 80c32ff8031a4e..e476bfef9d26a1 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -205,11 +205,12 @@ export const vectors = { [pqc, 'ML-KEM-1024'], ], 'encapsulateKey': [ - [pqc && !boringSSL, 'ML-KEM-768', 'AES-KW'], + [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-KW'], [pqc, 'ML-KEM-768', 'AES-GCM'], [pqc, 'ML-KEM-768', 'AES-CTR'], [pqc, 'ML-KEM-768', 'AES-CBC'], - [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc && !boringSSL, 'ML-KEM-768', 'ChaCha20-Poly1305'], [pqc, 'ML-KEM-768', 'HKDF'], [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], @@ -222,11 +223,12 @@ export const vectors = { [pqc, 'ML-KEM-1024'], ], 'decapsulateKey': [ - [pqc && !boringSSL, 'ML-KEM-768', 'AES-KW'], + [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-KW'], [pqc, 'ML-KEM-768', 'AES-GCM'], [pqc, 'ML-KEM-768', 'AES-CTR'], [pqc, 'ML-KEM-768', 'AES-CBC'], - [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc && !boringSSL, 'ML-KEM-768', 'ChaCha20-Poly1305'], [pqc, 'ML-KEM-768', 'HKDF'], [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 2b6074474c919f..60d7a87a7f8b4c 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -31,8 +31,8 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { algorithms.push('ChaCha20-Poly1305'); if (process.features.openssl_is_boringssl) { - algorithms = algorithms.filter((a) => a !== 'AES-KW' && a !== 'ChaCha20-Poly1305'); - common.printSkipMessage('Skipping unsupported AES-KW/ChaCha20-Poly1305 test cases'); + algorithms = algorithms.filter((a) => a !== 'ChaCha20-Poly1305'); + common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); } for (const algorithm of algorithms) { diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index a908f588a1c28e..f10d0d75973fc8 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -42,17 +42,10 @@ function assertSameSet(actual, expected, msg) { { algorithm: { name: 'AES-GCM', length: 128 }, usages: ['decrypt', 'encrypt', 'decrypt'], expected: ['encrypt', 'decrypt'] }, - ]; - - if (!process.features.openssl_is_boringssl) { - symmetric.push({ - algorithm: { name: 'AES-KW', length: 128 }, + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { symmetric.push({ @@ -172,17 +165,10 @@ function assertSameSet(actual, expected, msg) { { algorithm: { name: 'HMAC', hash: 'SHA-256' }, keyData: new Uint8Array(32), usages: ['verify', 'sign', 'verify', 'sign'], expected: ['sign', 'verify'] }, - ]; - - if (!process.features.openssl_is_boringssl) { - rawSymmetric.push({ - algorithm: { name: 'AES-KW' }, keyData: new Uint8Array(16), + { algorithm: { name: 'AES-KW' }, keyData: new Uint8Array(16), usages: ['wrapKey', 'unwrapKey', 'wrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { // KMAC does not support `raw` format, only `raw-secret` and `jwk`. @@ -455,17 +441,10 @@ function assertSameSet(actual, expected, msg) { { algorithm: { name: 'AES-GCM', length: 128 }, usages: ['decrypt', 'encrypt', 'decrypt'], expected: ['encrypt', 'decrypt'] }, - ]; - - if (!process.features.openssl_is_boringssl) { - jwkVectors.push({ - algorithm: { name: 'AES-KW', length: 128 }, + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { jwkVectors.push({ diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index 689eaeb38fd66f..d2057d1f782e7f 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -24,12 +24,12 @@ const kDerivedKeyTypes = [ ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ]; if (!process.features.openssl_is_boringssl) { kDerivedKeyTypes.push( - ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], - ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ['HMAC', 256, 'SHA3-256', 'sign', 'verify'], ['HMAC', 256, 'SHA3-384', 'sign', 'verify'], ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index ae9b8d2c54d8b1..186016feaa3929 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -135,6 +135,14 @@ const vectors = { 'deriveBits', ], }, + 'AES-KW': { + algorithm: { length: 256 }, + result: 'CryptoKey', + usages: [ + 'wrapKey', + 'unwrapKey', + ], + } }; if (!process.features.openssl_is_boringssl) { @@ -152,14 +160,6 @@ if (!process.features.openssl_is_boringssl) { 'deriveBits', ], }; - vectors['AES-KW'] = { - algorithm: { length: 256 }, - result: 'CryptoKey', - usages: [ - 'wrapKey', - 'unwrapKey', - ], - }; vectors['ChaCha20-Poly1305'] = { result: 'CryptoKey', usages: [ @@ -606,17 +606,10 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { [ 'AES-CBC', 256, ['encrypt', 'decrypt']], [ 'AES-GCM', 128, ['encrypt', 'decrypt']], [ 'AES-GCM', 256, ['encrypt', 'decrypt']], + [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], + [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], ]; - if (!process.features.openssl_is_boringssl) { - kTests.push( - [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], - [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], - ); - } else { - common.printSkipMessage('Skipping unsupported AES-KW test cases'); - } - const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index d162190a0c53a5..d479abe3dcc989 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -59,21 +59,17 @@ await subtle.deriveKey( true, ['encrypt', 'decrypt']); -if (!process.features.openssl_is_boringssl) { - const wrappingKey = await subtle.generateKey( - { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); +const wrappingKey = await subtle.generateKey( + { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); - const keyToWrap = await subtle.generateKey( - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +const keyToWrap = await subtle.generateKey( + { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); - const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); +const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); - await subtle.unwrapKey( - 'raw', wrapped, wrappingKey, 'AES-KW', - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); -} else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); -} +await subtle.unwrapKey( + 'raw', wrapped, wrappingKey, 'AES-KW', + { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); const { privateKey } = await subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index fb0803896e222f..19361ad873cbc0 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -39,14 +39,15 @@ const kWrappingData = { }, pair: false }, -}; - -if (!process.features.openssl_is_boringssl) { - kWrappingData['AES-KW'] = { + 'AES-KW': { generate: { length: 128 }, wrap: { }, pair: false - }; + }, +}; + + +if (!process.features.openssl_is_boringssl) { kWrappingData['ChaCha20-Poly1305'] = { wrap: { iv: new Uint8Array(12), @@ -56,7 +57,7 @@ if (!process.features.openssl_is_boringssl) { pair: false }; } else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); + common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); } if (hasOpenSSL(3)) { @@ -188,20 +189,15 @@ async function generateKeysToWrap() { usages: ['sign', 'verify'], pair: false, }, - ]; - - if (!process.features.openssl_is_boringssl) { - parameters.push({ + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey'], pair: false, - }); - } else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); - } + }, + ]; if (!process.features.openssl_is_boringssl) { parameters.push({ diff --git a/test/pummel/test-webcrypto-derivebits-pbkdf2.js b/test/pummel/test-webcrypto-derivebits-pbkdf2.js index cbe64bff77505c..bfb01ac0c94fe0 100644 --- a/test/pummel/test-webcrypto-derivebits-pbkdf2.js +++ b/test/pummel/test-webcrypto-derivebits-pbkdf2.js @@ -28,17 +28,10 @@ const kDerivedKeyTypes = [ ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ]; -if (!process.features.openssl_is_boringssl) { - kDerivedKeyTypes.push( - ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], - ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], - ); -} else { - common.printSkipMessage('Skipping unsupported AES-KW test cases'); -} - const kPasswords = { short: '5040737377307264', long: '55736572732073686f756c64207069636b206c6f6' + From cfcd7ec8d9fecaaa3ab649a1bc31e0055a962b2d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 22 Apr 2026 23:48:26 +0200 Subject: [PATCH 04/11] crypto: wire ChaCha20-Poly1305 in Web Cryptography when using BoringSSL Signed-off-by: Filip Skokan --- lib/internal/crypto/util.js | 2 +- src/crypto/crypto_chacha20_poly1305.cc | 78 +++++++++++++++++++ .../webcrypto/supports-modern-algorithms.mjs | 15 ++-- .../test-crypto-key-objects-to-crypto-key.js | 7 +- ...-webcrypto-aead-decrypt-detached-buffer.js | 5 +- .../test-webcrypto-deduplicate-usages.js | 37 ++++----- ...rypto-encrypt-decrypt-chacha20-poly1305.js | 3 - test/parallel/test-webcrypto-keygen.js | 20 ++--- test/parallel/test-webcrypto-wrap-unwrap.js | 23 ++---- 9 files changed, 117 insertions(+), 73 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 718954eaebbb7b..6a32215c71bb52 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -399,7 +399,7 @@ const conditionalAlgorithms = { 'Argon2d': !!Argon2Job, 'Argon2i': !!Argon2Job, 'Argon2id': !!Argon2Job, - 'ChaCha20-Poly1305': !process.features.openssl_is_boringssl || + 'ChaCha20-Poly1305': process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), 'cSHAKE128': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'shake128'), diff --git a/src/crypto/crypto_chacha20_poly1305.cc b/src/crypto/crypto_chacha20_poly1305.cc index 0fd3e0517317ca..43d63fa8c5e409 100644 --- a/src/crypto/crypto_chacha20_poly1305.cc +++ b/src/crypto/crypto_chacha20_poly1305.cc @@ -10,6 +10,9 @@ #include "v8.h" #include +#ifdef OPENSSL_IS_BORINGSSL +#include +#endif namespace node { @@ -110,10 +113,15 @@ Maybe ChaCha20Poly1305CipherTraits::AdditionalConfig( params->mode = mode; params->cipher = ncrypto::Cipher::CHACHA20_POLY1305; +#ifndef OPENSSL_IS_BORINGSSL + // On BoringSSL, ChaCha20-Poly1305 is not exposed via the EVP_CIPHER registry + // so FromNid() returns a null Cipher. We use EVP_AEAD directly in DoCipher + // instead. if (!params->cipher) { THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); return Nothing(); } +#endif // IV parameter (required) if (!ValidateIV(env, mode, args[offset], params)) { @@ -144,6 +152,75 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher( return WebCryptoCipherStatus::INVALID_KEY_TYPE; } +#ifdef OPENSSL_IS_BORINGSSL + // BoringSSL does not expose ChaCha20-Poly1305 via the EVP_CIPHER registry; + // it is only available through the EVP_AEAD API. Matches Chromium's + // WebCrypto ChaCha20-Poly1305 implementation. + const auto key_bytes = + reinterpret_cast(key_data.GetSymmetricKey()); + const auto ad_bytes = params.additional_data.data(); + const auto ad_len = params.additional_data.size(); + const auto iv_bytes = params.iv.data(); + const auto iv_len = params.iv.size(); + + bssl::ScopedEVP_AEAD_CTX ctx; + if (!EVP_AEAD_CTX_init(ctx.get(), + EVP_aead_chacha20_poly1305(), + key_bytes, + key_data.GetSymmetricKeySize(), + kChaCha20Poly1305TagSize, + nullptr)) { + return WebCryptoCipherStatus::FAILED; + } + + if (cipher_mode == kWebCryptoCipherEncrypt) { + size_t out_len = 0; + const size_t max_out_len = in.size() + kChaCha20Poly1305TagSize; + auto buf = DataPointer::Alloc(max_out_len); + if (!EVP_AEAD_CTX_seal(ctx.get(), + static_cast(buf.get()), + &out_len, + max_out_len, + iv_bytes, + iv_len, + in.data(), + in.size(), + ad_bytes, + ad_len)) { + return WebCryptoCipherStatus::FAILED; + } + buf = buf.resize(out_len); + *out = ByteSource::Allocated(buf.release()); + return WebCryptoCipherStatus::OK; + } + + // Decrypt + if (in.size() < kChaCha20Poly1305TagSize) { + return WebCryptoCipherStatus::FAILED; + } + size_t out_len = 0; + const size_t max_out_len = in.size(); // at most |in_len| bytes written + auto buf = DataPointer::Alloc(max_out_len == 0 ? 1 : max_out_len); + if (!EVP_AEAD_CTX_open(ctx.get(), + static_cast(buf.get()), + &out_len, + max_out_len, + iv_bytes, + iv_len, + in.data(), + in.size(), + ad_bytes, + ad_len)) { + return WebCryptoCipherStatus::FAILED; + } + if (out_len == 0) { + *out = ByteSource(); + } else { + buf = buf.resize(out_len); + *out = ByteSource::Allocated(buf.release()); + } + return WebCryptoCipherStatus::OK; +#else auto ctx = CipherCtxPointer::New(); CHECK(ctx); @@ -242,6 +319,7 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher( *out = ByteSource::Allocated(buf.release()); return WebCryptoCipherStatus::OK; +#endif // OPENSSL_IS_BORINGSSL } void ChaCha20Poly1305::Initialize(Environment* env, Local target) { diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index e476bfef9d26a1..2d370b8e21d3d5 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -7,7 +7,6 @@ const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2); const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); -const chacha = crypto.getCiphers().includes('chacha20-poly1305'); const ocb = hasOpenSSL(3); const kmac = hasOpenSSL(3); @@ -78,7 +77,7 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', length: 128 }], [false, 'Argon2d'], [false, 'Argon2i'], @@ -99,7 +98,7 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', length: 128 }], [argon2, 'Argon2d'], [argon2, 'Argon2i'], @@ -120,7 +119,7 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, 'AES-OCB'], [false, 'Argon2d'], [false, 'Argon2i'], @@ -186,9 +185,9 @@ export const vectors = { [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 16777215, memory: 8, passes: 1 }, 32], ], 'encrypt': [ - [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(16) }], - [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], [false, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', iv: Buffer.alloc(15) }], @@ -210,7 +209,7 @@ export const vectors = { [pqc, 'ML-KEM-768', 'AES-GCM'], [pqc, 'ML-KEM-768', 'AES-CTR'], [pqc, 'ML-KEM-768', 'AES-CBC'], - [pqc && !boringSSL, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], [pqc, 'ML-KEM-768', 'HKDF'], [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], @@ -228,7 +227,7 @@ export const vectors = { [pqc, 'ML-KEM-768', 'AES-GCM'], [pqc, 'ML-KEM-768', 'AES-CTR'], [pqc, 'ML-KEM-768', 'AES-CBC'], - [pqc && !boringSSL, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], [pqc, 'ML-KEM-768', 'HKDF'], [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 60d7a87a7f8b4c..5c3148647324b0 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -26,15 +26,10 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { { for (const length of [128, 192, 256]) { const key = createSecretKey(randomBytes(length >> 3)); - let algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; + const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; if (length === 256) algorithms.push('ChaCha20-Poly1305'); - if (process.features.openssl_is_boringssl) { - algorithms = algorithms.filter((a) => a !== 'ChaCha20-Poly1305'); - common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); - } - for (const algorithm of algorithms) { const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt']; for (const extractable of [true, false]) { diff --git a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js index a96e709095430f..316d706e7b7948 100644 --- a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js +++ b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js @@ -29,14 +29,11 @@ async function test(algorithmName, keyLength, ivLength, format = 'raw') { const tests = [ test('AES-GCM', 32, 12), + test('ChaCha20-Poly1305', 32, 12, 'raw-secret'), ]; if (hasOpenSSL(3)) { tests.push(test('AES-OCB', 32, 12, 'raw-secret')); } -if (!process.features.openssl_is_boringssl) { - tests.push(test('ChaCha20-Poly1305', 32, 12, 'raw-secret')); -} - Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index f10d0d75973fc8..70b35f6cfa3849 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -45,6 +45,9 @@ function assertSameSet(actual, expected, msg) { { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], expected: ['wrapKey', 'unwrapKey'] }, + { algorithm: { name: 'ChaCha20-Poly1305' }, + usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'], + expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] }, ]; if (hasOpenSSL(3)) { @@ -62,16 +65,6 @@ function assertSameSet(actual, expected, msg) { common.printSkipMessage('AES-OCB and KMAC require OpenSSL >= 3'); } - if (!process.features.openssl_is_boringssl) { - symmetric.push({ - algorithm: { name: 'ChaCha20-Poly1305' }, - usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'], - expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL'); - } - for (const { algorithm, usages, expected } of symmetric) { tests.push((async () => { const key = await subtle.generateKey(algorithm, true, usages); @@ -342,20 +335,16 @@ function assertSameSet(actual, expected, msg) { })()); // ChaCha20-Poly1305 raw-secret import. - if (!process.features.openssl_is_boringssl) { - tests.push((async () => { - const key = await subtle.importKey( - 'raw-secret', - new Uint8Array(32), - { name: 'ChaCha20-Poly1305' }, - true, - ['decrypt', 'encrypt', 'decrypt', 'encrypt']); - assertSameSet(key.usages, ['encrypt', 'decrypt']); - assert.strictEqual(key.usages.length, 2); - })()); - } else { - common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL'); - } + tests.push((async () => { + const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(32), + { name: 'ChaCha20-Poly1305' }, + true, + ['decrypt', 'encrypt', 'decrypt', 'encrypt']); + assertSameSet(key.usages, ['encrypt', 'decrypt']); + assert.strictEqual(key.usages.length, 2); + })()); // AES-OCB raw-secret import. if (hasOpenSSL(3)) { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js index 0f930a356712ed..723fd26ea5708b 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -5,9 +5,6 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) - common.skip('Skipping unsupported ChaCha20-Poly1305 test case'); - const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 186016feaa3929..d73ffd21e563a5 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -142,7 +142,16 @@ const vectors = { 'wrapKey', 'unwrapKey', ], - } + }, + 'ChaCha20-Poly1305': { + result: 'CryptoKey', + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ], + }, }; if (!process.features.openssl_is_boringssl) { @@ -160,15 +169,6 @@ if (!process.features.openssl_is_boringssl) { 'deriveBits', ], }; - vectors['ChaCha20-Poly1305'] = { - result: 'CryptoKey', - usages: [ - 'encrypt', - 'decrypt', - 'wrapKey', - 'unwrapKey', - ], - }; } else { common.printSkipMessage('Skipping unsupported test cases'); } diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 19361ad873cbc0..49f63e215fadfc 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -44,21 +44,15 @@ const kWrappingData = { wrap: { }, pair: false }, -}; - - -if (!process.features.openssl_is_boringssl) { - kWrappingData['ChaCha20-Poly1305'] = { + 'ChaCha20-Poly1305': { wrap: { iv: new Uint8Array(12), additionalData: new Uint8Array(16), tagLength: 128 }, pair: false - }; -} else { - common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); -} + } +}; if (hasOpenSSL(3)) { kWrappingData['AES-OCB'] = { @@ -197,19 +191,14 @@ async function generateKeysToWrap() { usages: ['wrapKey', 'unwrapKey'], pair: false, }, - ]; - - if (!process.features.openssl_is_boringssl) { - parameters.push({ + { algorithm: { name: 'ChaCha20-Poly1305' }, usages: ['encrypt', 'decrypt'], pair: false, - }); - } else { - common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); - } + }, + ]; if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { From b15dda69db792ff93334fb8ed0c66f2799eb2810 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 23 Apr 2026 00:37:02 +0200 Subject: [PATCH 05/11] test: adjust Web Cryptography WPT expectations for BoringSSL Signed-off-by: Filip Skokan --- test/wpt/status/WebCryptoAPI.cjs | 57 +++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 722a0b38398e1d..e3bb8e415b398d 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -6,16 +6,27 @@ const { hasOpenSSL } = require('../../common/crypto.js'); const s390x = os.arch() === 's390x'; -const conditionalSkips = {}; +const conditionalFileSkips = {}; +const conditionalSubtestSkips = {}; function skip(...files) { for (const file of files) { - conditionalSkips[file] = { - 'skip': `Unsupported in OpenSSL ${process.versions.openssl}`, + conditionalFileSkips[file] = { + 'skip': 'Unsupported in ' + (process.features.openssl_is_boringssl ? 'BoringSSL' : `OpenSSL ${process.versions.openssl}`), }; } } +function skipSubtests(...entries) { + for (const [file, regexp] of entries) { + conditionalSubtestSkips[file] ||= { + 'skipTests': [], + }; + + conditionalSubtestSkips[file].skipTests.push(regexp); + } +} + if (!hasOpenSSL(3, 0)) { skip( 'encrypt_decrypt/aes_ocb.tentative.https.any.js', @@ -34,7 +45,7 @@ if (!hasOpenSSL(3, 2)) { 'import_export/Argon2_importKey.tentative.https.any.js'); } -if (!hasOpenSSL(3, 5)) { +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { skip( 'encap_decap/encap_decap_bits.tentative.https.any.js', 'encap_decap/encap_decap_keys.tentative.https.any.js', @@ -47,8 +58,44 @@ if (!hasOpenSSL(3, 5)) { 'sign_verify/mldsa.tentative.https.any.js'); } +if (process.features.openssl_is_boringssl) { + skip( + 'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js', + 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js', + 'digest/cshake.tentative.https.any.js', + 'digest/sha3.tentative.https.any.js', + 'generateKey/failures_Ed448.tentative.https.any.js', + 'generateKey/failures_X448.tentative.https.any.js', + 'generateKey/successes_Ed448.tentative.https.any.js', + 'generateKey/successes_X448.tentative.https.any.js', + 'import_export/okp_importKey_Ed448.tentative.https.any.js', + 'import_export/okp_importKey_failures_Ed448.tentative.https.any.js', + 'import_export/okp_importKey_failures_X448.tentative.https.any.js', + 'import_export/okp_importKey_X448.tentative.https.any.js', + 'sign_verify/eddsa_curve448.tentative.https.any.js'); + + skipSubtests( + ['encap_decap/encap_decap_bits.tentative.https.any.js', /ml-kem-512/i], + ['encap_decap/encap_decap_keys.tentative.https.any.js', /ml-kem-512/i], + ['generateKey/failures_ML-KEM.tentative.https.any.js', /ml-kem-512/i], + ['generateKey/successes_ML-KEM.tentative.https.any.js', /ml-kem-512/i], + ['import_export/ML-KEM_importKey.tentative.https.any.js', /ml-kem-512/i]); +} + +function assertNoOverlap(fileSkips, subtestSkips) { + const subtestSkipFiles = new Set(Object.keys(subtestSkips)); + const overlap = Object.keys(fileSkips).filter((file) => subtestSkipFiles.has(file)); + + if (overlap.length !== 0) { + throw new Error(`conditionalFileSkips and conditionalSubtestSkips overlap: ${overlap.join(', ')}`); + } +} + +assertNoOverlap(conditionalFileSkips, conditionalSubtestSkips); + module.exports = { - ...conditionalSkips, + ...conditionalFileSkips, + ...conditionalSubtestSkips, 'algorithm-discards-context.https.window.js': { 'skip': 'Not relevant in Node.js context', }, From f00976feaa4912ead443b6cc7aa8bd6dc9505940 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 5 May 2026 10:35:51 +0200 Subject: [PATCH 06/11] src: simplify OpenSSL feature gates Introduce explicit OPENSSL_WITH_* feature macros for crypto capabilities that vary by OpenSSL version or BoringSSL support. Use those macros at call sites instead of repeating version and backend checks, and centralize PQC key metadata so key handling can query helper functions instead of duplicating algorithm switch lists. Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 30 +++---- deps/ncrypto/ncrypto.h | 93 ++++++++++++++++++---- src/crypto/crypto_aes.h | 2 +- src/crypto/crypto_argon2.cc | 4 +- src/crypto/crypto_argon2.h | 2 +- src/crypto/crypto_cipher.cc | 2 +- src/crypto/crypto_kem.cc | 2 +- src/crypto/crypto_kem.h | 2 +- src/crypto/crypto_keys.cc | 137 ++++++-------------------------- src/crypto/crypto_kmac.cc | 4 +- src/crypto/crypto_kmac.h | 5 +- src/crypto/crypto_pqc.cc | 153 +++++++++++++++++++++++++++--------- src/crypto/crypto_pqc.h | 2 + src/crypto/crypto_sig.cc | 6 +- src/crypto/crypto_util.cc | 2 +- src/node_crypto.cc | 12 +-- src/node_crypto.h | 4 +- 17 files changed, 258 insertions(+), 204 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 17701b71411d5e..0f94b9bd8b4f2f 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -19,7 +19,7 @@ #include #include #include -#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 #include #endif #endif @@ -36,8 +36,10 @@ constexpr static PQCMapping pqc_mappings[] = { {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, -#ifndef OPENSSL_IS_BORINGSSL +#if OPENSSL_WITH_PQC_ML_KEM_512 {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, +#endif +#if OPENSSL_WITH_PQC_SLH_DSA {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, @@ -1935,8 +1937,7 @@ DataPointer pbkdf2(const Digest& md, return {}; } -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 DataPointer argon2(const Buffer& pass, const Buffer& salt, uint32_t lanes, @@ -2029,7 +2030,6 @@ DataPointer argon2(const Buffer& pass, return {}; } #endif -#endif // ============================================================================ @@ -2189,7 +2189,7 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; int type = EVP_PKEY_id(key); -#if OPENSSL_WITH_PQC && !defined(OPENSSL_IS_BORINGSSL) +#if OPENSSL_WITH_OPENSSL_PQC // EVP_PKEY_id returns -1 when EVP_PKEY_* is only implemented in a provider // which is the case for all post-quantum NIST algorithms // one suggested way would be to use a chain of `EVP_PKEY_is_a` @@ -2789,7 +2789,7 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: -#ifndef OPENSSL_IS_BORINGSSL +#if OPENSSL_WITH_PQC_SLH_DSA case EVP_PKEY_SLH_DSA_SHA2_128F: case EVP_PKEY_SLH_DSA_SHA2_128S: case EVP_PKEY_SLH_DSA_SHA2_192F: @@ -4677,7 +4677,7 @@ HMACCtxPointer HMACCtxPointer::New() { return HMACCtxPointer(HMAC_CTX_new()); } -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {} EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept @@ -4765,7 +4765,7 @@ EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) { if (!mac) return EVPMacCtxPointer(); return EVPMacCtxPointer(EVP_MAC_CTX_new(mac)); } -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KMAC DataPointer hashDigest(const Buffer& buf, const EVP_MD* md) { @@ -4912,8 +4912,8 @@ const Digest Digest::FromName(const char* name) { // ============================================================================ // KEM Implementation -#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) -#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L +#if OPENSSL_WITH_KEM +#if OPENSSL_WITH_KEM_OPERATION_PARAM bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { const char* operation = nullptr; @@ -4921,7 +4921,7 @@ bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { case EVP_PKEY_RSA: operation = OSSL_KEM_PARAM_OPERATION_RSASVE; break; -#if OPENSSL_VERSION_PREREQ(3, 2) +#if OPENSSL_WITH_OPENSSL_DHKEM case EVP_PKEY_EC: case EVP_PKEY_X25519: case EVP_PKEY_X448: @@ -4958,7 +4958,7 @@ std::optional KEM::Encapsulate( return std::nullopt; } -#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L +#if OPENSSL_WITH_KEM_OPERATION_PARAM if (!SetOperationParameter(ctx.get(), public_key)) { return std::nullopt; } @@ -4999,7 +4999,7 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return {}; } -#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L +#if OPENSSL_WITH_KEM_OPERATION_PARAM if (!SetOperationParameter(ctx.get(), private_key)) { return {}; } @@ -5029,6 +5029,6 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return shared_key; } -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KEM } // namespace ncrypto diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index c430ae9beece75..5a41dd34f242f3 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -22,26 +22,91 @@ #ifndef OPENSSL_NO_ENGINE #include #endif // !OPENSSL_NO_ENGINE + +#ifndef OPENSSL_VERSION_PREREQ +#define OPENSSL_VERSION_PREREQ(maj, min) \ + (OPENSSL_VERSION_NUMBER >= (((maj) << 28) | ((min) << 20))) +#endif + // The FIPS-related functions are only available // when the OpenSSL itself was compiled with FIPS support. -#if defined(OPENSSL_FIPS) && OPENSSL_VERSION_MAJOR < 3 +#if defined(OPENSSL_FIPS) && !OPENSSL_VERSION_PREREQ(3, 0) #include #endif // OPENSSL_FIPS -// Define OPENSSL_WITH_PQC for post-quantum cryptography support -#if OPENSSL_VERSION_NUMBER >= 0x30500000L -#define OPENSSL_WITH_PQC 1 +#if OPENSSL_VERSION_PREREQ(3, 0) +#define OPENSSL_WITH_AES_OCB 1 +#else +#define OPENSSL_WITH_AES_OCB 0 +#endif + +#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_ARGON2 1 +#else +#define OPENSSL_WITH_ARGON2 0 +#endif + +#if OPENSSL_VERSION_PREREQ(3, 0) || defined(OPENSSL_IS_BORINGSSL) +#define OPENSSL_WITH_KEM 1 +#else +#define OPENSSL_WITH_KEM 0 +#endif + +#if OPENSSL_VERSION_PREREQ(3, 0) +#define OPENSSL_WITH_KMAC 1 +#else +#define OPENSSL_WITH_KMAC 0 +#endif + +#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 1 +#else +#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 0 +#endif + +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_OPENSSL_DHKEM 1 +#else +#define OPENSSL_WITH_OPENSSL_DHKEM 0 +#endif + +#if OPENSSL_WITH_KEM && !defined(OPENSSL_IS_BORINGSSL) && \ + !OPENSSL_VERSION_PREREQ(3, 5) +#define OPENSSL_WITH_KEM_OPERATION_PARAM 1 +#else +#define OPENSSL_WITH_KEM_OPERATION_PARAM 0 +#endif + +// Post-quantum cryptography support. Keep these explicit so code can +// distinguish provider API shape from the available algorithm set. +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 5) +#define OPENSSL_WITH_OPENSSL_PQC 1 +#else +#define OPENSSL_WITH_OPENSSL_PQC 0 +#endif + +#ifdef OPENSSL_IS_BORINGSSL +#define OPENSSL_WITH_BORINGSSL_PQC 1 +#else +#define OPENSSL_WITH_BORINGSSL_PQC 0 +#endif + +#define OPENSSL_WITH_PQC \ + (OPENSSL_WITH_OPENSSL_PQC || OPENSSL_WITH_BORINGSSL_PQC) +#define OPENSSL_WITH_PQC_ML_KEM_512 OPENSSL_WITH_OPENSSL_PQC +#define OPENSSL_WITH_PQC_SLH_DSA OPENSSL_WITH_OPENSSL_PQC + +#if OPENSSL_WITH_OPENSSL_PQC #define EVP_PKEY_ML_KEM_512 NID_ML_KEM_512 #define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 #define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #include -#elif defined(OPENSSL_IS_BORINGSSL) -#define OPENSSL_WITH_PQC 1 +#elif OPENSSL_WITH_BORINGSSL_PQC #define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 #define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #endif -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_PREREQ(3, 0) #define OSSL3_CONST const #else #define OSSL3_CONST @@ -1478,7 +1543,7 @@ class HMACCtxPointer final { DeleteFnPtr ctx_; }; -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC class EVPMacPointer final { public: EVPMacPointer() = default; @@ -1526,7 +1591,7 @@ class EVPMacCtxPointer final { private: DeleteFnPtr ctx_; }; -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KMAC #ifndef OPENSSL_NO_ENGINE class EnginePointer final { @@ -1639,8 +1704,7 @@ DataPointer pbkdf2(const Digest& md, uint32_t iterations, size_t length); -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 enum class Argon2Type { ARGON2D, ARGON2I, ARGON2ID }; DataPointer argon2(const Buffer& pass, @@ -1654,11 +1718,10 @@ DataPointer argon2(const Buffer& pass, const Buffer& ad, Argon2Type type); #endif -#endif // ============================================================================ // KEM (Key Encapsulation Mechanism) -#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) +#if OPENSSL_WITH_KEM class KEM final { public: @@ -1682,13 +1745,13 @@ class KEM final { const Buffer& ciphertext); private: -#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_NUMBER < 0x30500000L +#if OPENSSL_WITH_KEM_OPERATION_PARAM static bool SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key); #endif }; -#endif // OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) +#endif // OPENSSL_WITH_KEM // ============================================================================ // Version metadata diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h index 3cfd366d0f7a37..401e7b2c338a1b 100644 --- a/src/crypto/crypto_aes.h +++ b/src/crypto/crypto_aes.h @@ -38,7 +38,7 @@ constexpr unsigned kNoAuthTagLength = static_cast(-1); V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW) #endif -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_AES_OCB #define VARIANTS_OCB(V) \ V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB) \ V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB) \ diff --git a/src/crypto/crypto_argon2.cc b/src/crypto/crypto_argon2.cc index 7bb995ca51c0df..d5207f4be57bb2 100644 --- a/src/crypto/crypto_argon2.cc +++ b/src/crypto/crypto_argon2.cc @@ -2,8 +2,7 @@ #include "async_wrap-inl.h" #include "threadpoolwork-inl.h" -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 #include namespace node::crypto { @@ -159,4 +158,3 @@ void Argon2::RegisterExternalReferences(ExternalReferenceRegistry* registry) { } // namespace node::crypto #endif -#endif diff --git a/src/crypto/crypto_argon2.h b/src/crypto/crypto_argon2.h index 73e8460d204dd3..354d0a4be6f392 100644 --- a/src/crypto/crypto_argon2.h +++ b/src/crypto/crypto_argon2.h @@ -6,7 +6,7 @@ #include "crypto/crypto_util.h" namespace node::crypto { -#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 // Argon2 is a password-based key derivation algorithm // defined in https://datatracker.ietf.org/doc/html/rfc9106 diff --git a/src/crypto/crypto_cipher.cc b/src/crypto/crypto_cipher.cc index 2e9acf86099ee8..dec72c20412e4e 100644 --- a/src/crypto/crypto_cipher.cc +++ b/src/crypto/crypto_cipher.cc @@ -711,7 +711,7 @@ bool CipherBase::Final(std::unique_ptr* out) { static_cast(ctx_.getBlockSize()), BackingStoreInitializationMode::kUninitialized); -#if (OPENSSL_VERSION_NUMBER < 0x30000000L) +#if !OPENSSL_VERSION_PREREQ(3, 0) // OpenSSL v1.x doesn't verify the presence of the auth tag so do // it ourselves, see https://github.com/nodejs/node/issues/45874. if (kind_ == kDecipher && ctx_.isChaCha20Poly1305() && diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index cb062c0e2cdd8b..d30c6aaef6253f 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -1,6 +1,6 @@ #include "crypto/crypto_kem.h" -#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) +#if OPENSSL_WITH_KEM #include "async_wrap-inl.h" #include "base_object-inl.h" diff --git a/src/crypto/crypto_kem.h b/src/crypto/crypto_kem.h index fff3ff03dc2f64..e00aa04baa897e 100644 --- a/src/crypto/crypto_kem.h +++ b/src/crypto/crypto_kem.h @@ -10,7 +10,7 @@ #include "memory_tracker.h" #include "node_external_reference.h" -#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) +#if OPENSSL_WITH_KEM namespace node { namespace crypto { diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index ce09131bafc588..abd97ff7c47f3c 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -448,10 +448,7 @@ bool KeyObjectData::ToEncodedPrivateKey( bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - // SLH-DSA uses raw private key; ML-DSA/ML-KEM use the seed format and are - // handled in the RAW_SEED branch below. - is_raw_supported = - is_raw_supported || (IsPqcKeyId(id) && !IsPqcSeedKeyId(id)); + is_raw_supported = is_raw_supported || IsPqcRawPrivateKeyId(id); #endif if (!is_raw_supported) { THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); @@ -465,9 +462,9 @@ bool KeyObjectData::ToEncodedPrivateKey( return Buffer::Copy(env, raw_data.get(), raw_data.size()) .ToLocal(out); } else if (config.format == EVPKeyPointer::PKFormatType::RAW_SEED) { +#if OPENSSL_WITH_PQC Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); -#if OPENSSL_WITH_PQC if (!IsPqcSeedKeyId(pkey.id())) { THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); return false; @@ -1267,48 +1264,12 @@ Local KeyObjectHandle::GetAsymmetricKeyType() const { case EVP_PKEY_X448: return env()->crypto_x448_string(); #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - return env()->crypto_ml_dsa_44_string(); - case EVP_PKEY_ML_DSA_65: - return env()->crypto_ml_dsa_65_string(); - case EVP_PKEY_ML_DSA_87: - return env()->crypto_ml_dsa_87_string(); - case EVP_PKEY_ML_KEM_768: - return env()->crypto_ml_kem_768_string(); - case EVP_PKEY_ML_KEM_1024: - return env()->crypto_ml_kem_1024_string(); - -#ifndef OPENSSL_IS_BORINGSSL - case EVP_PKEY_ML_KEM_512: - return env()->crypto_ml_kem_512_string(); - case EVP_PKEY_SLH_DSA_SHA2_128F: - return env()->crypto_slh_dsa_sha2_128f_string(); - case EVP_PKEY_SLH_DSA_SHA2_128S: - return env()->crypto_slh_dsa_sha2_128s_string(); - case EVP_PKEY_SLH_DSA_SHA2_192F: - return env()->crypto_slh_dsa_sha2_192f_string(); - case EVP_PKEY_SLH_DSA_SHA2_192S: - return env()->crypto_slh_dsa_sha2_192s_string(); - case EVP_PKEY_SLH_DSA_SHA2_256F: - return env()->crypto_slh_dsa_sha2_256f_string(); - case EVP_PKEY_SLH_DSA_SHA2_256S: - return env()->crypto_slh_dsa_sha2_256s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_128F: - return env()->crypto_slh_dsa_shake_128f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_128S: - return env()->crypto_slh_dsa_shake_128s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_192F: - return env()->crypto_slh_dsa_shake_192f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_192S: - return env()->crypto_slh_dsa_shake_192s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_256F: - return env()->crypto_slh_dsa_shake_256f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_256S: - return env()->crypto_slh_dsa_shake_256s_string(); -#endif -#endif + default: + return GetPqcAsymmetricKeyType(env(), data_.GetAsymmetricKey().id()); +#else default: return Undefined(env()->isolate()); +#endif } } @@ -1417,36 +1378,14 @@ void KeyObjectHandle::RawPublicKey( Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: -#ifndef OPENSSL_IS_BORINGSSL - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: -#endif + is_raw_supported = is_raw_supported || IsPqcKeyId(id); #endif - break; - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!is_raw_supported) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } auto raw_data = pkey.rawPublicKey(); @@ -1472,30 +1411,14 @@ void KeyObjectHandle::RawPrivateKey( Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC -#ifndef OPENSSL_IS_BORINGSSL - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: -#endif + is_raw_supported = is_raw_supported || IsPqcRawPrivateKeyId(id); #endif - break; - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!is_raw_supported) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } auto raw_data = pkey.rawPrivateKey(); @@ -1584,26 +1507,14 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { const KeyObjectData& data = key->Data(); CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); +#if OPENSSL_WITH_PQC Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { -#if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: -#ifndef OPENSSL_IS_BORINGSSL - case EVP_PKEY_ML_KEM_512: -#endif - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - break; -#endif - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!IsPqcSeedKeyId(pkey.id())) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } -#if OPENSSL_WITH_PQC auto raw_data = pkey.rawSeed(); if (!raw_data) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed"); @@ -1612,6 +1523,8 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { args.GetReturnValue().Set( Buffer::Copy(env, raw_data.get(), raw_data.size()) .FromMaybe(Local())); +#else + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); #endif } @@ -2040,12 +1953,12 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_44); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_65); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_87); -#ifndef OPENSSL_IS_BORINGSSL +#if OPENSSL_WITH_PQC_ML_KEM_512 NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_512); #endif NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_768); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_1024); -#ifndef OPENSSL_IS_BORINGSSL +#if OPENSSL_WITH_PQC_SLH_DSA NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128F); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128S); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_192F); diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index fd431ffc1b47b7..ed4a8e9d526983 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -3,7 +3,7 @@ #include "node_internals.h" #include "threadpoolwork-inl.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC #include #include #include "crypto/crypto_keys.h" @@ -220,4 +220,4 @@ void Kmac::RegisterExternalReferences(ExternalReferenceRegistry* registry) { } // namespace node::crypto -#endif +#endif // OPENSSL_WITH_KMAC diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 9ee6192ee3dd17..5a8c9e5039f22b 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -10,8 +10,7 @@ namespace node::crypto { -// KMAC (Keccak Message Authentication Code) is available since OpenSSL 3.0. -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC enum class KmacVariant { KMAC128, KMAC256 }; @@ -72,7 +71,7 @@ namespace Kmac { void Initialize(Environment* env, v8::Local target) {} void RegisterExternalReferences(ExternalReferenceRegistry* registry) {} } // namespace Kmac -#endif +#endif // OPENSSL_WITH_KMAC } // namespace node::crypto diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc index c6c1dff6d63b62..8d4af1e7801180 100644 --- a/src/crypto/crypto_pqc.cc +++ b/src/crypto/crypto_pqc.cc @@ -17,41 +17,104 @@ namespace crypto { #if OPENSSL_WITH_PQC namespace { +using PqcKeyTypeGetter = Local (Environment::*)() const; + +enum PqcAlgorithmFlag { + kPqcRawPrivate = 1 << 0, + kPqcRawSeed = 1 << 1, + kPqcSignature = 1 << 2, +}; + struct PqcAlgorithm { int id; const char* name; - // true: rawSeed/NewRawSeed, false: rawPrivateKey/NewRawPrivate - bool use_seed; - // true: signature algorithm (ML-DSA, SLH-DSA), - // false: key encapsulation mechanism (ML-KEM). - bool is_signature; + PqcKeyTypeGetter key_type; + int flags; }; -// use_seed is true for algorithms whose private key material is carried as a -// seed (rawSeed/NewRawSeed), false for those that use the expanded private -// key (rawPrivateKey / NewRawPrivate). ML-KEM-512 and SLH-DSA are only -// exposed by OpenSSL and are not available in BoringSSL. +// ML-DSA and ML-KEM carry private material as a seed. SLH-DSA uses the +// expanded private key and is only exposed by OpenSSL. +constexpr int kPqcMlDsaFlags = kPqcRawSeed | kPqcSignature; +constexpr int kPqcMlKemFlags = kPqcRawSeed; +constexpr int kPqcSlhDsaFlags = kPqcRawPrivate | kPqcSignature; + constexpr PqcAlgorithm kPqcAlgorithms[] = { - {EVP_PKEY_ML_DSA_44, "ML-DSA-44", true, true}, - {EVP_PKEY_ML_DSA_65, "ML-DSA-65", true, true}, - {EVP_PKEY_ML_DSA_87, "ML-DSA-87", true, true}, - {EVP_PKEY_ML_KEM_768, "ML-KEM-768", true, false}, - {EVP_PKEY_ML_KEM_1024, "ML-KEM-1024", true, false}, - -#ifndef OPENSSL_IS_BORINGSSL - {EVP_PKEY_ML_KEM_512, "ML-KEM-512", true, false}, - {EVP_PKEY_SLH_DSA_SHA2_128F, "SLH-DSA-SHA2-128f", false, true}, - {EVP_PKEY_SLH_DSA_SHA2_128S, "SLH-DSA-SHA2-128s", false, true}, - {EVP_PKEY_SLH_DSA_SHA2_192F, "SLH-DSA-SHA2-192f", false, true}, - {EVP_PKEY_SLH_DSA_SHA2_192S, "SLH-DSA-SHA2-192s", false, true}, - {EVP_PKEY_SLH_DSA_SHA2_256F, "SLH-DSA-SHA2-256f", false, true}, - {EVP_PKEY_SLH_DSA_SHA2_256S, "SLH-DSA-SHA2-256s", false, true}, - {EVP_PKEY_SLH_DSA_SHAKE_128F, "SLH-DSA-SHAKE-128f", false, true}, - {EVP_PKEY_SLH_DSA_SHAKE_128S, "SLH-DSA-SHAKE-128s", false, true}, - {EVP_PKEY_SLH_DSA_SHAKE_192F, "SLH-DSA-SHAKE-192f", false, true}, - {EVP_PKEY_SLH_DSA_SHAKE_192S, "SLH-DSA-SHAKE-192s", false, true}, - {EVP_PKEY_SLH_DSA_SHAKE_256F, "SLH-DSA-SHAKE-256f", false, true}, - {EVP_PKEY_SLH_DSA_SHAKE_256S, "SLH-DSA-SHAKE-256s", false, true}, + {EVP_PKEY_ML_DSA_44, + "ML-DSA-44", + &Environment::crypto_ml_dsa_44_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_DSA_65, + "ML-DSA-65", + &Environment::crypto_ml_dsa_65_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_DSA_87, + "ML-DSA-87", + &Environment::crypto_ml_dsa_87_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_KEM_768, + "ML-KEM-768", + &Environment::crypto_ml_kem_768_string, + kPqcMlKemFlags}, + {EVP_PKEY_ML_KEM_1024, + "ML-KEM-1024", + &Environment::crypto_ml_kem_1024_string, + kPqcMlKemFlags}, + +#if OPENSSL_WITH_PQC_ML_KEM_512 + {EVP_PKEY_ML_KEM_512, + "ML-KEM-512", + &Environment::crypto_ml_kem_512_string, + kPqcMlKemFlags}, +#endif +#if OPENSSL_WITH_PQC_SLH_DSA + {EVP_PKEY_SLH_DSA_SHA2_128F, + "SLH-DSA-SHA2-128f", + &Environment::crypto_slh_dsa_sha2_128f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_128S, + "SLH-DSA-SHA2-128s", + &Environment::crypto_slh_dsa_sha2_128s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_192F, + "SLH-DSA-SHA2-192f", + &Environment::crypto_slh_dsa_sha2_192f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_192S, + "SLH-DSA-SHA2-192s", + &Environment::crypto_slh_dsa_sha2_192s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_256F, + "SLH-DSA-SHA2-256f", + &Environment::crypto_slh_dsa_sha2_256f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_256S, + "SLH-DSA-SHA2-256s", + &Environment::crypto_slh_dsa_sha2_256s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_128F, + "SLH-DSA-SHAKE-128f", + &Environment::crypto_slh_dsa_shake_128f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_128S, + "SLH-DSA-SHAKE-128s", + &Environment::crypto_slh_dsa_shake_128s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_192F, + "SLH-DSA-SHAKE-192f", + &Environment::crypto_slh_dsa_shake_192f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_192S, + "SLH-DSA-SHAKE-192s", + &Environment::crypto_slh_dsa_shake_192s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_256F, + "SLH-DSA-SHAKE-256f", + &Environment::crypto_slh_dsa_shake_256f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_256S, + "SLH-DSA-SHAKE-256s", + &Environment::crypto_slh_dsa_shake_256s_string, + kPqcSlhDsaFlags}, #endif }; @@ -69,6 +132,10 @@ const PqcAlgorithm* FindPqcAlgorithmByName(const char* name) { return nullptr; } +bool HasPqcAlgorithmFlag(const PqcAlgorithm* alg, PqcAlgorithmFlag flag) { + return alg != nullptr && (alg->flags & flag) != 0; +} + bool TrySetEncodedKey(Environment* env, DataPointer data, Local target, @@ -92,9 +159,9 @@ bool ExportJwkPqcKey(Environment* env, CHECK(alg); if (key.GetKeyType() == kKeyTypePrivate) { - DataPointer priv_data = - alg->use_seed ? pkey.rawSeed() : pkey.rawPrivateKey(); - if (alg->use_seed && !priv_data) { + const bool uses_seed = HasPqcAlgorithmFlag(alg, kPqcRawSeed); + DataPointer priv_data = uses_seed ? pkey.rawSeed() : pkey.rawPrivateKey(); + if (uses_seed && !priv_data) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "key does not have an available seed"); return false; @@ -154,8 +221,9 @@ KeyObjectData ImportJWKPqcKey(Environment* env, Local jwk) { .data = priv.data(), .len = priv.size(), }; - pkey = alg->use_seed ? EVPKeyPointer::NewRawSeed(alg->id, buf) - : EVPKeyPointer::NewRawPrivate(alg->id, buf); + pkey = HasPqcAlgorithmFlag(alg, kPqcRawSeed) + ? EVPKeyPointer::NewRawSeed(alg->id, buf) + : EVPKeyPointer::NewRawPrivate(alg->id, buf); } else { ByteSource pub = ByteSource::FromEncodedString(env, pub_value.As()); pkey = @@ -190,14 +258,19 @@ bool IsPqcKeyId(int id) { return FindPqcAlgorithmById(id) != nullptr; } +bool IsPqcRawPrivateKeyId(int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + return HasPqcAlgorithmFlag(alg, kPqcRawPrivate); +} + bool IsPqcSeedKeyId(int id) { const PqcAlgorithm* alg = FindPqcAlgorithmById(id); - return alg != nullptr && alg->use_seed; + return HasPqcAlgorithmFlag(alg, kPqcRawSeed); } bool IsPqcSignatureKeyId(int id) { const PqcAlgorithm* alg = FindPqcAlgorithmById(id); - return alg != nullptr && alg->is_signature; + return HasPqcAlgorithmFlag(alg, kPqcSignature); } int GetPqcNidFromName(const char* name) { @@ -206,6 +279,14 @@ int GetPqcNidFromName(const char* name) { } return NID_undef; } + +Local GetPqcAsymmetricKeyType(Environment* env, int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + if (alg == nullptr) return v8::Undefined(env->isolate()); + + Local key_type = (env->*(alg->key_type))(); + return key_type.As(); +} #endif } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_pqc.h b/src/crypto/crypto_pqc.h index bb4c41d0b06f0e..14f919d94c6f8a 100644 --- a/src/crypto/crypto_pqc.h +++ b/src/crypto/crypto_pqc.h @@ -29,6 +29,8 @@ bool IsPqcSeedKeyId(int id); bool IsPqcSignatureKeyId(int id); // Returns the EVP_PKEY id for the given PQC algorithm name, or NID_undef. int GetPqcNidFromName(const char* name); +// Returns the JS asymmetricKeyType string for a PQC id, or undefined. +v8::Local GetPqcAsymmetricKeyType(Environment* env, int id); #endif } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index 658c86d45d6936..d8a4fe395a5f47 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -238,9 +238,8 @@ bool UseP1363Encoding(const EVPKeyPointer& key, const DSASigEnc dsa_encoding) { } bool SupportsContextString(const EVPKeyPointer& key) { -#if OPENSSL_VERSION_NUMBER < 0x3020000fL && !defined(OPENSSL_IS_BORINGSSL) - return false; -#else + if (!OPENSSL_WITH_SIGNATURE_CONTEXT_STRING) return false; + const int id = key.id(); #if OPENSSL_WITH_PQC if (IsPqcSignatureKeyId(id)) return true; @@ -249,7 +248,6 @@ bool SupportsContextString(const EVPKeyPointer& key) { if (id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448) return true; #endif return false; -#endif } } // namespace diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 30b60cf1ac77ce..523be7f6cd5d23 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -139,7 +139,7 @@ void InitCryptoOnce() { OPENSSL_init_ssl(0, settings); -#if OPENSSL_WITH_PQC && !defined(OPENSSL_IS_BORINGSSL) +#if OPENSSL_WITH_OPENSSL_PQC // Configure all loaded providers to prefer seed-only format for ML-KEM and // ML-DSA private keys in PKCS#8 export, falling back to priv-only when a // seed is not available. The provider encoder reads these parameters at diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 25a3aa6d6fea51..91d80e0dd379ba 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -61,23 +61,23 @@ namespace crypto { V(Verify) \ V(X509Certificate) -#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 #define ARGON2_NAMESPACE_LIST(V) V(Argon2) #else #define ARGON2_NAMESPACE_LIST(V) -#endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2 +#endif // OPENSSL_WITH_ARGON2 -// KEM and KMAC functionality requires OpenSSL 3.0.0 or later -#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) +#if OPENSSL_WITH_KEM #define KEM_NAMESPACE_LIST(V) V(KEM) #else #define KEM_NAMESPACE_LIST(V) #endif -#if OPENSSL_VERSION_MAJOR >= 3 + +#if OPENSSL_WITH_KMAC #define KMAC_NAMESPACE_LIST(V) V(Kmac) #else #define KMAC_NAMESPACE_LIST(V) -#endif +#endif // OPENSSL_WITH_KMAC #define TURBOSHAKE_NAMESPACE_LIST(V) V(TurboShake) diff --git a/src/node_crypto.h b/src/node_crypto.h index c2f53f3a395801..ecc2b8c6a358c8 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -40,10 +40,10 @@ #include "crypto/crypto_hash.h" #include "crypto/crypto_hkdf.h" #include "crypto/crypto_hmac.h" -#if OPENSSL_VERSION_MAJOR >= 3 || defined(OPENSSL_IS_BORINGSSL) +#if OPENSSL_WITH_KEM #include "crypto/crypto_kem.h" #endif -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC #include "crypto/crypto_kmac.h" #endif #include "crypto/crypto_keygen.h" From f2c45cc45ae721cb098c3cf09c55b9adda1d3f44 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 5 May 2026 11:33:38 +0200 Subject: [PATCH 07/11] src: add BoringSSL EVP enumeration fallback BoringSSL declares EVP_CIPHER_do_all_sorted and EVP_MD_do_all_sorted, but stock no-decrepit builds do not provide those symbols. Add a Node build flag that keeps ncrypto and its dependents on a local BoringSSL fallback list when libdecrepit is absent. Keep embedders that provide the EVP enumeration symbols on the normal OpenSSL-compatible path, matching Electron's patched BoringSSL build. Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 30 ++++++++++++++++++ deps/ncrypto/ncrypto.gyp | 7 +++++ deps/ncrypto/ncrypto.h | 12 +++++++ src/crypto/crypto_hash.cc | 29 ++++++++++++++++- .../test-crypto-boringssl-evp-list.js | 31 +++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-crypto-boringssl-evp-list.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 0f94b9bd8b4f2f..d51cff7e2c4e24 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -9,6 +9,7 @@ #include #ifdef OPENSSL_IS_BORINGSSL #include +#include #include #endif #include @@ -76,6 +77,28 @@ using NetscapeSPKIPointer = DeleteFnPtr; static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; + +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +struct BoringSSLCipher { + const EVP_CIPHER* (*get)(); + const char* name; +}; + +constexpr BoringSSLCipher kBoringSSLCiphers[] = { + {EVP_aes_128_cbc, "aes-128-cbc"}, {EVP_aes_128_ctr, "aes-128-ctr"}, + {EVP_aes_128_ecb, "aes-128-ecb"}, {EVP_aes_128_gcm, "aes-128-gcm"}, + {EVP_aes_128_ofb, "aes-128-ofb"}, {EVP_aes_192_cbc, "aes-192-cbc"}, + {EVP_aes_192_ctr, "aes-192-ctr"}, {EVP_aes_192_ecb, "aes-192-ecb"}, + {EVP_aes_192_gcm, "aes-192-gcm"}, {EVP_aes_192_ofb, "aes-192-ofb"}, + {EVP_aes_256_cbc, "aes-256-cbc"}, {EVP_aes_256_ctr, "aes-256-ctr"}, + {EVP_aes_256_ecb, "aes-256-ecb"}, {EVP_aes_256_gcm, "aes-256-gcm"}, + {EVP_aes_256_ofb, "aes-256-ofb"}, {EVP_des_cbc, "des-cbc"}, + {EVP_des_ecb, "des-ecb"}, {EVP_des_ede, "des-ede"}, + {EVP_des_ede3_cbc, "des-ede3-cbc"}, {EVP_des_ede_cbc, "des-ede-cbc"}, + {EVP_rc2_cbc, "rc2-cbc"}, {EVP_rc4, "rc4"}, +}; + +#endif } // namespace // ============================================================================ @@ -4286,6 +4309,12 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) { CipherCallbackContext context; context.cb = std::move(callback); +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK + for (const auto& cipher : kBoringSSLCiphers) { + static_cast(cipher.get); + context.cb(cipher.name); + } +#else EVP_CIPHER_do_all_sorted( #if OPENSSL_VERSION_MAJOR >= 3 array_push_back, #endif &context); +#endif } // ============================================================================ diff --git a/deps/ncrypto/ncrypto.gyp b/deps/ncrypto/ncrypto.gyp index cf9b7c6cdb6d2c..1747f3ea0149b9 100644 --- a/deps/ncrypto/ncrypto.gyp +++ b/deps/ncrypto/ncrypto.gyp @@ -1,5 +1,6 @@ { 'variables': { + 'ncrypto_bssl_libdecrepit_missing%': 1, 'ncrypto_sources': [ 'engine.cc', 'ncrypto.cc', @@ -11,8 +12,14 @@ 'target_name': 'ncrypto', 'type': 'static_library', 'include_dirs': ['.'], + 'defines': [ + 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', + ], 'direct_dependent_settings': { 'include_dirs': ['.'], + 'defines': [ + 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', + ], }, 'sources': [ '<@(ncrypto_sources)' ], 'conditions': [ diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 5a41dd34f242f3..b27e2e76c3dcfc 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -28,6 +28,18 @@ (OPENSSL_VERSION_NUMBER >= (((maj) << 28) | ((min) << 20))) #endif +// BoringSSL declares the EVP_*_do_all* APIs, but their implementation may +// live in libdecrepit. This matches standalone ncrypto's build flag. +#ifndef NCRYPTO_BSSL_LIBDECREPIT_MISSING +#define NCRYPTO_BSSL_LIBDECREPIT_MISSING 0 +#endif + +#if defined(OPENSSL_IS_BORINGSSL) && NCRYPTO_BSSL_LIBDECREPIT_MISSING +#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 1 +#else +#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 0 +#endif + // The FIPS-related functions are only available // when the OpenSSL itself was compiled with FIPS support. #if defined(OPENSSL_FIPS) && !OPENSSL_VERSION_PREREQ(3, 0) diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 9b76b900049484..c42926bb4ce61f 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,6 +7,10 @@ #include "threadpoolwork-inl.h" #include "v8.h" +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +#include +#endif + #include namespace node { @@ -41,6 +45,24 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0); } +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +struct BoringSSLDigest { + const EVP_MD* (*get)(); + const char* name; +}; + +constexpr BoringSSLDigest kBoringSSLDigests[] = { + {EVP_md4, "md4"}, + {EVP_md5, "md5"}, + {EVP_sha1, "sha1"}, + {EVP_sha224, "sha224"}, + {EVP_sha256, "sha256"}, + {EVP_sha384, "sha384"}, + {EVP_sha512, "sha512"}, + {EVP_sha512_256, "sha512-256"}, +}; +#endif + #if OPENSSL_VERSION_MAJOR >= 3 void PushAliases(const char* name, void* data) { static_cast*>(data)->push_back(name); @@ -122,7 +144,12 @@ void SaveSupportedHashAlgorithms(const EVP_MD* md, const std::vector& GetSupportedHashAlgorithms(Environment* env) { if (env->supported_hash_algorithms.empty()) { MarkPopErrorOnReturn mark_pop_error_on_return; -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK + for (const auto& digest : kBoringSSLDigests) { + static_cast(digest.get); + env->supported_hash_algorithms.emplace_back(digest.name); + } +#elif OPENSSL_VERSION_MAJOR >= 3 // Since we'll fetch the EVP_MD*, cache them along the way to speed up // later lookups instead of throwing them away immediately. EVP_MD_do_all_sorted(SaveSupportedHashAlgorithmsAndCacheMD, env); diff --git a/test/parallel/test-crypto-boringssl-evp-list.js b/test/parallel/test-crypto-boringssl-evp-list.js new file mode 100644 index 00000000000000..3f142c24f28a7c --- /dev/null +++ b/test/parallel/test-crypto-boringssl-evp-list.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!process.features.openssl_is_boringssl) + common.skip('BoringSSL-only test'); + +const assert = require('assert'); +const { getCiphers, getHashes } = require('crypto'); + +const ciphers = getCiphers(); +[ + 'aes-128-cbc', + 'aes-256-gcm', + 'des-ede', + 'des-ede-cbc', + 'des-ede3-cbc', + 'rc2-cbc', + 'rc4', +].forEach((cipher) => assert(ciphers.includes(cipher), cipher)); + +const hashes = getHashes(); +[ + 'md4', + 'md5', + 'sha1', + 'sha256', + 'sha512-256', +].forEach((hash) => assert(hashes.includes(hash), hash)); From c978a8c78c75322c423fd21104248c5ce945e31c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 5 May 2026 14:12:16 +0200 Subject: [PATCH 08/11] tls: add unsupported renegotiation error Map BoringSSL's native renegotiation failure to ERR_TLS_RENEGOTIATION_UNSUPPORTED when TLSSocket#renegotiate() is called. This avoids exposing an implementation-specific OpenSSL error when the TLS backend does not support caller-initiated renegotiation. Signed-off-by: Filip Skokan --- doc/api/errors.md | 7 ++++++ lib/internal/errors.js | 2 ++ lib/internal/tls/wrap.js | 8 ++++++- .../test-tls-client-renegotiation-13.js | 24 ++++++++++++------- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 540e8122b9f876..a5988802f72ed5 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3106,6 +3106,13 @@ Failed to set PSK identity hint. Hint may be too long. An attempt was made to renegotiate TLS on a socket instance with renegotiation disabled. + + +### `ERR_TLS_RENEGOTIATION_UNSUPPORTED` + +An attempt was made to renegotiate TLS, but the TLS implementation does not +support caller-initiated renegotiation. + ### `ERR_TLS_REQUIRED_SERVER_NAME` diff --git a/lib/internal/errors.js b/lib/internal/errors.js index c40eed86bca834..998e618ced5b42 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1840,6 +1840,8 @@ E('ERR_TLS_PROTOCOL_VERSION_CONFLICT', 'TLS protocol version %j conflicts with secureProtocol %j', TypeError); E('ERR_TLS_RENEGOTIATION_DISABLED', 'TLS session renegotiation disabled for this socket', Error); +E('ERR_TLS_RENEGOTIATION_UNSUPPORTED', + 'TLS session renegotiation is unsupported by this TLS implementation', Error); // This should probably be a `TypeError`. E('ERR_TLS_REQUIRED_SERVER_NAME', diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js index d89e501432968a..05ce6955ed9217 100644 --- a/lib/internal/tls/wrap.js +++ b/lib/internal/tls/wrap.js @@ -72,6 +72,7 @@ const { ERR_TLS_INVALID_CONTEXT, ERR_TLS_INVALID_STATE, ERR_TLS_RENEGOTIATION_DISABLED, + ERR_TLS_RENEGOTIATION_UNSUPPORTED, ERR_TLS_REQUIRED_SERVER_NAME, ERR_TLS_SESSION_ATTACK, ERR_TLS_SNI_FROM_SERVER, @@ -1014,8 +1015,13 @@ TLSSocket.prototype.renegotiate = function(options, callback) { try { this._handle.renegotiate(); } catch (err) { + const isBoringSSLRenegotiationUnsupported = + process.features.openssl_is_boringssl && + err?.code === 'ERR_SSL_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED'; + const error = isBoringSSLRenegotiationUnsupported ? + new ERR_TLS_RENEGOTIATION_UNSUPPORTED() : err; if (callback) { - process.nextTick(callback, err); + process.nextTick(callback, error); } return false; } diff --git a/test/parallel/test-tls-client-renegotiation-13.js b/test/parallel/test-tls-client-renegotiation-13.js index 5afa8389ed37ca..80c4753d065ec1 100644 --- a/test/parallel/test-tls-client-renegotiation-13.js +++ b/test/parallel/test-tls-client-renegotiation-13.js @@ -32,14 +32,22 @@ connect({ assert.strictEqual(client.getProtocol(), 'TLSv1.3'); const ok = client.renegotiate({}, common.mustCall((err) => { - assert.throws(() => { throw err; }, { - message: hasOpenSSL3 ? - 'error:0A00010A:SSL routines::wrong ssl version' : - 'error:1420410A:SSL routines:SSL_renegotiate:wrong ssl version', - code: 'ERR_SSL_WRONG_SSL_VERSION', - library: 'SSL routines', - reason: 'wrong ssl version', - }); + if (process.features.openssl_is_boringssl) { + assert.throws(() => { throw err; }, { + message: 'TLS session renegotiation is unsupported by this TLS ' + + 'implementation', + code: 'ERR_TLS_RENEGOTIATION_UNSUPPORTED', + }); + } else { + assert.throws(() => { throw err; }, { + message: hasOpenSSL3 ? + 'error:0A00010A:SSL routines::wrong ssl version' : + 'error:1420410A:SSL routines:SSL_renegotiate:wrong ssl version', + code: 'ERR_SSL_WRONG_SSL_VERSION', + library: 'SSL routines', + reason: 'wrong ssl version', + }); + } cleanup(); })); From 7d85a98309df4aa67477cbe858490133987b6683 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 5 May 2026 14:13:39 +0200 Subject: [PATCH 09/11] test: update tls/crypto behaviour expectations when using BoringSSL Signed-off-by: Filip Skokan --- test/addons/openssl-get-ssl-ctx/binding.cc | 10 +- test/common/boringssl.js | 346 ++++++++++++++++++ test/parallel/test-crypto-dh-stateless.js | 226 +++++++----- test/parallel/test-crypto-keygen.js | 51 ++- test/parallel/test-crypto.js | 105 +++--- .../test-https-agent-session-reuse.js | 5 + .../test-https-client-renegotiation-limit.js | 5 + test/parallel/test-https-foafssl.js | 4 +- .../test-https-options-boolean-check.js | 43 ++- test/parallel/test-tls-alert.js | 27 ++ test/parallel/test-tls-client-auth.js | 5 +- .../test-tls-client-getephemeralkeyinfo.js | 6 + test/parallel/test-tls-client-mindhsize.js | 38 +- test/parallel/test-tls-client-reject.js | 15 +- .../test-tls-client-renegotiation-limit.js | 5 + test/parallel/test-tls-dhe.js | 5 + .../test-tls-disable-renegotiation.js | 5 + test/parallel/test-tls-ecdh-multiple.js | 4 +- test/parallel/test-tls-empty-sni-context.js | 9 +- test/parallel/test-tls-finished.js | 6 +- test/parallel/test-tls-getcipher.js | 58 ++- test/parallel/test-tls-getprotocol.js | 10 +- test/parallel/test-tls-handshake-error.js | 2 +- test/parallel/test-tls-honorcipherorder.js | 69 ++-- test/parallel/test-tls-junk-server.js | 2 +- test/parallel/test-tls-key-mismatch.js | 8 +- test/parallel/test-tls-max-send-fragment.js | 12 +- test/parallel/test-tls-min-max-version.js | 6 + test/parallel/test-tls-multi-key.js | 5 + test/parallel/test-tls-multi-pfx.js | 5 + test/parallel/test-tls-no-cert-required.js | 13 +- .../test-tls-options-boolean-check.js | 43 ++- test/parallel/test-tls-passphrase.js | 2 +- ...ls-psk-alpn-callback-exception-handling.js | 5 + test/parallel/test-tls-psk-circuit.js | 5 + test/parallel/test-tls-psk-server.js | 5 + .../test-tls-reduced-SECLEVEL-in-cipher.js | 5 + ...rver-failed-handshake-emits-clienterror.js | 2 +- test/parallel/test-tls-server-verify.js | 11 +- test/parallel/test-tls-session-cache.js | 7 +- test/parallel/test-tls-set-ciphers-error.js | 8 +- ...ls-set-default-ca-certificates-recovery.js | 4 +- test/parallel/test-tls-set-sigalgs.js | 14 +- ...tls-socket-failed-handshake-emits-error.js | 2 +- test/parallel/test-tls-ticket-cluster.js | 5 + test/parallel/test-tls-ticket.js | 6 + test/parallel/test-x509-escaping.js | 14 +- test/sequential/test-tls-connect.js | 2 +- test/sequential/test-tls-psk-client.js | 5 + 49 files changed, 968 insertions(+), 287 deletions(-) create mode 100644 test/common/boringssl.js diff --git a/test/addons/openssl-get-ssl-ctx/binding.cc b/test/addons/openssl-get-ssl-ctx/binding.cc index 3945ec870fb8b9..47468ffbcd789d 100644 --- a/test/addons/openssl-get-ssl-ctx/binding.cc +++ b/test/addons/openssl-get-ssl-ctx/binding.cc @@ -18,12 +18,12 @@ void GetSSLCtx(const v8::FunctionCallbackInfo& args) { return; } - // Verify the pointer is a valid SSL_CTX by calling an OpenSSL function. - const SSL_METHOD* method = SSL_CTX_get_ssl_method(ctx); - if (method == nullptr) { + // Verify the pointer is a valid SSL_CTX by calling a function available + // across OpenSSL-compatible TLS backends and checking context-owned state. + STACK_OF(SSL_CIPHER)* ciphers = SSL_CTX_get_ciphers(ctx); + if (ciphers == nullptr) { isolate->ThrowException(v8::Exception::Error( - v8::String::NewFromUtf8(isolate, - "SSL_CTX_get_ssl_method returned nullptr") + v8::String::NewFromUtf8(isolate, "SSL_CTX_get_ciphers returned nullptr") .ToLocalChecked())); return; } diff --git a/test/common/boringssl.js b/test/common/boringssl.js new file mode 100644 index 00000000000000..e6e91387c304c7 --- /dev/null +++ b/test/common/boringssl.js @@ -0,0 +1,346 @@ +/* eslint-disable node-core/crypto-check */ + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); + +// This module is for BoringSSL-specific branches in tests whose original +// OpenSSL coverage cannot run unchanged. Each helper should assert the +// observable BoringSSL behavior that explains why the OpenSSL-specific +// assertions are bypassed. + +/** + * BoringSSL exposes many removed or disabled TLS cipher suites as "no match" + * at secure-context creation time. This is used for suites such as + * finite-field DHE and anonymous ECDH that OpenSSL builds may still negotiate + * in tests. + * @param {Function} fn + */ +function assertNoCipherMatch(fn) { + assert.throws(fn, { + code: 'ERR_SSL_NO_CIPHER_MATCH', + library: 'SSL routines', + function: 'OPENSSL_internal', + reason: 'NO_CIPHER_MATCH', + }); +} + +/** + * BoringSSL does not parse OpenSSL cipher-string commands such as `@SECLEVEL`. + * Those are OpenSSL policy directives, not cipher names. + * @param {Function} fn + */ +function assertInvalidCommand(fn) { + assert.throws(fn, { + code: 'ERR_SSL_INVALID_COMMAND', + library: 'SSL routines', + function: 'OPENSSL_internal', + reason: 'INVALID_COMMAND', + }); +} + +/** + * Node's DHE tests exercise OpenSSL's finite-field DHE cipher support and DH + * parameter-size policy. BoringSSL does not offer these DHE cipher suites on + * this surface, so creating a server context with a DHE-only cipher list fails + * before a handshake can test DH parameter behavior. + */ +function assertFiniteFieldDheUnsupported() { + assertNoCipherMatch(() => { + tls.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + ciphers: 'DHE-RSA-AES128-GCM-SHA256', + }); + }); +} + +/** + * OpenSSL security levels reject small keys by policy and can be adjusted with + * `@SECLEVEL` in the cipher string. BoringSSL does not implement those security + * levels: the small-key server context is accepted, while the OpenSSL-specific + * `@SECLEVEL` command is rejected as invalid cipher-string syntax. + */ +function assertOpenSSLSecurityLevelsUnsupported() { + const options = { + key: fixtures.readKey('agent11-key.pem'), + cert: fixtures.readKey('agent11-cert.pem'), + ciphers: 'DEFAULT', + }; + + tls.createServer(options).close(); + + options.ciphers = 'DEFAULT:@SECLEVEL=0'; + assertInvalidCommand(() => tls.createServer(options)); +} + +/** + * Node's multi-key tests rely on OpenSSL accepting an array of private keys and + * matching them with an array of certificates. BoringSSL rejects this mixed + * EC/RSA identity configuration while configuring the certificate chain, before + * a client can negotiate either identity. + */ +function assertMultiKeyUnsupported() { + assert.throws(() => { + tls.createServer({ + key: [ + fixtures.readKey('ec10-key.pem'), + fixtures.readKey('agent1-key.pem'), + ], + cert: [ + fixtures.readKey('agent1-cert.pem'), + fixtures.readKey('ec10-cert.pem'), + ], + }); + }, { + code: 'ERR_OSSL_X509_KEY_TYPE_MISMATCH', + library: 'X.509 certificate routines', + function: 'OPENSSL_internal', + reason: 'KEY_TYPE_MISMATCH', + }); +} + +/** + * BoringSSL does not support caller-initiated renegotiation. Even on a TLS 1.2 + * connection, TLSSocket#renegotiate() returns false and the callback receives + * Node's BoringSSL-specific unsupported-renegotiation error instead of + * entering the native binding or exercising Node's renegotiation-limit logic. + */ +function testRenegotiationUnsupported() { + const server = tls.createServer({ + key: fixtures.readKey('rsa_private.pem'), + cert: fixtures.readKey('rsa_cert.crt'), + maxVersion: 'TLSv1.2', + }, (socket) => socket.resume()); + + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + maxVersion: 'TLSv1.2', + }, common.mustCall(() => { + const ok = client.renegotiate({}, common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ERR_TLS_RENEGOTIATION_UNSUPPORTED', + message: 'TLS session renegotiation is unsupported by this TLS ' + + 'implementation', + }); + client.destroy(); + server.close(); + })); + assert.strictEqual(ok, false); + })); + client.on('error', common.mustNotCall()); + })); +} + +/** + * OpenSSL exposes the negotiated ephemeral key type, name, and size for TLS + * clients. With BoringSSL the same ECDHE TLS 1.2 handshake succeeds, but + * getEphemeralKeyInfo() returns null on the server side and an object whose + * fields are undefined on the client side. + */ +function testEphemeralKeyInfoUnsupported() { + const server = tls.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + ciphers: 'ECDHE-RSA-AES256-GCM-SHA384', + ecdhCurve: 'prime256v1', + maxVersion: 'TLSv1.2', + }, common.mustCall((socket) => { + assert.strictEqual(socket.getEphemeralKeyInfo(), null); + socket.end(); + })); + + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + maxVersion: 'TLSv1.2', + }, common.mustCall(() => { + assert.deepStrictEqual(client.getEphemeralKeyInfo(), { + type: undefined, + name: undefined, + size: undefined, + }); + server.close(); + })); + })); +} + +/** + * The protocol matrix tests cover OpenSSL behavior for legacy TLS protocols. + * For BoringSSL we only need to exhibit that a TLSv1-only client cannot connect + * to a server whose minimum protocol is TLS 1.2; the client receives the + * protocol-version alert instead of the OpenSSL version-specific matrix. + */ +function testLegacyProtocolUnsupported() { + const server = tls.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + minVersion: 'TLSv1.2', + }, common.mustNotCall()); + + server.on('tlsClientError', common.mustCall()); + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + secureProtocol: 'TLSv1_method', + }, common.mustNotCall()); + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + server.close(); + })); + })); +} + +/** + * BoringSSL can load a multi-PFX option well enough to serve the ECDSA + * identity, but it does not provide the same OpenSSL multi-identity selection + * behavior. After the ECDSA handshake succeeds, an RSA-only client fails with + * no shared cipher instead of selecting the RSA identity from the same PFX list. + */ +function testMultiPfxSelectionDifference() { + const server = tls.createServer({ + pfx: [ + { + buf: fixtures.readKey('agent1.pfx'), + passphrase: 'sample', + }, + fixtures.readKey('ec.pfx'), + ], + }, common.mustCallAtLeast((socket) => socket.end(), 1)); + + server.listen(0, common.mustCall(() => { + const ecdsa = tls.connect(server.address().port, { + ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384', + maxVersion: 'TLSv1.2', + rejectUnauthorized: false, + }, common.mustCall(() => { + assert.strictEqual(ecdsa.getCipher().name, + 'ECDHE-ECDSA-AES256-GCM-SHA384'); + ecdsa.end(); + + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_NO_SHARED_CIPHER'); + })); + const rsa = tls.connect(server.address().port, { + ciphers: 'ECDHE-RSA-AES256-GCM-SHA384', + maxVersion: 'TLSv1.2', + rejectUnauthorized: false, + }, common.mustNotCall()); + rsa.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'); + server.close(); + })); + })); + })); +} + +/** + * PSK works for TLS 1.2 in BoringSSL, but Node's PSK tests also cover the + * default TLS 1.3 path. In that path BoringSSL does not complete a certificate- + * less PSK-only handshake through Node's current server setup: the server + * reports NO_CERTIFICATE_SET and the client receives an internal-error alert. + */ +function testPskTls13Unsupported() { + const key = Buffer.from('d731ef57be09e5204f0b205b60627028', 'hex'); + let gotClientError = false; + let gotServerError = false; + function maybeClose(server) { + if (gotClientError && gotServerError) + server.close(); + } + + const server = tls.createServer({ + ciphers: 'PSK+HIGH', + pskCallback() { return key; }, + }, common.mustNotCall()); + + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_NO_CERTIFICATE_SET'); + gotServerError = true; + maybeClose(server); + })); + + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + ciphers: 'PSK+HIGH', + checkServerIdentity() {}, + pskCallback() { + return { psk: key, identity: 'TestUser' }; + }, + }, common.mustNotCall()); + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR'); + gotClientError = true; + maybeClose(server); + })); + })); +} + +/** + * The OpenSSL ticket tests assume that once a TLS 1.3 session is reused, the + * client will not necessarily receive a replacement session event before close. + * BoringSSL emits new session tickets on both the initial and resumed TLS 1.3 + * connections, so the resumed connection still emits at least one 'session' + * event while isSessionReused() is true. + */ +function testTls13SessionTicketSemanticsDiffer() { + const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), + }, (socket) => socket.end()); + + let session; + let secondSessionEvents = 0; + + server.listen(0, common.mustCall(() => { + const first = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + }, common.mustCall(() => { + assert.strictEqual(first.isSessionReused(), false); + })); + first.on('session', common.mustCallAtLeast((sess) => { + session = sess; + }, 1)); + first.on('close', common.mustCall(() => { + assert(Buffer.isBuffer(session)); + + const second = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + session, + }, common.mustCall(() => { + assert.strictEqual(second.isSessionReused(), true); + })); + second.on('session', common.mustCallAtLeast(() => { + secondSessionEvents++; + }, 1)); + second.on('close', common.mustCall(() => { + assert(secondSessionEvents > 0); + server.close(); + })); + second.resume(); + })); + first.resume(); + })); +} + +module.exports = { + assertFiniteFieldDheUnsupported, + assertMultiKeyUnsupported, + assertNoCipherMatch, + assertOpenSSLSecurityLevelsUnsupported, + testEphemeralKeyInfoUnsupported, + testLegacyProtocolUnsupported, + testMultiPfxSelectionDifference, + testPskTls13Unsupported, + testRenegotiationUnsupported, + testTls13SessionTicketSemanticsDiffer, +}; diff --git a/test/parallel/test-crypto-dh-stateless.js b/test/parallel/test-crypto-dh-stateless.js index 9e13c62595ef26..0ade828eb2342b 100644 --- a/test/parallel/test-crypto-dh-stateless.js +++ b/test/parallel/test-crypto-dh-stateless.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); const { hasOpenSSL } = require('../common/crypto'); +const isBoringSSL = process.features.openssl_is_boringssl; // Error code for a key-type mismatch during (EC)DH. The underlying OpenSSL // error code varies by version, and in OpenSSL 4.0 by platform: some builds @@ -212,8 +213,28 @@ function testDHError(options, expected) { })); } -const alicePrivateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + +{ + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'P-256', + }); + + assert.throws(() => crypto.diffieHellman({ privateKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + + assert.throws(() => crypto.diffieHellman({ publicKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +if (isBoringSSL) { + common.printSkipMessage('Skipping finite-field DH KeyObject import and ' + + 'generation tests unsupported by BoringSSL'); +} else { + const alicePrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + @@ -224,10 +245,10 @@ const alicePrivateKey = crypto.createPrivateKey({ 'iIt9FmvFaaOVe2DupqSr6xzbf/zyON+WF5B5HNVOWXswgpgdUsCyygs98hKy/Xje\n' + 'TGzJUoWInW39t0YgMXenJrkS0m6wol8Rhxx81AGgELNV7EHZqg==\n' + '-----END PRIVATE KEY-----', - format: 'pem' -}); -const alicePublicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + + format: 'pem' + }); + const alicePublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + 'MIIBnzCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + @@ -238,11 +259,11 @@ const alicePublicKey = crypto.createPublicKey({ 'rDEz8mjIlnvbWpKB9+uYmbjfVoc3leFvUBqfG2In2m23Md1swsPxr3n7g68H66JX\n' + 'iBJKZLQMqNdbY14G9rdKmhhTJrQjC+i7Q/wI8JPhOFzHIGA=\n' + '-----END PUBLIC KEY-----', - format: 'pem' -}); + format: 'pem' + }); -const bobPrivateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + + const bobPrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + @@ -253,11 +274,11 @@ const bobPrivateKey = crypto.createPrivateKey({ 'GagGtIy3dV5f4FA0B/2C97jQ1pO16ah8gSLQRKsNpTCw2rqsZusE0rK6RaYAef7H\n' + 'y/0tmLIsHxLIn+WK9CANqMbCWoP4I178BQaqhiOBkNyNZ0ndqA==\n' + '-----END PRIVATE KEY-----', - format: 'pem' -}); + format: 'pem' + }); -const bobPublicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + + const bobPublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + @@ -268,83 +289,83 @@ const bobPublicKey = crypto.createPublicKey({ 'QFKfjzNaJRNMFFd4f2Dn8MSB4yu1xpA1T2i0JSk24vS2H55jx24xhUYtfhT2LJgK\n' + 'JvnaODey/xtY4Kql10ZKf43Lw6gdQC3G8opC9OxVxt9oNR7Z\n' + '-----END PUBLIC KEY-----', - format: 'pem' -}); + format: 'pem' + }); -assert.throws(() => crypto.diffieHellman({ privateKey: alicePrivateKey }), { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', -}); + assert.throws(() => crypto.diffieHellman({ privateKey: alicePrivateKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); -assert.throws(() => crypto.diffieHellman({ publicKey: alicePublicKey }), { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', -}); + assert.throws(() => crypto.diffieHellman({ publicKey: alicePublicKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); -const privateKey = Buffer.from( - '487CD880159D835FD0A8DBA9848898317283DB07E822741B344AD397BA84CDDD3920A51588' + + const privateKey = Buffer.from( + '487CD880159D835FD0A8DBA9848898317283DB07E822741B344AD397BA84CDDD3920A51588' + 'B891B03B3EBEF3C9F767D921FAC1294D4B5E09CABB6D1DE3EB4527989754FEB64D007EBBDA' + '2E6C8CE7A17EF41DE3C2DFE7CEAAF963199F55D5DBD9A415E77552FE69B7A41D87888B7D16' + '6BC569A3957B60EEA6A4ABEB1CDB7FFCF238DF961790791CD54E597B3082981D52C0B2CA0B' + '3DF212B2FD78DE4C6CC95285889D6DFDB746203177A726B912D26EB0A25F11871C7CD401A0' + '10B355EC41D9AA', 'hex'); -const publicKey = Buffer.from( - '8b6ea8abccff18d4819b7ce280db7b480edc02b5016d3c4835af622d85a9e9bc6bbc22b00d' + + const publicKey = Buffer.from( + '8b6ea8abccff18d4819b7ce280db7b480edc02b5016d3c4835af622d85a9e9bc6bbc22b00d' + '0c0848ddfafd0530f275007bc691c8cb74a189fecbabd63f0e4e94ef932eb51e94c5456800' + 'c4ce8628987d335466f4b16e1a04df21682d266eb3edf50b21802be3af58443c49da40529f' + '8f335a25134c1457787f60e7f0c481e32bb5c690354f68b4252936e2f4b61f9e63c76e3185' + '462d7e14f62c980a26f9da3837b2ff1b58e0aaa5d7464a7f8dcbc3a81d402dc6f28a42f4ec' + '55c6df68351ed9', 'hex'); -const group = crypto.getDiffieHellman('modp5'); -const dh = crypto.createDiffieHellman(group.getPrime(), group.getGenerator()); -dh.setPrivateKey(privateKey); + const group = crypto.getDiffieHellman('modp5'); + const dh = crypto.createDiffieHellman(group.getPrime(), group.getGenerator()); + dh.setPrivateKey(privateKey); -// Test simple Diffie-Hellman, no curves involved. -test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, - { publicKey: bobPublicKey, privateKey: bobPrivateKey }, - dh.computeSecret(publicKey)); + // Test simple Diffie-Hellman, no curves involved. + test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, + { publicKey: bobPublicKey, privateKey: bobPrivateKey }, + dh.computeSecret(publicKey)); -test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), - crypto.generateKeyPairSync('dh', { group: 'modp5' })); + test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), + crypto.generateKeyPairSync('dh', { group: 'modp5' })); -test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), - crypto.generateKeyPairSync('dh', { prime: group.getPrime() })); + test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), + crypto.generateKeyPairSync('dh', { prime: group.getPrime() })); -// DH parameter mismatch tests -{ - const list = [ + // DH parameter mismatch tests + { + const list = [ // Same generator, but different primes. - [{ group: 'modp5' }, { group: 'modp18' }]]; + [{ group: 'modp5' }, { group: 'modp18' }]]; - // TODO(danbev): Take a closer look if there should be a check in OpenSSL3 - // when the dh parameters differ. - if (!hasOpenSSL(3)) { + // TODO(danbev): Take a closer look if there should be a check in OpenSSL3 + // when the dh parameters differ. + if (!hasOpenSSL(3)) { // Same primes, but different generator. - list.push([{ group: 'modp5' }, { prime: group.getPrime(), generator: 5 }]); - // Same generator, but different primes. - list.push([{ primeLength: 1024 }, { primeLength: 1024 }]); - } + list.push([{ group: 'modp5' }, { prime: group.getPrime(), generator: 5 }]); + // Same generator, but different primes. + list.push([{ primeLength: 1024 }, { primeLength: 1024 }]); + } - for (const [params1, params2] of list) { - const options = { - privateKey: crypto.generateKeyPairSync('dh', params1).privateKey, - publicKey: crypto.generateKeyPairSync('dh', params2).publicKey, - }; - testDHError(options, { - name: 'Error', - code: hasOpenSSL(3) ? - 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' : - 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' - }); + for (const [params1, params2] of list) { + const options = { + privateKey: crypto.generateKeyPairSync('dh', params1).privateKey, + publicKey: crypto.generateKeyPairSync('dh', params2).publicKey, + }; + testDHError(options, { + name: 'Error', + code: hasOpenSSL(3) ? + 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' : + 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' + }); + } } -} -// This key combination will result in an unusually short secret, and should -// not cause an assertion failure. -{ - const shortPrivateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + + // This key combination will result in an unusually short secret, and should + // not cause an assertion failure. + { + const shortPrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + @@ -355,9 +376,9 @@ test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), 'RQD0QogW7ejSwMG8hCYibfrvMm0b5PHlwimISyEKh7VtDQ1frYN/Wr9ZbiV+FePJ\n' + '2j6RUKYNj1Pv+B4zdMgiLLjILAs8WUfbHciU21KSJh1izVQaUQ==\n' + '-----END PRIVATE KEY-----' - }); - const shortPublicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + + }); + const shortPublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + @@ -368,19 +389,20 @@ test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), 'taGX4mP3247golVx2DS4viDYs7UtaMdx03dWaP6y5StNUZQlgCIUzL7MYpC16V5y\n' + 'KkFrE+Kp/Z77gEjivaG6YuxVj4GPLxJYbNFVTel42oSVeKuq\n' + '-----END PUBLIC KEY-----', - format: 'pem' - }); + format: 'pem' + }); - testDH({ publicKey: shortPublicKey, privateKey: shortPrivateKey }, - Buffer.from( - '0099d0fa242af5db9ea7330e23937a27db041f79c581500fc7f9976' + + testDH({ publicKey: shortPublicKey, privateKey: shortPrivateKey }, + Buffer.from( + '0099d0fa242af5db9ea7330e23937a27db041f79c581500fc7f9976' + '554d59d5b9ced934778d72e19a1fefc81e9d981013198748c0b5c6c' + '762985eec687dc5bec5c9367b05837daee9d0bcc29024ed7f3abba1' + '2794b65a745117fb0d87bc5b1b2b68c296c3f686cc29e450e4e1239' + - '21f56a5733fe58aabf71f14582954059c2185d342b9b0fa10c2598a' + - '5426c2baee7f9a686fc1e16cd4757c852bf7225a2732250548efe28' + - 'debc26f1acdec51efe23d20786a6f8a14d360803bbc71972e87fd3', - 'hex')); + '21f56a5733fe58aabf71f14582954059c2185d342b9b0fa10c2598a' + + '5426c2baee7f9a686fc1e16cd4757c852bf7225a2732250548efe28' + + 'debc26f1acdec51efe23d20786a6f8a14d360803bbc71972e87fd3', + 'hex')); + } } // Test ECDH. @@ -401,20 +423,25 @@ test(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), }); } -test(crypto.generateKeyPairSync('x448'), - crypto.generateKeyPairSync('x448')); +if (isBoringSSL) { + common.printSkipMessage('Skipping x448 diffieHellman test cases ' + + 'unsupported by BoringSSL'); +} else { + test(crypto.generateKeyPairSync('x448'), + crypto.generateKeyPairSync('x448')); + + { + const options = { + privateKey: crypto.generateKeyPairSync('x448').privateKey, + publicKey: crypto.generateKeyPairSync('x25519').publicKey, + }; + testDHError(options, { code: keyTypeMismatchCode }); + } +} test(crypto.generateKeyPairSync('x25519'), crypto.generateKeyPairSync('x25519')); -{ - const options = { - privateKey: crypto.generateKeyPairSync('x448').privateKey, - publicKey: crypto.generateKeyPairSync('x25519').publicKey, - }; - testDHError(options, { code: keyTypeMismatchCode }); -} - // Test all key encoding formats for (const { privateKey: alicePriv, publicKey: bobPub } of [ crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), @@ -514,7 +541,7 @@ for (const { privateKey: alicePriv, publicKey: bobPub } of [ { const ec256 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); const ec384 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }); - const x448 = crypto.generateKeyPairSync('x448'); + const x448 = isBoringSSL ? null : crypto.generateKeyPairSync('x448'); const x25519 = crypto.generateKeyPairSync('x25519'); const ed25519 = crypto.generateKeyPairSync('ed25519'); @@ -564,18 +591,21 @@ for (const { privateKey: alicePriv, publicKey: bobPub } of [ /^ERR_OSSL_EVP_(OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE|INTERNAL_ERROR)$/ : 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' }); - // Incompatible key types (x448 + x25519) - testDHError({ - privateKey: privKey(x448.privateKey), - publicKey: pubKey(x25519.publicKey), - }, { code: keyTypeMismatchCode }); + if (!isBoringSSL) { + // Incompatible key types (x448 + x25519) + testDHError({ + privateKey: privKey(x448.privateKey), + publicKey: pubKey(x25519.publicKey), + }, { code: keyTypeMismatchCode }); + } // Zero x25519 public key testDHError({ privateKey: privKey(x25519.privateKey), publicKey: pubKey(zeroX25519PublicKey), - }, hasOpenSSL(3) ? - { code: 'ERR_OSSL_FAILED_DURING_DERIVATION' } : - { message: /Deriving bits failed/ }); + }, isBoringSSL ? { code: 'ERR_OSSL_EVP_INVALID_PEER_KEY' } : + hasOpenSSL(3) ? + { code: 'ERR_OSSL_FAILED_DURING_DERIVATION' } : + { message: /Deriving bits failed/ }); } } diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index 7911520af34481..751029e1921edb 100644 --- a/test/parallel/test-crypto-keygen.js +++ b/test/parallel/test-crypto-keygen.js @@ -15,6 +15,7 @@ const { const { inspect } = require('util'); const { hasOpenSSL3 } = require('../common/crypto'); +const isBoringSSL = process.features.openssl_is_boringssl; // Test invalid parameter encoding. { @@ -361,13 +362,24 @@ const { hasOpenSSL3 } = require('../common/crypto'); // Test invalid exponents. (caught by OpenSSL) for (const publicExponent of [1, 1 + 0x10001]) { - generateKeyPair('rsa', { - modulusLength: 4096, - publicExponent - }, common.mustCall((err) => { - assert.strictEqual(err.name, 'Error'); - assert.match(err.message, hasOpenSSL3 ? /exponent/ : /bad e value/); - })); + if (isBoringSSL) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'publicExponent is invalid', + }); + } else { + generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustCall((err) => { + assert.strictEqual(err.name, 'Error'); + assert.match(err.message, hasOpenSSL3 ? /exponent/ : /bad e value/); + })); + } } } @@ -494,16 +506,21 @@ const { hasOpenSSL3 } = require('../common/crypto'); }); })); - generateKeyPair('ec', { - namedCurve: 'secp256k1', - }, common.mustSucceed((publicKey, privateKey) => { - assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - namedCurve: 'secp256k1' - }); - assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - namedCurve: 'secp256k1' - }); - })); + if (isBoringSSL) { + common.printSkipMessage('Skipping secp256k1 keygen test case ' + + 'unsupported by BoringSSL'); + } else { + generateKeyPair('ec', { + namedCurve: 'secp256k1', + }, common.mustSucceed((publicKey, privateKey) => { + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + })); + } } { diff --git a/test/parallel/test-crypto.js b/test/parallel/test-crypto.js index fbb065dd442876..46f4571b33dfe8 100644 --- a/test/parallel/test-crypto.js +++ b/test/parallel/test-crypto.js @@ -62,7 +62,7 @@ assert.throws(() => { // Throws general Error, so there is no opensslErrorStack property. return err instanceof Error && err.name === 'Error' && - /^Error: mac verify failure$/.test(err) && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && !('opensslErrorStack' in err); }); @@ -72,7 +72,7 @@ assert.throws(() => { // Throws general Error, so there is no opensslErrorStack property. return err instanceof Error && err.name === 'Error' && - /^Error: mac verify failure$/.test(err) && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && !('opensslErrorStack' in err); }); @@ -82,7 +82,7 @@ assert.throws(() => { // Throws general Error, so there is no opensslErrorStack property. return err instanceof Error && err.name === 'Error' && - /^Error: not enough data$/.test(err) && + /^Error: (not enough data|BAD_PKCS12_DATA)$/.test(err) && !('opensslErrorStack' in err); }); @@ -211,49 +211,72 @@ assert.throws(() => { ].join('\n'); crypto.createSign('SHA256').update('test').sign(priv); }, (err) => { - if (!hasOpenSSL3) - assert.ok(!('opensslErrorStack' in err)); - assert.throws(() => { throw err; }, hasOpenSSL3 ? { - name: 'Error', - message: 'error:02000070:rsa routines::digest too big for rsa key', - library: 'rsa routines', - } : { - name: 'Error', - message: /routines:RSA_sign:digest too big for rsa key$/, - library: /rsa routines/i, - function: 'RSA_sign', - reason: /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i, - code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY' - }); + if (process.features.openssl_is_boringssl) { + // BoringSSL rejects the tiny RSA key while decoding it, before signing. + assert.throws(() => { throw err; }, { + name: 'Error', + message: 'error:06000066:public key routines:OPENSSL_internal:' + + 'DECODE_ERROR', + library: 'public key routines', + function: 'OPENSSL_internal', + reason: 'DECODE_ERROR', + code: 'ERR_OSSL_EVP_DECODE_ERROR' + }); + assert(Array.isArray(err.opensslErrorStack)); + assert(err.opensslErrorStack.length > 0); + } else { + if (!hasOpenSSL3) + assert.ok(!('opensslErrorStack' in err)); + assert.throws(() => { throw err; }, hasOpenSSL3 ? { + name: 'Error', + message: 'error:02000070:rsa routines::digest too big for rsa key', + library: 'rsa routines', + } : { + name: 'Error', + message: /routines:RSA_sign:digest too big for rsa key$/, + library: /rsa routines/i, + function: 'RSA_sign', + reason: /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i, + code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY' + }); + } return true; }); if (!hasOpenSSL3) { - assert.throws(() => { - // The correct header inside `rsa_private_pkcs8_bad.pem` should have been - // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- - // instead of - // -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- - const sha1_privateKey = fixtures.readKey('rsa_private_pkcs8_bad.pem', - 'ascii'); - // This would inject errors onto OpenSSL's error stack - crypto.createSign('sha1').sign(sha1_privateKey); - }, (err) => { - // Do the standard checks, but then do some custom checks afterwards. - assert.throws(() => { throw err; }, { - message: 'error:0D0680A8:asn1 encoding routines:asn1_check_tlen:' + - 'wrong tag', - library: 'asn1 encoding routines', - function: 'asn1_check_tlen', - reason: 'wrong tag', - code: 'ERR_OSSL_ASN1_WRONG_TAG', + // The correct header inside `rsa_private_pkcs8_bad.pem` should have been + // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- + // instead of + // -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- + const sha1_privateKey = fixtures.readKey('rsa_private_pkcs8_bad.pem', + 'ascii'); + + if (process.features.openssl_is_boringssl) { + // BoringSSL accepts the PKCS#8 payload despite the legacy PEM label. + const signature = crypto.createSign('sha1').sign(sha1_privateKey); + assert(Buffer.isBuffer(signature)); + assert.strictEqual(signature.length, 256); + } else { + assert.throws(() => { + // This would inject errors onto OpenSSL's error stack + crypto.createSign('sha1').sign(sha1_privateKey); + }, (err) => { + // Do the standard checks, but then do some custom checks afterwards. + assert.throws(() => { throw err; }, { + message: 'error:0D0680A8:asn1 encoding routines:asn1_check_tlen:' + + 'wrong tag', + library: 'asn1 encoding routines', + function: 'asn1_check_tlen', + reason: 'wrong tag', + code: 'ERR_OSSL_ASN1_WRONG_TAG', + }); + // Throws crypto error, so there is an opensslErrorStack property. + // The openSSL stack should have content. + assert(Array.isArray(err.opensslErrorStack)); + assert(err.opensslErrorStack.length > 0); + return true; }); - // Throws crypto error, so there is an opensslErrorStack property. - // The openSSL stack should have content. - assert(Array.isArray(err.opensslErrorStack)); - assert(err.opensslErrorStack.length > 0); - return true; - }); + } } // Make sure memory isn't released before being returned diff --git a/test/parallel/test-https-agent-session-reuse.js b/test/parallel/test-https-agent-session-reuse.js index 485f4b1ca308c9..c5b7b78b8e0272 100644 --- a/test/parallel/test-https-agent-session-reuse.js +++ b/test/parallel/test-https-agent-session-reuse.js @@ -5,6 +5,11 @@ const assert = require('assert'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); + return; +} + const https = require('https'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-https-client-renegotiation-limit.js b/test/parallel/test-https-client-renegotiation-limit.js index 6614090e737614..729176b7c1aa21 100644 --- a/test/parallel/test-https-client-renegotiation-limit.js +++ b/test/parallel/test-https-client-renegotiation-limit.js @@ -25,6 +25,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testRenegotiationUnsupported(); + return; +} + const assert = require('assert'); const tls = require('tls'); const https = require('https'); diff --git a/test/parallel/test-https-foafssl.js b/test/parallel/test-https-foafssl.js index ffa44f218b935d..a191bdcf32b73e 100644 --- a/test/parallel/test-https-foafssl.js +++ b/test/parallel/test-https-foafssl.js @@ -56,8 +56,8 @@ const server = https.createServer(options, common.mustCall(function(req, res) { cert = req.connection.getPeerCertificate(); assert.strictEqual(cert.subjectaltname, webIdUrl); - assert.strictEqual(cert.exponent, exponent); - assert.strictEqual(cert.modulus, modulus); + assert.strictEqual(cert.exponent.toLowerCase(), exponent.toLowerCase()); + assert.strictEqual(cert.modulus.toLowerCase(), modulus.toLowerCase()); res.writeHead(200, { 'content-type': 'text/plain' }); res.end(body, () => { console.log('stream finished'); }); console.log('sent response'); diff --git a/test/parallel/test-https-options-boolean-check.js b/test/parallel/test-https-options-boolean-check.js index 9740704e169f1e..fa02a165b80f10 100644 --- a/test/parallel/test-https-options-boolean-check.js +++ b/test/parallel/test-https-options-boolean-check.js @@ -40,9 +40,23 @@ const keyDataView = toDataView(keyBuff); const certDataView = toDataView(certBuff); const caArrDataView = toDataView(caCert); +function filterBoringSSLKeyCertArrayCases(options, setName) { + if (!process.features.openssl_is_boringssl) + return options; + + // The array-valued cases exercise multi-identity key/cert handling. + // BoringSSL may reject those cases with backend key/cert mismatch errors + // before the boolean/type validation this test is targeting. Keep the scalar + // cases so https.createServer() option type validation is still covered. + common.printSkipMessage( + `BoringSSL: skipping ${setName} key/cert array cases`); + return options.filter(([key, cert]) => !Array.isArray(key) && + !Array.isArray(cert)); +} + // Checks to ensure https.createServer doesn't throw an error // Format ['key', 'cert'] -[ +const validOptions = [ [keyBuff, certBuff], [false, certBuff], [keyBuff, false], @@ -62,13 +76,16 @@ const caArrDataView = toDataView(caCert); [false, [certStr, certStr2]], [[{ pem: keyBuff }], false], [[{ pem: keyBuff }, { pem: keyBuff }], false], -].forEach(([key, cert]) => { - https.createServer({ key, cert }); -}); +]; + +filterBoringSSLKeyCertArrayCases(validOptions, 'valid') + .forEach(([key, cert]) => { + https.createServer({ key, cert }); + }); // Checks to ensure https.createServer predictably throws an error // Format ['key', 'cert', 'expected message'] -[ +const invalidKeyOptions = [ [true, certBuff], [true, certStr], [true, certArrBuff], @@ -81,7 +98,10 @@ const caArrDataView = toDataView(caCert); [[true, keyStr2], [certStr, certStr2], 0], [[true, false], [certBuff, certBuff2], 0], [true, [certBuff, certBuff2]], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidKeyOptions, 'invalid key')) { const val = index === undefined ? key : key[index]; assert.throws(() => { https.createServer({ key, cert }); @@ -92,9 +112,9 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} -[ +const invalidCertOptions = [ [keyBuff, true], [keyStr, true], [keyArrBuff, true], @@ -107,7 +127,10 @@ const caArrDataView = toDataView(caCert); [[keyStr, keyStr2], [certStr, true], 1], [[keyStr, keyStr2], [true, false], 0], [[keyStr, keyStr2], true], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidCertOptions, 'invalid cert')) { const val = index === undefined ? cert : cert[index]; assert.throws(() => { https.createServer({ key, cert }); @@ -118,7 +141,7 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} // Checks to ensure https.createServer works with the CA parameter // Format ['key', 'cert', 'ca'] diff --git a/test/parallel/test-tls-alert.js b/test/parallel/test-tls-alert.js index 23c92e7293458f..64b7080e39ba25 100644 --- a/test/parallel/test-tls-alert.js +++ b/test/parallel/test-tls-alert.js @@ -48,6 +48,33 @@ const server = tls.Server({ key: loadPEM('agent2-key'), cert: loadPEM('agent2-cert') }, null).listen(0, common.mustCall(() => { + if (process.features.openssl_is_boringssl) { + let gotClientError = false; + let gotServerError = false; + function maybeClose() { + if (gotClientError && gotServerError) + server.close(); + } + + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + gotServerError = true; + maybeClose(); + })); + + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + secureProtocol: 'TLSv1_1_method', + }, common.mustNotCall()); + client.once('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + gotClientError = true; + maybeClose(); + })); + return; + } + const args = ['s_client', '-quiet', '-tls1_1', '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `127.0.0.1:${server.address().port}`]; diff --git a/test/parallel/test-tls-client-auth.js b/test/parallel/test-tls-client-auth.js index 67aed40914c9fe..517054c6e290dc 100644 --- a/test/parallel/test-tls-client-auth.js +++ b/test/parallel/test-tls-client-auth.js @@ -111,7 +111,10 @@ if (tls.DEFAULT_MAX_VERSION === 'TLSv1.3') connect({ // and sends a fatal Alert to the client that the client discovers there has // been a fatal error. pair.client.conn.once('error', common.mustCall((err) => { - assert.strictEqual(err.code, 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'); + const expectedErr = process.features.openssl_is_boringssl ? + 'ERR_SSL_TLSV1_ALERT_CERTIFICATE_REQUIRED' : + 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'; + assert.strictEqual(err.code, expectedErr); cleanup(); })); })); diff --git a/test/parallel/test-tls-client-getephemeralkeyinfo.js b/test/parallel/test-tls-client-getephemeralkeyinfo.js index 19728e3733d868..2107d024012c4d 100644 --- a/test/parallel/test-tls-client-getephemeralkeyinfo.js +++ b/test/parallel/test-tls-client-getephemeralkeyinfo.js @@ -2,6 +2,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); + +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testEphemeralKeyInfoUnsupported(); + return; +} + const fixtures = require('../common/fixtures'); const { hasOpenSSL } = require('../common/crypto'); diff --git a/test/parallel/test-tls-client-mindhsize.js b/test/parallel/test-tls-client-mindhsize.js index cd7b16ea566fe8..fa494c583a2f3b 100644 --- a/test/parallel/test-tls-client-mindhsize.js +++ b/test/parallel/test-tls-client-mindhsize.js @@ -85,21 +85,25 @@ function testDHE3072() { test(3072, false, null); } -if (hasOpenSSL(4, 0)) { - // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and - // ignores the server-supplied dhparam in favor of FFDHE-2048. The 3072 - // success case is therefore replaced by a 2048 success case. - testDHE2048(true, () => test(2048, false, null, 2048)); -} else if (secLevel > 1) { - // Minimum size for OpenSSL security level 2 and above is 2048 by default - testDHE2048(true, testDHE3072); +if (!process.features.openssl_is_boringssl) { + if (hasOpenSSL(4, 0)) { + // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and + // ignores the server-supplied dhparam in favor of FFDHE-2048. The 3072 + // success case is therefore replaced by a 2048 success case. + testDHE2048(true, () => test(2048, false, null, 2048)); + } else if (secLevel > 1) { + // Minimum size for OpenSSL security level 2 and above is 2048 by default + testDHE2048(true, testDHE3072); + } else { + testDHE1024(); + } + + assert.throws(() => test(512, true, common.mustNotCall()), + /DH parameter is less than 1024 bits/); } else { - testDHE1024(); + require('../common/boringssl').assertFiniteFieldDheUnsupported(); } -assert.throws(() => test(512, true, common.mustNotCall()), - /DH parameter is less than 1024 bits/); - for (const minDHSize of [0, -1, -Infinity, NaN]) { assert.throws(() => { tls.connect({ minDHSize }); @@ -118,7 +122,9 @@ for (const minDHSize of [true, false, null, undefined, {}, [], '', '1']) { }); } -process.on('exit', function() { - assert.strictEqual(nsuccess, 1); - assert.strictEqual(nerror, 1); -}); +if (!process.features.openssl_is_boringssl) { + process.on('exit', function() { + assert.strictEqual(nsuccess, 1); + assert.strictEqual(nerror, 1); + }); +} diff --git a/test/parallel/test-tls-client-reject.js b/test/parallel/test-tls-client-reject.js index 68922e3690eac0..cff0aabc89a774 100644 --- a/test/parallel/test-tls-client-reject.js +++ b/test/parallel/test-tls-client-reject.js @@ -30,7 +30,8 @@ const fixtures = require('../common/fixtures'); const options = { key: fixtures.readKey('rsa_private.pem'), - cert: fixtures.readKey('rsa_cert.crt') + cert: fixtures.readKey('rsa_cert.crt'), + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }; const server = tls.createServer(options, function(socket) { @@ -46,7 +47,8 @@ function unauthorized() { const socket = tls.connect({ port: server.address().port, servername: 'localhost', - rejectUnauthorized: false + rejectUnauthorized: false, + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(function() { let _data; assert(!socket.authorized); @@ -67,7 +69,8 @@ function unauthorized() { function rejectUnauthorized() { console.log('reject unauthorized'); const socket = tls.connect(server.address().port, { - servername: 'localhost' + servername: 'localhost', + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustNotCall()); socket.on('data', common.mustNotCall()); socket.on('error', common.mustCall(function(err) { @@ -80,7 +83,8 @@ function rejectUnauthorizedUndefined() { console.log('reject unauthorized undefined'); const socket = tls.connect(server.address().port, { servername: 'localhost', - rejectUnauthorized: undefined + rejectUnauthorized: undefined, + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustNotCall()); socket.on('data', common.mustNotCall()); socket.on('error', common.mustCall(function(err) { @@ -93,7 +97,8 @@ function authorized() { console.log('connect authorized'); const socket = tls.connect(server.address().port, { ca: [fixtures.readKey('rsa_cert.crt')], - servername: 'localhost' + servername: 'localhost', + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(function() { console.log('... authorized'); assert(socket.authorized); diff --git a/test/parallel/test-tls-client-renegotiation-limit.js b/test/parallel/test-tls-client-renegotiation-limit.js index 86111d6da0b402..9b7f62865b336d 100644 --- a/test/parallel/test-tls-client-renegotiation-limit.js +++ b/test/parallel/test-tls-client-renegotiation-limit.js @@ -31,6 +31,11 @@ if (!opensslCli) { common.skip('node compiled without OpenSSL CLI.'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testRenegotiationUnsupported(); + return; +} + const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-tls-dhe.js b/test/parallel/test-tls-dhe.js index 03750bc206adbe..b788d153293899 100644 --- a/test/parallel/test-tls-dhe.js +++ b/test/parallel/test-tls-dhe.js @@ -26,6 +26,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').assertFiniteFieldDheUnsupported(); + return; +} + const { opensslCli, hasOpenSSL, diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js index f91868c6345d71..84a6ead4a5441c 100644 --- a/test/parallel/test-tls-disable-renegotiation.js +++ b/test/parallel/test-tls-disable-renegotiation.js @@ -8,6 +8,11 @@ const fixtures = require('../common/fixtures'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testRenegotiationUnsupported(); + return; +} + const tls = require('tls'); // Renegotiation as a protocol feature was dropped after TLS1.2. diff --git a/test/parallel/test-tls-ecdh-multiple.js b/test/parallel/test-tls-ecdh-multiple.js index ee52f288610956..ed60044197d7da 100644 --- a/test/parallel/test-tls-ecdh-multiple.js +++ b/test/parallel/test-tls-ecdh-multiple.js @@ -26,7 +26,7 @@ function loadPEM(n) { // OpenSSL 4.0 disables support for deprecated elliptic curves from RFC 8422 // (including secp256k1) by default. -const ecdhCurve = hasOpenSSL(4, 0) ? +const ecdhCurve = process.features.openssl_is_boringssl || hasOpenSSL(4, 0) ? 'prime256v1:secp521r1' : 'secp256k1:prime256v1:secp521r1'; @@ -67,7 +67,7 @@ const server = tls.createServer(options, (conn) => { } // Deprecated RFC 8422 curves are disabled by default in OpenSSL 4.0. - if (hasOpenSSL(4, 0)) { + if (process.features.openssl_is_boringssl || hasOpenSSL(4, 0)) { unsupportedCurves.push('secp256k1'); } diff --git a/test/parallel/test-tls-empty-sni-context.js b/test/parallel/test-tls-empty-sni-context.js index e4136ff71e1d52..6ecdfbeecbe3c9 100644 --- a/test/parallel/test-tls-empty-sni-context.js +++ b/test/parallel/test-tls-empty-sni-context.js @@ -16,7 +16,7 @@ const options = { const server = tls.createServer(options, (c) => { assert.fail('Should not be called'); }).on('tlsClientError', common.mustCall((err, c) => { - assert.match(err.message, /no suitable signature algorithm/i); + assert.match(err.message, /no suitable signature algorithm|NO_CERTIFICATE_SET/i); server.close(); })).listen(0, common.mustCall(() => { const c = tls.connect({ @@ -26,9 +26,10 @@ const server = tls.createServer(options, (c) => { }, common.mustNotCall()); c.on('error', common.mustCall((err) => { - const expectedErr = hasOpenSSL(4, 0) ? - 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? - 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; + const expectedErr = process.features.openssl_is_boringssl ? + 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR' : hasOpenSSL(4, 0) ? + 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? + 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; assert.strictEqual(err.code, expectedErr); })); })); diff --git a/test/parallel/test-tls-finished.js b/test/parallel/test-tls-finished.js index 8b52934b049d95..b23b4567d27ec6 100644 --- a/test/parallel/test-tls-finished.js +++ b/test/parallel/test-tls-finished.js @@ -20,7 +20,8 @@ const msg = {}; const pem = (n) => fixtures.readKey(`${n}.pem`); const server = tls.createServer({ key: pem('agent1-key'), - cert: pem('agent1-cert') + cert: pem('agent1-cert'), + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall((alice) => { msg.server = { alice: alice.getFinished(), @@ -32,7 +33,8 @@ const server = tls.createServer({ server.listen(0, common.mustCall(() => { const bob = tls.connect({ port: server.address().port, - rejectUnauthorized: false + rejectUnauthorized: false, + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(() => { msg.client = { alice: bob.getPeerFinished(), diff --git a/test/parallel/test-tls-getcipher.js b/test/parallel/test-tls-getcipher.js index 4d5042d6e6beab..2d4de5639afb70 100644 --- a/test/parallel/test-tls-getcipher.js +++ b/test/parallel/test-tls-getcipher.js @@ -36,27 +36,42 @@ const options = { honorCipherOrder: true }; +const isBoringSSL = process.features.openssl_is_boringssl; let clients = 0; +const expectedClients = isBoringSSL ? 1 : 2; const server = tls.createServer(options, common.mustCall(() => { if (--clients === 0) server.close(); -}, 2)); +}, expectedClients)); server.listen(0, '127.0.0.1', common.mustCall(function() { - clients++; - tls.connect({ - host: '127.0.0.1', - port: this.address().port, - ciphers: 'AES256-SHA256', - rejectUnauthorized: false, - maxVersion: 'TLSv1.2', - }, common.mustCall(function() { - const cipher = this.getCipher(); - assert.strictEqual(cipher.name, 'AES256-SHA256'); - assert.strictEqual(cipher.standardName, 'TLS_RSA_WITH_AES_256_CBC_SHA256'); - assert.strictEqual(cipher.version, 'TLSv1.2'); - this.end(); - })); + if (isBoringSSL) { + // BoringSSL does not provide this static RSA TLS 1.2 cipher suite on + // Node's supported cipher surface, so keep the OpenSSL getCipher() + // assertion below limited to backends that can create the context. + common.printSkipMessage('BoringSSL does not provide AES256-SHA256'); + assert.throws(() => tls.createSecureContext({ ciphers: 'AES256-SHA256' }), { + code: 'ERR_SSL_NO_CIPHER_MATCH', + library: 'SSL routines', + function: 'OPENSSL_internal', + reason: 'NO_CIPHER_MATCH', + }); + } else { + clients++; + tls.connect({ + host: '127.0.0.1', + port: this.address().port, + ciphers: 'AES256-SHA256', + rejectUnauthorized: false, + maxVersion: 'TLSv1.2', + }, common.mustCall(function() { + const cipher = this.getCipher(); + assert.strictEqual(cipher.name, 'AES256-SHA256'); + assert.strictEqual(cipher.standardName, 'TLS_RSA_WITH_AES_256_CBC_SHA256'); + assert.strictEqual(cipher.version, 'TLSv1.2'); + this.end(); + })); + } clients++; tls.connect({ @@ -70,7 +85,9 @@ server.listen(0, '127.0.0.1', common.mustCall(function() { assert.strictEqual(cipher.name, 'ECDHE-RSA-AES256-GCM-SHA384'); assert.strictEqual(cipher.standardName, 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384'); - assert.strictEqual(cipher.version, 'TLSv1.2'); + assert.strictEqual(cipher.version, isBoringSSL ? + 'TLSv1/SSLv3' : + 'TLSv1.2'); this.end(); })); })); @@ -90,9 +107,14 @@ tls.createServer({ rejectUnauthorized: false }, common.mustCall(() => { const cipher = client.getCipher(); - assert.strictEqual(cipher.name, 'TLS_AES_256_GCM_SHA384'); + const expectedCipher = isBoringSSL ? + 'TLS_AES_128_GCM_SHA256' : + 'TLS_AES_256_GCM_SHA384'; + assert.strictEqual(cipher.name, expectedCipher); assert.strictEqual(cipher.standardName, cipher.name); - assert.strictEqual(cipher.version, 'TLSv1.3'); + assert.strictEqual(cipher.version, isBoringSSL ? + 'TLSv1/SSLv3' : + 'TLSv1.3'); client.end(); })); })); diff --git a/test/parallel/test-tls-getprotocol.js b/test/parallel/test-tls-getprotocol.js index 5fe46c43c376cf..2945ff99b5a290 100644 --- a/test/parallel/test-tls-getprotocol.js +++ b/test/parallel/test-tls-getprotocol.js @@ -12,7 +12,7 @@ const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); -const clientConfigs = [ +let clientConfigs = [ { secureProtocol: 'TLSv1_method', version: 'TLSv1', @@ -27,6 +27,14 @@ const clientConfigs = [ }, ]; +if (process.features.openssl_is_boringssl) { + // Remove the TLSv1 and TLSv1.1 cases. BoringSSL does not negotiate those + // legacy protocols in this configuration; keep TLSv1.2 to cover getProtocol() + // on a successful BoringSSL TLS handshake. + common.printSkipMessage('BoringSSL: skipping TLSv1/TLSv1.1 getProtocol cases'); + clientConfigs = clientConfigs.filter(({ version }) => version === 'TLSv1.2'); +} + const serverConfig = { secureProtocol: 'TLS_method', key: fixtures.readKey('agent2-key.pem'), diff --git a/test/parallel/test-tls-handshake-error.js b/test/parallel/test-tls-handshake-error.js index 5547964780cd60..94a21a14975b5d 100644 --- a/test/parallel/test-tls-handshake-error.js +++ b/test/parallel/test-tls-handshake-error.js @@ -20,7 +20,7 @@ const server = tls.createServer({ port: this.address().port, ciphers: 'no-such-cipher' }, common.mustNotCall()); - }, /no cipher match/i); + }, /no[_ ]cipher[_ ]match/i); server.close(); })); diff --git a/test/parallel/test-tls-honorcipherorder.js b/test/parallel/test-tls-honorcipherorder.js index 5f123cd739a4c0..d86a59aa4cdc6d 100644 --- a/test/parallel/test-tls-honorcipherorder.js +++ b/test/parallel/test-tls-honorcipherorder.js @@ -16,14 +16,40 @@ const util = require('util'); // default method is updated in the future const SSL_Method = 'TLSv1_2_method'; const localhost = '127.0.0.1'; +const config = process.features.openssl_is_boringssl ? { + serverCiphers: + 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', + clientPreferenceCiphers: + 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384', + clientPreferredCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + serverPreferredCipher: 'ECDHE-RSA-AES256-GCM-SHA384', + singleCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + defaultCipher: 'ECDHE-RSA-AES256-GCM-SHA384', + limitedDefaultCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + extraCases: [], +} : { + serverCiphers: 'AES256-SHA256:AES128-GCM-SHA256:AES128-SHA256:' + + 'ECDHE-RSA-AES128-GCM-SHA256', + clientPreferenceCiphers: 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', + clientPreferredCipher: 'AES128-GCM-SHA256', + serverPreferredCipher: 'AES256-SHA256', + singleCipher: 'AES128-SHA256', + defaultCipher: 'AES256-SHA256', + limitedDefaultCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + extraCases: [ + // Server has the preference of cipher suites. AES128-GCM-SHA256 is given + // higher priority over AES128-SHA256 among client cipher suites. + [true, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'], + [undefined, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'], + ], +}; function test(honorCipherOrder, clientCipher, expectedCipher, defaultCiphers) { const soptions = { secureProtocol: SSL_Method, key: fixtures.readKey('agent2-key.pem'), cert: fixtures.readKey('agent2-cert.pem'), - ciphers: 'AES256-SHA256:AES128-GCM-SHA256:AES128-SHA256:' + - 'ECDHE-RSA-AES128-GCM-SHA256', + ciphers: config.serverCiphers, honorCipherOrder: honorCipherOrder, }; @@ -57,34 +83,27 @@ function test(honorCipherOrder, clientCipher, expectedCipher, defaultCiphers) { } // Client explicitly has the preference of cipher suites, not the default. -test(false, 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', - 'AES128-GCM-SHA256'); +test(false, config.clientPreferenceCiphers, config.clientPreferredCipher); -// Server has the preference of cipher suites, and AES256-SHA256 is -// the server's top choice. -test(true, 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', - 'AES256-SHA256'); -test(undefined, 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', - 'AES256-SHA256'); - -// Server has the preference of cipher suites. AES128-GCM-SHA256 is given -// higher priority over AES128-SHA256 among client cipher suites. -test(true, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'); -test(undefined, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'); +// Server has the preference of cipher suites. +test(true, config.clientPreferenceCiphers, config.serverPreferredCipher); +test(undefined, config.clientPreferenceCiphers, config.serverPreferredCipher); +for (const args of config.extraCases) { + test(...args); +} // As client has only one cipher, server has no choice, irrespective // of honorCipherOrder. -test(true, 'AES128-SHA256', 'AES128-SHA256'); -test(undefined, 'AES128-SHA256', 'AES128-SHA256'); +test(true, config.singleCipher, config.singleCipher); +test(undefined, config.singleCipher, config.singleCipher); -// Client did not explicitly set ciphers and client offers -// tls.DEFAULT_CIPHERS. All ciphers of the server are included in the -// default list so the negotiated cipher is selected according to the -// server's top preference of AES256-SHA256. -test(true, tls.DEFAULT_CIPHERS, 'AES256-SHA256'); -test(true, null, 'AES256-SHA256'); -test(undefined, null, 'AES256-SHA256'); +// Client did not explicitly set ciphers and client offers tls.DEFAULT_CIPHERS. +// All ciphers of the server are included in the default list so the negotiated +// cipher is selected according to server preference. +test(true, tls.DEFAULT_CIPHERS, config.defaultCipher); +test(true, null, config.defaultCipher); +test(undefined, null, config.defaultCipher); // Ensure that `tls.DEFAULT_CIPHERS` is used when its a limited cipher set. -test(true, null, 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256'); +test(true, null, config.limitedDefaultCipher, config.limitedDefaultCipher); diff --git a/test/parallel/test-tls-junk-server.js b/test/parallel/test-tls-junk-server.js index 42f089f8f90ed2..b6ff3cd2a467f2 100644 --- a/test/parallel/test-tls-junk-server.js +++ b/test/parallel/test-tls-junk-server.js @@ -24,7 +24,7 @@ server.listen(0, common.mustCall(function() { // Different OpenSSL versions report different errors for junk data on a // TLS connection, depending on which record validation check fires first. const expectedErrorMessage = - /wrong version number|packet length too long|bad record type/; + /wrong[ _]version[ _]number|packet length too long|bad record type/i; req.once('error', common.mustCall(function(err) { assert.match(err.message, expectedErrorMessage); server.close(); diff --git a/test/parallel/test-tls-key-mismatch.js b/test/parallel/test-tls-key-mismatch.js index df8848a03de4a9..797c7c171dc5ff 100644 --- a/test/parallel/test-tls-key-mismatch.js +++ b/test/parallel/test-tls-key-mismatch.js @@ -31,9 +31,11 @@ const { hasOpenSSL3 } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); -const errorMessageRegex = hasOpenSSL3 ? - /^Error: error:05800074:x509 certificate routines::key values mismatch$/ : - /^Error: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch$/; +const errorMessageRegex = process.features.openssl_is_boringssl ? + /^Error: error:0b000074:X\.509 certificate routines:OPENSSL_internal:KEY_VALUES_MISMATCH$/ : + hasOpenSSL3 ? + /^Error: error:05800074:x509 certificate routines::key values mismatch$/ : + /^Error: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch$/; const options = { key: fixtures.readKey('agent1-key.pem'), diff --git a/test/parallel/test-tls-max-send-fragment.js b/test/parallel/test-tls-max-send-fragment.js index 009021045624bb..2e319fcdaeafea 100644 --- a/test/parallel/test-tls-max-send-fragment.js +++ b/test/parallel/test-tls-max-send-fragment.js @@ -60,9 +60,15 @@ const server = tls.createServer({ assert.throws(() => c.setMaxSendFragment(Symbol()), { name: 'TypeError' }); - // Lower and upper limits. - assert(!c.setMaxSendFragment(511)); - assert(!c.setMaxSendFragment(16385)); + // OpenSSL enforces Node's documented fragment size range. BoringSSL accepts + // both out-of-range values and reports success, so assert that difference + // explicitly instead of using a truthiness shortcut. + const acceptsOutOfRangeFragmentSize = + process.features.openssl_is_boringssl; + assert.strictEqual(c.setMaxSendFragment(511), + acceptsOutOfRangeFragmentSize); + assert.strictEqual(c.setMaxSendFragment(16385), + acceptsOutOfRangeFragmentSize); // Correct fragment size. assert(c.setMaxSendFragment(maxChunk)); diff --git a/test/parallel/test-tls-min-max-version.js b/test/parallel/test-tls-min-max-version.js index 4903d92f5c5700..abddbbeb0eba1b 100644 --- a/test/parallel/test-tls-min-max-version.js +++ b/test/parallel/test-tls-min-max-version.js @@ -4,6 +4,12 @@ const common = require('../common'); if (!common.hasCrypto) { common.skip('missing crypto'); } + +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testLegacyProtocolUnsupported(); + return; +} + const { hasOpenSSL, hasOpenSSL3, diff --git a/test/parallel/test-tls-multi-key.js b/test/parallel/test-tls-multi-key.js index 89f9931e5bdd77..0a9c6f108bf675 100644 --- a/test/parallel/test-tls-multi-key.js +++ b/test/parallel/test-tls-multi-key.js @@ -27,6 +27,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').assertMultiKeyUnsupported(); + return; +} + const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); diff --git a/test/parallel/test-tls-multi-pfx.js b/test/parallel/test-tls-multi-pfx.js index 526b77b1484cd3..fec697cd3b7093 100644 --- a/test/parallel/test-tls-multi-pfx.js +++ b/test/parallel/test-tls-multi-pfx.js @@ -3,6 +3,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testMultiPfxSelectionDifference(); + return; +} + const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-tls-no-cert-required.js b/test/parallel/test-tls-no-cert-required.js index b3dcfa516ab502..499ab2dfd14ed2 100644 --- a/test/parallel/test-tls-no-cert-required.js +++ b/test/parallel/test-tls-no-cert-required.js @@ -28,10 +28,15 @@ const assert = require('assert'); const tls = require('tls'); // Omitting the cert or pfx option to tls.createServer() should not throw. -// AECDH-NULL-SHA is a no-authentication/no-encryption cipher and hence -// doesn't need a certificate. -tls.createServer({ ciphers: 'AECDH-NULL-SHA' }) - .listen(0, common.mustCall(close)); +if (process.features.openssl_is_boringssl) { + // AECDH-NULL-SHA is a no-authentication/no-encryption cipher and hence + // does not need a certificate. BoringSSL does not provide that anonymous + // cipher suite, so only this cipher-specific no-cert case is skipped. + common.printSkipMessage('BoringSSL: skipping anonymous AECDH-NULL-SHA case'); +} else { + tls.createServer({ ciphers: 'AECDH-NULL-SHA' }) + .listen(0, common.mustCall(close)); +} tls.createServer(assert.fail) .listen(0, common.mustCall(close)); diff --git a/test/parallel/test-tls-options-boolean-check.js b/test/parallel/test-tls-options-boolean-check.js index 900a39f0c1cd42..f7dd7bb102f361 100644 --- a/test/parallel/test-tls-options-boolean-check.js +++ b/test/parallel/test-tls-options-boolean-check.js @@ -40,9 +40,23 @@ const keyDataView = toDataView(keyBuff); const certDataView = toDataView(certBuff); const caArrDataView = toDataView(caCert); +function filterBoringSSLKeyCertArrayCases(options, setName) { + if (!process.features.openssl_is_boringssl) + return options; + + // The array-valued cases exercise multi-identity key/cert handling. + // BoringSSL may reject those cases with backend key/cert mismatch errors + // before the boolean/type validation this test is targeting. Keep the scalar + // cases so tls.createServer() option type validation is still covered. + common.printSkipMessage( + `BoringSSL: skipping ${setName} key/cert array cases`); + return options.filter(([key, cert]) => !Array.isArray(key) && + !Array.isArray(cert)); +} + // Checks to ensure tls.createServer doesn't throw an error // Format ['key', 'cert'] -[ +const validOptions = [ [keyBuff, certBuff], [false, certBuff], [keyBuff, false], @@ -62,13 +76,16 @@ const caArrDataView = toDataView(caCert); [false, [certStr, certStr2]], [[{ pem: keyBuff }], false], [[{ pem: keyBuff }, { pem: keyBuff }], false], -].forEach(([key, cert]) => { - tls.createServer({ key, cert }); -}); +]; + +filterBoringSSLKeyCertArrayCases(validOptions, 'valid') + .forEach(([key, cert]) => { + tls.createServer({ key, cert }); + }); // Checks to ensure tls.createServer predictably throws an error // Format ['key', 'cert', 'expected message'] -[ +const invalidKeyOptions = [ [true, certBuff], [true, certStr], [true, certArrBuff], @@ -80,7 +97,10 @@ const caArrDataView = toDataView(caCert); [[true, keyStr2], [certStr, certStr2], 0], [[true, false], [certBuff, certBuff2], 0], [true, [certBuff, certBuff2]], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidKeyOptions, 'invalid key')) { const val = index === undefined ? key : key[index]; assert.throws(() => { tls.createServer({ key, cert }); @@ -91,9 +111,9 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} -[ +const invalidCertOptions = [ [keyBuff, true], [keyStr, true], [keyArrBuff, true], @@ -106,7 +126,10 @@ const caArrDataView = toDataView(caCert); [[keyStr, keyStr2], [certStr, true], 1], [[keyStr, keyStr2], [true, false], 0], [[keyStr, keyStr2], true], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidCertOptions, 'invalid cert')) { const val = index === undefined ? cert : cert[index]; assert.throws(() => { tls.createServer({ key, cert }); @@ -117,7 +140,7 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} // Checks to ensure tls.createServer works with the CA parameter // Format ['key', 'cert', 'ca'] diff --git a/test/parallel/test-tls-passphrase.js b/test/parallel/test-tls-passphrase.js index 8d802400f6ee3b..4372da249bb509 100644 --- a/test/parallel/test-tls-passphrase.js +++ b/test/parallel/test-tls-passphrase.js @@ -223,7 +223,7 @@ server.listen(0, common.mustCall(function() { }, onSecureConnect()); })).unref(); -const errMessageDecrypt = /bad decrypt/; +const errMessageDecrypt = /bad[ _]decrypt/i; // Missing passphrase assert.throws(function() { diff --git a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js index 881215672ecd0d..cdeb9f3b31f8fe 100644 --- a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js +++ b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js @@ -14,6 +14,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const assert = require('assert'); const { describe, it } = require('node:test'); const tls = require('tls'); diff --git a/test/parallel/test-tls-psk-circuit.js b/test/parallel/test-tls-psk-circuit.js index bdf9c86c26a7b6..c9c93d53350165 100644 --- a/test/parallel/test-tls-psk-circuit.js +++ b/test/parallel/test-tls-psk-circuit.js @@ -5,6 +5,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); diff --git a/test/parallel/test-tls-psk-server.js b/test/parallel/test-tls-psk-server.js index af038493469880..692550fc1c198b 100644 --- a/test/parallel/test-tls-psk-server.js +++ b/test/parallel/test-tls-psk-server.js @@ -5,6 +5,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const { opensslCli } = require('../common/crypto'); if (!opensslCli) { diff --git a/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js b/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js index 9f4458e0a7d671..cca22067a0fe19 100644 --- a/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js +++ b/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js @@ -4,6 +4,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').assertOpenSSLSecurityLevelsUnsupported(); + return; +} + const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js b/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js index 2fb43b9cbbf87a..9c30989af0afb3 100644 --- a/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js +++ b/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js @@ -22,7 +22,7 @@ const server = tls.createServer({}) 'Instance of Error should be passed to error handler'); assert.match( e.message, - /SSL routines:[^:]*:wrong version number/, + /SSL routines:[^:]*:wrong[ _]version[ _]number/i, ); server.close(); diff --git a/test/parallel/test-tls-server-verify.js b/test/parallel/test-tls-server-verify.js index 94f372d37a3b1f..439e321310305a 100644 --- a/test/parallel/test-tls-server-verify.js +++ b/test/parallel/test-tls-server-verify.js @@ -47,7 +47,7 @@ const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = const tls = require('tls'); const fixtures = require('../common/fixtures'); -const testCases = +let testCases = [{ title: 'Do not request certs. Everyone is unauthorized.', requestCert: false, rejectUnauthorized: false, @@ -125,6 +125,15 @@ const testCases = ] }, ]; +if (process.features.openssl_is_boringssl) { + // Remove the delayed client-certificate verification case. It depends on TLS + // renegotiation to request a client certificate after the initial handshake, + // but BoringSSL does not support caller-initiated renegotiation. + common.printSkipMessage( + 'BoringSSL: skipping renegotiated client certificate verification case'); + testCases = testCases.filter((tcase) => !tcase.renegotiate); +} + function filenamePEM(n) { return fixtures.path('keys', `${n}.pem`); } diff --git a/test/parallel/test-tls-session-cache.js b/test/parallel/test-tls-session-cache.js index aaf9c2c03c83e9..ae560e567980c9 100644 --- a/test/parallel/test-tls-session-cache.js +++ b/test/parallel/test-tls-session-cache.js @@ -37,6 +37,7 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); const { spawn } = require('child_process'); +const isBoringSSL = process.features.openssl_is_boringssl; doTest({ tickets: false }, function() { doTest({ tickets: true }, function() { @@ -56,7 +57,9 @@ function doTest(testOptions, callback) { requestCert: true, rejectUnauthorized: false, secureProtocol: 'TLS_method', - ciphers: 'RSA@SECLEVEL=0' + // BoringSSL supports the RSA cipher selector, but not OpenSSL's + // cipher-string policy command syntax. + ciphers: isBoringSSL ? 'RSA' : 'RSA@SECLEVEL=0' }; let requestCount = 0; let resumeCount = 0; @@ -105,7 +108,7 @@ function doTest(testOptions, callback) { server.listen(0, common.mustCall(function() { const args = [ 's_client', - '-tls1', + isBoringSSL ? '-tls1_2' : '-tls1', '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `localhost:${this.address().port}`, '-servername', 'ohgod', diff --git a/test/parallel/test-tls-set-ciphers-error.js b/test/parallel/test-tls-set-ciphers-error.js index 3cfc8c391bf7d5..b79bd512ffe1db 100644 --- a/test/parallel/test-tls-set-ciphers-error.js +++ b/test/parallel/test-tls-set-ciphers-error.js @@ -21,8 +21,12 @@ const { hasOpenSSL } = require('../common/crypto'); assert.throws(() => tls.createServer(options, common.mustNotCall()), /no[_ ]cipher[_ ]match/i); options.ciphers = 'TLS_not_a_cipher'; - assert.throws(() => tls.createServer(options, common.mustNotCall()), - /no[_ ]cipher[_ ]match/i); + if (process.features.openssl_is_boringssl) { + tls.createServer(options).close(); + } else { + assert.throws(() => tls.createServer(options, common.mustNotCall()), + /no[_ ]cipher[_ ]match/i); + } } // Cipher name matching is case-sensitive prior to OpenSSL 4.0, and diff --git a/test/parallel/test-tls-set-default-ca-certificates-recovery.js b/test/parallel/test-tls-set-default-ca-certificates-recovery.js index e3eb0e84149ae8..ea6f98d5686e03 100644 --- a/test/parallel/test-tls-set-default-ca-certificates-recovery.js +++ b/test/parallel/test-tls-set-default-ca-certificates-recovery.js @@ -27,7 +27,9 @@ function testRecovery(expectedCerts) { { const invalidCert = '-----BEGIN CERTIFICATE-----\nvalid cert content\n-----END CERTIFICATE-----'; assert.throws(() => tls.setDefaultCACertificates([fixtureCert, invalidCert]), { - code: 'ERR_OSSL_PEM_ASN1_LIB', + code: process.features.openssl_is_boringssl ? + 'ERR_OSSL_PEM_ASN.1_ENCODING_ROUTINES' : + 'ERR_OSSL_PEM_ASN1_LIB', }); assertEqualCerts(tls.getCACertificates('default'), expectedCerts); } diff --git a/test/parallel/test-tls-set-sigalgs.js b/test/parallel/test-tls-set-sigalgs.js index 1bce814f3e8604..e1bf8b93f8a342 100644 --- a/test/parallel/test-tls-set-sigalgs.js +++ b/test/parallel/test-tls-set-sigalgs.js @@ -39,9 +39,14 @@ function test(csigalgs, ssigalgs, shared_sigalgs, cerr, serr) { assert.ifError(pair.client.err); assert(pair.server.conn); assert(pair.client.conn); + // BoringSSL's OpenSSL-compatible SSL_get_shared_sigalgs() API always + // returns zero, so a successful handshake still reports an empty list. + const expectedSharedSigalgs = process.features.openssl_is_boringssl ? + [] : + shared_sigalgs; assert.deepStrictEqual( pair.server.conn.getSharedSigalgs(), - shared_sigalgs + expectedSharedSigalgs ); } else { if (serr) { @@ -69,10 +74,13 @@ test('RSA-PSS+SHA256:RSA-PSS+SHA512:ECDSA+SHA256', const handshakeErr = hasOpenSSL(4, 0) ? 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; +const noSharedSigalgsErr = process.features.openssl_is_boringssl ? + 'ERR_SSL_NO_COMMON_SIGNATURE_ALGORITHMS' : + 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'; test('RSA-PSS+SHA384', 'ECDSA+SHA256', undefined, handshakeErr, - 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'); + noSharedSigalgsErr); test('RSA-PSS+SHA384:ECDSA+SHA256', 'ECDSA+SHA384:RSA-PSS+SHA256', undefined, handshakeErr, - 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'); + noSharedSigalgsErr); diff --git a/test/parallel/test-tls-socket-failed-handshake-emits-error.js b/test/parallel/test-tls-socket-failed-handshake-emits-error.js index c88f0c3a1855f2..c64d4ad4aabe8d 100644 --- a/test/parallel/test-tls-socket-failed-handshake-emits-error.js +++ b/test/parallel/test-tls-socket-failed-handshake-emits-error.js @@ -22,7 +22,7 @@ const server = net.createServer(common.mustCall((c) => { 'Instance of Error should be passed to error handler'); assert.match( e.message, - /SSL routines:[^:]*:wrong version number/, + /SSL routines:[^:]*:wrong[ _]version[ _]number/i, ); })); diff --git a/test/parallel/test-tls-ticket-cluster.js b/test/parallel/test-tls-ticket-cluster.js index 2ed4abb93c8d47..f183b53f24c0b9 100644 --- a/test/parallel/test-tls-ticket-cluster.js +++ b/test/parallel/test-tls-ticket-cluster.js @@ -24,6 +24,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); + return; +} + const assert = require('assert'); const tls = require('tls'); const cluster = require('cluster'); diff --git a/test/parallel/test-tls-ticket.js b/test/parallel/test-tls-ticket.js index 0a77e52fb275cd..8316f5e8da8d8f 100644 --- a/test/parallel/test-tls-ticket.js +++ b/test/parallel/test-tls-ticket.js @@ -30,6 +30,12 @@ const net = require('net'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); +if (process.features.openssl_is_boringssl && + tls.DEFAULT_MAX_VERSION !== 'TLSv1.2') { + require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); + return; +} + const keys = crypto.randomBytes(48); const serverLog = []; const ticketLog = []; diff --git a/test/parallel/test-x509-escaping.js b/test/parallel/test-x509-escaping.js index a5937a09cb1535..ab91e334555669 100644 --- a/test/parallel/test-x509-escaping.js +++ b/test/parallel/test-x509-escaping.js @@ -438,7 +438,9 @@ const { hasOpenSSL3 } = require('../common/crypto'); const cert = fixtures.readKey('incorrect_san_correct_subject-cert.pem'); // The hostname is the CN, but not a SAN entry. - const servername = process.features.openssl_is_boringssl ? undefined : 'good.example.com'; + const servername = 'good.example.com'; + const cnFallback = process.features.openssl_is_boringssl ? undefined : + servername; const certX509 = new X509Certificate(cert); assert.strictEqual(certX509.subject, `CN=${servername}`); assert.strictEqual(certX509.subjectAltName, 'DNS:evil.example.com'); @@ -448,7 +450,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); assert.strictEqual(certX509.checkHost(servername, { subject: 'default' }), undefined); assert.strictEqual(certX509.checkHost(servername, { subject: 'always' }), - servername); + cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'never' }), undefined); @@ -483,11 +485,13 @@ const { hasOpenSSL3 } = require('../common/crypto'); assert.strictEqual(certX509.subjectAltName, 'IP Address:1.2.3.4'); // The newer X509Certificate API allows customizing this behavior: - assert.strictEqual(certX509.checkHost(servername), servername); + const cnFallback = process.features.openssl_is_boringssl ? undefined : + servername; + assert.strictEqual(certX509.checkHost(servername), cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'default' }), - servername); + cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'always' }), - servername); + cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'never' }), undefined); diff --git a/test/sequential/test-tls-connect.js b/test/sequential/test-tls-connect.js index 189b9afa6352bb..ca8a1d8128554e 100644 --- a/test/sequential/test-tls-connect.js +++ b/test/sequential/test-tls-connect.js @@ -57,5 +57,5 @@ const tls = require('tls'); port: common.PORT, ciphers: 'rick-128-roll', }, common.mustNotCall()); - }, /no cipher match/i); + }, /no[_ ]cipher[_ ]match/i); } diff --git a/test/sequential/test-tls-psk-client.js b/test/sequential/test-tls-psk-client.js index 65e628a6f4e0eb..2eb6228f79f265 100644 --- a/test/sequential/test-tls-psk-client.js +++ b/test/sequential/test-tls-psk-client.js @@ -5,6 +5,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const { opensslCli } = require('../common/crypto'); if (!opensslCli) { From e34bb1c13547f367cca72fd1a3ac9c8264b7bb63 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 5 May 2026 16:16:48 +0200 Subject: [PATCH 10/11] tools: add boringssl to tools/nix/openssl-matrix.nix --- tools/dep_updaters/update-nixpkgs-pin.sh | 10 +++++----- tools/nix/openssl-matrix.nix | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/dep_updaters/update-nixpkgs-pin.sh b/tools/dep_updaters/update-nixpkgs-pin.sh index 97bcd878181c7b..25cd1e463394c2 100755 --- a/tools/dep_updaters/update-nixpkgs-pin.sh +++ b/tools/dep_updaters/update-nixpkgs-pin.sh @@ -29,16 +29,16 @@ mv "$TMP_FILE" "$NIXPKGS_PIN_FILE" nix-instantiate -I "nixpkgs=$NIXPKGS_PIN_FILE" --eval --strict --json -E " let pkgs = import {}; + opensslAttrs = builtins.filter + (n: builtins.match \"openssl_[0-9]+(_[0-9]+)?\" n != null) + (builtins.attrNames pkgs); + extraMatrixAttrs = [ \"boringssl\" ]; attrs = builtins.filter (n: let t = builtins.tryEval pkgs.\${n}; in t.success && (builtins.tryEval t.value.version).success ) - ( - builtins.filter - (n: builtins.match \"openssl_[0-9]+(_[0-9]+)?\" n != null) - (builtins.attrNames pkgs) - ); + (opensslAttrs ++ extraMatrixAttrs); in { inherit attrs; diff --git a/tools/nix/openssl-matrix.nix b/tools/nix/openssl-matrix.nix index 3f9476acd7f7e0..5e85aae3f31ddd 100644 --- a/tools/nix/openssl-matrix.nix +++ b/tools/nix/openssl-matrix.nix @@ -11,5 +11,6 @@ openssl_3_5 openssl_3_6 openssl_4_0 + boringssl ; } From 078d5eda13ee8ac081b4da5778a3705e88ed8b9b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 5 May 2026 19:12:24 +0200 Subject: [PATCH 11/11] crypto: refactor PQC raw seed handling Factor ML-DSA and ML-KEM seed sizes and seed import/export helpers into shared helpers. Keep the provider-specific OpenSSL and BoringSSL paths contained in those helpers. Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 152 ++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 85 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index d51cff7e2c4e24..3232df9a3d15be 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -2100,69 +2100,99 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( } #if OPENSSL_WITH_PQC -EVPKeyPointer EVPKeyPointer::NewRawSeed( - int id, const Buffer& data) { - if (id == 0) return {}; +namespace { +constexpr size_t kPqcMlDsaSeedSize = 32; +constexpr size_t kPqcMlKemSeedSize = 64; -#ifdef OPENSSL_IS_BORINGSSL - // BoringSSL exposes seed-based construction via EVP_PKEY_from_private_seed, - // which needs an |EVP_PKEY_ALG*| rather than a NID. - const EVP_PKEY_ALG* alg = nullptr; +size_t GetPqcSeedSize(int id) { switch (id) { case EVP_PKEY_ML_DSA_44: - alg = EVP_pkey_ml_dsa_44(); - break; case EVP_PKEY_ML_DSA_65: - alg = EVP_pkey_ml_dsa_65(); - break; case EVP_PKEY_ML_DSA_87: - alg = EVP_pkey_ml_dsa_87(); - break; + return kPqcMlDsaSeedSize; +#if OPENSSL_WITH_PQC_ML_KEM_512 + case EVP_PKEY_ML_KEM_512: +#endif case EVP_PKEY_ML_KEM_768: - alg = EVP_pkey_ml_kem_768(); - break; case EVP_PKEY_ML_KEM_1024: - alg = EVP_pkey_ml_kem_1024(); - break; + return kPqcMlKemSeedSize; default: - return {}; + unreachable(); + } +} + +#if OPENSSL_WITH_BORINGSSL_PQC +const EVP_PKEY_ALG* GetPqcSeedAlg(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + return EVP_pkey_ml_dsa_44(); + case EVP_PKEY_ML_DSA_65: + return EVP_pkey_ml_dsa_65(); + case EVP_PKEY_ML_DSA_87: + return EVP_pkey_ml_dsa_87(); + case EVP_PKEY_ML_KEM_768: + return EVP_pkey_ml_kem_768(); + case EVP_PKEY_ML_KEM_1024: + return EVP_pkey_ml_kem_1024(); + default: + unreachable(); } - return EVPKeyPointer(EVP_PKEY_from_private_seed(alg, data.data, data.len)); +} #else - // ML-DSA and ML-KEM seeds use distinct OSSL_PARAM keys. - const char* param_name; +const char* GetPqcSeedParamName(int id) { switch (id) { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: - param_name = OSSL_PKEY_PARAM_ML_DSA_SEED; - break; + return OSSL_PKEY_PARAM_ML_DSA_SEED; case EVP_PKEY_ML_KEM_512: case EVP_PKEY_ML_KEM_768: case EVP_PKEY_ML_KEM_1024: - param_name = OSSL_PKEY_PARAM_ML_KEM_SEED; - break; + return OSSL_PKEY_PARAM_ML_KEM_SEED; default: - return {}; + unreachable(); } +} +#endif +EVPKeyPointer NewPqcKeyFromSeed(int id, + const Buffer& data) { +#if OPENSSL_WITH_BORINGSSL_PQC + return EVPKeyPointer( + EVP_PKEY_from_private_seed(GetPqcSeedAlg(id), data.data, data.len)); +#else OSSL_PARAM params[] = { - OSSL_PARAM_construct_octet_string( - param_name, const_cast(data.data), data.len), + OSSL_PARAM_construct_octet_string(GetPqcSeedParamName(id), + const_cast(data.data), + data.len), OSSL_PARAM_END}; - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); - if (ctx == nullptr) return {}; + auto ctx = EVPKeyCtxPointer::NewFromID(id); + if (!ctx) return {}; EVP_PKEY* pkey = nullptr; - if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 || - EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { - EVP_PKEY_CTX_free(ctx); + if (EVP_PKEY_fromdata_init(ctx.get()) <= 0 || + EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { return {}; } - return EVPKeyPointer(pkey); -#endif // OPENSSL_IS_BORINGSSL +#endif +} + +bool GetPqcSeed(EVP_PKEY* pkey, int id, const Buffer& out) { + size_t len = out.len; +#if OPENSSL_WITH_BORINGSSL_PQC + return EVP_PKEY_get_private_seed(pkey, out.data, &len) == 1; +#else + return EVP_PKEY_get_octet_string_param( + pkey, GetPqcSeedParamName(id), out.data, out.len, &len) == 1; +#endif +} +} // namespace + +EVPKeyPointer EVPKeyPointer::NewRawSeed( + int id, const Buffer& data) { + return NewPqcKeyFromSeed(id, data); } #endif @@ -2290,62 +2320,14 @@ DataPointer EVPKeyPointer::rawPublicKey() const { DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; -#ifdef OPENSSL_IS_BORINGSSL - size_t seed_len; - switch (id()) { - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - seed_len = 32; // ML-DSA uses 32-byte seeds - break; - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - seed_len = 64; // ML-KEM uses 64-byte seeds - break; - default: - return {}; - } - - if (auto data = DataPointer::Alloc(seed_len)) { - const Buffer buf = data; - size_t len = data.size(); - if (EVP_PKEY_get_private_seed(get(), buf.data, &len) != 1) return {}; - return data; - } - return {}; -#else - // Determine seed length and parameter name based on key type - size_t seed_len; - const char* param_name; - - switch (id()) { - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - seed_len = 32; // ML-DSA uses 32-byte seeds - param_name = OSSL_PKEY_PARAM_ML_DSA_SEED; - break; - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - seed_len = 64; // ML-KEM uses 64-byte seeds - param_name = OSSL_PKEY_PARAM_ML_KEM_SEED; - break; - default: - unreachable(); - } + const size_t seed_len = GetPqcSeedSize(id()); if (auto data = DataPointer::Alloc(seed_len)) { const Buffer buf = data; - size_t len = data.size(); - - if (EVP_PKEY_get_octet_string_param( - get(), param_name, buf.data, len, &seed_len) != 1) - return {}; + if (!GetPqcSeed(get(), id(), buf)) return {}; return data; } return {}; -#endif // OPENSSL_IS_BORINGSSL } #endif