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
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
#include "ncrypto.h"
1111
#include "node.h"
1212
#include "node_buffer.h"
13+
#include "node_mutex.h"
1314
#include "node_options.h"
1415
#include "util.h"
1516
#include "v8.h"
1617

1718
#ifdef NODE_OPENSSL_HAS_CERT_COMP
1819
#include <openssl/comp.h>
1920
#endif
21+
#include <openssl/evp.h>
2022
#include <openssl/rand.h>
2123
#include <openssl/x509.h>
24+
#include <openssl/x509v3.h>
2225
#ifdef __APPLE__
2326
#include <Security/Security.h>
2427
#endif
@@ -28,6 +31,8 @@
2831
#include <wincrypt.h>
2932
#endif
3033

34+
#include <array>
35+
#include <cstring>
3136
#include <set>
3237

3338
namespace node {
@@ -1374,6 +1379,238 @@ Local<FunctionTemplate> SecureContext::GetConstructorTemplate(
13741379
return tmpl;
13751380
}
13761381

1382+
namespace {
1383+
1384+
constexpr size_t kSpkiHashLength = 32; // SHA-256
1385+
using SpkiHash = std::array<uint8_t, kSpkiHashLength>;
1386+
1387+
// SHA-256 hashes of SubjectPublicKeyInfos that must never appear in a
1388+
// verified chain, regardless of what the trust store says. Each entry is
1389+
// the digest of the DER-encoded SPKI (what i2d_X509_PUBKEY produces).
1390+
//
1391+
// These are the eMudhra/emSign intermediates whose private keys were
1392+
// reported compromised in October 2026. Node.js has no equivalent of the
1393+
// browsers' CRLSet/OneCRL and no default online revocation checking, so a
1394+
// chain through one of these keys would otherwise still verify as long as
1395+
// it reaches a bundled root. The same list is shipped by Go
1396+
// (CVE-2026-111943, golang/go#82345) and Electron (electron/electron#55126).
1397+
// Each entry is annotated with the certificate fingerprint and subject from
1398+
// the public revocation records, for cross-referencing with CCADB.
1399+
constexpr SpkiHash kBlockedSPKIs[] = {
1400+
// 0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19
1401+
// C=US, O=eMudhra Inc, CN=EM DV TLS CA - G3C
1402+
{0x49, 0xdd, 0x35, 0x3d, 0xc2, 0xb9, 0x19, 0xa3, 0x77, 0x06, 0x45,
1403+
0x11, 0x7d, 0x0e, 0x60, 0xc1, 0x61, 0x15, 0xe7, 0xd1, 0xbc, 0x25,
1404+
0x9e, 0xef, 0xe7, 0x34, 0xb0, 0x0e, 0xa4, 0x34, 0xfe, 0x5f},
1405+
// 432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3
1406+
// C=US, O=eMudhra Inc, CN=EM EV TLS CA - G3C
1407+
{0x5c, 0x06, 0x60, 0xa2, 0xb1, 0x64, 0x91, 0x0d, 0x8f, 0xf5, 0x89,
1408+
0xc1, 0x8e, 0x14, 0x55, 0x2e, 0x9e, 0x0e, 0xc2, 0x37, 0xdc, 0xef,
1409+
0xe2, 0xcb, 0x73, 0x6d, 0x41, 0xa4, 0x81, 0x1f, 0x81, 0xa0},
1410+
// cb6e15165ac802e9b3f993f16c25d41f4e81c21ebde71ae1b012ac00bf9c7c3f
1411+
// C=US, O=eMudhra Inc, CN=EM OV TLS CA - G3C
1412+
{0x63, 0xec, 0xef, 0xee, 0x43, 0x1d, 0x49, 0x52, 0x3b, 0xc1, 0x13,
1413+
0x6c, 0x4e, 0x72, 0xd0, 0x20, 0x6a, 0x47, 0x70, 0x86, 0xca, 0xf6,
1414+
0x06, 0xcd, 0x5c, 0xf0, 0x25, 0x68, 0x6c, 0x10, 0x9c, 0xf8},
1415+
// 4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1
1416+
// C=US, O=eMudhra Inc, CN=EM OV CS CA - G3C
1417+
{0x92, 0xc0, 0x63, 0xd6, 0x7e, 0x8f, 0x14, 0xdb, 0x71, 0xd2, 0x88,
1418+
0xe1, 0x12, 0x66, 0xff, 0xda, 0xf6, 0x9c, 0xbf, 0x11, 0x5f, 0x7b,
1419+
0xab, 0x14, 0xf3, 0xbc, 0x2f, 0x68, 0xd2, 0x5e, 0x0e, 0x30},
1420+
// 42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493
1421+
// C=NL, O=eMudhra BV, CN=EM DV TLS CA - G3B
1422+
{0xeb, 0xed, 0x64, 0x75, 0xc2, 0xc2, 0xd8, 0x0f, 0xcb, 0x19, 0x9f,
1423+
0x3f, 0xe8, 0xdd, 0x6e, 0x6b, 0x3c, 0xe6, 0x0b, 0x77, 0xeb, 0x15,
1424+
0xd4, 0xe4, 0x2c, 0x3d, 0xec, 0x0c, 0x6f, 0x5b, 0x51, 0x05},
1425+
// 9e39de2881273acc225a149c72ba3e0f51424963669ab633ee1dc7df5db067b4
1426+
// C=NL, O=eMudhra BV, CN=EM OV CS CA - G3B
1427+
{0xec, 0x4f, 0xfa, 0x1a, 0xd0, 0xac, 0x88, 0x08, 0x13, 0x14, 0xb7,
1428+
0xaa, 0xff, 0xef, 0x6a, 0x91, 0xf9, 0x7c, 0xd5, 0x72, 0x91, 0xbb,
1429+
0x5b, 0xa6, 0x86, 0xc5, 0x41, 0x5f, 0x5e, 0xb4, 0xf6, 0xb6},
1430+
// 98b896625c0c230adeec3eb2cf6aad5001ecf9156a68644e82374ce5833bc25b
1431+
// C=NL, O=eMudhra BV, CN=EM OV TLS CA - G3B
1432+
{0x43, 0xbf, 0xfa, 0x19, 0xd6, 0xef, 0x65, 0x14, 0x51, 0xf6, 0x84,
1433+
0x1e, 0x01, 0x99, 0x31, 0xe9, 0x6f, 0x6f, 0x2e, 0xae, 0xff, 0x28,
1434+
0xab, 0x4b, 0x6f, 0x1c, 0xe4, 0x57, 0x6d, 0xe2, 0x71, 0x7b},
1435+
// e7568fe6e12331f4fd7de61da4c7559d0f8eb192a218d4741a3ba75184333838
1436+
// C=NL, O=eMudhra BV, CN=EM EV TLS CA - G3B
1437+
{0xe0, 0x25, 0x4c, 0x7f, 0xcd, 0xc2, 0x65, 0xe2, 0xf0, 0xf8, 0xa4,
1438+
0x28, 0xda, 0xbb, 0x75, 0xab, 0x4a, 0xe0, 0x39, 0xb4, 0x87, 0x2d,
1439+
0x62, 0xf5, 0xee, 0x57, 0xf0, 0x87, 0x7d, 0x5a, 0x8a, 0x41},
1440+
};
1441+
1442+
// Extra entries registered by the test-only hooks on the crypto binding.
1443+
// The list is process-wide and TLS handshakes may run on worker threads, so
1444+
// every access goes through the mutex.
1445+
Mutex& BlockedSPKIsForTestingMutex() {
1446+
static Mutex mutex;
1447+
return mutex;
1448+
}
1449+
1450+
std::vector<SpkiHash>& BlockedSPKIsForTesting() {
1451+
static std::vector<SpkiHash> list;
1452+
return list;
1453+
}
1454+
1455+
bool IsBlockedSPKIForTesting(const SpkiHash& digest) {
1456+
Mutex::ScopedLock lock(BlockedSPKIsForTestingMutex());
1457+
for (const auto& blocked : BlockedSPKIsForTesting()) {
1458+
if (digest == blocked) return true;
1459+
}
1460+
return false;
1461+
}
1462+
1463+
enum class SpkiCheck { kAllowed, kBlocked, kError };
1464+
1465+
// Hashes the DER-encoded SubjectPublicKeyInfo of `cert` and looks it up in
1466+
// the blocklist. A failure to encode or hash is reported as kError rather
1467+
// than kAllowed so that it cannot be used to slip past the check.
1468+
SpkiCheck CheckSPKI(X509* cert) {
1469+
if (cert == nullptr) return SpkiCheck::kAllowed;
1470+
uint8_t* spki = nullptr;
1471+
int spki_len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &spki);
1472+
if (spki_len <= 0) return SpkiCheck::kError;
1473+
SpkiHash digest;
1474+
unsigned int digest_len = 0;
1475+
int ok = EVP_Digest(spki,
1476+
static_cast<size_t>(spki_len),
1477+
digest.data(),
1478+
&digest_len,
1479+
Digest::SHA256,
1480+
nullptr);
1481+
OPENSSL_free(spki);
1482+
if (ok != 1 || digest_len != digest.size()) return SpkiCheck::kError;
1483+
for (const auto& blocked : kBlockedSPKIs) {
1484+
if (digest == blocked) return SpkiCheck::kBlocked;
1485+
}
1486+
if (IsBlockedSPKIForTesting(digest)) return SpkiCheck::kBlocked;
1487+
return SpkiCheck::kAllowed;
1488+
}
1489+
1490+
// Checks every certificate in `certs`. On kBlocked or kError, `*offender`
1491+
// points at the certificate the result applies to and `*depth` is its index
1492+
// in `certs`.
1493+
SpkiCheck CheckSPKIs(const STACK_OF(X509)* certs,
1494+
X509** offender,
1495+
int* depth) {
1496+
if (certs == nullptr) return SpkiCheck::kAllowed;
1497+
for (int i = 0; i < sk_X509_num(certs); i++) {
1498+
X509* cert = sk_X509_value(certs, i);
1499+
SpkiCheck result = CheckSPKI(cert);
1500+
if (result != SpkiCheck::kAllowed) {
1501+
*offender = cert;
1502+
*depth = i;
1503+
return result;
1504+
}
1505+
}
1506+
return SpkiCheck::kAllowed;
1507+
}
1508+
1509+
// Installed with SSL_CTX_set_cert_verify_callback on every SSL_CTX Node.js
1510+
// creates. It wraps X509_verify_cert so that a chain through a blocked
1511+
// public key is reported as X509_V_ERR_CERT_REVOKED even when the trust
1512+
// store would accept it. The built chain is always checked; when
1513+
// verification did not fully succeed (TLSWrap's per-certificate callback
1514+
// continues past errors so JS can apply rejectUnauthorized, leaving the
1515+
// error set on the context), the leaf and every peer-sent certificate are
1516+
// checked too, so the blocklist takes precedence over the original error.
1517+
//
1518+
// A hit is reported like any other X509_verify_cert error: the error and
1519+
// offending certificate are recorded on the context and the per-certificate
1520+
// verify callback, if any, decides whether the handshake continues. OpenSSL
1521+
// reads the error back into the verify result either way, so
1522+
// socket.authorizationError reports CERT_REVOKED.
1523+
//
1524+
// This is the only cert-verify callback Node.js installs; keep any other
1525+
// per-chain adjustments here, since a second registration would replace it.
1526+
int CertVerifyCallback(X509_STORE_CTX* ctx, void*) {
1527+
#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1)
1528+
X509_VERIFY_PARAM* param = X509_STORE_CTX_get0_param(ctx);
1529+
const unsigned int flags = X509_VERIFY_PARAM_get_hostflags(param);
1530+
if (!(flags & (X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT |
1531+
X509_CHECK_FLAG_NEVER_CHECK_SUBJECT))) {
1532+
// tls.checkServerIdentity() falls back to the CN when no DNS SAN exists.
1533+
// OpenSSL 4.1 requires this flag to apply name constraints to that CN.
1534+
ncrypto::DeleteFnPtr<GENERAL_NAMES, GENERAL_NAMES_free> names(
1535+
static_cast<GENERAL_NAMES*>(
1536+
X509_get_ext_d2i(X509_STORE_CTX_get0_cert(ctx),
1537+
NID_subject_alt_name,
1538+
nullptr,
1539+
nullptr)));
1540+
bool has_dns_san = false;
1541+
for (int i = 0; names && i < sk_GENERAL_NAME_num(names.get()); i++) {
1542+
if (sk_GENERAL_NAME_value(names.get(), i)->type == GEN_DNS) {
1543+
has_dns_san = true;
1544+
break;
1545+
}
1546+
}
1547+
if (!has_dns_san) {
1548+
X509_VERIFY_PARAM_set_hostflags(
1549+
param, flags | X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
1550+
}
1551+
}
1552+
#endif
1553+
1554+
int ok = X509_verify_cert(ctx) > 0 ? 1 : 0;
1555+
const bool verified = ok == 1 && X509_STORE_CTX_get_error(ctx) == X509_V_OK;
1556+
1557+
// The depth is the offender's index in the built chain (0 = leaf), as
1558+
// X509_verify_cert would report it. For certificates found only in the
1559+
// peer-sent stack their index there is the closest equivalent.
1560+
X509* offender = nullptr;
1561+
int depth = 0;
1562+
SpkiCheck result =
1563+
CheckSPKIs(X509_STORE_CTX_get0_chain(ctx), &offender, &depth);
1564+
if (result == SpkiCheck::kAllowed && !verified) {
1565+
X509* leaf = X509_STORE_CTX_get0_cert(ctx);
1566+
result = CheckSPKI(leaf);
1567+
if (result != SpkiCheck::kAllowed) {
1568+
offender = leaf;
1569+
depth = 0;
1570+
} else {
1571+
result = CheckSPKIs(
1572+
X509_STORE_CTX_get0_untrusted(ctx), &offender, &depth);
1573+
}
1574+
}
1575+
if (result == SpkiCheck::kAllowed) return ok;
1576+
1577+
X509_STORE_CTX_set_error(ctx,
1578+
result == SpkiCheck::kBlocked
1579+
? X509_V_ERR_CERT_REVOKED
1580+
: X509_V_ERR_UNSPECIFIED);
1581+
X509_STORE_CTX_set_error_depth(ctx, depth);
1582+
X509_STORE_CTX_set_current_cert(ctx, offender);
1583+
X509_STORE_CTX_verify_cb verify_cb = X509_STORE_CTX_get_verify_cb(ctx);
1584+
if (verify_cb == nullptr) return 0;
1585+
return verify_cb(0, ctx) ? 1 : 0;
1586+
}
1587+
1588+
} // namespace
1589+
1590+
void InstallCertVerifyCallback(SSL_CTX* ctx) {
1591+
SSL_CTX_set_cert_verify_callback(ctx, CertVerifyCallback, nullptr);
1592+
}
1593+
1594+
void SecureContext::AddBlockedSPKIForTesting(
1595+
const FunctionCallbackInfo<Value>& args) {
1596+
Environment* env = Environment::GetCurrent(args);
1597+
THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "SPKI hash");
1598+
ArrayBufferOrViewContents<uint8_t> hash(args[0]);
1599+
if (hash.size() != kSpkiHashLength) {
1600+
return THROW_ERR_OUT_OF_RANGE(env, "SPKI hash must be 32 bytes");
1601+
}
1602+
SpkiHash entry;
1603+
memcpy(entry.data(), hash.data(), entry.size());
1604+
Mutex::ScopedLock lock(BlockedSPKIsForTestingMutex());
1605+
BlockedSPKIsForTesting().push_back(entry);
1606+
}
1607+
1608+
void SecureContext::ClearBlockedSPKIsForTesting(
1609+
const FunctionCallbackInfo<Value>& args) {
1610+
Mutex::ScopedLock lock(BlockedSPKIsForTestingMutex());
1611+
BlockedSPKIsForTesting().clear();
1612+
}
1613+
13771614
void SecureContext::Initialize(Environment* env, Local<Object> target) {
13781615
Local<Context> context = env->context();
13791616
SetConstructorFunction(context,
@@ -1401,6 +1638,14 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
14011638
target,
14021639
"getCertificateCompressionAlgorithms",
14031640
GetCertificateCompressionAlgorithms);
1641+
SetMethod(context,
1642+
target,
1643+
"addBlockedSPKIForTesting",
1644+
AddBlockedSPKIForTesting);
1645+
SetMethod(context,
1646+
target,
1647+
"clearBlockedSPKIsForTesting",
1648+
ClearBlockedSPKIsForTesting);
14041649
}
14051650

