Skip to content

Commit

Permalink
crypto: use EVP_MD_fetch and cache EVP_MD for hashes
Browse files Browse the repository at this point in the history
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 <jasnell@gmail.com>
  • Loading branch information
joyeecheung authored and richardlau committed Mar 25, 2024
1 parent 242139f commit 68e8b2c
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 27 deletions.
10 changes: 8 additions & 2 deletions lib/internal/crypto/hash.js
Expand Up @@ -19,6 +19,8 @@ const {
normalizeHashName,
validateMaxBufferLength,
kHandle,
getCachedHashId,
getHashCache,
} = require('internal/crypto/util');

const {
Expand Down Expand Up @@ -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,
};
Expand Down
27 changes: 27 additions & 0 deletions lib/internal/crypto/util.js
Expand Up @@ -29,6 +29,7 @@ const {
getHashes: _getHashes,
setEngine: _setEngine,
secureHeapUsed: _secureHeapUsed,
getCachedAliases,
} = internalBinding('crypto');

const { getOptionValue } = require('internal/options');
Expand Down Expand Up @@ -66,6 +67,13 @@ const {
lazyDOMException,
} = require('internal/util');

const {
namespace: {
isBuildingSnapshot,
addSerializeCallback,
},
} = require('internal/v8/startup_snapshot');

const {
isDataView,
isArrayBufferView,
Expand All @@ -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()));
Expand Down Expand Up @@ -574,4 +599,6 @@ module.exports = {
getStringOption,
getUsagesUnion,
secureHeapUsed,
getCachedHashId,
getHashCache,
};
2 changes: 1 addition & 1 deletion src/crypto/README.md
Expand Up @@ -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>;
Expand Down
184 changes: 167 additions & 17 deletions src/crypto/crypto_hash.cc
Expand Up @@ -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;
Expand All @@ -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<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
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) {
Environment* env = Environment::GetCurrent(args);
MarkPopErrorOnReturn mark_pop_error_on_return;
CipherPushContext ctx(env);
EVP_MD_do_all_sorted(
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
array_push_back<EVP_MD,
EVP_MD_fetch,
EVP_MD_free,
EVP_get_digestbyname,
EVP_MD_get0_name>,
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<EVP_MD>,
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) {
Expand All @@ -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);

Expand All @@ -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>();
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -357,6 +508,5 @@ void InternalVerifyIntegrity(const v8::FunctionCallbackInfo<v8::Value>& args) {
args.GetReturnValue().Set(rc.FromMaybe(Local<Value>()));
}
}

} // namespace crypto
} // namespace node
3 changes: 2 additions & 1 deletion src/crypto/crypto_hash.h
Expand Up @@ -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);
Expand All @@ -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_;
};
Expand Down
8 changes: 4 additions & 4 deletions src/crypto/crypto_sig.cc
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 68e8b2c

Please sign in to comment.