88#include " ncrypto.h"
99#include " node.h"
1010#include " node_buffer.h"
11+ #include " node_mutex.h"
1112#include " node_options.h"
1213#include " util.h"
1314#include " v8.h"
1415
15- #include < openssl/x509 .h>
16+ #include < openssl/evp .h>
1617#include < openssl/pkcs12.h>
1718#include < openssl/rand.h>
19+ #include < openssl/x509.h>
20+ #include < openssl/x509v3.h>
1821#ifndef OPENSSL_NO_ENGINE
1922#include < openssl/engine.h>
2023#endif // !OPENSSL_NO_ENGINE
2730#include < wincrypt.h>
2831#endif
2932
33+ #include < array>
34+ #include < cstring>
3035#include < set>
3136
3237namespace node {
@@ -1288,6 +1293,223 @@ Local<FunctionTemplate> SecureContext::GetConstructorTemplate(
12881293 return tmpl;
12891294}
12901295
1296+ namespace {
1297+
1298+ constexpr size_t kSpkiHashLength = 32 ; // SHA-256
1299+ using SpkiHash = std::array<uint8_t , kSpkiHashLength >;
1300+
1301+ // SHA-256 hashes of SubjectPublicKeyInfos that must never appear in a
1302+ // verified chain, regardless of what the trust store says. Each entry is
1303+ // the digest of the DER-encoded SPKI (what i2d_X509_PUBKEY produces).
1304+ //
1305+ // These are the eMudhra/emSign intermediates whose private keys were
1306+ // reported compromised in October 2026. Node.js has no equivalent of the
1307+ // browsers' CRLSet/OneCRL and no default online revocation checking, so a
1308+ // chain through one of these keys would otherwise still verify as long as
1309+ // it reaches a bundled root. The same list is shipped by Go
1310+ // (CVE-2026-111943, golang/go#82345) and Electron (electron/electron#55126).
1311+ // Each entry is annotated with the certificate fingerprint and subject from
1312+ // the public revocation records, for cross-referencing with CCADB.
1313+ constexpr SpkiHash kBlockedSPKIs [] = {
1314+ // 0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19
1315+ // C=US, O=eMudhra Inc, CN=EM DV TLS CA - G3C
1316+ {0x49 , 0xdd , 0x35 , 0x3d , 0xc2 , 0xb9 , 0x19 , 0xa3 , 0x77 , 0x06 , 0x45 ,
1317+ 0x11 , 0x7d , 0x0e , 0x60 , 0xc1 , 0x61 , 0x15 , 0xe7 , 0xd1 , 0xbc , 0x25 ,
1318+ 0x9e , 0xef , 0xe7 , 0x34 , 0xb0 , 0x0e , 0xa4 , 0x34 , 0xfe , 0x5f },
1319+ // 432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3
1320+ // C=US, O=eMudhra Inc, CN=EM EV TLS CA - G3C
1321+ {0x5c , 0x06 , 0x60 , 0xa2 , 0xb1 , 0x64 , 0x91 , 0x0d , 0x8f , 0xf5 , 0x89 ,
1322+ 0xc1 , 0x8e , 0x14 , 0x55 , 0x2e , 0x9e , 0x0e , 0xc2 , 0x37 , 0xdc , 0xef ,
1323+ 0xe2 , 0xcb , 0x73 , 0x6d , 0x41 , 0xa4 , 0x81 , 0x1f , 0x81 , 0xa0 },
1324+ // cb6e15165ac802e9b3f993f16c25d41f4e81c21ebde71ae1b012ac00bf9c7c3f
1325+ // C=US, O=eMudhra Inc, CN=EM OV TLS CA - G3C
1326+ {0x63 , 0xec , 0xef , 0xee , 0x43 , 0x1d , 0x49 , 0x52 , 0x3b , 0xc1 , 0x13 ,
1327+ 0x6c , 0x4e , 0x72 , 0xd0 , 0x20 , 0x6a , 0x47 , 0x70 , 0x86 , 0xca , 0xf6 ,
1328+ 0x06 , 0xcd , 0x5c , 0xf0 , 0x25 , 0x68 , 0x6c , 0x10 , 0x9c , 0xf8 },
1329+ // 4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1
1330+ // C=US, O=eMudhra Inc, CN=EM OV CS CA - G3C
1331+ {0x92 , 0xc0 , 0x63 , 0xd6 , 0x7e , 0x8f , 0x14 , 0xdb , 0x71 , 0xd2 , 0x88 ,
1332+ 0xe1 , 0x12 , 0x66 , 0xff , 0xda , 0xf6 , 0x9c , 0xbf , 0x11 , 0x5f , 0x7b ,
1333+ 0xab , 0x14 , 0xf3 , 0xbc , 0x2f , 0x68 , 0xd2 , 0x5e , 0x0e , 0x30 },
1334+ // 42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493
1335+ // C=NL, O=eMudhra BV, CN=EM DV TLS CA - G3B
1336+ {0xeb , 0xed , 0x64 , 0x75 , 0xc2 , 0xc2 , 0xd8 , 0x0f , 0xcb , 0x19 , 0x9f ,
1337+ 0x3f , 0xe8 , 0xdd , 0x6e , 0x6b , 0x3c , 0xe6 , 0x0b , 0x77 , 0xeb , 0x15 ,
1338+ 0xd4 , 0xe4 , 0x2c , 0x3d , 0xec , 0x0c , 0x6f , 0x5b , 0x51 , 0x05 },
1339+ // 9e39de2881273acc225a149c72ba3e0f51424963669ab633ee1dc7df5db067b4
1340+ // C=NL, O=eMudhra BV, CN=EM OV CS CA - G3B
1341+ {0xec , 0x4f , 0xfa , 0x1a , 0xd0 , 0xac , 0x88 , 0x08 , 0x13 , 0x14 , 0xb7 ,
1342+ 0xaa , 0xff , 0xef , 0x6a , 0x91 , 0xf9 , 0x7c , 0xd5 , 0x72 , 0x91 , 0xbb ,
1343+ 0x5b , 0xa6 , 0x86 , 0xc5 , 0x41 , 0x5f , 0x5e , 0xb4 , 0xf6 , 0xb6 },
1344+ // 98b896625c0c230adeec3eb2cf6aad5001ecf9156a68644e82374ce5833bc25b
1345+ // C=NL, O=eMudhra BV, CN=EM OV TLS CA - G3B
1346+ {0x43 , 0xbf , 0xfa , 0x19 , 0xd6 , 0xef , 0x65 , 0x14 , 0x51 , 0xf6 , 0x84 ,
1347+ 0x1e , 0x01 , 0x99 , 0x31 , 0xe9 , 0x6f , 0x6f , 0x2e , 0xae , 0xff , 0x28 ,
1348+ 0xab , 0x4b , 0x6f , 0x1c , 0xe4 , 0x57 , 0x6d , 0xe2 , 0x71 , 0x7b },
1349+ // e7568fe6e12331f4fd7de61da4c7559d0f8eb192a218d4741a3ba75184333838
1350+ // C=NL, O=eMudhra BV, CN=EM EV TLS CA - G3B
1351+ {0xe0 , 0x25 , 0x4c , 0x7f , 0xcd , 0xc2 , 0x65 , 0xe2 , 0xf0 , 0xf8 , 0xa4 ,
1352+ 0x28 , 0xda , 0xbb , 0x75 , 0xab , 0x4a , 0xe0 , 0x39 , 0xb4 , 0x87 , 0x2d ,
1353+ 0x62 , 0xf5 , 0xee , 0x57 , 0xf0 , 0x87 , 0x7d , 0x5a , 0x8a , 0x41 },
1354+ };
1355+
1356+ // Extra entries registered by the test-only hooks on the crypto binding.
1357+ // The list is process-wide and TLS handshakes may run on worker threads, so
1358+ // every access goes through the mutex.
1359+ Mutex& BlockedSPKIsForTestingMutex () {
1360+ static Mutex mutex;
1361+ return mutex;
1362+ }
1363+
1364+ std::vector<SpkiHash>& BlockedSPKIsForTesting () {
1365+ static std::vector<SpkiHash> list;
1366+ return list;
1367+ }
1368+
1369+ bool IsBlockedSPKIForTesting (const SpkiHash& digest) {
1370+ Mutex::ScopedLock lock (BlockedSPKIsForTestingMutex ());
1371+ for (const auto & blocked : BlockedSPKIsForTesting ()) {
1372+ if (digest == blocked) return true ;
1373+ }
1374+ return false ;
1375+ }
1376+
1377+ enum class SpkiCheck { kAllowed , kBlocked , kError };
1378+
1379+ // Hashes the DER-encoded SubjectPublicKeyInfo of `cert` and looks it up in
1380+ // the blocklist. A failure to encode or hash is reported as kError rather
1381+ // than kAllowed so that it cannot be used to slip past the check.
1382+ SpkiCheck CheckSPKI (X509 * cert) {
1383+ if (cert == nullptr ) return SpkiCheck::kAllowed ;
1384+ uint8_t * spki = nullptr ;
1385+ int spki_len = i2d_X509_PUBKEY (X509_get_X509_PUBKEY (cert), &spki);
1386+ if (spki_len <= 0 ) return SpkiCheck::kError ;
1387+ SpkiHash digest;
1388+ unsigned int digest_len = 0 ;
1389+ int ok = EVP_Digest (spki,
1390+ static_cast <size_t >(spki_len),
1391+ digest.data (),
1392+ &digest_len,
1393+ EVP_sha256 (),
1394+ nullptr );
1395+ OPENSSL_free (spki);
1396+ if (ok != 1 || digest_len != digest.size ()) return SpkiCheck::kError ;
1397+ for (const auto & blocked : kBlockedSPKIs ) {
1398+ if (digest == blocked) return SpkiCheck::kBlocked ;
1399+ }
1400+ if (IsBlockedSPKIForTesting (digest)) return SpkiCheck::kBlocked ;
1401+ return SpkiCheck::kAllowed ;
1402+ }
1403+
1404+ // Checks every certificate in `certs`. On kBlocked or kError, `*offender`
1405+ // points at the certificate the result applies to and `*depth` is its index
1406+ // in `certs`.
1407+ SpkiCheck CheckSPKIs (const STACK_OF (X509 ) * certs,
1408+ X509** offender,
1409+ int* depth) {
1410+ if (certs == nullptr ) return SpkiCheck::kAllowed ;
1411+ for (int i = 0 ; i < sk_X509_num (certs); i++) {
1412+ X509 * cert = sk_X509_value (certs, i);
1413+ SpkiCheck result = CheckSPKI (cert);
1414+ if (result != SpkiCheck::kAllowed ) {
1415+ *offender = cert;
1416+ *depth = i;
1417+ return result;
1418+ }
1419+ }
1420+ return SpkiCheck::kAllowed ;
1421+ }
1422+
1423+ // Installed with SSL_CTX_set_cert_verify_callback on every SSL_CTX Node.js
1424+ // creates. It wraps X509_verify_cert so that a chain through a blocked
1425+ // public key is reported as X509_V_ERR_CERT_REVOKED even when the trust
1426+ // store would accept it. The built chain is always checked; when
1427+ // verification did not fully succeed (TLSWrap's per-certificate callback
1428+ // continues past errors so JS can apply rejectUnauthorized, leaving the
1429+ // error set on the context), the leaf and every peer-sent certificate are
1430+ // checked too, so the blocklist takes precedence over the original error.
1431+ //
1432+ // A hit is reported like any other X509_verify_cert error: the error and
1433+ // offending certificate are recorded on the context and the per-certificate
1434+ // verify callback, if any, decides whether the handshake continues. OpenSSL
1435+ // reads the error back into the verify result either way, so
1436+ // socket.authorizationError reports CERT_REVOKED.
1437+ //
1438+ // This is the only cert-verify callback Node.js installs; keep any other
1439+ // per-chain adjustments here, since a second registration would replace it.
1440+ int CertVerifyCallback (X509_STORE_CTX * ctx, void *) {
1441+ int ok = X509_verify_cert (ctx) > 0 ? 1 : 0 ;
1442+ const bool verified = ok == 1 && X509_STORE_CTX_get_error (ctx) == X509_V_OK ;
1443+
1444+ // The depth is the offender's index in the built chain (0 = leaf), as
1445+ // X509_verify_cert would report it. For certificates found only in the
1446+ // peer-sent stack their index there is the closest equivalent.
1447+ X509 * offender = nullptr ;
1448+ int depth = 0 ;
1449+ SpkiCheck result =
1450+ CheckSPKIs (X509_STORE_CTX_get0_chain (ctx), &offender, &depth);
1451+ if (result == SpkiCheck::kAllowed && !verified) {
1452+ X509 * leaf = X509_STORE_CTX_get0_cert (ctx);
1453+ result = CheckSPKI (leaf);
1454+ if (result != SpkiCheck::kAllowed ) {
1455+ offender = leaf;
1456+ depth = 0 ;
1457+ } else {
1458+ result =
1459+ CheckSPKIs (X509_STORE_CTX_get0_untrusted (ctx), &offender, &depth);
1460+ }
1461+ }
1462+ if (result == SpkiCheck::kAllowed ) return ok;
1463+
1464+ X509_STORE_CTX_set_error (ctx,
1465+ result == SpkiCheck::kBlocked
1466+ ? X509_V_ERR_CERT_REVOKED
1467+ : X509_V_ERR_UNSPECIFIED );
1468+ #ifdef OPENSSL_IS_BORINGSSL
1469+ // BoringSSL has no setters for the error depth and current certificate.
1470+ // Node.js' own verify callbacks do not read them, so only the error is
1471+ // recorded there.
1472+ (void )depth;
1473+ (void )offender;
1474+ #else
1475+ X509_STORE_CTX_set_error_depth (ctx, depth);
1476+ X509_STORE_CTX_set_current_cert (ctx, offender);
1477+ #endif
1478+ // The per-certificate callback is the one carried by the SSL object; it is
1479+ // what the library installs on the store context before calling this hook.
1480+ SSL * ssl = static_cast <SSL *>(
1481+ X509_STORE_CTX_get_ex_data (ctx, SSL_get_ex_data_X509_STORE_CTX_idx ()));
1482+ auto verify_cb = ssl != nullptr ? SSL_get_verify_callback (ssl) : nullptr ;
1483+ if (verify_cb == nullptr ) return 0 ;
1484+ return verify_cb (0 , ctx) ? 1 : 0 ;
1485+ }
1486+
1487+ } // namespace
1488+
1489+ void InstallCertVerifyCallback (SSL_CTX * ctx) {
1490+ SSL_CTX_set_cert_verify_callback (ctx, CertVerifyCallback, nullptr );
1491+ }
1492+
1493+ void SecureContext::AddBlockedSPKIForTesting (
1494+ const FunctionCallbackInfo<Value>& args) {
1495+ Environment* env = Environment::GetCurrent (args);
1496+ THROW_AND_RETURN_IF_NOT_BUFFER (env, args[0 ], " SPKI hash" );
1497+ ArrayBufferOrViewContents<uint8_t > hash (args[0 ]);
1498+ if (hash.size () != kSpkiHashLength ) {
1499+ return THROW_ERR_OUT_OF_RANGE (env, " SPKI hash must be 32 bytes" );
1500+ }
1501+ SpkiHash entry;
1502+ memcpy (entry.data (), hash.data (), entry.size ());
1503+ Mutex::ScopedLock lock (BlockedSPKIsForTestingMutex ());
1504+ BlockedSPKIsForTesting ().push_back (entry);
1505+ }
1506+
1507+ void SecureContext::ClearBlockedSPKIsForTesting (
1508+ const FunctionCallbackInfo<Value>& args) {
1509+ Mutex::ScopedLock lock (BlockedSPKIsForTestingMutex ());
1510+ BlockedSPKIsForTesting ().clear ();
1511+ }
1512+
12911513void SecureContext::Initialize (Environment* env, Local<Object> target) {
12921514 Local<Context> context = env->context ();
12931515 SetConstructorFunction (context,
@@ -1311,6 +1533,12 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
13111533 target,
13121534 " startLoadingCertificatesOffThread" ,
13131535 StartLoadingCertificatesOffThread);
1536+ SetMethod (
1537+ context, target, " addBlockedSPKIForTesting" , AddBlockedSPKIForTesting);
1538+ SetMethod (context,
1539+ target,
1540+ " clearBlockedSPKIsForTesting" ,
1541+ ClearBlockedSPKIsForTesting);
13141542}
13151543
13161544void SecureContext::RegisterExternalReferences (
@@ -1356,6 +1584,8 @@ void SecureContext::RegisterExternalReferences(
13561584 registry->Register (ResetRootCertStore);
13571585 registry->Register (GetUserRootCertificates);
13581586 registry->Register (StartLoadingCertificatesOffThread);
1587+ registry->Register (AddBlockedSPKIForTesting);
1588+ registry->Register (ClearBlockedSPKIsForTesting);
13591589}
13601590
13611591SecureContext* SecureContext::Create (Environment* env) {
@@ -1490,6 +1720,7 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
14901720 return ThrowCryptoError (env, ERR_get_error (), " SSL_CTX_new" );
14911721 }
14921722 SSL_CTX_set_app_data (sc->ctx_ .get (), sc);
1723+ InstallCertVerifyCallback (sc->ctx_ .get ());
14931724
14941725 // Disable SSLv2 in the case when method == TLS_method() and the
14951726 // cipher list contains SSLv2 ciphers (not the default, should be rare.)
0 commit comments