Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: use EVP_MD_fetch and cache EVP_MD for hashes #51034

Merged
merged 3 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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_free>;
Expand Down
184 changes: 167 additions & 17 deletions src/crypto/crypto_hash.cc
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading