Commit 3295b92089e for nodejs
commit 3295b92089e5a33486d6541ed60401bb5499595f
Author: Yagiz Nizipli <yagiz@nizipli.com>
Date: Thu Oct 29 12:12:30 2026 -0400
crypto: cache digests for synchronous operations
createHmac(), createSign(), createVerify() and the synchronous
one-shot sign, verify, HKDF and PBKDF2 functions looked up their
digest with Digest::FromName(). That returns OpenSSL's legacy EVP_MD,
so every EVP_DigestInit_ex() and HMAC init had to fetch the provider
implementation again through the method store.
Look these digests up through the per-Environment cache that
createHash() and crypto.hash() already use. Jobs on the thread pool
keep using Digest::FromName(): with the cache, Web Crypto digest and
HMAC throughput went down, because workers that finish sooner have to
be woken up for each new job more often.
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Assisted-by: Grok
PR-URL: https://github.com/nodejs/node/pull/67109
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 42f5a68768f..51f09e42710 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -8942,6 +8942,16 @@ const Digest Digest::Fetch(const char* name) {
return Digest();
}
+const Digest Digest::Ref(const EVP_MD* md) {
+#if NCRYPTO_USE_OPENSSL_PROVIDER
+ if (md != nullptr && EVP_MD_get0_provider(md) != nullptr) {
+ if (EVP_MD_up_ref(const_cast<EVP_MD*>(md)) != 1) return Digest();
+ return Digest(DeleteFnPtr<EVP_MD, EVP_MD_free>(const_cast<EVP_MD*>(md)));
+ }
+#endif
+ return Digest(md);
+}
+
// ============================================================================
// KEM Implementation
#if OPENSSL_WITH_KEM
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index a9468655aa6..adafe98b307 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -368,6 +368,9 @@ class Digest final {
static const Digest FromName(const char* name);
static const Digest Fetch(const char* name);
+ // Returns a Digest that holds its own reference to md if md was fetched from
+ // a provider.
+ static const Digest Ref(const EVP_MD* md);
private:
const EVP_MD* md_ = nullptr;
diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc
index 5ff56a322a1..492abc7c722 100644
--- a/src/crypto/crypto_hash.cc
+++ b/src/crypto/crypto_hash.cc
@@ -363,6 +363,25 @@ const EVP_MD* GetDigestImplementation(
#endif
}
+const ncrypto::Digest GetDigest(Environment* env, const char* name) {
+#if NCRYPTO_USE_OPENSSL_PROVIDER
+ MaybeCachedMD result = FetchAndMaybeCacheMD(env, name);
+ if (result.cached_md != nullptr) {
+ return ncrypto::Digest::Ref(result.cached_md);
+ }
+ return result.digest;
+#elif NCRYPTO_USE_BORINGSSL
+ return ncrypto::Digest::FromName(name);
+#endif
+}
+
+const ncrypto::Digest GetDigestForJob(Environment* env,
+ CryptoJobMode mode,
+ const char* name) {
+ if (mode == kCryptoJobSync) return GetDigest(env, name);
+ return ncrypto::Digest::FromName(name);
+}
+
void MarkInvalidXofLength() {
#if NCRYPTO_USE_OPENSSL_PROVIDER
ERR_raise(ERR_LIB_EVP, EVP_R_NOT_XOF_OR_INVALID_LENGTH);
diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h
index 1cdfa2054e4..7aaac944786 100644
--- a/src/crypto/crypto_hash.h
+++ b/src/crypto/crypto_hash.h
@@ -12,6 +12,18 @@
namespace node {
namespace crypto {
+// Like ncrypto::Digest::FromName(), but caches provider implementations per
+// Environment so that later lookups do not have to fetch them again.
+const ncrypto::Digest GetDigest(Environment* env, const char* name);
+
+// GetDigest() for synchronous jobs. Jobs on the thread pool keep using
+// ncrypto::Digest::FromName(): there, the cache lowered Web Crypto digest and
+// HMAC throughput, because workers that finish sooner have to be woken up for
+// each new job more often.
+const ncrypto::Digest GetDigestForJob(Environment* env,
+ CryptoJobMode mode,
+ const char* name);
+
class Hash final : public BaseObject {
public:
static void Initialize(Environment* env, v8::Local<v8::Object> target);
diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc
index 64e0bbb0d77..8e9809f280b 100644
--- a/src/crypto/crypto_hkdf.cc
+++ b/src/crypto/crypto_hkdf.cc
@@ -1,6 +1,7 @@
#include "crypto/crypto_hkdf.h"
#include "async_wrap-inl.h"
#include "base_object-inl.h"
+#include "crypto/crypto_hash.h"
#include "crypto/crypto_keys.h"
#include "env-inl.h"
#include "memory_tracker-inl.h"
@@ -9,7 +10,6 @@
namespace node {
-using ncrypto::Digest;
using v8::FunctionCallbackInfo;
using v8::JustVoid;
using v8::Maybe;
@@ -54,7 +54,7 @@ Maybe<void> HKDFTraits::AdditionalConfig(
CHECK(args[offset + 4]->IsUint32()); // Length
Utf8Value hash(env->isolate(), args[offset]);
- params->digest = Digest::FromName(*hash);
+ params->digest = GetDigestForJob(env, mode, *hash);
if (!params->digest) [[unlikely]] {
THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", hash);
return Nothing<void>();
diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc
index 541dfe7e781..36f07c7d98d 100644
--- a/src/crypto/crypto_hmac.cc
+++ b/src/crypto/crypto_hmac.cc
@@ -1,6 +1,7 @@
#include "crypto/crypto_hmac.h"
#include "async_wrap-inl.h"
#include "base_object-inl.h"
+#include "crypto/crypto_hash.h"
#include "crypto/crypto_keys.h"
#include "crypto/crypto_sig.h"
#include "crypto/crypto_util.h"
@@ -69,7 +70,7 @@ void Hmac::New(const FunctionCallbackInfo<Value>& args) {
void Hmac::HmacInit(const char* hash_type, const char* key, int key_len) {
HandleScope scope(env()->isolate());
- Digest md = Digest::FromName(hash_type);
+ Digest md = GetDigest(env(), hash_type);
if (!md) [[unlikely]] {
return THROW_ERR_CRYPTO_INVALID_DIGEST(
env(), "Invalid digest: %s", hash_type);
diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc
index ec0dfed171f..29aca157cdf 100644
--- a/src/crypto/crypto_pbkdf2.cc
+++ b/src/crypto/crypto_pbkdf2.cc
@@ -1,6 +1,7 @@
#include "crypto/crypto_pbkdf2.h"
#include "async_wrap-inl.h"
#include "base_object-inl.h"
+#include "crypto/crypto_hash.h"
#include "crypto/crypto_keys.h"
#include "crypto/crypto_util.h"
#include "env-inl.h"
@@ -11,7 +12,6 @@
namespace node {
-using ncrypto::Digest;
using v8::FunctionCallbackInfo;
using v8::Int32;
using v8::JustVoid;
@@ -104,7 +104,7 @@ Maybe<void> PBKDF2Traits::AdditionalConfig(
}
Utf8Value name(args.GetIsolate(), args[offset + 4]);
- params->digest = Digest::FromName(*name);
+ params->digest = GetDigestForJob(env, mode, *name);
if (!params->digest) [[unlikely]] {
THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", name);
return Nothing<void>();
diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc
index aeb9b11e865..b5fb9572c74 100644
--- a/src/crypto/crypto_sig.cc
+++ b/src/crypto/crypto_sig.cc
@@ -2,6 +2,7 @@
#include "async_wrap-inl.h"
#include "base_object-inl.h"
#include "crypto/crypto_ec.h"
+#include "crypto/crypto_hash.h"
#include "crypto/crypto_keys.h"
#include "crypto/crypto_util.h"
#include "env-inl.h"
@@ -315,7 +316,7 @@ bool VerifyPrehashed(const EVPKeyPointer& key,
SignBase::Error SignBase::Init(const char* digest) {
CHECK_NULL(mdctx_);
- auto md = Digest::FromName(digest);
+ auto md = GetDigest(env(), digest);
if (!md) [[unlikely]]
return Error::UnknownDigest;
@@ -666,7 +667,7 @@ Maybe<void> SignTraits::AdditionalConfig(
if (args[offset + 7]->IsString()) {
Utf8Value digest(env->isolate(), args[offset + 7]);
- params->digest = Digest::FromName(*digest);
+ params->digest = GetDigestForJob(env, mode, *digest);
if (!params->digest) [[unlikely]] {
THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", digest);
return Nothing<void>();