diff --git a/doc/api/cli.md b/doc/api/cli.md index 47e86d0c324395..befab1aef586b5 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -2868,12 +2868,15 @@ The following values are valid for `mode`: ### `--use-system-ca` Node.js uses the trusted CA certificates present in the system store along with -the `--use-bundled-ca`, `--use-openssl-ca` options. +the `--use-bundled-ca` option and the `NODE_EXTRA_CA_CERTS` environment variable. +On platforms other than Windows and macOS, this loads certificates from the directory +and file trusted by OpenSSL, similar to `--use-openssl-ca`, with the difference being +that it caches the certificates after first load. -This option is only supported on Windows and macOS, and the certificate trust policy -is planned to follow [Chromium's policy for locally trusted certificates][]: +On Windows and macOS, the certificate trust policy is planned to follow +[Chromium's policy for locally trusted certificates][]: -On macOS, the following certifcates are trusted: +On macOS, the following settings are respected: * Default and System Keychains * Trust: @@ -2883,8 +2886,8 @@ On macOS, the following certifcates are trusted: * Any certificate where the “When using this certificate” flag is set to “Never Trust” or * Any certificate where the “Secure Sockets Layer (SSL)” flag is set to “Never Trust.” -On Windows, the following certificates are currently trusted (unlike -Chromium's policy, distrust is not currently supported): +On Windows, the following settings are respected (unlike Chromium's policy, distrust +and intermediate CA are not currently supported): * Local Machine (accessed via `certlm.msc`) * Trust: @@ -2899,8 +2902,19 @@ Chromium's policy, distrust is not currently supported): * Trusted Root Certification Authorities * Enterprise Trust -> Group Policy -> Trusted Root Certification Authorities -On any supported system, Node.js would check that the certificate's key usage and extended key -usage are consistent with TLS use cases before using it for server authentication. +On Windows and macOS, Node.js would check that the user settings for the certificates +do not forbid them for TLS server authentication before using them. + +On other systems, Node.js loads certificates from the default certificate file +(typically `/etc/ssl/cert.pem`) and default certificate directory (typically +`/etc/ssl/certs`) that the version of OpenSSL that Node.js links to respects. +This typically works with the convention on major Linux distributions and other +Unix-like systems. If the overriding OpenSSL environment variables +(typically `SSL_CERT_FILE` and `SSL_CERT_DIR`, depending on the configuration +of the OpenSSL that Node.js links to) are set, the specified paths will be used to load +certificates instead. These environment variables can be used as workarounds +if the conventional paths used by the version of OpenSSL Node.js links to are +not consistent with the system configuration that the users have for some reason. ### `--v8-options` @@ -3541,7 +3555,8 @@ variable is ignored. added: v7.7.0 --> -If `--use-openssl-ca` is enabled, this overrides and sets OpenSSL's directory +If `--use-openssl-ca` is enabled, or if `--use-system-ca` is enabled on +platforms other than macOS and Windows, this overrides and sets OpenSSL's directory containing trusted certificates. Be aware that unless the child environment is explicitly set, this environment @@ -3554,7 +3569,8 @@ may cause them to trust the same CAs as node. added: v7.7.0 --> -If `--use-openssl-ca` is enabled, this overrides and sets OpenSSL's file +If `--use-openssl-ca` is enabled, or if `--use-system-ca` is enabled on +platforms other than macOS and Windows, this overrides and sets OpenSSL's file containing trusted certificates. Be aware that unless the child environment is explicitly set, this environment diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 28dedf33b18154..3e4b517fa462ef 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -223,7 +223,7 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, issuer); } -unsigned long LoadCertsFromFile( // NOLINT(runtime/int) +static unsigned long LoadCertsFromFile( // NOLINT(runtime/int) std::vector* certs, const char* file) { MarkPopErrorOnReturn mark_pop_error_on_return; @@ -645,6 +645,74 @@ void ReadWindowsCertificates( } #endif +static void LoadCertsFromDir(std::vector* certs, + std::string_view cert_dir) { + uv_fs_t dir_req; + auto cleanup = OnScopeLeave([&dir_req]() { uv_fs_req_cleanup(&dir_req); }); + int err = uv_fs_scandir(nullptr, &dir_req, cert_dir.data(), 0, nullptr); + if (err < 0) { + fprintf(stderr, + "Cannot open directory %s to load OpenSSL certificates.\n", + cert_dir.data()); + return; + } + + uv_fs_t stats_req; + auto cleanup_stats = + OnScopeLeave([&stats_req]() { uv_fs_req_cleanup(&stats_req); }); + for (;;) { + uv_dirent_t ent; + + int r = uv_fs_scandir_next(&dir_req, &ent); + if (r == UV_EOF) { + break; + } + if (r < 0) { + char message[64]; + uv_strerror_r(r, message, sizeof(message)); + fprintf(stderr, + "Cannot scan directory %s to load OpenSSL certificates.\n", + cert_dir.data()); + return; + } + + std::string file_path = std::string(cert_dir) + "/" + ent.name; + int stats_r = uv_fs_stat(nullptr, &stats_req, file_path.c_str(), nullptr); + if (stats_r == 0 && + (static_cast(stats_req.ptr)->st_mode & S_IFREG)) { + LoadCertsFromFile(certs, file_path.c_str()); + } + } +} + +// Loads CA certificates from the default certificate paths respected by +// OpenSSL. +void GetOpenSSLSystemCertificates(std::vector* system_store_certs) { + std::string cert_file; + // While configurable when OpenSSL is built, this is usually SSL_CERT_FILE. + if (!credentials::SafeGetenv(X509_get_default_cert_file_env(), &cert_file)) { + // This is usually /etc/ssl/cert.pem if we are using the OpenSSL statically + // linked and built with default configurations. + cert_file = X509_get_default_cert_file(); + } + + std::string cert_dir; + // While configurable when OpenSSL is built, this is usually SSL_CERT_DIR. + if (!credentials::SafeGetenv(X509_get_default_cert_dir_env(), &cert_dir)) { + // This is usually /etc/ssl/certs if we are using the OpenSSL statically + // linked and built with default configurations. + cert_dir = X509_get_default_cert_dir(); + } + + if (!cert_file.empty()) { + LoadCertsFromFile(system_store_certs, cert_file.c_str()); + } + + if (!cert_dir.empty()) { + LoadCertsFromDir(system_store_certs, cert_dir.c_str()); + } +} + static std::vector InitializeBundledRootCertificates() { // Read the bundled certificates in node_root_certs.h into // bundled_root_certs_vector. @@ -685,6 +753,9 @@ static std::vector InitializeSystemStoreCertificates() { #endif #ifdef _WIN32 ReadWindowsCertificates(&system_store_certs); +#endif +#if !defined(__APPLE__) && !defined(_WIN32) + GetOpenSSLSystemCertificates(&system_store_certs); #endif return system_store_certs; } diff --git a/test/parallel/test-native-certs.mjs b/test/parallel/test-native-certs.mjs index f27e1d81a4f05e..ed8769e92acb32 100644 --- a/test/parallel/test-native-certs.mjs +++ b/test/parallel/test-native-certs.mjs @@ -7,10 +7,6 @@ import fixtures from '../common/fixtures.js'; import { it, beforeEach, afterEach, describe } from 'node:test'; import { once } from 'events'; -if (!common.isMacOS && !common.isWindows) { - common.skip('--use-system-ca is only supported on macOS and Windows'); -} - if (!common.hasCrypto) { common.skip('requires crypto'); } @@ -34,6 +30,19 @@ if (!common.hasCrypto) { // $ $thumbprint = (Get-ChildItem -Path Cert:\CurrentUser\Root | \ // Where-Object { $_.Subject -match "StartCom Certification Authority" }).Thumbprint // $ Remove-Item -Path "Cert:\CurrentUser\Root\$thumbprint" +// +// On Debian/Ubuntu: +// 1. To add the certificate: +// $ sudo cp test/fixtures/keys/fake-startcom-root-cert.pem \ +// /usr/local/share/ca-certificates/fake-startcom-root-cert.crt +// $ sudo update-ca-certificates +// 2. To remove the certificate +// $ sudo rm /usr/local/share/ca-certificates/fake-startcom-root-cert.crt +// $ sudo update-ca-certificates --fresh +// +// For other Unix-like systems, consult their manuals, there are usually +// file-based processes similar to the Debian/Ubuntu one but with different +// file locations and update commands. const handleRequest = (req, res) => { const path = req.url; switch (path) {