Skip to content

Commit c455c04

Browse files
committed
crypto: support --use-system-ca on non-Windows and non-macOS
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
1 parent b2f3147 commit c455c04

File tree

3 files changed

+114
-21
lines changed

3 files changed

+114
-21
lines changed

doc/api/cli.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2866,7 +2866,10 @@ The following values are valid for `mode`:
28662866
### `--use-system-ca`
28672867

28682868
Node.js uses the trusted CA certificates present in the system store along with
2869-
the `--use-bundled-ca`, `--use-openssl-ca` options.
2869+
the `--use-bundled-ca` option and the `NODE_EXTRA_CA_CERTS` environment variable.
2870+
On platform other than Windows and macOS, this loads certificates from the directory
2871+
and file trusted by OpenSSL, similar to `--use-openssl-ca`, with the difference being
2872+
that it caches the certificates after first load.
28702873

28712874
This option is only supported on Windows and macOS, and the certificate trust policy
28722875
is planned to follow [Chromium's policy for locally trusted certificates][]:
@@ -2897,9 +2900,15 @@ Chromium's policy, distrust is not currently supported):
28972900
* Trusted Root Certification Authorities
28982901
* Enterprise Trust -> Group Policy -> Trusted Root Certification Authorities
28992902

2900-
On any supported system, Node.js would check that the certificate's key usage and extended key
2903+
On Windows and macOS, Node.js would check that the certificate's key usage and extended key
29012904
usage are consistent with TLS use cases before using it for server authentication.
29022905

2906+
On other systems, Node.js loads certificates from the default file
2907+
(typically `/etc/ssl/cert.pem`) and default directory (typically `/etc/ssl/certs`)
2908+
that the version of OpenSSL that Node.js links to respects.
2909+
If the overriding OpenSSL environment variables (typically `SSL_CERT_FILE` and
2910+
`SSL_CERT_DIR`) are set, they will be used to load certificates from instead.
2911+
29032912
### `--v8-options`
29042913

29052914
<!-- YAML
@@ -3539,7 +3548,8 @@ variable is ignored.
35393548
added: v7.7.0
35403549
-->
35413550

3542-
If `--use-openssl-ca` is enabled, this overrides and sets OpenSSL's directory
3551+
If `--use-openssl-ca` is enabled, or if `--use-system-ca` is enabled on
3552+
platforms other than macOS and Windows, this overrides and sets OpenSSL's directory
35433553
containing trusted certificates.
35443554

35453555
Be aware that unless the child environment is explicitly set, this environment
@@ -3552,7 +3562,8 @@ may cause them to trust the same CAs as node.
35523562
added: v7.7.0
35533563
-->
35543564

3555-
If `--use-openssl-ca` is enabled, this overrides and sets OpenSSL's file
3565+
If `--use-openssl-ca` is enabled, or if `--use-system-ca` is enabled on
3566+
platforms other than macOS and Windows, this overrides and sets OpenSSL's file
35563567
containing trusted certificates.
35573568

35583569
Be aware that unless the child environment is explicitly set, this environment

src/crypto/crypto_context.cc

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,15 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
221221
issuer);
222222
}
223223

