@@ -1438,32 +1438,137 @@ constexpr SpkiHash kBlockedSPKIs[] = {
14381438 0xfc , 0xd8 , 0xdd , 0x55 , 0xa8 , 0x75 , 0xd7 , 0x20 , 0xaa , 0xc2 },
14391439};
14401440
1441+ // Roots that stay in the bundle but whose trust for TLS server certificates
1442+ // is limited by issuance date, mirroring Mozilla's
1443+ // CKA_NSS_SERVER_DISTRUST_AFTER in certdata.txt. When a client verifies a
1444+ // server chain through one of these keys and the leaf certificate's
1445+ // notBefore is after the cutoff, the chain is rejected; certificates issued
1446+ // up to the cutoff keep working. Client certificates are not affected, see
1447+ // CertVerifyCallback. tools/mk-ca-bundle.pl does not carry this attribute
1448+ // into node_root_certs.h, so the dates are maintained here. The cutoff is
1449+ // the last valid second, as UTC seconds since the epoch.
1450+ //
1451+ // Node.js has no Certificate Transparency checking, so unlike Firefox it
1452+ // cannot detect a backdated notBefore. The same limitation applied to the
1453+ // StartCom/WoSign cutoff Node.js enforced in 2016.
1454+ struct DistrustAfter {
1455+ SpkiHash spki;
1456+ time_t cutoff;
1457+ };
1458+
1459+ constexpr DistrustAfter kDistrustAfter [] = {
1460+ // emSign Root CA - G1, cutoff 2026-11-16 23:59:59 UTC.
1461+ // https://bugzilla.mozilla.org/show_bug.cgi?id=2089115
1462+ // 40f6af0346a99aa1cd1d555a4e9cce62c7f9634603ee406615833dc8c8d00367
1463+ // C=IN, OU=emSign PKI, O=eMudhra Technologies Limited,
1464+ // CN=emSign Root CA - G1
1465+ {{0x37 , 0x6a , 0x1a , 0x70 , 0x82 , 0xa5 , 0x93 , 0xdc , 0xcc , 0x20 , 0xd5 ,
1466+ 0x61 , 0xd1 , 0x19 , 0xe9 , 0xab , 0x8d , 0x30 , 0xf1 , 0x1c , 0xc3 , 0x21 ,
1467+ 0xd0 , 0xa3 , 0x7f , 0xa4 , 0x1f , 0x0d , 0xf2 , 0x84 , 0xe0 , 0x1c },
1468+ 1794873599 }, // $ date -u -d @1794873599 = 2026-11-16 23:59:59 UTC
1469+ // Izenpe.com, cutoff 2026-04-15 23:59:59 UTC.
1470+ // CKA_NSS_SERVER_DISTRUST_AFTER in tools/certdata.txt
1471+ // 2530cc8e98321502bad96f9b1fba1b099e2d299e0f4548bb914f363bc0d4531f
1472+ // C=ES, O=IZENPE S.A., CN=Izenpe.com
1473+ {{0x95 , 0x2c , 0x20 , 0x39 , 0xc0 , 0x24 , 0x3e , 0xb5 , 0x15 , 0xdd , 0x73 ,
1474+ 0xd8 , 0x3f , 0xc3 , 0x64 , 0x31 , 0x84 , 0x87 , 0x4f , 0xeb , 0x08 , 0x62 ,
1475+ 0xa9 , 0x83 , 0x77 , 0x31 , 0xed , 0x9b , 0x47 , 0x42 , 0xe1 , 0x7a },
1476+ 1776297599 }, // $ date -u -d @1776297599 = 2026-04-15 23:59:59 UTC
1477+ // Autoridad de Certificacion Firmaprofesional CIF A62634068 (2014 root),
1478+ // cutoff 2026-08-06 07:01:00 UTC.
1479+ // CKA_NSS_SERVER_DISTRUST_AFTER in Mozilla's certdata.txt, newer than the
1480+ // copy in tools/certdata.txt
1481+ // 57de0583efd2b26e0361da99da9df4648def7ee8441c3b728afa9bcde0f9b26a
1482+ // C=ES, CN=Autoridad de Certificacion Firmaprofesional CIF A62634068
1483+ {{0x3b , 0x0d , 0x73 , 0xb4 , 0xbe , 0x4a , 0x85 , 0x4a , 0xdc , 0x3e , 0x51 ,
1484+ 0xd7 , 0xef , 0x9f , 0xa4 , 0x8a , 0xef , 0xbb , 0x2c , 0xdd , 0x82 , 0x4d ,
1485+ 0x67 , 0xbd , 0xc7 , 0xd7 , 0xd0 , 0x9a , 0x2a , 0xbc , 0x2d , 0x43 },
1486+ 1785999660 }, // $ date -u -d @1785999660 = 2026-08-06 07:01:00 UTC
1487+ // Test fixture, not a real CA: test/fixtures/keys/distrust-after-ca-key.pem
1488+ // with a cutoff of 2019-12-31 23:59:59 UTC. Its private key is public, so
1489+ // date-limiting it costs nothing and lets
1490+ // test/parallel/test-tls-distrust-after.js exercise the real list. The
1491+ // key is pinned in the fixtures Makefile; rotating it means updating this
1492+ // entry too.
1493+ // C=US, ST=CA, L=SF, O=Joyent, OU=Node.js, CN=distrust-after-ca
1494+ {{0x76 , 0xf8 , 0xf8 , 0x4a , 0xd8 , 0x4f , 0x02 , 0x4a , 0x56 , 0xc7 , 0x0c ,
1495+ 0x0f , 0xd5 , 0xff , 0x21 , 0x72 , 0x18 , 0x11 , 0x3b , 0xca , 0x81 , 0x44 ,
1496+ 0x5f , 0xe9 , 0x23 , 0x8d , 0xc1 , 0xad , 0x06 , 0xb9 , 0xa5 , 0xd8 },
1497+ 1577836799 }, // $ date -u -d @1577836799 = 2019-12-31 23:59:59 UTC
1498+ };
1499+
14411500enum class SpkiCheck { kAllowed , kBlocked , kError };
14421501
1443- // Hashes the DER-encoded SubjectPublicKeyInfo of `cert` and looks it up in
1444- // the blocklist. A failure to encode or hash is reported as kError rather
1445- // than kAllowed so that it cannot be used to slip past the check.
1446- SpkiCheck CheckSPKI (X509 * cert) {
1447- if (cert == nullptr ) return SpkiCheck::kAllowed ;
1502+ // Hashes the DER-encoded SubjectPublicKeyInfo of `cert` into `*digest`.
1503+ // Returns false if the key could not be encoded or hashed.
1504+ bool HashSPKI (X509 * cert, SpkiHash* digest) {
14481505 uint8_t * spki = nullptr ;
14491506 int spki_len = i2d_X509_PUBKEY (X509_get_X509_PUBKEY (cert), &spki);
1450- if (spki_len <= 0 ) return SpkiCheck::kError ;
1451- SpkiHash digest;
1507+ if (spki_len <= 0 ) return false ;
14521508 unsigned int digest_len = 0 ;
14531509 int ok = EVP_Digest (spki,
14541510 static_cast <size_t >(spki_len),
1455- digest. data (),
1511+ digest-> data (),
14561512 &digest_len,
14571513 Digest::SHA256 ,
14581514 nullptr );
14591515 OPENSSL_free (spki);
1460- if (ok != 1 || digest_len != digest.size ()) return SpkiCheck::kError ;
1516+ return ok == 1 && digest_len == digest->size ();
1517+ }
1518+
1519+ // Looks up the SubjectPublicKeyInfo of `cert` in the blocklist. A failure
1520+ // to encode or hash is reported as kError rather than kAllowed so that it
1521+ // cannot be used to slip past the check.
1522+ SpkiCheck CheckSPKI (X509 * cert) {
1523+ if (cert == nullptr ) return SpkiCheck::kAllowed ;
1524+ SpkiHash digest;
1525+ if (!HashSPKI (cert, &digest)) return SpkiCheck::kError ;
14611526 for (const auto & blocked : kBlockedSPKIs ) {
14621527 if (digest == blocked) return SpkiCheck::kBlocked ;
14631528 }
14641529 return SpkiCheck::kAllowed ;
14651530}
14661531
1532+ // Applies kDistrustAfter to `leaf` for a chain containing `cert`: kBlocked
1533+ // when `cert` is a date-limited root and `leaf` was issued after its cutoff,
1534+ // kError when the key or the date could not be read.
1535+ SpkiCheck CheckDistrustAfter (X509 * cert, X509 * leaf) {
1536+ if (cert == nullptr || leaf == nullptr ) return SpkiCheck::kAllowed ;
1537+ SpkiHash digest;
1538+ if (!HashSPKI (cert, &digest)) return SpkiCheck::kError ;
1539+ for (const auto & entry : kDistrustAfter ) {
1540+ if (digest != entry.spki ) continue ;
1541+ time_t cutoff = entry.cutoff ;
1542+ // X509_cmp_time returns 1 when notBefore is later than cutoff, -1 when
1543+ // earlier or equal, and 0 when the time could not be parsed. A leaf
1544+ // issued exactly at the cutoff second is therefore still accepted.
1545+ int cmp = X509_cmp_time (X509_get0_notBefore (leaf), &cutoff);
1546+ if (cmp == 0 ) return SpkiCheck::kError ;
1547+ if (cmp > 0 ) return SpkiCheck::kBlocked ;
1548+ }
1549+ return SpkiCheck::kAllowed ;
1550+ }
1551+
1552+ // Checks every certificate in `certs` against kDistrustAfter for `leaf`. On
1553+ // kBlocked or kError, `*offender` points at the date-limited certificate and
1554+ // `*depth` is its index in `certs`.
1555+ SpkiCheck CheckDistrustAfterChain (const STACK_OF (X509 ) * certs,
1556+ X509* leaf,
1557+ X509** offender,
1558+ int* depth) {
1559+ if (certs == nullptr ) return SpkiCheck::kAllowed ;
1560+ for (OPENSSL_SIZE_T i = 0 ; i < sk_X509_num (certs); i++) {
1561+ X509 * cert = sk_X509_value (certs, i);
1562+ SpkiCheck result = CheckDistrustAfter (cert, leaf);
1563+ if (result != SpkiCheck::kAllowed ) {
1564+ *offender = cert;
1565+ *depth = i;
1566+ return result;
1567+ }
1568+ }
1569+ return SpkiCheck::kAllowed ;
1570+ }
1571+
14671572// Checks every certificate in `certs`. On kBlocked or kError, `*offender`
14681573// points at the certificate the result applies to and `*depth` is its index
14691574// in `certs`.
@@ -1485,12 +1590,13 @@ SpkiCheck CheckSPKIs(const STACK_OF(X509) * certs,
14851590
14861591// Installed with SSL_CTX_set_cert_verify_callback on every SSL_CTX Node.js
14871592// creates. It wraps X509_verify_cert so that a chain through a blocked
1488- // public key is reported as X509_V_ERR_CERT_REVOKED even when the trust
1489- // store would accept it. The built chain is always checked; when
1490- // verification did not fully succeed (TLSWrap's per-certificate callback
1491- // continues past errors so JS can apply rejectUnauthorized, leaving the
1492- // error set on the context), the leaf and every peer-sent certificate are
1493- // checked too, so the blocklist takes precedence over the original error.
1593+ // public key, or through a date-limited root with a leaf issued after the
1594+ // cutoff, is reported as X509_V_ERR_CERT_REVOKED even when the trust store
1595+ // would accept it. The built chain is always checked; when verification did
1596+ // not fully succeed (TLSWrap's per-certificate callback continues past
1597+ // errors so JS can apply rejectUnauthorized, leaving the error set on the
1598+ // context), the leaf and every peer-sent certificate are checked too, so
1599+ // these checks take precedence over the original error.
14941600//
14951601// A hit is reported like any other X509_verify_cert error: the error and
14961602// offending certificate are recorded on the context and the per-certificate
@@ -1531,22 +1637,54 @@ int CertVerifyCallback(X509_STORE_CTX* ctx, void*) {
15311637 int ok = X509_verify_cert (ctx) > 0 ? 1 : 0 ;
15321638 const bool verified = ok == 1 && X509_STORE_CTX_get_error (ctx) == X509_V_OK ;
15331639
1640+ // The SSL object this verification belongs to. The library installs its
1641+ // per-certificate verify callback on the store context before calling this
1642+ // hook, and SSL_is_server tells which side of the handshake is verifying.
1643+ SSL * ssl = static_cast <SSL *>(
1644+ X509_STORE_CTX_get_ex_data (ctx, SSL_get_ex_data_X509_STORE_CTX_idx ()));
1645+
15341646 // The depth is the offender's index in the built chain (0 = leaf), as
15351647 // X509_verify_cert would report it. For certificates found only in the
15361648 // peer-sent stack their index there is the closest equivalent.
15371649 X509 * offender = nullptr ;
15381650 int depth = 0 ;
1539- SpkiCheck result =
1540- CheckSPKIs (X509_STORE_CTX_get0_chain (ctx), &offender, &depth);
1651+ X509 * leaf = X509_STORE_CTX_get0_cert (ctx);
1652+ const STACK_OF (X509 )* chain = X509_STORE_CTX_get0_chain (ctx);
1653+ const STACK_OF (X509 )* untrusted = X509_STORE_CTX_get0_untrusted (ctx);
1654+
1655+ // The SPKI blocklist applies to every verification: those keys are not
1656+ // trusted for anything.
1657+ SpkiCheck result = CheckSPKIs (chain, &offender, &depth);
15411658 if (result == SpkiCheck::kAllowed && !verified) {
1542- X509 * leaf = X509_STORE_CTX_get0_cert (ctx);
15431659 result = CheckSPKI (leaf);
15441660 if (result != SpkiCheck::kAllowed ) {
15451661 offender = leaf;
15461662 depth = 0 ;
15471663 } else {
1548- result =
1549- CheckSPKIs (X509_STORE_CTX_get0_untrusted (ctx), &offender, &depth);
1664+ result = CheckSPKIs (untrusted, &offender, &depth);
1665+ }
1666+ }
1667+
1668+ // The distrust-after dates apply only when this side is a client verifying
1669+ // a server certificate. CKA_NSS_SERVER_DISTRUST_AFTER is Mozilla's TLS
1670+ // server authentication policy; Firefox does not validate client
1671+ // certificates against its root store at all, and the roots stay fully
1672+ // trusted anchors for everything else. Several of the date-limited CAs are
1673+ // large issuers of personal certificates used for client authentication,
1674+ // so a server validating client certificates against the bundled store
1675+ // keeps accepting them. This is the same scope the 2016 StartCom/WoSign
1676+ // cutoff had ("Server does not need to check the whitelist", 89217d14509).
1677+ const bool verifying_server_cert = ssl == nullptr || !SSL_is_server (ssl);
1678+ if (result == SpkiCheck::kAllowed && verifying_server_cert) {
1679+ result = CheckDistrustAfterChain (chain, leaf, &offender, &depth);
1680+ if (result == SpkiCheck::kAllowed && !verified) {
1681+ result = CheckDistrustAfter (leaf, leaf);
1682+ if (result != SpkiCheck::kAllowed ) {
1683+ offender = leaf;
1684+ depth = 0 ;
1685+ } else {
1686+ result = CheckDistrustAfterChain (untrusted, leaf, &offender, &depth);
1687+ }
15501688 }
15511689 }
15521690 if (result == SpkiCheck::kAllowed ) return ok;
@@ -1565,10 +1703,8 @@ int CertVerifyCallback(X509_STORE_CTX* ctx, void*) {
15651703 X509_STORE_CTX_set_error_depth (ctx, depth);
15661704 X509_STORE_CTX_set_current_cert (ctx, offender);
15671705#endif
1568- // The per-certificate callback is the one carried by the SSL object; it is
1569- // what the library installs on the store context before calling this hook.
1570- SSL * ssl = static_cast <SSL *>(
1571- X509_STORE_CTX_get_ex_data (ctx, SSL_get_ex_data_X509_STORE_CTX_idx ()));
1706+ // Hand the error to the per-certificate callback carried by the SSL object
1707+ // so that rejectUnauthorized and authorizationError behave as usual.
15721708 auto verify_cb = ssl != nullptr ? SSL_get_verify_callback (ssl) : nullptr ;
15731709 if (verify_cb == nullptr ) return 0 ;
15741710 return verify_cb (0 , ctx) ? 1 : 0 ;
0 commit comments