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,
+ });
+}