镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit e198aa0

Browse files
panvaaduh95
authored andcommitted
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: #66108 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent aafff27 commit e198aa0

3 files changed

Lines changed: 110 additions & 160 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const { isBoringSSL } = require('../../test/common/crypto.js');
5+
const { createPublicKey, generateKeyPairSync } = require('crypto');
6+
7+
if (isBoringSSL) {
8+
console.log('Skipping: RSA-PSS key generation is not supported by BoringSSL');
9+
process.exit(0);
10+
}
11+
12+
const restrictions = {
13+
absent: {},
14+
defaults: { hashAlgorithm: 'sha1', mgf1HashAlgorithm: 'sha1', saltLength: 20 },
15+
sha256: { hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', saltLength: 32 },
16+
};
17+
18+
const bench = common.createBenchmark(main, {
19+
restrictions: Object.keys(restrictions),
20+
n: [5000],
21+
});
22+
23+
function main({ restrictions: name, n }) {
24+
const { privateKey } = generateKeyPairSync('rsa-pss', {
25+
modulusLength: 2048,
26+
...restrictions[name],
27+
});
28+
// Use a fresh KeyObject without decoding DER or reusing cached key details.
29+
bench.start();
30+
for (let index = 0; index < n; index++) {
31+
if (createPublicKey(privateKey).asymmetricKeyDetails.modulusLength !== 2048)
32+
throw new Error('Unexpected modulus length');
33+
}
34+
bench.end(n);
35+
}

‎deps/ncrypto/ncrypto.cc‎

Lines changed: 33 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -3406,34 +3406,6 @@ constexpr bool IsASN1Sequence(const unsigned char* data,
34063406
return true;
34073407
}
34083408

3409-
constexpr bool ReadASN1Element(const unsigned char* data,
3410-
size_t size,
3411-
unsigned char tag,
3412-
size_t* header_size,
3413-
size_t* content_size,
3414-
size_t* total_size) {
3415-
if (size < 2 || data[0] != tag) return false;
3416-
3417-
size_t offset;
3418-
size_t length;
3419-
if (data[1] & 0x80) {
3420-
size_t n_bytes = data[1] & ~0x80;
3421-
if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) return false;
3422-
length = 0;
3423-
for (size_t i = 0; i < n_bytes; i++) length = (length << 8) | data[i + 2];
3424-
offset = 2 + n_bytes;
3425-
} else {
3426-
offset = 2;
3427-
length = data[1];
3428-
}
3429-
3430-
if (offset > size || length > size - offset) return false;
3431-
*header_size = offset;
3432-
*content_size = length;
3433-
*total_size = offset + length;
3434-
return true;
3435-
}
3436-
34373409
constexpr bool IsEncryptedPrivateKeyInfo(
34383410
const Buffer<const unsigned char>& buffer) {
34393411
// Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE.
@@ -6177,147 +6149,48 @@ Rsa::OtherPrimeInfoPointer::OtherPrimeInfoPointer(BignumPointer&& r,
61776149

61786150
#if NCRYPTO_USE_OPENSSL3_PROVIDER
61796151
namespace {
6180-
int DigestAlgorithmIdentifierToNid(const unsigned char* data, size_t size) {
6181-
size_t sequence_header;
6182-
size_t sequence_len;
6183-
size_t sequence_total;
6184-
if (!ReadASN1Element(
6185-
data, size, 0x30, &sequence_header, &sequence_len, &sequence_total)) {
6186-
return NID_undef;
6187-
}
6188-
6189-
size_t oid_header;
6190-
size_t oid_len;
6191-
size_t oid_total;
6192-
const unsigned char* oid = data + sequence_header;
6193-
if (!ReadASN1Element(
6194-
oid, sequence_len, 0x06, &oid_header, &oid_len, &oid_total)) {
6195-
return NID_undef;
6196-
}
6197-
6198-
const unsigned char* oid_data = oid;
6199-
DeleteFnPtr<ASN1_OBJECT, ASN1_OBJECT_free> obj(
6200-
d2i_ASN1_OBJECT(nullptr, &oid_data, oid_total));
6201-
if (!obj) return NID_undef;
6202-
return OBJ_obj2nid(obj.get());
6152+
// Normalizes a provider digest name such as "SHA2-256" to the long name the
6153+
// rest of the key details use ("sha256"). The returned storage has static
6154+
// lifetime, which the string_view fields of PssParams require.
6155+
const char* RsaPssDigestLongName(const char* name) {
6156+
const EVP_MD* md = EVP_get_digestbyname(name);
6157+
if (md == nullptr) return nullptr;
6158+
const int nid = EVP_MD_get_type(md);
6159+
return nid != NID_undef ? OBJ_nid2ln(nid) : nullptr;
62036160
}
62046161

62056162
bool ReadRsaPssParams(const EVP_PKEY* pkey, Rsa::PssParams* params) {
6206-
const int der_len = i2d_PUBKEY(pkey, nullptr);
6207-
if (der_len <= 0) return false;
6208-
6209-
auto der = DataPointer::Alloc(der_len);
6210-
if (!der) return false;
6211-
6212-
auto serialized = static_cast<unsigned char*>(der.get());
6213-
if (i2d_PUBKEY(pkey, &serialized) != der_len) return false;
6214-
6215-
size_t outer_header;
6216-
size_t outer_len;
6217-
size_t outer_total;
6218-
const auto* data = static_cast<const unsigned char*>(der.get());
6219-
if (!ReadASN1Element(
6220-
data, der.size(), 0x30, &outer_header, &outer_len, &outer_total)) {
6221-
return false;
6222-
}
6223-
6224-
size_t alg_header;
6225-
size_t alg_len;
6226-
size_t alg_total;
6227-
const unsigned char* alg = data + outer_header;
6228-
if (!ReadASN1Element(
6229-
alg, outer_len, 0x30, &alg_header, &alg_len, &alg_total)) {
6230-
return false;
6231-
}
6232-
6233-
size_t oid_header;
6234-
size_t oid_len;
6235-
size_t oid_total;
6236-
const unsigned char* oid = alg + alg_header;
6237-
if (!ReadASN1Element(oid, alg_len, 0x06, &oid_header, &oid_len, &oid_total) ||
6238-
oid_total == alg_len) {
6239-
return false;
6240-
}
6241-
6242-
size_t pss_header;
6243-
size_t pss_len;
6244-
size_t pss_total;
6245-
const unsigned char* pss = oid + oid_total;
6246-
if (!ReadASN1Element(
6247-
pss, alg_len - oid_total, 0x30, &pss_header, &pss_len, &pss_total)) {
6163+
// The RSASSA-PSS-params sequence is exposed as a unit. The salt length is
6164+
// readable whenever the sequence is present, including when it is empty
6165+
// because every field carried its default, and unreadable when the algorithm
6166+
// identifier has no parameters at all. That is the distinction between a
6167+
// restricted key and an unrestricted one.
6168+
int salt_length = 0;
6169+
// TODO(panva): In a semver-major, reject malformed RSA-PSS parameters
6170+
// at key import instead of omitting asymmetricKeyDetails fields.
6171+
if (EVP_PKEY_get_int_param(
6172+
pkey, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, &salt_length) != 1 ||
6173+
salt_length < 0) {
62486174
return false;
62496175
}
6176+
params->salt_length = salt_length;
62506177

6251-
const unsigned char* cursor = pss + pss_header;
6252-
size_t remaining = pss_len;
6253-
while (remaining > 0) {
6254-
const unsigned char tag = cursor[0];
6255-
size_t item_header;
6256-
size_t item_len;
6257-
size_t item_total;
6258-
if (!ReadASN1Element(
6259-
cursor, remaining, tag, &item_header, &item_len, &item_total)) {
6260-
return false;
6178+
// The provider may omit default SHA-1 digest parameters. Keep the initialized
6179+
// defaults when a digest name is absent or cannot be resolved.
6180+
char name[80];
6181+
if (EVP_PKEY_get_utf8_string_param(
6182+
pkey, OSSL_PKEY_PARAM_RSA_DIGEST, name, sizeof(name), nullptr) == 1) {
6183+
if (const char* long_name = RsaPssDigestLongName(name)) {
6184+
params->digest = long_name;
62616185
}
6186+
}
62626187

6263-
const unsigned char* item = cursor + item_header;
6264-
switch (tag) {
6265-
case 0xa0: {
6266-
const int nid = DigestAlgorithmIdentifierToNid(item, item_len);
6267-
if (nid != NID_undef) params->digest = OBJ_nid2ln(nid);
6268-
break;
6269-
}
6270-
case 0xa1: {
6271-
size_t mgf_header;
6272-
size_t mgf_len;
6273-
size_t mgf_total;
6274-
if (!ReadASN1Element(
6275-
item, item_len, 0x30, &mgf_header, &mgf_len, &mgf_total)) {
6276-
return false;
6277-
}
6278-
const unsigned char* mgf = item + mgf_header;
6279-
size_t mgf_oid_header;
6280-
size_t mgf_oid_len;
6281-
size_t mgf_oid_total;
6282-
if (!ReadASN1Element(mgf,
6283-
mgf_len,
6284-
0x06,
6285-
&mgf_oid_header,
6286-
&mgf_oid_len,
6287-
&mgf_oid_total) ||
6288-
mgf_oid_total == mgf_len) {
6289-
return false;
6290-
}
6291-
const int nid = DigestAlgorithmIdentifierToNid(mgf + mgf_oid_total,
6292-
mgf_len - mgf_oid_total);
6293-
if (nid != NID_undef) params->mgf1_digest = OBJ_nid2ln(nid);
6294-
break;
6295-
}
6296-
case 0xa2: {
6297-
size_t int_header;
6298-
size_t int_len;
6299-
size_t int_total;
6300-
if (!ReadASN1Element(
6301-
item, item_len, 0x02, &int_header, &int_len, &int_total)) {
6302-
return false;
6303-
}
6304-
// TODO(panva): In a semver-major, reject malformed RSA-PSS parameters
6305-
// at key import instead of omitting asymmetricKeyDetails fields.
6306-
if (int_len == 0 || int_len > sizeof(uint64_t) ||
6307-
(item[int_header] & 0x80) != 0) {
6308-
return false;
6309-
}
6310-
uint64_t salt_length = 0;
6311-
for (size_t n = 0; n < int_len; n++) {
6312-
salt_length = (salt_length << 8) | item[int_header + n];
6313-
}
6314-
params->salt_length = static_cast<int64_t>(salt_length);
6315-
break;
6316-
}
6188+
if (EVP_PKEY_get_utf8_string_param(
6189+
pkey, OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, name, sizeof(name), nullptr) ==
6190+
1) {
6191+
if (const char* long_name = RsaPssDigestLongName(name)) {
6192+
params->mgf1_digest = long_name;
63176193
}
6318-
6319-
cursor += item_total;
6320-
remaining -= item_total;
63216194
}
63226195

63236196
return true;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto) common.skip('missing crypto');
5+
const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
6+
if (!hasOpenSSL(3) || isBoringSSL)
7+
common.skip('requires OpenSSL 3 provider support');
8+
9+
const assert = require('assert');
10+
const fixtures = require('../common/fixtures');
11+
const { createPublicKey } = require('crypto');
12+
13+
const publicKey = createPublicKey(
14+
fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem'));
15+
const der = publicKey.export({ format: 'der', type: 'spki' });
16+
const saltOffset = der.indexOf(Buffer.from([0xa2, 3, 2, 1, 16]));
17+
assert.notStrictEqual(saltOffset, -1);
18+
19+
const publicDetails = { modulusLength: 2048, publicExponent: 65537n };
20+
const restrictedDetails = {
21+
...publicDetails,
22+
hashAlgorithm: 'sha256',
23+
mgf1HashAlgorithm: 'sha256',
24+
saltLength: 16,
25+
};
26+
27+
function assertDetails(encoded, expected) {
28+
const key = createPublicKey({ key: encoded, format: 'der', type: 'spki' });
29+
assert.strictEqual(key.asymmetricKeyType, 'rsa-pss');
30+
assert.deepStrictEqual(key.asymmetricKeyDetails, expected);
31+
}
32+
33+
assertDetails(der, restrictedDetails);
34+
35+
for (const saltLength of [0, 32, 127, -1, -128]) {
36+
const encoded = Buffer.from(der);
37+
encoded.writeInt8(saltLength, saltOffset + 4);
38+
assertDetails(encoded, saltLength < 0 ? publicDetails : {
39+
...restrictedDetails,
40+
saltLength,
41+
});
42+
}

0 commit comments

Comments
 (0)