From edd66d0130714cb0ac4c6038965f15d0bd25cb8a Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 18 Jul 2025 21:57:53 +0200 Subject: [PATCH 1/2] crypto: add tls.setDefaultCACertificates() This API allows dynamically configuring CA certificates that will be used by the Node.js TLS clients by default. Once called, the provided certificates will become the default CA certificate list returned by `tls.getCACertificates('default')` and used by TLS connections that don't specify their own CA certificates. This function only affects the current Node.js thread. PR-URL: https://github.com/nodejs/node/pull/58822 Reviewed-By: Matteo Collina Reviewed-By: Tim Perry Reviewed-By: Ethan Arrowood --- doc/api/tls.md | 48 +++++ lib/tls.js | 32 +++ src/crypto/crypto_context.cc | 196 ++++++++++++++++-- test/common/tls.js | 32 +++ .../es-modules/custom-condition/load.cjs | 6 + test/fixtures/tls-extra-ca-override.js | 50 +++++ ...t-default-ca-certificates-append-fetch.mjs | 54 +++++ ...lt-ca-certificates-append-https-request.js | 71 +++++++ ...et-default-ca-certificates-array-buffer.js | 39 ++++ ...t-tls-set-default-ca-certificates-basic.js | 58 ++++++ ...t-tls-set-default-ca-certificates-error.js | 41 ++++ ...-default-ca-certificates-extra-override.js | 19 ++ ...set-default-ca-certificates-mixed-types.js | 46 ++++ ...ault-ca-certificates-precedence-bundled.js | 53 +++++ ...efault-ca-certificates-precedence-empty.js | 51 +++++ ...ls-set-default-ca-certificates-recovery.js | 43 ++++ ...et-default-ca-certificates-reset-fetch.mjs | 47 +++++ ...ult-ca-certificates-reset-https-request.js | 62 ++++++ ...fault-ca-certificates-append-system-ca.mjs | 49 +++++ ...efault-ca-certificates-override-system.mjs | 87 ++++++++ ...ult-ca-certificates-system-combinations.js | 58 ++++++ 21 files changed, 1128 insertions(+), 14 deletions(-) create mode 100644 test/fixtures/es-modules/custom-condition/load.cjs create mode 100644 test/fixtures/tls-extra-ca-override.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-append-fetch.mjs create mode 100644 test/parallel/test-tls-set-default-ca-certificates-append-https-request.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-array-buffer.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-basic.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-error.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-extra-override.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-mixed-types.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-precedence-bundled.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-precedence-empty.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-recovery.js create mode 100644 test/parallel/test-tls-set-default-ca-certificates-reset-fetch.mjs create mode 100644 test/parallel/test-tls-set-default-ca-certificates-reset-https-request.js create mode 100644 test/system-ca/test-set-default-ca-certificates-append-system-ca.mjs create mode 100644 test/system-ca/test-set-default-ca-certificates-override-system.mjs create mode 100644 test/system-ca/test-set-default-ca-certificates-system-combinations.js diff --git a/doc/api/tls.md b/doc/api/tls.md index 5fde522dc9ae31..7be5ee7fb29a56 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -2260,6 +2260,54 @@ openssl pkcs12 -certpbe AES-256-CBC -export -out client-cert.pem \ The server can be tested by connecting to it using the example client from [`tls.connect()`][]. +## `tls.setDefaultCACertificates(certs)` + + + +* `certs` {string\[]|ArrayBufferView\[]} An array of CA certificates in PEM format. + +Sets the default CA certificates used by Node.js TLS clients. If the provided +certificates are parsed successfully, they will become the default CA +certificate list returned by [`tls.getCACertificates()`][] and used +by subsequent TLS connections that don't specify their own CA certificates. +The certificates will be deduplicated before being set as the default. + +This function only affects the current Node.js thread. Previous +sessions cached by the HTTPS agent won't be affected by this change, so +this method should be called before any unwanted cachable TLS connections are +made. + +To use system CA certificates as the default: + +```cjs +const tls = require('node:tls'); +tls.setDefaultCACertificates(tls.getCACertificates('system')); +``` + +```mjs +import tls from 'node:tls'; +tls.setDefaultCACertificates(tls.getCACertificates('system')); +``` + +This function completely replaces the default CA certificate list. To add additional +certificates to the existing defaults, get the current certificates and append to them: + +```cjs +const tls = require('node:tls'); +const currentCerts = tls.getCACertificates('default'); +const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; +tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); +``` + +```mjs +import tls from 'node:tls'; +const currentCerts = tls.getCACertificates('default'); +const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; +tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); +``` + ## `tls.getCACertificates([type])` + +When importing WebAssembly modules, the +[WebAssembly JS String Builtins Proposal][] is automatically enabled through the +ESM Integration. This allows WebAssembly modules to directly use efficient +compile-time string builtins from the `wasm:js-string` namespace. + +For example, the following Wasm module exports a string `getLength` function using +the `wasm:js-string` `length` builtin: + +```text +(module + ;; Compile-time import of the string length builtin. + (import "wasm:js-string" "length" (func $string_length (param externref) (result i32))) + + ;; Define getLength, taking a JS value parameter assumed to be a string, + ;; calling string length on it and returning the result. + (func $getLength (param $str externref) (result i32) + local.get $str + call $string_length + ) + + ;; Export the getLength function. + (export "getLength" (func $get_length)) +) +``` + +```js +import { getLength } from './string-len.wasm'; +getLength('foo'); // Returns 3. +``` + +Wasm builtins are compile-time imports that are linked during module compilation +rather than during instantiation. They do not behave like normal module graph +imports and they cannot be inspected via `WebAssembly.Module.imports(mod)` +or virtualized unless recompiling the module using the direct +`WebAssembly.compile` API with string builtins disabled. + +Importing a module in the source phase before it has been instantiated will also +use the compile-time builtins automatically: + +```js +import source mod from './string-len.wasm'; +const { exports: { getLength } } = await WebAssembly.instantiate(mod, {}); +getLength('foo'); // Also returns 3. +``` + +### Reserved Wasm Namespaces + + + +When importing WebAssembly modules through the ESM Integration, they cannot use +import module names or import/export names that start with reserved prefixes: + +* `wasm-js:` - reserved in all module import names, module names and export + names. +* `wasm:` - reserved in module import names and export names (imported module + names are allowed in order to support future builtin polyfills). + +Importing a module using the above reserved names will throw a +`WebAssembly.LinkError`. + ## Top-level `await` @@ -1206,6 +1274,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][]. [Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports [Terminology]: #terminology [URL]: https://url.spec.whatwg.org/ +[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins [`"exports"`]: packages.md#exports [`"type"`]: packages.md#type [`--input-type`]: cli.md#--input-typetype diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 82c727909e8cc1..e837f2d1ff380b 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -506,7 +506,10 @@ translators.set('wasm', async function(url, source) { // TODO(joyeecheung): implement a translator that just uses // compiled = new WebAssembly.Module(source) to compile it // synchronously. - compiled = await WebAssembly.compile(source); + compiled = await WebAssembly.compile(source, { + // The ESM Integration auto-enables Wasm JS builtins by default when available. + builtins: ['js-string'], + }); } catch (err) { err.message = errPath(url) + ': ' + err.message; throw err; @@ -518,6 +521,13 @@ translators.set('wasm', async function(url, source) { if (impt.kind === 'global') { ArrayPrototypePush(wasmGlobalImports, impt); } + // Prefix reservations per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module. + if (impt.module.startsWith('wasm-js:')) { + throw new WebAssembly.LinkError(`Invalid Wasm import "${impt.module}" in ${url}`); + } + if (impt.name.startsWith('wasm:') || impt.name.startsWith('wasm-js:')) { + throw new WebAssembly.LinkError(`Invalid Wasm import name "${impt.module}" in ${url}`); + } importsList.add(impt.module); } @@ -527,6 +537,9 @@ translators.set('wasm', async function(url, source) { if (expt.kind === 'global') { wasmGlobalExports.add(expt.name); } + if (expt.name.startsWith('wasm:') || expt.name.startsWith('wasm-js:')) { + throw new WebAssembly.LinkError(`Invalid Wasm export name "${expt.name}" in ${url}`); + } exportsList.add(expt.name); } diff --git a/test/es-module/test-esm-wasm.mjs b/test/es-module/test-esm-wasm.mjs index 5a3101fc7594f6..86aa347c357551 100644 --- a/test/es-module/test-esm-wasm.mjs +++ b/test/es-module/test-esm-wasm.mjs @@ -403,4 +403,95 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () => strictEqual(stdout, ''); notStrictEqual(code, 0); }); + + it('should reject wasm: import names', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + '--no-warnings', + '--experimental-wasm-modules', + '--input-type=module', + '--eval', + `import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-name.wasm'))})`, + ]); + + match(stderr, /Invalid Wasm import name/); + strictEqual(stdout, ''); + notStrictEqual(code, 0); + }); + + it('should reject wasm-js: import names', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + '--no-warnings', + '--experimental-wasm-modules', + '--input-type=module', + '--eval', + `import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-name-wasm-js.wasm'))})`, + ]); + + match(stderr, /Invalid Wasm import name/); + strictEqual(stdout, ''); + notStrictEqual(code, 0); + }); + + it('should reject wasm-js: import module names', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + '--no-warnings', + '--experimental-wasm-modules', + '--input-type=module', + '--eval', + `import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-module.wasm'))})`, + ]); + + match(stderr, /Invalid Wasm import/); + strictEqual(stdout, ''); + notStrictEqual(code, 0); + }); + + it('should reject wasm: export names', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + '--no-warnings', + '--experimental-wasm-modules', + '--input-type=module', + '--eval', + `import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-export-name.wasm'))})`, + ]); + + match(stderr, /Invalid Wasm export/); + strictEqual(stdout, ''); + notStrictEqual(code, 0); + }); + + it('should reject wasm-js: export names', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + '--no-warnings', + '--experimental-wasm-modules', + '--input-type=module', + '--eval', + `import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-export-name-wasm-js.wasm'))})`, + ]); + + match(stderr, /Invalid Wasm export/); + strictEqual(stdout, ''); + notStrictEqual(code, 0); + }); + + it('should support js-string builtins', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + '--no-warnings', + '--experimental-wasm-modules', + '--input-type=module', + '--eval', + [ + 'import { strictEqual } from "node:assert";', + `import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/js-string-builtins.wasm'))};`, + 'strictEqual(wasmExports.getLength("hello"), 5);', + 'strictEqual(wasmExports.concatStrings("hello", " world"), "hello world");', + 'strictEqual(wasmExports.compareStrings("test", "test"), 1);', + 'strictEqual(wasmExports.compareStrings("test", "different"), 0);', + ].join('\n'), + ]); + + strictEqual(stderr, ''); + strictEqual(stdout, ''); + strictEqual(code, 0); + }); }); diff --git a/test/fixtures/es-modules/invalid-export-name-wasm-js.wasm b/test/fixtures/es-modules/invalid-export-name-wasm-js.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a6b9a7f7c5ad57d8347003d77940f6ec99dadf26 GIT binary patch literal 64 zcmZQbEY4+QU|?WmWlUgTtY>CoWMCI&t+>OW#*M7=47TYFmSOkvM@MmaWn9- SCoWMCIy{zCFW$NFfeejF|sf?YH>60vE(J@ PrZTcKGO(1S7MB13J$4H? literal 0 HcmV?d00001 diff --git a/test/fixtures/es-modules/invalid-export-name.wat b/test/fixtures/es-modules/invalid-export-name.wat new file mode 100644 index 00000000000000..ef99fef9cfa52f --- /dev/null +++ b/test/fixtures/es-modules/invalid-export-name.wat @@ -0,0 +1,7 @@ +;; Test WASM module with invalid export name starting with 'wasm:' +(module + (func $test (result i32) + i32.const 42 + ) + (export "wasm:invalid" (func $test)) +) \ No newline at end of file diff --git a/test/fixtures/es-modules/invalid-import-module.wasm b/test/fixtures/es-modules/invalid-import-module.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ead151ac0c84ad5a2504c1f7752a08e14b989a0e GIT binary patch literal 94 zcmZQbEY4+QU|?WmWlUgTtY?y71GvMW#*M7=47U@l%y7yFfcGPF*2}oFhY2Y iTx^Ui3<3{zCFW$NFfcGPF*2}oKx7#h ix!4$47z7x&8Dv@V5_3}-#h4g)p-Me-3-XIfAPNDJ%oP;? literal 0 HcmV?d00001 diff --git a/test/fixtures/es-modules/invalid-import-name-wasm-js.wat b/test/fixtures/es-modules/invalid-import-name-wasm-js.wat new file mode 100644 index 00000000000000..cb4d3eaf162818 --- /dev/null +++ b/test/fixtures/es-modules/invalid-import-name-wasm-js.wat @@ -0,0 +1,8 @@ +;; Test WASM module with invalid import name starting with 'wasm-js:' +(module + (import "test" "wasm-js:invalid" (func $invalidImport (result i32))) + (export "test" (func $test)) + (func $test (result i32) + call $invalidImport + ) +) \ No newline at end of file diff --git a/test/fixtures/es-modules/invalid-import-name.wasm b/test/fixtures/es-modules/invalid-import-name.wasm new file mode 100644 index 0000000000000000000000000000000000000000..3c631418294584fecbe13fc122c28dbab5670525 GIT binary patch literal 91 zcmZQbEY4+QU|?WmWlUgTtY;EsWGP84F5xK$id$vol_ln6rZ6xtGchu-b3mjR7`fOO fSr`NuxEW+w@)C1X8O4|wc%e!?a|`l|N+1dWJZlsM literal 0 HcmV?d00001 diff --git a/test/fixtures/es-modules/invalid-import-name.wat b/test/fixtures/es-modules/invalid-import-name.wat new file mode 100644 index 00000000000000..1aae87aaed4840 --- /dev/null +++ b/test/fixtures/es-modules/invalid-import-name.wat @@ -0,0 +1,8 @@ +;; Test WASM module with invalid import name starting with 'wasm:' +(module + (import "test" "wasm:invalid" (func $invalidImport (result i32))) + (export "test" (func $test)) + (func $test (result i32) + call $invalidImport + ) +) \ No newline at end of file diff --git a/test/fixtures/es-modules/js-string-builtins.wasm b/test/fixtures/es-modules/js-string-builtins.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b4c08587dd08e715fa2a79fead8fc46143d949d2 GIT binary patch literal 325 zcmZ9GF%tnX6omKXe2#F;aBfPR5i{xQH%ZBuiCj+Q#U4x}B$^V12y^ m*0XTp2)>iej}7&6