Commit 2b68fcd21a6 for nodejs

commit 2b68fcd21a6d55a00fbfcf970d6939ed62748373
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Fri Sep 18 11:41:00 2026 +0200

    crypto: decode PKCS#1 keys through providers

    Import RSA public keys through OSSL_DECODER on OpenSSL 3 so the
    resulting keys stay provider-backed. Preserve the PKCS#1 input structure
    and the ASN.1 encodings accepted by the legacy decoder.

    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 47794ab3a4f..eb5e28154cc 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -8,6 +8,9 @@
 #include <openssl/pkcs12.h>
 #include <openssl/rand.h>
 #include <openssl/x509v3.h>
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+#include <openssl/decoder.h>
+#endif
 #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
 #include <openssl/bytestring.h>
 #include <openssl/cipher.h>
@@ -3864,6 +3867,47 @@ EVPKeyPointer::operator const EC_KEY*() const {

 namespace {

+EVP_PKEY* DecodeRsaPublicKey(const unsigned char** data, size_t length) {
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+  // Borrow the EVP_PKEY constructor and its data from a context that stays
+  // alive until after the restricted decoder context is destroyed.
+  EVP_PKEY* raw = nullptr;
+  DeleteFnPtr<OSSL_DECODER_CTX, OSSL_DECODER_CTX_free> construct_ctx(
+      OSSL_DECODER_CTX_new_for_pkey(&raw,
+                                    "DER",
+                                    "type-specific",
+                                    KeyAlgorithm::RSA.name(),
+                                    EVP_PKEY_PUBLIC_KEY,
+                                    nullptr,
+                                    nullptr));
+  if (!construct_ctx) return nullptr;
+  auto* construct = OSSL_DECODER_CTX_get_construct(construct_ctx.get());
+  void* construct_data =
+      OSSL_DECODER_CTX_get_construct_data(construct_ctx.get());
+  if (construct == nullptr || construct_data == nullptr) return nullptr;
+
+  // Add only the type-specific RSA decoder: new_for_pkey() can also build
+  // chains that accept SPKI. The owning context retains the cleanup callback.
+  DeleteFnPtr<OSSL_DECODER, OSSL_DECODER_free> decoder(OSSL_DECODER_fetch(
+      nullptr, KeyAlgorithm::RSA.name(), "input=der,structure=type-specific"));
+  DeleteFnPtr<OSSL_DECODER_CTX, OSSL_DECODER_CTX_free> ctx(
+      OSSL_DECODER_CTX_new());
+  if (!decoder || !ctx ||
+      OSSL_DECODER_CTX_add_decoder(ctx.get(), decoder.get()) != 1 ||
+      OSSL_DECODER_CTX_set_input_type(ctx.get(), "DER") != 1 ||
+      OSSL_DECODER_CTX_set_selection(ctx.get(), EVP_PKEY_PUBLIC_KEY) != 1 ||
+      OSSL_DECODER_CTX_set_construct(ctx.get(), construct) != 1 ||
+      OSSL_DECODER_CTX_set_construct_data(ctx.get(), construct_data) != 1) {
+    return nullptr;
+  }
+  const int result = OSSL_DECODER_from_data(ctx.get(), data, &length);
+  EVPKeyPointer key(raw);
+  return result == 1 ? key.release() : nullptr;
+#else
+  return d2i_PublicKey(NID_rsaEncryption, nullptr, data, length);
+#endif
+}
+
 EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp,
                                                      const char* name,
                                                      auto&& parse) {
@@ -3991,7 +4035,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM(
           bp,
           "RSA PUBLIC KEY",
           [](const unsigned char** p, long l) {  // NOLINT(runtime/int)
-            return d2i_PublicKey(NID_rsaEncryption, nullptr, p, l);
+            return DecodeRsaPublicKey(p, l);
           })) {
     return ret;
   }
@@ -4026,7 +4070,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKey(
   EVP_PKEY* key = nullptr;

   if (config.type == PKEncodingType::PKCS1 &&
-      (key = d2i_PublicKey(NID_rsaEncryption, nullptr, &start, buffer.len))) {
+      (key = DecodeRsaPublicKey(&start, buffer.len))) {
     return EVPKeyPointer::ParseKeyResult(EVPKeyPointer(key));
   }

diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc
index 36bbf2c93f7..69eb8b89127 100644
--- a/test/cctest/test_node_crypto.cc
+++ b/test/cctest/test_node_crypto.cc
@@ -89,6 +89,193 @@ TEST(NodeCrypto, KeyAlgorithmNames) {
   EXPECT_FALSE(empty.isA(static_cast<const char*>(nullptr)));
 }

+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+TEST(NodeCrypto, ProviderPkcs1PublicKeyImport) {
+  ncrypto::ClearErrorOnReturn clear_errors;
+  auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::RSA);
+  ASSERT_TRUE(ctx);
+  ASSERT_TRUE(ctx.initForKeygen());
+  ASSERT_TRUE(ctx.setRsaKeygenBits(2048));
+  EVP_PKEY* raw = nullptr;
+  ASSERT_EQ(EVP_PKEY_keygen(ctx.get(), &raw), 1);
+  EVPKeyPointer key(raw);
+
+  for (const auto format :
+       {EVPKeyPointer::PKFormatType::PEM, EVPKeyPointer::PKFormatType::DER}) {
+    const EVPKeyPointer::PublicKeyEncodingConfig config(
+        false, format, EVPKeyPointer::PKEncodingType::PKCS1);
+    auto encoded = key.writePublicKey(config);
+    ASSERT_TRUE(encoded);
+    const BUF_MEM* mem = encoded.value;
+    ASSERT_NE(mem, nullptr);
+    const ncrypto::Buffer<const unsigned char> input{
+        reinterpret_cast<const unsigned char*>(mem->data), mem->length};
+    auto imported = EVPKeyPointer::TryParsePublicKey(config, input);
+    ASSERT_TRUE(imported);
+    EXPECT_NE(EVP_PKEY_get0_provider(imported.value.get()), nullptr);
+    EXPECT_TRUE(imported.value.isA(KeyAlgorithm::RSA));
+    EXPECT_EQ(EVP_PKEY_eq(key.get(), imported.value.get()), 1);
+  }
+}
+
+namespace {
+struct RsaLoadTestContext {
+  OSSL_FUNC_BIO_read_ex_fn* read = nullptr;
+  int selection = 0;
+  int loads = 0;
+  int frees = 0;
+};
+
+void* RsaLoadTestDecoderNew(void* context) {
+  return context;
+}
+
+void RsaLoadTestDecoderFree(void*) {}
+
+int RsaLoadTestDecode(void* context,
+                      OSSL_CORE_BIO* input,
+                      int selection,
+                      OSSL_CALLBACK* callback,
+                      void* arg,
+                      OSSL_PASSPHRASE_CALLBACK*,
+                      void*) {
+  auto* state = static_cast<RsaLoadTestContext*>(context);
+  unsigned char sentinel = 0;
+  size_t size = 0;
+  // Only exercise construction and reference ownership, not ASN.1 parsing.
+  if (!state->read(input, &sentinel, 1, &size) || size != 1 ||
+      sentinel != 0x42) {
+    return 0;
+  }
+  state->selection = selection;
+  char type[] = "RSA";
+  const OSSL_PARAM params[] = {
+      OSSL_PARAM_utf8_string(
+          OSSL_OBJECT_PARAM_DATA_TYPE, type, sizeof(type) - 1),
+      OSSL_PARAM_octet_string(
+          OSSL_OBJECT_PARAM_REFERENCE, &state, sizeof(state)),
+      OSSL_PARAM_END,
+  };
+  return callback(params, arg);
+}
+
+void* RsaLoadTestLoad(const void* reference, size_t size) {
+  if (size != sizeof(RsaLoadTestContext*)) return nullptr;
+  auto* state = *static_cast<RsaLoadTestContext* const*>(reference);
+  state->loads++;
+  return state;
+}
+
+void RsaLoadTestFree(void* context) {
+  static_cast<RsaLoadTestContext*>(context)->frees++;
+}
+
+int RsaLoadTestHas(const void*, int selection) {
+  return (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) == 0;
+}
+
+const OSSL_ALGORITHM* RsaLoadTestQuery(void*, int operation, int* no_cache) {
+  *no_cache = 0;
+  static const OSSL_DISPATCH decoder[] = {
+      {OSSL_FUNC_DECODER_NEWCTX,
+       reinterpret_cast<void (*)(void)>(RsaLoadTestDecoderNew)},
+      {OSSL_FUNC_DECODER_FREECTX,
+       reinterpret_cast<void (*)(void)>(RsaLoadTestDecoderFree)},
+      {OSSL_FUNC_DECODER_DECODE,
+       reinterpret_cast<void (*)(void)>(RsaLoadTestDecode)},
+      {0, nullptr},
+  };
+  static const OSSL_DISPATCH keymgmt[] = {
+      {OSSL_FUNC_KEYMGMT_LOAD,
+       reinterpret_cast<void (*)(void)>(RsaLoadTestLoad)},
+      {OSSL_FUNC_KEYMGMT_FREE,
+       reinterpret_cast<void (*)(void)>(RsaLoadTestFree)},
+      {OSSL_FUNC_KEYMGMT_HAS, reinterpret_cast<void (*)(void)>(RsaLoadTestHas)},
+      {0, nullptr},
+  };
+  static const OSSL_ALGORITHM decoders[] = {
+      {"RSA",
+       "provider=node-test-rsa-load,input=der,structure=type-specific",
+       decoder,
+       "Test RSA decoder without export"},
+      {nullptr, nullptr, nullptr, nullptr},
+  };
+  static const OSSL_ALGORITHM keymgmts[] = {
+      {"RSA",
+       "provider=node-test-rsa-load",
+       keymgmt,
+       "Test RSA reference load"},
+      {nullptr, nullptr, nullptr, nullptr},
+  };
+  if (operation == OSSL_OP_DECODER) return decoders;
+  return operation == OSSL_OP_KEYMGMT ? keymgmts : nullptr;
+}
+
+void RsaLoadTestTeardown(void* context) {
+  delete static_cast<RsaLoadTestContext*>(context);
+}
+
+int RsaLoadTestProviderInit(const OSSL_CORE_HANDLE*,
+                            const OSSL_DISPATCH* in,
+                            const OSSL_DISPATCH** out,
+                            void** context) {
+  auto state = std::make_unique<RsaLoadTestContext>();
+  for (; in->function_id != 0; in++) {
+    if (in->function_id == OSSL_FUNC_BIO_READ_EX) {
+      state->read = OSSL_FUNC_BIO_read_ex(in);
+    }
+  }
+  if (state->read == nullptr) return 0;
+  static const OSSL_DISPATCH dispatch[] = {
+      {OSSL_FUNC_PROVIDER_QUERY_OPERATION,
+       reinterpret_cast<void (*)(void)>(RsaLoadTestQuery)},
+      {OSSL_FUNC_PROVIDER_TEARDOWN,
+       reinterpret_cast<void (*)(void)>(RsaLoadTestTeardown)},
+      {0, nullptr},
+  };
+  *context = state.release();
+  *out = dispatch;
+  return 1;
+}
+}  // namespace
+
+TEST(NodeCrypto, ProviderPkcs1PublicKeyLoadWithoutExport) {
+  ncrypto::ClearErrorOnReturn clear_errors;
+  ncrypto::DeleteFnPtr<OSSL_LIB_CTX, OSSL_LIB_CTX_free> libctx(
+      OSSL_LIB_CTX_new());
+  ASSERT_TRUE(libctx);
+  ASSERT_EQ(OSSL_PROVIDER_add_builtin(
+                libctx.get(), "node-test-rsa-load", RsaLoadTestProviderInit),
+            1);
+  auto* provider = OSSL_PROVIDER_load(libctx.get(), "node-test-rsa-load");
+  auto unload_provider =
+      node::OnScopeLeave([provider] { OSSL_PROVIDER_unload(provider); });
+  ASSERT_NE(provider, nullptr);
+  auto* state = static_cast<RsaLoadTestContext*>(
+      OSSL_PROVIDER_get0_provider_ctx(provider));
+  OSSL_LIB_CTX* previous_libctx = OSSL_LIB_CTX_set0_default(libctx.get());
+  auto restore_libctx = node::OnScopeLeave(
+      [previous_libctx] { OSSL_LIB_CTX_set0_default(previous_libctx); });
+
+  // Neither decoder export nor keymgmt import is available in this provider.
+  const unsigned char sentinel[] = {0x42};
+  const EVPKeyPointer::PublicKeyEncodingConfig config(
+      false,
+      EVPKeyPointer::PKFormatType::DER,
+      EVPKeyPointer::PKEncodingType::PKCS1);
+  {
+    auto imported = EVPKeyPointer::TryParsePublicKey(config, {sentinel, 1});
+    ASSERT_TRUE(imported);
+    EXPECT_EQ(EVP_PKEY_get0_provider(imported.value.get()), provider);
+    EXPECT_TRUE(imported.value.isA(KeyAlgorithm::RSA));
+    EXPECT_EQ(state->selection, EVP_PKEY_PUBLIC_KEY);
+    EXPECT_EQ(state->loads, 1);
+    EXPECT_EQ(state->frees, 0);
+  }
+  EXPECT_EQ(state->frees, 1);
+}
+#endif
+
 TEST(NodeCrypto, UnsupportedRawExports) {
   using Error = EVPKeyPointer::RawExportError;
   EVPKeyPointer key;
diff --git a/test/parallel/test-crypto-rsa-pkcs1-public-key.js b/test/parallel/test-crypto-rsa-pkcs1-public-key.js
new file mode 100644
index 00000000000..ee37a17cce2
--- /dev/null
+++ b/test/parallel/test-crypto-rsa-pkcs1-public-key.js
@@ -0,0 +1,94 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('node:assert');
+const { createPrivateKey, createPublicKey, sign, verify } = require('node:crypto');
+const fixtures = require('../common/fixtures');
+const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
+
+const privateKey = createPrivateKey(fixtures.readKey('rsa_private_2048.pem'));
+const publicKey = createPublicKey(privateKey);
+const der = publicKey.export({ format: 'der', type: 'pkcs1' });
+const pem = publicKey.export({ format: 'pem', type: 'pkcs1' });
+const data = Buffer.from('PKCS#1 public key import');
+const signature = sign('sha256', data, privateKey);
+
+for (const [key, format] of [
+  [der, 'der'],
+  [Buffer.concat([der, Buffer.from('trailing data')]), 'der'],
+  [pem, 'pem'],
+  [`leading data\n${pem}trailing data\n`, 'pem'],
+]) {
+  const imported = createPublicKey({ key, format, type: 'pkcs1' });
+  assert.strictEqual(imported.type, 'public');
+  assert.strictEqual(imported.asymmetricKeyType, 'rsa');
+  assert.deepStrictEqual(imported.asymmetricKeyDetails,
+                         publicKey.asymmetricKeyDetails);
+  assert.deepStrictEqual(imported.export({ format: 'der', type: 'pkcs1' }), der);
+  assert.strictEqual(imported.export({ format: 'pem', type: 'pkcs1' }), pem);
+  assert(verify('sha256', data, imported, signature));
+}
+
+// The public PKCS#1 decoder must reject truncated keys and other DER structures.
+for (const invalid of [
+  der.subarray(0, der.length - 1),
+  publicKey.export({ format: 'der', type: 'spki' }),
+]) {
+  assert.throws(() => createPublicKey({
+    key: invalid, format: 'der', type: 'pkcs1',
+  }), { name: 'Error' });
+  assert.throws(() => createPublicKey(
+    `-----BEGIN RSA PUBLIC KEY-----\n${invalid.toString('base64')}\n` +
+    '-----END RSA PUBLIC KEY-----\n',
+  ), { name: 'Error' });
+}
+
+// Public-key creation continues to recognize PKCS#1 private keys separately.
+const privateDer = privateKey.export({ format: 'der', type: 'pkcs1' });
+assert.deepStrictEqual(createPublicKey({
+  key: privateDer, format: 'der', type: 'pkcs1',
+}).export({ format: 'der', type: 'pkcs1' }), der);
+
+// Preserve the ASN.1 forms accepted by the legacy RSA BIGNUM decoder. These
+// tiny keys exercise parsing only, without performing RSA operations.
+if (!isBoringSSL) {
+  for (const hex of [
+    '30800201110201030000',  // Indefinite-length BER SEQUENCE.
+    '300702020011020103',  // Redundant modulus padding.
+    '300702810111020103',  // Non-minimal INTEGER length encoding.
+    '30800201110201030000ffff',  // BER with trailing data.
+  ]) {
+    const imported = createPublicKey({
+      key: Buffer.from(hex, 'hex'), format: 'der', type: 'pkcs1',
+    });
+    assert.strictEqual(imported.type, 'public');
+    assert.strictEqual(imported.asymmetricKeyType, 'rsa');
+  }
+
+  // OpenSSL 4 rejects empty INTEGERs in both legacy and provider decoders.
+  const emptyExponent = {
+    key: Buffer.from('30050201110200', 'hex'), format: 'der', type: 'pkcs1',
+  };
+  if (hasOpenSSL(4)) {
+    assert.throws(() => createPublicKey(emptyExponent), { name: 'Error' });
+  } else {
+    const imported = createPublicKey(emptyExponent);
+    assert.strictEqual(imported.type, 'public');
+    assert.strictEqual(imported.asymmetricKeyType, 'rsa');
+  }
+}
+
+for (const hex of [
+  '3080020111020103',  // Missing BER end-of-contents marker.
+  '30800201110201030201010000',  // Third INTEGER inside BER SEQUENCE.
+  '3006220111020103',  // Constructed INTEGER.
+  '3006020111040103',  // OCTET STRING in place of the exponent.
+  '3009020111020103020101',  // Third INTEGER inside DER SEQUENCE.
+]) {
+  assert.throws(() => createPublicKey({
+    key: Buffer.from(hex, 'hex'), format: 'der', type: 'pkcs1',
+  }), { name: 'Error' });
+}