|
9 | 9 | #include "ncrypto.h" |
10 | 10 | #include "node.h" |
11 | 11 | #include "node_buffer.h" |
| 12 | +#include "node_mutex.h" |
12 | 13 | #include "node_options.h" |
13 | 14 | #include "util.h" |
14 | 15 | #include "v8.h" |
15 | 16 |
|
16 | 17 | #ifdef NODE_OPENSSL_HAS_CERT_COMP |
17 | 18 | #include <openssl/comp.h> |
18 | 19 | #endif |
| 20 | +#include <openssl/evp.h> |
19 | 21 | #include <openssl/rand.h> |
20 | 22 | #include <openssl/x509.h> |
| 23 | +#include <openssl/x509v3.h> |
21 | 24 | #ifdef __APPLE__ |
22 | 25 | #include <Security/Security.h> |
23 | 26 | #endif |
|
27 | 30 | #include <wincrypt.h> |
28 | 31 | #endif |
29 | 32 |
|
| 33 | +#include <array> |
| 34 | +#include <cstring> |
30 | 35 | #include <set> |
31 | 36 |
|
32 | 37 | namespace node { |
@@ -1421,6 +1426,250 @@ Local<FunctionTemplate> SecureContext::GetConstructorTemplate( |
1421 | 1426 | return tmpl; |
1422 | 1427 | } |
1423 | 1428 |
|
| 1429 | +namespace { |
| 1430 | + |
| 1431 | +constexpr size_t kSpkiHashLength = 32; // SHA-256 |
| 1432 | +using SpkiHash = std::array<uint8_t, kSpkiHashLength>; |
| 1433 | + |
| 1434 | +// SHA-256 hashes of SubjectPublicKeyInfos that must never appear in a |
| 1435 | +// verified chain, regardless of what the trust store says. Each entry is |
| 1436 | +// the digest of the DER-encoded SPKI (what i2d_X509_PUBKEY produces). |
| 1437 | +// |
| 1438 | +// These are the eMudhra/emSign intermediates whose private keys were |
| 1439 | +// reported compromised in October 2026. Node.js has no equivalent of the |
| 1440 | +// browsers' CRLSet/OneCRL and no default online revocation checking, so a |
| 1441 | +// chain through one of these keys would otherwise still verify as long as |
| 1442 | +// it reaches a bundled root. The same list is shipped by Go |
| 1443 | +// (CVE-2026-111943, golang/go#82345) and Electron (electron/electron#55126). |
| 1444 | +// Each entry is annotated with the certificate fingerprint and subject from |
| 1445 | +// the public revocation records, for cross-referencing with CCADB. |
| 1446 | +constexpr SpkiHash kBlockedSPKIs[] = { |
| 1447 | + // 0164d7ceae5bb909c272af151ff44b1177ae9b47d9424b94f1d9558c30934d19 |
| 1448 | + // C=US, O=eMudhra Inc, CN=EM DV TLS CA - G3C |
| 1449 | + {0x49, 0xdd, 0x35, 0x3d, 0xc2, 0xb9, 0x19, 0xa3, 0x77, 0x06, 0x45, |
| 1450 | + 0x11, 0x7d, 0x0e, 0x60, 0xc1, 0x61, 0x15, 0xe7, 0xd1, 0xbc, 0x25, |
| 1451 | + 0x9e, 0xef, 0xe7, 0x34, 0xb0, 0x0e, 0xa4, 0x34, 0xfe, 0x5f}, |
| 1452 | + // 432ec25f0d28de9006d97bf752f17bbb2cd41f669933fcb072d9a1148d0261f3 |
| 1453 | + // C=US, O=eMudhra Inc, CN=EM EV TLS CA - G3C |
| 1454 | + {0x5c, 0x06, 0x60, 0xa2, 0xb1, 0x64, 0x91, 0x0d, 0x8f, 0xf5, 0x89, |
| 1455 | + 0xc1, 0x8e, 0x14, 0x55, 0x2e, 0x9e, 0x0e, 0xc2, 0x37, 0xdc, 0xef, |
| 1456 | + 0xe2, 0xcb, 0x73, 0x6d, 0x41, 0xa4, 0x81, 0x1f, 0x81, 0xa0}, |
| 1457 | + // cb6e15165ac802e9b3f993f16c25d41f4e81c21ebde71ae1b012ac00bf9c7c3f |
| 1458 | + // C=US, O=eMudhra Inc, CN=EM OV TLS CA - G3C |
| 1459 | + {0x63, 0xec, 0xef, 0xee, 0x43, 0x1d, 0x49, 0x52, 0x3b, 0xc1, 0x13, |
| 1460 | + 0x6c, 0x4e, 0x72, 0xd0, 0x20, 0x6a, 0x47, 0x70, 0x86, 0xca, 0xf6, |
| 1461 | + 0x06, 0xcd, 0x5c, 0xf0, 0x25, 0x68, 0x6c, 0x10, 0x9c, 0xf8}, |
| 1462 | + // 4b2460a466f95fd40113bcdcca2ceb29b3b093921de45474da7df48874c630b1 |
| 1463 | + // C=US, O=eMudhra Inc, CN=EM OV CS CA - G3C |
| 1464 | + {0x92, 0xc0, 0x63, 0xd6, 0x7e, 0x8f, 0x14, 0xdb, 0x71, 0xd2, 0x88, |
| 1465 | + 0xe1, 0x12, 0x66, 0xff, 0xda, 0xf6, 0x9c, 0xbf, 0x11, 0x5f, 0x7b, |
| 1466 | + 0xab, 0x14, 0xf3, 0xbc, 0x2f, 0x68, 0xd2, 0x5e, 0x0e, 0x30}, |
| 1467 | + // 42c4accd12c4f59597925e1a08a7a90102608543809308d50ec427e118ba1493 |
| 1468 | + // C=NL, O=eMudhra BV, CN=EM DV TLS CA - G3B |
| 1469 | + {0xeb, 0xed, 0x64, 0x75, 0xc2, 0xc2, 0xd8, 0x0f, 0xcb, 0x19, 0x9f, |
| 1470 | + 0x3f, 0xe8, 0xdd, 0x6e, 0x6b, 0x3c, 0xe6, 0x0b, 0x77, 0xeb, 0x15, |
| 1471 | + 0xd4, 0xe4, 0x2c, 0x3d, 0xec, 0x0c, 0x6f, 0x5b, 0x51, 0x05}, |
| 1472 | + // 9e39de2881273acc225a149c72ba3e0f51424963669ab633ee1dc7df5db067b4 |
| 1473 | + // C=NL, O=eMudhra BV, CN=EM OV CS CA - G3B |
| 1474 | + {0xec, 0x4f, 0xfa, 0x1a, 0xd0, 0xac, 0x88, 0x08, 0x13, 0x14, 0xb7, |
| 1475 | + 0xaa, 0xff, 0xef, 0x6a, 0x91, 0xf9, 0x7c, 0xd5, 0x72, 0x91, 0xbb, |
| 1476 | + 0x5b, 0xa6, 0x86, 0xc5, 0x41, 0x5f, 0x5e, 0xb4, 0xf6, 0xb6}, |
| 1477 | + // 98b896625c0c230adeec3eb2cf6aad5001ecf9156a68644e82374ce5833bc25b |
| 1478 | + // C=NL, O=eMudhra BV, CN=EM OV TLS CA - G3B |
| 1479 | + {0x43, 0xbf, 0xfa, 0x19, 0xd6, 0xef, 0x65, 0x14, 0x51, 0xf6, 0x84, |
| 1480 | + 0x1e, 0x01, 0x99, 0x31, 0xe9, 0x6f, 0x6f, 0x2e, 0xae, 0xff, 0x28, |
| 1481 | + 0xab, 0x4b, 0x6f, 0x1c, 0xe4, 0x57, 0x6d, 0xe2, 0x71, 0x7b}, |
| 1482 | + // e7568fe6e12331f4fd7de61da4c7559d0f8eb192a218d4741a3ba75184333838 |
| 1483 | + // C=NL, O=eMudhra BV, CN=EM EV TLS CA - G3B |
| 1484 | + {0xe0, 0x25, 0x4c, 0x7f, 0xcd, 0xc2, 0x65, 0xe2, 0xf0, 0xf8, 0xa4, |
| 1485 | + 0x28, 0xda, 0xbb, 0x75, 0xab, 0x4a, 0xe0, 0x39, 0xb4, 0x87, 0x2d, |
| 1486 | + 0x62, 0xf5, 0xee, 0x57, 0xf0, 0x87, 0x7d, 0x5a, 0x8a, 0x41}, |
| 1487 | +}; |
| 1488 | + |
| 1489 | +// Extra entries registered by the test-only hooks on the crypto binding. |
| 1490 | +// The list is process-wide and TLS handshakes may run on worker threads, so |
| 1491 | +// every access goes through the mutex. |
| 1492 | +Mutex& BlockedSPKIsForTestingMutex() { |
| 1493 | + static Mutex mutex; |
| 1494 | + return mutex; |
| 1495 | +} |
| 1496 | + |
| 1497 | +std::vector<SpkiHash>& BlockedSPKIsForTesting() { |
| 1498 | + static std::vector<SpkiHash> list; |
| 1499 | + return list; |
| 1500 | +} |
| 1501 | + |
| 1502 | +bool IsBlockedSPKIForTesting(const SpkiHash& digest) { |
| 1503 | + Mutex::ScopedLock lock(BlockedSPKIsForTestingMutex()); |
| 1504 | + for (const auto& blocked : BlockedSPKIsForTesting()) { |
| 1505 | + if (digest == blocked) return true; |
| 1506 | + } |
| 1507 | + return false; |
| 1508 | +} |
| 1509 | + |
| 1510 | +enum class SpkiCheck { kAllowed, kBlocked, kError }; |
| 1511 | + |
| 1512 | +// Hashes the DER-encoded SubjectPublicKeyInfo of `cert` and looks it up in |
| 1513 | +// the blocklist. A failure to encode or hash is reported as kError rather |
| 1514 | +// than kAllowed so that it cannot be used to slip past the check. |
| 1515 | +SpkiCheck CheckSPKI(X509* cert) { |
| 1516 | + if (cert == nullptr) return SpkiCheck::kAllowed; |
| 1517 | + uint8_t* spki = nullptr; |
| 1518 | + int spki_len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &spki); |
| 1519 | + if (spki_len <= 0) return SpkiCheck::kError; |
| 1520 | + SpkiHash digest; |
| 1521 | + unsigned int digest_len = 0; |
| 1522 | + int ok = EVP_Digest(spki, |
| 1523 | + static_cast<size_t>(spki_len), |
| 1524 | + digest.data(), |
| 1525 | + &digest_len, |
| 1526 | + Digest::SHA256, |
| 1527 | + nullptr); |
| 1528 | + OPENSSL_free(spki); |
| 1529 | + if (ok != 1 || digest_len != digest.size()) return SpkiCheck::kError; |
| 1530 | + for (const auto& blocked : kBlockedSPKIs) { |
| 1531 | + if (digest == blocked) return SpkiCheck::kBlocked; |
| 1532 | + } |
| 1533 | + if (IsBlockedSPKIForTesting(digest)) return SpkiCheck::kBlocked; |
| 1534 | + return SpkiCheck::kAllowed; |
| 1535 | +} |
| 1536 | + |
| 1537 | +// Checks every certificate in `certs`. On kBlocked or kError, `*offender` |
| 1538 | +// points at the certificate the result applies to and `*depth` is its index |
| 1539 | +// in `certs`. |
| 1540 | +SpkiCheck CheckSPKIs(const STACK_OF(X509) * certs, |
| 1541 | + X509** offender, |
| 1542 | + int* depth) { |
| 1543 | + if (certs == nullptr) return SpkiCheck::kAllowed; |
| 1544 | + for (int i = 0; i < sk_X509_num(certs); i++) { |
| 1545 | + X509* cert = sk_X509_value(certs, i); |
| 1546 | + SpkiCheck result = CheckSPKI(cert); |
| 1547 | + if (result != SpkiCheck::kAllowed) { |
| 1548 | + *offender = cert; |
| 1549 | + *depth = i; |
| 1550 | + return result; |
| 1551 | + } |
| 1552 | + } |
| 1553 | + return SpkiCheck::kAllowed; |
| 1554 | +} |
| 1555 | + |
| 1556 | +// Installed with SSL_CTX_set_cert_verify_callback on every SSL_CTX Node.js |
| 1557 | +// creates. It wraps X509_verify_cert so that a chain through a blocked |
| 1558 | +// public key is reported as X509_V_ERR_CERT_REVOKED even when the trust |
| 1559 | +// store would accept it. The built chain is always checked; when |
| 1560 | +// verification did not fully succeed (TLSWrap's per-certificate callback |
| 1561 | +// continues past errors so JS can apply rejectUnauthorized, leaving the |
| 1562 | +// error set on the context), the leaf and every peer-sent certificate are |
| 1563 | +// checked too, so the blocklist takes precedence over the original error. |
| 1564 | +// |
| 1565 | +// A hit is reported like any other X509_verify_cert error: the error and |
| 1566 | +// offending certificate are recorded on the context and the per-certificate |
| 1567 | +// verify callback, if any, decides whether the handshake continues. OpenSSL |
| 1568 | +// reads the error back into the verify result either way, so |
| 1569 | +// socket.authorizationError reports CERT_REVOKED. |
| 1570 | +// |
| 1571 | +// This is the only cert-verify callback Node.js installs; keep any other |
| 1572 | +// per-chain adjustments here, since a second registration would replace it. |
| 1573 | +int CertVerifyCallback(X509_STORE_CTX* ctx, void*) { |
| 1574 | +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1) |
| 1575 | + X509_VERIFY_PARAM* param = X509_STORE_CTX_get0_param(ctx); |
| 1576 | + const unsigned int flags = X509_VERIFY_PARAM_get_hostflags(param); |
| 1577 | + if (!(flags & (X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT | |
| 1578 | + X509_CHECK_FLAG_NEVER_CHECK_SUBJECT))) { |
| 1579 | + // tls.checkServerIdentity() falls back to the CN when no DNS SAN exists. |
| 1580 | + // OpenSSL 4.1 requires this flag to apply name constraints to that CN. |
| 1581 | + ncrypto::DeleteFnPtr<GENERAL_NAMES, GENERAL_NAMES_free> names( |
| 1582 | + static_cast<GENERAL_NAMES*>( |
| 1583 | + X509_get_ext_d2i(X509_STORE_CTX_get0_cert(ctx), |
| 1584 | + NID_subject_alt_name, |
| 1585 | + nullptr, |
| 1586 | + nullptr))); |
| 1587 | + bool has_dns_san = false; |
| 1588 | + for (int i = 0; names && i < sk_GENERAL_NAME_num(names.get()); i++) { |
| 1589 | + if (sk_GENERAL_NAME_value(names.get(), i)->type == GEN_DNS) { |
| 1590 | + has_dns_san = true; |
| 1591 | + break; |
| 1592 | + } |
| 1593 | + } |
| 1594 | + if (!has_dns_san) { |
| 1595 | + X509_VERIFY_PARAM_set_hostflags( |
| 1596 | + param, flags | X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT); |
| 1597 | + } |
| 1598 | + } |
| 1599 | +#endif |
| 1600 | + |
| 1601 | + int ok = X509_verify_cert(ctx) > 0 ? 1 : 0; |
| 1602 | + const bool verified = ok == 1 && X509_STORE_CTX_get_error(ctx) == X509_V_OK; |
| 1603 | + |
| 1604 | + // The depth is the offender's index in the built chain (0 = leaf), as |
| 1605 | + // X509_verify_cert would report it. For certificates found only in the |
| 1606 | + // peer-sent stack their index there is the closest equivalent. |
| 1607 | + X509* offender = nullptr; |
| 1608 | + int depth = 0; |
| 1609 | + SpkiCheck result = |
| 1610 | + CheckSPKIs(X509_STORE_CTX_get0_chain(ctx), &offender, &depth); |
| 1611 | + if (result == SpkiCheck::kAllowed && !verified) { |
| 1612 | + X509* leaf = X509_STORE_CTX_get0_cert(ctx); |
| 1613 | + result = CheckSPKI(leaf); |
| 1614 | + if (result != SpkiCheck::kAllowed) { |
| 1615 | + offender = leaf; |
| 1616 | + depth = 0; |
| 1617 | + } else { |
| 1618 | + result = |
| 1619 | + CheckSPKIs(X509_STORE_CTX_get0_untrusted(ctx), &offender, &depth); |
| 1620 | + } |
| 1621 | + } |
| 1622 | + if (result == SpkiCheck::kAllowed) return ok; |
| 1623 | + |
| 1624 | + X509_STORE_CTX_set_error(ctx, |
| 1625 | + result == SpkiCheck::kBlocked |
| 1626 | + ? X509_V_ERR_CERT_REVOKED |
| 1627 | + : X509_V_ERR_UNSPECIFIED); |
| 1628 | +#ifdef OPENSSL_IS_BORINGSSL |
| 1629 | + // BoringSSL has no setters for the error depth and current certificate. |
| 1630 | + // Node.js' own verify callbacks do not read them, so only the error is |
| 1631 | + // recorded there. |
| 1632 | + (void)depth; |
| 1633 | + (void)offender; |
| 1634 | +#else |
| 1635 | + X509_STORE_CTX_set_error_depth(ctx, depth); |
| 1636 | + X509_STORE_CTX_set_current_cert(ctx, offender); |
| 1637 | +#endif |
| 1638 | + // The per-certificate callback is the one carried by the SSL object; it is |
| 1639 | + // what the library installs on the store context before calling this hook. |
| 1640 | + SSL* ssl = static_cast<SSL*>( |
| 1641 | + X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); |
| 1642 | + auto verify_cb = ssl != nullptr ? SSL_get_verify_callback(ssl) : nullptr; |
| 1643 | + if (verify_cb == nullptr) return 0; |
| 1644 | + return verify_cb(0, ctx) ? 1 : 0; |
| 1645 | +} |
| 1646 | + |
| 1647 | +} // namespace |
| 1648 | + |
| 1649 | +void InstallCertVerifyCallback(SSL_CTX* ctx) { |
| 1650 | + SSL_CTX_set_cert_verify_callback(ctx, CertVerifyCallback, nullptr); |
| 1651 | +} |
| 1652 | + |
| 1653 | +void SecureContext::AddBlockedSPKIForTesting( |
| 1654 | + const FunctionCallbackInfo<Value>& args) { |
| 1655 | + Environment* env = Environment::GetCurrent(args); |
| 1656 | + THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "SPKI hash"); |
| 1657 | + ArrayBufferOrViewContents<uint8_t> hash(args[0]); |
| 1658 | + if (hash.size() != kSpkiHashLength) { |
| 1659 | + return THROW_ERR_OUT_OF_RANGE(env, "SPKI hash must be 32 bytes"); |
| 1660 | + } |
| 1661 | + SpkiHash entry; |
| 1662 | + memcpy(entry.data(), hash.data(), entry.size()); |
| 1663 | + Mutex::ScopedLock lock(BlockedSPKIsForTestingMutex()); |
| 1664 | + BlockedSPKIsForTesting().push_back(entry); |
| 1665 | +} |
| 1666 | + |
| 1667 | +void SecureContext::ClearBlockedSPKIsForTesting( |
| 1668 | + const FunctionCallbackInfo<Value>& args) { |
| 1669 | + Mutex::ScopedLock lock(BlockedSPKIsForTestingMutex()); |
| 1670 | + BlockedSPKIsForTesting().clear(); |
| 1671 | +} |
| 1672 | + |
1424 | 1673 | void SecureContext::Initialize(Environment* env, Local<Object> target) { |
1425 | 1674 | Local<Context> context = env->context(); |
1426 | 1675 | SetConstructorFunction(context, |
@@ -1448,6 +1697,12 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) { |
1448 | 1697 | target, |
1449 | 1698 | "getCertificateCompressionAlgorithms", |
1450 | 1699 | GetCertificateCompressionAlgorithms); |
| 1700 | + SetMethod( |
| 1701 | + context, target, "addBlockedSPKIForTesting", AddBlockedSPKIForTesting); |
| 1702 | + SetMethod(context, |
| 1703 | + target, |
| 1704 | + "clearBlockedSPKIsForTesting", |
| 1705 | + ClearBlockedSPKIsForTesting); |
1451 | 1706 | } |
1452 | 1707 |
|
1453 | 1708 | void SecureContext::RegisterExternalReferences( |
@@ -1495,6 +1750,8 @@ void SecureContext::RegisterExternalReferences( |
1495 | 1750 | registry->Register(GetUserRootCertificates); |
1496 | 1751 | registry->Register(StartLoadingCertificatesOffThread); |
1497 | 1752 | registry->Register(GetCertificateCompressionAlgorithms); |
| 1753 | + registry->Register(AddBlockedSPKIForTesting); |
| 1754 | + registry->Register(ClearBlockedSPKIsForTesting); |
1498 | 1755 | } |
1499 | 1756 |
|
1500 | 1757 | SecureContext* SecureContext::Create(Environment* env) { |
@@ -1631,6 +1888,7 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) { |
1631 | 1888 |
|
1632 | 1889 | env->external_memory_accounter()->Increase(env->isolate(), kExternalSize); |
1633 | 1890 | SSL_CTX_set_app_data(sc->ctx_.get(), sc); |
| 1891 | + InstallCertVerifyCallback(sc->ctx_.get()); |
1634 | 1892 |
|
1635 | 1893 | // OpenSSL populates cert_comp_prefs with all available algorithms by |
1636 | 1894 | // default when compression libraries are linked. Clear them so that |
|
0 commit comments