Commit 28a1343be4a for nodejs
commit 28a1343be4af93f21c4477eb04835fd836632629
Author: Filip Skokan <panva.ip@gmail.com>
Date: Fri Sep 18 11:31:36 2026 +0200
crypto: derive keys through EVP_KDF
Use one owning KDF interface for HKDF expansion, PBKDF2 and scrypt,
sharing provider setup with Argon2.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66108
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 6c817b74241..47794ab3a4f 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -2631,6 +2631,21 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey,
// ============================================================================
// KDF
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+KDF::KDF(EVP_KDF* kdf) : kdf_(kdf) {}
+
+KDF KDF::Fetch(const char* algorithm, OSSL_LIB_CTX* libctx) {
+ return KDF(EVP_KDF_fetch(libctx, algorithm, nullptr));
+}
+
+bool KDF::derive(const Buffer<unsigned char>& out,
+ const OSSL_PARAM* params) const {
+ if (!kdf_) return false;
+ DeleteFnPtr<EVP_KDF_CTX, EVP_KDF_CTX_free> ctx(EVP_KDF_CTX_new(kdf_.get()));
+ return ctx && EVP_KDF_derive(ctx.get(), out.data, out.len, params) == 1;
+}
+#endif
+
const EVP_MD* getDigestByName(const char* name) {
// Historically, "dss1" and "DSS1" were DSA aliases for SHA-1
// exposed through the public API.
@@ -2664,16 +2679,6 @@ DataPointer hkdf(const Digest& md,
return {};
}
- auto ctx = EVPKeyCtxPointer::NewFromName("HKDF");
- // OpenSSL < 3.0.0 accepted only a void* as the argument of
- // EVP_PKEY_CTX_set_hkdf_md.
- const EVP_MD* md_ptr = md;
- if (!ctx || !EVP_PKEY_derive_init(ctx.get()) ||
- !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md_ptr) ||
- !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) {
- return {};
- }
-
std::string_view actual_salt;
static const char default_salt[EVP_MAX_MD_SIZE] = {0};
if (salt.len > 0) {
@@ -2682,12 +2687,9 @@ DataPointer hkdf(const Digest& md,
actual_salt = {default_salt, static_cast<unsigned>(md.size())};
}
- // We do not use EVP_PKEY_HKDF_MODE_EXTRACT_AND_EXPAND because and instead
- // implement the extraction step ourselves because EVP_PKEY_derive does not
- // handle zero-length keys, which are required for Web Crypto.
- // TODO(jasnell): Once OpenSSL 1.1.1 support is dropped completely, and once
- // BoringSSL is confirmed to support it, wen can hopefully drop this and use
- // EVP_KDF directly which does support zero length keys.
+ // Keep extraction as a one-shot HMAC. The legacy path requires it because
+ // EVP_PKEY_derive rejects the zero-length keys Web Crypto allows. Both
+ // backends expand a pseudorandom key of exactly one digest block.
unsigned char pseudorandom_key[EVP_MAX_MD_SIZE];
unsigned pseudorandom_key_len = sizeof(pseudorandom_key);
@@ -2700,26 +2702,112 @@ DataPointer hkdf(const Digest& md,
&pseudorandom_key_len) == nullptr) {
return {};
}
- if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) ||
+
+ auto buf = DataPointer::Alloc(length);
+ if (!buf) return {};
+
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ // Expand through EVP_KDF directly. The EVP_PKEY_HKDF interface reaches the
+ // same provider implementation, but only after allocating a second context
+ // and translating every parameter across the legacy bridge.
+ auto kdf = KDF::Fetch(OSSL_KDF_NAME_HKDF);
+ if (!kdf) return {};
+
+ const char* md_name = EVP_MD_get0_name(md);
+ if (md_name == nullptr) return {};
+
+ int mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY;
+ std::array<OSSL_PARAM, 5> params;
+ size_t n = 0;
+ params[n++] = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode);
+ params[n++] = OSSL_PARAM_construct_utf8_string(
+ OSSL_KDF_PARAM_DIGEST, const_cast<char*>(md_name), 0);
+ params[n++] = OSSL_PARAM_construct_octet_string(
+ OSSL_KDF_PARAM_KEY, pseudorandom_key, pseudorandom_key_len);
+ if (info.len > 0) {
+ params[n++] = OSSL_PARAM_construct_octet_string(
+ OSSL_KDF_PARAM_INFO, const_cast<unsigned char*>(info.data), info.len);
+ }
+ params[n++] = OSSL_PARAM_construct_end();
+
+ if (!kdf.derive({buf.get<unsigned char>(), length}, params.data())) {
+ return {};
+ }
+#else
+ auto ctx = EVPKeyCtxPointer::NewFromName("HKDF");
+ // OpenSSL < 3.0.0 accepted only a void* as the argument of
+ // EVP_PKEY_CTX_set_hkdf_md.
+ const EVP_MD* md_ptr = md;
+ if (!ctx || !EVP_PKEY_derive_init(ctx.get()) ||
+ !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md_ptr) ||
+ !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len) ||
+ !EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) ||
!EVP_PKEY_CTX_set1_hkdf_key(
ctx.get(), pseudorandom_key, pseudorandom_key_len)) {
return {};
}
- auto buf = DataPointer::Alloc(length);
- if (!buf) return {};
-
if (EVP_PKEY_derive(
ctx.get(), static_cast<unsigned char*>(buf.get()), &length) <= 0) {
return {};
}
+#endif
return buf;
}
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+namespace {
+bool ScryptDerive(const Buffer<const char>& pass,
+ const Buffer<const unsigned char>& salt,
+ uint64_t N,
+ uint64_t r,
+ uint64_t p,
+ uint64_t maxmem,
+ unsigned char* out,
+ size_t length) {
+ // EVP_PBE_scrypt limits these parameters to the provider's declared width.
+ if (r > UINT32_MAX || p > UINT32_MAX) {
+ ERR_raise(ERR_LIB_EVP, EVP_R_PARAMETER_TOO_LARGE);
+ return false;
+ }
+
+ // Keep EVP_PBE_scrypt's 32 MiB default instead of the provider's default.
+ if (maxmem == 0) maxmem = 32 * 1024 * 1024;
+
+ auto kdf = KDF::Fetch(OSSL_KDF_NAME_SCRYPT);
+ if (!kdf) return false;
+
+ unsigned char empty_salt = 0;
+ OSSL_PARAM params[] = {
+ OSSL_PARAM_construct_octet_string(
+ OSSL_KDF_PARAM_PASSWORD,
+ const_cast<char*>(pass.data == nullptr ? "" : pass.data),
+ pass.data == nullptr ? 0 : pass.len),
+ OSSL_PARAM_construct_octet_string(
+ OSSL_KDF_PARAM_SALT,
+ salt.data == nullptr ? &empty_salt
+ : const_cast<unsigned char*>(salt.data),
+ salt.data == nullptr ? 0 : salt.len),
+ OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_N, &N),
+ OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_R, &r),
+ OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_P, &p),
+ OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_MAXMEM, &maxmem),
+ OSSL_PARAM_END,
+ };
+ return kdf.derive({out, length}, params);
+}
+} // namespace
+#endif
+
bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ // A null output validates the parameters without deriving a key.
+ return ScryptDerive({nullptr, 0}, {nullptr, 0}, N, r, p, maxmem, nullptr, 0);
+#else
return EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem, nullptr, 0) ==
1;
+#endif
}
DataPointer scrypt(const Buffer<const char>& pass,
@@ -2734,6 +2822,10 @@ DataPointer scrypt(const Buffer<const char>& pass,
}
auto dp = DataPointer::Alloc(length);
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (dp && ScryptDerive(
+ pass, salt, N, r, p, maxmem, dp.get<unsigned char>(), length)) {
+#else
if (dp && EVP_PBE_scrypt(pass.data,
pass.len,
salt.data,
@@ -2744,6 +2836,7 @@ DataPointer scrypt(const Buffer<const char>& pass,
maxmem,
reinterpret_cast<unsigned char*>(dp.get()),
length)) {
+#endif
return dp;
}
@@ -2760,6 +2853,44 @@ DataPointer pbkdf2(const Digest& md,
}
auto dp = DataPointer::Alloc(length);
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ if (!dp) return {};
+ auto kdf = KDF::Fetch(OSSL_KDF_NAME_PBKDF2);
+ if (!kdf) return {};
+
+ const char* md_name = EVP_MD_get0_name(md);
+ if (md_name == nullptr) return {};
+
+#if OPENSSL_VERSION_MAJOR < 4
+ // Match PKCS5_PBKDF2_HMAC: OpenSSL 3 disables the provider's lower bounds,
+ // while OpenSSL 4 leaves them at their provider defaults.
+ int pkcs5 = 1;
+#endif
+ int iteration_count = static_cast<int>(iterations);
+ unsigned char empty_salt = 0;
+ OSSL_PARAM params[] = {
+ OSSL_PARAM_construct_octet_string(
+ OSSL_KDF_PARAM_PASSWORD,
+ const_cast<char*>(pass.data == nullptr ? "" : pass.data),
+ pass.data == nullptr ? 0 : pass.len),
+#if OPENSSL_VERSION_MAJOR < 4
+ OSSL_PARAM_construct_int(OSSL_KDF_PARAM_PKCS5, &pkcs5),
+#endif
+ OSSL_PARAM_construct_octet_string(
+ OSSL_KDF_PARAM_SALT,
+ salt.data == nullptr && salt.len == 0
+ ? &empty_salt
+ : const_cast<unsigned char*>(salt.data),
+ salt.len),
+ OSSL_PARAM_construct_int(OSSL_KDF_PARAM_ITER, &iteration_count),
+ OSSL_PARAM_construct_utf8_string(
+ OSSL_KDF_PARAM_DIGEST, const_cast<char*>(md_name), 0),
+ OSSL_PARAM_END,
+ };
+ if (kdf.derive({dp.get<unsigned char>(), length}, params)) {
+ return dp;
+ }
+#else
const EVP_MD* md_ptr = md;
if (dp && PKCS5_PBKDF2_HMAC(pass.data,
pass.len,
@@ -2771,6 +2902,7 @@ DataPointer pbkdf2(const Digest& md,
reinterpret_cast<unsigned char*>(dp.get()))) {
return dp;
}
+#endif
return {};
}
@@ -2807,8 +2939,7 @@ DataPointer argon2(const Buffer<const char>& pass,
// against the default context, otherwise Argon2 works in FIPS mode.
DeleteFnPtr<OSSL_LIB_CTX, OSSL_LIB_CTX_free> ctx;
if (lanes > 1) {
- if (!DeleteFnPtr<EVP_KDF, EVP_KDF_free>{
- EVP_KDF_fetch(nullptr, algorithm.data(), nullptr)}) {
+ if (!KDF::Fetch(algorithm.data())) {
return {};
}
@@ -2822,18 +2953,11 @@ DataPointer argon2(const Buffer<const char>& pass,
}
}
- auto kdf = DeleteFnPtr<EVP_KDF, EVP_KDF_free>{
- EVP_KDF_fetch(ctx.get(), algorithm.data(), nullptr)};
+ auto kdf = KDF::Fetch(algorithm.data(), ctx.get());
if (!kdf) {
return {};
}
- auto kctx =
- DeleteFnPtr<EVP_KDF_CTX, EVP_KDF_CTX_free>{EVP_KDF_CTX_new(kdf.get())};
- if (!kctx) {
- return {};
- }
-
std::vector<OSSL_PARAM> params;
params.reserve(9);
@@ -2865,10 +2989,7 @@ DataPointer argon2(const Buffer<const char>& pass,
params.push_back(OSSL_PARAM_construct_end());
auto dp = DataPointer::Alloc(length);
- if (dp && EVP_KDF_derive(kctx.get(),
- reinterpret_cast<unsigned char*>(dp.get()),
- length,
- params.data()) == 1) {
+ if (dp && kdf.derive({dp.get<unsigned char>(), length}, params.data())) {
return dp;
}
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index 53151503c36..37a8a6040c2 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -2171,6 +2171,28 @@ Buffer<char> ExportChallenge(const char* input, size_t length);
// ============================================================================
// KDF
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+class KDF final {
+ public:
+ KDF() = default;
+ KDF(KDF&&) noexcept = default;
+ KDF& operator=(KDF&&) noexcept = default;
+ NCRYPTO_DISALLOW_COPY(KDF)
+
+ inline operator bool() const { return kdf_ != nullptr; }
+
+ static KDF Fetch(const char* algorithm, OSSL_LIB_CTX* libctx = nullptr);
+
+ // Each derivation uses a fresh context. A null output can be used for
+ // parameter validation by KDFs that support it, such as scrypt.
+ bool derive(const Buffer<unsigned char>& out, const OSSL_PARAM* params) const;
+
+ private:
+ explicit KDF(EVP_KDF* kdf);
+ DeleteFnPtr<EVP_KDF, EVP_KDF_free> kdf_;
+};
+#endif
+
const EVP_MD* getDigestByName(const char* name);
const EVP_CIPHER* getCipherByName(const char* name);