| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent ff6be2e commit cda1dab
21 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2342,6 +2342,54 @@ openssl pkcs12 -certpbe AES-256-CBC -export -out client-cert.pem \ | |||
| 2342 | 2342 | The server can be tested by connecting to it using the example client from | |
| 2343 | 2343 | [`tls.connect()`][]. | |
| 2344 | 2344 | ||
| 2345 | + ## `tls.setDefaultCACertificates(certs)` | ||
| 2346 | + | ||
| 2347 | + <!-- YAML | ||
| 2348 | + added: REPLACEME | ||
| 2349 | + --> | ||
| 2350 | + | ||
| 2351 | + * `certs` {string\[]|ArrayBufferView\[]} An array of CA certificates in PEM format. | ||
| 2352 | + | ||
| 2353 | + Sets the default CA certificates used by Node.js TLS clients. If the provided | ||
| 2354 | + certificates are parsed successfully, they will become the default CA | ||
| 2355 | + certificate list returned by [`tls.getCACertificates()`][] and used | ||
| 2356 | + by subsequent TLS connections that don't specify their own CA certificates. | ||
| 2357 | + The certificates will be deduplicated before being set as the default. | ||
| 2358 | + | ||
| 2359 | + This function only affects the current Node.js thread. Previous | ||
| 2360 | + sessions cached by the HTTPS agent won't be affected by this change, so | ||
| 2361 | + this method should be called before any unwanted cachable TLS connections are | ||
| 2362 | + made. | ||
| 2363 | + | ||
| 2364 | + To use system CA certificates as the default: | ||
| 2365 | + | ||
| 2366 | + ```cjs | ||
| 2367 | + const tls = require('node:tls'); | ||
| 2368 | + tls.setDefaultCACertificates(tls.getCACertificates('system')); | ||
| 2369 | + ``` | ||
| 2370 | + | ||
| 2371 | + ```mjs | ||
| 2372 | + import tls from 'node:tls'; | ||
| 2373 | + tls.setDefaultCACertificates(tls.getCACertificates('system')); | ||
| 2374 | + ``` | ||
| 2375 | + | ||
| 2376 | + This function completely replaces the default CA certificate list. To add additional | ||
| 2377 | + certificates to the existing defaults, get the current certificates and append to them: | ||
| 2378 | + | ||
| 2379 | + ```cjs | ||
| 2380 | + const tls = require('node:tls'); | ||
| 2381 | + const currentCerts = tls.getCACertificates('default'); | ||
| 2382 | + const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; | ||
| 2383 | + tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); | ||
| 2384 | + ``` | ||
| 2385 | + | ||
| 2386 | + ```mjs | ||
| 2387 | + import tls from 'node:tls'; | ||
| 2388 | + const currentCerts = tls.getCACertificates('default'); | ||
| 2389 | + const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; | ||
| 2390 | + tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); | ||
| 2391 | + ``` | ||
| 2392 | + | ||
| 2345 | 2393 | ## `tls.getCACertificates([type])` | |
| 2346 | 2394 | ||
| 2347 | 2395 | <!-- YAML | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -37,6 +37,7 @@ const { | |||
| 37 | 37 | ERR_TLS_CERT_ALTNAME_INVALID, | |
| 38 | 38 | ERR_OUT_OF_RANGE, | |
| 39 | 39 | ERR_INVALID_ARG_VALUE, | |
| 40 | + ERR_INVALID_ARG_TYPE, | ||
| 40 | 41 | } = require('internal/errors').codes; | |
| 41 | 42 | const internalUtil = require('internal/util'); | |
| 42 | 43 | internalUtil.assertCrypto(); | |
@@ -51,6 +52,8 @@ const { | |||
| 51 | 52 | getBundledRootCertificates, | |
| 52 | 53 | getExtraCACertificates, | |
| 53 | 54 | getSystemCACertificates, | |
| 55 | + resetRootCertStore, | ||
| 56 | + getUserRootCertificates, | ||
| 54 | 57 | getSSLCiphers, | |
| 55 | 58 | } = internalBinding('crypto'); | |
| 56 | 59 | const { Buffer } = require('buffer'); | |
@@ -123,8 +126,17 @@ function cacheSystemCACertificates() { | |||
| 123 | 126 | } | |
| 124 | 127 | ||
| 125 | 128 | let defaultCACertificates; | |
| 129 | + let hasResetDefaultCACertificates = false; | ||
| 130 | + | ||
| 126 | 131 | function cacheDefaultCACertificates() { | |
| 127 | 132 | if (defaultCACertificates) { return defaultCACertificates; } | |
| 133 | + | ||
| 134 | + if (hasResetDefaultCACertificates) { | ||
| 135 | + defaultCACertificates = getUserRootCertificates(); | ||
| 136 | + ObjectFreeze(defaultCACertificates); | ||
| 137 | + return defaultCACertificates; | ||
| 138 | + } | ||
| 139 | + | ||
| 128 | 140 | defaultCACertificates = []; | |
| 129 | 141 | ||
| 130 | 142 | if (!getOptionValue('--use-openssl-ca')) { | |
@@ -172,6 +184,26 @@ function getCACertificates(type = 'default') { | |||
| 172 | 184 | } | |
| 173 | 185 | exports.getCACertificates = getCACertificates; | |
| 174 | 186 | ||
| 187 | + function setDefaultCACertificates(certs) { | ||
| 188 | + if (!ArrayIsArray(certs)) { | ||
| 189 | + throw new ERR_INVALID_ARG_TYPE('certs', 'Array', certs); | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + // Verify that all elements in the array are strings | ||
| 193 | + for (let i = 0; i < certs.length; i++) { | ||
| 194 | + if (typeof certs[i] !== 'string' && !isArrayBufferView(certs[i])) { | ||
| 195 | + throw new ERR_INVALID_ARG_TYPE( | ||
| 196 | + `certs[${i}]`, ['string', 'ArrayBufferView'], certs[i]); | ||
| 197 | + } | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + resetRootCertStore(certs); | ||
| 201 | + defaultCACertificates = undefined; // Reset the cached default certificates | ||
| 202 | + hasResetDefaultCACertificates = true; | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + exports.setDefaultCACertificates = setDefaultCACertificates; | ||
| 206 | + | ||
| 175 | 207 | // Convert protocols array into valid OpenSSL protocols list | |
| 176 | 208 | // ("\x06spdy/2\x08http/1.1\x08http/1.0") | |
| 177 | 209 | function convertProtocols(protocols) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -27,6 +27,8 @@ | |||
| 27 | 27 | #include <wincrypt.h> | |
| 28 | 28 | #endif | |
| 29 | 29 | ||
| 30 | + #include <set> | ||
| 31 | + | ||
| 30 | 32 | namespace node { | |
| 31 | 33 | ||
| 32 | 34 | using ncrypto::BignumPointer; | |
@@ -81,10 +83,28 @@ static std::atomic<bool> has_cached_bundled_root_certs{false}; | |||
| 81 | 83 | static std::atomic<bool> has_cached_system_root_certs{false}; | |
| 82 | 84 | static std::atomic<bool> has_cached_extra_root_certs{false}; | |
| 83 | 85 | ||
| 86 | + // Used for sets of X509. | ||
| 87 | + struct X509Less { | ||
| 88 | + bool operator()(const X509* lhs, const X509* rhs) const noexcept { | ||
| 89 | + return X509_cmp(const_cast<X509*>(lhs), const_cast<X509*>(rhs)) < 0; | ||
| 90 | + } | ||
| 91 | + }; | ||
| 92 | + using X509Set = std::set<X509*, X509Less>; | ||
| 93 | + | ||
| 94 | + // Per-thread root cert store. See NewRootCertStore() on what it contains. | ||
| 95 | + static thread_local X509_STORE* root_cert_store = nullptr; | ||
| 96 | + // If the user calls tls.setDefaultCACertificates() this will be used | ||
| 97 | + // to hold the user-provided certificates, the root_cert_store and any new | ||
| 98 | + // copy generated by NewRootCertStore() will then contain the certificates | ||
| 99 | + // from this set. | ||
| 100 | + static thread_local std::unique_ptr<X509Set> root_certs_from_users; | ||
| 101 | + | ||
| 84 | 102 | X509_STORE* GetOrCreateRootCertStore() { | |
| 85 | - // Guaranteed thread-safe by standard, just don't use -fno-threadsafe-statics. | ||
| 86 | - static X509_STORE* store = NewRootCertStore(); | ||
| 87 | - return store; | ||
| 103 | + if (root_cert_store != nullptr) { | ||
| 104 | + return root_cert_store; | ||
| 105 | + } | ||
| 106 | + root_cert_store = NewRootCertStore(); | ||
| 107 | + return root_cert_store; | ||
| 88 | 108 | } | |
| 89 | 109 | ||
| 90 | 110 | // Takes a string or buffer and loads it into a BIO. | |
@@ -225,14 +245,11 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, | |||
| 225 | 245 | issuer); | |
| 226 | 246 | } | |
| 227 | 247 | ||
| 228 | - static unsigned long LoadCertsFromFile( // NOLINT(runtime/int) | ||
| 248 | + static unsigned long LoadCertsFromBIO( // NOLINT(runtime/int) | ||
| 229 | 249 | std::vector<X509*>* certs, | |
| 230 | - const char* file) { | ||
| 250 | + BIOPointer bio) { | ||
| 231 | 251 | MarkPopErrorOnReturn mark_pop_error_on_return; | |
| 232 | 252 | ||
| 233 | - auto bio = BIOPointer::NewFile(file, "r"); | ||
| 234 | - if (!bio) return ERR_get_error(); | ||
| 235 | - | ||
| 236 | 253 | while (X509* x509 = PEM_read_bio_X509( | |
| 237 | 254 | bio.get(), nullptr, NoPasswordCallback, nullptr)) { | |
| 238 | 255 | certs->push_back(x509); | |
@@ -248,6 +265,17 @@ static unsigned long LoadCertsFromFile( // NOLINT(runtime/int) | |||
| 248 | 265 | } | |
| 249 | 266 | } | |
| 250 | 267 | ||
| 268 | + static unsigned long LoadCertsFromFile( // NOLINT(runtime/int) | ||
| 269 | + std::vector<X509*>* certs, | ||
| 270 | + const char* file) { | ||
| 271 | + MarkPopErrorOnReturn mark_pop_error_on_return; | ||
| 272 | + | ||
| 273 | + auto bio = BIOPointer::NewFile(file, "r"); | ||
| 274 | + if (!bio) return ERR_get_error(); | ||
| 275 | + | ||
| 276 | + return LoadCertsFromBIO(certs, std::move(bio)); | ||
| 277 | + } | ||
| 278 | + | ||
| 251 | 279 | // Indicates the trust status of a certificate. | |
| 252 | 280 | enum class TrustStatus { | |
| 253 | 281 | // Trust status is unknown / uninitialized. | |
@@ -829,11 +857,24 @@ static std::vector<X509*>& GetExtraCACertificates() { | |||
| 829 | 857 | // NODE_EXTRA_CA_CERTS are cached after first load. Certificates | |
| 830 | 858 | // from --use-system-ca are not cached and always reloaded from | |
| 831 | 859 | // disk. | |
| 860 | + // 8. If users have reset the root cert store by calling | ||
| 861 | + // tls.setDefaultCACertificates(), the store will be populated with | ||
| 862 | + // the certificates provided by users. | ||
| 832 | 863 | // TODO(joyeecheung): maybe these rules need a bit of consolidation? | |
| 833 | 864 | X509_STORE* NewRootCertStore() { | |
| 834 | 865 | X509_STORE* store = X509_STORE_new(); | |
| 835 | 866 | CHECK_NOT_NULL(store); | |
| 836 | 867 | ||
| 868 | + // If the root cert store is already reset by users through | ||
| 869 | + // tls.setDefaultCACertificates(), just create a copy from the | ||
| 870 | + // user-provided certificates. | ||
| 871 | + if (root_certs_from_users != nullptr) { | ||
| 872 | + for (X509* cert : *root_certs_from_users) { | ||
| 873 | + CHECK_EQ(1, X509_STORE_add_cert(store, cert)); | ||
| 874 | + } | ||
| 875 | + return store; | ||
| 876 | + } | ||
| 877 | + | ||
| 837 | 878 | #ifdef NODE_OPENSSL_SYSTEM_CERT_PATH | |
| 838 | 879 | if constexpr (sizeof(NODE_OPENSSL_SYSTEM_CERT_PATH) > 1) { | |
| 839 | 880 | ERR_set_mark(); | |
@@ -901,14 +942,57 @@ void GetBundledRootCertificates(const FunctionCallbackInfo<Value>& args) { | |||
| 901 | 942 | Array::New(env->isolate(), result, arraysize(root_certs))); | |
| 902 | 943 | } | |
| 903 | 944 | ||
| 945 | + bool ArrayOfStringsToX509s(Local<Context> context, | ||
| 946 | + Local<Array> cert_array, | ||
| 947 | + std::vector<X509*>* certs) { | ||
| 948 | + ClearErrorOnReturn clear_error_on_return; | ||
| 949 | + Isolate* isolate = context->GetIsolate(); | ||
| 950 | + Environment* env = Environment::GetCurrent(context); | ||
| 951 | + uint32_t array_length = cert_array->Length(); | ||
| 952 | + | ||
| 953 | + std::vector<v8::Global<Value>> cert_items; | ||
| 954 | + if (FromV8Array(context, cert_array, &cert_items).IsNothing()) { | ||
| 955 | + return false; | ||
| 956 | + } | ||
| 957 | + | ||
| 958 | + for (uint32_t i = 0; i < array_length; i++) { | ||
| 959 | + Local<Value> cert_val = cert_items[i].Get(isolate); | ||
| 960 | + // Parse the PEM certificate. | ||
| 961 | + BIOPointer bio(LoadBIO(env, cert_val)); | ||
| 962 | + if (!bio) { | ||
| 963 | + ThrowCryptoError(env, ERR_get_error(), "Failed to load certificate data"); | ||
| 964 | + return false; | ||
| 965 | + } | ||
| 966 | + | ||
| 967 | + // Read all certificates from this PEM string | ||
| 968 | + size_t start = certs->size(); | ||
| 969 | + auto err = LoadCertsFromBIO(certs, std::move(bio)); | ||
| 970 | + if (err != 0) { | ||
| 971 | + size_t end = certs->size(); | ||
| 972 | + // Clean up any certificates we've already parsed upon failure. | ||
| 973 | + for (size_t j = start; j < end; ++j) { | ||
| 974 | + X509_free((*certs)[j]); | ||
| 975 | + } | ||
| 976 | + ThrowCryptoError(env, err, "Failed to parse certificate"); | ||
| 977 | + return false; | ||
| 978 | + } | ||
| 979 | + } | ||
| 980 | + | ||
| 981 | + return true; | ||
| 982 | + } | ||
| 983 | + | ||
| 984 | + template <typename It> | ||
| 904 | 985 | MaybeLocal<Array> X509sToArrayOfStrings(Environment* env, | |
| 905 | - const std::vector<X509*>& certs) { | ||
| 986 | + It first, | ||
| 987 | + It last, | ||
| 988 | + size_t size) { | ||
| 906 | 989 | ClearErrorOnReturn clear_error_on_return; | |
| 907 | 990 | EscapableHandleScope scope(env->isolate()); | |
| 908 | 991 | ||
| 909 | - LocalVector<Value> result(env->isolate(), certs.size()); | ||
| 910 | - for (size_t i = 0; i < certs.size(); ++i) { | ||
| 911 | - X509View view(certs[i]); | ||
| 992 | + LocalVector<Value> result(env->isolate(), size); | ||
| 993 | + size_t i = 0; | ||
| 994 | + for (It cur = first; cur != last; ++cur, ++i) { | ||
| 995 | + X509View view(*cur); | ||
| 912 | 996 | auto pem_bio = view.toPEM(); | |
| 913 | 997 | if (!pem_bio) { | |
| 914 | 998 | ThrowCryptoError(env, ERR_get_error(), "X509 to PEM conversion"); | |
@@ -933,10 +1017,87 @@ MaybeLocal<Array> X509sToArrayOfStrings(Environment* env, | |||
| 933 | 1017 | return scope.Escape(Array::New(env->isolate(), result.data(), result.size())); | |
| 934 | 1018 | } | |
| 935 | 1019 | ||
| 1020 | + void GetUserRootCertificates(const FunctionCallbackInfo<Value>& args) { | ||
| 1021 | + Environment* env = Environment::GetCurrent(args); | ||
| 1022 | + CHECK_NOT_NULL(root_certs_from_users); | ||
| 1023 | + Local<Array> results; | ||
| 1024 | + if (X509sToArrayOfStrings(env, | ||
| 1025 | + root_certs_from_users->begin(), | ||
| 1026 | + root_certs_from_users->end(), | ||
| 1027 | + root_certs_from_users->size()) | ||
| 1028 | + .ToLocal(&results)) { | ||
| 1029 | + args.GetReturnValue().Set(results); | ||
| 1030 | + } | ||
| 1031 | + } | ||
| 1032 | + | ||
| 1033 | + void ResetRootCertStore(const FunctionCallbackInfo<Value>& args) { | ||
| 1034 | + Local<Context> context = args.GetIsolate()->GetCurrentContext(); | ||
| 1035 | + CHECK(args[0]->IsArray()); | ||
| 1036 | + Local<Array> cert_array = args[0].As<Array>(); | ||
| 1037 | + | ||
| 1038 | + if (cert_array->Length() == 0) { | ||
| 1039 | + // If the array is empty, just clear the user certs and reset the store. | ||
| 1040 | + if (root_cert_store != nullptr) { | ||
| 1041 | + X509_STORE_free(root_cert_store); | ||
| 1042 | + root_cert_store = nullptr; | ||
| 1043 | + } | ||
| 1044 | + | ||
| 1045 | + // Free any existing certificates in the old set. | ||
| 1046 | + if (root_certs_from_users != nullptr) { | ||
| 1047 | + for (X509* cert : *root_certs_from_users) { | ||
| 1048 | + X509_free(cert); | ||
| 1049 | + } | ||
| 1050 | + } | ||
| 1051 | + root_certs_from_users = std::make_unique<X509Set>(); | ||
| 1052 | + return; | ||
| 1053 | + } | ||
| 1054 | + | ||
| 1055 | + // Parse certificates from the array | ||
| 1056 | + std::unique_ptr<std::vector<X509*>> certs = | ||
| 1057 | + std::make_unique<std::vector<X509*>>(); | ||
| 1058 | + if (!ArrayOfStringsToX509s(context, cert_array, certs.get())) { | ||
| 1059 | + // Error already thrown by ArrayOfStringsToX509s | ||
| 1060 | + return; | ||
| 1061 | + } | ||
| 1062 | + | ||
| 1063 | + if (certs->empty()) { | ||
| 1064 | + Environment* env = Environment::GetCurrent(context); | ||
| 1065 | + return THROW_ERR_CRYPTO_OPERATION_FAILED( | ||
| 1066 | + env, "No valid certificates found in the provided array"); | ||
| 1067 | + } | ||
| 1068 | + | ||
| 1069 | + auto new_set = std::make_unique<X509Set>(); | ||
| 1070 | + for (X509* cert : *certs) { | ||
| 1071 | + auto [it, inserted] = new_set->insert(cert); | ||
| 1072 | + if (!inserted) { // Free duplicate certificates from the vector. | ||
| 1073 | + X509_free(cert); | ||
| 1074 | + } | ||
| 1075 | + } | ||
| 1076 | + | ||
| 1077 | + // Free any existing certificates in the old set. | ||
| 1078 | + if (root_certs_from_users != nullptr) { | ||
| 1079 | + for (X509* cert : *root_certs_from_users) { | ||
| 1080 | + X509_free(cert); | ||
| 1081 | + } | ||
| 1082 | + } | ||
| 1083 | + std::swap(root_certs_from_users, new_set); | ||
| 1084 | + | ||
| 1085 | + // Reset the global root cert store and create a new one with the | ||
| 1086 | + // certificates. | ||
| 1087 | + if (root_cert_store != nullptr) { | ||
| 1088 | + X509_STORE_free(root_cert_store); | ||
| 1089 | + } | ||
| 1090 | + | ||
| 1091 | + // TODO(joyeecheung): we can probably just reset it to nullptr | ||
| 1092 | + // and let the next call to NewRootCertStore() create a new one. | ||
| 1093 | + root_cert_store = NewRootCertStore(); | ||
| 1094 | + } | ||
| 1095 | + | ||
| 936 | 1096 | void GetSystemCACertificates(const FunctionCallbackInfo<Value>& args) { | |
| 937 | 1097 | Environment* env = Environment::GetCurrent(args); | |
| 938 | 1098 | Local<Array> results; | |
| 939 | - if (X509sToArrayOfStrings(env, GetSystemStoreCACertificates()) | ||
| 1099 | + std::vector<X509*>& certs = GetSystemStoreCACertificates(); | ||
| 1100 | + if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size()) | ||
| 940 | 1101 | .ToLocal(&results)) { | |
| 941 | 1102 | args.GetReturnValue().Set(results); | |
| 942 | 1103 | } | |
@@ -948,7 +1109,9 @@ void GetExtraCACertificates(const FunctionCallbackInfo<Value>& args) { | |||
| 948 | 1109 | return args.GetReturnValue().Set(Array::New(env->isolate())); | |
| 949 | 1110 | } | |
| 950 | 1111 | Local<Array> results; | |
| 951 | - if (X509sToArrayOfStrings(env, GetExtraCACertificates()).ToLocal(&results)) { | ||
| 1112 | + std::vector<X509*>& certs = GetExtraCACertificates(); | ||
| 1113 | + if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size()) | ||
| 1114 | + .ToLocal(&results)) { | ||
| 952 | 1115 | args.GetReturnValue().Set(results); | |
| 953 | 1116 | } | |
| 954 | 1117 | } | |
@@ -1044,6 +1207,9 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) { | |||
| 1044 | 1207 | context, target, "getSystemCACertificates", GetSystemCACertificates); | |
| 1045 | 1208 | SetMethodNoSideEffect( | |
| 1046 | 1209 | context, target, "getExtraCACertificates", GetExtraCACertificates); | |
| 1210 | + SetMethod(context, target, "resetRootCertStore", ResetRootCertStore); | ||
| 1211 | + SetMethodNoSideEffect( | ||
| 1212 | + context, target, "getUserRootCertificates", GetUserRootCertificates); | ||
| 1047 | 1213 | } | |
| 1048 | 1214 | ||
| 1049 | 1215 | void SecureContext::RegisterExternalReferences( | |
@@ -1086,6 +1252,8 @@ void SecureContext::RegisterExternalReferences( | |||
| 1086 | 1252 | registry->Register(GetBundledRootCertificates); | |
| 1087 | 1253 | registry->Register(GetSystemCACertificates); | |
| 1088 | 1254 | registry->Register(GetExtraCACertificates); | |
| 1255 | + registry->Register(ResetRootCertStore); | ||
| 1256 | + registry->Register(GetUserRootCertificates); | ||
| 1089 | 1257 | } | |
| 1090 | 1258 | ||
| 1091 | 1259 | SecureContext* SecureContext::Create(Environment* env) { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments