Commit 59b5ac83ba1 for nodejs

commit 59b5ac83ba1a5a5e0da3694cef447fdf105e191f
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Tue Sep 8 07:52:07 2026 +0200

    crypto: read RSA-PSS restrictions from provider

    Query salt length and digest restrictions instead of serializing the key
    to SPKI and parsing its algorithm identifier. A readable salt length
    distinguishes restricted keys, including empty parameter sequences, from
    unrestricted keys.

    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/benchmark/crypto/rsa-pss-key-details.js b/benchmark/crypto/rsa-pss-key-details.js
new file mode 100644
index 00000000000..18cc8f92f45
--- /dev/null
+++ b/benchmark/crypto/rsa-pss-key-details.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const common = require('../common.js');
+const { isBoringSSL } = require('../../test/common/crypto.js');
+const { createPublicKey, generateKeyPairSync } = require('crypto');
+
+if (isBoringSSL) {
+  console.log('Skipping: RSA-PSS key generation is not supported by BoringSSL');
+  process.exit(0);
+}
+
+const restrictions = {
+  absent: {},
+  defaults: { hashAlgorithm: 'sha1', mgf1HashAlgorithm: 'sha1', saltLength: 20 },
+  sha256: { hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', saltLength: 32 },
+};
+
+const bench = common.createBenchmark(main, {
+  restrictions: Object.keys(restrictions),
+  n: [5000],
+});
+
+function main({ restrictions: name, n }) {
+  const { privateKey } = generateKeyPairSync('rsa-pss', {
+    modulusLength: 2048,
+    ...restrictions[name],
+  });
+  // Use a fresh KeyObject without decoding DER or reusing cached key details.
+  bench.start();
+  for (let index = 0; index < n; index++) {
+    if (createPublicKey(privateKey).asymmetricKeyDetails.modulusLength !== 2048)
+      throw new Error('Unexpected modulus length');
+  }
+  bench.end(n);
+}
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 268875f6b36..bf46d2f4d62 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -3955,34 +3955,6 @@ constexpr bool IsASN1Sequence(const unsigned char* data,
   return true;
 }

-constexpr bool ReadASN1Element(const unsigned char* data,
-                               size_t size,
-                               unsigned char tag,
-                               size_t* header_size,
-                               size_t* content_size,
-                               size_t* total_size) {
-  if (size < 2 || data[0] != tag) return false;
-
-  size_t offset;
-  size_t length;
-  if (data[1] & 0x80) {
-    size_t n_bytes = data[1] & ~0x80;
-    if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) return false;
-    length = 0;
-    for (size_t i = 0; i < n_bytes; i++) length = (length << 8) | data[i + 2];
-    offset = 2 + n_bytes;
-  } else {
-    offset = 2;
-    length = data[1];
-  }
-
-  if (offset > size || length > size - offset) return false;
-  *header_size = offset;
-  *content_size = length;
-  *total_size = offset + length;
-  return true;
-}
-
 constexpr bool IsEncryptedPrivateKeyInfo(
     const Buffer<const unsigned char>& buffer) {
   // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE.
@@ -6744,147 +6716,48 @@ Rsa::OtherPrimeInfoPointer::OtherPrimeInfoPointer(BignumPointer&& r,

 #if NCRYPTO_USE_OPENSSL3_PROVIDER
 namespace {
-int DigestAlgorithmIdentifierToNid(const unsigned char* data, size_t size) {
-  size_t sequence_header;
-  size_t sequence_len;
-  size_t sequence_total;
-  if (!ReadASN1Element(
-          data, size, 0x30, &sequence_header, &sequence_len, &sequence_total)) {
-    return NID_undef;
-  }
-
-  size_t oid_header;
-  size_t oid_len;
-  size_t oid_total;
-  const unsigned char* oid = data + sequence_header;
-  if (!ReadASN1Element(
-          oid, sequence_len, 0x06, &oid_header, &oid_len, &oid_total)) {
-    return NID_undef;
-  }
-
-  const unsigned char* oid_data = oid;
-  DeleteFnPtr<ASN1_OBJECT, ASN1_OBJECT_free> obj(
-      d2i_ASN1_OBJECT(nullptr, &oid_data, oid_total));
-  if (!obj) return NID_undef;
-  return OBJ_obj2nid(obj.get());
+// Normalizes a provider digest name such as "SHA2-256" to the long name the
+// rest of the key details use ("sha256"). The returned storage has static
+// lifetime, which the string_view fields of PssParams require.
+const char* RsaPssDigestLongName(const char* name) {
+  const EVP_MD* md = EVP_get_digestbyname(name);
+  if (md == nullptr) return nullptr;
+  const int nid = EVP_MD_get_type(md);
+  return nid != NID_undef ? OBJ_nid2ln(nid) : nullptr;
 }

 bool ReadRsaPssParams(const EVP_PKEY* pkey, Rsa::PssParams* params) {
-  const int der_len = i2d_PUBKEY(pkey, nullptr);
-  if (der_len <= 0) return false;
-
-  auto der = DataPointer::Alloc(der_len);
-  if (!der) return false;
-
-  auto serialized = static_cast<unsigned char*>(der.get());
-  if (i2d_PUBKEY(pkey, &serialized) != der_len) return false;
-
-  size_t outer_header;
-  size_t outer_len;
-  size_t outer_total;
-  const auto* data = static_cast<const unsigned char*>(der.get());
-  if (!ReadASN1Element(
-          data, der.size(), 0x30, &outer_header, &outer_len, &outer_total)) {
-    return false;
-  }
-
-  size_t alg_header;
-  size_t alg_len;
-  size_t alg_total;
-  const unsigned char* alg = data + outer_header;
-  if (!ReadASN1Element(
-          alg, outer_len, 0x30, &alg_header, &alg_len, &alg_total)) {
-    return false;
-  }
-
-  size_t oid_header;
-  size_t oid_len;
-  size_t oid_total;
-  const unsigned char* oid = alg + alg_header;
-  if (!ReadASN1Element(oid, alg_len, 0x06, &oid_header, &oid_len, &oid_total) ||
-      oid_total == alg_len) {
-    return false;
-  }
-
-  size_t pss_header;
-  size_t pss_len;
-  size_t pss_total;
-  const unsigned char* pss = oid + oid_total;
-  if (!ReadASN1Element(
-          pss, alg_len - oid_total, 0x30, &pss_header, &pss_len, &pss_total)) {
+  // The RSASSA-PSS-params sequence is exposed as a unit. The salt length is
+  // readable whenever the sequence is present, including when it is empty
+  // because every field carried its default, and unreadable when the algorithm
+  // identifier has no parameters at all. That is the distinction between a
+  // restricted key and an unrestricted one.
+  int salt_length = 0;
+  // TODO(panva): In a semver-major, reject malformed RSA-PSS parameters
+  // at key import instead of omitting asymmetricKeyDetails fields.
+  if (EVP_PKEY_get_int_param(
+          pkey, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, &salt_length) != 1 ||
+      salt_length < 0) {
     return false;
   }
+  params->salt_length = salt_length;

-  const unsigned char* cursor = pss + pss_header;
-  size_t remaining = pss_len;
-  while (remaining > 0) {
-    const unsigned char tag = cursor[0];
-    size_t item_header;
-    size_t item_len;
-    size_t item_total;
-    if (!ReadASN1Element(
-            cursor, remaining, tag, &item_header, &item_len, &item_total)) {
-      return false;
+  // The provider may omit default SHA-1 digest parameters. Keep the initialized
+  // defaults when a digest name is absent or cannot be resolved.
+  char name[80];
+  if (EVP_PKEY_get_utf8_string_param(
+          pkey, OSSL_PKEY_PARAM_RSA_DIGEST, name, sizeof(name), nullptr) == 1) {
+    if (const char* long_name = RsaPssDigestLongName(name)) {
+      params->digest = long_name;
     }
+  }

-    const unsigned char* item = cursor + item_header;
-    switch (tag) {
-      case 0xa0: {
-        const int nid = DigestAlgorithmIdentifierToNid(item, item_len);
-        if (nid != NID_undef) params->digest = OBJ_nid2ln(nid);
-        break;
-      }
-      case 0xa1: {
-        size_t mgf_header;
-        size_t mgf_len;
-        size_t mgf_total;
-        if (!ReadASN1Element(
-                item, item_len, 0x30, &mgf_header, &mgf_len, &mgf_total)) {
-          return false;
-        }
-        const unsigned char* mgf = item + mgf_header;
-        size_t mgf_oid_header;
-        size_t mgf_oid_len;
-        size_t mgf_oid_total;
-        if (!ReadASN1Element(mgf,
-                             mgf_len,
-                             0x06,
-                             &mgf_oid_header,
-                             &mgf_oid_len,
-                             &mgf_oid_total) ||
-            mgf_oid_total == mgf_len) {
-          return false;
-        }
-        const int nid = DigestAlgorithmIdentifierToNid(mgf + mgf_oid_total,
-                                                       mgf_len - mgf_oid_total);
-        if (nid != NID_undef) params->mgf1_digest = OBJ_nid2ln(nid);
-        break;
-      }
-      case 0xa2: {
-        size_t int_header;
-        size_t int_len;
-        size_t int_total;
-        if (!ReadASN1Element(
-                item, item_len, 0x02, &int_header, &int_len, &int_total)) {
-          return false;
-        }
-        // TODO(panva): In a semver-major, reject malformed RSA-PSS parameters
-        // at key import instead of omitting asymmetricKeyDetails fields.
-        if (int_len == 0 || int_len > sizeof(uint64_t) ||
-            (item[int_header] & 0x80) != 0) {
-          return false;
-        }
-        uint64_t salt_length = 0;
-        for (size_t n = 0; n < int_len; n++) {
-          salt_length = (salt_length << 8) | item[int_header + n];
-        }
-        params->salt_length = static_cast<int64_t>(salt_length);
-        break;
-      }
+  if (EVP_PKEY_get_utf8_string_param(
+          pkey, OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, name, sizeof(name), nullptr) ==
+      1) {
+    if (const char* long_name = RsaPssDigestLongName(name)) {
+      params->mgf1_digest = long_name;
     }
-
-    cursor += item_total;
-    remaining -= item_total;
   }

   return true;
diff --git a/test/parallel/test-crypto-rsa-pss-parameters.js b/test/parallel/test-crypto-rsa-pss-parameters.js
new file mode 100644
index 00000000000..cda01e7a16d
--- /dev/null
+++ b/test/parallel/test-crypto-rsa-pss-parameters.js
@@ -0,0 +1,42 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto) common.skip('missing crypto');
+const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
+if (!hasOpenSSL(3) || isBoringSSL)
+  common.skip('requires OpenSSL 3 provider support');
+
+const assert = require('assert');
+const fixtures = require('../common/fixtures');
+const { createPublicKey } = require('crypto');
+
+const publicKey = createPublicKey(
+  fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem'));
+const der = publicKey.export({ format: 'der', type: 'spki' });
+const saltOffset = der.indexOf(Buffer.from([0xa2, 3, 2, 1, 16]));
+assert.notStrictEqual(saltOffset, -1);
+
+const publicDetails = { modulusLength: 2048, publicExponent: 65537n };
+const restrictedDetails = {
+  ...publicDetails,
+  hashAlgorithm: 'sha256',
+  mgf1HashAlgorithm: 'sha256',
+  saltLength: 16,
+};
+
+function assertDetails(encoded, expected) {
+  const key = createPublicKey({ key: encoded, format: 'der', type: 'spki' });
+  assert.strictEqual(key.asymmetricKeyType, 'rsa-pss');
+  assert.deepStrictEqual(key.asymmetricKeyDetails, expected);
+}
+
+assertDetails(der, restrictedDetails);
+
+for (const saltLength of [0, 32, 127, -1, -128]) {
+  const encoded = Buffer.from(der);
+  encoded.writeInt8(saltLength, saltOffset + 4);
+  assertDetails(encoded, saltLength < 0 ? publicDetails : {
+    ...restrictedDetails,
+    saltLength,
+  });
+}