Skip to content

Commit 0d6c04a

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 0d6c04a

File tree

3 files changed

+101
-11
lines changed

3 files changed

+101
-11
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: 73 additions & 3 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
}
@@ -643,6 +643,73 @@ void ReadWindowsCertificates(
643643
}
644644
#endif
645645

646+
void LoadCertsFromDir(std::vector<X509*>* certs, std::string_view cert_dir) {
647+
uv_fs_t dir_req;
648+
auto cleanup = OnScopeLeave([&dir_req]() { uv_fs_req_cleanup(&dir_req); });
649+
int err = uv_fs_scandir(nullptr, &dir_req, cert_dir.data(), 0, nullptr);
650+
if (err < 0) {
651+
fprintf(stderr,
652+
"Cannot open directory %s to load OpenSSL certificates.\n",
653+
cert_dir.data());
654+
return;
655+
}
656+
657+
uv_fs_t stats_req;
658+
auto cleanup_stats =
659+
OnScopeLeave([&stats_req]() { uv_fs_req_cleanup(&stats_req); });
660+
for (;;) {
661+
uv_dirent_t ent;
662+
663+
int r = uv_fs_scandir_next(&dir_req, &ent);
664+
if (r == UV_EOF) {
665+
break;
666+
}
667+
if (r < 0) {
668+
char message[64];
669+
uv_strerror_r(r, message, sizeof(message));
670+
fprintf(stderr,
671+
"Cannot scan directory %s to load OpenSSL certificates.\n",
672+
cert_dir.data());
673+
return;
674+
}
675+
676+
std::string file_path = std::string(cert_dir) + "/" + ent.name;
677+
int stats_r = uv_fs_stat(nullptr, &stats_req, file_path.c_str(), nullptr);
678+
if (stats_r == 0 &&
679+
(static_cast<uv_stat_t*>(stats_req.ptr)->st_mode & S_IFREG)) {
680+
LoadCertsFromFile(certs, file_path.c_str());
681+
}
682+
}
683+
}
684+
685+
// Loads CA certificates from the default certificate paths respected by
686+
// OpenSSL.
687+
void GetOpenSSLSystemCertificates(std::vector<X509*>* system_store_certs) {
688+
std::string cert_file;
689+
// While configurable when OpenSSL is built, this is usually SSL_CERT_FILE.
690+
if (!credentials::SafeGetenv(X509_get_default_cert_file_env(), &cert_file)) {
691+
// This is usually /etc/ssl/cert.pem if we are using the OpenSSL statically
692+
// linked and built with default configurations.
693+
cert_file = X509_get_default_cert_file();
694+
}
695+
696+
std::string cert_dir;
697+
// While configurable when OpenSSL is built, this is usually SSL_CERT_DIR.
698+
if (!credentials::SafeGetenv(X509_get_default_cert_dir_env(), &cert_dir)) {
699+
// This is usually /etc/ssl/certs if we are using the OpenSSL statically
700+
// linked and built with default configurations.
701+
cert_dir = X509_get_default_cert_dir();
702+
}
703+
704+
if (!cert_file.empty()) {
705+
LoadCertsFromFile(system_store_certs, cert_file.c_str());
706+
}
707+
708+
if (!cert_dir.empty()) {
709+
LoadCertsFromDir(system_store_certs, cert_dir.c_str());
710+
}
711+
}
712+
646713
static std::vector<X509*> InitializeBundledRootCertificates() {
647714
// Read the bundled certificates in node_root_certs.h into
648715
// bundled_root_certs_vector.
@@ -683,6 +750,9 @@ static std::vector<X509*> InitializeSystemStoreCertificates() {
683750
#endif
684751
#ifdef _WIN32
685752
ReadWindowsCertificates(&system_store_certs);
753+
#endif
754+
#if !defined(__APPLE__) && !defined(_WIN32)
755+
GetOpenSSLSystemCertificates(&system_store_certs);
686756
#endif
687757
return system_store_certs;
688758
}
@@ -1297,7 +1367,7 @@ void SecureContext::SetAllowPartialTrustChain(
12971367
void SecureContext::SetCACert(const BIOPointer& bio) {
12981368
ClearErrorOnReturn clear_error_on_return;
12991369
if (!bio) return;
1300-
while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509_AUX(
1370+
while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509(
13011371
bio.get(), nullptr, NoPasswordCallback, nullptr))) {
13021372
CHECK_EQ(1,
13031373
X509_STORE_add_cert(GetCertStoreOwnedByThisSecureContext(), x509));

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)