From 68e8b2c49296d34bd74391527dc25908a694ef8a Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 5 Jan 2024 23:16:27 +0100 Subject: [PATCH] crypto: use EVP_MD_fetch and cache EVP_MD for hashes On OpenSSL 3, migrate from EVP_get_digestbyname() to EVP_MD_fetch() to get the implementation and use a per-Environment cache for it. The EVP_MDs are freed during Environment cleanup. Drive-by: declare the smart pointer for EVP_MD_CTX as EVPMDCtxPointer instead of EVPMDPointer to avoid confusion with EVP_MD pointers. PR-URL: https://github.com/nodejs/node/pull/51034 Refs: https://www.openssl.org/docs/man3.0/man7/crypto.html#Explicit-fetching Refs: https://github.com/nodejs/performance/issues/136 Reviewed-By: James M Snell --- lib/internal/crypto/hash.js | 10 +- lib/internal/crypto/util.js | 27 ++++++ src/crypto/README.md | 2 +- src/crypto/crypto_hash.cc | 184 ++++++++++++++++++++++++++++++++---- src/crypto/crypto_hash.h | 3 +- src/crypto/crypto_sig.cc | 8 +- src/crypto/crypto_sig.h | 2 +- src/crypto/crypto_util.h | 2 +- src/env.h | 14 +++ 9 files changed, 225 insertions(+), 27 deletions(-) diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 4ed97034f612fb..f3072d61dd0be0 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -19,6 +19,8 @@ const { normalizeHashName, validateMaxBufferLength, kHandle, + getCachedHashId, + getHashCache, } = require('internal/crypto/util'); const { @@ -59,13 +61,17 @@ const kFinalized = Symbol('kFinalized'); function Hash(algorithm, options) { if (!new.target) return new Hash(algorithm, options); - if (!(algorithm instanceof _Hash)) + const isCopy = algorithm instanceof _Hash; + if (!isCopy) validateString(algorithm, 'algorithm'); const xofLen = typeof options === 'object' && options !== null ? options.outputLength : undefined; if (xofLen !== undefined) validateUint32(xofLen, 'options.outputLength'); - this[kHandle] = new _Hash(algorithm, xofLen); + // Lookup the cached ID from JS land because it's faster than decoding + // the string in C++ land. + const algorithmId = isCopy ? -1 : getCachedHashId(algorithm); + this[kHandle] = new _Hash(algorithm, xofLen, algorithmId, getHashCache()); this[kState] = { [kFinalized]: false, }; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 45a236a1130249..d756b067798a57 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -29,6 +29,7 @@ const { getHashes: _getHashes, setEngine: _setEngine, secureHeapUsed: _secureHeapUsed, + getCachedAliases, } = internalBinding('crypto'); const { getOptionValue } = require('internal/options'); @@ -66,6 +67,13 @@ const { lazyDOMException, } = require('internal/util'); +const { + namespace: { + isBuildingSnapshot, + addSerializeCallback, + }, +} = require('internal/v8/startup_snapshot'); + const { isDataView, isArrayBufferView, @@ -87,6 +95,23 @@ function toBuf(val, encoding) { return val; } +let _hashCache; +function getHashCache() { + if (_hashCache === undefined) { + _hashCache = getCachedAliases(); + if (isBuildingSnapshot()) { + // For dynamic linking, clear the map. + addSerializeCallback(() => { _hashCache = undefined; }); + } + } + return _hashCache; +} + +function getCachedHashId(algorithm) { + const result = getHashCache()[algorithm]; + return result === undefined ? -1 : result; +} + const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers())); const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes())); const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); @@ -574,4 +599,6 @@ module.exports = { getStringOption, getUsagesUnion, secureHeapUsed, + getCachedHashId, + getHashCache, }; diff --git a/src/crypto/README.md b/src/crypto/README.md index 51659f1f363b3b..e761f156d189d6 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -79,7 +79,7 @@ using SSLPointer = DeleteFnPtr; using PKCS8Pointer = DeleteFnPtr; using EVPKeyPointer = DeleteFnPtr; using EVPKeyCtxPointer = DeleteFnPtr; -using EVPMDPointer = DeleteFnPtr; +using EVPMDCtxPointer = DeleteFnPtr; using RSAPointer = DeleteFnPtr; using ECPointer = DeleteFnPtr; using BignumPointer = DeleteFnPtr; diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 12dd5de3bd7de7..2a709126544833 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -14,11 +14,13 @@ namespace node { using v8::Context; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; +using v8::Int32; using v8::Isolate; using v8::Just; using v8::Local; using v8::Maybe; using v8::MaybeLocal; +using v8::Name; using v8::Nothing; using v8::Object; using v8::Uint32; @@ -34,22 +36,170 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0); } +#if OPENSSL_VERSION_MAJOR >= 3 +void PushAliases(const char* name, void* data) { + static_cast*>(data)->push_back(name); +} + +EVP_MD* GetCachedMDByID(Environment* env, size_t id) { + CHECK_LT(id, env->evp_md_cache.size()); + EVP_MD* result = env->evp_md_cache[id].get(); + CHECK_NOT_NULL(result); + return result; +} + +struct MaybeCachedMD { + EVP_MD* explicit_md = nullptr; + const EVP_MD* implicit_md = nullptr; + int32_t cache_id = -1; +}; + +MaybeCachedMD FetchAndMaybeCacheMD(Environment* env, const char* search_name) { + const EVP_MD* implicit_md = EVP_get_digestbyname(search_name); + if (!implicit_md) return {nullptr, nullptr, -1}; + + const char* real_name = EVP_MD_get0_name(implicit_md); + if (!real_name) return {nullptr, implicit_md, -1}; + + auto it = env->alias_to_md_id_map.find(real_name); + if (it != env->alias_to_md_id_map.end()) { + size_t id = it->second; + return {GetCachedMDByID(env, id), implicit_md, static_cast(id)}; + } + + // EVP_*_fetch() does not support alias names, so we need to pass it the + // real/original algorithm name. + // We use EVP_*_fetch() as a filter here because it will only return an + // instance if the algorithm is supported by the public OpenSSL APIs (some + // algorithms are used internally by OpenSSL and are also passed to this + // callback). + EVP_MD* explicit_md = EVP_MD_fetch(nullptr, real_name, nullptr); + if (!explicit_md) return {nullptr, implicit_md, -1}; + + // Cache the EVP_MD* fetched. + env->evp_md_cache.emplace_back(explicit_md); + size_t id = env->evp_md_cache.size() - 1; + + // Add all the aliases to the map to speed up next lookup. + std::vector aliases; + EVP_MD_names_do_all(explicit_md, PushAliases, &aliases); + for (const auto& alias : aliases) { + env->alias_to_md_id_map.emplace(alias, id); + } + env->alias_to_md_id_map.emplace(search_name, id); + + return {explicit_md, implicit_md, static_cast(id)}; +} + +void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md, + const char* from, + const char* to, + void* arg) { + if (!from) return; + Environment* env = static_cast(arg); + auto result = FetchAndMaybeCacheMD(env, from); + if (result.explicit_md) { + env->supported_hash_algorithms.push_back(from); + } +} + +#else +void SaveSupportedHashAlgorithms(const EVP_MD* md, + const char* from, + const char* to, + void* arg) { + if (!from) return; + Environment* env = static_cast(arg); + env->supported_hash_algorithms.push_back(from); +} +#endif // OPENSSL_VERSION_MAJOR >= 3 + +const std::vector& GetSupportedHashAlgorithms(Environment* env) { + if (env->supported_hash_algorithms.empty()) { + MarkPopErrorOnReturn mark_pop_error_on_return; +#if 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); +#else + EVP_MD_do_all_sorted(SaveSupportedHashAlgorithms, env); +#endif + } + return env->supported_hash_algorithms; +} + void Hash::GetHashes(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - MarkPopErrorOnReturn mark_pop_error_on_return; - CipherPushContext ctx(env); - EVP_MD_do_all_sorted( + Local context = args.GetIsolate()->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + const std::vector& results = GetSupportedHashAlgorithms(env); + + Local ret; + if (ToV8Value(context, results).ToLocal(&ret)) { + args.GetReturnValue().Set(ret); + } +} + +void Hash::GetCachedAliases(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = args.GetIsolate()->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + std::vector> names; + std::vector> values; + size_t size = env->alias_to_md_id_map.size(); #if OPENSSL_VERSION_MAJOR >= 3 - array_push_back, + names.reserve(size); + values.reserve(size); + for (auto& [alias, id] : env->alias_to_md_id_map) { + names.push_back(OneByteString(isolate, alias.c_str(), alias.size())); + values.push_back(v8::Uint32::New(isolate, id)); + } #else - array_push_back, + CHECK(env->alias_to_md_id_map.empty()); +#endif + Local prototype = v8::Null(isolate); + Local result = + Object::New(isolate, prototype, names.data(), values.data(), size); + args.GetReturnValue().Set(result); +} + +const EVP_MD* GetDigestImplementation(Environment* env, + Local algorithm, + Local cache_id_val, + Local algorithm_cache) { + CHECK(algorithm->IsString()); + CHECK(cache_id_val->IsInt32()); + CHECK(algorithm_cache->IsObject()); + +#if OPENSSL_VERSION_MAJOR >= 3 + int32_t cache_id = cache_id_val.As()->Value(); + if (cache_id != -1) { // Alias already cached, return the cached EVP_MD*. + return GetCachedMDByID(env, cache_id); + } + + // Only decode the algorithm when we don't have it cached to avoid + // unnecessary overhead. + Isolate* isolate = env->isolate(); + Utf8Value utf8(isolate, algorithm); + + auto result = FetchAndMaybeCacheMD(env, *utf8); + if (result.cache_id != -1) { + // Add the alias to both C++ side and JS side to speedup the lookup + // next time. + env->alias_to_md_id_map.emplace(*utf8, result.cache_id); + if (algorithm_cache.As() + ->Set(isolate->GetCurrentContext(), + algorithm, + v8::Int32::New(isolate, result.cache_id)) + .IsNothing()) { + return nullptr; + } + } + + return result.explicit_md ? result.explicit_md : result.implicit_md; +#else + Utf8Value utf8(env->isolate(), algorithm); + return EVP_get_digestbyname(*utf8); #endif - &ctx); - args.GetReturnValue().Set(ctx.ToJSArray()); } void Hash::Initialize(Environment* env, Local target) { @@ -65,6 +215,7 @@ void Hash::Initialize(Environment* env, Local target) { SetConstructorFunction(context, target, "Hash", t); SetMethodNoSideEffect(context, target, "getHashes", GetHashes); + SetMethodNoSideEffect(context, target, "getCachedAliases", GetCachedAliases); HashJob::Initialize(env, target); @@ -77,24 +228,24 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(HashUpdate); registry->Register(HashDigest); registry->Register(GetHashes); + registry->Register(GetCachedAliases); HashJob::RegisterExternalReferences(registry); registry->Register(InternalVerifyIntegrity); } +// new Hash(algorithm, algorithmId, xofLen, algorithmCache) void Hash::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); const Hash* orig = nullptr; const EVP_MD* md = nullptr; - if (args[0]->IsObject()) { ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As()); md = EVP_MD_CTX_md(orig->mdctx_.get()); } else { - const Utf8Value hash_type(env->isolate(), args[0]); - md = EVP_get_digestbyname(*hash_type); + md = GetDigestImplementation(env, args[0], args[2], args[3]); } Maybe xof_md_len = Nothing(); @@ -284,7 +435,7 @@ bool HashTraits::DeriveBits( Environment* env, const HashConfig& params, ByteSource* out) { - EVPMDPointer ctx(EVP_MD_CTX_new()); + EVPMDCtxPointer ctx(EVP_MD_CTX_new()); if (UNLIKELY(!ctx || EVP_DigestInit_ex(ctx.get(), params.digest, nullptr) <= 0 || @@ -357,6 +508,5 @@ void InternalVerifyIntegrity(const v8::FunctionCallbackInfo& args) { args.GetReturnValue().Set(rc.FromMaybe(Local())); } } - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 2d17c3510ed214..a90acc895b97b2 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -25,6 +25,7 @@ class Hash final : public BaseObject { bool HashUpdate(const char* data, size_t len); static void GetHashes(const v8::FunctionCallbackInfo& args); + static void GetCachedAliases(const v8::FunctionCallbackInfo& args); protected: static void New(const v8::FunctionCallbackInfo& args); @@ -34,7 +35,7 @@ class Hash final : public BaseObject { Hash(Environment* env, v8::Local wrap); private: - EVPMDPointer mdctx_ {}; + EVPMDCtxPointer mdctx_{}; unsigned int md_len_ = 0; ByteSource digest_; }; diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index e38bc9eec083ad..5b1acf2677d1b6 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -73,7 +73,7 @@ bool ApplyRSAOptions(const ManagedEVPPKey& pkey, } std::unique_ptr Node_SignFinal(Environment* env, - EVPMDPointer&& mdctx, + EVPMDCtxPointer&& mdctx, const ManagedEVPPKey& pkey, int padding, Maybe pss_salt_len) { @@ -391,7 +391,7 @@ Sign::SignResult Sign::SignFinal( if (!mdctx_) return SignResult(kSignNotInitialised); - EVPMDPointer mdctx = std::move(mdctx_); + EVPMDCtxPointer mdctx = std::move(mdctx_); if (!ValidateDSAParameters(pkey.get())) return SignResult(kSignPrivateKey); @@ -511,7 +511,7 @@ SignBase::Error Verify::VerifyFinal(const ManagedEVPPKey& pkey, unsigned char m[EVP_MAX_MD_SIZE]; unsigned int m_len; *verify_result = false; - EVPMDPointer mdctx = std::move(mdctx_); + EVPMDCtxPointer mdctx = std::move(mdctx_); if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) return kSignPublicKey; @@ -696,7 +696,7 @@ bool SignTraits::DeriveBits( const SignConfiguration& params, ByteSource* out) { ClearErrorOnReturn clear_error_on_return; - EVPMDPointer context(EVP_MD_CTX_new()); + EVPMDCtxPointer context(EVP_MD_CTX_new()); EVP_PKEY_CTX* ctx = nullptr; switch (params.mode) { diff --git a/src/crypto/crypto_sig.h b/src/crypto/crypto_sig.h index 1a4cda42272e51..633201473e4645 100644 --- a/src/crypto/crypto_sig.h +++ b/src/crypto/crypto_sig.h @@ -42,7 +42,7 @@ class SignBase : public BaseObject { SET_SELF_SIZE(SignBase) protected: - EVPMDPointer mdctx_; + EVPMDCtxPointer mdctx_; }; class Sign : public SignBase { diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index ac231f59907918..0ae2946e5e5884 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -62,7 +62,7 @@ using SSLPointer = DeleteFnPtr; using PKCS8Pointer = DeleteFnPtr; using EVPKeyPointer = DeleteFnPtr; using EVPKeyCtxPointer = DeleteFnPtr; -using EVPMDPointer = DeleteFnPtr; +using EVPMDCtxPointer = DeleteFnPtr; using RSAPointer = DeleteFnPtr; using ECPointer = DeleteFnPtr; using BignumPointer = DeleteFnPtr; diff --git a/src/env.h b/src/env.h index 9a2ac179101210..20671217050e92 100644 --- a/src/env.h +++ b/src/env.h @@ -49,6 +49,10 @@ #include "uv.h" #include "v8.h" +#if HAVE_OPENSSL +#include +#endif + #include #include #include @@ -1028,6 +1032,16 @@ class Environment : public MemoryRetainer { kExitInfoFieldCount }; +#if HAVE_OPENSSL +#if OPENSSL_VERSION_MAJOR >= 3 + // We declare another alias here to avoid having to include crypto_util.h + using EVPMDPointer = DeleteFnPtr; + std::vector evp_md_cache; +#endif // OPENSSL_VERSION_MAJOR >= 3 + std::unordered_map alias_to_md_id_map; + std::vector supported_hash_algorithms; +#endif // HAVE_OPENSSL + private: // V8 has changed the constructor of exceptions, support both APIs before Node // updates to V8 12.1.