Skip to content

Commit ca8ee04

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

3 files changed

Lines changed: 327 additions & 2 deletions

File tree

deps/ncrypto/ncrypto.cc

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
#include <openssl/pkcs12.h>
99
#include <openssl/rand.h>
1010
#include <openssl/x509v3.h>
11+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
12+
#include <openssl/decoder.h>
13+
#endif
1114
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
1215
#include <openssl/bytestring.h>
1316
#include <openssl/cipher.h>
@@ -3864,6 +3867,47 @@ EVPKeyPointer::operator const EC_KEY*() const {
38643867

38653868
namespace {
38663869

3870+
EVP_PKEY* DecodeRsaPublicKey(const unsigned char** data, size_t length) {
3871+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
3872+
// Borrow the EVP_PKEY constructor and its data from a context that stays
3873+
// alive until after the restricted decoder context is destroyed.
3874+
EVP_PKEY* raw = nullptr;
3875+
DeleteFnPtr<OSSL_DECODER_CTX, OSSL_DECODER_CTX_free> construct_ctx(
3876+
OSSL_DECODER_CTX_new_for_pkey(&raw,
3877+
"DER",
3878+
"type-specific",
3879+
KeyAlgorithm::RSA.name(),
3880+
EVP_PKEY_PUBLIC_KEY,
3881+
nullptr,
3882+
nullptr));
3883+
if (!construct_ctx) return nullptr;
3884+
auto* construct = OSSL_DECODER_CTX_get_construct(construct_ctx.get());
3885+
void* construct_data =
3886+
OSSL_DECODER_CTX_get_construct_data(construct_ctx.get());
3887+
if (construct == nullptr || construct_data == nullptr) return nullptr;
3888+
3889+
// Add only the type-specific RSA decoder: new_for_pkey() can also build
3890+
// chains that accept SPKI. The owning context retains the cleanup callback.
3891+
DeleteFnPtr<OSSL_DECODER, OSSL_DECODER_free> decoder(OSSL_DECODER_fetch(
3892+
nullptr, KeyAlgorithm::RSA.name(), "input=der,structure=type-specific"));
3893+
DeleteFnPtr<OSSL_DECODER_CTX, OSSL_DECODER_CTX_free> ctx(
3894+
OSSL_DECODER_CTX_new());
3895+
if (!decoder || !ctx ||
3896+
OSSL_DECODER_CTX_add_decoder(ctx.get(), decoder.get()) != 1 ||
3897+
OSSL_DECODER_CTX_set_input_type(ctx.get(), "DER") != 1 ||
3898+
OSSL_DECODER_CTX_set_selection(ctx.get(), EVP_PKEY_PUBLIC_KEY) != 1 ||
3899+
OSSL_DECODER_CTX_set_construct(ctx.get(), construct) != 1 ||
3900+
OSSL_DECODER_CTX_set_construct_data(ctx.get(), construct_data) != 1) {
3901+
return nullptr;
3902+
}
3903+
const int result = OSSL_DECODER_from_data(ctx.get(), data, &length);
3904+
EVPKeyPointer key(raw);
3905+
return result == 1 ? key.release() : nullptr;
3906+
#else
3907+
return d2i_PublicKey(NID_rsaEncryption, nullptr, data, length);
3908+
#endif
3909+
}
3910+
38673911
EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp,
38683912
const char* name,
38693913
auto&& parse) {
@@ -3991,7 +4035,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM(
39914035
bp,
39924036
"RSA PUBLIC KEY",
39934037
[](const unsigned char** p, long l) { // NOLINT(runtime/int)
3994-
return d2i_PublicKey(NID_rsaEncryption, nullptr, p, l);
4038+
return DecodeRsaPublicKey(p, l);
39954039
})) {
39964040
return ret;
39974041
}
@@ -4026,7 +4070,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKey(
40264070
EVP_PKEY* key = nullptr;
40274071

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

test/cctest/test_node_crypto.cc

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,193 @@ TEST(NodeCrypto, KeyAlgorithmNames) {
8989
EXPECT_FALSE(empty.isA(static_cast<const char*>(nullptr)));
9090
}
9191

92+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
93+
TEST(NodeCrypto, ProviderPkcs1PublicKeyImport) {
94+
ncrypto::ClearErrorOnReturn clear_errors;
95+
auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::RSA);
96+
ASSERT_TRUE(ctx);
97+
ASSERT_TRUE(ctx.initForKeygen());
98+
ASSERT_TRUE(ctx.setRsaKeygenBits(2048));
99+
EVP_PKEY* raw = nullptr;
100+
ASSERT_EQ(EVP_PKEY_keygen(ctx.get(), &raw), 1);
101+
EVPKeyPointer key(raw);
102+
103+
for (const auto format :
104+
{EVPKeyPointer::PKFormatType::PEM, EVPKeyPointer::PKFormatType::DER}) {
105+
const EVPKeyPointer::PublicKeyEncodingConfig config(
106+
false, format, EVPKeyPointer::PKEncodingType::PKCS1);
107+
auto encoded = key.writePublicKey(config);
108+
ASSERT_TRUE(encoded);
109+
const BUF_MEM* mem = encoded.value;
110+
ASSERT_NE(mem, nullptr);
111+
const ncrypto::Buffer<const unsigned char> input{
112+
reinterpret_cast<const unsigned char*>(mem->data), mem->length};
113+
auto imported = EVPKeyPointer::TryParsePublicKey(config, input);
114+
ASSERT_TRUE(imported);
115+
EXPECT_NE(EVP_PKEY_get0_provider(imported.value.get()), nullptr);
116+
EXPECT_TRUE(imported.value.isA(KeyAlgorithm::RSA));
117+
EXPECT_EQ(EVP_PKEY_eq(key.get(), imported.value.get()), 1);
118+
}
119+
}
120+
121+
namespace {
122+
struct RsaLoadTestContext {
123+
OSSL_FUNC_BIO_read_ex_fn* read = nullptr;
124+
int selection = 0;
125+
int loads = 0;
126+
int frees = 0;
127+
};
128+
129+
void* RsaLoadTestDecoderNew(void* context) {
130+
return context;
131+
}
132+
133+
void RsaLoadTestDecoderFree(void*) {}
134+
135+
int RsaLoadTestDecode(void* context,
136+
OSSL_CORE_BIO* input,
137+
int selection,
138+
OSSL_CALLBACK* callback,
139+
void* arg,
140+
OSSL_PASSPHRASE_CALLBACK*,
141+
void*) {
142+
auto* state = static_cast<RsaLoadTestContext*>(context);
143+
unsigned char sentinel = 0;
144+
size_t size = 0;
145+
// Only exercise construction and reference ownership, not ASN.1 parsing.
146+
if (!state->read(input, &sentinel, 1, &size) || size != 1 ||
147+
sentinel != 0x42) {
148+
return 0;
149+
}
150+
state->selection = selection;
151+
char type[] = "RSA";
152+
const OSSL_PARAM params[] = {
153+
OSSL_PARAM_utf8_string(
154+
OSSL_OBJECT_PARAM_DATA_TYPE, type, sizeof(type) - 1),
155+
OSSL_PARAM_octet_string(
156+
OSSL_OBJECT_PARAM_REFERENCE, &state, sizeof(state)),
157+
OSSL_PARAM_END,
158+
};
159+
return callback(params, arg);
160+
}
161+
162+
void* RsaLoadTestLoad(const void* reference, size_t size) {
163+
if (size != sizeof(RsaLoadTestContext*)) return nullptr;
164+
auto* state = *static_cast<RsaLoadTestContext* const*>(reference);
165+
state->loads++;
166+
return state;
167+
}
168+
169+
void RsaLoadTestFree(void* context) {
170+
static_cast<RsaLoadTestContext*>(context)->frees++;
171+
}
172+
173+
int RsaLoadTestHas(const void*, int selection) {
174+
return (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) == 0;
175+
}
176+
177+
const OSSL_ALGORITHM* RsaLoadTestQuery(void*, int operation, int* no_cache) {
178+
*no_cache = 0;
179+
static const OSSL_DISPATCH decoder[] = {
180+
{OSSL_FUNC_DECODER_NEWCTX,
181+
reinterpret_cast<void (*)(void)>(RsaLoadTestDecoderNew)},
182+
{OSSL_FUNC_DECODER_FREECTX,
183+
reinterpret_cast<void (*)(void)>(RsaLoadTestDecoderFree)},
184+
{OSSL_FUNC_DECODER_DECODE,
185+
reinterpret_cast<void (*)(void)>(RsaLoadTestDecode)},
186+
{0, nullptr},
187+
};
188+
static const OSSL_DISPATCH keymgmt[] = {
189+
{OSSL_FUNC_KEYMGMT_LOAD,
190+
reinterpret_cast<void (*)(void)>(RsaLoadTestLoad)},
191+
{OSSL_FUNC_KEYMGMT_FREE,
192+
reinterpret_cast<void (*)(void)>(RsaLoadTestFree)},
193+
{OSSL_FUNC_KEYMGMT_HAS, reinterpret_cast<void (*)(void)>(RsaLoadTestHas)},
194+
{0, nullptr},
195+
};
196+
static const OSSL_ALGORITHM decoders[] = {
197+
{"RSA",
198+
"provider=node-test-rsa-load,input=der,structure=type-specific",
199+
decoder,
200+
"Test RSA decoder without export"},
201+
{nullptr, nullptr, nullptr, nullptr},
202+
};
203+
static const OSSL_ALGORITHM keymgmts[] = {
204+
{"RSA",
205+
"provider=node-test-rsa-load",
206+
keymgmt,
207+
"Test RSA reference load"},
208+
{nullptr, nullptr, nullptr, nullptr},
209+
};
210+
if (operation == OSSL_OP_DECODER) return decoders;
211+
return operation == OSSL_OP_KEYMGMT ? keymgmts : nullptr;
212+
}
213+
214+
void RsaLoadTestTeardown(void* context) {
215+
delete static_cast<RsaLoadTestContext*>(context);
216+
}
217+
218+
int RsaLoadTestProviderInit(const OSSL_CORE_HANDLE*,
219+
const OSSL_DISPATCH* in,
220+
const OSSL_DISPATCH** out,
221+
void** context) {
222+
auto state = std::make_unique<RsaLoadTestContext>();
223+
for (; in->function_id != 0; in++) {
224+
if (in->function_id == OSSL_FUNC_BIO_READ_EX) {
225+
state->read = OSSL_FUNC_BIO_read_ex(in);
226+
}
227+
}
228+
if (state->read == nullptr) return 0;
229+
static const OSSL_DISPATCH dispatch[] = {
230+
{OSSL_FUNC_PROVIDER_QUERY_OPERATION,
231+
reinterpret_cast<void (*)(void)>(RsaLoadTestQuery)},
232+
{OSSL_FUNC_PROVIDER_TEARDOWN,
233+
reinterpret_cast<void (*)(void)>(RsaLoadTestTeardown)},
234+
{0, nullptr},
235+
};
236+
*context = state.release();
237+
*out = dispatch;
238+
return 1;
239+
}
240+
} // namespace
241+
242+
TEST(NodeCrypto, ProviderPkcs1PublicKeyLoadWithoutExport) {
243+
ncrypto::ClearErrorOnReturn clear_errors;
244+
ncrypto::DeleteFnPtr<OSSL_LIB_CTX, OSSL_LIB_CTX_free> libctx(
245+
OSSL_LIB_CTX_new());
246+
ASSERT_TRUE(libctx);
247+
ASSERT_EQ(OSSL_PROVIDER_add_builtin(
248+
libctx.get(), "node-test-rsa-load", RsaLoadTestProviderInit),
249+
1);
250+
auto* provider = OSSL_PROVIDER_load(libctx.get(), "node-test-rsa-load");
251+
auto unload_provider =
252+
node::OnScopeLeave([provider] { OSSL_PROVIDER_unload(provider); });
253+
ASSERT_NE(provider, nullptr);
254+
auto* state = static_cast<RsaLoadTestContext*>(
255+
OSSL_PROVIDER_get0_provider_ctx(provider));
256+
OSSL_LIB_CTX* previous_libctx = OSSL_LIB_CTX_set0_default(libctx.get());
257+
auto restore_libctx = node::OnScopeLeave(
258+
[previous_libctx] { OSSL_LIB_CTX_set0_default(previous_libctx); });
259+
260+
// Neither decoder export nor keymgmt import is available in this provider.
261+
const unsigned char sentinel[] = {0x42};
262+
const EVPKeyPointer::PublicKeyEncodingConfig config(
263+
false,
264+
EVPKeyPointer::PKFormatType::DER,
265+
EVPKeyPointer::PKEncodingType::PKCS1);
266+
{
267+
auto imported = EVPKeyPointer::TryParsePublicKey(config, {sentinel, 1});
268+
ASSERT_TRUE(imported);
269+
EXPECT_EQ(EVP_PKEY_get0_provider(imported.value.get()), provider);
270+
EXPECT_TRUE(imported.value.isA(KeyAlgorithm::RSA));
271+
EXPECT_EQ(state->selection, EVP_PKEY_PUBLIC_KEY);
272+
EXPECT_EQ(state->loads, 1);
273+
EXPECT_EQ(state->frees, 0);
274+
}
275+
EXPECT_EQ(state->frees, 1);
276+
}
277+
#endif
278+
92279
TEST(NodeCrypto, UnsupportedRawExports) {
93280
using Error = EVPKeyPointer::RawExportError;
94281
EVPKeyPointer key;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('node:assert');
8+
const { createPrivateKey, createPublicKey, sign, verify } = require('node:crypto');
9+
const fixtures = require('../common/fixtures');
10+
const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
11+
12+
const privateKey = createPrivateKey(fixtures.readKey('rsa_private_2048.pem'));
13+
const publicKey = createPublicKey(privateKey);
14+
const der = publicKey.export({ format: 'der', type: 'pkcs1' });
15+
const pem = publicKey.export({ format: 'pem', type: 'pkcs1' });
16+
const data = Buffer.from('PKCS#1 public key import');
17+
const signature = sign('sha256', data, privateKey);
18+
19+
for (const [key, format] of [
20+
[der, 'der'],
21+
[Buffer.concat([der, Buffer.from('trailing data')]), 'der'],
22+
[pem, 'pem'],
23+
[`leading data\n${pem}trailing data\n`, 'pem'],
24+
]) {
25+
const imported = createPublicKey({ key, format, type: 'pkcs1' });
26+
assert.strictEqual(imported.type, 'public');
27+
assert.strictEqual(imported.asymmetricKeyType, 'rsa');
28+
assert.deepStrictEqual(imported.asymmetricKeyDetails,
29+
publicKey.asymmetricKeyDetails);
30+
assert.deepStrictEqual(imported.export({ format: 'der', type: 'pkcs1' }), der);
31+
assert.strictEqual(imported.export({ format: 'pem', type: 'pkcs1' }), pem);
32+
assert(verify('sha256', data, imported, signature));
33+
}
34+
35+
// The public PKCS#1 decoder must reject truncated keys and other DER structures.
36+
for (const invalid of [
37+
der.subarray(0, der.length - 1),
38+
publicKey.export({ format: 'der', type: 'spki' }),
39+
]) {
40+
assert.throws(() => createPublicKey({
41+
key: invalid, format: 'der', type: 'pkcs1',
42+
}), { name: 'Error' });
43+
assert.throws(() => createPublicKey(
44+
`-----BEGIN RSA PUBLIC KEY-----\n${invalid.toString('base64')}\n` +
45+
'-----END RSA PUBLIC KEY-----\n',
46+
), { name: 'Error' });
47+
}
48+
49+
// Public-key creation continues to recognize PKCS#1 private keys separately.
50+
const privateDer = privateKey.export({ format: 'der', type: 'pkcs1' });
51+
assert.deepStrictEqual(createPublicKey({
52+
key: privateDer, format: 'der', type: 'pkcs1',
53+
}).export({ format: 'der', type: 'pkcs1' }), der);
54+
55+
// Preserve the ASN.1 forms accepted by the legacy RSA BIGNUM decoder. These
56+
// tiny keys exercise parsing only, without performing RSA operations.
57+
if (!isBoringSSL) {
58+
for (const hex of [
59+
'30800201110201030000', // Indefinite-length BER SEQUENCE.
60+
'300702020011020103', // Redundant modulus padding.
61+
'300702810111020103', // Non-minimal INTEGER length encoding.
62+
'30800201110201030000ffff', // BER with trailing data.
63+
]) {
64+
const imported = createPublicKey({
65+
key: Buffer.from(hex, 'hex'), format: 'der', type: 'pkcs1',
66+
});
67+
assert.strictEqual(imported.type, 'public');
68+
assert.strictEqual(imported.asymmetricKeyType, 'rsa');
69+
}
70+
71+
// OpenSSL 4 rejects empty INTEGERs in both legacy and provider decoders.
72+
const emptyExponent = {
73+
key: Buffer.from('30050201110200', 'hex'), format: 'der', type: 'pkcs1',
74+
};
75+
if (hasOpenSSL(4)) {
76+
assert.throws(() => createPublicKey(emptyExponent), { name: 'Error' });
77+
} else {
78+
const imported = createPublicKey(emptyExponent);
79+
assert.strictEqual(imported.type, 'public');
80+
assert.strictEqual(imported.asymmetricKeyType, 'rsa');
81+
}
82+
}
83+
84+
for (const hex of [
85+
'3080020111020103', // Missing BER end-of-contents marker.
86+
'30800201110201030201010000', // Third INTEGER inside BER SEQUENCE.
87+
'3006220111020103', // Constructed INTEGER.
88+
'3006020111040103', // OCTET STRING in place of the exponent.
89+
'3009020111020103020101', // Third INTEGER inside DER SEQUENCE.
90+
]) {
91+
assert.throws(() => createPublicKey({
92+
key: Buffer.from(hex, 'hex'), format: 'der', type: 'pkcs1',
93+
}), { name: 'Error' });
94+
}

0 commit comments

Comments
 (0)