Skip to content

Commit 68e8b2c

Browse files
joyeecheungrichardlau
authored andcommitted
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: #51034 Refs: https://www.openssl.org/docs/man3.0/man7/crypto.html#Explicit-fetching Refs: nodejs/performance#136 Reviewed-By: James M Snell <[email protected]>
1 parent 242139f commit 68e8b2c

File tree

9 files changed

+225
-27
lines changed

9 files changed

+225
-27
lines changed

lib/internal/crypto/hash.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const {
1919
normalizeHashName,
2020
validateMaxBufferLength,
2121
kHandle,
22+
getCachedHashId,
23+
getHashCache,
2224
} = require('internal/crypto/util');
2325

2426
const {
@@ -59,13 +61,17 @@ const kFinalized = Symbol('kFinalized');
5961
function Hash(algorithm, options) {
6062
if (!new.target)
6163
return new Hash(algorithm, options);
62-
if (!(algorithm instanceof _Hash))
64+
const isCopy = algorithm instanceof _Hash;
65+
if (!isCopy)
6366
validateString(algorithm, 'algorithm');
6467
const xofLen = typeof options === 'object' && options !== null ?
6568
options.outputLength : undefined;
6669
if (xofLen !== undefined)
6770
validateUint32(xofLen, 'options.outputLength');
68-
this[kHandle] = new _Hash(algorithm, xofLen);
71+
// Lookup the cached ID from JS land because it's faster than decoding
72+
// the string in C++ land.
73+
const algorithmId = isCopy ? -1 : getCachedHashId(algorithm);
74+
this[kHandle] = new _Hash(algorithm, xofLen, algorithmId, getHashCache());
6975
this[kState] = {
7076
[kFinalized]: false,
7177
};

lib/internal/crypto/util.js

+27
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const {
2929
getHashes: _getHashes,
3030
setEngine: _setEngine,
3131
secureHeapUsed: _secureHeapUsed,
32+
getCachedAliases,
3233
} = internalBinding('crypto');
3334

3435
const { getOptionValue } = require('internal/options');
@@ -66,6 +67,13 @@ const {
6667
lazyDOMException,
6768
} = require('internal/util');
6869

70+
const {
71+
namespace: {
72+
isBuildingSnapshot,
73+
addSerializeCallback,
74+
},
75+
} = require('internal/v8/startup_snapshot');
76+
6977
const {
7078
isDataView,
7179
isArrayBufferView,
@@ -87,6 +95,23 @@ function toBuf(val, encoding) {
8795
return val;
8896
}
8997

98+
let _hashCache;
99+
function getHashCache() {
100+
if (_hashCache === undefined) {
101+
_hashCache = getCachedAliases();
102+
if (isBuildingSnapshot()) {
103+
// For dynamic linking, clear the map.
104+
addSerializeCallback(() => { _hashCache = undefined; });
105+
}
106+
}
107+
return _hashCache;
108+
}
109+
110+
function getCachedHashId(algorithm) {
111+
const result = getHashCache()[algorithm];
112+
return result === undefined ? -1 : result;
113+
}
114+
90115
const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers()));
91116
const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes()));
92117
const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves()));
@@ -574,4 +599,6 @@ module.exports = {
574599
getStringOption,
575600
getUsagesUnion,
576601
secureHeapUsed,
602+
getCachedHashId,
603+
getHashCache,
577604
};

src/crypto/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ using SSLPointer = DeleteFnPtr<SSL, SSL_free>;
7979
using PKCS8Pointer = DeleteFnPtr<PKCS8_PRIV_KEY_INFO, PKCS8_PRIV_KEY_INFO_free>;
8080
using EVPKeyPointer = DeleteFnPtr<EVP_PKEY, EVP_PKEY_free>;
8181
using EVPKeyCtxPointer = DeleteFnPtr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
82-
using EVPMDPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
82+
using EVPMDCtxPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
8383
using RSAPointer = DeleteFnPtr<RSA, RSA_free>;
8484
using ECPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
8585
using BignumPointer = DeleteFnPtr<BIGNUM, BN_clear_free>;

src/crypto/crypto_hash.cc

+167-17
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ namespace node {
1414
using v8::Context;
1515
using v8::FunctionCallbackInfo;
1616
using v8::FunctionTemplate;
17+
using v8::Int32;
1718
using v8::Isolate;
1819
using v8::Just;
1920
using v8::Local;
2021
using v8::Maybe;
2122
using v8::MaybeLocal;
23+
using v8::Name;
2224
using v8::Nothing;
2325
using v8::Object;
2426
using v8::Uint32;
@@ -34,22 +36,170 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const {
3436
tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0);
3537
}
3638

