Skip to content

Commit c78e1bb

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 69d32d1 commit c78e1bb

File tree

3 files changed

+112
-13
lines changed

3 files changed

+112
-13
lines changed

doc/api/cli.md

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

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

28732876
This option is only supported on Windows and macOS, and the certificate trust policy
28742877
is planned to follow [Chromium's policy for locally trusted certificates][]:
@@ -2899,9 +2902,15 @@ Chromium's policy, distrust is not currently supported):
28992902
* Trusted Root Certification Authorities
29002903
* Enterprise Trust -> Group Policy -> Trusted Root Certification Authorities
29012904

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

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

29072916
<!-- YAML
@@ -3541,7 +3550,8 @@ variable is ignored.
35413550
added: v7.7.0
35423551
-->
35433552

3544-
If `--use-openssl-ca` is enabled, this overrides and sets OpenSSL's directory
3553+
If `--use-openssl-ca` is enabled, or if `--use-system-ca` is enabled on
3554+
platforms other than macOS and Windows, this overrides and sets OpenSSL's directory
35453555
containing trusted certificates.
35463556

35473557
Be aware that unless the child environment is explicitly set, this environment
@@ -3554,7 +3564,8 @@ may cause them to trust the same CAs as node.
35543564
added: v7.7.0
35553565
-->
35563566

3557-
If `--use-openssl-ca` is enabled, this overrides and sets OpenSSL's file
3567+
If `--use-openssl-ca` is enabled, or if `--use-system-ca` is enabled on
3568+
platforms other than macOS and Windows, this overrides and sets OpenSSL's file
35583569
containing trusted certificates.
35593570

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

src/crypto/crypto_context.cc

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,17 +221,23 @@ 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,
226-
const char* file) {
226+
const char* file,
227+
bool check_usage) {
227228
MarkPopErrorOnReturn mark_pop_error_on_return;
228229

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

232-
while (X509* x509 = PEM_read_bio_X509(
233+
while (X509* x509 = PEM_read_bio_X509_AUX(
233234
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
234-
certs->push_back(x509);
235+
if (!check_usage ||
236+
X509_check_purpose(x509, X509_PURPOSE_SSL_SERVER, 0) == 1) {
237+
certs->push_back(x509);
238+
} else {
239+
X509_free(x509);
240+
}
235241
}
236242

237243
unsigned long err = ERR_peek_last_error(); // NOLINT(runtime/int)
@@ -643,6 +649,75 @@ void ReadWindowsCertificates(
643649
}
644650
#endif
645651

652+
void LoadCertsFromDir(std::vector<X509*>* certs,
653+
std::string_view cert_dir,
654+
bool check_usage) {
655+
uv_fs_t dir_req;
656+
auto cleanup = OnScopeLeave([&dir_req]() { uv_fs_req_cleanup(&dir_req); });
657+
int err = uv_fs_scandir(nullptr, &dir_req, cert_dir.data(), 0, nullptr);
658+
if (err < 0) {
659+
fprintf(stderr,
660+
"Cannot open directory %s to load OpenSSL certificates.\n",
661+
cert_dir.data());
662+
return;
663+
}
664+
665+
uv_fs_t stats_req;
666+
auto cleanup_stats =
667+
OnScopeLeave([&stats_req]() { uv_fs_req_cleanup(&stats_req); });
668+
for (;;) {
669+
uv_dirent_t ent;
670+
671+
int r = uv_fs_scandir_next(&dir_req, &ent);
672+
if (r == UV_EOF) {
673+
break;
674+
}
675+
if (r < 0) {
676+
char message[64];
677+
uv_strerror_r(r, message, sizeof(message));
678+
fprintf(stderr,
679+
"Cannot scan directory %s to load OpenSSL certificates.\n",
680+
cert_dir.data());
681+
return;
682+
}
683+
684+
std::string file_path = std::string(cert_dir) + "/" + ent.name;
685+
int stats_r = uv_fs_stat(nullptr, &stats_req, file_path.c_str(), nullptr);
686+
if (stats_r == 0 &&
687+
(static_cast<uv_stat_t*>(stats_req.ptr)->st_mode & S_IFREG)) {
688+
LoadCertsFromFile(certs, file_path.c_str(), check_usage);
689+
}
690+
}
691+
}
692+
693+
// Loads CA certificates from the default certificate paths respected by
694+
// OpenSSL.
695+
void GetOpenSSLSystemCertificates(std::vector<X509*>* system_store_certs) {
696+
std::string cert_file;
697+
// While configurable when OpenSSL is built, this is usually SSL_CERT_FILE.
698+
if (!credentials::SafeGetenv(X509_get_default_cert_file_env(), &cert_file)) {
699+
// This is usually /etc/ssl/cert.pem if we are using the OpenSSL statically
700+
// linked and built with default configurations.
701+
cert_file = X509_get_default_cert_file();
702+
}
703+
704+
std::string cert_dir;
705+
// While configurable when OpenSSL is built, this is usually SSL_CERT_DIR.
706+
if (!credentials::SafeGetenv(X509_get_default_cert_dir_env(), &cert_dir)) {
707+
// This is usually /etc/ssl/certs if we are using the OpenSSL statically
708+
// linked and built with default configurations.
709+
cert_dir = X509_get_default_cert_dir();
710+
}
711+
712+
if (!cert_file.empty()) {
713+
LoadCertsFromFile(system_store_certs, cert_file.c_str(), true);
714+
}
715+
716+
if (!cert_dir.empty()) {
717+
LoadCertsFromDir(system_store_certs, cert_dir.c_str(), true);
718+
}
719+
}
720+
646721
static std::vector<X509*> InitializeBundledRootCertificates() {
647722
// Read the bundled certificates in node_root_certs.h into
648723
// bundled_root_certs_vector.
@@ -683,6 +758,9 @@ static std::vector<X509*> InitializeSystemStoreCertificates() {
683758
#endif
684759
#ifdef _WIN32
685760
ReadWindowsCertificates(&system_store_certs);
761+
#endif
762+
#if !defined(__APPLE__) && !defined(_WIN32)
763+
GetOpenSSLSystemCertificates(&system_store_certs);
686764
#endif
687765
return system_store_certs;
688766
}
@@ -699,7 +777,8 @@ static std::vector<X509*> InitializeExtraCACertificates() {
699777
std::vector<X509*> extra_certs;
700778
unsigned long err = LoadCertsFromFile( // NOLINT(runtime/int)
701779
&extra_certs,
702-
extra_root_certs_file.c_str());
780+
extra_root_certs_file.c_str(),
781+
false);
703782
if (err) {
704783
char buf[256];
705784
ERR_error_string_n(err, buf, sizeof(buf));

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)