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 <jasnell@gmail.com>
This commit is contained in:
parent
59e7444766
commit
57c22e4a22
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -79,7 +79,7 @@ using SSLPointer = DeleteFnPtr<SSL, SSL_free>;
|
||||
using PKCS8Pointer = DeleteFnPtr<PKCS8_PRIV_KEY_INFO, PKCS8_PRIV_KEY_INFO_free>;
|
||||
using EVPKeyPointer = DeleteFnPtr<EVP_PKEY, EVP_PKEY_free>;
|
||||
using EVPKeyCtxPointer = DeleteFnPtr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
|
||||
using EVPMDPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
|
||||
using EVPMDCtxPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
|
||||
using RSAPointer = DeleteFnPtr<RSA, RSA_free>;
|
||||
using ECPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
|
||||
using BignumPointer = DeleteFnPtr<BIGNUM, BN_clear_free>;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
void Hash::GetHashes(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
MarkPopErrorOnReturn mark_pop_error_on_return;
|
||||
CipherPushContext ctx(env);
|
||||
EVP_MD_do_all_sorted(
|
||||
#if OPENSSL_VERSION_MAJOR >= 3
|
||||
array_push_back<EVP_MD,
|
||||
EVP_MD_fetch,
|
||||
EVP_MD_free,
|
||||
EVP_get_digestbyname,
|
||||
EVP_MD_get0_name>,
|
||||
void PushAliases(const char* name, void* data) {
|
||||
static_cast<std::vector<std::string>*>(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<int32_t>(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<std::string> 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<int32_t>(id)};
|
||||
}
|
||||
|
||||
void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md,
|
||||
const char* from,
|
||||
const char* to,
|
||||
void* arg) {
|
||||
if (!from) return;
|
||||
Environment* env = static_cast<Environment*>(arg);
|
||||
auto result = FetchAndMaybeCacheMD(env, from);
|
||||
if (result.explicit_md) {
|
||||
env->supported_hash_algorithms.push_back(from);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
array_push_back<EVP_MD>,
|
||||
void SaveSupportedHashAlgorithms(const EVP_MD* md,
|
||||
const char* from,
|
||||
const char* to,
|
||||
void* arg) {
|
||||
if (!from) return;
|
||||
Environment* env = static_cast<Environment*>(arg);
|
||||
env->supported_hash_algorithms.push_back(from);
|
||||
}
|
||||
#endif // OPENSSL_VERSION_MAJOR >= 3
|
||||
|
||||
const std::vector<std::string>& 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<Value>& args) {
|
||||
Local<Context> context = args.GetIsolate()->GetCurrentContext();
|
||||
Environment* env = Environment::GetCurrent(context);
|
||||
const std::vector<std::string>& results = GetSupportedHashAlgorithms(env);
|
||||
|
||||
Local<Value> ret;
|
||||
if (ToV8Value(context, results).ToLocal(&ret)) {
|
||||
args.GetReturnValue().Set(ret);
|
||||
}
|
||||
}
|
||||
|
||||
void Hash::GetCachedAliases(const FunctionCallbackInfo<Value>& args) {
|
||||
Isolate* isolate = args.GetIsolate();
|
||||
Local<Context> context = args.GetIsolate()->GetCurrentContext();
|
||||
Environment* env = Environment::GetCurrent(context);
|
||||
std::vector<Local<Name>> names;
|
||||
std::vector<Local<Value>> values;
|
||||
size_t size = env->alias_to_md_id_map.size();
|
||||
#if OPENSSL_VERSION_MAJOR >= 3
|
||||
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
|
||||
CHECK(env->alias_to_md_id_map.empty());
|
||||
#endif
|
||||
Local<Value> prototype = v8::Null(isolate);
|
||||
Local<Object> result =
|
||||
Object::New(isolate, prototype, names.data(), values.data(), size);
|
||||
args.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
const EVP_MD* GetDigestImplementation(Environment* env,
|
||||
Local<Value> algorithm,
|
||||
Local<Value> cache_id_val,
|
||||
Local<Value> 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<Int32>()->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<Object>()
|
||||
->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<Object> target) {
|
||||
@ -65,6 +215,7 @@ void Hash::Initialize(Environment* env, Local<Object> 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<Value>& 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<Object>());
|
||||
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<unsigned int> xof_md_len = Nothing<unsigned int>();
|
||||
@ -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<v8::Value>& args) {
|
||||
args.GetReturnValue().Set(rc.FromMaybe(Local<Value>()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crypto
|
||||
} // namespace node
|
||||
|
@ -25,6 +25,7 @@ class Hash final : public BaseObject {
|
||||
bool HashUpdate(const char* data, size_t len);
|
||||
|
||||
static void GetHashes(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void GetCachedAliases(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
protected:
|
||||
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
@ -34,7 +35,7 @@ class Hash final : public BaseObject {
|
||||
Hash(Environment* env, v8::Local<v8::Object> wrap);
|
||||
|
||||
private:
|
||||
EVPMDPointer mdctx_ {};
|
||||
EVPMDCtxPointer mdctx_{};
|
||||
unsigned int md_len_ = 0;
|
||||
ByteSource digest_;
|
||||
};
|
||||
|
@ -73,7 +73,7 @@ bool ApplyRSAOptions(const ManagedEVPPKey& pkey,
|
||||
}
|
||||
|
||||
std::unique_ptr<BackingStore> Node_SignFinal(Environment* env,
|
||||
EVPMDPointer&& mdctx,
|
||||
EVPMDCtxPointer&& mdctx,
|
||||
const ManagedEVPPKey& pkey,
|
||||
int padding,
|
||||
Maybe<int> 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) {
|
||||
|
@ -42,7 +42,7 @@ class SignBase : public BaseObject {
|
||||
SET_SELF_SIZE(SignBase)
|
||||
|
||||
protected:
|
||||
EVPMDPointer mdctx_;
|
||||
EVPMDCtxPointer mdctx_;
|
||||
};
|
||||
|
||||
class Sign : public SignBase {
|
||||
|
@ -62,7 +62,7 @@ using SSLPointer = DeleteFnPtr<SSL, SSL_free>;
|
||||
using PKCS8Pointer = DeleteFnPtr<PKCS8_PRIV_KEY_INFO, PKCS8_PRIV_KEY_INFO_free>;
|
||||
using EVPKeyPointer = DeleteFnPtr<EVP_PKEY, EVP_PKEY_free>;
|
||||
using EVPKeyCtxPointer = DeleteFnPtr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
|
||||
using EVPMDPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
|
||||
using EVPMDCtxPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
|
||||
using RSAPointer = DeleteFnPtr<RSA, RSA_free>;
|
||||
using ECPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
|
||||
using BignumPointer = DeleteFnPtr<BIGNUM, BN_clear_free>;
|
||||
|
14
src/env.h
14
src/env.h
@ -49,6 +49,10 @@
|
||||
#include "uv.h"
|
||||
#include "v8.h"
|
||||
|
||||
#if HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
@ -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<EVP_MD, EVP_MD_free>;
|
||||
std::vector<EVPMDPointer> evp_md_cache;
|
||||
#endif // OPENSSL_VERSION_MAJOR >= 3
|
||||
std::unordered_map<std::string, size_t> alias_to_md_id_map;
|
||||
std::vector<std::string> supported_hash_algorithms;
|
||||
#endif // HAVE_OPENSSL
|
||||
|
||||
private:
|
||||
inline void ThrowError(v8::Local<v8::Value> (*fun)(v8::Local<v8::String>,
|
||||
v8::Local<v8::Value>),
|
||||
|
Loading…
x
Reference in New Issue
Block a user