Skip to content

Commit

Permalink
crypto: support --use-system-ca on non-Windows and non-macOS
Browse files Browse the repository at this point in the history
On other platforms, load from the OpenSSL default certificate
file and diretory.
This is different from --use-openssl-ca in that it caches
the certificates on first load, instead of always reading
from disk every time a new root store is needed.

When used together with the statically-linked OpenSSL, the
default configuration usually leads to this behavior:

- If SSL_CERT_FILE is used, load from SSL_CERT_FILE. Otherwise
  load from /etc/ssl/cert.pem
- If SSL_CERT_DIR is used, load from all the files under
  SSL_CERT_DIR. Otherwise, load from all the files under
  /etc/ssl/certs
  • Loading branch information
joyeecheung committed Feb 12, 2025
1 parent b2f3147 commit c455c04
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 21 deletions.
19 changes: 15 additions & 4 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2866,7 +2866,10 @@ 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 platform 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][]:
Expand Down Expand Up @@ -2897,9 +2900,15 @@ 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
On Windows and macOS, 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 other systems, Node.js loads certificates from the default file
(typically `/etc/ssl/cert.pem`) and default directory (typically `/etc/ssl/certs`)
that the version of OpenSSL that Node.js links to respects.
If the overriding OpenSSL environment variables (typically `SSL_CERT_FILE` and
`SSL_CERT_DIR`) are set, they will be used to load certificates from instead.

### `--v8-options`

<!-- YAML
Expand Down Expand Up @@ -3539,7 +3548,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
Expand All @@ -3552,7 +3562,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
Expand Down
99 changes: 86 additions & 13 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,15 @@ 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<X509*>* certs,
const char* file) {
MarkPopErrorOnReturn mark_pop_error_on_return;

auto bio = BIOPointer::NewFile(file, "r");
if (!bio) return ERR_get_error();

while (X509* x509 = PEM_read_bio_X509(
while (X509* x509 = PEM_read_bio_X509_AUX(
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
certs->push_back(x509);
}
Expand All @@ -244,6 +244,19 @@ unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
}
}

static void LoadCertsFromFileOrWarn(std::vector<X509*>* certs,
const char* file) {
unsigned long err = LoadCertsFromFile(certs, file); // NOLINT(runtime/int)
if (err) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
fprintf(stderr,
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
extra_root_certs_file.c_str(),
buf);
}
}

// Indicates the trust status of a certificate.
enum class TrustStatus {
// Trust status is unknown / uninitialized.
Expand Down Expand Up @@ -643,6 +656,73 @@ void ReadWindowsCertificates(
}
#endif

void LoadCertsFromDir(std::vector<X509*>* 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<uv_stat_t*>(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<X509*>* 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<X509*> InitializeBundledRootCertificates() {
// Read the bundled certificates in node_root_certs.h into
// bundled_root_certs_vector.
Expand Down Expand Up @@ -682,6 +762,9 @@ static std::vector<X509*> InitializeSystemStoreCertificates() {
#endif
#ifdef _WIN32
ReadWindowsCertificates(&system_store_certs);
#endif
#if !defined(__APPLE__) && !defined(_WIN32)
GetOpenSSLSystemCertificates(&system_store_certs);
#endif
return system_store_certs;
}
Expand All @@ -696,17 +779,7 @@ static std::vector<X509*>& GetSystemStoreRootCertificates() {

static std::vector<X509*> InitializeExtraCACertificates() {
std::vector<X509*> extra_certs;
unsigned long err = LoadCertsFromFile( // NOLINT(runtime/int)
&extra_certs,
extra_root_certs_file.c_str());
if (err) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
fprintf(stderr,
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
extra_root_certs_file.c_str(),
buf);
}
LoadCertsFromFileOrWarn(&extra_certs, extra_root_certs_file.c_str());
return extra_certs;
}

Expand Down
17 changes: 13 additions & 4 deletions test/parallel/test-native-certs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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) {
Expand Down

0 comments on commit c455c04

Please sign in to comment.