14061651
void SecureContext::RegisterExternalReferences(
@@ -1448,6 +1693,8 @@ void SecureContext::RegisterExternalReferences(
14481693
registry->Register(GetUserRootCertificates);
14491694
registry->Register(StartLoadingCertificatesOffThread);
14501695
registry->Register(GetCertificateCompressionAlgorithms);
1696+
registry->Register(AddBlockedSPKIForTesting);
1697+
registry->Register(ClearBlockedSPKIsForTesting);
14511698
}
14521699

14531700
SecureContext* SecureContext::Create(Environment* env) {
@@ -1584,6 +1831,7 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
15841831

15851832
env->external_memory_accounter()->Increase(env->isolate(), kExternalSize);
15861833
SSL_CTX_set_app_data(sc->ctx_.get(), sc);
1834+
InstallCertVerifyCallback(sc->ctx_.get());
15871835

15881836
// OpenSSL populates cert_comp_prefs with all available algorithms by
15891837
// default when compression libraries are linked. Clear them so that

‎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
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <base_object-inl.h>
77
#include <crypto/crypto_bio.h>
88
#include <crypto/crypto_client_hello.h>
9+
#include <crypto/crypto_context.h>
910
#include <crypto/crypto_tls_certificates.h>
1011
#include <crypto/crypto_util.h>
1112
#include <env-inl.h>
@@ -215,6 +216,10 @@ void DTLSContext::New(const FunctionCallbackInfo<Value>& args) {
215216
"Failed to create DTLS SSL_CTX");
216217
}
217218

219+
// DTLS builds its SSL_CTX here instead of through SecureContext, so the
220+
// chain verification hook shared with TLS has to be installed explicitly.
221+
crypto::InstallCertVerifyCallback(ctx.get());
222+
218223
// Default to DTLS 1.2 only. DTLS 1.0 (based on TLS 1.1) is deprecated
219224
// by RFC 8996 and lacks AEAD cipher suites.
220225
//

0 commit comments

Comments
 (0)