224-
unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
224+
static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
225225
std::vector<X509*>* certs,
226226
const char* file) {
227227
MarkPopErrorOnReturn mark_pop_error_on_return;
228228

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

232-
while (X509* x509 = PEM_read_bio_X509(
232+
while (X509* x509 = PEM_read_bio_X509_AUX(
233233
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
234234
certs->push_back(x509);
235235
}
@@ -244,6 +244,19 @@ unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
244244
}
245245
}
246246

247+
static void LoadCertsFromFileOrWarn(std::vector<X509*>* certs,
248+
const char* file) {
249+
unsigned long err = LoadCertsFromFile(certs, file); // NOLINT(runtime/int)
250+
if (err) {
251+
char buf[256];
252+
ERR_error_string_n(err, buf, sizeof(buf));
253+
fprintf(stderr,
254+
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
255+
extra_root_certs_file.c_str(),
256+
buf);
257+
}
258+
}
259+
247260
// Indicates the trust status of a certificate.
248261
enum class TrustStatus {
249262
// Trust status is unknown / uninitialized.
@@ -643,6 +656,73 @@ void ReadWindowsCertificates(
643656
}
644657
#endif
645658

659+
void LoadCertsFromDir(std::vector<X509*>* certs, std::string_view cert_dir) {
660+
uv_fs_t dir_req;
661+
auto cleanup = OnScopeLeave([&dir_req]() { uv_fs_req_cleanup(&dir_req); });
662+
int err = uv_fs_scandir(nullptr, &dir_req, cert_dir.data(), 0, nullptr);
663+
if (err < 0) {
664+
fprintf(stderr,
665+
"Cannot open directory %s to load OpenSSL certificates.\n",
666+
cert_dir.data());
667+
return;
668+
}
669+
670+
uv_fs_t stats_req;
671+
auto cleanup_stats =
672+
OnScopeLeave([&stats_req]() { uv_fs_req_cleanup(&stats_req); });
673+
for (;;) {
674+
uv_dirent_t ent;
675+
676+
int r = uv_fs_scandir_next(&dir_req, &ent);
677+
if (r == UV_EOF) {
678+
break;
679+
}
680+
if (r < 0) {
681+
char message[64];
682+
uv_strerror_r(r, message, sizeof(message));
683+
fprintf(stderr,
684+
"Cannot scan directory %s to load OpenSSL certificates.\n",
685+
cert_dir.data());
686+
return;
687+
}
688+
689+
std::string file_path = std::string(cert_dir) + "/" + ent.name;
690+
int stats_r = uv_fs_stat(nullptr, &stats_req, file_path.c_str(), nullptr);
691+
if (stats_r == 0 &&
692+
(static_cast<uv_stat_t*>(stats_req.ptr)->st_mode & S_IFREG)) {
693+
LoadCertsFromFile(certs, file_path.c_str());
694+
}
695+
}
696+
}
697+
698+
// Loads CA certificates from the default certificate paths respected by
699+
// OpenSSL.
700+
void GetOpenSSLSystemCertificates(std::vector<X509*>* system_store_certs) {
701+
std::string cert_file;
702+
// While configurable when OpenSSL is built, this is usually SSL_CERT_FILE.
703+
if (!credentials::SafeGetenv(X509_get_default_cert_file_env(), &cert_file)) {
704+
// This is usually /etc/ssl/cert.pem if we are using the OpenSSL statically
705+
// linked and built with default configurations.
706+
cert_file = X509_get_default_cert_file();
707+
}
708+
709+
std::string cert_dir;
710+
// While configurable when OpenSSL is built, this is usually SSL_CERT_DIR.
711+
if (!credentials::SafeGetenv(X509_get_default_cert_dir_env(), &cert_dir)) {
712+
// This is usually /etc/ssl/certs if we are using the OpenSSL statically
713+
// linked and built with default configurations.
714+
cert_dir = X509_get_default_cert_dir();
715+
}
716+
717+
if (!cert_file.empty()) {
718+
LoadCertsFromFile(system_store_certs, cert_file.c_str());
719+
}
720+
721+
if (!cert_dir.empty()) {
722+
LoadCertsFromDir(system_store_certs, cert_dir.c_str());
723+
}
724+
}
725+
646726
static std::vector<X509*> InitializeBundledRootCertificates() {
647727
// Read the bundled certificates in node_root_certs.h into
648728
// bundled_root_certs_vector.
@@ -682,6 +762,9 @@ static std::vector<X509*> InitializeSystemStoreCertificates() {
682762
#endif
683763
#ifdef _WIN32
684764
ReadWindowsCertificates(&system_store_certs);
765+
#endif
766+
#if !defined(__APPLE__) && !defined(_WIN32)
767+
GetOpenSSLSystemCertificates(&system_store_certs);
685768
#endif
686769
return system_store_certs;
687770
}
@@ -696,17 +779,7 @@ static std::vector<X509*>& GetSystemStoreRootCertificates() {
696779

697780
static std::vector<X509*> InitializeExtraCACertificates() {
698781
std::vector<X509*> extra_certs;
699-
unsigned long err = LoadCertsFromFile( // NOLINT(runtime/int)
700-
&extra_certs,
701-
extra_root_certs_file.c_str());
702-
if (err) {
703-
char buf[256];
704-
ERR_error_string_n(err, buf, sizeof(buf));
705-
fprintf(stderr,
706-
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
707-
extra_root_certs_file.c_str(),
708-
buf);
709-
}
782+
LoadCertsFromFileOrWarn(&extra_certs, extra_root_certs_file.c_str());
710783
return extra_certs;
711784
}
712785

test/parallel/test-native-certs.mjs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ import fixtures from '../common/fixtures.js';
77
import { it, beforeEach, afterEach, describe } from 'node:test';
88
import { once } from 'events';
99

10-
if (!common.isMacOS && !common.isWindows) {
11-
common.skip('--use-system-ca is only supported on macOS and Windows');
12-
}
13-
1410
if (!common.hasCrypto) {
1511
common.skip('requires crypto');
1612
}
@@ -34,6 +30,19 @@ if (!common.hasCrypto) {
3430
// $ $thumbprint = (Get-ChildItem -Path Cert:\CurrentUser\Root | \
3531
// Where-Object { $_.Subject -match "StartCom Certification Authority" }).Thumbprint
3632
// $ Remove-Item -Path "Cert:\CurrentUser\Root\$thumbprint"
33+
//
34+
// On Debian/Ubuntu:
35+
// 1. To add the certificate:
36+
// $ sudo cp test/fixtures/keys/fake-startcom-root-cert.pem \
37+
// /usr/local/share/ca-certificates/fake-startcom-root-cert.crt
38+
// $ sudo update-ca-certificates
39+
// 2. To remove the certificate
40+
// $ sudo rm /usr/local/share/ca-certificates/fake-startcom-root-cert.crt
41+
// $ sudo update-ca-certificates --fresh
42+
//
43+
// For other UNIX-like systems, consult their manuals, there are usually
44+
// file-based processes similar to the Debian/Ubuntu one but with different
45+
// file locations and update commands.
3746
const handleRequest = (req, res) => {
3847
const path = req.url;
3948
switch (path) {

0 commit comments

Comments
 (0)