39+
#if OPENSSL_VERSION_MAJOR >= 3
40+
void PushAliases(const char* name, void* data) {
41+
static_cast<std::vector<std::string>*>(data)->push_back(name);
42+
}
43+
44+
EVP_MD* GetCachedMDByID(Environment* env, size_t id) {
45+
CHECK_LT(id, env->evp_md_cache.size());
46+
EVP_MD* result = env->evp_md_cache[id].get();
47+
CHECK_NOT_NULL(result);
48+
return result;
49+
}
50+
51+
struct MaybeCachedMD {
52+
EVP_MD* explicit_md = nullptr;
53+
const EVP_MD* implicit_md = nullptr;
54+
int32_t cache_id = -1;
55+
};
56+
57+
MaybeCachedMD FetchAndMaybeCacheMD(Environment* env, const char* search_name) {
58+
const EVP_MD* implicit_md = EVP_get_digestbyname(search_name);
59+
if (!implicit_md) return {nullptr, nullptr, -1};
60+
61+
const char* real_name = EVP_MD_get0_name(implicit_md);
62+
if (!real_name) return {nullptr, implicit_md, -1};
63+
64+
auto it = env->alias_to_md_id_map.find(real_name);
65+
if (it != env->alias_to_md_id_map.end()) {
66+
size_t id = it->second;
67+
return {GetCachedMDByID(env, id), implicit_md, static_cast<int32_t>(id)};
68+
}
69+
70+
// EVP_*_fetch() does not support alias names, so we need to pass it the
71+
// real/original algorithm name.
72+
// We use EVP_*_fetch() as a filter here because it will only return an
73+
// instance if the algorithm is supported by the public OpenSSL APIs (some
74+
// algorithms are used internally by OpenSSL and are also passed to this
75+
// callback).
76+
EVP_MD* explicit_md = EVP_MD_fetch(nullptr, real_name, nullptr);
77+
if (!explicit_md) return {nullptr, implicit_md, -1};
78+
79+
// Cache the EVP_MD* fetched.
80+
env->evp_md_cache.emplace_back(explicit_md);
81+
size_t id = env->evp_md_cache.size() - 1;
82+
83+
// Add all the aliases to the map to speed up next lookup.
84+
std::vector<std::string> aliases;
85+
EVP_MD_names_do_all(explicit_md, PushAliases, &aliases);
86+
for (const auto& alias : aliases) {
87+
env->alias_to_md_id_map.emplace(alias, id);
88+
}
89+
env->alias_to_md_id_map.emplace(search_name, id);
90+
91+
return {explicit_md, implicit_md, static_cast<int32_t>(id)};
92+
}
93+
94+
void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md,
95+
const char* from,
96+
const char* to,
97+
void* arg) {
98+
if (!from) return;
99+
Environment* env = static_cast<Environment*>(arg);
100+
auto result = FetchAndMaybeCacheMD(env, from);
101+
if (result.explicit_md) {
102+
env->supported_hash_algorithms.push_back(from);
103+
}
104+
}
105+
106+
#else
107+
void SaveSupportedHashAlgorithms(const EVP_MD* md,
108+
const char* from,
109+
const char* to,
110+
void* arg) {
111+
if (!from) return;
112+
Environment* env = static_cast<Environment*>(arg);
113+
env->supported_hash_algorithms.push_back(from);
114+
}
115+
#endif // OPENSSL_VERSION_MAJOR >= 3
116+
117+
const std::vector<std::string>& GetSupportedHashAlgorithms(Environment* env) {
118+
if (env->supported_hash_algorithms.empty()) {
119+
MarkPopErrorOnReturn mark_pop_error_on_return;
120+
#if OPENSSL_VERSION_MAJOR >= 3
121+
// Since we'll fetch the EVP_MD*, cache them along the way to speed up
122+
// later lookups instead of throwing them away immediately.
123+
EVP_MD_do_all_sorted(SaveSupportedHashAlgorithmsAndCacheMD, env);
124+
#else
125+
EVP_MD_do_all_sorted(SaveSupportedHashAlgorithms, env);
126+
#endif
127+
}
128+
return env->supported_hash_algorithms;
129+
}
130+
37131
void Hash::GetHashes(const FunctionCallbackInfo<Value>& args) {
38-
Environment* env = Environment::GetCurrent(args);
39-
MarkPopErrorOnReturn mark_pop_error_on_return;
40-
CipherPushContext ctx(env);
41-
EVP_MD_do_all_sorted(
132+
Local<Context> context = args.GetIsolate()->GetCurrentContext();
133+
Environment* env = Environment::GetCurrent(context);
134+
const std::vector<std::string>& results = GetSupportedHashAlgorithms(env);
135+
136+
Local<Value> ret;
137+
if (ToV8Value(context, results).ToLocal(&ret)) {
138+
args.GetReturnValue().Set(ret);
139+
}
140+
}
141+
142+
void Hash::GetCachedAliases(const FunctionCallbackInfo<Value>& args) {
143+
Isolate* isolate = args.GetIsolate();
144+
Local<Context> context = args.GetIsolate()->GetCurrentContext();
145+
Environment* env = Environment::GetCurrent(context);
146+
std::vector<Local<Name>> names;
147+
std::vector<Local<Value>> values;
148+
size_t size = env->alias_to_md_id_map.size();
42149
#if OPENSSL_VERSION_MAJOR >= 3
43-
array_push_back<EVP_MD,
44-
EVP_MD_fetch,
45-
EVP_MD_free,
46-
EVP_get_digestbyname,
47-
EVP_MD_get0_name>,
150+
names.reserve(size);
151+
values.reserve(size);
152+
for (auto& [alias, id] : env->alias_to_md_id_map) {
153+
names.push_back(OneByteString(isolate, alias.c_str(), alias.size()));
154+
values.push_back(v8::Uint32::New(isolate, id));
155+
}
48156
#else
49-
array_push_back<EVP_MD>,
157+
CHECK(env->alias_to_md_id_map.empty());
158+
#endif
159+
Local<Value> prototype = v8::Null(isolate);
160+
Local<Object> result =
161+
Object::New(isolate, prototype, names.data(), values.data(), size);
162+
args.GetReturnValue().Set(result);
163+
}
164+
165+
const EVP_MD* GetDigestImplementation(Environment* env,
166+
Local<Value> algorithm,
167+
Local<Value> cache_id_val,
168+
Local<Value> algorithm_cache) {
169+
CHECK(algorithm->IsString());
170+
CHECK(cache_id_val->IsInt32());
171+
CHECK(algorithm_cache->IsObject());
172+
173+
#if OPENSSL_VERSION_MAJOR >= 3
174+
int32_t cache_id = cache_id_val.As<Int32>()->Value();
175+
if (cache_id != -1) { // Alias already cached, return the cached EVP_MD*.
176+
return GetCachedMDByID(env, cache_id);
177+
}
178+
179+
// Only decode the algorithm when we don't have it cached to avoid
180+
// unnecessary overhead.
181+
Isolate* isolate = env->isolate();
182+
Utf8Value utf8(isolate, algorithm);
183+
184+
auto result = FetchAndMaybeCacheMD(env, *utf8);
185+
if (result.cache_id != -1) {
186+
// Add the alias to both C++ side and JS side to speedup the lookup
187+
// next time.
188+
env->alias_to_md_id_map.emplace(*utf8, result.cache_id);
189+
if (algorithm_cache.As<Object>()
190+
->Set(isolate->GetCurrentContext(),
191+
algorithm,
192+
v8::Int32::New(isolate, result.cache_id))
193+
.IsNothing()) {
194+
return nullptr;
195+
}
196+
}
197+
198+
return result.explicit_md ? result.explicit_md : result.implicit_md;
199+
#else
200+
Utf8Value utf8(env->isolate(), algorithm);
201+
return EVP_get_digestbyname(*utf8);
50202
#endif
51-
&ctx);
52-
args.GetReturnValue().Set(ctx.ToJSArray());
53203
}
54204

