Skip to content

Commit 0c4d131

Browse files
mcollinaaduh95
authored andcommitted
crypto: prevent stale OpenSSL errors
Clear errors raised by key comparison, SPKAC parsing, and JWK import. Ensure TLS and private-key parsing only inspect errors from their own OpenSSL calls. Assisted-by: Codex Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #67225 Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent a1bdd2d commit 0c4d131

11 files changed

Lines changed: 283 additions & 5 deletions

‎deps/ncrypto/ncrypto.cc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,7 @@ int64_t PortableTimeGM(struct tm* t) {
933933
// SPKAC
934934

935935
bool VerifySpkac(const char* input, size_t length) {
936+
ClearErrorOnReturn clear_error_on_return;
936937
#ifdef OPENSSL_IS_BORINGSSL
937938
// OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters,
938939
// while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not.
@@ -951,6 +952,7 @@ bool VerifySpkac(const char* input, size_t length) {
951952
}
952953

953954
BIOPointer ExportPublicKey(const char* input, size_t length) {
955+
ClearErrorOnReturn clear_error_on_return;
954956
BIOPointer bio(BIO_new(BIO_s_mem()));
955957
if (!bio) return {};
956958

@@ -972,6 +974,7 @@ BIOPointer ExportPublicKey(const char* input, size_t length) {
972974
}
973975

974976
Buffer<char> ExportChallenge(const char* input, size_t length) {
977+
ClearErrorOnReturn clear_error_on_return;
975978
#ifdef OPENSSL_IS_BORINGSSL
976979
// OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters,
977980
// while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not.
@@ -4542,6 +4545,7 @@ bool ECKeyHasMissingOid(const EVPKeyPointer& key) {
45424545
EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
45434546
const PrivateKeyEncodingConfig& config,
45444547
const Buffer<const unsigned char>& buffer) {
4548+
ERR_clear_error();
45454549
static constexpr auto keyOrError = [](EVPKeyPointer pkey,
45464550
bool had_passphrase = false) {
45474551
if (unsigned long err = ERR_peek_error()) { // NOLINT(runtime/int)

‎src/crypto/crypto_keys.cc‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ namespace node {
2121

2222
using ncrypto::BignumPointer;
2323
using ncrypto::BIOPointer;
24+
using ncrypto::ClearErrorOnReturn;
2425
using ncrypto::ECKeyPointer;
2526
using ncrypto::ECPointPointer;
2627
using ncrypto::EVPKeyCtxPointer;
@@ -680,6 +681,7 @@ static KeyObjectData ImportRawKey(Environment* env,
680681
// Shared helper for importing a JWK asymmetric key. Extracts kty from the
681682
// JWK object and dispatches to the appropriate importer.
682683
static KeyObjectData ImportJWKFromArgs(Environment* env, Local<Object> jwk) {
684+
ClearErrorOnReturn clear_error_on_return;
683685
Local<Value> kty;
684686
if (!jwk->Get(env->context(), env->jwk_kty_string()).ToLocal(&kty) ||
685687
!kty->IsString()) {
@@ -1311,6 +1313,7 @@ void KeyObjectHandle::Equals(const FunctionCallbackInfo<Value>& args) {
13111313
case kKeyTypePublic:
13121314
// Fall through
13131315
case kKeyTypePrivate: {
1316+
ClearErrorOnReturn clear_error_on_return;
13141317
EVP_PKEY* pkey = key.GetAsymmetricKey().get();
13151318
EVP_PKEY* pkey2 = key2.GetAsymmetricKey().get();
13161319
#ifndef OPENSSL_IS_BORINGSSL

‎src/crypto/crypto_tls.cc‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ namespace crypto {
7373

7474
namespace {
7575

76+
void InjectOpenSSLErrorForTesting(const FunctionCallbackInfo<Value>&) {
77+
#if NCRYPTO_USE_OPENSSL_PROVIDER
78+
ERR_raise(ERR_LIB_USER, ERR_R_INTERNAL_ERROR);
79+
#elif NCRYPTO_USE_BORINGSSL
80+
ERR_put_error(ERR_LIB_USER, 0, ERR_R_INTERNAL_ERROR, __FILE__, __LINE__);
81+
#else
82+
#error "Unsupported crypto backend"
83+
#endif
84+
}
85+
86+
void HasOpenSSLErrorForTesting(const FunctionCallbackInfo<Value>& args) {
87+
args.GetReturnValue().Set(ERR_peek_error() != 0);
88+
}
89+
7690
// Our custom implementation of the certificate verify callback
7791
// used when establishing a TLS handshake. Because we cannot perform
7892
// I/O quickly enough with X509_STORE_CTX_ APIs in this callback,
@@ -883,6 +897,7 @@ void TLSWrap::ClearOut() {
883897
for (;;) {
884898
{
885899
SSLLibraryCallScope ssl_library_call_scope(this);
900+
if (ERR_set_mark() != 0) ERR_clear_error();
886901
read = SSL_read(ssl_.get(), out + length, kClearOutChunkSize - length);
887902
}
888903
Debug(this, "Read %d bytes of cleartext output", read);
@@ -1016,6 +1031,7 @@ void TLSWrap::ClearIn() {
10161031
int written;
10171032
{
10181033
SSLLibraryCallScope ssl_library_call_scope(this);
1034+
if (ERR_set_mark() != 0) ERR_clear_error();
10191035
written = SSL_write(ssl_.get(), bs->Data(), bs->ByteLength());
10201036
}
10211037
Debug(this, "Writing %zu bytes, written = %d", bs->ByteLength(), written);
@@ -1214,6 +1230,7 @@ int TLSWrap::DoWrite(WriteWrap* w,
12141230
NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(length);
12151231
{
12161232
SSLLibraryCallScope ssl_library_call_scope(this);
1233+
if (ERR_set_mark() != 0) ERR_clear_error();
12171234
written = SSL_write(ssl_.get(), bs->Data(), length);
12181235
}
12191236
} else {
@@ -1222,6 +1239,7 @@ int TLSWrap::DoWrite(WriteWrap* w,
12221239
NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(buf->len);
12231240
{
12241241
SSLLibraryCallScope ssl_library_call_scope(this);
1242+
if (ERR_set_mark() != 0) ERR_clear_error();
12251243
written = SSL_write(ssl_.get(), buf->base, buf->len);
12261244
}
12271245

@@ -2351,6 +2369,12 @@ void TLSWrap::Initialize(
23512369
Isolate* isolate = env->isolate();
23522370

23532371
SetMethod(context, target, "wrap", TLSWrap::Wrap);
2372+
SetMethod(context,
2373+
target,
2374+
"injectOpenSSLErrorForTesting",
2375+
InjectOpenSSLErrorForTesting);
2376+
SetMethodNoSideEffect(
2377+
context, target, "hasOpenSSLErrorForTesting", HasOpenSSLErrorForTesting);
23542378

23552379
NODE_DEFINE_CONSTANT(target, HAVE_SSL_TRACE);
23562380

@@ -2445,6 +2469,8 @@ void TLSWrap::Initialize(
24452469
void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
24462470
registry->Register(TLSWrap::Wrap);
24472471
registry->Register(GetWriteQueueSize);
2472+
registry->Register(InjectOpenSSLErrorForTesting);
2473+
registry->Register(HasOpenSSLErrorForTesting);
24482474

24492475
registry->Register(CertCbDone);
24502476
registry->Register(ClientHelloDone);

‎test/cctest/test_node_crypto.cc‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ std::string BioContents(const ncrypto::BIOPointer& bio) {
7171
return std::string(mem->data, mem->length);
7272
}
7373

74+
void RaiseTestError() {
75+
#if NCRYPTO_USE_OPENSSL_PROVIDER
76+
ERR_raise(ERR_LIB_USER, ERR_R_INTERNAL_ERROR);
77+
#elif NCRYPTO_USE_BORINGSSL
78+
ERR_put_error(ERR_LIB_USER, 0, ERR_R_INTERNAL_ERROR, __FILE__, __LINE__);
79+
#else
80+
#error "Unsupported crypto backend"
81+
#endif
82+
}
83+
7484
} // namespace
7585

7686
/*
@@ -102,6 +112,50 @@ TEST(NodeCrypto, TryGetIntCipherOutputLength) {
102112
0, static_cast<size_t>(INT_MAX) + 1, &output_len));
103113
}
104114

115+
TEST(NodeCrypto, SpkacOperationsClearErrorQueue) {
116+
ncrypto::ClearErrorOnReturn clear_errors;
117+
constexpr std::string_view invalid = "garbage";
118+
119+
EXPECT_FALSE(ncrypto::VerifySpkac(invalid.data(), invalid.size()));
120+
EXPECT_EQ(ERR_peek_error(), 0UL);
121+
122+
EXPECT_FALSE(ncrypto::ExportPublicKey(invalid.data(), invalid.size()));
123+
EXPECT_EQ(ERR_peek_error(), 0UL);
124+
125+
const auto challenge =
126+
ncrypto::ExportChallenge(invalid.data(), invalid.size());
127+
EXPECT_EQ(challenge.data, nullptr);
128+
EXPECT_EQ(challenge.len, 0UL);
129+
EXPECT_EQ(ERR_peek_error(), 0UL);
130+
}
131+
132+
TEST(NodeCrypto, PrivateKeyParsingIgnoresUnrelatedErrors) {
133+
ncrypto::ClearErrorOnReturn clear_errors;
134+
auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::RSA);
135+
ASSERT_TRUE(ctx);
136+
ASSERT_TRUE(ctx.initForKeygen());
137+
ASSERT_TRUE(ctx.setRsaKeygenBits(2048));
138+
EVP_PKEY* raw = nullptr;
139+
ASSERT_EQ(EVP_PKEY_keygen(ctx.get(), &raw), 1);
140+
EVPKeyPointer key(raw);
141+
142+
const EVPKeyPointer::PrivateKeyEncodingConfig config(
143+
false,
144+
EVPKeyPointer::PKFormatType::PEM,
145+
EVPKeyPointer::PKEncodingType::PKCS8);
146+
auto encoded = key.writePrivateKey(config);
147+
ASSERT_TRUE(encoded);
148+
const BUF_MEM* mem = encoded.value;
149+
ASSERT_NE(mem, nullptr);
150+
151+
RaiseTestError();
152+
ASSERT_NE(ERR_peek_error(), 0UL);
153+
auto parsed = EVPKeyPointer::TryParsePrivateKey(
154+
config, {reinterpret_cast<const unsigned char*>(mem->data), mem->length});
155+
EXPECT_TRUE(parsed);
156+
EXPECT_EQ(ERR_peek_error(), 0UL);
157+
}
158+
105159
TEST(NodeCrypto, KeyAlgorithmNames) {
106160
EXPECT_EQ(KeyAlgorithm::FromName("rsa-pss"), &KeyAlgorithm::RSA_PSS);
107161
EXPECT_EQ(KeyAlgorithm::FromName("ML-DSA-44"), &KeyAlgorithm::ML_DSA_44);

‎test/cctest/test_node_crypto_env.cc‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,56 @@ TEST_F(NodeCryptoEnv, LoadBIO) {
3535
"any errors on the OpenSSL error stack\n";
3636
}
3737

38+
TEST_F(NodeCryptoEnv, KeyObjectEqualsClearsErrorQueue) {
39+
v8::HandleScope handle_scope(isolate_);
40+
Argv argv;
41+
Env env{handle_scope, argv};
42+
43+
ERR_clear_error();
44+
auto result = node::LoadEnvironment(*env, R"JS(
45+
const { generateKeyPairSync } = require('node:crypto');
46+
const rsa = generateKeyPairSync('rsa', { modulusLength: 2048 });
47+
const ec = generateKeyPairSync('ec', { namedCurve: 'P-256' });
48+
return rsa.publicKey.equals(ec.publicKey);
49+
)JS")
50+
.ToLocalChecked();
51+
EXPECT_TRUE(result->IsFalse());
52+
EXPECT_EQ(ERR_peek_error(), 0UL);
53+
}
54+
55+
TEST_F(NodeCryptoEnv, InvalidEcJwkClearsErrorQueue) {
56+
v8::HandleScope handle_scope(isolate_);
57+
Argv argv;
58+
Env env{handle_scope, argv};
59+
60+
ERR_clear_error();
61+
auto result = node::LoadEnvironment(*env, R"JS(
62+
const { diffieHellman, generateKeyPairSync } = require('node:crypto');
63+
const { privateKey } = generateKeyPairSync(
64+
'ec', { namedCurve: 'P-256' });
65+
try {
66+
diffieHellman({
67+
privateKey,
68+
publicKey: {
69+
key: {
70+
kty: 'EC',
71+
crv: 'P-256',
72+
x: 'AAAA',
73+
y: 'AAAA',
74+
},
75+
format: 'jwk',
76+
},
77+
});
78+
return false;
79+
} catch (error) {
80+
return error.code === 'ERR_CRYPTO_INVALID_JWK';
81+
}
82+
)JS")
83+
.ToLocalChecked();
84+
EXPECT_TRUE(result->IsTrue());
85+
EXPECT_EQ(ERR_peek_error(), 0UL);
86+
}
87+
3888
#if NCRYPTO_USE_OPENSSL_PROVIDER
3989
TEST_F(NodeCryptoEnv, ExportIncompleteRsaPrivateKeyAsJwk) {
4090
v8::HandleScope handle_scope(isolate_);

‎test/parallel/test-crypto-encap-decap.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ for (const [name, {
111111
}] of Object.entries(keys)) {
112112
if (!supported) {
113113
assert.throws(() => crypto.encapsulate(publicKey),
114-
{ code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM|ERR_CRYPTO_OPERATION_FAILED/ });
114+
{ code: /ERR_OSSL_UNSUPPORTED|ERR_OSSL_PEM_NO_START_LINE|ERR_CRYPTO_OPERATION_FAILED/ });
115115
continue;
116116
}
117117

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 {
9+
Certificate,
10+
createPrivateKey,
11+
createPublicKey,
12+
diffieHellman,
13+
generateKeyPairSync,
14+
} = require('node:crypto');
15+
const fixtures = require('../common/fixtures');
16+
17+
const privateKeyPem = fixtures.readKey('rsa_private.pem');
18+
const rsaPublicKey = createPublicKey(fixtures.readKey('rsa_public.pem'));
19+
const ec = generateKeyPairSync('ec', { namedCurve: 'P-256' });
20+
21+
function parseValidPrivateKey() {
22+
// This must not pick up an error from the preceding operation.
23+
createPrivateKey(privateKeyPem);
24+
}
25+
26+
assert.strictEqual(rsaPublicKey.equals(ec.publicKey), false);
27+
parseValidPrivateKey();
28+
29+
for (const method of ['verifySpkac', 'exportPublicKey', 'exportChallenge']) {
30+
Certificate[method](Buffer.from('garbage'));
31+
parseValidPrivateKey();
32+
}
33+
34+
assert.throws(() => diffieHellman({
35+
privateKey: ec.privateKey,
36+
publicKey: {
37+
key: {
38+
kty: 'EC',
39+
crv: 'P-256',
40+
x: 'AAAA',
41+
y: 'AAAA',
42+
},
43+
format: 'jwk',
44+
},
45+
}), { code: 'ERR_CRYPTO_INVALID_JWK' });
46+
parseValidPrivateKey();

‎test/parallel/test-crypto-pqc-key-objects-ml-dsa.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ for (const [asymmetricKeyType, pubLen] of [
102102

103103
if (!hasOpenSSL(3, 5) && !isBoringSSL) {
104104
assert.throws(() => createPublicKey(keys.public), {
105-
code: 'ERR_OSSL_EVP_DECODE_ERROR',
105+
code: 'ERR_OSSL_UNSUPPORTED',
106106
});
107107

108108
for (const pem of [keys.private, keys.private_seed_only, keys.private_priv_only]) {

‎test/parallel/test-crypto-pqc-key-objects-ml-kem.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ for (const [asymmetricKeyType, pubLen] of [
102102

103103
if (!hasOpenSSL(3, 5) && !isBoringSSL) {
104104
assert.throws(() => createPublicKey(keys.public), {
105-
code: 'ERR_OSSL_EVP_DECODE_ERROR',
105+
code: 'ERR_OSSL_UNSUPPORTED',
106106
});
107107

108108
for (const pem of [keys.private, keys.private_seed_only, keys.private_priv_only]) {
@@ -113,7 +113,7 @@ for (const [asymmetricKeyType, pubLen] of [
113113
} else if (isBoringSSL && asymmetricKeyType === 'ml-kem-512') {
114114
// BoringSSL does not support ML-KEM-512.
115115
assert.throws(() => createPublicKey(keys.public),
116-
{ code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' });
116+
{ code: 'ERR_OSSL_PEM_NO_START_LINE' });
117117
for (const pem of [keys.private, keys.private_seed_only, keys.private_priv_only]) {
118118
assert.throws(() => createPrivateKey(pem),
119119
{ code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' });

‎test/parallel/test-crypto-pqc-key-objects-slh-dsa.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ for (const asymmetricKeyType of [
101101

102102
if (!hasOpenSSL(3, 5)) {
103103
assert.throws(() => createPublicKey(keys.public), {
104-
code: isBoringSSL ? 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' : 'ERR_OSSL_EVP_DECODE_ERROR',
104+
code: isBoringSSL ? 'ERR_OSSL_PEM_NO_START_LINE' : 'ERR_OSSL_UNSUPPORTED',
105105
});
106106

107107
assert.throws(() => createPrivateKey(keys.private), {

0 commit comments

Comments
 (0)