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:
Joyee Cheung 2024-01-05 23:16:27 +01:00 committed by GitHub
parent 59e7444766
commit 57c22e4a22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 226 additions and 28 deletions

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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>;

View File

@ -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

View File

@ -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_;
};

View File

@ -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) {

View File

@ -42,7 +42,7 @@ class SignBase : public BaseObject {
SET_SELF_SIZE(SignBase)
protected:
EVPMDPointer mdctx_;
EVPMDCtxPointer mdctx_;
};
class Sign : public SignBase {

View File

@ -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>;

View File

@ -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>),