55205
void Hash::Initialize(Environment* env, Local<Object> target) {
@@ -65,6 +215,7 @@ void Hash::Initialize(Environment* env, Local<Object> target) {
65215
SetConstructorFunction(context, target, "Hash", t);
66216

67217
SetMethodNoSideEffect(context, target, "getHashes", GetHashes);
218+
SetMethodNoSideEffect(context, target, "getCachedAliases", GetCachedAliases);
68219

69220
HashJob::Initialize(env, target);
70221

@@ -77,24 +228,24 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
77228
registry->Register(HashUpdate);
78229
registry->Register(HashDigest);
79230
registry->Register(GetHashes);
231+
registry->Register(GetCachedAliases);
80232

81233
HashJob::RegisterExternalReferences(registry);
82234

83235
registry->Register(InternalVerifyIntegrity);
84236
}
85237

238+
// new Hash(algorithm, algorithmId, xofLen, algorithmCache)
86239
void Hash::New(const FunctionCallbackInfo<Value>& args) {
87240
Environment* env = Environment::GetCurrent(args);
88241

89242
const Hash* orig = nullptr;
90243
const EVP_MD* md = nullptr;
91-
92244
if (args[0]->IsObject()) {
93245
ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As<Object>());
94246
md = EVP_MD_CTX_md(orig->mdctx_.get());
95247
} else {
96-
const Utf8Value hash_type(env->isolate(), args[0]);
97-
md = EVP_get_digestbyname(*hash_type);
248+
md = GetDigestImplementation(env, args[0], args[2], args[3]);
98249
}
99250

100251
Maybe<unsigned int> xof_md_len = Nothing<unsigned int>();
@@ -284,7 +435,7 @@ bool HashTraits::DeriveBits(
284435
Environment* env,
285436
const HashConfig& params,
286437
ByteSource* out) {
287-
EVPMDPointer ctx(EVP_MD_CTX_new());
438+
EVPMDCtxPointer ctx(EVP_MD_CTX_new());
288439

289440
if (UNLIKELY(!ctx ||
290441
EVP_DigestInit_ex(ctx.get(), params.digest, nullptr) <= 0 ||
@@ -357,6 +508,5 @@ void InternalVerifyIntegrity(const v8::FunctionCallbackInfo<v8::Value>& args) {
357508
args.GetReturnValue().Set(rc.FromMaybe(Local<Value>()));
358509
}
359510
}
360-
361511
} // namespace crypto
362512
} // namespace node

src/crypto/crypto_hash.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Hash final : public BaseObject {
2525
bool HashUpdate(const char* data, size_t len);
2626

2727
static void GetHashes(const v8::FunctionCallbackInfo<v8::Value>& args);
28+
static void GetCachedAliases(const v8::FunctionCallbackInfo<v8::Value>& args);
2829

2930
protected:
3031
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -34,7 +35,7 @@ class Hash final : public BaseObject {
3435
Hash(Environment* env, v8::Local<v8::Object> wrap);
3536

3637
private:
37-
EVPMDPointer mdctx_ {};
38+
EVPMDCtxPointer mdctx_{};
3839
unsigned int md_len_ = 0;
3940
ByteSource digest_;
4041
};

src/crypto/crypto_sig.cc

+4-4
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ bool ApplyRSAOptions(const ManagedEVPPKey& pkey,
7373
}
7474

7575
std::unique_ptr<BackingStore> Node_SignFinal(Environment* env,
76-
EVPMDPointer&& mdctx,
76+
EVPMDCtxPointer&& mdctx,
7777
const ManagedEVPPKey& pkey,
7878
int padding,
7979
Maybe<int> pss_salt_len) {
@@ -391,7 +391,7 @@ Sign::SignResult Sign::SignFinal(
391391
if (!mdctx_)
392392
return SignResult(kSignNotInitialised);
393393

394-
EVPMDPointer mdctx = std::move(mdctx_);
394+
EVPMDCtxPointer mdctx = std::move(mdctx_);
395395

396396
if (!ValidateDSAParameters(pkey.get()))
397397
return SignResult(kSignPrivateKey);
@@ -511,7 +511,7 @@ SignBase::Error Verify::VerifyFinal(const ManagedEVPPKey& pkey,
511511
unsigned char m[EVP_MAX_MD_SIZE];
512512
unsigned int m_len;
513513
*verify_result = false;
514-
EVPMDPointer mdctx = std::move(mdctx_);
514+
EVPMDCtxPointer mdctx = std::move(mdctx_);
515515

516516
if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len))
517517
return kSignPublicKey;
@@ -696,7 +696,7 @@ bool SignTraits::DeriveBits(
696696
const SignConfiguration& params,
697697
ByteSource* out) {
698698
ClearErrorOnReturn clear_error_on_return;
699-
EVPMDPointer context(EVP_MD_CTX_new());
699+
EVPMDCtxPointer context(EVP_MD_CTX_new());
700700
EVP_PKEY_CTX* ctx = nullptr;
701701

702702
switch (params.mode) {

0 commit comments

Comments
 (0)