tls: reject chains containing blocked public keys · nodejs/node@db7cfbb · GitHub
Skip to content

Commit db7cfbb

Browse files
committed
tls: reject chains containing blocked public keys
Node.js embeds the Mozilla root store but has no equivalent of the browsers' CRLSet/OneCRL and no default online revocation checking, so a certificate chain through a compromised intermediate still verifies as long as it reaches a trusted root. Removing the affected roots would over-block, since other legitimate intermediates chain to them. Add a small compiled-in blocklist of SubjectPublicKeyInfo SHA-256 hashes and check every verified chain against it from a single SSL_CTX_set_cert_verify_callback hook. A hit is reported as X509_V_ERR_CERT_REVOKED through the normal verification path, so it honours rejectUnauthorized and surfaces as authorizationError on both client and server sockets. The hook is installed for every SSL_CTX Node.js creates: SecureContext, QUIC and DTLS. The OpenSSL 4.1 CN name constraint handling previously registered from crypto_tls.cc moves into the same hook, since OpenSSL allows only one. The initial entries are the eight eMudhra/emSign intermediates whose private keys were reported compromised in October 2026. The same list is shipped by Go (CVE-2026-111943) and Electron. Sessions resumed from before this change are not re-verified, as is standard for TLS resumption; the exposure ends with the session timeout. Test-only hooks on the crypto binding, reachable with --expose-internals, allow the test to block a fixture key and exercise the full handshake. Refs: electron/electron#55126 Refs: golang/go#82345 Assisted-by: Claude, Codex Signed-off-by: Ulises Gascon <ulisesgascongonzalez@gmail.com>
1 parent 531d28f commit db7cfbb

6 files changed

Lines changed: 532 additions & 33 deletions

File tree

‎src/crypto/crypto_context.cc‎

Lines changed: 248 additions & 0 deletions

‎src/crypto/crypto_context.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ X509_STORE* NewRootCertStore(Environment* env);
2727

2828
X509_STORE* GetOrCreateRootCertStore(Environment* env);
2929

30+
// Installs the certificate verification callback that applies the SPKI
31+
// blocklist (and, on OpenSSL 4.1+, the CN name-constraint handling) to every
32+
// chain verified through `ctx`. Must be called for every SSL_CTX Node.js
33+
// creates, since SSL_CTX_set_cert_verify_callback replaces any earlier hook.
34+
void InstallCertVerifyCallback(SSL_CTX* ctx);
35+
3036
ncrypto::BIOPointer LoadBIO(Environment* env, v8::Local<v8::Value> v);
3137

3238
class SecureContext final : public BaseObject {
@@ -154,6 +160,15 @@ class SecureContext final : public BaseObject {
154160
template <bool primary>
155161
static void GetCertificate(const v8::FunctionCallbackInfo<v8::Value>& args);
156162

163+
// Test-only hooks for the SPKI blocklist that every SSL_CTX applies
164+
// during certificate verification. Reachable through
165+
// internalBinding('crypto') with --expose-internals. The argument is the
166+
// SHA-256 of a DER-encoded SubjectPublicKeyInfo.
167+
static void AddBlockedSPKIForTesting(
168+
const v8::FunctionCallbackInfo<v8::Value>& args);
169+
static void ClearBlockedSPKIsForTesting(
170+
const v8::FunctionCallbackInfo<v8::Value>& args);
171+
157172
static int TicketKeyCallback(SSL* ssl,
158173
unsigned char* name,
159174
unsigned char* iv,

‎src/crypto/crypto_tls.cc‎

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -73,36 +73,6 @@ namespace crypto {
7373

7474
namespace {
7575

76-
#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1)
77-
int VerifyCertChain(X509_STORE_CTX* ctx, void*) {
78-
X509_VERIFY_PARAM* param = X509_STORE_CTX_get0_param(ctx);
79-
const unsigned int flags = X509_VERIFY_PARAM_get_hostflags(param);
80-
if (!(flags & (X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT |
81-
X509_CHECK_FLAG_NEVER_CHECK_SUBJECT))) {
82-
// tls.checkServerIdentity() falls back to the CN when no DNS SAN exists.
83-
// OpenSSL 4.1 requires this flag to apply name constraints to that CN.
84-
ncrypto::DeleteFnPtr<GENERAL_NAMES, GENERAL_NAMES_free> names(
85-
static_cast<GENERAL_NAMES*>(
86-
X509_get_ext_d2i(X509_STORE_CTX_get0_cert(ctx),
87-
NID_subject_alt_name,
88-
nullptr,
89-
nullptr)));
90-
bool has_dns_san = false;
91-
for (int i = 0; names && i < sk_GENERAL_NAME_num(names.get()); i++) {
92-
if (sk_GENERAL_NAME_value(names.get(), i)->type == GEN_DNS) {
93-
has_dns_san = true;
94-
break;
95-
}
96-
}
97-
if (!has_dns_san) {
98-
X509_VERIFY_PARAM_set_hostflags(
99-
param, flags | X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
100-
}
101-
}
102-
return X509_verify_cert(ctx) > 0 ? 1 : 0;
103-
}
104-
#endif
105-
10676
// Our custom implementation of the certificate verify callback
10777
// used when establishing a TLS handshake. Because we cannot perform
10878
// I/O quickly enough with X509_STORE_CTX_ APIs in this callback,
@@ -360,9 +330,10 @@ int TLSExtStatusCallback(SSL* s, void* arg) {
360330
void ConfigureSecureContext(SecureContext* sc) {
361331
// OCSP stapling
362332
sc->ctx().setStatusCallback(TLSExtStatusCallback);
363-
#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1)
364-
SSL_CTX_set_cert_verify_callback(sc->ctx().get(), VerifyCertChain, nullptr);
365-
#endif
333+
// The cert-verify callback (SPKI blocklist and the OpenSSL 4.1 CN name
334+
// constraint handling) is installed once per SSL_CTX in
335+
// SecureContext::Init, so that it also covers contexts created without a
336+
// TLSWrap.
366337
}
367338

368339
inline bool Set(

‎src/dtls/dtls_context.cc‎

Lines changed: 5 additions & 0 deletions

0 commit comments

Comments
 (0)