diff --git a/.github/workflows/test-shared-openssl.yml b/.github/workflows/test-shared-openssl.yml new file mode 100644 index 00000000000000..2e0621ed8ede41 --- /dev/null +++ b/.github/workflows/test-shared-openssl.yml @@ -0,0 +1,123 @@ +# Throwaway workflow used solely to gather test results for Node.js +# linked against OpenSSL 3.5, 3.6, and 4.0. +name: TEMP - Test Shared libraries (OpenSSL) + +on: + pull_request: + push: + branches: + - tmp-openssl4 + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + FLAKY_TESTS: keep_retrying + +permissions: + contents: read + +jobs: + build-tarball: + name: Build slim tarball + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Make tarball + run: | + export DATESTRING=$(date "+%Y-%m-%d") + export COMMIT=$(git rev-parse --short=10 "$GITHUB_SHA") + ./configure && make tar -j4 SKIP_XZ=1 SKIP_SHARED_DEPS=1 + env: + DISTTYPE: nightly + + - name: Upload tarball artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: tarballs + path: '*.tar.gz' + compression-level: 0 + + build: + needs: build-tarball + strategy: + fail-fast: false + matrix: + opensslPkg: + - openssl_1_1 + - openssl_3 + - openssl_3_5 + - openssl_3_6 + - openssl_4_0 + system: + - x86_64-linux + - aarch64-linux + - x86_64-darwin + - aarch64-darwin + include: + - system: x86_64-linux + runner: ubuntu-24.04 + - system: aarch64-linux + runner: ubuntu-24.04-arm + - system: x86_64-darwin + runner: macos-15-intel + - system: aarch64-darwin + runner: macos-latest + name: '${{ matrix.system }}: with shared libraries (${{ matrix.opensslPkg }})' + runs-on: ${{ matrix.runner }} + # We only care about the test results here, not about gating anything. + continue-on-error: true + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: tarballs + path: tarballs + + - name: Extract tarball + run: | + tar xzf tarballs/*.tar.gz -C "$RUNNER_TEMP" + echo "TAR_DIR=$RUNNER_TEMP/$(basename tarballs/*.tar.gz .tar.gz)" >> "$GITHUB_ENV" + + - uses: cachix/install-nix-action@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3 + with: + extra_nix_config: sandbox = true + + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: nodejs + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + + - name: Configure sccache + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + core.exportVariable('SCCACHE_GHA_ENABLED', 'on'); + core.exportVariable('ACTIONS_CACHE_SERVICE_V2', 'on'); + core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + core.exportVariable('NIX_SCCACHE', '(import {}).sccache'); + + - name: Build Node.js and run tests + env: + # Allow evaluating packages marked insecure in nixpkgs (e.g. openssl_1_1). + NIXPKGS_ALLOW_INSECURE: '1' + run: | + nix-shell \ + -I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \ + --impure \ + --pure --keep TAR_DIR --keep FLAKY_TESTS --keep NIXPKGS_ALLOW_INSECURE \ + --keep SCCACHE_GHA_ENABLED --keep ACTIONS_CACHE_SERVICE_V2 --keep ACTIONS_RESULTS_URL --keep ACTIONS_RUNTIME_TOKEN \ + --arg loadJSBuiltinsDynamically false \ + --arg useSeparateDerivationForV8 true \ + --arg ccache "${NIX_SCCACHE:-null}" \ + --arg devTools '[]' \ + --arg benchmarkTools '[]' \ + --arg opensslPkg '(import {}).${{ matrix.opensslPkg }}' \ + ${{ endsWith(matrix.system, '-darwin') && '--arg withAmaro false --arg withLief false --arg withSQLite false --arg withFFI false --arg extraConfigFlags ''["--without-inspector" "--without-node-options"]'' \' || '\' }} + --run ' + make -C "$TAR_DIR" run-ci -j4 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9 --skip-tests=$CI_SKIP_TESTS" + ' "$TAR_DIR/shell.nix" diff --git a/shell.nix b/shell.nix index 8a873741072e1e..ef895118833d69 100644 --- a/shell.nix +++ b/shell.nix @@ -19,6 +19,7 @@ withFFI ? true, withSSL ? true, withTemporal ? false, + opensslPkg ? pkgs.openssl_3_5, sharedLibDeps ? ( import ./tools/nix/sharedLibDeps.nix { inherit @@ -29,6 +30,7 @@ withFFI withSSL withTemporal + opensslPkg ; } ), diff --git a/test/addons/openssl-binding/binding.cc b/test/addons/openssl-binding/binding.cc index fbb96fd3b20c56..6c85f914097733 100644 --- a/test/addons/openssl-binding/binding.cc +++ b/test/addons/openssl-binding/binding.cc @@ -46,7 +46,7 @@ inline void Initialize(v8::Local exports, .ToLocalChecked(); assert(exports->Set(context, key, value).IsJust()); - const SSL_METHOD* method = TLSv1_2_server_method(); + const SSL_METHOD* method = TLS_server_method(); assert(method != nullptr); key = v8::String::NewFromUtf8(isolate, "hash").ToLocalChecked(); diff --git a/test/parallel/test-crypto-dh-stateless.js b/test/parallel/test-crypto-dh-stateless.js index 86846a613c0a52..4f7a2abc91b557 100644 --- a/test/parallel/test-crypto-dh-stateless.js +++ b/test/parallel/test-crypto-dh-stateless.js @@ -7,6 +7,22 @@ const assert = require('assert'); const crypto = require('crypto'); const { hasOpenSSL } = require('../common/crypto'); +// Error code for a key-type mismatch during ECDH/EdDH derivation. The +// underlying OpenSSL error code varies by version (and in OpenSSL 4.0, by +// platform: some builds report a generic internal error instead +// of a typed key-type mismatch). +// TODO(panva): report the OpenSSL 4.0 generic internal error behavior +// upstream and tighten this check once fixed. +let keyTypeMismatchCode; +if (hasOpenSSL(4, 0)) { + keyTypeMismatchCode = + /^ERR_OSSL_EVP_(OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE|INTERNAL_ERROR)$/; +} else if (hasOpenSSL(3)) { + keyTypeMismatchCode = 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE'; +} else { + keyTypeMismatchCode = 'ERR_OSSL_EVP_DIFFERENT_KEY_TYPES'; +} + assert.throws(() => crypto.diffieHellman(), { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE', @@ -397,9 +413,7 @@ test(crypto.generateKeyPairSync('x25519'), privateKey: crypto.generateKeyPairSync('x448').privateKey, publicKey: crypto.generateKeyPairSync('x25519').publicKey, }; - testDHError(options, { code: hasOpenSSL(3) ? - 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' : - 'ERR_OSSL_EVP_DIFFERENT_KEY_TYPES' }); + testDHError(options, { code: keyTypeMismatchCode }); } // Test all key encoding formats @@ -541,23 +555,21 @@ for (const { privateKey: alicePriv, publicKey: bobPub } of [ testDHError({ privateKey: privKey(ec256.privateKey), publicKey: pubKey(x25519.publicKey), - }, { code: hasOpenSSL(3) ? - 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' : - 'ERR_OSSL_EVP_DIFFERENT_KEY_TYPES' }); + }, { code: keyTypeMismatchCode }); // Unsupported key type (ed25519) testDHError({ privateKey: privKey(ed25519.privateKey), publicKey: pubKey(ed25519.publicKey), - }, { code: 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' }); + }, { code: hasOpenSSL(4, 0) ? + /^ERR_OSSL_EVP_(OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE|INTERNAL_ERROR)$/ : + 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' }); // Incompatible key types (x448 + x25519) testDHError({ privateKey: privKey(x448.privateKey), publicKey: pubKey(x25519.publicKey), - }, { code: hasOpenSSL(3) ? - 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' : - 'ERR_OSSL_EVP_DIFFERENT_KEY_TYPES' }); + }, { code: keyTypeMismatchCode }); // Zero x25519 public key testDHError({ diff --git a/test/parallel/test-tls-alert-handling.js b/test/parallel/test-tls-alert-handling.js index e0ed4db98fbdfe..c319e766ce8a02 100644 --- a/test/parallel/test-tls-alert-handling.js +++ b/test/parallel/test-tls-alert-handling.js @@ -97,11 +97,11 @@ function sendBADTLSRecord() { // Different OpenSSL versions send different TLS alerts when the peer // receives an invalid record on an established connection. assert.match(err.code, - /ERR_SSL_(TLSV1_ALERT_PROTOCOL_VERSION|TLSV1_ALERT_RECORD_OVERFLOW|SSL\/TLS_ALERT_UNEXPECTED_MESSAGE)/); + /ERR_SSL_(TLSV1_ALERT_PROTOCOL_VERSION|TLSV1_ALERT_RECORD_OVERFLOW|(SSL\/)?TLS_ALERT_UNEXPECTED_MESSAGE)/); assert.strictEqual(err.library, 'SSL routines'); if (!hasOpenSSL3 && !process.features.openssl_is_boringssl) assert.strictEqual(err.function, 'ssl3_read_bytes'); assert.match(err.reason, - /tlsv1[\s_]alert[\s_]protocol[\s_]version|tlsv1[\s_]alert[\s_]record[\s_]overflow|ssl\/tls[\s_]alert[\s_]unexpected[\s_]message/i); + /tlsv1[\s_]alert[\s_]protocol[\s_]version|tlsv1[\s_]alert[\s_]record[\s_]overflow|(ssl\/)?tls[\s_]alert[\s_]unexpected[\s_]message/i); })); } diff --git a/test/parallel/test-tls-client-getephemeralkeyinfo.js b/test/parallel/test-tls-client-getephemeralkeyinfo.js index 0f132c565e4400..19728e3733d868 100644 --- a/test/parallel/test-tls-client-getephemeralkeyinfo.js +++ b/test/parallel/test-tls-client-getephemeralkeyinfo.js @@ -70,7 +70,10 @@ function test(size, type, name, cipher) { test(undefined, undefined, undefined, 'AES256-SHA256'); test('auto', 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); -if (!hasOpenSSL(3, 2)) { +if (hasOpenSSL(4, 0)) { + // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and + // always selects FFDHE-2048 regardless of the server-supplied dhparam. +} else if (!hasOpenSSL(3, 2)) { test(1024, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); } else { test(3072, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); diff --git a/test/parallel/test-tls-client-mindhsize.js b/test/parallel/test-tls-client-mindhsize.js index 778e4b710b4e92..cd7b16ea566fe8 100644 --- a/test/parallel/test-tls-client-mindhsize.js +++ b/test/parallel/test-tls-client-mindhsize.js @@ -13,6 +13,7 @@ const secLevel = require('internal/crypto/util').getOpenSSLSecLevel(); const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); +const { hasOpenSSL } = require('../common/crypto'); const key = fixtures.readKey('agent2-key.pem'); const cert = fixtures.readKey('agent2-cert.pem'); @@ -24,7 +25,7 @@ function loadDHParam(n) { return fixtures.readKey(`dh${n}.pem`); } -function test(size, err, next) { +function test(size, err, next, minDHSizeOverride) { const options = { key: key, cert: cert, @@ -46,7 +47,7 @@ function test(size, err, next) { // so that it fails when it makes a connection to the tls // server where is too small. This depends on the openssl // security level - const minDHSize = (secLevel > 1) ? 3072 : 2048; + const minDHSize = minDHSizeOverride ?? ((secLevel > 1) ? 3072 : 2048); const client = tls.connect({ minDHSize: minDHSize, port: this.address().port, @@ -84,7 +85,12 @@ function testDHE3072() { test(3072, false, null); } -if (secLevel > 1) { +if (hasOpenSSL(4, 0)) { + // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and + // ignores the server-supplied dhparam in favor of FFDHE-2048. The 3072 + // success case is therefore replaced by a 2048 success case. + testDHE2048(true, () => test(2048, false, null, 2048)); +} else if (secLevel > 1) { // Minimum size for OpenSSL security level 2 and above is 2048 by default testDHE2048(true, testDHE3072); } else { diff --git a/test/parallel/test-tls-dhe.js b/test/parallel/test-tls-dhe.js index 89cda5f52a6b31..03750bc206adbe 100644 --- a/test/parallel/test-tls-dhe.js +++ b/test/parallel/test-tls-dhe.js @@ -28,6 +28,7 @@ if (!common.hasCrypto) { const { opensslCli, + hasOpenSSL, } = require('../common/crypto'); // OpenSSL has a set of security levels which affect what algorithms @@ -104,9 +105,15 @@ function testCustomParam(keylen, expectedCipher) { } (async () => { - // By default, DHE is disabled while ECDHE is enabled. + // By default, DHE is disabled while ECDHE is enabled. OpenSSL 4.0 + // implements RFC 7919 FFDHE negotiation for TLS 1.2 which enables DHE + // (with FFDHE-2048) even without a server-supplied dhparam. for (const dhparam of [undefined, null]) { - await test(dhparam, null, ecdheCipher); + if (hasOpenSSL(4, 0)) { + await test(dhparam, 2048, dheCipher); + } else { + await test(dhparam, null, ecdheCipher); + } } // The DHE parameters selected by OpenSSL depend on the strength of the @@ -124,14 +131,24 @@ function testCustomParam(keylen, expectedCipher) { // Custom DHE parameters are supported (but discouraged). // 1024 is disallowed at security level 2 and above so use 3072 instead - // for higher security levels + // for higher security levels. + // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and + // ignores the server-supplied dhparam in favor of FFDHE-2048, so the + // negotiated key length is always 2048. if (secLevel < 2) { await testCustomParam(1024, dheCipher); + } else if (hasOpenSSL(4, 0)) { + await test(loadDHParam(3072), 2048, dheCipher); } else { await testCustomParam(3072, dheCipher); } await testCustomParam(2048, dheCipher); - // Invalid DHE parameters are discarded. ECDHE remains enabled. - await testCustomParam('error', ecdheCipher); + // Invalid DHE parameters are discarded. Prior to OpenSSL 4.0 this + // disabled DHE and ECDHE was negotiated; since 4.0, FFDHE-2048 is used. + if (hasOpenSSL(4, 0)) { + await test(loadDHParam('error'), 2048, dheCipher); + } else { + await testCustomParam('error', ecdheCipher); + } })().then(common.mustCall()); diff --git a/test/parallel/test-tls-ecdh-multiple.js b/test/parallel/test-tls-ecdh-multiple.js index 957f8e0407a6de..ee52f288610956 100644 --- a/test/parallel/test-tls-ecdh-multiple.js +++ b/test/parallel/test-tls-ecdh-multiple.js @@ -8,7 +8,7 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { opensslCli } = require('../common/crypto'); +const { opensslCli, hasOpenSSL } = require('../common/crypto'); const crypto = require('crypto'); if (!opensslCli) { @@ -24,11 +24,17 @@ function loadPEM(n) { return fixtures.readKey(`${n}.pem`); } +// OpenSSL 4.0 disables support for deprecated elliptic curves from RFC 8422 +// (including secp256k1) by default. +const ecdhCurve = hasOpenSSL(4, 0) ? + 'prime256v1:secp521r1' : + 'secp256k1:prime256v1:secp521r1'; + const options = { key: loadPEM('agent2-key'), cert: loadPEM('agent2-cert'), ciphers: '-ALL:ECDHE-RSA-AES128-SHA256', - ecdhCurve: 'secp256k1:prime256v1:secp521r1', + ecdhCurve, maxVersion: 'TLSv1.2', }; @@ -60,6 +66,11 @@ const server = tls.createServer(options, (conn) => { unsupportedCurves.push('brainpoolP256r1'); } + // Deprecated RFC 8422 curves are disabled by default in OpenSSL 4.0. + if (hasOpenSSL(4, 0)) { + unsupportedCurves.push('secp256k1'); + } + unsupportedCurves.forEach((ecdhCurve) => { assert.throws(() => tls.createServer({ ecdhCurve }), /Error: Failed to set ECDH curve/); diff --git a/test/parallel/test-tls-error-stack.js b/test/parallel/test-tls-error-stack.js index 36deb05b511234..02021b060ecb83 100644 --- a/test/parallel/test-tls-error-stack.js +++ b/test/parallel/test-tls-error-stack.js @@ -11,6 +11,9 @@ const tls = require('tls'); assert.throws(() => { tls.createSecureContext({ clientCertEngine: 'x' }); }, (err) => { + if (err.code === 'ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED') + common.skip('OpenSSL dropped engine support'); + return err.name === 'Error' && /could not load the shared library/.test(err.message) && Array.isArray(err.opensslErrorStack) && diff --git a/test/parallel/test-tls-ocsp-callback.js b/test/parallel/test-tls-ocsp-callback.js index 3a2d8e45f772ac..3a54ca84eb89b8 100644 --- a/test/parallel/test-tls-ocsp-callback.js +++ b/test/parallel/test-tls-ocsp-callback.js @@ -29,6 +29,7 @@ if (!common.hasCrypto) { const crypto = require('crypto'); const tls = require('tls'); const fixtures = require('../common/fixtures'); +const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); @@ -90,7 +91,10 @@ function test(testOptions, cb) { client.on('OCSPResponse', common.mustCall((resp) => { if (testOptions.response) { - assert.strictEqual(resp.toString(), testOptions.response); + if (Buffer.isBuffer(testOptions.response)) + assert.deepStrictEqual(resp, testOptions.response); + else + assert.strictEqual(resp.toString(), testOptions.response); client.destroy(); } else { assert.strictEqual(resp, null); @@ -103,10 +107,27 @@ function test(testOptions, cb) { })); } +// OpenSSL 3.6+ validates that the value passed to +// SSL_set_tlsext_status_ocsp_resp parses as DER, so the test responses need +// to be valid DER-encoded OCSPResponse values. +// Minimal OCSPResponse is SEQUENCE { ENUMERATED responseStatus } where +// 0 = successful and 1 = malformedRequest. +const response1 = Buffer.from([0x30, 0x03, 0x0a, 0x01, 0x00]); +const response2 = Buffer.from([0x30, 0x03, 0x0a, 0x01, 0x01]); + test({ ocsp: true, response: false }); -test({ ocsp: true, response: 'hello world' }); +test({ ocsp: true, response: response1 }); test({ ocsp: false }); if (!crypto.getFips()) { - test({ ocsp: true, response: 'hello pfx', pfx: pfx, passphrase: 'sample' }); + test({ ocsp: true, response: response2, pfx: pfx, passphrase: 'sample' }); +} + +// Older OpenSSL versions accept arbitrary bytes (not just DER) as the OCSP +// response, so additionally exercise the string path there. +if (!hasOpenSSL(3, 6)) { + test({ ocsp: true, response: 'hello world' }); + if (!crypto.getFips()) { + test({ ocsp: true, response: 'hello pfx', pfx: pfx, passphrase: 'sample' }); + } } diff --git a/test/parallel/test-tls-set-ciphers-error.js b/test/parallel/test-tls-set-ciphers-error.js index 0df5a9288de1df..3cfc8c391bf7d5 100644 --- a/test/parallel/test-tls-set-ciphers-error.js +++ b/test/parallel/test-tls-set-ciphers-error.js @@ -7,12 +7,13 @@ if (!common.hasCrypto) const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); +const { hasOpenSSL } = require('../common/crypto'); { const options = { key: fixtures.readKey('agent2-key.pem'), cert: fixtures.readKey('agent2-cert.pem'), - ciphers: 'aes256-sha' + ciphers: 'DES-CBC-SHA' }; assert.throws(() => tls.createServer(options, common.mustNotCall()), /no[_ ]cipher[_ ]match/i); @@ -23,3 +24,19 @@ const fixtures = require('../common/fixtures'); assert.throws(() => tls.createServer(options, common.mustNotCall()), /no[_ ]cipher[_ ]match/i); } + +// Cipher name matching is case-sensitive prior to OpenSSL 4.0, and +// case-insensitive starting with OpenSSL 4.0. +{ + const options = { + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + ciphers: 'aes256-sha', + }; + if (hasOpenSSL(4, 0)) { + tls.createServer(options).close(); + } else { + assert.throws(() => tls.createServer(options, common.mustNotCall()), + /no[_ ]cipher[_ ]match/i); + } +} diff --git a/tools/nix/pkgs.nix b/tools/nix/pkgs.nix index f4dab340752819..d7467c52a5b478 100644 --- a/tools/nix/pkgs.nix +++ b/tools/nix/pkgs.nix @@ -1,10 +1,10 @@ arg: let repo = "https://github.com/NixOS/nixpkgs"; - rev = "13043924aaa7375ce482ebe2494338e058282925"; + rev = "ec9a189b46ab91b00e19c8e4abe55d6af792e43d"; nixpkgs = import (builtins.fetchTarball { url = "${repo}/archive/${rev}.tar.gz"; - sha256 = "1pbv1c3syp94rh147s2nhbzfcib01blz3s7g290m43s3nk71404z"; + sha256 = "1s42s1na2r6dhs25dcpfs8gl9xw31ajg4wj7h9jskn0ml7ksw6wc"; }) arg; in nixpkgs diff --git a/tools/nix/sharedLibDeps.nix b/tools/nix/sharedLibDeps.nix index 27e97c132ec71b..823b46086bb0d7 100644 --- a/tools/nix/sharedLibDeps.nix +++ b/tools/nix/sharedLibDeps.nix @@ -6,6 +6,7 @@ withSSL ? true, withFFI ? true, withTemporal ? false, + opensslPkg ? pkgs.openssl_3_5, }: { inherit (pkgs) @@ -51,7 +52,7 @@ ffi = pkgs.libffiReal; }) // (pkgs.lib.optionalAttrs withSSL ({ - openssl = pkgs.openssl_3_5; + openssl = opensslPkg; })) // (pkgs.lib.optionalAttrs withTemporal { inherit (pkgs) temporal_capi;