mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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>
This commit is contained in:
1 parent
6adf7c1722
commit
59b5ac83ba
3 files changed
+110
-160
No files matched your search
@@ -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);
|
||||
}
|
||||
Vendored
+33
-160
@@ -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)) {
|
||||
// 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;
|
||||
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user