diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c63475dc60f..771d9034b64 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,11 @@ updates: commit-message: prefix: meta open-pull-requests-limit: 10 + groups: + codeql-action: + applies-to: version-updates + patterns: + - github/codeql-action - package-ecosystem: npm directory: /tools/eslint diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0b83c888ecd..1b3179ef4af 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,18 +25,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: languages: ${{ matrix.language }} config-file: ./.github/codeql-config.yml - name: Autobuild - uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: category: /language:${{matrix.language}} diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 037ea7e810e..6b173d8bc97 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -41,7 +41,11 @@ jobs: '--no-validate-metadata', '--tap', '-', ], { cwd: process.env.RUNNER_TEMP, - input: Buffer.from(JSON.stringify([{ id: commit.sha, message: commit.commit.message }])), + input: Buffer.from(JSON.stringify([{ + id: commit.sha, + message: commit.commit.message, + author: commit.commit.author, + }])), silent: true, ignoreReturnCode: true, }); diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 600dadc17bc..8f29a390c67 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -188,6 +188,7 @@ jobs: # to be set here because `checkout` configures GitHub authentication # for push as well. token: ${{ secrets.GH_USER_TOKEN }} + persist-credentials: true - name: Start the Commit Queue if: steps.get_mergeable_prs.outputs.numbers != '' diff --git a/.github/workflows/notify-on-push.yml b/.github/workflows/notify-on-push.yml index 16bd91bccd2..25421b8447d 100644 --- a/.github/workflows/notify-on-push.yml +++ b/.github/workflows/notify-on-push.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04-arm steps: - name: Slack Notification - uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 + uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 env: SLACK_COLOR: '#DE512A' SLACK_ICON: https://github.com/nodejs.png?size=48 @@ -50,7 +50,7 @@ jobs: COMMITS: ${{ toJSON(github.event.commits) }} - name: Slack Notification if: ${{ failure() && steps.commit-check.conclusion == 'failure' && github.repository == 'nodejs/node' }} - uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 + uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 env: SLACK_COLOR: '#DE512A' SLACK_ICON: https://github.com/nodejs.png?size=48 diff --git a/.github/workflows/notify-on-review-wanted.yml b/.github/workflows/notify-on-review-wanted.yml index 2f1f3af8139..effc6c209eb 100644 --- a/.github/workflows/notify-on-review-wanted.yml +++ b/.github/workflows/notify-on-review-wanted.yml @@ -34,7 +34,7 @@ jobs: fi - name: Slack Notification - uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # 2.4.0 + uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 env: MSG_MINIMAL: actions url SLACK_COLOR: '#3d85c6' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5042342ca70..4a7415bc90e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs @@ -76,6 +76,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: sarif_file: results.sarif diff --git a/BUILDING.md b/BUILDING.md index 36d80912f58..6770bd3711b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -776,16 +776,7 @@ Follow , and you should notice that obj file will be bigger than the normal one. First, install ccache. Assuming the installation of ccache is in `c:\ccache` -(where you can find `ccache.exe`), copy `c:\ccache\ccache.exe` to `c:\ccache\cl.exe` -with this command. - -```powershell -cp c:\ccache\ccache.exe c:\ccache\cl.exe -``` - -With newer version of Visual Studio, it may need the copy to be `clang-cl.exe` -instead. If the output of `vcbuild.bat` suggests missing `clang-cl.exe`, copy -it differently: +(where you can find `ccache.exe`), setup aliases as the following commands: ```powershell cp c:\ccache\ccache.exe c:\ccache\clang-cl.exe @@ -800,12 +791,6 @@ When building Node.js, provide a path to your ccache via the option: This will allow for near-instantaneous rebuilds when switching branches back and forth that were built with cache. -To use it with ClangCL, run this instead: - -```powershell -.\vcbuild.bat clang-cl ccache c:\ccache\ -``` - ### Android Android is not a supported platform. Patches to improve the Android build are diff --git a/GOVERNANCE.md b/GOVERNANCE.md index a1fd4148aae..c71ec5ee96a 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -12,6 +12,7 @@ * [Ideal Nominees](#ideal-nominees) * [Nominating a new Collaborator](#nominating-a-new-collaborator) * [Onboarding](#onboarding) + * [Restoring emeritus Collaborators](#restoring-emeritus-collaborators) * [Consensus seeking process](#consensus-seeking-process) @@ -68,7 +69,8 @@ See: * Merging pull requests The TSC can remove inactive collaborators or provide them with _emeritus_ -status. Emeriti may request that the TSC restore them to active status. +status. Emeriti may request that the TSC restore them to active status. See +[Restoring emeritus Collaborators](#restoring-emeritus-collaborators). A collaborator is automatically made emeritus (and removed from active collaborator status) if it has been more than 12 months since the collaborator @@ -335,6 +337,29 @@ After the nomination passes, a TSC member onboards the new collaborator. See [the onboarding guide](./onboarding.md) for details of the onboarding process. +### Restoring emeritus Collaborators + +An emeritus collaborator who has resumed contributing may request restoration to +active status by opening an issue in [the TSC issue tracker][]. The request +describes their recent contributions and their intent to take on collaborator +responsibilities again. There is no new nomination and no vote. The request +stays open for one week, matching the window for a collaborator nomination. If +no TSC member objects, the request passes. + +Before restoring access, a TSC member confirms that the account making the +request is still under the control of the same person. See +[The Authenticity of Contributors](#the-authenticity-of-contributors). + +After the request passes, a TSC member re-onboards the returning collaborator, +reversing the applicable +[offboarding tasks](./doc/contributing/offboarding.md). As in +[the onboarding guide][], the returning collaborator authors the pull request +moving themselves from the emeriti list back to the active list in the README. +That restarts the activity clock the [inactive collaborator workflow][] measures. + +An emeritus TSC member returning as a collaborator rejoins the TSC through a TSC +motion under [Section 3 of the TSC Charter][TSC Charter]. + ## Consensus seeking process The TSC follows a [Consensus Seeking][] decision-making model per the @@ -343,5 +368,8 @@ The TSC follows a [Consensus Seeking][] decision-making model per the [Consensus Seeking]: https://en.wikipedia.org/wiki/Consensus-seeking_decision-making [TSC Charter]: https://github.com/nodejs/TSC/blob/HEAD/TSC-Charter.md [discussion in the nodejs/collaborators]: https://github.com/nodejs/collaborators/discussions/categories/collaborator-nominations +[inactive collaborator workflow]: https://github.com/nodejs/node/blob/HEAD/.github/workflows/find-inactive-collaborators.yml [nodejs/help]: https://github.com/nodejs/help [nodejs/node]: https://github.com/nodejs/node +[the TSC issue tracker]: https://github.com/nodejs/TSC/issues +[the onboarding guide]: ./onboarding.md#exercise-make-a-pull-request-adding-yourself-to-the-readme diff --git a/README.md b/README.md index 0376c0e9814..d90887e0f5d 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,8 @@ For information about the governance of the Node.js project, see **Antoine du Hamel** <> (he/him) - [Support me](https://github.com/sponsors/aduh95) * [anonrig](https://github.com/anonrig) - **Yagiz Nizipli** <> (he/him) - [Support me](https://github.com/sponsors/anonrig) +* [araujogui](https://github.com/araujogui) - + **Guilherme Araújo** <> (he/him) * [atlowChemi](https://github.com/atlowChemi) - **Chemi Atlow** <> (he/him) * [avivkeller](https://github.com/avivkeller) - @@ -761,6 +763,8 @@ maintaining the Node.js project. **Wiyeong Seo** <> * [iam-frankqiu](https://github.com/iam-frankqiu) - **Frank Qiu** <> (he/him) +* [inoway46](https://github.com/inoway46) - + **Yuya Inoue** <> (he/him) * [milesguicent](https://github.com/milesguicent) - **Miles Guicent** <> (he/him) * [preveen-stack](https://github.com/preveen-stack) - diff --git a/SECURITY.md b/SECURITY.md index e86448191d6..cdf2469343b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -500,7 +500,15 @@ The following are **not** vulnerabilities in Node.js: * **Operator-controlled flags**: Behavior unlocked by flags the operator explicitly passes (e.g., `--localstorage-file`) is the operator's responsibility. The permission model does not restrict how Node.js behaves - when the operator intentionally configures it. + when the operator intentionally configures it. This includes any file or + resource that Node.js itself creates, writes, or reads at a location the + operator selected through a flag, including every path derived from a + template or pattern in that flag. For example, trace files rotated by + `--trace-event-file-pattern` (`${rotation}`) being written without a + matching `--allow-fs-write` entry is not a permission model bypass. Such + paths are part of the operator's configuration, not application file-system + access. Inconsistent checks on these paths are treated as regular bugs and + should be reported through the public issue tracker. * **`node:sqlite` and the permission model**: `DatabaseSync` operates with the same file-system privileges as the process. Using SQL pragmas or built-in diff --git a/benchmark/compare.js b/benchmark/compare.js index 77874e8af6c..38fba5ba267 100644 --- a/benchmark/compare.js +++ b/benchmark/compare.js @@ -1,6 +1,7 @@ 'use strict'; const { spawn, fork } = require('node:child_process'); +const { closeSync, openSync, writeSync } = require('node:fs'); const { inspect } = require('util'); const path = require('path'); const CLI = require('./_cli.js'); @@ -27,7 +28,9 @@ const cli = new CLI(`usage: ./node compare.js [options] [--] ... --no-progress don't show benchmark progress indicator --analyze perform statistical analysis after benchmarks complete (Welch's t-test, effect size) instead - of printing csv output + of printing csv output to stdout + --csv filename write csv output to filename (can be combined + with --analyze). Use - to write to stdout. --scale 1000 rate-to-integer multiplier for histogram precision when using --analyze (default: 1000) --max-regression N exit with code 1 if any statistically @@ -60,6 +63,16 @@ if (benchmarks.length === 0) { return; } +const cvsToStdout = cli.optional.csv === '-'; +const csvFd = cli.optional.csv === undefined || cvsToStdout ? + null : + openSync(cli.optional.csv, 'w'); +const outputCsv = !analyze || csvFd !== null || cvsToStdout; + +function writeCsv(line) { + writeSync(csvFd || process.stdout.fd, `${line}\n`); +} + // When --analyze is set, collect results for statistical analysis. const results = analyze ? new Map() : null; @@ -78,17 +91,19 @@ for (const filename of benchmarks) { } // queue.length = binary.length * runs * benchmarks.length -// Print csv header (unless analyzing inline). -if (!analyze) { - console.log('"binary","filename","configuration","rate","time"'); +// Print csv header unless only analyzing inline. +if (outputCsv) { + writeCsv('"binary","filename","configuration","rate","time"'); } const kStartOfQueue = 0; -const showProgress = !cli.optional['no-progress']; +const showProgress = !cli.optional['no-progress'] && !cvsToStdout; let progress; if (showProgress) { - progress = new BenchmarkProgress(queue, benchmarks, { analyze }); + progress = new BenchmarkProgress(queue, benchmarks, { + analyze: analyze || csvFd !== null, + }); progress.startQueue(kStartOfQueue); } @@ -126,11 +141,13 @@ if (showProgress) { results.set(name, { old: [], new: [] }); } results.get(name)[job.binary].push(data.rate); - } else { + } + + if (outputCsv) { // Escape quotes (") for correct csv formatting - conf = conf.replace(/"/g, '""'); - console.log(`"${job.binary}","${job.filename}","${conf}",` + - `${data.rate},${data.time}`); + const csvConf = conf.replace(/"/g, '""'); + writeCsv(`"${job.binary}","${job.filename}","${csvConf}",` + + `${data.rate},${data.time}`); } if (showProgress) { // One item in the subqueue has been completed. @@ -153,8 +170,9 @@ if (showProgress) { // If there are more benchmarks execute the next if (i + 1 < queue.length) { recursive(i + 1); - } else if (analyze) { - printAnalysis(results, scale, maxRegression); + } else { + if (csvFd !== null) closeSync(csvFd); + if (analyze) printAnalysis(results, scale, maxRegression); } }); })(kStartOfQueue); @@ -261,41 +279,41 @@ function printAnalysis(results, scale, maxRegression) { const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.length)); const rpad = (s, n) => ' '.repeat(Math.max(0, n - s.length)) + s; - console.log(`${pad('', maxNameLen)} confidence` + - ` improvement accuracy (*) (**) (***)`); + writeSync(process.stdout.fd, `${pad('', maxNameLen)} confidence` + + ` improvement accuracy (*) (**) (***)\n`); for (const row of rows) { const imp = `${row.improvement >= 0 ? '+' : ''}${row.improvement.toFixed(2)} %`; - console.log( - `${pad(row.name, maxNameLen)} ${pad(row.stars, 10)}` + + writeSync(process.stdout.fd, + `${pad(row.name, maxNameLen)} ${pad(row.stars, 10)}` + ` ${rpad(imp, 11)}` + ` ±${row.ci95.toFixed(2)}%` + ` ±${row.ci99.toFixed(2)}%` + ` ±${row.ci999.toFixed(2)}%` + - `${row.inconclusive ? ' (inconclusive)' : ''}`, + `${row.inconclusive ? ' (inconclusive)' : ''}\n`, ); } if (skipped > 0) { - console.log(''); - console.log( - `Note: ${skipped} configuration${skipped === 1 ? ' was' : 's were'}` + + writeSync(process.stdout.fd, '\n'); + writeSync(process.stdout.fd, + `Note: ${skipped} configuration${skipped === 1 ? ' was' : 's were'}` + ` skipped because Welch's t-test requires at least 2 samples per` + - ` binary. Use --runs 2 or higher.`, + ` binary. Use --runs 2 or higher.\n`, ); } // --- Bar chart visualization --- printChart(rows, maxNameLen); - console.log(''); - console.log( - `Rates were scaled by ${scale}x into HdrHistogram (3 significant figures).\n` + - `Use --scale to adjust precision if needed.\n`, + writeSync(process.stdout.fd, '\n'); + writeSync(process.stdout.fd, + `Rates were scaled by ${scale}x into HdrHistogram (3 significant figures).\n` + + `Use --scale to adjust precision if needed.\n\n`, ); const anyFamilyWise = rows.filter((r) => r.pAdjusted < 0.05).length; - console.log( - `Be aware that when doing many comparisons the risk of a false-positive\n` + + writeSync(process.stdout.fd, + `Be aware that when doing many comparisons the risk of a false-positive\n` + `result increases. In this case, there are ${rows.length} comparisons, ` + `you can thus\nexpect the following amount of false-positive results:\n` + ` ${(rows.length * 0.05).toFixed(2)} false positives, when considering ` + @@ -307,19 +325,19 @@ function printAnalysis(results, scale, maxRegression) { `\nThe stars above are per-benchmark and uncorrected. Adjusting for the ` + `size of\nthis comparison set (Holm-Bonferroni), ${anyFamilyWise} ` + `comparison${anyFamilyWise === 1 ? '' : 's'} remain${anyFamilyWise === 1 ? 's' : ''} ` + - `significant at 5%.\n--max-regression uses the corrected values.`, + `significant at 5%.\n--max-regression uses the corrected values.\n`, ); // Gate: exit with error if any regression is shown to exceed the limit. if (maxRegression > 0) { if (underpowered > 0) { - console.log(''); - console.log( - `Note: ${underpowered} of ${rows.length} comparison` + + writeSync(process.stdout.fd, '\n'); + writeSync(process.stdout.fd, + `Note: ${underpowered} of ${rows.length} comparison` + `${rows.length === 1 ? '' : 's'} could not resolve an effect as ` + `small as ${maxRegression}%, and are marked (inconclusive). They are ` + `not\nevidence of no regression -- the samples are too noisy to tell. ` + - `Raise --runs,\nor pin cores with --set CPUSET, to narrow them.`, + `Raise --runs,\nor pin cores with --set CPUSET, to narrow them.\n`, ); } @@ -340,18 +358,18 @@ function printAnalysis(results, scale, maxRegression) { ); if (failures.length > 0) { - console.log(''); - console.log( - `FAIL: ${failures.length} benchmark${failures.length === 1 ? '' : 's'}` + + writeSync(process.stdout.fd, '\n'); + writeSync(process.stdout.fd, + `FAIL: ${failures.length} benchmark${failures.length === 1 ? '' : 's'}` + ` regressed by more than ${maxRegression}%` + ` (interval excludes the threshold,\n` + - `family-wise corrected across ${rows.length} comparisons):`, + `family-wise corrected across ${rows.length} comparisons):\n`, ); for (const f of failures) { - console.log( - ` ${f.name} ${f.improvement.toFixed(2)}% ` + + writeSync(process.stdout.fd, + ` ${f.name} ${f.improvement.toFixed(2)}% ` + `(95% CI up to ${(f.improvement + f.ci95).toFixed(2)}%, ` + - `adjusted p=${f.pAdjusted.toExponential(2)})`, + `adjusted p=${f.pAdjusted.toExponential(2)})\n`, ); } process.exitCode = 1; @@ -388,8 +406,8 @@ function printChart(rows, maxNameLen) { axisCenter + ' '.repeat(Math.max(0, halfWidth - Math.ceil(axisCenter.length / 2) - axisRight.length)) + axisRight; - console.log(''); - console.log(leftLabel); + writeSync(process.stdout.fd, '\n'); + writeSync(process.stdout.fd, `${leftLabel}\n`); for (const row of rows) { const imp = row.improvement; @@ -421,6 +439,6 @@ function printChart(rows, maxNameLen) { const label = `${row.improvement >= 0 ? '+' : ''}${row.improvement.toFixed(2)}%`; const sig = row.stars.trim(); - console.log(`${pad(row.name, maxNameLen)} ${chars.join('')} ${label} ${sig}`); + writeSync(process.stdout.fd, `${pad(row.name, maxNameLen)} ${chars.join('')} ${label} ${sig}\n`); } } diff --git a/benchmark/crypto/create-keyobject.js b/benchmark/crypto/create-keyobject.js index 7cd6db2d567..df95ad39323 100644 --- a/benchmark/crypto/create-keyobject.js +++ b/benchmark/crypto/create-keyobject.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { hasOpenSSL, isBoringSSL } = require('../../test/common/crypto.js'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); @@ -26,7 +26,7 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); -} else if (process.features.openssl_is_boringssl) { +} else if (isBoringSSL) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } diff --git a/benchmark/crypto/ec-jwk-export.js b/benchmark/crypto/ec-jwk-export.js new file mode 100644 index 00000000000..0539fc6468a --- /dev/null +++ b/benchmark/crypto/ec-jwk-export.js @@ -0,0 +1,19 @@ +'use strict'; + +const common = require('../common.js'); +const { generateKeyPairSync } = require('crypto'); + +const bench = common.createBenchmark(main, { + namedCurve: ['prime256v1', 'secp384r1', 'secp521r1', 'secp256k1'], + type: ['public', 'private'], + n: [10000], +}); + +function main({ namedCurve, type, n }) { + const key = generateKeyPairSync('ec', { namedCurve })[`${type}Key`]; + const options = { format: 'jwk' }; + bench.start(); + for (let index = 0; index < n; index++) + key.export(options); + bench.end(n); +} diff --git a/benchmark/crypto/ec-key-details.js b/benchmark/crypto/ec-key-details.js new file mode 100644 index 00000000000..e7266e9ad19 --- /dev/null +++ b/benchmark/crypto/ec-key-details.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common.js'); +const { KeyObject } = require('crypto'); + +const bench = common.createBenchmark(main, { + namedCurve: ['P-256', 'P-384', 'P-521'], + type: ['public', 'private'], + n: [10000], +}); + +async function main({ namedCurve, type, n }) { + const pair = await crypto.subtle.generateKey({ + name: 'ECDSA', namedCurve, + }, true, ['sign', 'verify']); + const cryptoKey = pair[`${type}Key`]; + bench.start(); + for (let index = 0; index < n; index++) { + if (!KeyObject.from(cryptoKey).asymmetricKeyDetails.namedCurve) + throw new Error('Missing named curve'); + } + bench.end(n); +} diff --git a/benchmark/crypto/ec-raw-export.js b/benchmark/crypto/ec-raw-export.js new file mode 100644 index 00000000000..15c1d9333bb --- /dev/null +++ b/benchmark/crypto/ec-raw-export.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common.js'); +const { generateKeyPairSync } = require('crypto'); + +const bench = common.createBenchmark(main, { + namedCurve: ['prime256v1', 'secp384r1', 'secp521r1'], + format: ['raw-private', 'raw-public'], + type: ['uncompressed', 'compressed'], + n: [10000], +}, { + combinationFilter: ({ format, type }) => format === 'raw-public' || type === 'uncompressed', +}); + +function main({ namedCurve, format, type, n }) { + const pair = generateKeyPairSync('ec', { namedCurve }); + const key = format === 'raw-private' ? pair.privateKey : pair.publicKey; + const options = format === 'raw-public' ? { format, type } : { format }; + bench.start(); + for (let index = 0; index < n; index++) + key.export(options); + bench.end(n); +} diff --git a/benchmark/crypto/kem.js b/benchmark/crypto/kem.js index a544fc2124a..34374dfc849 100644 --- a/benchmark/crypto/kem.js +++ b/benchmark/crypto/kem.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { hasOpenSSL, isBoringSSL } = require('../../test/common/crypto.js'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); @@ -24,7 +24,7 @@ if (hasOpenSSL(3, 5)) { keyFixtures['ml-kem-512'] = readKeyPair('ml_kem_512_public', 'ml_kem_512_private'); keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private'); keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private'); -} else if (process.features.openssl_is_boringssl) { +} else if (isBoringSSL) { keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private_seed_only'); keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private_seed_only'); } @@ -45,7 +45,8 @@ if (Object.keys(keyFixtures).length === 0) { } const bench = common.createBenchmark(main, { - keyType: Object.keys(keyFixtures), + // Keep one size per family by default; other fixtures remain available via keyType. + keyType: ['rsa', 'p-256', 'x25519', 'ml-kem-768'].filter((type) => keyFixtures[type]), mode: ['sync', 'async', 'async-parallel'], keyFormat: ['keyObject', 'keyObject.unique', 'pem', 'der', 'jwk', 'raw-public', 'raw-private', 'raw-seed'], @@ -57,6 +58,9 @@ const bench = common.createBenchmark(main, { // assess whether mutexes over the key material impact the operation if (p.keyFormat === 'keyObject.unique') return p.mode === 'async-parallel'; + // Compare execution modes with pre-imported keys; measure parsing synchronously. + if (p.mode !== 'sync' && p.keyFormat !== 'keyObject') + return false; // raw-public is only supported for encapsulate, not rsa if (p.keyFormat === 'raw-public') return p.keyType !== 'rsa' && p.op === 'encapsulate'; @@ -127,7 +131,8 @@ function main({ n, mode, keyFormat, keyType, op }) { keyFixtures[keyType].publicKey : keyFixtures[keyType].privateKey; const createKeyFn = isEncapsulate ? crypto.createPublicKey : crypto.createPrivateKey; - const pems = [...Buffer.alloc(n)].map(() => pemSource); + const count = keyFormat === 'keyObject.unique' ? n : 1; + const pems = Array(count).fill(pemSource); const keyObjects = pems.map(createKeyFn); // Warm up OpenSSL's provider operation cache for each key object diff --git a/benchmark/crypto/keyobject-serialization.js b/benchmark/crypto/keyobject-serialization.js new file mode 100644 index 00000000000..76cd6cb3387 --- /dev/null +++ b/benchmark/crypto/keyobject-serialization.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common.js'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../../test/common/crypto.js'); +const fixtures = require('../../test/common/fixtures.js'); +const { createPrivateKey, createPublicKey } = require('crypto'); + +const keys = { + 'rsa': 'rsa_private_2048', + 'rsa-pss': 'rsa_pss_private_2048', + 'p-256': 'ec_p256_private', + 'p-384': 'ec_p384_private', + 'p-521': 'ec_p521_private', + 'ed25519': 'ed25519_private', + 'x25519': 'x25519_private', +}; +if (!isBoringSSL) { + keys.ed448 = 'ed448_private'; + keys.x448 = 'x448_private'; + if (!hasFIPS()) keys['rsa-multiprime'] = 'rsa_private_2048_3_primes'; +} +if (hasOpenSSL(3, 5) || isBoringSSL) { + keys['ml-dsa-44'] = 'ml_dsa_44_private_seed_only'; + keys['ml-dsa-87'] = 'ml_dsa_87_private_seed_only'; + keys['ml-kem-768'] = 'ml_kem_768_private_seed_only'; + keys['ml-kem-1024'] = 'ml_kem_1024_private_seed_only'; +} +if (hasOpenSSL(3, 5)) { + keys['slh-dsa-sha2-128s'] = 'slh_dsa_sha2_128s_private'; + keys['slh-dsa-shake-256s'] = 'slh_dsa_shake_256s_private'; +} + +const bench = common.createBenchmark(main, { + // Keep one size per family by default; other fixtures remain available via keyType. + keyType: ['rsa', 'rsa-pss', 'p-256', 'ed25519', 'x25519', + 'ml-dsa-44', 'ml-kem-768', 'slh-dsa-sha2-128s'].filter((type) => keys[type]), + operation: ['import', 'export'], + // PEM can be selected with format=pem; DER covers ASN.1 encoding by default. + format: ['jwk', 'der', 'raw-public', 'raw-private', 'raw-seed'], + type: ['public', 'private'], + n: [1e4], +}, { + combinationFilter({ keyType, format, type }) { + if (format === 'jwk') return keyType !== 'rsa-pss'; + if (!format.startsWith('raw-')) return true; + if (keyType.startsWith('rsa')) return false; + if (format === 'raw-public') return type === 'public'; + if (format === 'raw-private') return type === 'private' && !keyType.startsWith('ml-'); + return type === 'private' && keyType.startsWith('ml-'); + }, +}); + +function main({ keyType, operation, format, type, n }) { + const privateKey = createPrivateKey(fixtures.readKey(`${keys[keyType]}.pem`)); + const key = type === 'private' ? privateKey : createPublicKey(privateKey); + const options = { format }; + if (format === 'pem' || format === 'der') { + options.type = type === 'private' ? 'pkcs8' : 'spki'; + } + let run; + if (operation === 'export') { + run = () => key.export(options); + } else { + const input = { ...options, key: key.export(options) }; + if (format.startsWith('raw-')) { + input.asymmetricKeyType = key.asymmetricKeyType; + if (input.asymmetricKeyType === 'ec') { + input.namedCurve = key.asymmetricKeyDetails.namedCurve; + } + } + const importKey = type === 'private' ? createPrivateKey : createPublicKey; + run = () => importKey(input); + } + // Resolve provider operations and warm the JS path before timing. + for (let i = 0; i < 100; i++) run(); + bench.start(); + for (let i = 0; i < n; i++) run(); + bench.end(n); +} diff --git a/benchmark/crypto/mac.js b/benchmark/crypto/mac.js index d1028fa414e..ea9d3a56e59 100644 --- a/benchmark/crypto/mac.js +++ b/benchmark/crypto/mac.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { hasOpenSSL, isBoringSSL } = require('../../test/common/crypto.js'); const assert = require('node:assert'); const { createHmac, @@ -10,7 +10,7 @@ const { } = require('node:crypto'); if (!hasOpenSSL(3) || - process.features.openssl_is_boringssl || + isBoringSSL || typeof createMac !== 'function' || typeof getMacs !== 'function') { console.log('Skipping: generic MAC API requires OpenSSL >= 3'); diff --git a/benchmark/crypto/oneshot-sign.js b/benchmark/crypto/oneshot-sign.js index 72e3726d9a5..d2a8bab446d 100644 --- a/benchmark/crypto/oneshot-sign.js +++ b/benchmark/crypto/oneshot-sign.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { hasOpenSSL, isBoringSSL } = require('../../test/common/crypto.js'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); @@ -19,15 +19,12 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private'); -} else if (process.features.openssl_is_boringssl) { +} else if (isBoringSSL) { keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); -let pems; -let keyObjects; - const bench = common.createBenchmark(main, { keyType: Object.keys(keyFixtures), mode: ['sync', 'async', 'async-parallel'], @@ -39,6 +36,9 @@ const bench = common.createBenchmark(main, { // assess whether mutexes over the key material impact the operation if (p.keyFormat === 'keyObject.unique') return p.mode === 'async-parallel'; + // Compare execution modes with pre-imported keys; measure parsing synchronously. + if (p.mode !== 'sync' && p.keyFormat !== 'keyObject') + return false; // raw-private is not supported for rsa and ml-dsa if (p.keyFormat === 'raw-private') return p.keyType !== 'rsa' && !p.keyType.startsWith('ml-'); @@ -97,8 +97,9 @@ function measureAsyncParallel(n, digest, privateKey, keys) { } function main({ n, mode, keyFormat, keyType }) { - pems ||= [...Buffer.alloc(n)].map(() => keyFixtures[keyType]); - keyObjects ||= pems.map(crypto.createPrivateKey); + const count = keyFormat === 'keyObject.unique' ? n : 1; + const pems = Array(count).fill(keyFixtures[keyType]); + const keyObjects = pems.map(crypto.createPrivateKey); // Warm up OpenSSL's provider operation cache for each key object for (const keyObject of keyObjects) { diff --git a/benchmark/crypto/oneshot-verify.js b/benchmark/crypto/oneshot-verify.js index 8b397b02dbf..eb65d912877 100644 --- a/benchmark/crypto/oneshot-verify.js +++ b/benchmark/crypto/oneshot-verify.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { hasOpenSSL, isBoringSSL } = require('../../test/common/crypto.js'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); @@ -26,15 +26,12 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); -} else if (process.features.openssl_is_boringssl) { +} else if (isBoringSSL) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); -let pems; -let keyObjects; - const bench = common.createBenchmark(main, { keyType: Object.keys(keyFixtures), mode: ['sync', 'async', 'async-parallel'], @@ -46,6 +43,9 @@ const bench = common.createBenchmark(main, { // assess whether mutexes over the key material impact the operation if (p.keyFormat === 'keyObject.unique') return p.mode === 'async-parallel'; + // Compare execution modes with pre-imported keys; measure parsing synchronously. + if (p.mode !== 'sync' && p.keyFormat !== 'keyObject') + return false; // raw-public is not supported by rsa if (p.keyFormat === 'raw-public') return p.keyType !== 'rsa'; @@ -104,8 +104,9 @@ function measureAsyncParallel(n, digest, signature, publicKey, keys) { } function main({ n, mode, keyFormat, keyType }) { - pems ||= [...Buffer.alloc(n)].map(() => keyFixtures[keyType].publicKey); - keyObjects ||= pems.map(crypto.createPublicKey); + const count = keyFormat === 'keyObject.unique' ? n : 1; + const pems = Array(count).fill(keyFixtures[keyType].publicKey); + const keyObjects = pems.map(crypto.createPublicKey); // Warm up OpenSSL's provider operation cache for each key object const warmupDigest = keyType === 'rsa' || keyType === 'ec' ? 'sha256' : null; diff --git a/benchmark/crypto/prepare-asymmetric-key.js b/benchmark/crypto/prepare-asymmetric-key.js new file mode 100644 index 00000000000..dd536c3f451 --- /dev/null +++ b/benchmark/crypto/prepare-asymmetric-key.js @@ -0,0 +1,178 @@ +'use strict'; + +const common = require('../common.js'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); +const { hasOpenSSL, isBoringSSL } = require('../../test/common/crypto.js'); + +const inputs = [ + 'public-keyobject', 'public-keyobject-wrapped', 'public-pem-buffer-wrapped', + 'private-keyobject', 'private-keyobject-wrapped', + 'private-pem-string', 'private-pem-buffer', 'private-pem-arraybuffer', + 'private-pem-string-wrapped', 'private-pem-buffer-wrapped', 'private-pem-arraybuffer-wrapped', + 'private-jwk', + 'public-der-pkcs1-bytes', 'private-der-pkcs1-bytes', 'private-pem-pkcs8-bytes', + 'private-der-pkcs8-bytes', 'private-der-pkcs8-base64', 'private-der-pkcs8-hex', + 'public-der-spki-bytes', 'private-der-sec1-bytes', + 'raw-public-ec-buffer', 'raw-public-ed25519-buffer', + 'raw-private-ec-buffer', 'raw-private-ec-arraybuffer', 'raw-private-ed25519-buffer', + 'pem-passphrase-absent', 'pem-passphrase-empty', 'pem-passphrase-string', + 'pem-passphrase-base64', 'pem-passphrase-buffer', 'pem-passphrase-arraybuffer', + 'file-passphrase-absent', 'file-passphrase-string', 'file-direct', + 'provider-passphrase-absent', 'provider-passphrase-absent-properties', + 'provider-passphrase-empty', 'provider-passphrase-string', 'provider-passphrase-base64', + 'provider-passphrase-buffer', 'provider-passphrase-arraybuffer', 'provider-direct', +]; + +if (hasOpenSSL(3, 5) || isBoringSSL) + inputs.push('raw-seed-ml-dsa-44-buffer'); + +const bench = common.createBenchmark(main, { + input: inputs, + context: ['consume-public', 'consume-private', 'create-public'], + n: [1e6], +}, { + flags: ['--expose-internals'], + combinationFilter({ input, context }) { + if (input === 'private-keyobject' || input === 'private-keyobject-wrapped') + return true; + if (input.startsWith('public-') || input.startsWith('raw-public-')) + return context === 'consume-public'; + return context === 'consume-private'; + }, +}); + +function main({ input, context, n }) { + const { + prepareAsymmetricKey, + kConsumePublic, + kConsumePrivate, + kCreatePublic, + } = require('internal/crypto/keys'); + const ctx = { + 'consume-public': kConsumePublic, + 'consume-private': kConsumePrivate, + 'create-public': kCreatePublic, + }[context]; + const fixtureDir = path.resolve(__dirname, '../../test/fixtures/keys'); + const privateKeyPath = path.join(fixtureDir, 'rsa_private_2048.pem'); + const privateKey = crypto.createPrivateKey(fs.readFileSync(privateKeyPath)); + const publicKey = crypto.createPublicKey(privateKey); + const pem = privateKey.export({ format: 'pem', type: 'pkcs8' }); + const buffer = Buffer.from(pem); + const arraybuffer = Uint8Array.from(buffer).buffer; + const fileURL = pathToFileURL(privateKeyPath); + const providerURL = new URL('pkcs11:object=signing-key;type=private'); + const passphrase = Buffer.from('password'); + + let key; + switch (input) { + case 'public-keyobject': key = publicKey; break; + case 'public-keyobject-wrapped': key = { key: publicKey }; break; + case 'public-pem-buffer-wrapped': + key = { key: Buffer.from(publicKey.export({ format: 'pem', type: 'spki' })) }; + break; + case 'private-keyobject': key = privateKey; break; + case 'private-keyobject-wrapped': key = { key: privateKey }; break; + case 'private-pem-string': key = pem; break; + case 'private-pem-buffer': key = buffer; break; + case 'private-pem-arraybuffer': key = arraybuffer; break; + case 'private-pem-string-wrapped': key = { key: pem }; break; + case 'private-pem-buffer-wrapped': key = { key: buffer }; break; + case 'private-pem-arraybuffer-wrapped': key = { key: arraybuffer }; break; + case 'private-jwk': key = { key: privateKey.export({ format: 'jwk' }), format: 'jwk' }; break; + case 'public-der-pkcs1-bytes': key = { key: publicKey, format: 'der', type: 'pkcs1' }; break; + case 'private-der-pkcs1-bytes': key = { key: privateKey, format: 'der', type: 'pkcs1' }; break; + case 'private-pem-pkcs8-bytes': key = { key: privateKey, format: 'pem', type: 'pkcs8' }; break; + case 'private-der-pkcs8-bytes': key = { key: privateKey, format: 'der', type: 'pkcs8' }; break; + case 'private-der-pkcs8-base64': + key = { key: privateKey, format: 'der', type: 'pkcs8', encoding: 'base64' }; + break; + case 'private-der-pkcs8-hex': + key = { key: privateKey, format: 'der', type: 'pkcs8', encoding: 'hex' }; + break; + case 'public-der-spki-bytes': key = { key: publicKey, format: 'der', type: 'spki' }; break; + case 'private-der-sec1-bytes': + key = { + key: crypto.createPrivateKey(fs.readFileSync(path.join(fixtureDir, 'ec_p256_private.pem'))), + format: 'der', type: 'sec1', + }; + break; + case 'raw-public-ec-buffer': + case 'raw-public-ed25519-buffer': + case 'raw-private-ec-buffer': + case 'raw-private-ec-arraybuffer': + case 'raw-private-ed25519-buffer': { + const asymmetricKeyType = input.includes('-ec-') ? 'ec' : 'ed25519'; + const fixture = asymmetricKeyType === 'ec' ? 'ec_p256_private.pem' : 'ed25519_private.pem'; + let rawKey = crypto.createPrivateKey(fs.readFileSync(path.join(fixtureDir, fixture))); + const format = input.startsWith('raw-public-') ? 'raw-public' : 'raw-private'; + if (format === 'raw-public') rawKey = crypto.createPublicKey(rawKey); + const bytes = rawKey.export({ format }); + key = { + key: input.endsWith('-arraybuffer') ? Uint8Array.from(bytes).buffer : bytes, + format, + asymmetricKeyType, + namedCurve: asymmetricKeyType === 'ec' ? 'prime256v1' : undefined, + }; + break; + } + case 'raw-seed-ml-dsa-44-buffer': { + const seedKey = crypto.createPrivateKey( + fs.readFileSync(path.join(fixtureDir, 'ml_dsa_44_private_seed_only.pem'))); + key = { key: seedKey.export({ format: 'raw-seed' }), format: 'raw-seed', asymmetricKeyType: 'ml-dsa-44' }; + break; + } + case 'pem-passphrase-absent': key = { key: privateKey }; break; + case 'pem-passphrase-empty': key = { key: privateKey, passphrase: '' }; break; + case 'pem-passphrase-string': key = { key: privateKey, passphrase: 'password' }; break; + case 'pem-passphrase-base64': + key = { key: privateKey, passphrase: passphrase.toString('base64'), encoding: 'base64' }; + break; + case 'pem-passphrase-buffer': key = { key: privateKey, passphrase }; break; + case 'pem-passphrase-arraybuffer': + key = { key: privateKey, passphrase: Uint8Array.from(passphrase).buffer }; + break; + case 'file-passphrase-absent': key = { key: fileURL }; break; + case 'file-passphrase-string': key = { key: fileURL, passphrase: 'password' }; break; + case 'file-direct': key = fileURL; break; + case 'provider-passphrase-absent': key = { key: providerURL }; break; + case 'provider-passphrase-absent-properties': key = { key: providerURL, properties: 'provider=default' }; break; + case 'provider-passphrase-empty': key = { key: providerURL, passphrase: '' }; break; + case 'provider-passphrase-string': key = { key: providerURL, passphrase: 'password' }; break; + case 'provider-passphrase-base64': + key = { key: providerURL, passphrase: passphrase.toString('base64'), encoding: 'base64' }; + break; + case 'provider-passphrase-buffer': key = { key: providerURL, passphrase }; break; + case 'provider-passphrase-arraybuffer': + key = { key: providerURL, passphrase: Uint8Array.from(passphrase).buffer }; + break; + case 'provider-direct': key = providerURL; break; + default: throw new Error(`Unsupported input: ${input}`); + } + + if (input.startsWith('pem-passphrase-')) { + const bytes = key.passphrase === undefined ? undefined : + typeof key.passphrase === 'string' ? Buffer.from(key.passphrase, key.encoding) : + Buffer.from(key.passphrase); + key = { + key: Buffer.from(privateKey.export({ + format: 'pem', type: 'pkcs8', + ...(bytes === undefined ? {} : { cipher: 'aes-256-cbc', passphrase: bytes }), + })), + format: 'pem', type: 'pkcs8', passphrase: key.passphrase, encoding: key.encoding, + }; + } else if (key.key instanceof crypto.KeyObject && key.format !== undefined) { + const bytes = Buffer.from(key.key.export({ format: key.format, type: key.type })); + key.key = key.encoding === undefined ? bytes : bytes.toString(key.encoding); + } + + prepareAsymmetricKey(key, ctx); + bench.start(); + for (let index = 0; index < n; index++) { + prepareAsymmetricKey(key, ctx); + } + bench.end(n); +} diff --git a/benchmark/crypto/rsa-key-details.js b/benchmark/crypto/rsa-key-details.js new file mode 100644 index 00000000000..cb114226e30 --- /dev/null +++ b/benchmark/crypto/rsa-key-details.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common.js'); +const { KeyObject } = require('crypto'); + +const bench = common.createBenchmark(main, { + type: ['public', 'private'], + n: [10000], +}); + +async function main({ type, n }) { + const pair = await crypto.subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, true, ['sign', 'verify']); + const cryptoKey = pair[`${type}Key`]; + // Use a fresh KeyObject so each iteration retrieves uncached key details. + bench.start(); + for (let index = 0; index < n; index++) { + if (KeyObject.from(cryptoKey).asymmetricKeyDetails.modulusLength !== 2048) + throw new Error('Unexpected modulus length'); + } + bench.end(n); +} diff --git a/benchmark/crypto/rsa-pss-key-details.js b/benchmark/crypto/rsa-pss-key-details.js new file mode 100644 index 00000000000..18cc8f92f45 --- /dev/null +++ b/benchmark/crypto/rsa-pss-key-details.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common.js'); +const { isBoringSSL } = require('../../test/common/crypto.js'); +const { createPublicKey, generateKeyPairSync } = require('crypto'); + +if (isBoringSSL) { + console.log('Skipping: RSA-PSS key generation is not supported by BoringSSL'); + process.exit(0); +} + +const restrictions = { + absent: {}, + defaults: { hashAlgorithm: 'sha1', mgf1HashAlgorithm: 'sha1', saltLength: 20 }, + sha256: { hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', saltLength: 32 }, +}; + +const bench = common.createBenchmark(main, { + restrictions: Object.keys(restrictions), + n: [5000], +}); + +function main({ restrictions: name, n }) { + const { privateKey } = generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + ...restrictions[name], + }); + // Use a fresh KeyObject without decoding DER or reusing cached key details. + bench.start(); + for (let index = 0; index < n; index++) { + if (createPublicKey(privateKey).asymmetricKeyDetails.modulusLength !== 2048) + throw new Error('Unexpected modulus length'); + } + bench.end(n); +} diff --git a/benchmark/perf_hooks/histogram-qrde.js b/benchmark/perf_hooks/histogram-qrde.js new file mode 100644 index 00000000000..b23abd48531 --- /dev/null +++ b/benchmark/perf_hooks/histogram-qrde.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common.js'); +const { createHistogram } = require('perf_hooks'); + +const bench = common.createBenchmark(main, { + n: [5], + bins: [100, 1000], + samples: [1e6], + unique: [100, 1000, 10000], + dequantize: ['none', 'hdr', 'all'], +}, { + test: { + n: 1, + bins: 10, + samples: 100, + unique: 10, + }, +}); + +async function main({ n, bins, samples, unique, dequantize }) { + const histogram = createHistogram(); + const maximum = 1e12; + + for (let i = 0; i < samples; i++) { + const index = i % unique; + const rank = unique === 1 ? 0 : index / (unique - 1); + histogram.record(Math.max(1, Math.round(maximum ** rank))); + } + + await histogram.qrde({ bins, dequantize }); + bench.start(); + for (let i = 0; i < n; i++) { + await histogram.qrde({ bins, dequantize }); + } + bench.end(n); +} diff --git a/benchmark/perf_hooks/histogram-sliding-window-record.js b/benchmark/perf_hooks/histogram-sliding-window-record.js new file mode 100644 index 00000000000..192a1f0cc5f --- /dev/null +++ b/benchmark/perf_hooks/histogram-sliding-window-record.js @@ -0,0 +1,24 @@ +'use strict'; + +const assert = require('assert'); +const common = require('../common.js'); +const { createSlidingWindowHistogram } = require('perf_hooks'); + +const bench = common.createBenchmark(main, { + n: [1e6], + mode: ['count', 'time'], + chunks: [6], +}); + +function main({ n, mode, chunks }) { + const options = mode === 'count' ? + { chunks, recordsPerChunk: 1000 } : + { chunks, chunkDuration: 1 }; + const histogram = createSlidingWindowHistogram(options); + + bench.start(); + for (let i = 0; i < n; i++) histogram.record((i % 1000) + 1); + bench.end(n); + + assert.ok(histogram.snapshot().count > 0); +} diff --git a/benchmark/perf_hooks/histogram-sliding-window-snapshot.js b/benchmark/perf_hooks/histogram-sliding-window-snapshot.js new file mode 100644 index 00000000000..9bdc907054a --- /dev/null +++ b/benchmark/perf_hooks/histogram-sliding-window-snapshot.js @@ -0,0 +1,29 @@ +'use strict'; + +const assert = require('assert'); +const common = require('../common.js'); +const { createSlidingWindowHistogram } = require('perf_hooks'); + +const bench = common.createBenchmark(main, { + n: [100], + chunks: [2, 8], + recordsPerChunk: [1000], +}); + +let snapshot; + +function main({ n, chunks, recordsPerChunk }) { + const histogram = createSlidingWindowHistogram({ + chunks, + recordsPerChunk, + }); + for (let i = 0; i < chunks * recordsPerChunk; i++) { + histogram.record((i % 1000) + 1); + } + + bench.start(); + for (let i = 0; i < n; i++) snapshot = histogram.snapshot(); + bench.end(n); + + assert.strictEqual(snapshot.count, chunks * recordsPerChunk); +} diff --git a/benchmark/webstreams/lifecycle.js b/benchmark/webstreams/lifecycle.js new file mode 100644 index 00000000000..421538e4bfd --- /dev/null +++ b/benchmark/webstreams/lifecycle.js @@ -0,0 +1,76 @@ +'use strict'; +const common = require('../common.js'); +const assert = require('assert'); +const { + ReadableStream, + WritableStream, + TransformStream, +} = require('node:stream/web'); + +const bench = common.createBenchmark(main, { + n: [5e4], + kind: ['readable', 'pipe-to', 'pipe-through'], +}); + +const chunk = Buffer.alloc(1024); + +function makeSource() { + let i = 0; + return { + pull(controller) { + if (i++ < 4) + controller.enqueue(chunk); + else + controller.close(); + }, + }; +} + +async function readable(n) { + let chunks = 0; + bench.start(); + for (let i = 0; i < n; i++) { + const reader = new ReadableStream(makeSource()).getReader(); + while (!(await reader.read()).done) chunks++; + } + bench.end(n); + assert.strictEqual(chunks, n * 4); +} + +async function pipeTo(n) { + let chunks = 0; + bench.start(); + for (let i = 0; i < n; i++) { + await new ReadableStream(makeSource()) + .pipeTo(new WritableStream({ write() { chunks++; } })); + } + bench.end(n); + assert.strictEqual(chunks, n * 4); +} + +async function pipeThrough(n) { + let chunks = 0; + bench.start(); + for (let i = 0; i < n; i++) { + const reader = new ReadableStream(makeSource()) + .pipeThrough(new TransformStream()) + .getReader(); + while (!(await reader.read()).done) chunks++; + } + bench.end(n); + assert.strictEqual(chunks, n * 4); +} + +function main({ n, kind }) { + switch (kind) { + case 'readable': + readable(n); + break; + case 'pipe-to': + pipeTo(n); + break; + case 'pipe-through': + pipeThrough(n); + break; + } +} diff --git a/deps/googletest/include/gtest/gtest-death-test.h b/deps/googletest/include/gtest/gtest-death-test.h index afd7b3a4685..337313ea209 100644 --- a/deps/googletest/include/gtest/gtest-death-test.h +++ b/deps/googletest/include/gtest/gtest-death-test.h @@ -105,54 +105,10 @@ GTEST_API_ bool InDeathTestChild(); // // On the regular expressions used in death tests: // -// On POSIX-compliant systems (*nix), we use the library, -// which uses the POSIX extended regex syntax. -// -// On other platforms (e.g. Windows or Mac), we only support a simple regex -// syntax implemented as part of Google Test. This limited -// implementation should be enough most of the time when writing -// death tests; though it lacks many features you can find in PCRE -// or POSIX extended regex syntax. For example, we don't support -// union ("x|y"), grouping ("(xy)"), brackets ("[xy]"), and -// repetition count ("x{5,7}"), among others. -// -// Below is the syntax that we do support. We chose it to be a -// subset of both PCRE and POSIX extended regex, so it's easy to -// learn wherever you come from. In the following: 'A' denotes a -// literal character, period (.), or a single \\ escape sequence; -// 'x' and 'y' denote regular expressions; 'm' and 'n' are for -// natural numbers. -// -// c matches any literal character c -// \\d matches any decimal digit -// \\D matches any character that's not a decimal digit -// \\f matches \f -// \\n matches \n -// \\r matches \r -// \\s matches any ASCII whitespace, including \n -// \\S matches any character that's not a whitespace -// \\t matches \t -// \\v matches \v -// \\w matches any letter, _, or decimal digit -// \\W matches any character that \\w doesn't match -// \\c matches any literal character c, which must be a punctuation -// . matches any single character except \n -// A? matches 0 or 1 occurrences of A -// A* matches 0 or many occurrences of A -// A+ matches 1 or many occurrences of A -// ^ matches the beginning of a string (not that of each line) -// $ matches the end of a string (not that of each line) -// xy matches x followed by y -// -// If you accidentally use PCRE or POSIX extended regex features -// not implemented by us, you will get a run-time failure. In that -// case, please try to rewrite your regular expression within the -// above syntax. -// -// This implementation is *not* meant to be as highly tuned or robust -// as a compiled regex library, but should perform well enough for a -// death test, which already incurs significant overhead by launching -// a child process. +// Depending on the platform, this may use RE2, the POSIX library, +// the C++11 standard library's engine with ECMAScript syntax, or +// another similar engine. Regular expressions should be simple and portable +// enough to work across the engines of interest. // // Known caveats: // diff --git a/deps/googletest/include/gtest/internal/gtest-port.h b/deps/googletest/include/gtest/internal/gtest-port.h index 154be3c1602..3b2947b852e 100644 --- a/deps/googletest/include/gtest/internal/gtest-port.h +++ b/deps/googletest/include/gtest/internal/gtest-port.h @@ -176,7 +176,7 @@ // GTEST_USES_POSIX_RE - enhanced POSIX regex is used. Do not confuse with // GTEST_HAS_POSIX_RE (see above) which users can // define themselves. -// GTEST_USES_SIMPLE_RE - our own simple regex is used; +// GTEST_USES_STD_RE - std::regex from the C++ standard library is used; // the above RE\b(s) are mutually exclusive. // GTEST_HAS_ABSL - Google Test is compiled with Abseil. @@ -438,8 +438,9 @@ typedef struct _RTL_CRITICAL_SECTION GTEST_CRITICAL_SECTION; #include // NOLINT #define GTEST_USES_POSIX_RE 1 #else -// Use our own simple regex implementation. -#define GTEST_USES_SIMPLE_RE 1 +// Use std::regex from the C++ standard library. +#include // NOLINT +#define GTEST_USES_STD_RE 1 #endif #ifndef GTEST_HAS_EXCEPTIONS @@ -992,12 +993,11 @@ class GTEST_API_ [[nodiscard]] RE { RE2 regex_; }; -#elif defined(GTEST_USES_POSIX_RE) || defined(GTEST_USES_SIMPLE_RE) +#elif defined(GTEST_USES_POSIX_RE) || defined(GTEST_USES_STD_RE) GTEST_DISABLE_MSC_WARNINGS_PUSH_(4251 \ /* class A needs to have dll-interface to be used by clients of class B */) -// A simple C++ wrapper for . It uses the POSIX Extended -// Regular Expression syntax. +// A simple C++ wrapper for or . class GTEST_API_ [[nodiscard]] RE { public: // A copy constructor is required by the Standard to initialize object @@ -1037,9 +1037,9 @@ class GTEST_API_ [[nodiscard]] RE { regex_t full_regex_; // For FullMatch(). regex_t partial_regex_; // For PartialMatch(). -#else // GTEST_USES_SIMPLE_RE +#else // GTEST_USES_STD_RE - std::string full_pattern_; // For FullMatch(); + std::regex regex_; #endif }; @@ -1755,14 +1755,16 @@ class [[nodiscard]] MutexBase { #define GTEST_DECLARE_STATIC_MUTEX_(mutex) \ extern ::testing::internal::MutexBase mutex +#if defined(PTHREAD_NULL) +#define GTEST_INTERNAL_PTHREAD_NULL PTHREAD_NULL +#else +#define GTEST_INTERNAL_PTHREAD_NULL (pthread_t{}) +#endif + // Defines and statically (i.e. at link time) initializes a static mutex. -// The initialization list here does not explicitly initialize each field, -// instead relying on default initialization for the unspecified fields. In -// particular, the owner_ field (a pthread_t) is not explicitly initialized. -// This allows initialization to work whether pthread_t is a scalar or struct. -// The flag -Wmissing-field-initializers must not be specified for this to work. -#define GTEST_DEFINE_STATIC_MUTEX_(mutex) \ - ::testing::internal::MutexBase mutex = {PTHREAD_MUTEX_INITIALIZER, false, 0} +#define GTEST_DEFINE_STATIC_MUTEX_(mutex) \ + ::testing::internal::MutexBase mutex = {PTHREAD_MUTEX_INITIALIZER, false, \ + GTEST_INTERNAL_PTHREAD_NULL} // The Mutex class can only be used for mutexes created at runtime. It // shares its API with MutexBase otherwise. diff --git a/deps/googletest/src/gtest-internal-inl.h b/deps/googletest/src/gtest-internal-inl.h index 4bebca1bc65..5a6332a755a 100644 --- a/deps/googletest/src/gtest-internal-inl.h +++ b/deps/googletest/src/gtest-internal-inl.h @@ -980,26 +980,7 @@ inline UnitTestImpl* GetUnitTestImpl() { return UnitTest::GetInstance()->impl(); } -#ifdef GTEST_USES_SIMPLE_RE - -// Internal helper functions for implementing the simple regular -// expression matcher. -GTEST_API_ bool IsInSet(char ch, const char* str); -GTEST_API_ bool IsAsciiDigit(char ch); -GTEST_API_ bool IsAsciiPunct(char ch); -GTEST_API_ bool IsRepeat(char ch); -GTEST_API_ bool IsAsciiWhiteSpace(char ch); -GTEST_API_ bool IsAsciiWordChar(char ch); -GTEST_API_ bool IsValidEscape(char ch); -GTEST_API_ bool AtomMatchesChar(bool escaped, char pattern, char ch); -GTEST_API_ bool ValidateRegex(const char* regex); -GTEST_API_ bool MatchRegexAtHead(const char* regex, const char* str); -GTEST_API_ bool MatchRepetitionAndRegexAtHead(bool escaped, char ch, - char repeat, const char* regex, - const char* str); -GTEST_API_ bool MatchRegexAnywhere(const char* regex, const char* str); - -#endif // GTEST_USES_SIMPLE_RE + // Parses the command line for Google Test flags, without initializing // other parts of Google Test. diff --git a/deps/googletest/src/gtest-port.cc b/deps/googletest/src/gtest-port.cc index be5b16e76d3..68f77f71247 100644 --- a/deps/googletest/src/gtest-port.cc +++ b/deps/googletest/src/gtest-port.cc @@ -766,249 +766,46 @@ void RE::Init(const char* regex) { delete[] full_pattern; } -#elif defined(GTEST_USES_SIMPLE_RE) - -// Returns true if and only if ch appears anywhere in str (excluding the -// terminating '\0' character). -bool IsInSet(char ch, const char* str) { - return ch != '\0' && strchr(str, ch) != nullptr; -} - -// Returns true if and only if ch belongs to the given classification. -// Unlike similar functions in , these aren't affected by the -// current locale. -bool IsAsciiDigit(char ch) { return '0' <= ch && ch <= '9'; } -bool IsAsciiPunct(char ch) { - return IsInSet(ch, "^-!\"#$%&'()*+,./:;<=>?@[\\]_`{|}~"); -} -bool IsRepeat(char ch) { return IsInSet(ch, "?*+"); } -bool IsAsciiWhiteSpace(char ch) { return IsInSet(ch, " \f\n\r\t\v"); } -bool IsAsciiWordChar(char ch) { - return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || - ('0' <= ch && ch <= '9') || ch == '_'; -} - -// Returns true if and only if "\\c" is a supported escape sequence. -bool IsValidEscape(char c) { - return (IsAsciiPunct(c) || IsInSet(c, "dDfnrsStvwW")); -} - -// Returns true if and only if the given atom (specified by escaped and -// pattern) matches ch. The result is undefined if the atom is invalid. -bool AtomMatchesChar(bool escaped, char pattern_char, char ch) { - if (escaped) { // "\\p" where p is pattern_char. - switch (pattern_char) { - case 'd': - return IsAsciiDigit(ch); - case 'D': - return !IsAsciiDigit(ch); - case 'f': - return ch == '\f'; - case 'n': - return ch == '\n'; - case 'r': - return ch == '\r'; - case 's': - return IsAsciiWhiteSpace(ch); - case 'S': - return !IsAsciiWhiteSpace(ch); - case 't': - return ch == '\t'; - case 'v': - return ch == '\v'; - case 'w': - return IsAsciiWordChar(ch); - case 'W': - return !IsAsciiWordChar(ch); - } - return IsAsciiPunct(pattern_char) && pattern_char == ch; - } - - return (pattern_char == '.' && ch != '\n') || pattern_char == ch; -} - -// Helper function used by ValidateRegex() to format error messages. -static std::string FormatRegexSyntaxError(const char* regex, int index) { - return (Message() << "Syntax error at index " << index - << " in simple regular expression \"" << regex << "\": ") - .GetString(); -} - -// Generates non-fatal failures and returns false if regex is invalid; -// otherwise returns true. -bool ValidateRegex(const char* regex) { - if (regex == nullptr) { - ADD_FAILURE() << "NULL is not a valid simple regular expression."; - return false; - } - - bool is_valid = true; - - // True if and only if ?, *, or + can follow the previous atom. - bool prev_repeatable = false; - for (int i = 0; regex[i]; i++) { - if (regex[i] == '\\') { // An escape sequence - i++; - if (regex[i] == '\0') { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i - 1) - << "'\\' cannot appear at the end."; - return false; - } - - if (!IsValidEscape(regex[i])) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i - 1) - << "invalid escape sequence \"\\" << regex[i] << "\"."; - is_valid = false; - } - prev_repeatable = true; - } else { // Not an escape sequence. - const char ch = regex[i]; - - if (ch == '^' && i > 0) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) - << "'^' can only appear at the beginning."; - is_valid = false; - } else if (ch == '$' && regex[i + 1] != '\0') { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) - << "'$' can only appear at the end."; - is_valid = false; - } else if (IsInSet(ch, "()[]{}|")) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) << "'" << ch - << "' is unsupported."; - is_valid = false; - } else if (IsRepeat(ch) && !prev_repeatable) { - ADD_FAILURE() << FormatRegexSyntaxError(regex, i) << "'" << ch - << "' can only follow a repeatable token."; - is_valid = false; - } - - prev_repeatable = !IsInSet(ch, "^$?*+"); - } - } - - return is_valid; -} - -// Matches a repeated regex atom followed by a valid simple regular -// expression. The regex atom is defined as c if escaped is false, -// or \c otherwise. repeat is the repetition meta character (?, *, -// or +). The behavior is undefined if str contains too many -// characters to be indexable by size_t, in which case the test will -// probably time out anyway. We are fine with this limitation as -// std::string has it too. -bool MatchRepetitionAndRegexAtHead(bool escaped, char c, char repeat, - const char* regex, const char* str) { - const size_t min_count = (repeat == '+') ? 1 : 0; - const size_t max_count = (repeat == '?') ? 1 : static_cast(-1) - 1; - // We cannot call numeric_limits::max() as it conflicts with the - // max() macro on Windows. - - for (size_t i = 0; i <= max_count; ++i) { - // We know that the atom matches each of the first i characters in str. - if (i >= min_count && MatchRegexAtHead(regex, str + i)) { - // We have enough matches at the head, and the tail matches too. - // Since we only care about *whether* the pattern matches str - // (as opposed to *how* it matches), there is no need to find a - // greedy match. - return true; - } - if (str[i] == '\0' || !AtomMatchesChar(escaped, c, str[i])) return false; - } - return false; -} - -// Returns true if and only if regex matches a prefix of str. regex must -// be a valid simple regular expression and not start with "^", or the -// result is undefined. -bool MatchRegexAtHead(const char* regex, const char* str) { - if (*regex == '\0') // An empty regex matches a prefix of anything. - return true; - - // "$" only matches the end of a string. Note that regex being - // valid guarantees that there's nothing after "$" in it. - if (*regex == '$') return *str == '\0'; - - // Is the first thing in regex an escape sequence? - const bool escaped = *regex == '\\'; - if (escaped) ++regex; - if (IsRepeat(regex[1])) { - // MatchRepetitionAndRegexAtHead() calls MatchRegexAtHead(), so - // here's an indirect recursion. It terminates as the regex gets - // shorter in each recursion. - return MatchRepetitionAndRegexAtHead(escaped, regex[0], regex[1], regex + 2, - str); - } else { - // regex isn't empty, isn't "$", and doesn't start with a - // repetition. We match the first atom of regex with the first - // character of str and recurse. - return (*str != '\0') && AtomMatchesChar(escaped, *regex, *str) && - MatchRegexAtHead(regex + 1, str + 1); - } -} - -// Returns true if and only if regex matches any substring of str. regex must -// be a valid simple regular expression, or the result is undefined. -// -// The algorithm is recursive, but the recursion depth doesn't exceed -// the regex length, so we won't need to worry about running out of -// stack space normally. In rare cases the time complexity can be -// exponential with respect to the regex length + the string length, -// but usually it's must faster (often close to linear). -bool MatchRegexAnywhere(const char* regex, const char* str) { - if (regex == nullptr || str == nullptr) return false; - - if (*regex == '^') return MatchRegexAtHead(regex + 1, str); - - // A successful match can be anywhere in str. - do { - if (MatchRegexAtHead(regex, str)) return true; - } while (*str++ != '\0'); - return false; -} - -// Implements the RE class. +#elif defined(GTEST_USES_STD_RE) RE::~RE() = default; // Returns true if and only if regular expression re matches the entire str. bool RE::FullMatch(const char* str, const RE& re) { - return re.is_valid_ && MatchRegexAnywhere(re.full_pattern_.c_str(), str); + if (!re.is_valid_ || str == nullptr) return false; + return std::regex_match(str, re.regex_); } // Returns true if and only if regular expression re matches a substring of // str (including str itself). bool RE::PartialMatch(const char* str, const RE& re) { - return re.is_valid_ && MatchRegexAnywhere(re.pattern_.c_str(), str); + if (!re.is_valid_ || str == nullptr) return false; + return std::regex_search(str, re.regex_); } // Initializes an RE from its string representation. void RE::Init(const char* regex) { - full_pattern_.clear(); - pattern_.clear(); + pattern_ = regex == nullptr ? "" : regex; + is_valid_ = false; - if (regex != nullptr) { - pattern_ = regex; - } - - is_valid_ = ValidateRegex(regex); - if (!is_valid_) { - // No need to calculate the full pattern when the regex is invalid. + if (regex == nullptr) { + ADD_FAILURE() << "NULL is not a valid regular expression."; return; } - // Reserves enough bytes to hold the regular expression used for a - // full match: we need space to prepend a '^' and append a '$'. - full_pattern_.reserve(pattern_.size() + 2); - - if (pattern_.empty() || pattern_.front() != '^') { - full_pattern_.push_back('^'); // Makes sure full_pattern_ starts with '^'. +#if GTEST_HAS_EXCEPTIONS + try { + regex_ = std::regex(regex, std::regex_constants::ECMAScript); + } catch (const std::regex_error& e) { + ADD_FAILURE() << "Regular expression \"" << regex + << "\" is not a valid regular expression: " << e.what(); + return; } +#else + regex_ = std::regex(regex, std::regex_constants::ECMAScript); +#endif - full_pattern_.append(pattern_); - - if (pattern_.empty() || pattern_.back() != '$') { - full_pattern_.push_back('$'); // Makes sure full_pattern_ ends with '$'. - } + is_valid_ = true; } #endif // GTEST_USES_POSIX_RE diff --git a/deps/googletest/src/gtest.cc b/deps/googletest/src/gtest.cc index 47c60da2291..3772f18552d 100644 --- a/deps/googletest/src/gtest.cc +++ b/deps/googletest/src/gtest.cc @@ -4648,7 +4648,7 @@ std::string JsonUnitTestResultPrinter::EscapeJson(const std::string& str) { m << "\\r"; break; default: - if (ch < ' ') { + if (static_cast(ch) < ' ' || ch == '\x7F') { m << "\\u00" << String::FormatByte(static_cast(ch)); } else { m << ch; diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index b727bb06cdb..c2b2f8757cb 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -8,6 +8,9 @@ #include #include #include +#if NCRYPTO_USE_OPENSSL3_PROVIDER +#include +#endif #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK #include #include @@ -29,40 +32,6 @@ #include #endif #endif -#if OPENSSL_WITH_PQC -struct PQCMapping { - const char* name; - int nid; -}; - -constexpr static PQCMapping pqc_mappings[] = { - {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, - {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, - {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, - {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, - {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, - -#if OPENSSL_WITH_PQC_ML_KEM_512 - {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, -#endif -#if OPENSSL_WITH_PQC_SLH_DSA - {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, - {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, - {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, - {"SLH-DSA-SHA2-192s", EVP_PKEY_SLH_DSA_SHA2_192S}, - {"SLH-DSA-SHA2-256f", EVP_PKEY_SLH_DSA_SHA2_256F}, - {"SLH-DSA-SHA2-256s", EVP_PKEY_SLH_DSA_SHA2_256S}, - {"SLH-DSA-SHAKE-128f", EVP_PKEY_SLH_DSA_SHAKE_128F}, - {"SLH-DSA-SHAKE-128s", EVP_PKEY_SLH_DSA_SHAKE_128S}, - {"SLH-DSA-SHAKE-192f", EVP_PKEY_SLH_DSA_SHAKE_192F}, - {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, - {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, - {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, -#endif -}; - -#endif - // EVP_PKEY_CTX_set_dsa_paramgen_q_bits was added in OpenSSL 1.1.1e. #if OPENSSL_VERSION_NUMBER < 0x1010105fL #define EVP_PKEY_CTX_set_dsa_paramgen_q_bits(ctx, qbits) \ @@ -225,8 +194,10 @@ bool GetOptionalPKeyBnParam(const EVP_PKEY* pkey, return true; } -EVPKeyPointer NewPKeyFromData(int id, int selection, OSSL_PARAM* params) { - auto ctx = EVPKeyCtxPointer::NewFromID(id); +EVPKeyPointer NewPKeyFromData(const KeyAlgorithm& algorithm, + int selection, + OSSL_PARAM* params) { + auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(algorithm); if (!ctx || EVP_PKEY_fromdata_init(ctx.get()) != 1) return {}; EVP_PKEY* pkey = nullptr; @@ -266,7 +237,7 @@ EVPKeyPointer NewDhPKey(const BIGNUM* p, OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); if (!params) return {}; - return NewPKeyFromData(EVP_PKEY_DH, selection, params.get()); + return NewPKeyFromData(KeyAlgorithm::DH, selection, params.get()); } EVPKeyPointer NewDhPKey(const char* group_name, @@ -275,7 +246,7 @@ EVPKeyPointer NewDhPKey(const char* group_name, if (group_name == nullptr) return {}; if (pub == nullptr && priv == nullptr) { - EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new_from_name(nullptr, "DH", nullptr)); + auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::DH); OSSL_PARAM params[] = { OSSL_PARAM_construct_utf8_string( OSSL_PKEY_PARAM_GROUP_NAME, const_cast(group_name), 0), @@ -311,7 +282,7 @@ EVPKeyPointer NewDhPKey(const char* group_name, OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); if (!params) return {}; - return NewPKeyFromData(EVP_PKEY_DH, selection, params.get()); + return NewPKeyFromData(KeyAlgorithm::DH, selection, params.get()); } bool GetDhParams(const EVP_PKEY* pkey, @@ -1681,8 +1652,7 @@ bool X509View::enumUsages(UsageCallback callback) const { bool X509View::ifRsa(KeyCallback callback) const { if (cert_ == nullptr) return true; OSSL3_CONST EVP_PKEY* pkey = X509_get0_pubkey(cert_); - auto id = EVP_PKEY_id(pkey); - if (id == EVP_PKEY_RSA || id == EVP_PKEY_RSA2 || id == EVP_PKEY_RSA_PSS) { + if (EVPKeyPointer::isRsaVariant(pkey)) { #if NCRYPTO_USE_OPENSSL3_PROVIDER Rsa rsa(pkey); #else @@ -1698,8 +1668,7 @@ bool X509View::ifRsa(KeyCallback callback) const { bool X509View::ifEc(KeyCallback callback) const { if (cert_ == nullptr) return true; OSSL3_CONST EVP_PKEY* pkey = X509_get0_pubkey(cert_); - auto id = EVP_PKEY_id(pkey); - if (id == EVP_PKEY_EC) { + if (EVPKeyPointer::isA(pkey, KeyAlgorithm::EC)) { #if NCRYPTO_USE_OPENSSL3_PROVIDER Ec ec(pkey); #else @@ -1860,25 +1829,18 @@ int BIOPointer::Write(BIOPointer* bio, std::string_view message) { // DHPointer namespace { -bool EqualNoCase(const std::string_view a, const std::string_view b) { - if (a.size() != b.size()) return false; - return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) { - return std::tolower(a) == std::tolower(b); - }); -} - #if NCRYPTO_USE_OPENSSL3_PROVIDER const char* GetOpenSSLDhGroupName(const std::string_view name, DHPointer::FindGroupOption option) { if (option != DHPointer::FindGroupOption::NO_SMALL_PRIMES && - EqualNoCase(name, "modp5")) { + CaseInsensitiveNameEqual()(name, "modp5")) { return "modp_1536"; } - if (EqualNoCase(name, "modp14")) return "modp_2048"; - if (EqualNoCase(name, "modp15")) return "modp_3072"; - if (EqualNoCase(name, "modp16")) return "modp_4096"; - if (EqualNoCase(name, "modp17")) return "modp_6144"; - if (EqualNoCase(name, "modp18")) return "modp_8192"; + if (CaseInsensitiveNameEqual()(name, "modp14")) return "modp_2048"; + if (CaseInsensitiveNameEqual()(name, "modp15")) return "modp_3072"; + if (CaseInsensitiveNameEqual()(name, "modp16")) return "modp_4096"; + if (CaseInsensitiveNameEqual()(name, "modp17")) return "modp_6144"; + if (CaseInsensitiveNameEqual()(name, "modp18")) return "modp_8192"; return nullptr; } @@ -2112,7 +2074,7 @@ DH* DHPointer::release() { BignumPointer DHPointer::FindGroup(const std::string_view name, FindGroupOption option) { #define V(n, p) \ - if (EqualNoCase(name, n)) return BignumPointer(p(nullptr)); + if (CaseInsensitiveNameEqual()(name, n)) return BignumPointer(p(nullptr)); if (option != FindGroupOption::NO_SMALL_PRIMES) { #ifndef OPENSSL_IS_BORINGSSL // Boringssl does not support the 768 and 1024 small primes @@ -2180,7 +2142,7 @@ DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) { DHPointer DHPointer::New(size_t bits, unsigned int generator) { #if NCRYPTO_USE_OPENSSL3_PROVIDER - auto param_ctx = EVPKeyCtxPointer::NewFromID(EVP_PKEY_DH); + auto param_ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::DH); if (!param_ctx.initForParamgen() || !param_ctx.setDhParameters(bits, generator)) { return {}; @@ -2672,6 +2634,21 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, // ============================================================================ // KDF +#if NCRYPTO_USE_OPENSSL3_PROVIDER +KDF::KDF(EVP_KDF* kdf) : kdf_(kdf) {} + +KDF KDF::Fetch(const char* algorithm, OSSL_LIB_CTX* libctx) { + return KDF(EVP_KDF_fetch(libctx, algorithm, nullptr)); +} + +bool KDF::derive(const Buffer& out, + const OSSL_PARAM* params) const { + if (!kdf_) return false; + DeleteFnPtr ctx(EVP_KDF_CTX_new(kdf_.get())); + return ctx && EVP_KDF_derive(ctx.get(), out.data, out.len, params) == 1; +} +#endif + const EVP_MD* getDigestByName(const char* name) { // Historically, "dss1" and "DSS1" were DSA aliases for SHA-1 // exposed through the public API. @@ -2681,10 +2658,6 @@ const EVP_MD* getDigestByName(const char* name) { return EVP_get_digestbyname(name); } -const EVP_CIPHER* getCipherByName(const char* name) { - return EVP_get_cipherbyname(name); -} - bool checkHkdfLength(const Digest& md, size_t length) { // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as // the output of the hash function. 255 is a hard limit because HKDF appends @@ -2705,16 +2678,6 @@ DataPointer hkdf(const Digest& md, return {}; } - auto ctx = EVPKeyCtxPointer::NewFromID(EVP_PKEY_HKDF); - // OpenSSL < 3.0.0 accepted only a void* as the argument of - // EVP_PKEY_CTX_set_hkdf_md. - const EVP_MD* md_ptr = md; - if (!ctx || !EVP_PKEY_derive_init(ctx.get()) || - !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md_ptr) || - !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) { - return {}; - } - std::string_view actual_salt; static const char default_salt[EVP_MAX_MD_SIZE] = {0}; if (salt.len > 0) { @@ -2723,12 +2686,9 @@ DataPointer hkdf(const Digest& md, actual_salt = {default_salt, static_cast(md.size())}; } - // We do not use EVP_PKEY_HKDF_MODE_EXTRACT_AND_EXPAND because and instead - // implement the extraction step ourselves because EVP_PKEY_derive does not - // handle zero-length keys, which are required for Web Crypto. - // TODO(jasnell): Once OpenSSL 1.1.1 support is dropped completely, and once - // BoringSSL is confirmed to support it, wen can hopefully drop this and use - // EVP_KDF directly which does support zero length keys. + // Keep extraction as a one-shot HMAC. The legacy path requires it because + // EVP_PKEY_derive rejects the zero-length keys Web Crypto allows. Both + // backends expand a pseudorandom key of exactly one digest block. unsigned char pseudorandom_key[EVP_MAX_MD_SIZE]; unsigned pseudorandom_key_len = sizeof(pseudorandom_key); @@ -2741,26 +2701,112 @@ DataPointer hkdf(const Digest& md, &pseudorandom_key_len) == nullptr) { return {}; } - if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) || + + auto buf = DataPointer::Alloc(length); + if (!buf) return {}; + +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // Expand through EVP_KDF directly. The EVP_PKEY_HKDF interface reaches the + // same provider implementation, but only after allocating a second context + // and translating every parameter across the legacy bridge. + auto kdf = KDF::Fetch(OSSL_KDF_NAME_HKDF); + if (!kdf) return {}; + + const char* md_name = EVP_MD_get0_name(md); + if (md_name == nullptr) return {}; + + int mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY; + std::array params; + size_t n = 0; + params[n++] = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode); + params[n++] = OSSL_PARAM_construct_utf8_string( + OSSL_KDF_PARAM_DIGEST, const_cast(md_name), 0); + params[n++] = OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_KEY, pseudorandom_key, pseudorandom_key_len); + if (info.len > 0) { + params[n++] = OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_INFO, const_cast(info.data), info.len); + } + params[n++] = OSSL_PARAM_construct_end(); + + if (!kdf.derive({buf.get(), length}, params.data())) { + return {}; + } +#else + auto ctx = EVPKeyCtxPointer::NewFromName("HKDF"); + // OpenSSL < 3.0.0 accepted only a void* as the argument of + // EVP_PKEY_CTX_set_hkdf_md. + const EVP_MD* md_ptr = md; + if (!ctx || !EVP_PKEY_derive_init(ctx.get()) || + !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md_ptr) || + !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len) || + !EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) || !EVP_PKEY_CTX_set1_hkdf_key( ctx.get(), pseudorandom_key, pseudorandom_key_len)) { return {}; } - auto buf = DataPointer::Alloc(length); - if (!buf) return {}; - if (EVP_PKEY_derive( ctx.get(), static_cast(buf.get()), &length) <= 0) { return {}; } +#endif return buf; } +#if NCRYPTO_USE_OPENSSL3_PROVIDER +namespace { +bool ScryptDerive(const Buffer& pass, + const Buffer& salt, + uint64_t N, + uint64_t r, + uint64_t p, + uint64_t maxmem, + unsigned char* out, + size_t length) { + // EVP_PBE_scrypt limits these parameters to the provider's declared width. + if (r > UINT32_MAX || p > UINT32_MAX) { + ERR_raise(ERR_LIB_EVP, EVP_R_PARAMETER_TOO_LARGE); + return false; + } + + // Keep EVP_PBE_scrypt's 32 MiB default instead of the provider's default. + if (maxmem == 0) maxmem = 32 * 1024 * 1024; + + auto kdf = KDF::Fetch(OSSL_KDF_NAME_SCRYPT); + if (!kdf) return false; + + unsigned char empty_salt = 0; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_PASSWORD, + const_cast(pass.data == nullptr ? "" : pass.data), + pass.data == nullptr ? 0 : pass.len), + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SALT, + salt.data == nullptr ? &empty_salt + : const_cast(salt.data), + salt.data == nullptr ? 0 : salt.len), + OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_N, &N), + OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_R, &r), + OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_P, &p), + OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_MAXMEM, &maxmem), + OSSL_PARAM_END, + }; + return kdf.derive({out, length}, params); +} +} // namespace +#endif + bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // A null output validates the parameters without deriving a key. + return ScryptDerive({nullptr, 0}, {nullptr, 0}, N, r, p, maxmem, nullptr, 0); +#else return EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem, nullptr, 0) == 1; +#endif } DataPointer scrypt(const Buffer& pass, @@ -2775,6 +2821,10 @@ DataPointer scrypt(const Buffer& pass, } auto dp = DataPointer::Alloc(length); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (dp && ScryptDerive( + pass, salt, N, r, p, maxmem, dp.get(), length)) { +#else if (dp && EVP_PBE_scrypt(pass.data, pass.len, salt.data, @@ -2785,6 +2835,7 @@ DataPointer scrypt(const Buffer& pass, maxmem, reinterpret_cast(dp.get()), length)) { +#endif return dp; } @@ -2801,6 +2852,44 @@ DataPointer pbkdf2(const Digest& md, } auto dp = DataPointer::Alloc(length); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!dp) return {}; + auto kdf = KDF::Fetch(OSSL_KDF_NAME_PBKDF2); + if (!kdf) return {}; + + const char* md_name = EVP_MD_get0_name(md); + if (md_name == nullptr) return {}; + +#if OPENSSL_VERSION_MAJOR < 4 + // Match PKCS5_PBKDF2_HMAC: OpenSSL 3 disables the provider's lower bounds, + // while OpenSSL 4 leaves them at their provider defaults. + int pkcs5 = 1; +#endif + int iteration_count = static_cast(iterations); + unsigned char empty_salt = 0; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_PASSWORD, + const_cast(pass.data == nullptr ? "" : pass.data), + pass.data == nullptr ? 0 : pass.len), +#if OPENSSL_VERSION_MAJOR < 4 + OSSL_PARAM_construct_int(OSSL_KDF_PARAM_PKCS5, &pkcs5), +#endif + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SALT, + salt.data == nullptr && salt.len == 0 + ? &empty_salt + : const_cast(salt.data), + salt.len), + OSSL_PARAM_construct_int(OSSL_KDF_PARAM_ITER, &iteration_count), + OSSL_PARAM_construct_utf8_string( + OSSL_KDF_PARAM_DIGEST, const_cast(md_name), 0), + OSSL_PARAM_END, + }; + if (kdf.derive({dp.get(), length}, params)) { + return dp; + } +#else const EVP_MD* md_ptr = md; if (dp && PKCS5_PBKDF2_HMAC(pass.data, pass.len, @@ -2812,6 +2901,7 @@ DataPointer pbkdf2(const Digest& md, reinterpret_cast(dp.get()))) { return dp; } +#endif return {}; } @@ -2848,8 +2938,7 @@ DataPointer argon2(const Buffer& pass, // against the default context, otherwise Argon2 works in FIPS mode. DeleteFnPtr ctx; if (lanes > 1) { - if (!DeleteFnPtr{ - EVP_KDF_fetch(nullptr, algorithm.data(), nullptr)}) { + if (!KDF::Fetch(algorithm.data())) { return {}; } @@ -2863,18 +2952,11 @@ DataPointer argon2(const Buffer& pass, } } - auto kdf = DeleteFnPtr{ - EVP_KDF_fetch(ctx.get(), algorithm.data(), nullptr)}; + auto kdf = KDF::Fetch(algorithm.data(), ctx.get()); if (!kdf) { return {}; } - auto kctx = - DeleteFnPtr{EVP_KDF_CTX_new(kdf.get())}; - if (!kctx) { - return {}; - } - std::vector params; params.reserve(9); @@ -2906,10 +2988,7 @@ DataPointer argon2(const Buffer& pass, params.push_back(OSSL_PARAM_construct_end()); auto dp = DataPointer::Alloc(length); - if (dp && EVP_KDF_derive(kctx.get(), - reinterpret_cast(dp.get()), - length, - params.data()) == 1) { + if (dp && kdf.derive({dp.get(), length}, params.data())) { return dp; } @@ -2944,120 +3023,278 @@ EVPKeyPointer::PrivateKeyEncodingConfig::operator=( return *new (this) PrivateKeyEncodingConfig(other); } +// clang-format off +// NOLINTBEGIN(whitespace/line_length) +const KeyAlgorithm KeyAlgorithm::RSA("RSA", Family::Other); +const KeyAlgorithm KeyAlgorithm::RSA_PSS("RSA-PSS", Family::Other); +const KeyAlgorithm KeyAlgorithm::DSA("DSA", Family::Other); +const KeyAlgorithm KeyAlgorithm::DH("DH", Family::Other); +const KeyAlgorithm KeyAlgorithm::EC("EC", Family::Other); +const KeyAlgorithm KeyAlgorithm::ED25519("Ed25519", Family::EdDSA); +const KeyAlgorithm KeyAlgorithm::ED448("Ed448", Family::EdDSA); +const KeyAlgorithm KeyAlgorithm::X25519("X25519", Family::XDH); +const KeyAlgorithm KeyAlgorithm::X448("X448", Family::XDH); +const KeyAlgorithm KeyAlgorithm::SM2("SM2", Family::Other, /* has_key_type */ false); +const KeyAlgorithm KeyAlgorithm::ML_DSA_44("ML-DSA-44", Family::MLDSA); +const KeyAlgorithm KeyAlgorithm::ML_DSA_65("ML-DSA-65", Family::MLDSA); +const KeyAlgorithm KeyAlgorithm::ML_DSA_87("ML-DSA-87", Family::MLDSA); +const KeyAlgorithm KeyAlgorithm::ML_KEM_512("ML-KEM-512", Family::MLKEM); +const KeyAlgorithm KeyAlgorithm::ML_KEM_768("ML-KEM-768", Family::MLKEM); +const KeyAlgorithm KeyAlgorithm::ML_KEM_1024("ML-KEM-1024", Family::MLKEM); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_128F("SLH-DSA-SHA2-128f", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_128S("SLH-DSA-SHA2-128s", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_192F("SLH-DSA-SHA2-192f", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_192S("SLH-DSA-SHA2-192s", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_256F("SLH-DSA-SHA2-256f", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_256S("SLH-DSA-SHA2-256s", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_128F("SLH-DSA-SHAKE-128f", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_128S("SLH-DSA-SHAKE-128s", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_192F("SLH-DSA-SHAKE-192f", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_192S("SLH-DSA-SHAKE-192s", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_256F("SLH-DSA-SHAKE-256f", Family::SLHDSA); +const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_256S("SLH-DSA-SHAKE-256s", Family::SLHDSA); +// NOLINTEND(whitespace/line_length) +// clang-format on + +namespace { +#if NCRYPTO_USE_OPENSSL3_PROVIDER +constexpr char kSignatureContextString[] = "context-string"; +constexpr char kSignatureInstance[] = "instance"; +#endif +const KeyAlgorithm* const kKeyAlgorithms[] = { + &KeyAlgorithm::RSA, + &KeyAlgorithm::RSA_PSS, + &KeyAlgorithm::DSA, + &KeyAlgorithm::DH, + &KeyAlgorithm::EC, + &KeyAlgorithm::ED25519, + &KeyAlgorithm::ED448, + &KeyAlgorithm::X25519, + &KeyAlgorithm::X448, + &KeyAlgorithm::SM2, + &KeyAlgorithm::ML_DSA_44, + &KeyAlgorithm::ML_DSA_65, + &KeyAlgorithm::ML_DSA_87, + &KeyAlgorithm::ML_KEM_512, + &KeyAlgorithm::ML_KEM_768, + &KeyAlgorithm::ML_KEM_1024, + &KeyAlgorithm::SLH_DSA_SHA2_128F, + &KeyAlgorithm::SLH_DSA_SHA2_128S, + &KeyAlgorithm::SLH_DSA_SHA2_192F, + &KeyAlgorithm::SLH_DSA_SHA2_192S, + &KeyAlgorithm::SLH_DSA_SHA2_256F, + &KeyAlgorithm::SLH_DSA_SHA2_256S, + &KeyAlgorithm::SLH_DSA_SHAKE_128F, + &KeyAlgorithm::SLH_DSA_SHAKE_128S, + &KeyAlgorithm::SLH_DSA_SHAKE_192F, + &KeyAlgorithm::SLH_DSA_SHAKE_192S, + &KeyAlgorithm::SLH_DSA_SHAKE_256F, + &KeyAlgorithm::SLH_DSA_SHAKE_256S, +}; +} // namespace + +const KeyAlgorithm* KeyAlgorithm::FromName(const char* name) { + if (name == nullptr) return nullptr; + for (const auto* algorithm : kKeyAlgorithms) { + if (CaseInsensitiveNameEqual()(name, algorithm->name())) return algorithm; + } + return nullptr; +} + +void KeyAlgorithm::ForEachPqc(Callback callback) { + for (const auto* algorithm : kKeyAlgorithms) { + if (algorithm->isPqc() && algorithm->isAvailable()) callback(*algorithm); + } +} + +bool KeyAlgorithm::isRsa() const { + return this == &RSA || this == &RSA_PSS; +} + +bool KeyAlgorithm::isPqc() const { + return family_ == Family::MLDSA || family_ == Family::MLKEM || + family_ == Family::SLHDSA; +} + +bool KeyAlgorithm::isOkp() const { + return family_ == Family::EdDSA || family_ == Family::XDH; +} + +bool KeyAlgorithm::isOneShot() const { + return family_ == Family::EdDSA || family_ == Family::MLDSA || + family_ == Family::SLHDSA; +} + +bool KeyAlgorithm::supportsRawPublic() const { + return isOkp() || isPqc(); +} + +bool KeyAlgorithm::supportsRawPrivate() const { + return isOkp() || family_ == Family::SLHDSA; +} + +size_t KeyAlgorithm::seedSize() const { + if (family_ == Family::MLDSA) return 32; + if (family_ == Family::MLKEM) return 64; + return 0; +} + +namespace { +#if !NCRYPTO_USE_OPENSSL3_PROVIDER +struct LegacyKeyAlgorithm { + const char* name; + int id; +#if NCRYPTO_USE_BORINGSSL + const EVP_PKEY_ALG* (*raw_key_algorithm)() = nullptr; +#endif +}; + +// These backends require native key IDs. BoringSSL also uses EVP_PKEY_ALG +// descriptors for raw keys; keep both adapters in the same table. +// clang-format off +// NOLINTBEGIN(whitespace/line_length) +const LegacyKeyAlgorithm kLegacyKeyAlgorithms[] = { + {KeyAlgorithm::RSA.name(), EVP_PKEY_RSA}, + {KeyAlgorithm::RSA_PSS.name(), EVP_PKEY_RSA_PSS}, + {KeyAlgorithm::DSA.name(), EVP_PKEY_DSA}, + {KeyAlgorithm::DH.name(), EVP_PKEY_DH}, + {KeyAlgorithm::EC.name(), EVP_PKEY_EC}, +#if NCRYPTO_USE_BORINGSSL + {KeyAlgorithm::ED25519.name(), EVP_PKEY_ED25519, EVP_pkey_ed25519}, + {KeyAlgorithm::X25519.name(), EVP_PKEY_X25519, EVP_pkey_x25519}, +#else + {KeyAlgorithm::ED25519.name(), EVP_PKEY_ED25519}, + {KeyAlgorithm::X25519.name(), EVP_PKEY_X25519}, +#endif + {"HKDF", EVP_PKEY_HKDF}, + {KeyAlgorithm::ED448.name(), EVP_PKEY_ED448}, + {KeyAlgorithm::X448.name(), EVP_PKEY_X448}, +#ifndef OPENSSL_NO_SM2 + {KeyAlgorithm::SM2.name(), EVP_PKEY_SM2}, +#endif +#if NCRYPTO_USE_BORINGSSL + {KeyAlgorithm::ML_DSA_44.name(), EVP_PKEY_ML_DSA_44, EVP_pkey_ml_dsa_44}, + {KeyAlgorithm::ML_DSA_65.name(), EVP_PKEY_ML_DSA_65, EVP_pkey_ml_dsa_65}, + {KeyAlgorithm::ML_DSA_87.name(), EVP_PKEY_ML_DSA_87, EVP_pkey_ml_dsa_87}, + {KeyAlgorithm::ML_KEM_768.name(), EVP_PKEY_ML_KEM_768, EVP_pkey_ml_kem_768}, + {KeyAlgorithm::ML_KEM_1024.name(), EVP_PKEY_ML_KEM_1024, EVP_pkey_ml_kem_1024}, +#endif +}; +// NOLINTEND(whitespace/line_length) +// clang-format on + +const LegacyKeyAlgorithm* FindLegacyKeyAlgorithm(const char* name) { + if (name == nullptr) return nullptr; + for (const auto& algorithm : kLegacyKeyAlgorithms) { + if (CaseInsensitiveNameEqual()(name, algorithm.name)) return &algorithm; + } + return nullptr; +} + +int GetLegacyKeyId(const char* name) { + const auto* algorithm = FindLegacyKeyAlgorithm(name); + return algorithm == nullptr ? NID_undef : algorithm->id; +} + +#if NCRYPTO_USE_BORINGSSL +const EVP_PKEY_ALG* GetBoringSSLKeyAlgorithm(const KeyAlgorithm& algorithm) { + const auto* entry = FindLegacyKeyAlgorithm(algorithm.name()); + return entry != nullptr && entry->raw_key_algorithm != nullptr + ? entry->raw_key_algorithm() + : nullptr; +} +#endif +#endif +} // namespace + +void ConfigurePqcEncoding() { +#if NCRYPTO_USE_OPENSSL3_PROVIDER && OPENSSL_VERSION_PREREQ(3, 5) + // Configure all loaded providers to prefer seed-only format for ML-KEM and + // ML-DSA private keys in PKCS#8 export, falling back to priv-only when a + // seed is not available. The provider encoder reads these parameters at + // encoding time via ossl_prov_ctx_get_param(). + OSSL_PROVIDER_do_all( + nullptr, + [](OSSL_PROVIDER* provider, void*) -> int { + OSSL_PROVIDER_add_conf_parameter( + provider, "ml-kem.output_formats", "seed-only,priv-only"); + OSSL_PROVIDER_add_conf_parameter( + provider, "ml-dsa.output_formats", "seed-only,priv-only"); + return 1; + }, + nullptr); +#endif +} + +bool KeyAlgorithm::isAvailable() const { + MarkPopErrorOnReturn mark_pop_error_on_return; + return static_cast(EVPKeyCtxPointer::NewFromName(name_)); +} + EVPKeyPointer EVPKeyPointer::New() { return EVPKeyPointer(EVP_PKEY_new()); } EVPKeyPointer EVPKeyPointer::NewRawPublic( - int id, const Buffer& data) { - if (id == 0) return {}; + const KeyAlgorithm& algorithm, const Buffer& data) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return EVPKeyPointer(EVP_PKEY_new_raw_public_key_ex( + nullptr, algorithm.name(), nullptr, data.data, data.len)); +#elif NCRYPTO_USE_BORINGSSL + const auto* alg = GetBoringSSLKeyAlgorithm(algorithm); + if (alg == nullptr) return {}; + return EVPKeyPointer(EVP_PKEY_from_raw_public_key(alg, data.data, data.len)); +#else + const int id = GetLegacyKeyId(algorithm.name()); + if (id == NID_undef) return {}; return EVPKeyPointer( EVP_PKEY_new_raw_public_key(id, nullptr, data.data, data.len)); +#endif } EVPKeyPointer EVPKeyPointer::NewRawPrivate( - int id, const Buffer& data) { - if (id == 0) return {}; + const KeyAlgorithm& algorithm, const Buffer& data) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return EVPKeyPointer(EVP_PKEY_new_raw_private_key_ex( + nullptr, algorithm.name(), nullptr, data.data, data.len)); +#elif NCRYPTO_USE_BORINGSSL + const auto* alg = GetBoringSSLKeyAlgorithm(algorithm); + if (alg == nullptr) return {}; + return EVPKeyPointer(EVP_PKEY_from_raw_private_key(alg, data.data, data.len)); +#else + const int id = GetLegacyKeyId(algorithm.name()); + if (id == NID_undef) return {}; return EVPKeyPointer( EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len)); -} - -#if OPENSSL_WITH_PQC -namespace { -constexpr size_t kPqcMlDsaSeedSize = 32; -constexpr size_t kPqcMlKemSeedSize = 64; - -size_t GetPqcSeedSize(int id) { - switch (id) { - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - return kPqcMlDsaSeedSize; -#if OPENSSL_WITH_PQC_ML_KEM_512 - case EVP_PKEY_ML_KEM_512: -#endif - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - return kPqcMlKemSeedSize; - default: - unreachable(); - } -} - -#if OPENSSL_WITH_BORINGSSL_PQC -const EVP_PKEY_ALG* GetPqcSeedAlg(int id) { - switch (id) { - case EVP_PKEY_ML_DSA_44: - return EVP_pkey_ml_dsa_44(); - case EVP_PKEY_ML_DSA_65: - return EVP_pkey_ml_dsa_65(); - case EVP_PKEY_ML_DSA_87: - return EVP_pkey_ml_dsa_87(); - case EVP_PKEY_ML_KEM_768: - return EVP_pkey_ml_kem_768(); - case EVP_PKEY_ML_KEM_1024: - return EVP_pkey_ml_kem_1024(); - default: - unreachable(); - } -} -#else -const char* GetPqcSeedParamName(int id) { - switch (id) { - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - return OSSL_PKEY_PARAM_ML_DSA_SEED; - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - return OSSL_PKEY_PARAM_ML_KEM_SEED; - default: - unreachable(); - } -} #endif +} -EVPKeyPointer NewPqcKeyFromSeed(int id, - const Buffer& data) { -#if OPENSSL_WITH_BORINGSSL_PQC +EVPKeyPointer EVPKeyPointer::NewRawSeed( + const KeyAlgorithm& algorithm, const Buffer& data) { + if (algorithm.seedSize() == 0) return {}; +#if NCRYPTO_USE_BORINGSSL + const auto* seed_alg = GetBoringSSLKeyAlgorithm(algorithm); + if (seed_alg == nullptr) return {}; return EVPKeyPointer( - EVP_PKEY_from_private_seed(GetPqcSeedAlg(id), data.data, data.len)); -#else + EVP_PKEY_from_private_seed(seed_alg, data.data, data.len)); +#elif NCRYPTO_USE_OPENSSL3_PROVIDER + // ML-DSA and ML-KEM both use the provider parameter "seed". OSSL_PARAM params[] = { - OSSL_PARAM_construct_octet_string(GetPqcSeedParamName(id), - const_cast(data.data), - data.len), + OSSL_PARAM_construct_octet_string( + "seed", const_cast(data.data), data.len), OSSL_PARAM_END}; - - auto ctx = EVPKeyCtxPointer::NewFromID(id); + auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(algorithm); if (!ctx) return {}; - EVP_PKEY* pkey = nullptr; if (EVP_PKEY_fromdata_init(ctx.get()) <= 0 || EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { return {}; } return EVPKeyPointer(pkey); -#endif -} - -bool GetPqcSeed(EVP_PKEY* pkey, int id, const Buffer& out) { - size_t len = out.len; -#if OPENSSL_WITH_BORINGSSL_PQC - return EVP_PKEY_get_private_seed(pkey, out.data, &len) == 1; #else - return EVP_PKEY_get_octet_string_param( - pkey, GetPqcSeedParamName(id), out.data, out.len, &len) == 1; + return {}; #endif } -} // namespace - -EVPKeyPointer EVPKeyPointer::NewRawSeed( - int id, const Buffer& data) { - return NewPqcKeyFromSeed(id, data); -} -#endif EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) { if (!dh) return {}; @@ -3125,7 +3362,7 @@ EVPKeyPointer EVPKeyPointer::NewRSA(const Rsa& rsa) { OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); if (!params) return {}; - return NewPKeyFromData(EVP_PKEY_RSA, selection, params.get()); + return NewPKeyFromData(KeyAlgorithm::RSA, selection, params.get()); } #else EVPKeyPointer EVPKeyPointer::NewRSA(RSAPointer&& rsa) { @@ -3162,41 +3399,255 @@ EVP_PKEY* EVPKeyPointer::release() { return pkey_.release(); } -int EVPKeyPointer::id(const EVP_PKEY* key) { - if (key == nullptr) return 0; - int type = EVP_PKEY_id(key); -#if OPENSSL_WITH_OPENSSL_PQC - // EVP_PKEY_id returns -1 when EVP_PKEY_* is only implemented in a provider - // which is the case for all post-quantum NIST algorithms - // one suggested way would be to use a chain of `EVP_PKEY_is_a` - // https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870 - // or, this way there are less calls to the OpenSSL provider, just - // getting the name once - if (type == -1) { - const char* type_name = EVP_PKEY_get0_type_name(key); - if (type_name == nullptr) return -1; - - for (const auto& mapping : pqc_mappings) { - if (strcmp(type_name, mapping.name) == 0) { - return mapping.nid; - } +bool EVPKeyPointer::isA(const EVP_PKEY* key, const char* name) { + if (key == nullptr || name == nullptr) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // EVP_PKEY_is_a() can match an untyped key to an unknown legacy name. + return EVP_PKEY_get0_type_name(key) != nullptr && + EVP_PKEY_is_a(key, name) == 1; +#else + const int id = GetLegacyKeyId(name); + return id != NID_undef && EVP_PKEY_id(key) == id; +#endif +} + +// Returns true unless the key is known not to be SM2, so that a key whose curve +// cannot be determined opts out of the prehashed fallback rather than into it. +bool EVPKeyPointer::mayBeSM2() const { +#ifdef OPENSSL_NO_SM2 + return false; +#else + if (isA(KeyAlgorithm::SM2)) return true; + if (!isA(KeyAlgorithm::EC)) return false; + +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // An ECKeyPointer would also need the public point, which a provider-backed + // key need not expose. + char group_name[64]; + size_t group_name_len = 0; + if (EVP_PKEY_get_utf8_string_param(get(), + OSSL_PKEY_PARAM_GROUP_NAME, + group_name, + sizeof(group_name), + &group_name_len) != 1) { + return true; + } + return OBJ_sn2nid(group_name) == NID_sm2 || + EC_curve_nist2nid(group_name) == NID_sm2; +#else + ECKeyPointer ec(*this); + if (!ec) return true; + + const EC_GROUP* group = ec.getGroup(); + if (group == nullptr) return true; + return EC_GROUP_get_curve_name(group) == NID_sm2; +#endif +#endif +} + +bool EVPKeyPointer::isA(const char* name) const { + return isA(get(), name); +} + +bool EVPKeyPointer::isA(const EVP_PKEY* key, const KeyAlgorithm& algorithm) { + return isA(key, algorithm.name()); +} + +bool EVPKeyPointer::isA(const KeyAlgorithm& algorithm) const { + return isA(get(), algorithm); +} + +const KeyAlgorithm* EVPKeyPointer::getAlgorithm() const { + if (!pkey_) return nullptr; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // Provider primary names identify algorithms. Legacy ASN.1 methods can + // share names (for example, SM2 uses EC), so resolve those through isA(). + // The fallback also handles providers with a noncanonical primary alias. + if (EVP_PKEY_get0_provider(get()) != nullptr) { + if (const auto* algorithm = + KeyAlgorithm::FromName(EVP_PKEY_get0_type_name(get()))) { + return algorithm; } } + for (const auto* algorithm : kKeyAlgorithms) { + if (isA(*algorithm)) return algorithm; + } +#else + const int id = EVP_PKEY_id(get()); + for (const auto& algorithm : kLegacyKeyAlgorithms) { + if (id == algorithm.id) return KeyAlgorithm::FromName(algorithm.name); + } #endif - return type; + return nullptr; +} + +const char* EVPKeyPointer::getKeyTypeName() const { + const auto* algorithm = getAlgorithm(); + return algorithm == nullptr ? nullptr : algorithm->keyTypeName(); +} + +bool EVPKeyPointer::supportsRawPublic() const { + const auto* algorithm = getAlgorithm(); + return algorithm != nullptr && algorithm->supportsRawPublic(); } -int EVPKeyPointer::base_id(const EVP_PKEY* key) { - if (key == nullptr) return 0; - return EVP_PKEY_base_id(key); +bool EVPKeyPointer::supportsRawPrivate() const { + const auto* algorithm = getAlgorithm(); + return algorithm != nullptr && algorithm->supportsRawPrivate(); } -int EVPKeyPointer::id() const { - return id(get()); +bool EVPKeyPointer::supportsContextString() const { + const auto* algorithm = getAlgorithm(); + if (algorithm == nullptr || !algorithm->isOneShot()) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + MarkPopErrorOnReturn mark_pop_error_on_return; + DeleteFnPtr signature( + EVP_SIGNATURE_fetch(nullptr, EVP_PKEY_get0_type_name(get()), nullptr)); + if (!signature) return false; + const OSSL_PARAM* params = EVP_SIGNATURE_settable_ctx_params(signature.get()); + return params != nullptr && + OSSL_PARAM_locate_const(params, kSignatureContextString) != nullptr && + (algorithm != &KeyAlgorithm::ED25519 || + OSSL_PARAM_locate_const(params, kSignatureInstance) != nullptr); +#elif NCRYPTO_USE_BORINGSSL + return algorithm->isPqc(); +#else + return false; +#endif +} + +namespace { +constexpr size_t kEd25519PointSize = 32; +constexpr size_t kEd448PointSize = 57; + +// Ed25519 has cofactor 8, so the first eight entries are the full +// canonical small-order subgroup: identity, one point of order 2, +// two points of order 4, and four points of order 8. +constexpr unsigned char kEd25519SmallOrderPoints[][kEd25519PointSize] = { + // Identity. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 2. + {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, + // Order 4. + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 8. + {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a}, + {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa}, + {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05}, + {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85}, + // Non-canonical encodings of the same small-order points. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, + {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, + {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, +}; + +// Ed448 has cofactor 4, so these four entries are the full canonical +// small-order subgroup: identity, one point of order 2, and two points +// of order 4. +constexpr unsigned char kEd448SmallOrderPoints[][kEd448PointSize] = { + // Identity. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 2. + {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}, + // Order 4. + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, +}; + +template +bool ContainsPoint(const unsigned char* candidate, + const unsigned char (&points)[Count][PointSize]) { + for (const auto& point : points) { + if (memcmp(candidate, point, PointSize) == 0) return true; + } + return false; } -int EVPKeyPointer::base_id() const { - return base_id(get()); +bool IsSmallOrderEdDsaPoint(const EVPKeyPointer& key, + const unsigned char* candidate, + size_t size) { + if (key.isA(KeyAlgorithm::ED25519)) { + return size == kEd25519PointSize && + ContainsPoint(candidate, kEd25519SmallOrderPoints); + } + if (key.isA(KeyAlgorithm::ED448)) { + return size == kEd448PointSize && + ContainsPoint(candidate, kEd448SmallOrderPoints); + } + return false; +} + +} // namespace + +bool EVPKeyPointer::hasSmallOrderEdDsaPoint( + const Buffer& signature) const { + const size_t point_size = isA(KeyAlgorithm::ED25519) ? kEd25519PointSize + : isA(KeyAlgorithm::ED448) ? kEd448PointSize + : 0; + if (point_size == 0) return false; + + if (signature.len != point_size * 2) return false; + + if (IsSmallOrderEdDsaPoint(*this, signature.data, point_size)) { + return true; + } + + unsigned char raw_public_key[kEd448PointSize]; + size_t raw_public_key_size = point_size; + if (EVP_PKEY_get_raw_public_key( + get(), raw_public_key, &raw_public_key_size) != 1) { + return false; + } + + return IsSmallOrderEdDsaPoint(*this, raw_public_key, raw_public_key_size); } int EVPKeyPointer::bits() const { @@ -3239,20 +3690,77 @@ DataPointer EVPKeyPointer::rawPublicKey() const { return {}; } -#if OPENSSL_WITH_PQC -DataPointer EVPKeyPointer::rawSeed() const { - if (!pkey_) return {}; +namespace { +DataPointer GetRawSeed([[maybe_unused]] EVP_PKEY* key, size_t seed_len) { + auto data = DataPointer::Alloc(seed_len); + if (!data) return {}; +#if NCRYPTO_USE_BORINGSSL || NCRYPTO_USE_OPENSSL3_PROVIDER + const Buffer buf = data; + size_t len = data.size(); +#endif +#if NCRYPTO_USE_BORINGSSL + if (EVP_PKEY_get_private_seed(key, buf.data, &len) != 1) return {}; +#elif NCRYPTO_USE_OPENSSL3_PROVIDER + if (EVP_PKEY_get_octet_string_param(key, "seed", buf.data, buf.len, &len) != + 1) + return {}; +#else + return {}; +#endif + return data; +} - const size_t seed_len = GetPqcSeedSize(id()); +} // namespace - if (auto data = DataPointer::Alloc(seed_len)) { - const Buffer buf = data; - if (!GetPqcSeed(get(), id(), buf)) return {}; - return data; +Result EVPKeyPointer::rawSeed() + const { + const auto* algorithm = getAlgorithm(); + if (algorithm == nullptr || algorithm->seedSize() == 0) { + return RawExportError::UNSUPPORTED_KEY_TYPE; } - return {}; + auto data = GetRawSeed(get(), algorithm->seedSize()); + if (!data) return RawExportError::MISSING_SEED; + return data; +} + +Result +EVPKeyPointer::exportRawJwk(bool include_private) const { + const auto* algorithm = getAlgorithm(); + if (algorithm == nullptr || (!algorithm->isOkp() && !algorithm->isPqc())) { + return RawExportError::UNSUPPORTED_KEY_TYPE; + } + RawJwkData data{algorithm, {}, {}}; + if (include_private) { + const size_t seed_len = algorithm->seedSize(); + data.private_key = + seed_len != 0 ? GetRawSeed(get(), seed_len) : rawPrivateKey(); + if (!data.private_key) { + return seed_len != 0 ? RawExportError::MISSING_SEED + : RawExportError::FAILED; + } + } + data.public_key = rawPublicKey(); + if (!data.public_key) return RawExportError::FAILED; + return data; +} + +EVPKeyPointer EVPKeyPointer::NewRawJwk( + const KeyAlgorithm& algorithm, + const Buffer& public_key, + const std::optional>& private_key) { + if (!algorithm.isOkp() && !algorithm.isPqc()) return {}; + if (!private_key) return NewRawPublic(algorithm, public_key); + auto key = algorithm.seedSize() != 0 ? NewRawSeed(algorithm, *private_key) + : NewRawPrivate(algorithm, *private_key); + if (!key) return {}; + const auto derived_public = key.rawPublicKey(); + if (!derived_public || derived_public.size() != public_key.len || + CRYPTO_memcmp(derived_public.get(), public_key.data, public_key.len) != + 0) { + return {}; + } + return key; } -#endif DataPointer EVPKeyPointer::rawPrivateKey() const { if (!pkey_) return {}; @@ -3337,7 +3845,7 @@ bool EVPKeyPointer::set(const ECKeyPointer& eckey) { OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get())); if (!params) return false; - auto pkey = NewPKeyFromData(EVP_PKEY_EC, selection, params.get()); + auto pkey = NewPKeyFromData(KeyAlgorithm::EC, selection, params.get()); if (!pkey) return false; reset(pkey.release()); return true; @@ -3355,6 +3863,47 @@ EVPKeyPointer::operator const EC_KEY*() const { namespace { +EVP_PKEY* DecodeRsaPublicKey(const unsigned char** data, size_t length) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // Borrow the EVP_PKEY constructor and its data from a context that stays + // alive until after the restricted decoder context is destroyed. + EVP_PKEY* raw = nullptr; + DeleteFnPtr construct_ctx( + OSSL_DECODER_CTX_new_for_pkey(&raw, + "DER", + "type-specific", + KeyAlgorithm::RSA.name(), + EVP_PKEY_PUBLIC_KEY, + nullptr, + nullptr)); + if (!construct_ctx) return nullptr; + auto* construct = OSSL_DECODER_CTX_get_construct(construct_ctx.get()); + void* construct_data = + OSSL_DECODER_CTX_get_construct_data(construct_ctx.get()); + if (construct == nullptr || construct_data == nullptr) return nullptr; + + // Add only the type-specific RSA decoder: new_for_pkey() can also build + // chains that accept SPKI. The owning context retains the cleanup callback. + DeleteFnPtr decoder(OSSL_DECODER_fetch( + nullptr, KeyAlgorithm::RSA.name(), "input=der,structure=type-specific")); + DeleteFnPtr ctx( + OSSL_DECODER_CTX_new()); + if (!decoder || !ctx || + OSSL_DECODER_CTX_add_decoder(ctx.get(), decoder.get()) != 1 || + OSSL_DECODER_CTX_set_input_type(ctx.get(), "DER") != 1 || + OSSL_DECODER_CTX_set_selection(ctx.get(), EVP_PKEY_PUBLIC_KEY) != 1 || + OSSL_DECODER_CTX_set_construct(ctx.get(), construct) != 1 || + OSSL_DECODER_CTX_set_construct_data(ctx.get(), construct_data) != 1) { + return nullptr; + } + const int result = OSSL_DECODER_from_data(ctx.get(), data, &length); + EVPKeyPointer key(raw); + return result == 1 ? key.release() : nullptr; +#else + return d2i_PublicKey(NID_rsaEncryption, nullptr, data, length); +#endif +} + EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp, const char* name, auto&& parse) { @@ -3406,34 +3955,6 @@ constexpr bool IsASN1Sequence(const unsigned char* data, return true; } -constexpr bool ReadASN1Element(const unsigned char* data, - size_t size, - unsigned char tag, - size_t* header_size, - size_t* content_size, - size_t* total_size) { - if (size < 2 || data[0] != tag) return false; - - size_t offset; - size_t length; - if (data[1] & 0x80) { - size_t n_bytes = data[1] & ~0x80; - if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) return false; - length = 0; - for (size_t i = 0; i < n_bytes; i++) length = (length << 8) | data[i + 2]; - offset = 2 + n_bytes; - } else { - offset = 2; - length = data[1]; - } - - if (offset > size || length > size - offset) return false; - *header_size = offset; - *content_size = length; - *total_size = offset + length; - return true; -} - constexpr bool IsEncryptedPrivateKeyInfo( const Buffer& buffer) { // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE. @@ -3482,7 +4003,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM( bp, "RSA PUBLIC KEY", [](const unsigned char** p, long l) { // NOLINT(runtime/int) - return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); + return DecodeRsaPublicKey(p, l); })) { return ret; } @@ -3517,7 +4038,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKey( EVP_PKEY* key = nullptr; if (config.type == PKEncodingType::PKCS1 && - (key = d2i_PublicKey(EVP_PKEY_RSA, nullptr, &start, buffer.len))) { + (key = DecodeRsaPublicKey(&start, buffer.len))) { return EVPKeyPointer::ParseKeyResult(EVPKeyPointer(key)); } @@ -3633,7 +4154,7 @@ bool WriteEncryptedTraditionalPEM(BIO* bio, } bool ECKeyHasMissingOid(const EVPKeyPointer& key) { - if (key.id() != EVP_PKEY_EC) return false; + if (!key.isA(KeyAlgorithm::EC)) return false; const Ec ec(key.get()); const EC_GROUP* group = ec.getGroup(); @@ -3827,11 +4348,11 @@ Result EVPKeyPointer::writePrivateKey( switch (config.type) { case PKEncodingType::PKCS1: { // PKCS1 is only permitted for RSA keys. - if (id() != EVP_PKEY_RSA) return Result(false); + if (!isA(KeyAlgorithm::RSA)) return Result(false); #if NCRYPTO_USE_OPENSSL3_PROVIDER const EVP_CIPHER* cipher = - config.format == PKFormatType::PEM ? config.cipher : nullptr; + config.format == PKFormatType::PEM ? config.cipher.get() : nullptr; if (cipher != nullptr && passphrase.len == 0) { err = !WriteEncryptedTraditionalPEM(bio.get(), get(), cipher, passphrase); @@ -3909,11 +4430,11 @@ Result EVPKeyPointer::writePrivateKey( } case PKEncodingType::SEC1: { // SEC1 is only permitted for EC keys - if (id() != EVP_PKEY_EC) return Result(false); + if (!isA(KeyAlgorithm::EC)) return Result(false); #if NCRYPTO_USE_OPENSSL3_PROVIDER const EVP_CIPHER* cipher = - config.format == PKFormatType::PEM ? config.cipher : nullptr; + config.format == PKFormatType::PEM ? config.cipher.get() : nullptr; err = !WriteEncodedPKey(bio.get(), get(), OSSL_KEYMGMT_SELECT_ALL, @@ -3979,7 +4500,7 @@ Result EVPKeyPointer::writePublicKey( if (config.type == ncrypto::EVPKeyPointer::PKEncodingType::PKCS1) { // PKCS#1 is only valid for RSA keys. #if NCRYPTO_USE_OPENSSL3_PROVIDER - if (id() != EVP_PKEY_RSA) return Result(false); + if (!isA(KeyAlgorithm::RSA)) return Result(false); if (!WriteEncodedPKey(bio.get(), get(), OSSL_KEYMGMT_SELECT_PUBLIC_KEY, @@ -4058,59 +4579,30 @@ Result EVPKeyPointer::writePublicKey( return bio; } -bool EVPKeyPointer::isRsaVariant() const { - if (!pkey_) return false; - int type = id(); - return type == EVP_PKEY_RSA || type == EVP_PKEY_RSA2 || - type == EVP_PKEY_RSA_PSS; +bool EVPKeyPointer::isRsaVariant(const EVP_PKEY* key) { +#if !NCRYPTO_USE_OPENSSL3_PROVIDER && !NCRYPTO_USE_BORINGSSL + if (key != nullptr && EVP_PKEY_id(key) == EVP_PKEY_RSA2) return true; +#endif + return isA(key, KeyAlgorithm::RSA) || isA(key, KeyAlgorithm::RSA_PSS); } -bool EVPKeyPointer::isOneShotVariant() const { - if (!pkey_) return false; - int type = id(); - switch (type) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: -#if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: -#if OPENSSL_WITH_PQC_SLH_DSA - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: -#endif -#endif - return true; - default: - return false; - } +bool EVPKeyPointer::isRsaVariant() const { + return isRsaVariant(get()); } bool EVPKeyPointer::isSigVariant() const { - if (!pkey_) return false; - int type = id(); - return type == EVP_PKEY_EC || type == EVP_PKEY_DSA; + return isA(KeyAlgorithm::EC) || isA(KeyAlgorithm::DSA); } int EVPKeyPointer::getDefaultSignPadding() const { - return id() == EVP_PKEY_RSA_PSS ? RSA_PKCS1_PSS_PADDING : RSA_PKCS1_PADDING; + return isA(KeyAlgorithm::RSA_PSS) ? RSA_PKCS1_PSS_PADDING : RSA_PKCS1_PADDING; } std::optional EVPKeyPointer::getBytesOfRS() const { if (!pkey_) return std::nullopt; - int bits, id = base_id(); + int bits; - if (id == EVP_PKEY_DSA) { + if (isA(KeyAlgorithm::DSA)) { #if NCRYPTO_USE_OPENSSL3_PROVIDER DeleteFnPtr q; if (!GetPKeyBnParam(get(), OSSL_PKEY_PARAM_FFC_Q, &q)) return std::nullopt; @@ -4128,13 +4620,9 @@ std::optional EVPKeyPointer::getBytesOfRS() const { } if (!has_bits) return std::nullopt; #endif - } else if (id == EVP_PKEY_EC) { + } else if (isA(KeyAlgorithm::EC)) { #if NCRYPTO_USE_OPENSSL3_PROVIDER - Ec ec(get()); - if (!ec) return std::nullopt; - const EC_GROUP* group = ec.getGroup(); - if (group == nullptr) return std::nullopt; - bits = EC_GROUP_order_bits(group); + bits = EVP_PKEY_bits(get()); #else const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(get()); if (ec_key == nullptr) return std::nullopt; @@ -4152,8 +4640,7 @@ std::optional EVPKeyPointer::getBytesOfRS() const { } EVPKeyPointer::operator Rsa() const { - int type = id(); - if (type != EVP_PKEY_RSA && type != EVP_PKEY_RSA_PSS) return {}; + if (!isA(KeyAlgorithm::RSA) && !isA(KeyAlgorithm::RSA_PSS)) return {}; #if NCRYPTO_USE_OPENSSL3_PROVIDER return Rsa(get()); @@ -4172,8 +4659,7 @@ EVPKeyPointer::operator Rsa() const { } EVPKeyPointer::operator Dsa() const { - int type = id(); - if (type != EVP_PKEY_DSA) return {}; + if (!isA(KeyAlgorithm::DSA)) return {}; #if NCRYPTO_USE_OPENSSL3_PROVIDER return Dsa(get()); @@ -4187,9 +4673,10 @@ EVPKeyPointer::operator Dsa() const { bool EVPKeyPointer::validateDsaParameters() const { if (!pkey_) return false; #if OPENSSL_VERSION_MAJOR >= 3 - if (EVP_default_properties_is_fips_enabled(nullptr) && EVP_PKEY_DSA == id()) { + if (EVP_default_properties_is_fips_enabled(nullptr) && + isA(KeyAlgorithm::DSA)) { #else - if (FIPS_mode() && EVP_PKEY_DSA == id()) { + if (FIPS_mode() && isA(KeyAlgorithm::DSA)) { #endif // Validate DSA2 parameters from FIPS 186-4. #if NCRYPTO_USE_OPENSSL3_PROVIDER @@ -4739,6 +5226,19 @@ const Cipher Cipher::FromName(const char* name, CipherCache* cache) { #endif } +const Cipher Cipher::FromNameForKeyEncoding(const char* name) { + // Key serializers have their own cipher restrictions. Preserve their policy + // instead of applying the filters used by the general cipher operations. +#if NCRYPTO_USE_OPENSSL3_PROVIDER + MarkPopErrorOnReturn mark_pop_error_on_return; + DeleteFnPtr fetched( + EVP_CIPHER_fetch(nullptr, name, nullptr)); + if (fetched) return Cipher(std::move(fetched)); +#endif + // Preserve serializer errors for known ciphers that cannot be fetched. + return Cipher(EVP_get_cipherbyname(name)); +} + const Cipher Cipher::FromNid(int nid, CipherCache* cache) { MarkPopErrorOnReturn mark_pop_error_on_return; const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid); @@ -5349,11 +5849,29 @@ bool ECPointPointer::mul(const EC_GROUP* group, const BIGNUM* priv_key) { // ============================================================================ +bool ECKeyPointer::checkPrivateKey() const { + const auto group = getGroup(); + const auto priv = getPrivateKey(); + const auto pub = getPublicKey(); + if (group == nullptr || priv == nullptr || pub == nullptr) return false; + + auto order = BignumPointer::New(); + if (!order || !EC_GROUP_get_order(group, order.get(), nullptr) || + BN_is_zero(priv) || BN_is_negative(priv) || + BN_cmp(priv, order.get()) >= 0) { + return false; + } + + auto expected = ECPointPointer::New(group); + return expected && expected.mul(group, priv) && + EC_POINT_cmp(group, expected.get(), pub, nullptr) == 0; +} + #if NCRYPTO_USE_LEGACY_KEY_TYPES ECKeyPointer::ECKeyPointer() : key_(nullptr) {} ECKeyPointer::ECKeyPointer(const EVPKeyPointer& key) : key_(nullptr) { - if (key.id() != EVP_PKEY_EC) return; + if (!key.isA(KeyAlgorithm::EC)) return; const EC_KEY* ec = key; if (ec != nullptr) key_.reset(EC_KEY_dup(ec)); } @@ -5506,7 +6024,7 @@ ECKeyPointer ECKeyPointer::New(const EC_GROUP* group) { ECKeyPointer::ECKeyPointer() : group_(nullptr), pub_(nullptr), priv_(nullptr) {} ECKeyPointer::ECKeyPointer(const EVPKeyPointer& key) : ECKeyPointer() { - if (key.id() != EVP_PKEY_EC) return; + if (!key.isA(KeyAlgorithm::EC)) return; char group_name[80]; size_t group_name_len = 0; if (EVP_PKEY_get_utf8_string_param(key.get(), @@ -5592,7 +6110,7 @@ ECKeyPointer ECKeyPointer::clone() const { bool ECKeyPointer::generate() { if (!group_) return false; const int nid = EC_GROUP_get_curve_name(group_.get()); - auto ctx = EVPKeyCtxPointer::NewFromID(EVP_PKEY_EC); + auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::EC); if (!ctx || !ctx.initForKeygen() || !ctx.setEcParameters(nid, OPENSSL_EC_NAMED_CURVE)) { return false; @@ -5791,12 +6309,24 @@ EVPKeyCtxPointer EVPKeyCtxPointer::New(const EVPKeyPointer& key) { return EVPKeyCtxPointer(EVP_PKEY_CTX_new(key.get(), nullptr)); } -EVPKeyCtxPointer EVPKeyCtxPointer::NewFromID(int id) { +EVPKeyCtxPointer EVPKeyCtxPointer::NewFromName(const char* name) { + if (name == nullptr) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return EVPKeyCtxPointer(EVP_PKEY_CTX_new_from_name(nullptr, name, nullptr)); +#else + const int id = GetLegacyKeyId(name); + if (id == NID_undef) return {}; #ifdef OPENSSL_IS_BORINGSSL - // DSA keys are not supported with BoringSSL + // DSA keys are not supported with BoringSSL. if (id == EVP_PKEY_DSA) return {}; #endif return EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(id, nullptr)); +#endif +} + +EVPKeyCtxPointer EVPKeyCtxPointer::NewFromAlgorithm( + const KeyAlgorithm& algorithm) { + return NewFromName(algorithm.name()); } bool EVPKeyCtxPointer::initForDerive(const EVPKeyPointer& peer) { @@ -5851,10 +6381,15 @@ bool EVPKeyCtxPointer::setDsaParameters(uint32_t bits, } bool EVPKeyCtxPointer::setEcParameters(int curve, int encoding) { - if (!ctx_) return false; + return setEcParameters(OBJ_nid2sn(curve), encoding); +} + +bool EVPKeyCtxPointer::setEcParameters(const char* group_name, int encoding) { + if (!ctx_ || group_name == nullptr) return false; + const int curve = Ec::GetCurveIdFromName(group_name); #if NCRYPTO_USE_OPENSSL3_PROVIDER - const char* group_name = OBJ_nid2sn(curve); - if (group_name == nullptr) return false; + // Keep the historical aliases while allowing names known only to providers. + if (curve != NID_undef) group_name = OBJ_nid2sn(curve); const char* encoding_name = nullptr; switch (encoding) { @@ -5876,7 +6411,8 @@ bool EVPKeyCtxPointer::setEcParameters(int curve, int encoding) { }; return EVP_PKEY_CTX_set_params(ctx_.get(), params) == 1; #else - return EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx_.get(), curve) == 1 && + return curve != NID_undef && + EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx_.get(), curve) == 1 && EVP_PKEY_CTX_set_ec_param_enc(ctx_.get(), encoding) == 1; #endif } @@ -6181,147 +6717,48 @@ Rsa::OtherPrimeInfoPointer::OtherPrimeInfoPointer(BignumPointer&& r, #if NCRYPTO_USE_OPENSSL3_PROVIDER namespace { -int DigestAlgorithmIdentifierToNid(const unsigned char* data, size_t size) { - size_t sequence_header; - size_t sequence_len; - size_t sequence_total; - if (!ReadASN1Element( - data, size, 0x30, &sequence_header, &sequence_len, &sequence_total)) { - return NID_undef; - } - - size_t oid_header; - size_t oid_len; - size_t oid_total; - const unsigned char* oid = data + sequence_header; - if (!ReadASN1Element( - oid, sequence_len, 0x06, &oid_header, &oid_len, &oid_total)) { - return NID_undef; - } - - const unsigned char* oid_data = oid; - DeleteFnPtr obj( - d2i_ASN1_OBJECT(nullptr, &oid_data, oid_total)); - if (!obj) return NID_undef; - return OBJ_obj2nid(obj.get()); +// Normalizes a provider digest name such as "SHA2-256" to the long name the +// rest of the key details use ("sha256"). The returned storage has static +// lifetime, which the string_view fields of PssParams require. +const char* RsaPssDigestLongName(const char* name) { + const EVP_MD* md = EVP_get_digestbyname(name); + if (md == nullptr) return nullptr; + const int nid = EVP_MD_get_type(md); + return nid != NID_undef ? OBJ_nid2ln(nid) : nullptr; } bool ReadRsaPssParams(const EVP_PKEY* pkey, Rsa::PssParams* params) { - const int der_len = i2d_PUBKEY(pkey, nullptr); - if (der_len <= 0) return false; - - auto der = DataPointer::Alloc(der_len); - if (!der) return false; - - auto serialized = static_cast(der.get()); - if (i2d_PUBKEY(pkey, &serialized) != der_len) return false; - - size_t outer_header; - size_t outer_len; - size_t outer_total; - const auto* data = static_cast(der.get()); - if (!ReadASN1Element( - data, der.size(), 0x30, &outer_header, &outer_len, &outer_total)) { - return false; - } - - size_t alg_header; - size_t alg_len; - size_t alg_total; - const unsigned char* alg = data + outer_header; - if (!ReadASN1Element( - alg, outer_len, 0x30, &alg_header, &alg_len, &alg_total)) { - return false; - } - - size_t oid_header; - size_t oid_len; - size_t oid_total; - const unsigned char* oid = alg + alg_header; - if (!ReadASN1Element(oid, alg_len, 0x06, &oid_header, &oid_len, &oid_total) || - oid_total == alg_len) { + // The RSASSA-PSS-params sequence is exposed as a unit. The salt length is + // readable whenever the sequence is present, including when it is empty + // because every field carried its default, and unreadable when the algorithm + // identifier has no parameters at all. That is the distinction between a + // restricted key and an unrestricted one. + int salt_length = 0; + // TODO(panva): In a semver-major, reject malformed RSA-PSS parameters + // at key import instead of omitting asymmetricKeyDetails fields. + if (EVP_PKEY_get_int_param( + pkey, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, &salt_length) != 1 || + salt_length < 0) { return false; } + params->salt_length = salt_length; - size_t pss_header; - size_t pss_len; - size_t pss_total; - const unsigned char* pss = oid + oid_total; - if (!ReadASN1Element( - pss, alg_len - oid_total, 0x30, &pss_header, &pss_len, &pss_total)) { - return false; - } - - const unsigned char* cursor = pss + pss_header; - size_t remaining = pss_len; - while (remaining > 0) { - const unsigned char tag = cursor[0]; - size_t item_header; - size_t item_len; - size_t item_total; - if (!ReadASN1Element( - cursor, remaining, tag, &item_header, &item_len, &item_total)) { - return false; + // The provider may omit default SHA-1 digest parameters. Keep the initialized + // defaults when a digest name is absent or cannot be resolved. + char name[80]; + if (EVP_PKEY_get_utf8_string_param( + pkey, OSSL_PKEY_PARAM_RSA_DIGEST, name, sizeof(name), nullptr) == 1) { + if (const char* long_name = RsaPssDigestLongName(name)) { + params->digest = long_name; } + } - const unsigned char* item = cursor + item_header; - switch (tag) { - case 0xa0: { - const int nid = DigestAlgorithmIdentifierToNid(item, item_len); - if (nid != NID_undef) params->digest = OBJ_nid2ln(nid); - break; - } - case 0xa1: { - size_t mgf_header; - size_t mgf_len; - size_t mgf_total; - if (!ReadASN1Element( - item, item_len, 0x30, &mgf_header, &mgf_len, &mgf_total)) { - return false; - } - const unsigned char* mgf = item + mgf_header; - size_t mgf_oid_header; - size_t mgf_oid_len; - size_t mgf_oid_total; - if (!ReadASN1Element(mgf, - mgf_len, - 0x06, - &mgf_oid_header, - &mgf_oid_len, - &mgf_oid_total) || - mgf_oid_total == mgf_len) { - return false; - } - const int nid = DigestAlgorithmIdentifierToNid(mgf + mgf_oid_total, - mgf_len - mgf_oid_total); - if (nid != NID_undef) params->mgf1_digest = OBJ_nid2ln(nid); - break; - } - case 0xa2: { - size_t int_header; - size_t int_len; - size_t int_total; - if (!ReadASN1Element( - item, item_len, 0x02, &int_header, &int_len, &int_total)) { - return false; - } - // TODO(panva): In a semver-major, reject malformed RSA-PSS parameters - // at key import instead of omitting asymmetricKeyDetails fields. - if (int_len == 0 || int_len > sizeof(uint64_t) || - (item[int_header] & 0x80) != 0) { - return false; - } - uint64_t salt_length = 0; - for (size_t n = 0; n < int_len; n++) { - salt_length = (salt_length << 8) | item[int_header + n]; - } - params->salt_length = static_cast(salt_length); - break; - } + if (EVP_PKEY_get_utf8_string_param( + pkey, OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, name, sizeof(name), nullptr) == + 1) { + if (const char* long_name = RsaPssDigestLongName(name)) { + params->mgf1_digest = long_name; } - - cursor += item_total; - remaining -= item_total; } return true; @@ -6390,14 +6827,22 @@ ASN1StringPointer EncodeRsaPssParams(const Rsa::PssParams& params) { Rsa::Rsa() : rsa_(false) {} -Rsa::Rsa(const EVP_PKEY* pkey) : Rsa() { - const int type = EVPKeyPointer::id(pkey); - if (type != EVP_PKEY_RSA && type != EVP_PKEY_RSA_PSS) return; - rsa_pss_ = type == EVP_PKEY_RSA_PSS; +Rsa::Rsa(const EVP_PKEY* pkey, Selection selection) : Rsa() { + rsa_pss_ = EVPKeyPointer::isA(pkey, KeyAlgorithm::RSA_PSS); + if (!EVPKeyPointer::isA(pkey, KeyAlgorithm::RSA) && !rsa_pss_) return; if (!GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_N, &n_) || !GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_E, &e_)) { return; } + if (rsa_pss_) { + MarkPopErrorOnReturn pop_errors; + PssParams params; + if (ReadRsaPssParams(pkey, ¶ms)) pss_params_ = params; + } + if (selection == Selection::Public) { + rsa_ = true; + return; + } if (!GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_D, &d_) || !GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_FACTOR1, &p_) || !GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_FACTOR2, &q_) || @@ -6420,12 +6865,6 @@ Rsa::Rsa(const EVP_PKEY* pkey) : Rsa() { other_prime_infos_.push_back(std::move(info)); } - if (type == EVP_PKEY_RSA_PSS) { - MarkPopErrorOnReturn pop_errors; - PssParams params; - if (ReadRsaPssParams(pkey, ¶ms)) pss_params_ = params; - } - rsa_ = true; } #else @@ -6433,6 +6872,14 @@ Rsa::Rsa() : rsa_(nullptr) {} Rsa::Rsa(OSSL3_CONST RSA* ptr) : rsa_(ptr) {} #endif +Rsa Rsa::PublicOnly(const EVPKeyPointer& key) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return Rsa(key.get(), Selection::Public); +#else + return key; +#endif +} + const Rsa::PublicKey Rsa::getPublicKey() const { #if NCRYPTO_USE_OPENSSL3_PROVIDER if (!rsa_) return {}; @@ -6487,6 +6934,26 @@ const Rsa::OtherPrimeInfos Rsa::getOtherPrimeInfos() const { return infos; } +bool Rsa::checkPrimeProduct() const { + const auto pub = getPublicKey(); + const auto priv = getPrivateKey(); + if (pub.n == nullptr || priv.p == nullptr || priv.q == nullptr) return false; + auto product = BignumPointer::New(); + BignumCtxPointer ctx(BN_CTX_new()); + if (!product || !ctx || + BN_mul(product.get(), priv.p, priv.q, ctx.get()) != 1) { + return false; + } + for (const auto& info : getOtherPrimeInfos()) { + auto next = BignumPointer::New(); + if (!next || BN_mul(next.get(), product.get(), info.r, ctx.get()) != 1) { + return false; + } + product = std::move(next); + } + return BN_cmp(product.get(), pub.n) == 0; +} + const std::optional Rsa::getPssParams() const { #if NCRYPTO_USE_OPENSSL3_PROVIDER return pss_params_; @@ -6821,7 +7288,7 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) { Ec::Ec() : ec_(nullptr), pub_(nullptr) {} Ec::Ec(const EVP_PKEY* pkey) : Ec() { - if (EVPKeyPointer::id(pkey) != EVP_PKEY_EC) return; + if (!EVPKeyPointer::isA(pkey, KeyAlgorithm::EC)) return; char group_name[80]; size_t group_name_len = 0; if (EVP_PKEY_get_utf8_string_param(pkey, @@ -6916,6 +7383,182 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } +DataPointer Ec::TryExportPublic(const EVPKeyPointer& key, + point_conversion_form_t form) { + if (!key || form != POINT_CONVERSION_UNCOMPRESSED) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + { + MarkPopErrorOnReturn pop_errors; + size_t length = 0; + if (EVP_PKEY_get_octet_string_param( + key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &length) == 1) { + auto bytes = DataPointer::Alloc(length); + if (bytes && length != 0 && + EVP_PKEY_get_octet_string_param(key.get(), + OSSL_PKEY_PARAM_PUB_KEY, + bytes.get(), + length, + &length) == 1 && + (bytes.get()[0] & ~1) == form) { + return bytes.resize(length); + } + } + } +#endif + return {}; +} + +DataPointer Ec::ExportPrivate(const EVPKeyPointer& key) { + if (!key) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + { + MarkPopErrorOnReturn pop_errors; + BignumPointer priv; + BignumPointer order; + if (GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, &priv) && + GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_EC_ORDER, &order)) { + return priv.encodePadded(order.byteLength()); + } + } +#endif + ECKeyPointer ec(key); + if (!ec || ec.getPrivateKey() == nullptr) return {}; + auto order = BignumPointer::New(); + if (!order || !EC_GROUP_get_order(ec.getGroup(), order.get(), nullptr)) + return {}; + return BignumPointer::EncodePadded(ec.getPrivateKey(), order.byteLength()); +} + +bool Ec::GetKeyComponents(const EVPKeyPointer& key, + BignumPointer* x, + BignumPointer* y, + BignumPointer* priv, + int* degree) { + if (!key) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const int nid = GetCurveId(key); + switch (nid) { + case NID_X9_62_prime256v1: + case NID_secp256k1: + *degree = 256; + break; + case NID_secp384r1: + *degree = 384; + break; + case NID_secp521r1: + *degree = 521; + break; + default: + *degree = 0; + } + if (*degree != 0) { + MarkPopErrorOnReturn pop_errors; + unsigned char x_bytes[66]{}; + unsigned char y_bytes[66]{}; + const size_t width = (*degree + 7) / 8; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_EC_PUB_X, x_bytes, width), + OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_EC_PUB_Y, y_bytes, width), + OSSL_PARAM_construct_end(), + }; + if (EVP_PKEY_get_params(key.get(), params) == 1 && + OSSL_PARAM_modified(¶ms[0]) && OSSL_PARAM_modified(¶ms[1])) { + x->reset(BN_native2bn(x_bytes, width, nullptr)); + y->reset(BN_native2bn(y_bytes, width, nullptr)); + return *x && *y && + (priv == nullptr || + GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, priv)); + } + } +#endif + ECKeyPointer ec(key); + if (!ec || ec.getPublicKey() == nullptr) return false; + *degree = EC_GROUP_get_degree(ec.getGroup()); + x->reset(BN_new()); + y->reset(BN_new()); + if (!*x || !*y || + EC_POINT_get_affine_coordinates( + ec.getGroup(), ec.getPublicKey(), x->get(), y->get(), nullptr) != 1) { + return false; + } + if (priv != nullptr) { + if (ec.getPrivateKey() == nullptr) return false; + priv->reset(BN_dup(ec.getPrivateKey())); + if (!*priv) return false; + } + return true; +} + +int Ec::GetCurveId(const EVPKeyPointer& key) { + if (!key) return NID_undef; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + char name[80]; + size_t length = 0; + if (EVP_PKEY_get_utf8_string_param( + key.get(), OSSL_PKEY_PARAM_GROUP_NAME, name, sizeof(name), &length) != + 1) { + return NID_undef; + } + return GetCurveIdFromName(name); +#else + const EC_KEY* ec = key; + if (ec == nullptr) return NID_undef; + const EC_GROUP* group = EC_KEY_get0_group(ec); + return group == nullptr ? NID_undef : EC_GROUP_get_curve_name(group); +#endif +} + +std::optional Ec::GetCurveName(const EVPKeyPointer& key) { + if (!key) return std::nullopt; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + size_t length = 0; + if (EVP_PKEY_get_utf8_string_param( + key.get(), OSSL_PKEY_PARAM_GROUP_NAME, nullptr, 0, &length) != 1) { + return std::nullopt; + } + std::string name(length, '\0'); + if (EVP_PKEY_get_utf8_string_param(key.get(), + OSSL_PKEY_PARAM_GROUP_NAME, + name.data(), + name.size() + 1, + &length) != 1) { + return std::nullopt; + } + name.resize(length); + // Preserve the public short names for the curves OpenSSL already knows. + const int nid = GetCurveIdFromName(name.c_str()); + return nid == NID_undef ? name : std::string(OBJ_nid2sn(nid)); +#else + const int nid = GetCurveId(key); + if (nid == NID_undef) return std::nullopt; + return std::string(OBJ_nid2sn(nid)); +#endif +} + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +namespace { +bool IsAvailableEcGroup(const char* name) { + MarkPopErrorOnReturn mark; + auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::EC); + return ctx.initForParamgen() && + ctx.setEcParameters(name, OPENSSL_EC_NAMED_CURVE) && ctx.paramgen(); +} +} // namespace +#endif + +bool Ec::CheckCurveName(const char* name) { + if (name == nullptr) return false; + if (GetCurveIdFromName(name) != NID_undef) return true; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // Keep invalid names a synchronous argument error. Generation contexts can + // defer rejecting a group until parameter generation. Use the same parameter + // generation path as key generation without requiring parameter import. + return IsAvailableEcGroup(name); +#else + return false; +#endif +} + int Ec::GetCurveIdFromName(const char* name) { int nid = EC_curve_nist2nid(name); if (nid == NID_undef) { @@ -6924,14 +7567,26 @@ int Ec::GetCurveIdFromName(const char* name) { return nid; } +const KeyAlgorithm* Ec::GetNamedKeyAlgorithm(const char* name) { + // Preserve the aliases accepted by the historical namedCurve option. + const int nid = GetCurveIdFromName(name); + if (nid == NID_undef) return nullptr; + const auto* algorithm = KeyAlgorithm::FromName(OBJ_nid2sn(nid)); + return algorithm != nullptr && algorithm->isOkp() ? algorithm : nullptr; +} + bool Ec::GetCurves(Ec::GetCurveCallback callback) { const size_t count = EC_get_builtin_curves(nullptr, 0); std::vector curves(count); if (EC_get_builtin_curves(curves.data(), count) != count) { return false; } - for (auto curve : curves) { - if (!callback(OBJ_nid2sn(curve.nid))) return false; + for (const auto& curve : curves) { + const char* name = OBJ_nid2sn(curve.nid); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!IsAvailableEcGroup(name)) continue; +#endif + if (!callback(name)) return false; } return true; } @@ -7067,18 +7722,17 @@ std::optional EVPMDCtxPointer::signInitWithContext( return std::nullopt; } return ctx; -#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) +#elif NCRYPTO_USE_OPENSSL3_PROVIDER EVP_PKEY_CTX* ctx = nullptr; -#ifdef OSSL_SIGNATURE_PARAM_INSTANCE // Ed25519 requires the INSTANCE param to switch into Ed25519ctx mode. // Without it, OpenSSL silently ignores the context string. - if (key.id() == EVP_PKEY_ED25519) { + if (key.isA(KeyAlgorithm::ED25519)) { const OSSL_PARAM params[] = { OSSL_PARAM_construct_utf8_string( - OSSL_SIGNATURE_PARAM_INSTANCE, const_cast("Ed25519ctx"), 0), + kSignatureInstance, const_cast("Ed25519ctx"), 0), OSSL_PARAM_construct_octet_string( - OSSL_SIGNATURE_PARAM_CONTEXT_STRING, + kSignatureContextString, const_cast(context_string.data), context_string.len), OSSL_PARAM_END}; @@ -7089,11 +7743,10 @@ std::optional EVPMDCtxPointer::signInitWithContext( } return ctx; } -#endif // OSSL_SIGNATURE_PARAM_INSTANCE const OSSL_PARAM params[] = { OSSL_PARAM_construct_octet_string( - OSSL_SIGNATURE_PARAM_CONTEXT_STRING, + kSignatureContextString, const_cast(context_string.data), context_string.len), OSSL_PARAM_END}; @@ -7122,18 +7775,17 @@ std::optional EVPMDCtxPointer::verifyInitWithContext( return std::nullopt; } return ctx; -#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) +#elif NCRYPTO_USE_OPENSSL3_PROVIDER EVP_PKEY_CTX* ctx = nullptr; -#ifdef OSSL_SIGNATURE_PARAM_INSTANCE // Ed25519 requires the INSTANCE param to switch into Ed25519ctx mode. // Without it, OpenSSL silently ignores the context string. - if (key.id() == EVP_PKEY_ED25519) { + if (key.isA(KeyAlgorithm::ED25519)) { const OSSL_PARAM params[] = { OSSL_PARAM_construct_utf8_string( - OSSL_SIGNATURE_PARAM_INSTANCE, const_cast("Ed25519ctx"), 0), + kSignatureInstance, const_cast("Ed25519ctx"), 0), OSSL_PARAM_construct_octet_string( - OSSL_SIGNATURE_PARAM_CONTEXT_STRING, + kSignatureContextString, const_cast(context_string.data), context_string.len), OSSL_PARAM_END}; @@ -7144,11 +7796,10 @@ std::optional EVPMDCtxPointer::verifyInitWithContext( } return ctx; } -#endif // OSSL_SIGNATURE_PARAM_INSTANCE const OSSL_PARAM params[] = { OSSL_PARAM_construct_octet_string( - OSSL_SIGNATURE_PARAM_CONTEXT_STRING, + kSignatureContextString, const_cast(context_string.data), context_string.len), OSSL_PARAM_END}; @@ -7672,7 +8323,7 @@ std::pair X509Name::Iterator::operator*() const { Dsa::Dsa() : dsa_(false) {} Dsa::Dsa(const EVP_PKEY* pkey) : Dsa() { - if (EVPKeyPointer::id(pkey) != EVP_PKEY_DSA) return; + if (!EVPKeyPointer::isA(pkey, KeyAlgorithm::DSA)) return; if (!GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_P, &p_) || !GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_Q, &q_)) { return; @@ -7818,37 +8469,25 @@ const Digest Digest::Fetch(const char* name) { // ============================================================================ // KEM Implementation #if OPENSSL_WITH_KEM -#if OPENSSL_WITH_KEM_OPERATION_PARAM +#if NCRYPTO_USE_OPENSSL3_PROVIDER bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { - const char* operation = nullptr; - - switch (EVP_PKEY_id(key.get())) { - case EVP_PKEY_RSA: - operation = OSSL_KEM_PARAM_OPERATION_RSASVE; - break; -#if OPENSSL_WITH_OPENSSL_DHKEM - case EVP_PKEY_EC: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: - operation = OSSL_KEM_PARAM_OPERATION_DHKEM; - break; -#endif - default: - unreachable(); - } - - if (operation != nullptr) { - OSSL_PARAM params[] = { - OSSL_PARAM_utf8_string( - OSSL_KEM_PARAM_OPERATION, const_cast(operation), 0), - OSSL_PARAM_END}; + const OSSL_PARAM* settable = EVP_PKEY_CTX_settable_params(ctx); + if (settable == nullptr || + OSSL_PARAM_locate_const(settable, "operation") == nullptr) + return true; - if (EVP_PKEY_CTX_set_params(ctx, params) <= 0) { - return false; - } + const char* operation = nullptr; + if (key.isA(KeyAlgorithm::RSA)) { + operation = "RSASVE"; + } else if (key.isA(KeyAlgorithm::EC) || key.isA(KeyAlgorithm::X25519) || + key.isA(KeyAlgorithm::X448)) { + operation = "DHKEM"; } - - return true; + if (operation == nullptr) return true; + OSSL_PARAM params[] = { + OSSL_PARAM_utf8_string("operation", const_cast(operation), 0), + OSSL_PARAM_END}; + return EVP_PKEY_CTX_set_params(ctx, params) > 0; } #endif @@ -7863,7 +8502,7 @@ std::optional KEM::Encapsulate( return std::nullopt; } -#if OPENSSL_WITH_KEM_OPERATION_PARAM +#if NCRYPTO_USE_OPENSSL3_PROVIDER if (!SetOperationParameter(ctx.get(), public_key)) { return std::nullopt; } @@ -7904,7 +8543,7 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return {}; } -#if OPENSSL_WITH_KEM_OPERATION_PARAM +#if NCRYPTO_USE_OPENSSL3_PROVIDER if (!SetOperationParameter(ctx.get(), private_key)) { return {}; } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 6b1edceed06..13444e226b6 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -120,53 +121,6 @@ #define OPENSSL_WITH_AES_GCM_SIV 0 #endif -#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2) -#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 1 -#else -#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 0 -#endif - -#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 2) -#define OPENSSL_WITH_OPENSSL_DHKEM 1 -#else -#define OPENSSL_WITH_OPENSSL_DHKEM 0 -#endif - -#if OPENSSL_WITH_KEM && !defined(OPENSSL_IS_BORINGSSL) && \ - !OPENSSL_VERSION_PREREQ(3, 5) -#define OPENSSL_WITH_KEM_OPERATION_PARAM 1 -#else -#define OPENSSL_WITH_KEM_OPERATION_PARAM 0 -#endif - -// Post-quantum cryptography support. Keep these explicit so code can -// distinguish provider API shape from the available algorithm set. -#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 5) -#define OPENSSL_WITH_OPENSSL_PQC 1 -#else -#define OPENSSL_WITH_OPENSSL_PQC 0 -#endif - -#ifdef OPENSSL_IS_BORINGSSL -#define OPENSSL_WITH_BORINGSSL_PQC 1 -#else -#define OPENSSL_WITH_BORINGSSL_PQC 0 -#endif - -#define OPENSSL_WITH_PQC \ - (OPENSSL_WITH_OPENSSL_PQC || OPENSSL_WITH_BORINGSSL_PQC) -#define OPENSSL_WITH_PQC_ML_KEM_512 OPENSSL_WITH_OPENSSL_PQC -#define OPENSSL_WITH_PQC_SLH_DSA OPENSSL_WITH_OPENSSL_PQC - -#if OPENSSL_WITH_OPENSSL_PQC -#define EVP_PKEY_ML_KEM_512 NID_ML_KEM_512 -#define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 -#define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 -#elif OPENSSL_WITH_BORINGSSL_PQC -#define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 -#define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 -#endif - #if OPENSSL_VERSION_PREREQ(3, 0) #define OSSL3_CONST const #else @@ -366,6 +320,7 @@ class DataPointer; class DHPointer; class ECKeyPointer; class EVPKeyPointer; +class KeyAlgorithm; class MacCache; class EVPMacCtxPointer; class EVPMacPointer; @@ -587,6 +542,7 @@ class Cipher final { unsigned char* iv) const; static const Cipher FromName(const char* name, CipherCache* cache = nullptr); + static const Cipher FromNameForKeyEncoding(const char* name); static const Cipher FromNid(int nid, CipherCache* cache = nullptr); static const Cipher FromCtx(const CipherCtxPointer& ctx); @@ -694,8 +650,10 @@ class Dsa final { class Rsa final { public: Rsa(); + enum class Selection { Public, Private }; + static Rsa PublicOnly(const EVPKeyPointer& key); #if NCRYPTO_USE_OPENSSL3_PROVIDER - explicit Rsa(const EVP_PKEY* pkey); + explicit Rsa(const EVP_PKEY* pkey, Selection selection = Selection::Private); #else Rsa(OSSL3_CONST RSA* rsa); #endif @@ -750,6 +708,8 @@ class Rsa final { const PublicKey getPublicKey() const; const PrivateKey getPrivateKey() const; const OtherPrimeInfos getOtherPrimeInfos() const; + // Check that n is the product of all private-key prime factors. + bool checkPrimeProduct() const; const std::optional getPssParams() const; bool setPublicKey(BignumPointer&& n, BignumPointer&& e); @@ -812,6 +772,18 @@ class Ec final { #endif static int GetCurveIdFromName(const char* name); + static int GetCurveId(const EVPKeyPointer& key); + static std::optional GetCurveName(const EVPKeyPointer& key); + static bool CheckCurveName(const char* name); + static DataPointer TryExportPublic(const EVPKeyPointer& key, + point_conversion_form_t form); + static DataPointer ExportPrivate(const EVPKeyPointer& key); + static bool GetKeyComponents(const EVPKeyPointer& key, + BignumPointer* x, + BignumPointer* y, + BignumPointer* priv, + int* degree); + static const KeyAlgorithm* GetNamedKeyAlgorithm(const char* name); using GetCurveCallback = std::function; static bool GetCurves(GetCurveCallback callback); @@ -1085,6 +1057,79 @@ class CipherCtxPointer final { DeleteFnPtr ctx_; }; +// Known key algorithms are identified by provider names, never synthetic NIDs. +// Descriptors have static lifetime; availability is queried from the backend. +class KeyAlgorithm final { + public: + static const KeyAlgorithm RSA; + static const KeyAlgorithm RSA_PSS; + static const KeyAlgorithm DSA; + static const KeyAlgorithm DH; + static const KeyAlgorithm EC; + static const KeyAlgorithm ED25519; + static const KeyAlgorithm ED448; + static const KeyAlgorithm X25519; + static const KeyAlgorithm X448; + static const KeyAlgorithm SM2; + static const KeyAlgorithm ML_DSA_44; + static const KeyAlgorithm ML_DSA_65; + static const KeyAlgorithm ML_DSA_87; + static const KeyAlgorithm ML_KEM_512; + static const KeyAlgorithm ML_KEM_768; + static const KeyAlgorithm ML_KEM_1024; + static const KeyAlgorithm SLH_DSA_SHA2_128F; + static const KeyAlgorithm SLH_DSA_SHA2_128S; + static const KeyAlgorithm SLH_DSA_SHA2_192F; + static const KeyAlgorithm SLH_DSA_SHA2_192S; + static const KeyAlgorithm SLH_DSA_SHA2_256F; + static const KeyAlgorithm SLH_DSA_SHA2_256S; + static const KeyAlgorithm SLH_DSA_SHAKE_128F; + static const KeyAlgorithm SLH_DSA_SHAKE_128S; + static const KeyAlgorithm SLH_DSA_SHAKE_192F; + static const KeyAlgorithm SLH_DSA_SHAKE_192S; + static const KeyAlgorithm SLH_DSA_SHAKE_256F; + static const KeyAlgorithm SLH_DSA_SHAKE_256S; + + // Look up a canonical name case-insensitively, including unavailable + // algorithms. + static const KeyAlgorithm* FromName(const char* name); + using Callback = std::function; + static void ForEachPqc(Callback callback); + + const char* name() const { return name_; } + const char* keyTypeName() const { + return key_type_name_[0] == '\0' ? nullptr : key_type_name_.data(); + } + bool isRsa() const; + bool isAvailable() const; + bool isPqc() const; + bool isOkp() const; + bool isOneShot() const; + bool supportsRawPublic() const; + bool supportsRawPrivate() const; + size_t seedSize() const; + + private: + enum class Family { Other, EdDSA, XDH, MLDSA, MLKEM, SLHDSA }; + static constexpr size_t kMaxKeyTypeNameLength = 32; + template + constexpr KeyAlgorithm(const char (&name)[N], + Family family, + bool has_key_type = true) + : name_(name), family_(family) { + static_assert(N <= kMaxKeyTypeNameLength); + if (has_key_type) { + for (size_t i = 0; i < N; i++) { + key_type_name_[i] = + name[i] >= 'A' && name[i] <= 'Z' ? name[i] + ('a' - 'A') : name[i]; + } + } + } + const char* name_; + std::array key_type_name_{}; + Family family_; +}; + class EVPKeyCtxPointer final { public: EVPKeyCtxPointer(); @@ -1109,6 +1154,7 @@ class EVPKeyCtxPointer final { bool setDhParameters(int prime_size, uint32_t generator); bool setDsaParameters(uint32_t bits, std::optional q_bits); bool setEcParameters(int curve, int encoding); + bool setEcParameters(const char* group_name, int encoding); bool setRsaOaepMd(const Digest& md); bool setRsaMgf1Md(const Digest& md); @@ -1147,7 +1193,8 @@ class EVPKeyCtxPointer final { int initForSign(); static EVPKeyCtxPointer New(const EVPKeyPointer& key); - static EVPKeyCtxPointer NewFromID(int id); + static EVPKeyCtxPointer NewFromName(const char* name); + static EVPKeyCtxPointer NewFromAlgorithm(const KeyAlgorithm& algorithm); private: DeleteFnPtr ctx_; @@ -1156,14 +1203,12 @@ class EVPKeyCtxPointer final { class EVPKeyPointer final { public: static EVPKeyPointer New(); - static EVPKeyPointer NewRawPublic(int id, + static EVPKeyPointer NewRawPublic(const KeyAlgorithm& algorithm, const Buffer& data); - static EVPKeyPointer NewRawPrivate(int id, + static EVPKeyPointer NewRawPrivate(const KeyAlgorithm& algorithm, const Buffer& data); -#if OPENSSL_WITH_PQC - static EVPKeyPointer NewRawSeed(int id, + static EVPKeyPointer NewRawSeed(const KeyAlgorithm& algorithm, const Buffer& data); -#endif static EVPKeyPointer NewDH(DHPointer&& dh); #if NCRYPTO_USE_OPENSSL3_PROVIDER static EVPKeyPointer NewRSA(const Rsa& rsa); @@ -1211,7 +1256,7 @@ class EVPKeyPointer final { using PublicKeyEncodingConfig = AsymmetricKeyEncodingConfig; struct PrivateKeyEncodingConfig : public AsymmetricKeyEncodingConfig { - const EVP_CIPHER* cipher = nullptr; + Cipher cipher; std::optional passphrase = std::nullopt; PrivateKeyEncodingConfig() = default; PrivateKeyEncodingConfig(bool output_key_object, @@ -1268,11 +1313,19 @@ class EVPKeyPointer final { void reset(EVP_PKEY* pkey = nullptr); EVP_PKEY* release(); - static int id(const EVP_PKEY* key); - static int base_id(const EVP_PKEY* key); - - int id() const; - int base_id() const; + static bool isA(const EVP_PKEY* key, const char* name); + bool isA(const char* name) const; + static bool isA(const EVP_PKEY* key, const KeyAlgorithm& algorithm); + bool isA(const KeyAlgorithm& algorithm) const; + // Resolve a known algorithm without caching key or provider state. + const KeyAlgorithm* getAlgorithm() const; + // Stable public key-type name, or nullptr for an unsupported key type. + const char* getKeyTypeName() const; + bool supportsRawPublic() const; + bool supportsRawPrivate() const; + bool supportsContextString() const; + bool hasSmallOrderEdDsaPoint( + const Buffer& signature) const; int bits() const; size_t size() const; @@ -1282,9 +1335,22 @@ class EVPKeyPointer final { DataPointer rawPrivateKey() const; BIOPointer derPublicKey() const; -#if OPENSSL_WITH_PQC - DataPointer rawSeed() const; -#endif + enum class RawExportError { UNSUPPORTED_KEY_TYPE, MISSING_SEED, FAILED }; + Result rawSeed() const; + + struct RawJwkData { + const KeyAlgorithm* algorithm = nullptr; + DataPointer public_key; + DataPointer private_key; + }; + // Raw JWK material for OKP and AKP keys. Private bytes use the JWK + // representation (a seed for ML-DSA/ML-KEM, a raw private key otherwise). + Result exportRawJwk(bool include_private) const; + static EVPKeyPointer NewRawJwk( + const KeyAlgorithm& algorithm, + const Buffer& public_key, + const std::optional>& private_key = + std::nullopt); Result writePrivateKey( const PrivateKeyEncodingConfig& config) const; @@ -1300,9 +1366,10 @@ class EVPKeyPointer final { operator Rsa() const; operator Dsa() const; + static bool isRsaVariant(const EVP_PKEY* key); bool isRsaVariant() const; - bool isOneShotVariant() const; bool isSigVariant() const; + bool mayBeSM2() const; bool validateDsaParameters() const; private: @@ -1764,6 +1831,7 @@ class ECKeyPointer final { bool setPublicKeyRaw(const BignumPointer& x, const BignumPointer& y); bool generate(); bool checkKey() const; + bool checkPrivateKey() const; DataPointer computeSecret(const ECPointPointer& peer) const; const EC_GROUP* getGroup() const; @@ -2069,6 +2137,10 @@ class EnginePointer final { // FIPS bool isFipsEnabled(); +// Configure seed-preserving PQC private-key encoding when the backend supports +// it. +void ConfigurePqcEncoding(); + bool setFipsEnabled(bool enabled, CryptoErrorList* errors); uint64_t getFipsStateGeneration(); @@ -2105,8 +2177,29 @@ Buffer ExportChallenge(const char* input, size_t length); // ============================================================================ // KDF +#if NCRYPTO_USE_OPENSSL3_PROVIDER +class KDF final { + public: + KDF() = default; + KDF(KDF&&) noexcept = default; + KDF& operator=(KDF&&) noexcept = default; + NCRYPTO_DISALLOW_COPY(KDF) + + inline operator bool() const { return kdf_ != nullptr; } + + static KDF Fetch(const char* algorithm, OSSL_LIB_CTX* libctx = nullptr); + + // Each derivation uses a fresh context. A null output can be used for + // parameter validation by KDFs that support it, such as scrypt. + bool derive(const Buffer& out, const OSSL_PARAM* params) const; + + private: + explicit KDF(EVP_KDF* kdf); + DeleteFnPtr kdf_; +}; +#endif + const EVP_MD* getDigestByName(const char* name); -const EVP_CIPHER* getCipherByName(const char* name); // Verify that the specified HKDF output length is valid for the given digest. // The maximum length for HKDF output for a given digest is 255 times the @@ -2180,7 +2273,7 @@ class KEM final { const Buffer& ciphertext); private: -#if OPENSSL_WITH_KEM_OPERATION_PARAM +#if NCRYPTO_USE_OPENSSL3_PROVIDER static bool SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key); #endif diff --git a/deps/openssl/openssl_asm.gypi b/deps/openssl/openssl_asm.gypi index d1b3fbbcd6a..6ad60656807 100644 --- a/deps/openssl/openssl_asm.gypi +++ b/deps/openssl/openssl_asm.gypi @@ -77,5 +77,8 @@ # Other architectures don't use assembly 'includes': ['config/archs/linux-x86_64/asm/openssl.gypi'], }], + [ 'clang==1 and OS!="win"', { + 'cflags': [ '-Wno-unused-command-line-argument' ], + }], ], } diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 46cf1685980..f41dc045fc2 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -3013,8 +3013,7 @@ changes: Checks whether the certificate matches the given email address. If the `'subject'` option is undefined or set to `'default'`, the certificate -subject is only considered if the subject alternative name extension either does -not exist or does not contain any email addresses. +subject is considered according to OpenSSL's default behavior. If the `'subject'` option is set to `'always'` and if the subject alternative name extension either does not exist or does not contain a matching email @@ -3058,9 +3057,7 @@ comparisons are case-insensitive, the returned subject name might also differ from the given `name` in capitalization. If the `'subject'` option is undefined or set to `'default'`, the certificate -subject is only considered if the subject alternative name extension either does -not exist or does not contain any DNS names. This behavior is consistent with -[RFC 2818][] ("HTTP Over TLS"). +subject is considered according to OpenSSL's default behavior. If the `'subject'` option is set to `'always'` and if the subject alternative name extension either does not exist or does not contain a matching DNS name, @@ -5453,6 +5450,39 @@ const derivedKey = hkdfSync('sha512', 'key', 'salt', 'info', 64); console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653' ``` +### `crypto.parsePKCS12(bundle[, options])` + + + +* `bundle` {ArrayBuffer|Buffer|TypedArray|DataView} A DER-encoded PKCS#12 + (`.p12` or `.pfx`) bundle. +* `options` {Object} + * `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} The passphrase + protecting the bundle. Omitting this option is equivalent to passing `''`. +* Returns: {Object} + * `privateKey` {KeyObject|null} The first private key in the bundle, or + `null` if none is present. + * `certificate` {X509Certificate|null} The certificate matching `privateKey`, + or `null` if no matching certificate is present. + * `additionalCertificates` {X509Certificate\[]} All other certificates in + the bundle. If there is no private key, this contains all certificates. + May be empty. + +Parses a PKCS#12 bundle, commonly stored with a `.p12` or `.pfx` extension, +and returns its private key and certificates. + +```mjs +import { parsePKCS12 } from 'node:crypto'; +import { readFileSync } from 'node:fs'; + +const { privateKey, certificate, additionalCertificates } = parsePKCS12( + readFileSync('bundle.p12'), + { passphrase: 'secret' }, +); +``` + ### `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)` + +* `options` {Object} + * `chunks` {number} The number of histogram chunks retained. Must be an + integer between `1` and `1024`. + * `chunkDuration` {number} The duration of each chunk in milliseconds. Must + be an integer between `1` and `18_446_744_073_709`. Exactly one of + `chunkDuration` and `recordsPerChunk` must be specified. + * `recordsPerChunk` {number} The number of calls to `record()` assigned to + each chunk. Must be an integer between `1` and `Number.MAX_SAFE_INTEGER`. + Exactly one of `chunkDuration` and `recordsPerChunk` must be specified. + * `lowest` {number|bigint} The lowest discernible value. Must be an integer + value greater than `0`. **Default:** `1`. + * `highest` {number|bigint} The highest recordable value. Must be an integer + value that is equal to or greater than two times `lowest`. + **Default:** `Number.MAX_SAFE_INTEGER`. + * `figures` {number} The number of accuracy digits. Must be an integer between + `1` and `5`. **Default:** `3`. +* Returns: {SlidingWindowHistogram} + +Creates a {SlidingWindowHistogram} that retains the latest `chunks` histogram +chunks. Rotation is lazy and does not create a timer. Time-based rotation is +evaluated when `record()` or `snapshot()` is called. Count-based rotation is +evaluated when `record()` is called. + +One histogram chunk is allocated during construction. Additional chunks are +allocated lazily. The maximum native memory used by the window scales with +`chunks` and with the `lowest`, `highest`, and `figures` histogram options. + +The window boundary has chunk-level precision. With `N` chunks of duration +`D`, a recorded value is retained for between `(N - 1) * D` and `N * D` +milliseconds. Once a count-based window is populated, it retains between +`(N - 1) * C + 1` and `N * C` recording attempts, where `C` is +`recordsPerChunk`. Recording attempts which exceed `highest` are included when +determining count-based rotation. + +```js +const { createSlidingWindowHistogram } = require('node:perf_hooks'); + +const window = createSlidingWindowHistogram({ + chunks: 6, + chunkDuration: 10_000, +}); + +window.record(20_000_000); + +// Materialize the current window as an independent Histogram. +const snapshot = window.snapshot(); +console.log(snapshot.percentile(99)); +``` + ## `perf_hooks.importHistogram(data)` + +* `options` {Object} + * `bins` {number} The number of equal-probability density bins to return. + Must be between 1 and 1000. Cannot be used with `probabilities`. + **Default:** `100`. + * `probabilities` {number\[]} Custom probability boundaries. The array must + contain between 2 and 1001 strictly increasing values, start with `0`, and + end with `1`. Cannot be used with `bins`. + * `dequantize` {string} Controls whether repeated bucket values are spread + deterministically over their equivalent-value ranges. May be `'none'`, + `'hdr'`, or `'all'`. **Default:** `'hdr'`. + * `cache` {boolean} When `true`, retains the expanded histogram snapshot for + reuse by subsequent calls with `cache: true`. The snapshot is invalidated + when the histogram is modified. **Default:** `false`. +* Returns: {Promise} Fulfills with an {Object} containing: + * `probabilities` {Float64Array} The probability boundaries used by the + estimate. + * `quantiles` {Float64Array} The quantiles at the probability boundaries. + * `densities` {Float64Array} The density within each quantile interval. + * `count` {bigint} The number of values in the histogram snapshot. + * `bucketCount` {number} The number of occupied HDR buckets. + * `corrections` {number} The number of non-monotonic floating-point results + that were clamped to the preceding quantile. + * `dequantize` {string} The selected dequantization mode. + +Returns a quantile-respectful density estimate based on the Harrell-Davis +quantile estimator. By default, `bins` generates equal probability boundaries. +The `probabilities` option can instead focus the estimate on regions such as +p90, p99, p99.9, and p99.99. The density for interval `i` contains probability +mass `probabilities[i + 1] - probabilities[i]`. The histogram is snapshotted +when the method is called. Snapshot expansion and the estimate are calculated +in the libuv thread pool. Highly concentrated beta weights use a second-order +asymptotic approximation to avoid numerical convergence loss at large sample +counts. + +Setting `cache` to `true` avoids repeating snapshot capture and expansion when +several estimates are requested from an unchanged histogram. The retained +snapshot uses memory proportional to the number of occupied HDR buckets and is +released when the histogram is next modified. + +QRDE temporarily uses approximately one additional HDR count array plus 32 +bytes per occupied bucket. With `cache: true`, the expanded 32-byte-per-bucket +snapshot remains allocated. The following estimates use `lowest: 1` and +`highest: Number.MAX_SAFE_INTEGER` and exclude allocator and JavaScript object +overhead: + +| `figures` | Histogram | Maximum expanded snapshot | Peak cache-miss QRDE | +| --------- | --------: | ------------------------: | -------------------: | +| 1 | 6.3 KiB | 25 KiB | 31 KiB | +| 2 | 47 KiB | 188 KiB | 235 KiB | +| 3 | 352 KiB | 1.4 MiB | 1.7 MiB | +| 4 | 5.0 MiB | 20 MiB | 25 MiB | +| 5 | 37 MiB | 148 MiB | 185 MiB | + +The maximum snapshot column assumes every representable bucket is occupied. +Lower `highest` values reduce histogram and temporary copy sizes. Concurrent +calls that miss the cache each require their own temporary copy and expanded +snapshot. + +HDR histograms aggregate observations into equivalent-value buckets. The +`'hdr'` dequantization mode models repeated values in buckets wider than one +unit as a continuous uniform distribution over the bucket resolution. This +reduces density artifacts introduced by HDR quantization while preserving +repeated unit-resolution values as point masses. The `'all'` mode also +dequantizes repeated unit-resolution values. Use `'none'` to calculate the +grouped Harrell-Davis estimator using bucket midpoints directly. + +An empty histogram returns the requested `probabilities` but produces empty +`quantiles` and `densities` arrays. A non-dequantized interval whose quantile +boundaries are equal has an infinite density. + ### `histogram.reset()` + +Records values into a lazily rotated ring of histogram chunks. Instances are +created using [`perf_hooks.createSlidingWindowHistogram()`][] and cannot be +constructed directly. A `SlidingWindowHistogram` does not extend {Histogram}; +call `snapshot()` to materialize the current window as a {Histogram}. + +`SlidingWindowHistogram` instances cannot be cloned or transferred through a +{MessagePort}. + +### `slidingWindowHistogram.record(val)` + + + +* `val` {number|bigint} The amount to record. + +Records `val` in the current chunk. For a count-based window, every call that +reaches the native histogram counts toward rotation, including values which +exceed the configured `highest` value. + +### `slidingWindowHistogram.reset()` + + + +Invalidates all chunks in the current window. Allocated chunks are reset +lazily when reused. + +### `slidingWindowHistogram.snapshot()` + + + +* Returns: {Histogram} + +Materializes the current window as a new, independent {Histogram}. Values +recorded or expired after this method returns do not change the returned +histogram. Materialization allocates one histogram and merges every retained +chunk. + ## Histogram analysis examples The `Histogram` class provides statistical analysis methods useful for @@ -3031,6 +3210,7 @@ dns.promises.resolve('localhost'); [`'exit'`]: process.md#event-exit [`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options [`histogram.export()`]: #histogramexport +[`perf_hooks.createSlidingWindowHistogram()`]: #perf_hookscreateslidingwindowhistogramoptions [`perf_hooks.eventLoopUtilization()`]: #perf_hookseventlooputilizationutilization1-utilization2 [`perf_hooks.importHistogram()`]: #perf_hooksimporthistogramdata [`perf_hooks.monitorEventLoopDelay()`]: #perf_hooksmonitoreventloopdelayoptions diff --git a/doc/api/permissions.md b/doc/api/permissions.md index 89cb7713327..2887d9624f6 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -341,6 +341,14 @@ There are constraints you need to know before using this system: to read files before environment initialization. As a result, such flags are not subject to the rules of the Permission Model. The same applies for V8 flags that can be set via runtime through `v8.setFlagsFromString`. +* Files that Node.js itself creates, writes, or reads at a location selected + by an operator flag may not be consistently checked against the Permission + Model, in particular when the flag accepts a template or pattern that + expands to several paths. For example, trace files rotated by + `--trace-event-file-pattern` (`${rotation}`) can be written even when the + expanded path is not covered by `--allow-fs-write`. Because the location is + chosen by the operator, gaps like this are treated as regular bugs rather + than vulnerabilities. Please report them through the regular issue tracker. * OpenSSL engines cannot be requested at runtime when the Permission Model is enabled, affecting the built-in crypto, https, and tls modules. * Run-Time Loadable Extensions cannot be loaded when the Permission Model is diff --git a/doc/api/quic.md b/doc/api/quic.md index a6335bc3527..71d1d7215d0 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -321,10 +321,17 @@ There are two ways to write data to a stream: up front or can be expressed as an iterable. * **Writer** — access [`stream.writer`][] to push data incrementally. The writer exposes synchronous methods (`writeSync()`, `writevSync()`, - `endSync()`) that return immediately, as well as async equivalents - (`write()`, `writev()`, `end()`) that wait for drain when backpressured. + `endSync()`) that return immediately, as well as asynchronous counterparts + (`write()`, `writev()`, `end()`). The asynchronous `write()` and `writev()` + methods use the stream/iter strict backpressure policy: when the write buffer + is full, they reject with `ERR_INVALID_STATE` instead of waiting for capacity. + If a drain is already pending, `end()` waits for it before closing. Check + `writer.canWrite` before writing. To wait for capacity, use `ondrain()` from + `node:stream/iter`, then retry the write. The stream's `onblocked` callback + reports that transport flow control has blocked progress, but does not + signal that writer capacity is available again. `writeSync()` returns `false` when the write buffer is full; the caller - should wait for drain before retrying. + should wait with `ondrain()` before retrying. These two approaches are mutually exclusive for a given stream. @@ -2322,12 +2329,16 @@ The Writer has the following methods: * `writeSync(chunk)` — Synchronous write. Returns `true` if accepted, `false` if flow-controlled. Data is NOT accepted on `false`. -* `write(chunk[, options])` — Async write with drain wait. `options.signal` - is checked at entry but not observed during the write. +* `write(chunk[, options])` — Async write. Rejects with `ERR_INVALID_STATE` + when the stream is flow-controlled rather than waiting for capacity. + `options.signal` is checked at entry but not observed during the write. * `writevSync(chunks)` — Synchronous vectored write. All-or-nothing. -* `writev(chunks[, options])` — Async vectored write. +* `writev(chunks[, options])` — Async vectored write. Rejects with + `ERR_INVALID_STATE` when the stream is flow-controlled rather than waiting + for capacity. * `endSync()` — Synchronous close. Returns total bytes or `-1`. -* `end([options])` — Async close. +* `end([options])` — Async close. If a drain is already pending, waits for it + before closing. * `fail(reason)` — Errors the stream (sends `RESET_STREAM` to peer). When `reason` is a [`QuicError`][], its [`error.errorCode`][] is used as the wire code on the resulting `RESET_STREAM` frame; otherwise @@ -2337,7 +2348,20 @@ The Writer has the following methods: See [`stream.destroy()`][] for a full-stream abort that also resets the readable side via `STOP_SENDING`. * `canWrite` — `true` if writes will be accepted, `false` if at capacity, - or `null` if closed/errored. + or `null` if closed/errored. When `writeSync()` returns `false`, use + `ondrain()` from `node:stream/iter` to wait before retrying. If `ondrain()` + returns `null`, no drain wait is available and the write should not be + retried. + +```mjs +import { ondrain } from 'node:stream/iter'; + +while (!writer.writeSync(chunk)) { + const drain = ondrain(writer); + if (drain === null) break; + await drain; +} +``` The bytes from each `writeSync()` / `writevSync()` / `write()` / `writev()` input chunk are copied into an internal buffer, so the caller's source diff --git a/doc/api/util.md b/doc/api/util.md index d6719b88f4f..f39b7cca265 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -383,6 +383,213 @@ The `--throw-deprecation` command-line flag and `process.throwDeprecation` property take precedence over `--trace-deprecation` and `process.traceDeprecation`. +## `util.debounce(fn, wait[, options])` + + + +* `fn` {Function} The function to debounce. +* `wait` {integer} The number of milliseconds to delay `fn`. +* `options` {Object} + * `leading` {boolean} When `true`, invokes `fn` immediately when a new + debounce window begins. **Default:** `false`. + * `rejectOnCancel` {boolean} When `true`, a call superseded by a later call + rejects with an `AbortError`. **Default:** `false`. + * `signal` {AbortSignal} An `AbortSignal` that cancels pending calls and + prevents future calls when aborted. +* Returns: {Function} The debounced function. + +Creates a function that delays calling `fn` until `wait` milliseconds have +elapsed since the most recent invocation. The debounced function returns a +{Promise} for the value returned by `fn`. If `fn` throws or returns a rejected +promise, the returned promise is rejected with the same reason. + +When the debounced function is called more than once before the delay expires, +`fn` receives the arguments from the most recent call. By default, the promises +from all calls resolve or reject with the result of that invocation. If +`options.rejectOnCancel` is `true`, the promises from superseded calls reject +with an `AbortError` instead. + +When `options.leading` is `true`, the first call in a debounce window invokes +`fn` immediately. Calls made during that window are delayed until `wait` +milliseconds have elapsed since the most recent call. A trailing invocation +only occurs if the debounced function was called again during the window. +The window begins before `fn` is invoked, so recursive calls and calls made +while an asynchronous `fn` is pending are part of the same window if they occur +before the delay expires. This also applies to calls made after a synchronous +`fn` returns but before the delay expires. + +If `options.signal` is aborted, pending and future calls reject with an +`AbortError`, with the signal's reason set as the error's `cause`, and `fn` is +not invoked by those calls. If the signal is already aborted, `debounce()` +throws an `AbortError`. + +The returned function has the following properties: + +* `cancel([reason])` cancels the current debounce window. Its pending promises + reject with an `AbortError`. If provided, `reason` is set as the error's + `cause`. +* `flush()` cancels the delay and invokes `fn` immediately. It has no effect if + no invocation is pending. +* `pending` {Promise|null} is the promise returned by the most recent call in + the current debounce window, or `null` if no invocation is pending. +* `pendingCount` {integer} is the number of calls awaiting the invocation in + the current debounce window. +* `ref()` makes the pending and future timeout keep the Node.js event loop + active. Returns the debounced function. +* `unref()` allows the event loop to exit while a timeout is pending. This also + applies to future timeouts. Returns the debounced function. + +When invoked, `fn` has the debounced function as its `this` value. After a +trailing invocation, a new debounce window can begin even if a promise returned +by `fn` is still pending. The debounced function preserves the `name` and +`length` of `fn`. + +```mjs +import { setTimeout as wait } from 'node:timers/promises'; +import { debounce } from 'node:util'; + +const fn = debounce(async (value) => { + await wait(100); + return value; +}, 50); + +const first = fn(1); +const second = fn(2); + +console.log(await first); // 2 +console.log(await second); // 2 +``` + +A debounced function can be used to trigger an action after a period of +inactivity. Each call resets the timeout: + +```cjs +const { debounce } = require('node:util'); + +const onInactivity = debounce(() => { + console.log('No activity for 5 seconds'); +}, 5_000).unref(); + +process.stdin.on('data', (data) => { + console.log(`Received ${data.length} bytes`); + onInactivity(); +}); + +// Start the initial inactivity timeout. +onInactivity(); +``` + +## `util.throttle(fn, limit, interval[, options])` + + + +* `fn` {Function} The function to throttle. +* `limit` {integer} The maximum number of times to invoke `fn` during an + interval. Must be greater than `0`. +* `interval` {integer} The length of each interval in milliseconds. +* `options` {Object} + * `concurrency` {number} The maximum number of invocations of `fn` whose + return values may be unsettled at once. Must be a positive integer or + `Infinity`. **Default:** `Infinity`. + * `maxPending` {number} The maximum number of calls that may be queued when + `overflow` is `'queue'`. Must be a non-negative integer or `Infinity`. + **Default:** `Infinity`. + * `overflow` {string} Determines how calls exceeding the limit are handled. + **Default:** `'queue'`. + * `'queue'`: Queue calls in the order received. + * `'drop'`: Reject calls immediately without queueing them. + * `signal` {AbortSignal} An `AbortSignal` that cancels pending calls and + prevents future calls when aborted. + * `strict` {boolean} When `true`, ensures that `limit` is not exceeded during + any rolling interval. **Default:** `false`. +* Returns: {Function} The throttled function. + +Creates a function that limits how often `fn` is invoked. By default, calls that +exceed the limit are queued in the order received rather than discarded. The +throttled function returns a {Promise} for the value returned by `fn`. If `fn` +throws or returns a rejected promise, the returned promise is rejected with the +same reason. + +An invocation starts only when both rate and concurrency capacity are +available. Rate capacity is consumed when `fn` starts, not when a call enters +the queue. Concurrency capacity is released when the value returned by `fn` +settles. Non-promise values settle during the next microtask. + +When `options.overflow` is `'drop'`, calls made without available rate or +concurrency capacity are rejected immediately. When `options.overflow` is +`'queue'` and `options.maxPending` calls are already queued, additional calls +are also rejected immediately. `maxPending` has no effect when `overflow` is +`'drop'`. + +In both cases, rejected calls return a promise rejected with an +`ERR_THROTTLED` error. The rejected promise is marked as handled, so ignoring it +does not emit an `'unhandledRejection'` event. Awaiting or explicitly handling +the promise still observes the rejection. Rejected calls do not consume rate +or concurrency capacity, enter the queue, or schedule a timeout. + +By default, the interval begins when the first call in a new window invokes +`fn`. Up to `limit` calls can invoke `fn` during that window. Queued calls are +processed in groups of up to `limit` as each subsequent window begins. This +windowed behavior can result in calls occurring close together at a window +boundary. + +When `options.strict` is `true`, invocation times are tracked individually. +This ensures that no more than `limit` calls begin during any rolling interval, +at the cost of additional bookkeeping. + +If `options.signal` is aborted, pending and future calls reject with an +`AbortError`, with the signal's reason set as the error's `cause`, and `fn` is +not invoked by those calls. If the signal is already aborted, `throttle()` +throws an `AbortError`. + +The returned function has the following properties: + +* `cancel([reason])` cancels all queued calls and resets the current throttle + window. The queued promises reject with an `AbortError`. If provided, + `reason` is set as the error's `cause`. Does not cancel invocations that have + already started. +* `hasImmediateCapacity()` returns `true` if a call made at that moment could + invoke `fn` without being queued or rejected. The check does not reserve + capacity, and the throttled function always checks again when called. It + returns `false` while calls are queued to preserve their order. Callers can + avoid creating a timeout by only calling the throttled function when this + method returns `true`. +* `pending` {Promise|null} is the promise returned by the most recently queued + call, or `null` if no invocation is queued. +* `pendingCount` {integer} is the number of calls awaiting invocation. +* `activeCount` {integer} is the number of invocations whose return values have + not settled. +* `ref()` makes the pending and future timeout keep the Node.js event loop + active. Returns the throttled function. +* `unref()` allows the event loop to exit while a timeout is pending. This also + applies to future timeouts. Returns the throttled function. + +Calls that have already invoked `fn` are not affected by `cancel()` or by an +aborted signal. When invoked, `fn` has the throttled function as its `this` +value. The throttled function preserves the `name` and `length` of `fn`. + +```mjs +import { throttle } from 'node:util'; + +const request = throttle(async (id) => { + const response = await fetch(`https://example.com/items/${id}`); + return response.json(); +}, 2, 1_000); + +// At most two requests begin during each one-second interval. All other calls +// remain queued and retain their original arguments. +const results = await Promise.all([ + request(1), + request(2), + request(3), + request(4), +]); +``` + ## `util.diff(actual, expected)` + +* `promise` {Promise} The promise to mark as handled + +Marks a promise as handled so that unhandled rejections are ignored and are not +reported to the `'unhandledrejection'` event. + ## Class: `util.MIMEType` * `kind` **Default:** `zlib.constants.Z_FULL_FLUSH` for zlib-based streams, - `zlib.constants.BROTLI_OPERATION_FLUSH` for Brotli-based streams. + `zlib.constants.BROTLI_OPERATION_FLUSH` for Brotli-based streams, and + `zlib.constants.ZSTD_e_flush` for Zstd-based streams. * `callback` {Function} Flush pending data. Don't call this frivolously, premature flushes negatively @@ -1088,8 +1089,14 @@ Only applicable to deflate algorithm. added: v0.7.0 --> -Reset the compressor/decompressor to factory defaults. Only applicable to -the inflate and deflate algorithms. +For inflate and deflate streams, reset the compressor/decompressor to factory +defaults. + +For Zstd streams, cancel the current frame and start a new session while +preserving the configured parameters and dictionary. If `pledgedSrcSize` was +configured for a Zstd compressor, it applies again to the next frame. + +Calling `reset()` while a write is in progress throws an `Error`. ## Class: `ZstdOptions` @@ -1117,6 +1124,9 @@ Each Zstd-based class takes an `options` object. All options are optional. * `finishFlush` {integer} **Default:** `zlib.constants.ZSTD_e_end` * `chunkSize` {integer} **Default:** `16 * 1024` * `params` {Object} Key-value object containing indexed [Zstd parameters][]. +* `pledgedSrcSize` {number} Expected total size of the uncompressed input. It + must be a non-negative safe integer and must match the input size when + compression finishes. Only applicable to Zstd compressors. * `maxOutputLength` {integer} Limits output size when using [convenience methods][]. **Default:** [`buffer.kMaxLength`][] * `info` {boolean} If `true`, returns an object with `buffer` and `engine`. **Default:** `false` @@ -1741,6 +1751,8 @@ Compress a chunk of data with [`ZstdCompress`][]. ### `zlib.zstdDecompress(buffer[, options], callback)` +> Stability: 1 - Experimental + + + + + + + + + diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index deec07dda90..b07ce9097bb 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -13,8 +13,6 @@ const [ECDH, X25519] = await Promise.all([ subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']), ]); -const boringSSL = process.features.openssl_is_boringssl; - export const vectors = { 'encrypt': [ [false, 'Invalid'], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 4b44e02d479..67fad15f7f3 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -1,14 +1,16 @@ import * as crypto from 'node:crypto' -import { hasOpenSSL } from '../../common/crypto.js' +import { hasOpenSSL, isBoringSSL } from '../../common/crypto.js' -const boringSSL = process.features.openssl_is_boringssl; +const boringSSL = isBoringSSL; const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2); const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); +const sha3 = crypto.getHashes().includes('sha3-256'); const ocb = hasOpenSSL(3); const kmac = hasOpenSSL(3); +const hybridKems = pqc && (!boringSSL || (sha3 && shake256)); const { subtle } = globalThis.crypto; const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); @@ -94,6 +96,9 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [true, 'ChaCha20-Poly1305'], + [hybridKems, 'MLKEM768-P256'], + [hybridKems, 'MLKEM768-X25519'], + [hybridKems, 'MLKEM1024-P384'], [ocb, { name: 'AES-OCB', length: 128 }], [false, 'Argon2d'], [false, 'Argon2i'], @@ -115,6 +120,9 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [true, 'ChaCha20-Poly1305'], + [hybridKems, 'MLKEM768-P256'], + [hybridKems, 'MLKEM768-X25519'], + [hybridKems, 'MLKEM1024-P384'], [ocb, { name: 'AES-OCB', length: 128 }], [argon2, 'Argon2d'], [argon2, 'Argon2i'], @@ -136,6 +144,9 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [true, 'ChaCha20-Poly1305'], + [hybridKems, 'MLKEM768-P256'], + [hybridKems, 'MLKEM768-X25519'], + [hybridKems, 'MLKEM1024-P384'], [ocb, 'AES-OCB'], [false, 'Argon2d'], [false, 'Argon2i'], @@ -159,6 +170,9 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], + [hybridKems, 'MLKEM768-P256'], + [hybridKems, 'MLKEM768-X25519'], + [hybridKems, 'MLKEM1024-P384'], [false, 'AES-CTR'], [false, 'AES-CBC'], [false, 'AES-GCM'], @@ -218,6 +232,9 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], + [hybridKems, 'MLKEM768-P256'], + [hybridKems, 'MLKEM768-X25519'], + [hybridKems, 'MLKEM1024-P384'], ], 'encapsulateKey': [ [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], @@ -233,6 +250,9 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], [pqc && kmac, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [hybridKems, 'MLKEM768-P256', 'HKDF'], + [hybridKems, 'MLKEM768-X25519', 'HKDF'], + [hybridKems, 'MLKEM1024-P384', 'HKDF'], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 248 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 512 }], @@ -244,6 +264,9 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], + [hybridKems, 'MLKEM768-P256'], + [hybridKems, 'MLKEM768-X25519'], + [hybridKems, 'MLKEM1024-P384'], ], 'decapsulateKey': [ [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], @@ -259,6 +282,9 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], [pqc && kmac, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [hybridKems, 'MLKEM768-P256', 'HKDF'], + [hybridKems, 'MLKEM768-X25519', 'HKDF'], + [hybridKems, 'MLKEM1024-P384', 'HKDF'], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 248 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 512 }], diff --git a/test/fixtures/webcrypto/supports-secure-curves.mjs b/test/fixtures/webcrypto/supports-secure-curves.mjs index a3a84f6c9f9..fa3f1d36521 100644 --- a/test/fixtures/webcrypto/supports-secure-curves.mjs +++ b/test/fixtures/webcrypto/supports-secure-curves.mjs @@ -1,10 +1,10 @@ -import { hasOpenSSL } from '../../common/crypto.js' +import { hasOpenSSL, isBoringSSL } from '../../common/crypto.js' const supportsContext = hasOpenSSL(3, 2); const { subtle } = globalThis.crypto; -const boringSSL = process.features.openssl_is_boringssl; +const boringSSL = isBoringSSL; const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); let X448; diff --git a/test/fixtures/webcrypto/supports-sha3.mjs b/test/fixtures/webcrypto/supports-sha3.mjs index 73f5b778eeb..fe256dcfb16 100644 --- a/test/fixtures/webcrypto/supports-sha3.mjs +++ b/test/fixtures/webcrypto/supports-sha3.mjs @@ -1,6 +1,8 @@ +import { isBoringSSL } from '../../common/crypto.js' + const { subtle } = globalThis.crypto; -const boringSSL = process.features.openssl_is_boringssl; +const boringSSL = isBoringSSL; const RSA_KEY_GEN = { modulusLength: 2048, diff --git a/test/node-api/test_worker_buffer_callback/test-free-called.js b/test/node-api/test_worker_buffer_callback/test-free-called.js index 2a3cc9e47c2..1d2b43daa8c 100644 --- a/test/node-api/test_worker_buffer_callback/test-free-called.js +++ b/test/node-api/test_worker_buffer_callback/test-free-called.js @@ -6,12 +6,13 @@ const { Worker } = require('worker_threads'); const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`); const { getFreeCallCount } = require(binding); +assert.strictEqual(getFreeCallCount(), 0); + // Test that buffers allocated with a free callback through our APIs are // released when a Worker owning it exits. const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true }); -assert.strictEqual(getFreeCallCount(), 0); w.on('exit', common.mustCall(() => { assert.strictEqual(getFreeCallCount(), 1); })); diff --git a/test/parallel/test-abort-controller-any-timeout.js b/test/parallel/test-abort-controller-any-timeout.js index 2d94afaa63d..675be3af703 100644 --- a/test/parallel/test-abort-controller-any-timeout.js +++ b/test/parallel/test-abort-controller-any-timeout.js @@ -1,28 +1,42 @@ +// Flags: --expose-gc 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const { once } = require('node:events'); const { describe, it } = require('node:test'); describe('AbortSignal.any() with timeout signals', () => { it('should abort when the first timeout signal fires', async () => { - const signal = AbortSignal.any([AbortSignal.timeout(9000), AbortSignal.timeout(110000)]); + const signal = AbortSignal.any([ + AbortSignal.timeout(common.platformTimeout(1000)), + AbortSignal.timeout(110000), + ]); + let timeout; const abortPromise = Promise.race([ once(signal, 'abort').then(() => { throw signal.reason; }), - new Promise((resolve) => setTimeout(resolve, 10000)), + new Promise((resolve) => { + timeout = setTimeout(resolve, common.platformTimeout(10000)); + }), ]); - // The promise should be aborted by the 9000ms timeout - await assert.rejects( - () => abortPromise, - { - name: 'TimeoutError', - message: 'The operation was aborted due to timeout' - } - ); + // Collect after this turn so the WeakRefs no longer keep the timeout + // signals alive by themselves. + setImmediate(common.mustCall(() => globalThis.gc())); + + try { + await assert.rejects( + () => abortPromise, + { + name: 'TimeoutError', + message: 'The operation was aborted due to timeout' + } + ); + } finally { + clearTimeout(timeout); + } }); }); diff --git a/test/parallel/test-abortsignal-any-source-gc.mjs b/test/parallel/test-abortsignal-any-source-gc.mjs new file mode 100644 index 00000000000..06fd0c0e29f --- /dev/null +++ b/test/parallel/test-abortsignal-any-source-gc.mjs @@ -0,0 +1,31 @@ +// Flags: --expose-gc + +import '../common/index.mjs'; +import { gcUntil } from '../common/gc.js'; +import assert from 'node:assert/strict'; +import { it } from 'node:test'; + +for (const nested of [false, true]) { + for (const accessor of ['aborted', 'reason', 'throwIfAborted']) { + it(`preserves ${accessor} after source GC (nested: ${nested})`, async () => { + let controller = new AbortController(); + const sourceRef = new WeakRef(controller.signal); + let signal = AbortSignal.any([controller.signal]); + if (nested) signal = AbortSignal.any([signal]); + const reason = { message: 'stop' }; + + controller.abort(reason); + controller = null; + + // Do not observe the composite or attach a listener before source GC. + await gcUntil('source signal is collected', () => sourceRef.deref() === undefined); + + // Exercise each entry point before any other accessor can refresh state. + if (accessor === 'aborted') assert.strictEqual(signal.aborted, true); + if (accessor === 'reason') assert.strictEqual(signal.reason, reason); + assert.throws(() => signal.throwIfAborted(), (err) => err === reason); + assert.strictEqual(signal.aborted, true); + assert.strictEqual(signal.reason, reason); + }); + } +} diff --git a/test/parallel/test-abortsignal-drop-settled-signals.mjs b/test/parallel/test-abortsignal-drop-settled-signals.mjs index 224d65abc70..d4c81a7165d 100644 --- a/test/parallel/test-abortsignal-drop-settled-signals.mjs +++ b/test/parallel/test-abortsignal-drop-settled-signals.mjs @@ -122,16 +122,22 @@ describe('when there is a long-lived signal', () => { }, true); }); - it('does not keep retained dependent signals without listeners', (t, done) => { + it('propagates abort to retained dependent signals without listeners', (t, done) => { const ac = new AbortController(); const retainedSignals = []; - const kDependantSignals = Object.getOwnPropertySymbols(ac.signal).find( - (s) => s.toString() === 'Symbol(kDependantSignals)' - ); function run(iteration) { if (iteration > limit) { - t.assert.strictEqual(ac.signal[kDependantSignals]?.size ?? 0, 0); + const kDependantSignals = Object.getOwnPropertySymbols(ac.signal).find( + (s) => s.toString() === 'Symbol(kDependantSignals)' + ); + t.assert.strictEqual(ac.signal[kDependantSignals].size, limit); + ac.abort('stop'); + for (const signal of retainedSignals) { + t.assert.strictEqual(signal.aborted, true); + t.assert.strictEqual(signal.reason, 'stop'); + t.assert.throws(() => signal.throwIfAborted(), (err) => err === 'stop'); + } done(); return; } @@ -143,6 +149,24 @@ describe('when there is a long-lived signal', () => { run(1); }); + it('drops unreachable dependent signals without listeners', async () => { + const ac = new AbortController(); + const size = () => { + const sym = Object.getOwnPropertySymbols(ac.signal).find( + (s) => s.toString() === 'Symbol(kDependantSignals)' + ); + return ac.signal[sym]?.size ?? 0; + }; + + // Reuse a long-lived source across batches to catch accumulating WeakRefs. + for (let batch = 0; batch < 3; batch++) { + for (let i = 0; i < limit; i++) { + AbortSignal.any([ac.signal]); + } + await gcUntil('unreachable dependents are dropped', () => size() === 0); + } + }); + it('drops observed dependent signals once they are transitively aborted', async () => { const longLived = new AbortController(); const handler = () => {}; diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 80d8bf1b728..3350b67821c 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -278,6 +278,10 @@ test('es6 Maps and Sets', () => { assertDeepAndStrictEqual(new Set([[1, 2], [3, 4]]), new Set([[3, 4], [1, 2]])); assertNotDeepOrStrict(new Set([{ a: 0 }]), new Set([{ a: 1 }])); assertNotDeepOrStrict(new Set([Symbol()]), new Set([Symbol()])); + // A null/primitive member lined up against object-only members in the other + // set must report inequality, not throw on `member.constructor`. + assertNotDeepOrStrict(new Set([null, {}, {}]), new Set([{}, {}, {}])); + assertNotDeepOrStrict(new Set([undefined, {}, {}]), new Set([{}, {}, {}])); { const a = [ 1, 2 ]; @@ -298,6 +302,17 @@ test('es6 Maps and Sets', () => { new Map([[[1], 1], [{}, 2]]), new Map([[[1], 2], [{}, 1]]) ); + // A null/primitive key that lines up with object-only keys in the other map + // must report inequality, not throw on `key.constructor`. Refs: object keys + // of `b` equal in count to `a.size` used to skip the primitive-key handling. + assertNotDeepOrStrict( + new Map([[null, 1], [{}, 2]]), + new Map([[{}, 9], [{}, 9]]) + ); + assertNotDeepOrStrict( + new Map([[undefined, 1], [{}, 2]]), + new Map([[{}, 9], [{}, 9]]) + ); assertNotDeepOrStrict(new Set([1]), [1]); assertNotDeepOrStrict(new Set(), []); diff --git a/test/parallel/test-benchmark-compare.js b/test/parallel/test-benchmark-compare.js new file mode 100644 index 00000000000..29795d6b743 --- /dev/null +++ b/test/parallel/test-benchmark-compare.js @@ -0,0 +1,46 @@ +'use strict'; + +require('../common'); + +const assert = require('node:assert'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { readFileSync } = require('node:fs'); +const path = require('node:path'); +const tmpdir = require('../common/tmpdir'); + +const compare = path.resolve(__dirname, '../../benchmark/compare.js'); + +tmpdir.refresh(); + +const csv = tmpdir.resolve('compare.csv'); +spawnSyncAndExitWithoutError(process.execPath, [ + compare, + '--old', process.execPath, + '--new', process.execPath, + '--runs', '1', + '--filter', 'buffer-compare-offset.js', + '--set', 'method=offset', + '--set', 'size=16', + '--set', 'n=1', + '--no-progress', + '--analyze', + '--csv', csv, + 'buffers', +], { + encoding: 'utf8', + timeout: 30_000, +}, { + stderr: '', + stdout(stdout) { + assert.match(stdout, /confidence\s+improvement\s+accuracy/); + assert.doesNotMatch(stdout, /"binary","filename"/); + }, +}); + +const lines = readFileSync(csv, 'utf8').trim().split('\n'); +const filename = path.join('buffers', 'buffer-compare-offset.js'); +assert.strictEqual(lines[0], + '"binary","filename","configuration","rate","time"'); +assert.strictEqual(lines.length, 3); +assert(lines[1].startsWith(`"old","${filename}",`)); +assert(lines[2].startsWith(`"new","${filename}",`)); diff --git a/test/parallel/test-buffer-tostring.js b/test/parallel/test-buffer-tostring.js index a3dad0146d7..676d2f85f56 100644 --- a/test/parallel/test-buffer-tostring.js +++ b/test/parallel/test-buffer-tostring.js @@ -9,6 +9,13 @@ for (const encoding of ['utf8', 'utf-8', 'ucs2', 'ucs-2', 'ascii', 'latin1', assert.strictEqual(Buffer.from('foo', encoding).toString(encoding), 'foo'); } +// Ignore an incomplete trailing code unit when decoding unaligned UTF-16LE. +for (const size of [514, 516]) { + const buffer = Buffer.alloc(size, 0x61); + assert.strictEqual(buffer.toString('utf16le', 1), + '\u6161'.repeat((size - 1) >>> 1)); +} + // base64 ['base64', 'BASE64'].forEach((encoding) => { assert.strictEqual(Buffer.from('Zm9v', encoding).toString(encoding), 'Zm9v'); diff --git a/test/parallel/test-child-process-exec-encoding.js b/test/parallel/test-child-process-exec-encoding.js index 21ab207fca8..78a9b55b9df 100644 --- a/test/parallel/test-child-process-exec-encoding.js +++ b/test/parallel/test-child-process-exec-encoding.js @@ -41,7 +41,7 @@ if (process.argv[2] === 'child') { [undefined, null, 'buffer', 'invalid'].forEach((encoding) => { run({ encoding }, common.mustCall((stdout, stderr) => { assert(stdout instanceof Buffer); - assert(stdout instanceof Buffer); + assert(stderr instanceof Buffer); assert.strictEqual(stdout.toString(), expectedStdout); assert.strictEqual(stderr.toString(), expectedStderr); })); diff --git a/test/parallel/test-child-process-kill-spawn-error.js b/test/parallel/test-child-process-kill-spawn-error.js new file mode 100644 index 00000000000..5889e169492 --- /dev/null +++ b/test/parallel/test-child-process-kill-spawn-error.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +// Killing a child process that never spawned must not signal the process +// group of the caller. The check runs in a detached child so that a +// regression cannot take the test runner down with it. +const childPath = fixtures.path('child-process-kill-spawn-error.js'); +const child = spawn(process.execPath, [childPath], { detached: true }); + +child.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0); +})); diff --git a/test/parallel/test-child-process-spawn-timeout-clear-on-error.js b/test/parallel/test-child-process-spawn-timeout-clear-on-error.js new file mode 100644 index 00000000000..f674e4ed55f --- /dev/null +++ b/test/parallel/test-child-process-spawn-timeout-clear-on-error.js @@ -0,0 +1,19 @@ +'use strict'; + +// Measures the child's actual exit time, not just its 'error' event. +// The outer spawnSync timeout catches a leaked inner timer. + +const common = require('../common'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); + +const bugStallMs = common.platformTimeout(10000); +const outerTimeoutMs = common.platformTimeout(2000); + +spawnSyncAndExitWithoutError(process.execPath, ['-e', ` + const { spawn } = require('child_process'); + const cp = spawn(process.execPath, ['--version'], { + cwd: '/nonexistent/path/that/should/never/exist', + timeout: ${bugStallMs}, + }); + cp.on('error', () => {}); +`], { timeout: outerTimeoutMs }); diff --git a/test/parallel/test-cli-node-options.js b/test/parallel/test-cli-node-options.js index 90c399790f6..375c46c11ab 100644 --- a/test/parallel/test-cli-node-options.js +++ b/test/parallel/test-cli-node-options.js @@ -12,7 +12,7 @@ const { Worker } = require('worker_threads'); const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); tmpdir.refresh(); const printA = path.relative(tmpdir.path, fixtures.path('printA.js')); @@ -65,7 +65,7 @@ if (common.isLinux) { if (common.hasCrypto) { expectNoWorker('--use-openssl-ca', 'B\n'); expectNoWorker('--use-bundled-ca', 'B\n'); - if (!hasOpenSSL3) + if (!hasOpenSSL(3)) expectNoWorker('--openssl-config=_ossl_cfg', 'B\n'); if (common.isMacOS) { expect('--use-system-ca', 'B\n'); diff --git a/test/parallel/test-common-wpt-script-modifier.js b/test/parallel/test-common-wpt-script-modifier.js new file mode 100644 index 00000000000..c2a39b81194 --- /dev/null +++ b/test/parallel/test-common-wpt-script-modifier.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { spawnSync } = require('child_process'); + +if (process.env.NODE_TEST_WPT_MODIFIER_PROBE === '1') { + const { WPTRunner } = require('../common/wpt'); + const runner = new WPTRunner('html/webappapis/atob'); + runner.setScriptModifier(common.mustCall((script) => { + assert.strictEqual(script.filename, + fixtures.path('wpt', 'html/webappapis/atob', 'base64.any.js')); + script.filename += '.modified'; + script.code = `test(() => { + assert_true(new Error().stack.includes(${JSON.stringify(`${script.filename}:`)})); + }, 'modified script retains its filename');`; + })); + runner.runJsTests(); +} else { + // eslint-disable-next-line no-unused-vars + const { WPT_REPORT, ...env } = { ...process.env, NODE_TEST_WPT_MODIFIER_PROBE: '1' }; + const result = spawnSync(process.execPath, [__filename, 'base64.any.js'], { + env, + encoding: 'utf8', + timeout: common.platformTimeout(10_000), + }); + const { error, status, stdout, stderr } = result; + assert.ifError(error); + assert.strictEqual(status, 0, stdout + stderr); + const results = stdout.split('\n').filter((line) => line.startsWith('[PASS]')); + assert.deepStrictEqual(results, [ + '[PASS] modified script retains its filename', + ]); +} diff --git a/test/parallel/test-config-json-schema.js b/test/parallel/test-config-json-schema.js index 82679660a30..08973bd17ee 100644 --- a/test/parallel/test-config-json-schema.js +++ b/test/parallel/test-config-json-schema.js @@ -10,9 +10,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { common.skip('this test requires OpenSSL 3.x'); } diff --git a/test/parallel/test-crypto-argon2-job.js b/test/parallel/test-crypto-argon2-job.js index 7c16f9d850a..b3c04e8927e 100644 --- a/test/parallel/test-crypto-argon2-job.js +++ b/test/parallel/test-crypto-argon2-job.js @@ -4,10 +4,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); +if (hasFIPS(3)) + common.skip('Argon2 is not available in FIPS mode'); // Exercises the native Argon2 job directly via internalBinding, bypassing // the JS validators, to ensure that if invalid parameters ever reach the diff --git a/test/parallel/test-crypto-argon2.js b/test/parallel/test-crypto-argon2.js index 1f238e61a61..98367bab547 100644 --- a/test/parallel/test-crypto-argon2.js +++ b/test/parallel/test-crypto-argon2.js @@ -3,7 +3,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); @@ -28,6 +28,19 @@ const secret = Buffer.alloc(8, 0x03); const associatedData = Buffer.alloc(12, 0x04); const defaults = { message, nonce, parallelism: 1, tagLength: 64, memory: 8, passes: 3 }; +if (hasFIPS(3)) { + assert.throws(() => crypto.argon2Sync('argon2id', defaults), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + crypto.argon2('argon2id', defaults, common.mustCall((err, result) => { + assert.strictEqual(err?.name, 'Error'); + assert.match(err.message, /:digital envelope routines::unsupported$/); + assert.strictEqual(result, undefined); + })); + return; +} + const good = [ // Test vectors from RFC 9106 https://www.rfc-editor.org/rfc/rfc9106.html#name-test-vectors // and OpenSSL 3.2 https://github.com/openssl/openssl/blob/6dfa998f7ea150f9c6d4e4727cf6d5c82a68a8da/test/recipes/30-test_evp_data/evpkdf_argon2.txt diff --git a/test/parallel/test-crypto-async-sign-verify.js b/test/parallel/test-crypto-async-sign-verify.js index 2fb17748324..dbc69e20ff9 100644 --- a/test/parallel/test-crypto-async-sign-verify.js +++ b/test/parallel/test-crypto-async-sign-verify.js @@ -3,12 +3,14 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const util = require('util'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); + function test( publicFixture, privateFixture, @@ -65,6 +67,17 @@ function test( } } +function testSignFailure(privateFixture, algorithm, options, code, message) { + const key = { key: fixtures.readKey(privateFixture), ...options }; + const data = Buffer.from('Hello world'); + assert.throws(() => crypto.sign(algorithm, data, key), { code }); + crypto.sign(algorithm, data, key, common.mustCall((err) => { + // Async crypto jobs in v24 preserve the OpenSSL message without a code. + assert.strictEqual(err?.name, 'Error'); + assert.strictEqual(err.message, message); + })); +} + // RSA w/ default padding test('rsa_public.pem', 'rsa_private.pem', 'sha256', true); test('rsa_public.pem', 'rsa_private.pem', 'sha256', true, @@ -89,19 +102,25 @@ test('rsa_public.pem', 'rsa_private.pem', 'sha256', false, // ED25519 test('ed25519_public.pem', 'ed25519_private.pem', undefined, true); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { // ED448 test('ed448_public.pem', 'ed448_private.pem', undefined, true); // ECDSA w/ der signature encoding - test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', - false); - test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', - false, { dsaEncoding: 'der' }); - - // ECDSA w/ ieee-p1363 signature encoding - test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', false, - { dsaEncoding: 'ieee-p1363' }); + if (fips3) { + testSignFailure('ec_secp256k1_private.pem', 'sha384', {}, + 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE', + 'error:03000096:digital envelope routines::operation not supported for this keytype'); + } else { + test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', + false); + test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', + false, { dsaEncoding: 'der' }); + + // ECDSA w/ ieee-p1363 signature encoding + test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', false, + { dsaEncoding: 'ieee-p1363' }); + } // DSA w/ der signature encoding test('dsa_public.pem', 'dsa_private.pem', 'sha256', @@ -156,7 +175,7 @@ MCowBQYDK2VuAyEA6pwGRbadNQAI/tYN8+/p/0/hbsdHfOEGr1ADiLVk/Gc= const signature = crypto.randomBytes(16); let expected = /no default digest/; - if (hasOpenSSL3 || process.features.openssl_is_boringssl) { + if (hasOpenSSL(3) || isBoringSSL) { expected = /operation[\s_]not[\s_]supported[\s_]for[\s_]this[\s_]keytype/i; } @@ -167,11 +186,21 @@ MCowBQYDK2VuAyEA6pwGRbadNQAI/tYN8+/p/0/hbsdHfOEGr1ADiLVk/Gc= } { - const { privateKey } = crypto.generateKeyPairSync('rsa', { - modulusLength: 512 - }); - crypto.sign('sha512', 'message', privateKey, common.mustCall((err) => { - assert.ok(err); - assert.match(err.message, /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i); - })); + if (fips3) { + crypto.generateKeyPair('rsa', { modulusLength: 512 }, + common.mustCall((err) => { + assert.strictEqual(err?.name, 'Error'); + assert.strictEqual( + err.message, 'error:020000AE:rsa routines::invalid modulus'); + })); + } else { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + crypto.sign('sha512', 'message', privateKey, common.mustCall((err) => { + assert.ok(err); + assert.match( + err.message, /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i); + })); + } } diff --git a/test/parallel/test-crypto-authenticated-stream.js b/test/parallel/test-crypto-authenticated-stream.js index 51b928ec36b..45736d3303d 100644 --- a/test/parallel/test-crypto-authenticated-stream.js +++ b/test/parallel/test-crypto-authenticated-stream.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const fs = require('fs'); const stream = require('stream'); const tmpdir = require('../common/tmpdir'); @@ -120,6 +121,16 @@ function test(config) { return; } + if (hasFIPS(3)) { + assert.throws(() => crypto.createDecipheriv( + config.cipher, config.key, config.iv, { + authTagLength: config.authTagLength, + }), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + return; + } + direct(config); mstream(config); fstream(config); diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index db1d12ae671..f321bb0105f 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -29,9 +29,10 @@ const assert = require('assert'); const crypto = require('crypto'); const { inspect } = require('util'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL, hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); -const isFipsEnabled = crypto.getFips(); +const isFipsEnabled = crypto.getFips() === 1; +const fips3 = hasFIPS(3); // // Test authenticated encryption modes. @@ -43,6 +44,10 @@ const TEST_CASES = require(fixtures.path('aead-vectors.js')); const errMessages = { auth: / auth/, + // OpenSSL 4.1 adds a provider error for AEAD tag mismatches. + // https://github.com/openssl/openssl/pull/32587 + badDecrypt: hasOpenSSL(4, 1) ? + { code: 'ERR_OSSL_BAD_DECRYPT' } : /Unsupported state or unable to authenticate data/, state: / state/, FIPS: /not supported in FIPS mode/, length: /Invalid initialization vector/, @@ -127,7 +132,8 @@ for (const test of TEST_CASES) { assert.strictEqual(msg, test.plain); } else { // Assert that final throws if input data could not be verified! - assert.throws(function() { decrypt.final('hex'); }, errMessages.auth); + assert.throws(function() { decrypt.final('hex'); }, + isCCM || isSIV ? errMessages.auth : errMessages.badDecrypt); } } } @@ -357,7 +363,8 @@ for (const test of TEST_CASES) { decipher.update(ciphertext); assert.throws(() => { decipher.final(); - }, /Unsupported state or unable to authenticate data/); + }, algo === 'aes-128-siv' ? + /Unsupported state or unable to authenticate data/ : errMessages.badDecrypt); } } } @@ -718,6 +725,14 @@ for (const test of TEST_CASES) { const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); const tag = cipher.getAuthTag(); + if (fips3 && mode === 'ccm') { + assert.throws(() => crypto.createDecipheriv( + `aes-128-${mode}`, key, iv, opts), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + continue; + } + const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, opts); decipher.setAuthTag(tag); assert.throws(() => { @@ -804,7 +819,7 @@ for (const test of TEST_CASES) { } catch (err) { // OpenSSL without https://github.com/openssl/openssl/pull/32427 // cannot finalize an empty CCM message unless update() was called. - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { assert.strictEqual(err.code, 'ERR_OSSL_TAG_NOT_SET'); } else { assert.match(err.message, /Unsupported state/); @@ -818,7 +833,14 @@ for (const test of TEST_CASES) { } } -if (!process.features.openssl_is_boringssl) { +if (fips3) { + assert.throws(() => crypto.createCipheriv( + 'chacha20-poly1305', Buffer.alloc(32), Buffer.alloc(12), { + authTagLength: 16, + }), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); +} else if (!isBoringSSL) { const key = Buffer.alloc(32); const iv = Buffer.alloc(12); @@ -836,7 +858,7 @@ if (!process.features.openssl_is_boringssl) { // ChaCha20-Poly1305 should respect the authTagLength option and should not // require the authentication tag before calls to update() during decryption. -if (!process.features.openssl_is_boringssl) { +if (!fips3 && !isBoringSSL) { const key = Buffer.alloc(32); const iv = Buffer.alloc(12); @@ -887,7 +909,7 @@ if (!process.features.openssl_is_boringssl) { // shorter tags as long as their length was valid according to NIST SP 800-38D. // For ChaCha20-Poly1305, we intentionally deviate from that because there are // no recommended or approved authentication tag lengths below 16 bytes. -if (!process.features.openssl_is_boringssl) { +if (!fips3 && !isBoringSSL) { const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { return algo === 'chacha20-poly1305' && tampered === false; }); @@ -926,7 +948,7 @@ if (!process.features.openssl_is_boringssl) { } // https://github.com/nodejs/node/issues/45874 -if (!process.features.openssl_is_boringssl) { +if (!fips3 && !isBoringSSL) { const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { return algo === 'chacha20-poly1305' && tampered === false; }); @@ -953,7 +975,7 @@ if (!process.features.openssl_is_boringssl) { assert.throws(() => { decipher.final(); - }, /Unsupported state or unable to authenticate data/); + }, errMessages.badDecrypt); } else { common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); } @@ -973,7 +995,7 @@ if (ciphers.includes('aes-128-ccm')) { const tag = cipher.getAuthTag(); assert.strictEqual(tag.length, 16); - if (isFipsEnabled && hasOpenSSL3) { + if (fips3) { assert.throws(() => crypto.createDecipheriv( 'aes-128-ccm', key, nonce, { authTagLength: 16 }), { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', diff --git a/test/parallel/test-crypto-boringssl-evp-list.js b/test/parallel/test-crypto-boringssl-evp-list.js index 3f142c24f28..f2d8fd4796c 100644 --- a/test/parallel/test-crypto-boringssl-evp-list.js +++ b/test/parallel/test-crypto-boringssl-evp-list.js @@ -1,10 +1,12 @@ 'use strict'; const common = require('../common'); + +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); -if (!process.features.openssl_is_boringssl) +if (!isBoringSSL) common.skip('BoringSSL-only test'); const assert = require('assert'); diff --git a/test/parallel/test-crypto-certificate.js b/test/parallel/test-crypto-certificate.js index 28d20ba61c7..54f61d8a3f6 100644 --- a/test/parallel/test-crypto-certificate.js +++ b/test/parallel/test-crypto-certificate.js @@ -26,6 +26,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const { Certificate } = crypto; const fixtures = require('../common/fixtures'); @@ -41,8 +42,8 @@ function copyArrayBuffer(buf) { function checkMethods(certificate) { - if (!process.features.openssl_is_boringssl) - assert.strictEqual(certificate.verifySpkac(spkacValid), true); + if (!isBoringSSL) + assert.strictEqual(certificate.verifySpkac(spkacValid), !hasFIPS(3)); assert.strictEqual(certificate.verifySpkac(spkacFail), false); assert.strictEqual( @@ -57,11 +58,12 @@ function checkMethods(certificate) { ); assert.strictEqual(certificate.exportChallenge(spkacFail), ''); - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { const ab = copyArrayBuffer(spkacValid); - assert.strictEqual(certificate.verifySpkac(ab), true); - assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), true); - assert.strictEqual(certificate.verifySpkac(new DataView(ab)), true); + const expected = !hasFIPS(3); + assert.strictEqual(certificate.verifySpkac(ab), expected); + assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), expected); + assert.strictEqual(certificate.verifySpkac(new DataView(ab)), expected); } } diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index 672dc75446c..4c2a0c1fd1c 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -5,8 +5,9 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); -const isFipsEnabled = crypto.getFips(); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); +const isFipsEnabled = crypto.getFips() === 1; +const fips3 = hasFIPS(3); function testCipher1(key, iv) { // Test encryption and decryption with explicit key and iv @@ -164,10 +165,12 @@ function testSm4Xts() { { const Cipheriv = crypto.Cipheriv; - const key = '123456789012345678901234'; - const iv = '12345678'; + const algorithm = fips3 ? 'aes-128-cbc' : 'des-ede3-cbc'; + const key = fips3 ? + '1234567890123456' : '123456789012345678901234'; + const iv = fips3 ? '1234567890123456' : '12345678'; - const instance = Cipheriv('des-ede3-cbc', key, iv); + const instance = Cipheriv(algorithm, key, iv); assert(instance instanceof Cipheriv, 'Cipheriv is expected to return a new ' + 'instance when called without `new`'); @@ -197,10 +200,12 @@ function testSm4Xts() { { const Decipheriv = crypto.Decipheriv; - const key = '123456789012345678901234'; - const iv = '12345678'; + const algorithm = fips3 ? 'aes-128-cbc' : 'des-ede3-cbc'; + const key = fips3 ? + '1234567890123456' : '123456789012345678901234'; + const iv = fips3 ? '1234567890123456' : '12345678'; - const instance = Decipheriv('des-ede3-cbc', key, iv); + const instance = Decipheriv(algorithm, key, iv); assert(instance instanceof Decipheriv, 'Decipheriv expected to return a new' + ' instance when called without `new`'); @@ -231,8 +236,10 @@ function testSm4Xts() { testCipher1('0123456789abcd0123456789', '12345678'); testCipher1('0123456789abcd0123456789', Buffer.from('12345678')); testCipher1(Buffer.from('0123456789abcd0123456789'), '12345678'); -testCipher1(Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); -testCipher2(Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); +testCipher1( + Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); +testCipher2( + Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); if (!isFipsEnabled) { testCipher3(Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'), @@ -286,8 +293,8 @@ assert.throws( errMessage); // But all other IV lengths should be accepted. -const minIvLength = hasOpenSSL3 ? 8 : 1; -const maxIvLength = hasOpenSSL3 ? 64 : 256; +const minIvLength = hasOpenSSL(3) ? 8 : 1; +const maxIvLength = hasOpenSSL(3) ? 64 : 256; for (let n = minIvLength; n < maxIvLength; n += 1) { if (isFipsEnabled && n < 12) continue; crypto.createCipheriv('aes-128-gcm', Buffer.alloc(16), Buffer.alloc(n)); diff --git a/test/parallel/test-crypto-classes.js b/test/parallel/test-crypto-classes.js index 429bc91d441..48d68c93fb6 100644 --- a/test/parallel/test-crypto-classes.js +++ b/test/parallel/test-crypto-classes.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); // 'ClassName' : ['args', 'for', 'constructor'] const TEST_CASES = { @@ -21,8 +21,17 @@ const TEST_CASES = { 'ECDH': ['prime256v1'], }; -if (!crypto.getFips()) { - TEST_CASES.DiffieHellman = [hasOpenSSL3 ? 1024 : 256]; +if (hasFIPS(3)) { + TEST_CASES.Hmac = ['sha1', '0123456789abcdef']; + TEST_CASES.Cipheriv = [ + 'aes-128-cbc', '0123456789abcdef', '1234567890abcdef']; + TEST_CASES.Decipheriv = TEST_CASES.Cipheriv; + TEST_CASES.Sign = ['RSA-SHA256']; + TEST_CASES.Verify = ['RSA-SHA256']; + TEST_CASES.DiffieHellman = [2048]; + TEST_CASES.DiffieHellmanGroup = ['modp14']; +} else if (crypto.getFips() !== 1) { + TEST_CASES.DiffieHellman = [hasOpenSSL(3) ? 1024 : 256]; } for (const [clazz, args] of Object.entries(TEST_CASES)) { diff --git a/test/parallel/test-crypto-dh-constructor.js b/test/parallel/test-crypto-dh-constructor.js index eb867493248..edf7ab08e44 100644 --- a/test/parallel/test-crypto-dh-constructor.js +++ b/test/parallel/test-crypto-dh-constructor.js @@ -5,23 +5,21 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasFIPS } = require('../common/crypto'); -const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; -const dh1 = crypto.createDiffieHellman(size); -const p1 = dh1.getPrime('buffer'); +const prime = crypto.getDiffieHellman('modp14').getPrime('buffer'); { const DiffieHellman = crypto.DiffieHellman; - const dh = DiffieHellman(p1, 'buffer'); + const dh = DiffieHellman(prime, 'buffer'); assert(dh instanceof DiffieHellman, 'DiffieHellman is expected to return a ' + 'new instance when called without `new`'); } { const DiffieHellmanGroup = crypto.DiffieHellmanGroup; - const dhg = DiffieHellmanGroup('modp5'); + const dhg = DiffieHellmanGroup(hasFIPS(3) ? 'modp14' : 'modp5'); assert(dhg instanceof DiffieHellmanGroup, 'DiffieHellmanGroup is expected ' + 'to return a new instance when ' + 'called without `new`'); diff --git a/test/parallel/test-crypto-dh-curves.js b/test/parallel/test-crypto-dh-curves.js index 68668ae6a28..f965aa53d86 100644 --- a/test/parallel/test-crypto-dh-curves.js +++ b/test/parallel/test-crypto-dh-curves.js @@ -5,7 +5,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const { DH_CHECK_P_NOT_PRIME, DH_CHECK_P_NOT_SAFE_PRIME, @@ -21,7 +21,7 @@ const p = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' + 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF'; crypto.createDiffieHellman(p, 'hex'); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { const notPrime = Buffer.from(p, 'hex'); notPrime[notPrime.length - 1] = 0xfd; assert.strictEqual( @@ -63,7 +63,7 @@ assert.throws( }); // Confirm DH_check() results are exposed for optional examination. -const bad_dh = process.features.openssl_is_boringssl ? +const bad_dh = isBoringSSL ? crypto.createDiffieHellman('abcd', 'hex', 0) : crypto.createDiffieHellman('02', 'hex'); assert.notStrictEqual(bad_dh.verifyError, 0); @@ -77,7 +77,7 @@ if (hasOpenSSL(3)) { () => crypto.createDiffieHellman(Buffer.from(p, 'hex'), Buffer.from(p, 'hex')), { code: 'ERR_OSSL_DH_BAD_GENERATOR' }); -} else if (!process.features.openssl_is_boringssl) { +} else if (!isBoringSSL) { assert.strictEqual( crypto.createDiffieHellman(Buffer.from(p, 'hex'), Buffer.from(p, 'hex')).verifyError, @@ -123,120 +123,126 @@ if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) { // ECDH should check that point is on curve const ecdh3 = crypto.createECDH('secp256k1'); - const key3 = ecdh3.generateKeys(); - - assert.throws( - () => ecdh2.computeSecret(key3, 'latin1', 'buffer'), - { - code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', - name: 'Error', - message: 'Public key is not valid for specified curve' + if (hasFIPS(3)) { + assert.throws(() => ecdh3.generateKeys(), { + code: 'ERR_CRYPTO_OPERATION_FAILED', }); + } else { + const key3 = ecdh3.generateKeys(); - // ECDH should allow .setPrivateKey()/.setPublicKey() - const ecdh4 = crypto.createECDH('prime256v1'); + assert.throws( + () => ecdh2.computeSecret(key3, 'latin1', 'buffer'), + { + code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + name: 'Error', + message: 'Public key is not valid for specified curve' + }); - ecdh4.setPrivateKey(ecdh1.getPrivateKey()); - ecdh4.setPublicKey(ecdh1.getPublicKey()); + // ECDH should allow .setPrivateKey()/.setPublicKey() + const ecdh4 = crypto.createECDH('prime256v1'); - const ecdh4Secret = ecdh4.computeSecret(ecdh2.getPublicKey()); - assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), - ecdh4Secret); + ecdh4.setPrivateKey(ecdh1.getPrivateKey()); + ecdh4.setPublicKey(ecdh1.getPublicKey()); - assert.throws(() => { - ecdh4.setPublicKey(ecdh3.getPublicKey()); - }, { message: 'Failed to convert Buffer to EC_POINT' }); - assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), - ecdh4Secret); + const ecdh4Secret = ecdh4.computeSecret(ecdh2.getPublicKey()); + assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), + ecdh4Secret); - // Verify that we can use ECDH without having to use newly generated keys. - const ecdh5 = crypto.createECDH('secp256k1'); + assert.throws(() => { + ecdh4.setPublicKey(ecdh3.getPublicKey()); + }, { message: 'Failed to convert Buffer to EC_POINT' }); + assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), + ecdh4Secret); - // Verify errors are thrown when retrieving keys from an uninitialized object. - assert.throws(() => { - ecdh5.getPublicKey(); - }, /^Error: Failed to get ECDH public key$/); + // Verify that we can use ECDH without having to use newly generated keys. + const ecdh5 = crypto.createECDH('secp256k1'); - assert.throws(() => { - ecdh5.getPrivateKey(); - }, /^Error: Failed to get ECDH private key$/); + // Verify errors are thrown when retrieving keys from an uninitialized object. + assert.throws(() => { + ecdh5.getPublicKey(); + }, /^Error: Failed to get ECDH public key$/); - // A valid private key for the secp256k1 curve. - const cafebabeKey = 'cafebabe'.repeat(8); - // Associated compressed and uncompressed public keys (points). - const cafebabePubPtComp = + assert.throws(() => { + ecdh5.getPrivateKey(); + }, /^Error: Failed to get ECDH private key$/); + + // A valid private key for the secp256k1 curve. + const cafebabeKey = 'cafebabe'.repeat(8); + // Associated compressed and uncompressed public keys (points). + const cafebabePubPtComp = '03672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3'; - const cafebabePubPtUnComp = + const cafebabePubPtUnComp = '04672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3' + '2e02c7f93d13dc2732b760ca377a5897b9dd41a1c1b29dc0442fdce6d0a04d1d'; - ecdh5.setPrivateKey(cafebabeKey, 'hex'); - assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); - // Show that the public point (key) is generated while setting the - // private key. - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - - // Compressed and uncompressed public points/keys for other party's - // private key. - // 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF - const peerPubPtComp = + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + // Show that the public point (key) is generated while setting the + // private key. + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Compressed and uncompressed public points/keys for other party's + // private key. + // 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF + const peerPubPtComp = '02c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae'; - const peerPubPtUnComp = + const peerPubPtUnComp = '04c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae' + 'b651944a574a362082a77e3f2b5d9223eb54d7f2f76846522bf75f3bedb8178e'; - const sharedSecret = + const sharedSecret = '1da220b5329bbe8bfd19ceef5a5898593f411a6f12ea40f2a8eead9a5cf59970'; - assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), - sharedSecret); - assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'), - sharedSecret); - assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), - sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), + sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'), + sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), + sharedSecret); - // Verify that we still have the same key pair as before the computation. - assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - - // Verify setting and getting compressed and non-compressed serializations. - ecdh5.setPublicKey(cafebabePubPtComp, 'hex'); - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - assert.strictEqual( - ecdh5.getPublicKey('hex', 'compressed'), - cafebabePubPtComp - ); - ecdh5.setPublicKey(cafebabePubPtUnComp, 'hex'); - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - assert.strictEqual( - ecdh5.getPublicKey('hex', 'compressed'), - cafebabePubPtComp - ); - - // Show why allowing the public key to be set on this type - // does not make sense. - ecdh5.setPublicKey(peerPubPtComp, 'hex'); - assert.strictEqual(ecdh5.getPublicKey('hex'), peerPubPtUnComp); - assert.throws(() => { - // Error because the public key does not match the private key anymore. - ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'); - }, /Invalid key pair/); - - // Set to a valid key to show that later attempts to set an invalid key are - // rejected. - ecdh5.setPrivateKey(cafebabeKey, 'hex'); - - // Some invalid private keys for the secp256k1 curve. - const errMessage = /Private key is not valid for specified curve/; - ['0000000000000000000000000000000000000000000000000000000000000000', - 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', - 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', - ].forEach((element) => { - assert.throws(() => { - ecdh5.setPrivateKey(element, 'hex'); - }, errMessage); - // Verify object state did not change. + // Verify that we still have the same key pair as before the computation. assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); - }); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Verify setting and getting compressed and non-compressed serializations. + ecdh5.setPublicKey(cafebabePubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + ecdh5.setPublicKey(cafebabePubPtUnComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + + // Show why allowing the public key to be set on this type + // does not make sense. + ecdh5.setPublicKey(peerPubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), peerPubPtUnComp); + assert.throws(() => { + // Error because the public key does not match the private key anymore. + ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'); + }, /Invalid key pair/); + + // Set to a valid key to show that later attempts to set an invalid key are + // rejected. + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + + // Some invalid private keys for the secp256k1 curve. + const errMessage = /Private key is not valid for specified curve/; + ['0000000000000000000000000000000000000000000000000000000000000000', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + ].forEach((element) => { + assert.throws(() => { + ecdh5.setPrivateKey(element, 'hex'); + }, errMessage); + // Verify object state did not change. + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + }); + } } // Use of invalid keys was not cleaning up ERR stack, and was causing diff --git a/test/parallel/test-crypto-dh-errors.js b/test/parallel/test-crypto-dh-errors.js index d7527d82617..398d2e2c0f7 100644 --- a/test/parallel/test-crypto-dh-errors.js +++ b/test/parallel/test-crypto-dh-errors.js @@ -5,7 +5,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); // https://github.com/nodejs/node/issues/32738 // XXX(bnoordhuis) validateInt32() throwing ERR_OUT_OF_RANGE and RangeError @@ -25,7 +25,7 @@ assert.throws(() => crypto.createDiffieHellman('abcdef', 13.37), { }); for (const bits of [-1, 0, 1]) { - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { assert.throws(() => crypto.createDiffieHellman(bits), { code: 'ERR_OSSL_DH_MODULUS_TOO_SMALL', name: 'Error', diff --git a/test/parallel/test-crypto-dh-generate-keys.js b/test/parallel/test-crypto-dh-generate-keys.js index acf7e2d09b2..65efb369c08 100644 --- a/test/parallel/test-crypto-dh-generate-keys.js +++ b/test/parallel/test-crypto-dh-generate-keys.js @@ -6,10 +6,9 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); { - const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; + const prime = crypto.getDiffieHellman('modp14').getPrime(); function unlessInvalidState(f) { try { @@ -22,7 +21,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); } function testGenerateKeysChangesKeys(setup, expected) { - const dh = crypto.createDiffieHellman(size); + const dh = crypto.createDiffieHellman(prime); setup(dh); const firstPublicKey = unlessInvalidState(() => dh.getPublicKey()); const firstPrivateKey = unlessInvalidState(() => dh.getPrivateKey()); diff --git a/test/parallel/test-crypto-dh-group-setters.js b/test/parallel/test-crypto-dh-group-setters.js index 37d0a44d0e1..ccc370e2352 100644 --- a/test/parallel/test-crypto-dh-group-setters.js +++ b/test/parallel/test-crypto-dh-group-setters.js @@ -1,12 +1,14 @@ 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const crypto = require('crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { common.skip('Skipping unsupported Diffie-Hellman tests'); } diff --git a/test/parallel/test-crypto-dh-leak.js b/test/parallel/test-crypto-dh-leak.js index df1ba89737c..f58a5a152b7 100644 --- a/test/parallel/test-crypto-dh-leak.js +++ b/test/parallel/test-crypto-dh-leak.js @@ -9,12 +9,11 @@ if (common.isASan) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); const before = process.memoryUsage.rss(); { - const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; - const dh = crypto.createDiffieHellman(size); + const prime = crypto.getDiffieHellman('modp14').getPrime(); + const dh = crypto.createDiffieHellman(prime); const publicKey = dh.generateKeys(); const privateKey = dh.getPrivateKey(); for (let i = 0; i < 5e4; i += 1) { diff --git a/test/parallel/test-crypto-dh-modp2-views.js b/test/parallel/test-crypto-dh-modp2-views.js index a28e615b7f3..ce2d8823bee 100644 --- a/test/parallel/test-crypto-dh-modp2-views.js +++ b/test/parallel/test-crypto-dh-modp2-views.js @@ -5,24 +5,30 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { modp2buf } = require('../common/crypto'); +const { hasFIPS, modp2buf, isBoringSSL } = require('../common/crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { common.skip('Skipping unsupported Diffie-Hellman tests'); } -const modp2 = crypto.createDiffieHellmanGroup('modp2'); +if (hasFIPS(3)) { + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} else { + const modp2 = crypto.createDiffieHellmanGroup('modp2'); -const views = common.getArrayBufferViews(modp2buf); -for (const buf of [modp2buf, ...views]) { - // Ensure specific generator (string with encoding) works as expected with - // any ArrayBufferViews as the first argument to createDiffieHellman(). - const exmodp2 = crypto.createDiffieHellman(buf, '02', 'hex'); - modp2.generateKeys(); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) - .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) - .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); + const views = common.getArrayBufferViews(modp2buf); + for (const buf of [modp2buf, ...views]) { + // Ensure specific generator (string with encoding) works as expected with + // any ArrayBufferViews as the first argument to createDiffieHellman(). + const exmodp2 = crypto.createDiffieHellman(buf, '02', 'hex'); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); + } } diff --git a/test/parallel/test-crypto-dh-modp2.js b/test/parallel/test-crypto-dh-modp2.js index eb262f235ff..a0ec31a1eb4 100644 --- a/test/parallel/test-crypto-dh-modp2.js +++ b/test/parallel/test-crypto-dh-modp2.js @@ -5,44 +5,51 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { modp2buf } = require('../common/crypto'); +const { hasFIPS, modp2buf, isBoringSSL } = require('../common/crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { common.skip('Skipping unsupported Diffie-Hellman tests'); } -const modp2 = crypto.createDiffieHellmanGroup('modp2'); +if (hasFIPS(3)) { + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} else { + const modp2 = crypto.createDiffieHellmanGroup('modp2'); -{ + { // Ensure specific generator (buffer) works as expected. - const exmodp2 = crypto.createDiffieHellman(modp2buf, Buffer.from([2])); - modp2.generateKeys(); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + const exmodp2 = crypto.createDiffieHellman(modp2buf, Buffer.from([2])); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); -} + assert.strictEqual(modp2Secret, exmodp2Secret); + } -{ + { // Ensure specific generator (string without encoding) works as expected. - const exmodp2 = crypto.createDiffieHellman(modp2buf, '\x02'); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + const exmodp2 = crypto.createDiffieHellman(modp2buf, '\x02'); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); -} + assert.strictEqual(modp2Secret, exmodp2Secret); + } -{ + { // Ensure specific generator (numeric) works as expected. - const exmodp2 = crypto.createDiffieHellman(modp2buf, 2); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + const exmodp2 = crypto.createDiffieHellman(modp2buf, 2); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); + assert.strictEqual(modp2Secret, exmodp2Secret); + } + } diff --git a/test/parallel/test-crypto-dh-odd-key.js b/test/parallel/test-crypto-dh-odd-key.js index fbe42be425e..c96227770e3 100644 --- a/test/parallel/test-crypto-dh-odd-key.js +++ b/test/parallel/test-crypto-dh-odd-key.js @@ -27,19 +27,24 @@ if (!common.hasCrypto) { const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); function test() { const odd = Buffer.alloc(39, 'A'); - const c = crypto.createDiffieHellman(hasOpenSSL3 ? 1024 : 32); + const size = hasFIPS(3) ? 2048 : (hasOpenSSL(3) ? 1024 : 32); + const c = crypto.createDiffieHellman(size); c.setPrivateKey(odd); c.generateKeys(); } -// FIPS requires a length of at least 1024 -if (!crypto.getFips()) { +if (hasFIPS(3)) { + test(); + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} else if (crypto.getFips() !== 1) { test(); } else { - assert.throws(function() { test(); }, /key size too small/); + assert.throws(test, /key size too small/); } diff --git a/test/parallel/test-crypto-dh-shared.js b/test/parallel/test-crypto-dh-shared.js index 515405034d7..5e6db278324 100644 --- a/test/parallel/test-crypto-dh-shared.js +++ b/test/parallel/test-crypto-dh-shared.js @@ -5,9 +5,11 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); -const alice = crypto.createDiffieHellmanGroup('modp5'); -const bob = crypto.createDiffieHellmanGroup('modp5'); +const group = hasFIPS(3) ? 'modp14' : 'modp5'; +const alice = crypto.createDiffieHellmanGroup(group); +const bob = crypto.createDiffieHellmanGroup(group); alice.generateKeys(); bob.generateKeys(); const aSecret = alice.computeSecret(bob.getPublicKey()).toString('hex'); diff --git a/test/parallel/test-crypto-dh-stateless.js b/test/parallel/test-crypto-dh-stateless.js index 2fddaddd2f4..a86b6934f6c 100644 --- a/test/parallel/test-crypto-dh-stateless.js +++ b/test/parallel/test-crypto-dh-stateless.js @@ -5,8 +5,11 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); -const isBoringSSL = process.features.openssl_is_boringssl; +const { + hasOpenSSL, + isBoringSSL: commonIsBoringSSL, +} = require('../common/crypto'); +const isBoringSSL = commonIsBoringSSL; // Error code for a key-type mismatch during (EC)DH. The underlying OpenSSL // error code varies by version, and in OpenSSL 4.0 by platform: some builds diff --git a/test/parallel/test-crypto-dh.js b/test/parallel/test-crypto-dh.js index 01a7f0f3ddd..7b9a9d50d89 100644 --- a/test/parallel/test-crypto-dh.js +++ b/test/parallel/test-crypto-dh.js @@ -7,11 +7,14 @@ if (!common.hasCrypto) { const assert = require('assert'); const crypto = require('crypto'); const { - hasOpenSSL3, + hasOpenSSL, + hasFIPS, + isBoringSSL, } = require('../common/crypto'); { - const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; + const size = hasFIPS(3) ? + 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); const dh1 = crypto.createDiffieHellman(size); const p1 = dh1.getPrime('buffer'); const dh2 = crypto.createDiffieHellman(p1, 'buffer'); @@ -57,7 +60,7 @@ const { assert.strictEqual(secret1, secret4); let wrongBlockLength; - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { wrongBlockLength = { message: /wrong[\s_]final[\s_]block[\s_]length/i, code: /ERR_OSSL_(EVP_)?WRONG_FINAL_BLOCK_LENGTH/, @@ -98,7 +101,7 @@ const { // Through a fluke of history, g=0 defaults to DH_GENERATOR (2). { const g = 0; - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { assert.throws(() => crypto.createDiffieHellman('abcdef', g), { code: 'ERR_CRYPTO_OPERATION_FAILED', name: 'Error' @@ -110,7 +113,7 @@ const { } { - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { assert.throws(() => crypto.createDiffieHellman('abcdef', Buffer.from([2])), { code: 'ERR_CRYPTO_OPERATION_FAILED', name: 'Error' diff --git a/test/parallel/test-crypto-ecb.js b/test/parallel/test-crypto-ecb.js index 06c88272438..65c373d35d2 100644 --- a/test/parallel/test-crypto-ecb.js +++ b/test/parallel/test-crypto-ecb.js @@ -25,14 +25,14 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); const crypto = require('crypto'); if (crypto.getFips()) { common.skip('BF-ECB is not FIPS 140-2 compatible'); } -if (hasOpenSSL3) { +if (hasOpenSSL(3)) { common.skip('Blowfish is only available with the legacy provider in ' + 'OpenSSl 3.x'); } diff --git a/test/parallel/test-crypto-ecdh-convert-key.js b/test/parallel/test-crypto-ecdh-convert-key.js index c0046099df9..8c910b1b605 100644 --- a/test/parallel/test-crypto-ecdh-convert-key.js +++ b/test/parallel/test-crypto-ecdh-convert-key.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { ECDH, createSign, getCurves } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // A valid private key for the secp256k1 curve. const cafebabeKey = 'cafebabe'.repeat(8); @@ -93,11 +94,17 @@ if (getCurves().includes('secp256k1')) { // Compare to getPublicKey. const ecdh1 = ECDH('secp256k1'); - ecdh1.generateKeys(); - ecdh1.setPrivateKey(cafebabeKey, 'hex'); - assert.strictEqual(ecdh1.getPublicKey('hex', 'uncompressed'), uncompressed); - assert.strictEqual(ecdh1.getPublicKey('hex', 'compressed'), compressed); - assert.strictEqual(ecdh1.getPublicKey('hex', 'hybrid'), hybrid); + if (hasFIPS(3)) { + assert.throws(() => ecdh1.generateKeys(), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + } else { + ecdh1.generateKeys(); + ecdh1.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh1.getPublicKey('hex', 'uncompressed'), uncompressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'compressed'), compressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'hybrid'), hybrid); + } } // See https://github.com/nodejs/node/issues/26133, failed ConvertKey diff --git a/test/parallel/test-crypto-eddsa-variants.js b/test/parallel/test-crypto-eddsa-variants.js index 691534f13f5..10e9fba7e52 100644 --- a/test/parallel/test-crypto-eddsa-variants.js +++ b/test/parallel/test-crypto-eddsa-variants.js @@ -7,6 +7,8 @@ const assert = require('assert'); const crypto = require('crypto'); const { hasOpenSSL, + hasFIPS, + isBoringSSL, } = require('../common/crypto'); // RFC 8032 Section 7 test vectors for Ed25519, Ed25519ctx, and Ed448. @@ -179,7 +181,7 @@ function createKeyPair(algorithm, secretKeyHex, publicKeyHex) { } for (const v of vectors) { - if (v.algorithm.startsWith('Ed448') && process.features.openssl_is_boringssl) continue; + if (v.algorithm.startsWith('Ed448') && isBoringSSL) continue; if (v.algorithm.endsWith('ctx') || v.context) { if (!hasOpenSSL(3, 2)) continue; } @@ -191,7 +193,18 @@ for (const v of vectors) { const signKey = context ? { key: privateKey, context } : privateKey; const verifyKey = context ? { key: publicKey, context } : publicKey; - const sig = crypto.sign(null, message, signKey); + let sig; + try { + sig = crypto.sign(null, message, signKey); + } catch (err) { + if (!hasFIPS(3) || + (!v.algorithm.endsWith('ctx') && !v.context)) { + throw err; + } + assert.strictEqual( + err.code, 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION'); + continue; + } assert.deepStrictEqual(sig, expectedSig); assert.strictEqual( crypto.verify(null, message, verifyKey, expectedSig), true); diff --git a/test/parallel/test-crypto-encap-decap.js b/test/parallel/test-crypto-encap-decap.js index 4b19d717944..db93fab32cb 100644 --- a/test/parallel/test-crypto-encap-decap.js +++ b/test/parallel/test-crypto-encap-decap.js @@ -6,10 +6,15 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, + isBoringSSL: commonIsBoringSSL, +} = require('../common/crypto'); const { promisify } = require('util'); -const isBoringSSL = process.features.openssl_is_boringssl; +const isBoringSSL = commonIsBoringSSL; +const isFips = hasFIPS(3); if (!hasOpenSSL(3) && !isBoringSSL) { assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' }); @@ -36,7 +41,7 @@ const keys = { privateKey: fixtures.readKey('rsa_pss_private_2048.pem', 'ascii'), }, 'p-256': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('ec_p256_public.pem', 'ascii'), privateKey: fixtures.readKey('ec_p256_private.pem', 'ascii'), sharedSecretLength: 32, @@ -44,7 +49,7 @@ const keys = { raw: true, }, 'p-384': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('ec_p384_public.pem', 'ascii'), privateKey: fixtures.readKey('ec_p384_private.pem', 'ascii'), sharedSecretLength: 48, @@ -52,7 +57,7 @@ const keys = { raw: true, }, 'p-521': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('ec_p521_public.pem', 'ascii'), privateKey: fixtures.readKey('ec_p521_private.pem', 'ascii'), sharedSecretLength: 64, @@ -65,7 +70,7 @@ const keys = { privateKey: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), }, 'x25519': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('x25519_public.pem', 'ascii'), privateKey: fixtures.readKey('x25519_private.pem', 'ascii'), sharedSecretLength: 32, @@ -73,7 +78,7 @@ const keys = { raw: true, }, 'x448': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('x448_public.pem', 'ascii'), privateKey: fixtures.readKey('x448_private.pem', 'ascii'), sharedSecretLength: 64, diff --git a/test/parallel/test-crypto-fips-indicator-strict.js b/test/parallel/test-crypto-fips-indicator-strict.js index cc65d001626..5e003d7015b 100644 --- a/test/parallel/test-crypto-fips-indicator-strict.js +++ b/test/parallel/test-crypto-fips-indicator-strict.js @@ -6,7 +6,13 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -if (process.features.openssl_is_boringssl) { +const { + isBoringSSL, + hasFIPS, + hasOpenSSL, +} = require('../common/crypto'); + +if (isBoringSSL) { common.skip('BoringSSL does not support FIPS'); } @@ -18,7 +24,6 @@ const { Worker } = require('node:worker_threads'); const { spawnSyncAndExitWithoutError, } = require('../common/child_process'); -const { hasFIPS, hasOpenSSL } = require('../common/crypto'); const channelName = 'crypto.fips.indicator'; const mode = process.env.NODE_TEST_FIPS_FORCE_MODE; diff --git a/test/parallel/test-crypto-fips.js b/test/parallel/test-crypto-fips.js index ea20d895b1c..ad67ec181c7 100644 --- a/test/parallel/test-crypto-fips.js +++ b/test/parallel/test-crypto-fips.js @@ -4,7 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { isBoringSSL, hasOpenSSL } = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support FIPS'); const assert = require('assert'); @@ -14,7 +16,6 @@ const { spawnSyncAndAssert } = require('../common/child_process'); const fixtures = require('../common/fixtures'); const { internalBinding } = require('internal/test/binding'); const { testFipsCrypto } = internalBinding('crypto'); -const { hasOpenSSL, hasOpenSSL3 } = require('../common/crypto'); const FIPS_ENABLED = 1; const FIPS_DISABLED = 0; @@ -23,11 +24,11 @@ const FIPS_ERROR_STRING2 = '--force-fips at startup.'; const FIPS_UNSUPPORTED_ERROR_STRING = 'fips mode not supported'; const FIPS_ENABLE_ERROR_STRING = - hasOpenSSL3 ? + hasOpenSSL(3) ? '--enable-fips requires an active OpenSSL provider named "fips"' : 'OpenSSL error when trying to enable FIPS:'; const FIPS_FORCE_ERROR_STRING = - hasOpenSSL3 ? + hasOpenSSL(3) ? '--force-fips requires an active OpenSSL provider named "fips"' : 'OpenSSL error when trying to enable FIPS:'; @@ -146,7 +147,7 @@ if (!sharedOpenSSL()) { 'require("crypto").getFips()', { ...process.env, 'OPENSSL_CONF': ' ' }); - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { // Disabling FIPS mode should not throw after OpenSSL updates the default // property query. testHelper( @@ -191,7 +192,7 @@ assert.ok(test_result === 1 || test_result === 0); // ("Error: Cannot set FIPS mode in a non-FIPS build."). // Due to this uncertainty the following tests are skipped when configured // with --shared-openssl. -if (!sharedOpenSSL() && !hasOpenSSL3) { +if (!sharedOpenSSL() && !hasOpenSSL(3)) { // OpenSSL config file should be able to turn on FIPS mode testHelper( 'stdout', @@ -224,7 +225,7 @@ if (!sharedOpenSSL() && !hasOpenSSL3) { // will not work as expected with that version. // TODO(danbev) Revisit these test once FIPS support is available in // OpenSSL 3.x. -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { testHelper( 'stdout', [`--openssl-config=${CNF_FIPS_OFF}`], diff --git a/test/parallel/test-crypto-getcipherinfo.js b/test/parallel/test-crypto-getcipherinfo.js index 35eced3b296..faf45c766c5 100644 --- a/test/parallel/test-crypto-getcipherinfo.js +++ b/test/parallel/test-crypto-getcipherinfo.js @@ -8,9 +8,9 @@ const { createCipheriv, createHash, getCiphers, - getCipherInfo + getCipherInfo, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); @@ -18,7 +18,7 @@ const ciphers = getCiphers(); assert.strictEqual(getCipherInfo(-1), undefined); assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined); -if (hasOpenSSL3) { +if (hasOpenSSL(3)) { assert.deepStrictEqual( ciphers.filter((cipher) => cipher.includes('cbc-hmac')), []); for (const cipher of [ @@ -54,7 +54,7 @@ if (ciphers.includes('aes-128-wrap-inv')) { } assert(!ciphers.some((cipher) => /^\d+(?:\.\d+)+$/.test(cipher))); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { // A failed provider fetch must not contaminate the OpenSSL error queue. assert.throws(() => createHash('sha256', { outputLength: 28 }), { code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH', @@ -63,7 +63,7 @@ if (!process.features.openssl_is_boringssl) { for (const cipher of ciphers) { const info = getCipherInfo(cipher); - if (process.features.openssl_is_boringssl && !info) { + if (isBoringSSL && !info) { // BoringSSL reports some legacy ciphers in getCiphers() but returns no // info for them (e.g. des-ede3, des-ede3-ecb, rc2-40-cbc). common.printSkipMessage(`Skipping unsupported ${cipher} test case`); @@ -115,7 +115,7 @@ assert(getCipherInfo('aes-128-cbc', { ivLength: 16 })); assert(!getCipherInfo('aes-128-ccm', { ivLength: 1 })); assert(!getCipherInfo('aes-128-ccm', { ivLength: 14 })); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { for (let n = 7; n <= 13; n++) assert(getCipherInfo('aes-128-ccm', { ivLength: n })); } else { @@ -123,7 +123,10 @@ if (!process.features.openssl_is_boringssl) { } assert(!getCipherInfo('aes-128-ocb', { ivLength: 16 })); -if (!process.features.openssl_is_boringssl) { +if (hasFIPS(3)) { + assert.strictEqual( + getCipherInfo('aes-128-ocb', { ivLength: 12 }), undefined); +} else if (!isBoringSSL) { for (let n = 1; n < 16; n++) assert(getCipherInfo('aes-128-ocb', { ivLength: n })); } else { diff --git a/test/parallel/test-crypto-hash-stream-pipe.js b/test/parallel/test-crypto-hash-stream-pipe.js index ac851646a91..c6b9433456e 100644 --- a/test/parallel/test-crypto-hash-stream-pipe.js +++ b/test/parallel/test-crypto-hash-stream-pipe.js @@ -20,7 +20,9 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -30,10 +32,10 @@ const crypto = require('crypto'); const stream = require('stream'); const s = new stream.PassThrough(); -const h = process.features.openssl_is_boringssl ? +const h = isBoringSSL ? crypto.createHash('sha512') : crypto.createHash('sha3-512'); -const expect = process.features.openssl_is_boringssl ? +const expect = isBoringSSL ? 'fba055c6fd0c5b6645407749ed7a8b41' + 'b8f629f2163c3ca3701d864adabda1f8' + '93c37bf82b22fdd151ba8e357f611da4' + diff --git a/test/parallel/test-crypto-hash.js b/test/parallel/test-crypto-hash.js index 00f2240c876..94b73525678 100644 --- a/test/parallel/test-crypto-hash.js +++ b/test/parallel/test-crypto-hash.js @@ -1,5 +1,7 @@ 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) { common.skip('missing crypto'); } @@ -189,7 +191,7 @@ assert.throws( } // Test XOF hash functions and the outputLength option. -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { // Default outputLengths. assert.strictEqual(crypto.createHash('shake128').digest('hex'), '7f9c2ba4e88f827d616045507605853e'); diff --git a/test/parallel/test-crypto-hkdf.js b/test/parallel/test-crypto-hkdf.js index 80cf5e9aff5..7051c2fae07 100644 --- a/test/parallel/test-crypto-hkdf.js +++ b/test/parallel/test-crypto-hkdf.js @@ -13,7 +13,7 @@ const { hkdfSync, getHashes } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); { assert.throws(() => hkdf(), { @@ -137,12 +137,12 @@ const { hasOpenSSL3 } = require('../common/crypto'); } const algorithms = [ - ['sha256', 'secret', 'salt', 'info', 10], + ['sha256', '0123456789abcdef', '0123456789abcdef', 'info', 10], ['sha256', '', '', '', 10], ['sha256', '', 'salt', '', 10], ['sha512', 'secret', 'salt', '', 15], ]; -if (!hasOpenSSL3 && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3) && !isBoringSSL) algorithms.push(['whirlpool', 'secret', '', 'info', 20]); algorithms.forEach(([ hash, secret, salt, info, length ]) => { @@ -233,7 +233,7 @@ algorithms.forEach(([ hash, secret, salt, info, length ]) => { }); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { const kKnownUnsupported = ['shake128', 'shake256']; for (const hash of getHashes()) { if (kKnownUnsupported.includes(hash)) continue; diff --git a/test/parallel/test-crypto-hmac.js b/test/parallel/test-crypto-hmac.js index 79abba6952b..ca6a424c529 100644 --- a/test/parallel/test-crypto-hmac.js +++ b/test/parallel/test-crypto-hmac.js @@ -6,6 +6,9 @@ if (!common.hasCrypto) { const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); + +const fips3 = hasFIPS(3); { const Hmac = crypto.Hmac; @@ -24,7 +27,7 @@ assert.throws( // This used to segfault. See: https://github.com/nodejs/node/issues/9819 assert.throws( - () => crypto.createHmac('sha256', 'key').digest({ + () => crypto.createHmac('sha256', '0123456789abcdef').digest({ toString: () => { throw new Error('boom'); }, }), { @@ -40,9 +43,14 @@ assert.throws( }); function testHmac(algo, key, data, expected) { - // FIPS does not support MD5. - if (crypto.getFips() && algo === 'md5') + if (crypto.getFips() === 1 && algo === 'md5') { + if (fips3) { + assert.throws(() => crypto.createHmac(algo, Buffer.alloc(32)), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); + } return; + } if (!Array.isArray(data)) data = [data]; @@ -70,12 +78,13 @@ function testHmac(algo, key, data, expected) { { // Historically, dss1 and DSS1 are SHA-1 aliases. + const key = '0123456789abcdef'; const expected = - crypto.createHmac('sha1', 'key').update('data').digest('hex'); + crypto.createHmac('sha1', key).update('data').digest('hex'); for (const algo of ['dss1', 'DSS1']) { assert.strictEqual( - crypto.createHmac(algo, 'key').update('data').digest('hex'), + crypto.createHmac(algo, key).update('data').digest('hex'), expected); } } @@ -439,9 +448,12 @@ const rfc2202_sha1 = [ for (const { key, data, hmac } of rfc2202_sha1) testHmac('sha1', key, data, hmac); -assert.strictEqual( - crypto.createHmac('sha256', 'w00t').digest('ucs2'), - crypto.createHmac('sha256', 'w00t').digest().toString('ucs2')); +{ + const key = '0123456789abcdef'; + assert.strictEqual( + crypto.createHmac('sha256', key).digest('ucs2'), + crypto.createHmac('sha256', key).digest().toString('ucs2')); +} // Check initialized -> uninitialized state transition after calling digest(). { @@ -480,7 +492,7 @@ assert.strictEqual( { assert.throws( - () => crypto.createHmac('sha7', 'key'), + () => crypto.createHmac('sha7', '0123456789abcdef'), /Invalid digest/); } diff --git a/test/parallel/test-crypto-jwk-raw-validation.js b/test/parallel/test-crypto-jwk-raw-validation.js new file mode 100644 index 00000000000..d2085b43529 --- /dev/null +++ b/test/parallel/test-crypto-jwk-raw-validation.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { createPrivateKey } = require('node:crypto'); +const fixtures = require('../common/fixtures'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); + +const cases = [ + ['ed25519_private.pem', 'OKP', 'crv', 'x', 'd'], +]; +if (hasOpenSSL(3, 5) || isBoringSSL) { + cases.push(['ml_dsa_44_private_seed_only.pem', 'AKP', 'alg', 'pub', 'priv']); +} + +for (const [file, kty, name, pub, priv] of cases) { + const jwk = createPrivateKey(fixtures.readKey(file)).export({ format: 'jwk' }); + const invalid = { code: 'ERR_CRYPTO_INVALID_JWK', message: `Invalid JWK ${kty} key` }; + const invalidName = kty === 'AKP' ? { + code: 'ERR_CRYPTO_INVALID_JWK', message: 'Unsupported JWK AKP "alg"', + } : invalid; + const importKey = (key) => createPrivateKey({ format: 'jwk', key }); + + assert.throws(() => importKey({ ...jwk, [name]: 'unknown' }), invalidName); + assert.throws(() => importKey({ ...jwk, [name]: jwk[name].toLowerCase() }), invalidName); + assert.throws(() => importKey({ ...jwk, [pub]: undefined }), invalid); + assert.throws(() => importKey({ ...jwk, [priv]: 1 }), invalid); + assert.throws(() => importKey({ ...jwk, [name]: 'unknown', [pub]: undefined }), invalidName); + + // A recognized algorithm from the other schema remains invalid. + const otherName = kty === 'AKP' ? 'Ed25519' : 'ML-DSA-44'; + assert.throws(() => importKey({ ...jwk, [name]: otherName }), invalidName); + + for (const field of [name, pub, priv]) { + const error = new Error(`getter for ${field}`); + const key = { ...jwk }; + Object.defineProperty(key, field, { get() { throw error; } }); + assert.throws(() => importKey(key), (actual) => actual === error); + } +} diff --git a/test/parallel/test-crypto-key-encoding-provider-cipher.js b/test/parallel/test-crypto-key-encoding-provider-cipher.js new file mode 100644 index 00000000000..3ded8ed0a62 --- /dev/null +++ b/test/parallel/test-crypto-key-encoding-provider-cipher.js @@ -0,0 +1,113 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasFIPS, hasOpenSSL, isBoringSSL } = require('../common/crypto'); +if (isBoringSSL || !hasOpenSSL(3)) + common.skip('OpenSSL providers are required'); + +const assert = require('assert'); +const { + createPrivateKey, + createPublicKey, + generateKeyPair, + generateKeyPairSync, + getCiphers, + sign, + verify, +} = require('crypto'); + +// Resolve an AES-256-CBC provider alias and retain the fetched cipher while +// encoding private keys and running asynchronous key generation jobs. +const cipher = '2.16.840.1.101.3.4.1.42'; +const passphrase = 'provider cipher passphrase'; +const keyOptions = { namedCurve: 'prime256v1' }; +const { publicKey, privateKey } = generateKeyPairSync('ec', keyOptions); +const data = Buffer.from('encrypted private key'); + +function checkPrivateKey(encoded, format, type, expectedPublicKey) { + const decrypted = createPrivateKey({ + key: encoded, + format, + type, + passphrase, + }); + assert(createPublicKey(decrypted).equals(expectedPublicKey)); + const signature = sign('sha256', data, decrypted); + assert(verify('sha256', data, expectedPublicKey, signature)); +} + +for (const format of ['pem', 'der']) { + const privateKeyEncoding = { + format, + type: 'pkcs8', + cipher, + passphrase, + }; + + const exported = privateKey.export(privateKeyEncoding); + if (format === 'pem') + assert.match(exported, /^-----BEGIN ENCRYPTED PRIVATE KEY-----/); + checkPrivateKey(exported, format, 'pkcs8', publicKey); + + const generated = generateKeyPairSync('ec', { + ...keyOptions, + privateKeyEncoding, + }); + checkPrivateKey(generated.privateKey, format, 'pkcs8', generated.publicKey); + + // Async jobs copy the encoding configuration. Its fetched cipher must remain + // valid after the configuration used to create the job has been destroyed. + generateKeyPair('ec', { + ...keyOptions, + privateKeyEncoding, + }, common.mustSucceed((generatedPublicKey, generatedPrivateKey) => { + checkPrivateKey(generatedPrivateKey, format, 'pkcs8', generatedPublicKey); + })); +} + +// Provider lookup must preserve the restrictions of each key format. +const sec1 = { format: 'pem', type: 'sec1', cipher, passphrase }; +// Traditional PEM encryption uses MD5 to derive its key. +if (!hasFIPS()) + checkPrivateKey(privateKey.export(sec1), 'pem', 'sec1', publicKey); +assert.throws(() => privateKey.export({ ...sec1, format: 'der' }), { + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', +}); + +const unknownCipher = { + format: 'pem', + type: 'pkcs8', + cipher: 'unknown-private-key-cipher', + passphrase, +}; +const unknownCipherError = { + code: 'ERR_CRYPTO_UNKNOWN_CIPHER', + message: 'Unknown cipher', +}; +assert.throws(() => privateKey.export(unknownCipher), unknownCipherError); +assert.throws(() => generateKeyPairSync('ec', { + ...keyOptions, + privateKeyEncoding: unknownCipher, +}), unknownCipherError); +assert.throws(() => generateKeyPair('ec', { + ...keyOptions, + privateKeyEncoding: unknownCipher, +}, common.mustNotCall()), unknownCipherError); + +// Provider-only ciphers must reach the serializer, which still rejects ciphers +// without an ASN.1 identifier when writing PKCS8. +if (getCiphers().includes('aes-128-cbc-cts')) { + for (const format of ['pem', 'der']) { + assert.throws(() => privateKey.export({ + format, + type: 'pkcs8', + cipher: 'aes-128-cbc-cts', + passphrase, + }), { + code: 'ERR_OSSL_ASN1_CIPHER_HAS_NO_OBJECT_IDENTIFIER', + }); + } +} diff --git a/test/parallel/test-crypto-key-objects-ec-jwk-private.js b/test/parallel/test-crypto-key-objects-ec-jwk-private.js new file mode 100644 index 00000000000..bede4e2f31c --- /dev/null +++ b/test/parallel/test-crypto-key-objects-ec-jwk-private.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createECDH, + createPrivateKey, + createPublicKey, + getCurves, + getFips, + sign, + verify, +} = require('crypto'); + +const curves = [ + ['prime256v1', 'P-256', 32, + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'], + ['secp384r1', 'P-384', 48, + 'ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf' + + '581a0db248b0a77aecec196accc52973'], + ['secp521r1', 'P-521', 66, + '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + + 'fa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'], +]; +if (!getFips() && getCurves().includes('secp256k1')) { + curves.push(['secp256k1', 'secp256k1', 32, + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141']); +} + +for (const [namedCurve, crv, width, orderHex] of curves) { + const order = BigInt(`0x${orderHex}`); + const encode = (scalar) => Buffer.from( + scalar.toString(16).padStart(width * 2, '0'), 'hex'); + const makeJwk = (scalar) => { + const ecdh = createECDH(namedCurve); + ecdh.setPrivateKey(encode(scalar)); + const point = ecdh.getPublicKey(); + return { + kty: 'EC', + crv, + x: point.subarray(1, 1 + width).toString('base64url'), + y: point.subarray(1 + width).toString('base64url'), + d: encode(scalar).toString('base64url'), + }; + }; + const generator = makeJwk(1n); + const other = makeJwk(2n); + const message = Buffer.from('EC JWK private key consistency'); + + for (const jwk of [generator, other, makeJwk(order - 1n)]) { + const key = createPrivateKey({ format: 'jwk', key: jwk }); + assert.deepStrictEqual(key.export({ format: 'jwk' }), jwk); + const publicJwk = { kty: jwk.kty, crv, x: jwk.x, y: jwk.y }; + const publicKey = createPublicKey({ format: 'jwk', key: publicJwk }); + assert(verify('sha256', message, publicKey, sign('sha256', message, key))); + } + + const invalid = [ + { ...generator, d: other.d }, + { ...generator, x: other.x, y: other.y }, + ...[0n, order, order + 1n].map((scalar) => ({ + ...generator, d: encode(scalar).toString('base64url'), + })), + { ...generator, d: '' }, + { + ...generator, + x: Buffer.alloc(width).toString('base64url'), + y: Buffer.alloc(width).toString('base64url'), + }, + ]; + for (const jwk of invalid) { + assert.throws(() => createPrivateKey({ format: 'jwk', key: jwk }), { + code: 'ERR_CRYPTO_INVALID_JWK', + }); + } +} diff --git a/test/parallel/test-crypto-key-objects-messageport.js b/test/parallel/test-crypto-key-objects-messageport.js index d23fde0d00d..a379b84185c 100644 --- a/test/parallel/test-crypto-key-objects-messageport.js +++ b/test/parallel/test-crypto-key-objects-messageport.js @@ -9,6 +9,7 @@ const { generateKeyPairSync, KeyObject, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; const { createContext } = require('vm'); const { @@ -45,7 +46,7 @@ process.env.HAS_STARTED_WORKER = 1; // The main thread generates keys and passes them to worker threads. const secretKey = generateKeySync('aes', { length: 128 }); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024 + modulusLength: hasFIPS(3) ? 2048 : 1024 }); const cryptoKey = await subtle.generateKey( { name: 'AES-CBC', length: 128 }, false, ['encrypt']); diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js index e8d5aced4e2..5fee248e148 100644 --- a/test/parallel/test-crypto-key-objects-raw.js +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -7,7 +7,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); // EC: NIST and OpenSSL curve names are both recognized for raw-public and raw-private { @@ -67,7 +67,7 @@ const { hasOpenSSL } = require('../common/crypto'); ['x25519', 'x25519_public.pem'], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { rawPublicKeys.push( ['ed448', 'ed448_public.pem'], ['x448', 'x448_public.pem'], @@ -76,7 +76,7 @@ const { hasOpenSSL } = require('../common/crypto'); common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); } - if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + if (hasOpenSSL(3, 5) || isBoringSSL) { rawPublicKeys.push( ['ml-dsa-44', 'ml_dsa_44_public.pem'], ['ml-kem-768', 'ml_kem_768_public.pem'], @@ -122,7 +122,7 @@ if (hasOpenSSL(3, 5)) { const unsupportedKeyTypes = [ ['rsa', 'rsa_public_2048.pem', 'rsa_private_2048.pem'], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { unsupportedKeyTypes.push(['dsa', 'dsa_public.pem', 'dsa_private.pem']); } else { common.printSkipMessage('Skipping unsupported dsa test case'); @@ -148,7 +148,7 @@ if (hasOpenSSL(3, 5)) { } // DH keys also don't support raw formats - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { const privKeyObj = crypto.createPrivateKey( fixtures.readKey('dh_private.pem', 'ascii')); assert.throws(() => privKeyObj.export({ format: 'raw-private' }), @@ -174,7 +174,7 @@ if (hasOpenSSL(3, 5)) { // PQC import throws when PQC is not supported if (!hasOpenSSL(3, 5)) { - const unsupported = process.features.openssl_is_boringssl ? + const unsupported = isBoringSSL ? // BoringSSL supports ML-DSA and ML-KEM-{768,1024}, but not ML-KEM-512 or SLH-DSA. ['ml-kem-512', 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f'] : [ @@ -236,7 +236,7 @@ if (!hasOpenSSL(3, 5)) { } // ML-KEM: public keys of different type cannot be imported as the other type -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { const mlKem768Pub = crypto.createPublicKey( fixtures.readKey('ml_kem_768_public.pem', 'ascii')); const mlKem1024Pub = crypto.createPublicKey( @@ -255,7 +255,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { } // ML-DSA: -44 and -65 public keys cannot be imported as the other type -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { const mlDsa44Pub = crypto.createPublicKey( fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); const mlDsa65Pub = crypto.createPublicKey( @@ -337,10 +337,10 @@ if (hasOpenSSL(3, 5)) { namedCurve: 'P-256', }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); } - for (const type of process.features.openssl_is_boringssl ? + for (const type of isBoringSSL ? ['ed25519', 'x25519'] : ['ed25519', 'ed448', 'x25519', 'x448']) { const priv = crypto.createPrivateKey( @@ -368,7 +368,7 @@ if (hasOpenSSL(3, 5)) { } // raw-private cannot be used for ml-kem and ml-dsa -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { for (const type of ['ml-kem-768', 'ml-dsa-44']) { const priv = crypto.createPrivateKey( fixtures.readKey(`${type.replaceAll('-', '_')}_private_seed_only.pem`, 'ascii')); @@ -476,7 +476,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { { code: 'ERR_INVALID_ARG_VALUE' }); // PQC raw-seed -> createPublicKey - if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + if (hasOpenSSL(3, 5) || isBoringSSL) { const mlDsaPriv = crypto.createPrivateKey( fixtures.readKey('ml_dsa_44_private_seed_only.pem', 'ascii')); const mlDsaPub = crypto.createPublicKey( @@ -525,10 +525,10 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // x25519, ed25519, x448, and ed448 cannot be used as 'ec' namedCurve values { - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); } - for (const type of process.features.openssl_is_boringssl ? + for (const type of isBoringSSL ? ['ed25519', 'x25519'] : ['ed25519', 'x25519', 'ed448', 'x448']) { const priv = crypto.createPrivateKey( diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index a498df144f3..a8fe94f1dd4 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -12,7 +12,10 @@ const { randomBytes, generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { kSupportedAlgorithms } = require('internal/crypto/util'); +const fips = hasFIPS(); +const rejectsXCurves = hasFIPS(3, 5); const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => { return name.startsWith('SHA-') || name.startsWith('SHA3-'); @@ -133,7 +136,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - if (allowZeroKey) { + if (allowZeroKey && !fips) { const zeroKey = createSecretKey(Buffer.alloc(0)) .toCryptoKey(algorithm, true, usages); assert.strictEqual(zeroKey.algorithm.length, 0); @@ -141,6 +144,16 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const explicitZeroKey = createSecretKey(Buffer.alloc(0)) .toCryptoKey({ ...algorithm, length: 0 }, true, usages); assert.strictEqual(explicitZeroKey.algorithm.length, 0); + } else if (allowZeroKey) { + for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) { + assert.throws(() => { + createSecretKey(Buffer.alloc(0)) + .toCryptoKey(zeroAlgorithm, true, usages); + }, { + name: 'NotSupportedError', + message: 'Invalid key length', + }); + } } else { assert.throws(() => { createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); @@ -155,12 +168,15 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { message: 'Usages cannot be empty when importing a secret key.' }); - assert.throws(() => { - key.toCryptoKey({ ...algorithm, length: 0 }, true, usages); - }, { - name: 'DataError', - message: invalidLengthMessage, - }); + assert.throws( + () => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages), + allowZeroKey && fips ? { + name: 'NotSupportedError', + message: 'Invalid key length', + } : { + name: 'DataError', + message: invalidLengthMessage, + }); } function hmacVectors() { @@ -232,6 +248,14 @@ function ecVectors(name, usagesByType) { } function cfrgVectors(name, usagesByType) { + if (rejectsXCurves && name.startsWith('X')) { + assert.throws(() => generateKeyPairSync(name.toLowerCase()), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + return []; + } + const keyPair = generateKeyPairSync(name.toLowerCase()); return asymmetricVectors(keyPair, name, usagesByType); } @@ -317,9 +341,11 @@ const invalid = { 'HMAC': () => macInvalid( { name: 'HMAC', hash: 'SHA-256' }, 'HmacImportParams.length cannot be 0'), - 'X25519': () => invalidAsymmetricKeyType('X25519', 'Ed25519'), }; +if (!rejectsXCurves) + invalid.X25519 = () => invalidAsymmetricKeyType('X25519', 'Ed25519'); + for (const name of ['AES-CBC', 'AES-CTR', 'AES-GCM', 'AES-OCB']) { if (name in kSupportedAlgorithms.importKey) tests[name] = symmetricVectors(name, ['encrypt', 'decrypt']); @@ -350,9 +376,11 @@ for (const [name, usages, invalidAlgorithm] of [ ]) { if (name in kSupportedAlgorithms.importKey) { tests[name] = cfrgVectors(name, usages); - invalid[name] = () => { - invalidAsymmetricKeyType(name, invalidAlgorithm); - }; + if (!rejectsXCurves) { + invalid[name] = () => { + invalidAsymmetricKeyType(name, invalidAlgorithm); + }; + } } } @@ -370,7 +398,11 @@ for (const name of ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']) { } } -const unsupportedToCryptoKeyAlgorithms = new Set(); +const unsupportedToCryptoKeyAlgorithms = new Set([ + 'MLKEM768-P256', + 'MLKEM768-X25519', + 'MLKEM1024-P384', +]); for (const name of Object.keys(kSupportedAlgorithms.importKey)) { const vectors = tests[name]; diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index cfcd50f12a3..f4af79e31b5 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -24,7 +24,18 @@ const { generateKeyPairSync, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, + isBoringSSL, +} = require('../common/crypto'); + +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips30 = fips3 && !fips35; +const fips4 = hasFIPS(4); +const rejectsXCurves = fips35; +const fipsDigestErrorCode = 'ERR_OSSL_DIGEST_NOT_ALLOWED'; const fixtures = require('../common/fixtures'); @@ -201,15 +212,26 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', // It should also be possible to import an encrypted private key as a public // key. + const passphrase = 'password'; + if (fips4) { + assert.throws(() => privateKey.export({ + type: 'pkcs8', + format: 'pem', + passphrase: '123', + cipher: 'aes-128-cbc' + }), { + code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + }); + } const decryptedKey = createPublicKey({ key: privateKey.export({ type: 'pkcs8', format: 'pem', - passphrase: '123', + passphrase, cipher: 'aes-128-cbc' }), format: 'pem', - passphrase: '123' + passphrase }); assert.strictEqual(decryptedKey.type, 'public'); assert.strictEqual(decryptedKey.asymmetricKeyType, 'rsa'); @@ -322,9 +344,9 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', // This should not cause a crash: https://github.com/nodejs/node/issues/25247 assert.throws(() => { createPrivateKey({ key: '' }); - }, hasOpenSSL3 ? { + }, hasOpenSSL(3) ? { message: 'error:1E08010C:DECODER routines::unsupported', - } : process.features.openssl_is_boringssl ? { + } : isBoringSSL ? { message: 'error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE', code: 'ERR_OSSL_PEM_NO_START_LINE', reason: 'NO_START_LINE', @@ -354,10 +376,10 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', type: 'pkcs1' }); createPrivateKey({ key, format: 'der', type: 'pkcs1' }); - }, hasOpenSSL3 ? { + }, hasOpenSSL(3) ? { message: /error:1E08010C:DECODER routines::unsupported/, library: 'DECODER routines' - } : process.features.openssl_is_boringssl ? { + } : isBoringSSL ? { library: 'public key routines', message: 'error:06000066:public key routines:OPENSSL_internal:DECODE_ERROR' } : { @@ -444,8 +466,10 @@ for (const info of [ } }, ]) { const keyType = info.keyType; + const fipsUnsupported = + rejectsXCurves && keyType.startsWith('x'); - if (process.features.openssl_is_boringssl && keyType.endsWith('448')) { + if (isBoringSSL && keyType.endsWith('448')) { common.printSkipMessage(`Skipping unsupported ${keyType} test case`); continue; } @@ -462,19 +486,32 @@ for (const info of [ } { - const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); - assert.strictEqual(key.type, 'private'); - assert.strictEqual(key.asymmetricKeyType, keyType); - assert.strictEqual(key.symmetricKeySize, undefined); - assert.strictEqual( - key.export({ type: 'pkcs8', format: 'pem' }), info.private); - assert.deepStrictEqual( - key.export({ format: 'jwk' }), info.jwk); + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } } { - for (const input of [ - info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const inputs = [info.private, info.public]; + if (fipsUnsupported) { + assert.throws( + () => createPublicKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + inputs.push({ key: info.jwk, format: 'jwk' }); + } + for (const input of inputs) { const key = createPublicKey(input); assert.strictEqual(key.type, 'public'); assert.strictEqual(key.asymmetricKeyType, keyType); @@ -498,21 +535,32 @@ for (const info of [ assert(Buffer.isBuffer(rawPriv)); assert(Buffer.isBuffer(rawPub)); - const importedPriv = createPrivateKey({ + const privateOptions = { key: rawPriv, format: 'raw-private', asymmetricKeyType: keyType, - }); - assert.strictEqual(importedPriv.type, 'private'); - assert.strictEqual(importedPriv.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPriv.export({ format: 'raw-private' }), rawPriv); - - const importedPub = createPublicKey({ + }; + const publicOptions = { key: rawPub, format: 'raw-public', asymmetricKeyType: keyType, - }); - assert.strictEqual(importedPub.type, 'public'); - assert.strictEqual(importedPub.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPub.export({ format: 'raw-public' }), rawPub); + }; + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey(privateOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws( + () => createPublicKey(publicOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + } else { + const importedPriv = createPrivateKey(privateOptions); + assert.strictEqual(importedPriv.type, 'private'); + assert.strictEqual(importedPriv.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPriv.export({ format: 'raw-private' }), rawPriv); + + const importedPub = createPublicKey(publicOptions); + assert.strictEqual(importedPub.type, 'public'); + assert.strictEqual(importedPub.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPub.export({ format: 'raw-public' }), rawPub); + } } } @@ -602,8 +650,9 @@ for (const info of [ } }, ]) { const { keyType, namedCurve } = info; + const fipsUnsupported = fips3 && namedCurve === 'secp256k1'; - if (process.features.openssl_is_boringssl && !getCurves().includes(namedCurve)) { + if (isBoringSSL && !getCurves().includes(namedCurve)) { common.printSkipMessage(`Skipping unsupported ${keyType} test case`); continue; } @@ -621,20 +670,33 @@ for (const info of [ } { - const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); - assert.strictEqual(key.type, 'private'); - assert.strictEqual(key.asymmetricKeyType, keyType); - assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); - assert.strictEqual(key.symmetricKeySize, undefined); - assert.strictEqual( - key.export({ type: 'pkcs8', format: 'pem' }), info.private); - assert.deepStrictEqual( - key.export({ format: 'jwk' }), info.jwk); + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } } { - for (const input of [ - info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const inputs = [info.private, info.public]; + if (fipsUnsupported) { + assert.throws( + () => createPublicKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + inputs.push({ key: info.jwk, format: 'jwk' }); + } + for (const input of inputs) { const key = createPublicKey(input); assert.strictEqual(key.type, 'public'); assert.strictEqual(key.asymmetricKeyType, keyType); @@ -669,23 +731,35 @@ for (const info of [ assert(Buffer.isBuffer(rawPriv)); assert(Buffer.isBuffer(rawPub)); - const importedPriv = createPrivateKey({ + const privateOptions = { key: rawPriv, format: 'raw-private', asymmetricKeyType: keyType, namedCurve, - }); - assert.strictEqual(importedPriv.type, 'private'); - assert.strictEqual(importedPriv.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPriv.export({ format: 'raw-private' }), rawPriv); + }; - const importedPub = createPublicKey({ + const publicOptions = { key: rawPub, format: 'raw-public', asymmetricKeyType: keyType, namedCurve, - }); - assert.strictEqual(importedPub.type, 'public'); - assert.strictEqual(importedPub.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPub.export({ format: 'raw-public' }), rawPub); + }; + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey(privateOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws( + () => createPublicKey(publicOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + } else { + const importedPriv = createPrivateKey(privateOptions); + assert.strictEqual(importedPriv.type, 'private'); + assert.strictEqual(importedPriv.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPriv.export({ format: 'raw-private' }), rawPriv); + + const importedPub = createPublicKey(publicOptions); + assert.strictEqual(importedPub.type, 'public'); + assert.strictEqual(importedPub.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPub.export({ format: 'raw-public' }), rawPub); + } } } @@ -728,7 +802,7 @@ for (const info of [ { // Reading an encrypted key without a passphrase should fail. - assert.throws(() => createPrivateKey(privateDsa), hasOpenSSL3 ? { + assert.throws(() => createPrivateKey(privateDsa), hasOpenSSL(3) ? { name: 'Error', message: 'error:07880109:common libcrypto routines::interrupted or ' + 'cancelled', @@ -744,7 +818,7 @@ for (const info of [ key: privateDsa, format: 'pem', passphrase: Buffer.alloc(1025, 'a') - }), hasOpenSSL3 ? { name: 'Error' } : { + }), hasOpenSSL(3) ? { name: 'Error' } : { code: 'ERR_OSSL_PEM_BAD_PASSWORD_READ', name: 'Error' }); @@ -755,8 +829,10 @@ for (const info of [ key: privateDsa, format: 'pem', passphrase: Buffer.alloc(1024, 'a') - }), { - message: /bad decrypt|BAD_DECRYPT/ + }), fips4 ? { + code: 'ERR_OSSL_INVALID_SALT_LENGTH', + } : { + message: /bad decrypt|BAD_DECRYPT/, }); const publicKey = createPublicKey(publicDsa); @@ -767,11 +843,28 @@ for (const info of [ () => publicKey.export({ format: 'jwk' }), { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); - const privateKey = createPrivateKey({ - key: privateDsa, + const privateKeyData = + createPrivateKey(fixtures.readKey('dsa_private.pem')).export({ + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: 'password', + }); + const privateKeyOptions = { + key: privateKeyData, format: 'pem', - passphrase: 'secret' - }); + passphrase: 'password', + }; + if (fips4) { + assert.throws(() => createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: 'secret', + }), { + code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + }); + } + const privateKey = createPrivateKey(privateKeyOptions); assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'dsa'); assert.strictEqual(privateKey.symmetricKeySize, undefined); @@ -780,7 +873,7 @@ for (const info of [ { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); } -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { // Test RSA-PSS. { // This key pair does not restrict the message digest algorithm or salt @@ -815,20 +908,27 @@ if (!process.features.openssl_is_boringssl) { { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); for (const key of [privatePem, privateKey]) { - // Any algorithm should work. - for (const algo of ['sha1', 'sha256']) { - // Any salt length should work. - for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { - const signature = createSign(algo) - .update('foo') - .sign({ key, saltLength }); - - for (const pkey of [key, publicKey, publicPem]) { - const okay = createVerify(algo) - .update('foo') - .verify({ key: pkey, saltLength }, signature); - - assert.ok(okay); + if (fips30) { + // With no explicit parameters, this key defaults to SHA-1 for PSS. + assert.throws(() => createSign('sha256').update('foo').sign(key), { + code: fipsDigestErrorCode, + }); + } else { + // Any algorithm should work. + for (const algo of ['sha1', 'sha256']) { + // Any salt length should work. + for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { + const signature = createSign(algo) + .update('foo') + .sign({ key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify(algo) + .update('foo') + .verify({ key: pkey, saltLength }, signature); + + assert.ok(okay); + } } } } @@ -896,12 +996,15 @@ if (!process.features.openssl_is_boringssl) { // Signing with anything other than sha256 should fail. assert.throws(() => { createSign('sha1').sign(key); - }, /digest not allowed/); + }, fips30 ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); // Signing with salt lengths less than 16 bytes should fail. for (const saltLength of [8, 10, 12]) { assert.throws(() => { - createSign('sha1').sign({ key, saltLength }); + createSign(fips3 ? 'sha256' : 'sha1') + .sign({ key, saltLength }); }, /pss saltlen too small/); } @@ -994,7 +1097,9 @@ if (!process.features.openssl_is_boringssl) { for (const algo of ['sha1', 'sha256']) { assert.throws(() => { createSign(algo).sign(key); - }, /digest not allowed/); + }, fips30 && algo === 'sha1' ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); } // sha512 should produce a valid signature. @@ -1053,24 +1158,32 @@ if (!process.features.openssl_is_boringssl) { { // Exporting a JWK unsupported curve EC key const supported = ['prime256v1', 'secp256k1', 'secp384r1', 'secp521r1']; - // Find an unsupported curve regardless of whether a FIPS compliant crypto - // provider is currently in use. - const namedCurve = getCurves().find((curve) => !supported.includes(curve)); + // FIPS-disallowed curves are omitted from getCurves(). Select one explicitly + // to keep checking their rejection; otherwise find a curve JWK cannot encode. + const namedCurve = fips3 ? 'secp256k1' : + getCurves().find((curve) => !supported.includes(curve)); assert(namedCurve); - const keyPair = generateKeyPairSync('ec', { namedCurve }); - const { publicKey, privateKey } = keyPair; - assert.throws( - () => publicKey.export({ format: 'jwk' }), - { - code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', - message: `Unsupported JWK EC curve: ${namedCurve}.` - }); - assert.throws( - () => privateKey.export({ format: 'jwk' }), - { - code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', - message: `Unsupported JWK EC curve: ${namedCurve}.` + if (fips3) { + assert.throws(() => generateKeyPairSync('ec', { namedCurve }), { + name: 'Error', + message: 'error:08000081:elliptic curve routines::unknown group', }); + } else { + const keyPair = generateKeyPairSync('ec', { namedCurve }); + const { publicKey, privateKey } = keyPair; + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); + } } { @@ -1113,12 +1226,19 @@ if (!process.features.openssl_is_boringssl) { { const first = generateKeyPairSync('ed25519'); - const second = generateKeyPairSync('x25519'); + if (rejectsXCurves) { + assert.throws(() => generateKeyPairSync('x25519'), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + } else { + const second = generateKeyPairSync('x25519'); - assert(!first.publicKey.equals(second.publicKey)); - assert(!first.publicKey.equals(second.privateKey)); - assert(!first.privateKey.equals(second.privateKey)); - assert(!first.privateKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.publicKey)); + } } { diff --git a/test/parallel/test-crypto-key-store.js b/test/parallel/test-crypto-key-store.js index b6f01241667..e777440f2c0 100644 --- a/test/parallel/test-crypto-key-store.js +++ b/test/parallel/test-crypto-key-store.js @@ -2,7 +2,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL 3.x'); @@ -84,26 +84,33 @@ const data = Buffer.from('hello store'); } { - const alice = generateKeyPairSync('x25519'); - const bob = generateKeyPairSync('x25519'); - const file = path.join(tmpdir.path, 'x25519.pem'); - fs.writeFileSync(file, alice.privateKey.export({ - format: 'pem', - type: 'pkcs8', - })); - const url = pathToFileURL(file); + if (hasFIPS(3, 5)) { + assert.throws(() => generateKeyPairSync('x25519'), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + } else { + const alice = generateKeyPairSync('x25519'); + const bob = generateKeyPairSync('x25519'); + const file = path.join(tmpdir.path, 'x25519.pem'); + fs.writeFileSync(file, alice.privateKey.export({ + format: 'pem', + type: 'pkcs8', + })); + const url = pathToFileURL(file); - const expected = diffieHellman({ - privateKey: alice.privateKey, - publicKey: bob.publicKey, - }); - assert.deepStrictEqual( - diffieHellman({ privateKey: url, publicKey: bob.publicKey }), - expected); + const expected = diffieHellman({ + privateKey: alice.privateKey, + publicKey: bob.publicKey, + }); + assert.deepStrictEqual( + diffieHellman({ privateKey: url, publicKey: bob.publicKey }), + expected); - if (hasOpenSSL(3, 2)) { - const { sharedKey, ciphertext } = encapsulate(alice.publicKey); - assert.deepStrictEqual(decapsulate(url, ciphertext), sharedKey); + if (hasOpenSSL(3, 2)) { + const { sharedKey, ciphertext } = encapsulate(alice.publicKey); + assert.deepStrictEqual(decapsulate(url, ciphertext), sharedKey); + } } } diff --git a/test/parallel/test-crypto-keygen-async-dsa-key-object.js b/test/parallel/test-crypto-keygen-async-dsa-key-object.js index ea35facbdc7..52c82ea5725 100644 --- a/test/parallel/test-crypto-keygen-async-dsa-key-object.js +++ b/test/parallel/test-crypto-keygen-async-dsa-key-object.js @@ -4,7 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { isBoringSSL, hasOpenSSL } = require('../common/crypto'); + +if (isBoringSSL) common.skip('not supported by BoringSSL'); const assert = require('assert'); @@ -12,25 +14,23 @@ const { generateKeyPair, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); - // Test async DSA key object generation. { generateKeyPair('dsa', { - modulusLength: hasOpenSSL3 ? 2048 : 512, + modulusLength: hasOpenSSL(3) ? 2048 : 512, divisorLength: 256 }, common.mustSucceed((publicKey, privateKey) => { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'dsa'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: hasOpenSSL3 ? 2048 : 512, + modulusLength: hasOpenSSL(3) ? 2048 : 512, divisorLength: 256 }); assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'dsa'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: hasOpenSSL3 ? 2048 : 512, + modulusLength: hasOpenSSL(3) ? 2048 : 512, divisorLength: 256 }); })); diff --git a/test/parallel/test-crypto-keygen-async-dsa.js b/test/parallel/test-crypto-keygen-async-dsa.js index d7c857d35e2..5d0254491cd 100644 --- a/test/parallel/test-crypto-keygen-async-dsa.js +++ b/test/parallel/test-crypto-keygen-async-dsa.js @@ -4,20 +4,21 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) - common.skip('not supported by BoringSSL'); - -const assert = require('assert'); -const { - generateKeyPair, -} = require('crypto'); const { + isBoringSSL, assertApproximateSize, testSignVerify, spkiExp, + hasOpenSSL, } = require('../common/crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +if (isBoringSSL) + common.skip('not supported by BoringSSL'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); // Test async DSA key generation. { @@ -27,7 +28,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); }; generateKeyPair('dsa', { - modulusLength: hasOpenSSL3 ? 2048 : 512, + modulusLength: hasOpenSSL(3) ? 2048 : 512, divisorLength: 256, publicKeyEncoding: { type: 'spki', @@ -35,7 +36,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); }, privateKeyEncoding: { cipher: 'aes-128-cbc', - passphrase: 'secret', + passphrase: 'password', ...privateKeyEncoding } }, common.mustSucceed((publicKey, privateKeyDER) => { @@ -44,8 +45,8 @@ const { hasOpenSSL3 } = require('../common/crypto'); // The private key is DER-encoded. assert(Buffer.isBuffer(privateKeyDER)); - assertApproximateSize(publicKey, hasOpenSSL3 ? 1194 : 440); - assertApproximateSize(privateKeyDER, hasOpenSSL3 ? 721 : 336); + assertApproximateSize(publicKey, hasOpenSSL(3) ? 1194 : 440); + assertApproximateSize(privateKeyDER, hasOpenSSL(3) ? 721 : 336); // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => { @@ -63,7 +64,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); testSignVerify(publicKey, { key: privateKeyDER, ...privateKeyEncoding, - passphrase: 'secret' + passphrase: 'password' }); })); } diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js index b0945dcc83a..2aaab36d376 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js @@ -8,11 +8,12 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); // Test async elliptic curve key generation with 'jwk' encoding and named // curve. for (const curve of ['P-384', 'P-256', 'P-521', 'secp256k1']) { - if (process.features.openssl_is_boringssl && curve === 'secp256k1') { + if (isBoringSSL && curve === 'secp256k1') { common.printSkipMessage(`Skipping unsupported ${curve} test case`); continue; } @@ -24,7 +25,12 @@ for (const curve of ['P-384', 'P-256', 'P-521', 'secp256k1']) { privateKeyEncoding: { format: 'jwk' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3) && curve === 'secp256k1') { + assert.strictEqual(err?.message, 'error:08000081:elliptic curve routines::unknown group'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'object'); assert.strictEqual(typeof privateKey, 'object'); assert.strictEqual(publicKey.x, privateKey.x); diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js index 449d1a97f9f..f1a1bdf8322 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js @@ -8,11 +8,12 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test async elliptic curve key generation with 'jwk' encoding and RSA. { generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, publicKeyEncoding: { format: 'jwk' }, diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js index 731960b0d56..84ecccd55ee 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js @@ -8,11 +8,13 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); +const rejectsXCurves = hasFIPS(3, 5); // Test async elliptic curve key generation with 'jwk' encoding. { for (const type of ['ed25519', 'ed448', 'x25519', 'x448']) { - if (process.features.openssl_is_boringssl && type.endsWith('448')) { + if (isBoringSSL && type.endsWith('448')) { common.printSkipMessage(`Skipping unsupported ${type} test case`); continue; } @@ -23,7 +25,12 @@ const { privateKeyEncoding: { format: 'jwk' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (rejectsXCurves && type.startsWith('x')) { + assert.strictEqual(err?.message, 'error:0308010C:digital envelope routines::unsupported'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'object'); assert.strictEqual(typeof privateKey, 'object'); assert.strictEqual(publicKey.x, privateKey.x); diff --git a/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js b/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js index 3203dfe16eb..c1bc6fc95ea 100644 --- a/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js +++ b/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js @@ -10,15 +10,17 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, } = require('../common/crypto'); // Test async RSA key generation with an encrypted private key, but encoded as DER. { + const isFips = hasFIPS(3); generateKeyPair('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'der' @@ -29,7 +31,7 @@ const { } }, common.mustSucceed((publicKeyDER, privateKeyDER) => { assert(Buffer.isBuffer(publicKeyDER)); - assertApproximateSize(publicKeyDER, 74); + assertApproximateSize(publicKeyDER, isFips ? 270 : 74); assert(Buffer.isBuffer(privateKeyDER)); diff --git a/test/parallel/test-crypto-keygen-async-encrypted-private-key.js b/test/parallel/test-crypto-keygen-async-encrypted-private-key.js index 727cccc6f3e..37a0121742d 100644 --- a/test/parallel/test-crypto-keygen-async-encrypted-private-key.js +++ b/test/parallel/test-crypto-keygen-async-encrypted-private-key.js @@ -10,15 +10,18 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, } = require('../common/crypto'); // Test async RSA key generation with an encrypted private key, but encoded as DER. { + const isFips = hasFIPS(3); + const passphrase = 'password'; generateKeyPair('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'der' @@ -27,11 +30,11 @@ const { type: 'pkcs8', format: 'der', cipher: 'aes-256-cbc', - passphrase: 'secret' + passphrase } }, common.mustSucceed((publicKeyDER, privateKeyDER) => { assert(Buffer.isBuffer(publicKeyDER)); - assertApproximateSize(publicKeyDER, 74); + assertApproximateSize(publicKeyDER, isFips ? 270 : 74); assert(Buffer.isBuffer(privateKeyDER)); @@ -59,7 +62,7 @@ const { key: privateKeyDER, format: 'der', type: 'pkcs8', - passphrase: 'secret' + passphrase }; testEncryptDecrypt(publicKey, privateKey); testSignVerify(publicKey, privateKey); diff --git a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js index 246cbe5dd1a..cd7e59be97f 100644 --- a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js +++ b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js @@ -4,20 +4,21 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) - common.skip('BoringSSL does not support paramEncoding: explicit'); - -const assert = require('assert'); -const { - generateKeyPair, -} = require('crypto'); const { + isBoringSSL, testSignVerify, spkiExp, pkcs8EncExp, + hasOpenSSL, } = require('../common/crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +if (isBoringSSL) + common.skip('BoringSSL does not support paramEncoding: explicit'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); // Test async elliptic curve key generation, e.g. for ECDSA, with an encrypted // private key with paramEncoding explicit. @@ -43,7 +44,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => testSignVerify(publicKey, privateKey), - hasOpenSSL3 ? { + hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto ' + 'routines::interrupted or cancelled' } : { diff --git a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js index c3b8ab6e8f5..5c0878fc634 100644 --- a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js +++ b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js @@ -4,19 +4,22 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { + isBoringSSL, + hasFIPS, + testSignVerify, + spkiExp, + sec1EncExp, + hasOpenSSL, +} = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support paramEncoding: explicit'); const assert = require('assert'); const { generateKeyPair, } = require('crypto'); -const { - testSignVerify, - spkiExp, - sec1EncExp, - hasOpenSSL3, -} = require('../common/crypto'); { // Test async explicit elliptic curve key generation with an encrypted @@ -34,7 +37,12 @@ const { cipher: 'aes-128-cbc', passphrase: 'secret' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3)) { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, spkiExp); assert.strictEqual(typeof privateKey, 'string'); @@ -42,7 +50,7 @@ const { // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => testSignVerify(publicKey, privateKey), - hasOpenSSL3 ? { + hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto ' + 'routines::interrupted or cancelled' } : { diff --git a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js index 8084cdfc0b3..e6209a09041 100644 --- a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js +++ b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js @@ -4,18 +4,20 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { + isBoringSSL, + testSignVerify, + spkiExp, + sec1Exp, +} = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support paramEncoding: explicit'); const assert = require('assert'); const { generateKeyPair, } = require('crypto'); -const { - testSignVerify, - spkiExp, - sec1Exp, -} = require('../common/crypto'); // Test async explicit elliptic curve key generation, e.g. for ECDSA, // with a SEC1 private key with paramEncoding explicit. diff --git a/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js b/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js index 4c11401d0fc..ba86fcdb94e 100644 --- a/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js +++ b/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js @@ -12,7 +12,7 @@ const { testSignVerify, spkiExp, pkcs8EncExp, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); // Test async elliptic curve key generation, e.g. for ECDSA, with an encrypted @@ -39,7 +39,7 @@ const { // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => testSignVerify(publicKey, privateKey), - hasOpenSSL3 ? { + hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto ' + 'routines::interrupted or cancelled' } : { diff --git a/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js b/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js index 0503ff74787..84ea9d2f7a9 100644 --- a/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js +++ b/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js @@ -9,10 +9,11 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testSignVerify, spkiExp, sec1EncExp, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); { @@ -31,7 +32,12 @@ const { cipher: 'aes-128-cbc', passphrase: 'secret' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3)) { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, spkiExp); assert.strictEqual(typeof privateKey, 'string'); @@ -39,7 +45,7 @@ const { // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => testSignVerify(publicKey, privateKey), - hasOpenSSL3 ? { + hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto ' + 'routines::interrupted or cancelled' } : { diff --git a/test/parallel/test-crypto-keygen-async-rsa.js b/test/parallel/test-crypto-keygen-async-rsa.js index c80d7d33492..7a372ded9fc 100644 --- a/test/parallel/test-crypto-keygen-async-rsa.js +++ b/test/parallel/test-crypto-keygen-async-rsa.js @@ -10,17 +10,19 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, pkcs1EncExp, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); // Test async RSA key generation with an encrypted private key. { + const isFips = hasFIPS(3); generateKeyPair('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'der' @@ -31,7 +33,12 @@ const { cipher: 'aes-256-cbc', passphrase: 'secret' } - }, common.mustSucceed((publicKeyDER, privateKey) => { + }, common.mustCall((err, publicKeyDER, privateKey) => { + if (isFips) { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + assert.ifError(err); assert(Buffer.isBuffer(publicKeyDER)); assertApproximateSize(publicKeyDER, 74); @@ -44,7 +51,7 @@ const { type: 'pkcs1', format: 'der', }; - const expectedError = hasOpenSSL3 ? { + const expectedError = hasOpenSSL(3) ? { name: 'Error', message: 'error:07880109:common libcrypto routines::interrupted or ' + 'cancelled' diff --git a/test/parallel/test-crypto-keygen-bit-length.js b/test/parallel/test-crypto-keygen-bit-length.js index 13234589a5d..e5d5917cc6f 100644 --- a/test/parallel/test-crypto-keygen-bit-length.js +++ b/test/parallel/test-crypto-keygen-bit-length.js @@ -4,7 +4,13 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { + isBoringSSL, + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support arbitrary RSA modulus length ' + 'or RSA-PSS/DSA key generation'); @@ -12,7 +18,8 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); + +const fips3 = hasFIPS(3); // This tests check that generateKeyPair returns correct bit length in // KeyObject's asymmetricKeyDetails. @@ -20,23 +27,38 @@ const { hasOpenSSL3 } = require('../common/crypto'); { generateKeyPair('rsa', { modulusLength: 513, - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (fips3) { + assert.strictEqual(err?.message, 'error:020000AE:rsa routines::invalid modulus'); + return; + } + assert.ifError(err); assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 513); assert.strictEqual(publicKey.asymmetricKeyDetails.modulusLength, 513); })); generateKeyPair('rsa-pss', { modulusLength: 513, - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (fips3) { + assert.strictEqual(err?.message, 'error:020000AE:rsa routines::invalid modulus'); + return; + } + assert.ifError(err); assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 513); assert.strictEqual(publicKey.asymmetricKeyDetails.modulusLength, 513); })); - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { generateKeyPair('dsa', { modulusLength: 2049, divisorLength: 256, - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (fips3) { + assert.strictEqual(err?.message, 'error:05000072:dsa routines::bad ffc parameters'); + return; + } + assert.ifError(err); assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 2049); assert.strictEqual(publicKey.asymmetricKeyDetails.modulusLength, 2049); })); diff --git a/test/parallel/test-crypto-keygen-deprecation.js b/test/parallel/test-crypto-keygen-deprecation.js index 926dfbbc4ae..62266382e2d 100644 --- a/test/parallel/test-crypto-keygen-deprecation.js +++ b/test/parallel/test-crypto-keygen-deprecation.js @@ -15,13 +15,15 @@ DeprecationWarning.push([ common.expectWarning({ DeprecationWarning }); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); +const modulusLength = hasFIPS(3) ? 2048 : 512; const { generateKeyPair } = require('crypto'); { // This test makes sure deprecated options still work as intended generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, saltLength: 16, hash: 'sha256', mgf1Hash: 'sha256' @@ -29,7 +31,7 @@ const { generateKeyPair } = require('crypto'); assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', @@ -39,7 +41,7 @@ const { generateKeyPair } = require('crypto'); assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-dh-classic.js b/test/parallel/test-crypto-keygen-dh-classic.js index 44af7730126..a78d068ef79 100644 --- a/test/parallel/test-crypto-keygen-dh-classic.js +++ b/test/parallel/test-crypto-keygen-dh-classic.js @@ -4,7 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { isBoringSSL, hasFIPS } = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support DH key pair generation'); const assert = require('assert'); @@ -15,7 +17,7 @@ const { // Test classic Diffie-Hellman key generation. { generateKeyPair('dh', { - primeLength: 512 + primeLength: hasFIPS(3) ? 2048 : 512 }, common.mustSucceed((publicKey, privateKey) => { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'dh'); diff --git a/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js b/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js index 854ad6e35ef..4f7ffcfce06 100644 --- a/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js +++ b/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js @@ -5,6 +5,8 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); +const modulusLength = hasFIPS(3) ? 2048 : 512; const { generateKeyPair, } = require('crypto'); @@ -13,7 +15,7 @@ const { // simultaneously so long as they're identical values. { generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, saltLength: 16, hash: 'sha256', hashAlgorithm: 'sha256', @@ -23,7 +25,7 @@ const { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', @@ -33,7 +35,7 @@ const { assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-ec-group-names.js b/test/parallel/test-crypto-keygen-ec-group-names.js new file mode 100644 index 00000000000..b7afa67f9c2 --- /dev/null +++ b/test/parallel/test-crypto-keygen-ec-group-names.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { generateKeyPair, generateKeyPairSync, sign, verify } = require('crypto'); + +const message = Buffer.from('EC group names'); +for (const [alias, canonical] of [ + ['P-256', 'prime256v1'], + ['P-384', 'secp384r1'], + ['P-521', 'secp521r1'], +]) { + for (const namedCurve of [alias, canonical]) { + const check = common.mustCall((publicKey, privateKey) => { + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, + { namedCurve: canonical }); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, + { namedCurve: canonical }); + assert(verify('sha256', message, publicKey, + sign('sha256', message, privateKey))); + }, 2); + const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve }); + check(publicKey, privateKey); + generateKeyPair('ec', { namedCurve }, common.mustSucceed(check)); + } +} + +for (const namedCurve of ['', 'node-test-unknown-curve']) { + assert.throws(() => generateKeyPairSync('ec', { namedCurve }), { + code: 'ERR_CRYPTO_INVALID_CURVE', + }); + assert.throws(() => generateKeyPair('ec', { namedCurve }, common.mustNotCall()), { + code: 'ERR_CRYPTO_INVALID_CURVE', + }); +} diff --git a/test/parallel/test-crypto-keygen-eddsa.js b/test/parallel/test-crypto-keygen-eddsa.js index 0a132235ea2..ff17d03e752 100644 --- a/test/parallel/test-crypto-keygen-eddsa.js +++ b/test/parallel/test-crypto-keygen-eddsa.js @@ -8,15 +8,22 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); +const rejectsXCurves = hasFIPS(3, 5); // Test EdDSA key generation. { for (const keyType of ['ed25519', 'ed448', 'x25519', 'x448']) { - if (process.features.openssl_is_boringssl && keyType.endsWith('448')) { + if (isBoringSSL && keyType.endsWith('448')) { common.printSkipMessage(`Skipping unsupported ${keyType} test case`); continue; } - generateKeyPair(keyType, common.mustSucceed((publicKey, privateKey) => { + generateKeyPair(keyType, common.mustCall((err, publicKey, privateKey) => { + if (rejectsXCurves && keyType.startsWith('x')) { + assert.strictEqual(err?.message, 'error:0308010C:digital envelope routines::unsupported'); + return; + } + assert.ifError(err); assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, keyType); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); diff --git a/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js b/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js index 6c7938f99e1..ae1ca23c29e 100644 --- a/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js +++ b/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js @@ -8,11 +8,14 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); + +const fips4 = hasFIPS(4); // Passing an empty passphrase string should not throw ERR_OSSL_CRYPTO_MALLOC_FAILURE even on OpenSSL 3. // Regression test for https://github.com/nodejs/node/issues/41428. generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, publicKeyEncoding: { type: 'spki', format: 'pem' @@ -23,7 +26,12 @@ generateKeyPair('rsa', { cipher: 'aes-256-cbc', passphrase: '' } -}, common.mustSucceed((publicKey, privateKey) => { +}, common.mustCall((err, publicKey, privateKey) => { + if (fips4) { + assert.strictEqual(err?.code, 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'string'); assert.strictEqual(typeof privateKey, 'string'); })); diff --git a/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js b/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js index cb873ff0474..ccf98bcd376 100644 --- a/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js +++ b/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js @@ -10,23 +10,38 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testSignVerify, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); +const fips4 = hasFIPS(4); + // Passing an empty passphrase string should not cause OpenSSL's default // passphrase prompt in the terminal. // See https://github.com/nodejs/node/issues/35898. for (const type of ['pkcs1', 'pkcs8']) { generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, privateKeyEncoding: { type, format: 'pem', cipher: 'aes-256-cbc', passphrase: '' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3) && type === 'pkcs1') { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + if (fips4) { + assert.strictEqual( + err?.code, + 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + ); + return; + } + assert.ifError(err); assert.strictEqual(publicKey.type, 'public'); for (const passphrase of ['', Buffer.alloc(0)]) { @@ -41,7 +56,7 @@ for (const type of ['pkcs1', 'pkcs8']) { // the key, and not specifying a passphrase should fail when decoding it. assert.throws(() => { return testSignVerify(publicKey, privateKey); - }, hasOpenSSL3 ? { + }, hasOpenSSL(3) ? { name: 'Error', code: 'ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED', message: 'error:07880109:common libcrypto routines::interrupted or cancelled' diff --git a/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js b/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js index 9086e2e8a5f..00cd4ba05f0 100644 --- a/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js +++ b/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js @@ -4,7 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { isBoringSSL, hasFIPS } = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support DSA key pair generation'); const assert = require('assert'); @@ -13,6 +15,8 @@ const { generateKeyPairSync, } = require('crypto'); +const fips3 = hasFIPS(3); + // Test invalid parameter encoding. { assert.throws(() => generateKeyPairSync('dsa', { @@ -25,7 +29,11 @@ const { } }), { name: 'Error', - code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', - message: 'Unsupported JWK Key Type.' + ...fips3 ? { + message: 'error:05000072:dsa routines::bad ffc parameters', + } : { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', + message: 'Unsupported JWK Key Type.', + }, }); } diff --git a/test/parallel/test-crypto-keygen-key-object-without-encoding.js b/test/parallel/test-crypto-keygen-key-object-without-encoding.js index abcd282871b..3c44d174df6 100644 --- a/test/parallel/test-crypto-keygen-key-object-without-encoding.js +++ b/test/parallel/test-crypto-keygen-key-object-without-encoding.js @@ -9,6 +9,7 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testEncryptDecrypt, testSignVerify, } = require('../common/crypto'); @@ -17,7 +18,7 @@ const { { // If no publicKeyEncoding is specified, a key object should be returned. generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, privateKeyEncoding: { type: 'pkcs1', format: 'pem' @@ -36,7 +37,7 @@ const { // If no privateKeyEncoding is specified, a key object should be returned. generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, publicKeyEncoding: { type: 'pkcs1', format: 'pem' diff --git a/test/parallel/test-crypto-keygen-key-objects.js b/test/parallel/test-crypto-keygen-key-objects.js index a0f1bdf2bcb..2d22e99360e 100644 --- a/test/parallel/test-crypto-keygen-key-objects.js +++ b/test/parallel/test-crypto-keygen-key-objects.js @@ -8,18 +8,20 @@ const assert = require('assert'); const { generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test sync key generation with key objects. { + const modulusLength = hasFIPS(3) ? 2048 : 512; const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 512 + modulusLength }); assert.strictEqual(typeof publicKey, 'object'); assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n }); @@ -27,7 +29,7 @@ const { assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n }); } diff --git a/test/parallel/test-crypto-keygen-missing-oid.js b/test/parallel/test-crypto-keygen-missing-oid.js index 1e4f309292e..78b70ee5030 100644 --- a/test/parallel/test-crypto-keygen-missing-oid.js +++ b/test/parallel/test-crypto-keygen-missing-oid.js @@ -11,7 +11,7 @@ const { getCurves, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); // This test creates EC key pairs on curves without associated OIDs. // Specifying a key encoding should not crash. @@ -21,8 +21,12 @@ const { hasOpenSSL3 } = require('../common/crypto'); if (!getCurves().includes(namedCurve)) continue; - const expectedErrorCode = - hasOpenSSL3 ? 'ERR_OSSL_MISSING_OID' : 'ERR_OSSL_EC_MISSING_OID'; + const expectedError = hasFIPS(3) ? { + name: 'Error', + message: 'error:08000081:elliptic curve routines::unknown group', + } : { + code: hasOpenSSL(3) ? 'ERR_OSSL_MISSING_OID' : 'ERR_OSSL_EC_MISSING_OID', + }; const params = { namedCurve, publicKeyEncoding: { @@ -33,13 +37,9 @@ const { hasOpenSSL3 } = require('../common/crypto'); assert.throws(() => { generateKeyPairSync('ec', params); - }, { - code: expectedErrorCode - }); + }, expectedError); - generateKeyPair('ec', params, common.mustCall((err) => { - assert.strictEqual(err.code, expectedErrorCode); - })); + generateKeyPair('ec', params, common.expectsError(expectedError)); } } } diff --git a/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js b/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js index 559c6f0af05..44243791b38 100644 --- a/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js +++ b/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js @@ -4,7 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { isBoringSSL, hasFIPS } = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support RSA-PSS key pair generation'); const assert = require('assert'); @@ -15,11 +17,12 @@ const { // 'rsa-pss' should not add a RSASSA-PSS-params sequence by default. // Regression test for: https://github.com/nodejs/node/issues/39936 { + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512 + modulusLength }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n }; assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); @@ -30,6 +33,7 @@ const { // AlgorithmIdentifier member of the SubjectPublicKeyInfo has the expected // length of 11 bytes (as opposed to > 11 bytes if node added params). const spki = publicKey.export({ format: 'der', type: 'spki' }); - assert.strictEqual(spki[3], 11, spki.toString('hex')); + assert.strictEqual( + spki[3], hasFIPS(3) ? 32 : 11, spki.toString('hex')); })); } diff --git a/test/parallel/test-crypto-keygen-non-standard-public-exponent.js b/test/parallel/test-crypto-keygen-non-standard-public-exponent.js index f54a9e8a6d9..96feb6bf8d1 100644 --- a/test/parallel/test-crypto-keygen-non-standard-public-exponent.js +++ b/test/parallel/test-crypto-keygen-non-standard-public-exponent.js @@ -8,28 +8,39 @@ const assert = require('assert'); const { generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test sync key generation with key objects with a non-standard // publicExponent { - const { publicKey, privateKey } = generateKeyPairSync('rsa', { - publicExponent: 3, - modulusLength: 512 - }); + if (hasFIPS(3)) { + assert.throws(() => generateKeyPairSync('rsa', { + publicExponent: 3, + modulusLength: 2048, + }), { + name: 'Error', + message: 'error:020000B2:rsa routines::pub exponent out of range', + }); + } else { + const { publicKey, privateKey } = generateKeyPairSync('rsa', { + publicExponent: 3, + modulusLength: 512 + }); - assert.strictEqual(typeof publicKey, 'object'); - assert.strictEqual(publicKey.type, 'public'); - assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); - assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, - publicExponent: 3n - }); + assert.strictEqual(typeof publicKey, 'object'); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 3n + }); - assert.strictEqual(typeof privateKey, 'object'); - assert.strictEqual(privateKey.type, 'private'); - assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); - assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, - publicExponent: 3n - }); + assert.strictEqual(typeof privateKey, 'object'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 3n + }); + } } diff --git a/test/parallel/test-crypto-keygen-promisify.js b/test/parallel/test-crypto-keygen-promisify.js index cd6ca7d6e3e..158367cef46 100644 --- a/test/parallel/test-crypto-keygen-promisify.js +++ b/test/parallel/test-crypto-keygen-promisify.js @@ -10,6 +10,7 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, pkcs1PubExp, @@ -19,9 +20,10 @@ const { promisify } = require('util'); // Test the util.promisified API with async RSA key generation. { + const isFips = hasFIPS(3); promisify(generateKeyPair)('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'pem' @@ -34,11 +36,11 @@ const { promisify } = require('util'); const { publicKey, privateKey } = keys; assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, pkcs1PubExp); - assertApproximateSize(publicKey, 180); + assertApproximateSize(publicKey, isFips ? 426 : 180); assert.strictEqual(typeof privateKey, 'string'); assert.match(privateKey, pkcs1PrivExp); - assertApproximateSize(privateKey, 512); + assertApproximateSize(privateKey, isFips ? 1675 : 512); testEncryptDecrypt(publicKey, privateKey); testSignVerify(publicKey, privateKey); diff --git a/test/parallel/test-crypto-keygen-raw.js b/test/parallel/test-crypto-keygen-raw.js index e55c3f10eed..0006e5e1535 100644 --- a/test/parallel/test-crypto-keygen-raw.js +++ b/test/parallel/test-crypto-keygen-raw.js @@ -11,19 +11,29 @@ const { createPublicKey, createPrivateKey, } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); + +const rejectsXCurves = hasFIPS(3, 5); // Test generateKeyPairSync with raw encoding for EdDSA/ECDH key types. { const types = ['ed25519', 'x25519']; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { types.push('ed448', 'x448'); } for (const type of types) { - const { publicKey, privateKey } = generateKeyPairSync(type, { + const options = { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, - }); + }; + if (rejectsXCurves && type.startsWith('x')) { + assert.throws(() => generateKeyPairSync(type, options), { + name: 'Error', + message: 'error:0308010C:digital envelope routines::unsupported', + }); + continue; + } + const { publicKey, privateKey } = generateKeyPairSync(type, options); assert(Buffer.isBuffer(publicKey)); assert(Buffer.isBuffer(privateKey)); @@ -50,17 +60,24 @@ const { hasOpenSSL } = require('../common/crypto'); // Test async generateKeyPair with raw encoding for EdDSA/ECDH key types. { const types = ['ed25519', 'x25519']; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { types.push('ed448', 'x448'); } for (const type of types) { - generateKeyPair(type, { + const options = { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, - }, common.mustSucceed((publicKey, privateKey) => { - assert(Buffer.isBuffer(publicKey)); - assert(Buffer.isBuffer(privateKey)); - })); + }; + generateKeyPair(type, options, + common.mustCall((err, publicKey, privateKey) => { + if (rejectsXCurves && type.startsWith('x')) { + assert.strictEqual(err?.message, 'error:0308010C:digital envelope routines::unsupported'); + return; + } + assert.ifError(err); + assert(Buffer.isBuffer(publicKey)); + assert(Buffer.isBuffer(privateKey)); + })); } } @@ -158,7 +175,7 @@ const { hasOpenSSL } = require('../common/crypto'); } // Test error: raw with DSA. -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { assert.throws(() => generateKeyPairSync('dsa', { modulusLength: 2048, publicKeyEncoding: { format: 'raw-public' }, @@ -205,7 +222,7 @@ if (!process.features.openssl_is_boringssl) { } // PQC key types -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { // Test raw encoding for ML-DSA key types (raw-public + raw-seed only). { for (const type of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { @@ -232,7 +249,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // Test raw encoding for ML-KEM key types (raw-public + raw-seed only). { for (const type of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { - if (process.features.openssl_is_boringssl && type === 'ml-kem-512') { + if (isBoringSSL && type === 'ml-kem-512') { common.printSkipMessage(`Skipping unsupported ${type} test case`); continue; } @@ -259,7 +276,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // Test raw encoding for SLH-DSA key types. { for (const type of ['slh-dsa-sha2-128f', 'slh-dsa-shake-128f']) { - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { common.printSkipMessage(`Skipping unsupported ${type} test case`); continue; } @@ -274,7 +291,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { } // Test error: raw-seed with SLH-DSA (not supported). - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { assert.throws(() => generateKeyPairSync('slh-dsa-sha2-128f', { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-seed' }, diff --git a/test/parallel/test-crypto-keygen-rfc8017-9-1.js b/test/parallel/test-crypto-keygen-rfc8017-9-1.js index fbefb1b4f64..e1781093c0c 100644 --- a/test/parallel/test-crypto-keygen-rfc8017-9-1.js +++ b/test/parallel/test-crypto-keygen-rfc8017-9-1.js @@ -4,7 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { isBoringSSL, hasFIPS } = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support RSA-PSS key pair generation'); const assert = require('assert'); @@ -16,14 +18,14 @@ const { // hash function, it is RECOMMENDED that the hash function be the same as the // one that is applied to the message." { - + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, hashAlgorithm: 'sha256', saltLength: 16 }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js b/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js index bc96d57ed0c..f1543462c39 100644 --- a/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js +++ b/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js @@ -4,7 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { isBoringSSL, hasFIPS } = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support RSA-PSS key pair generation'); const assert = require('assert'); @@ -15,12 +17,13 @@ const { // RFC 8017, A.2.3.: "For a given hashAlgorithm, the default value of // saltLength is the octet length of the hash value." { + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, hashAlgorithm: 'sha512' }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha512', mgf1HashAlgorithm: 'sha512', @@ -32,12 +35,12 @@ const { // It is still possible to explicitly set saltLength to 0. generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, hashAlgorithm: 'sha512', saltLength: 0 }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha512', mgf1HashAlgorithm: 'sha512', diff --git a/test/parallel/test-crypto-keygen-rsa-pss.js b/test/parallel/test-crypto-keygen-rsa-pss.js index 3ce0d40e8d1..386db4fe011 100644 --- a/test/parallel/test-crypto-keygen-rsa-pss.js +++ b/test/parallel/test-crypto-keygen-rsa-pss.js @@ -4,7 +4,14 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) +const { + isBoringSSL, + hasFIPS, + testEncryptDecrypt, + testSignVerify, +} = require('../common/crypto'); + +if (isBoringSSL) common.skip('BoringSSL does not support RSA-PSS key pair generation'); const assert = require('assert'); @@ -12,15 +19,12 @@ const { constants, generateKeyPair, } = require('crypto'); -const { - testEncryptDecrypt, - testSignVerify, -} = require('../common/crypto'); // Test RSA-PSS. { + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, saltLength: 16, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256' @@ -28,7 +32,7 @@ const { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', @@ -38,7 +42,7 @@ const { assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-sync.js b/test/parallel/test-crypto-keygen-sync.js index a100379e21f..54a91fcfbca 100644 --- a/test/parallel/test-crypto-keygen-sync.js +++ b/test/parallel/test-crypto-keygen-sync.js @@ -10,6 +10,7 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, pkcs1PubExp, @@ -19,9 +20,10 @@ const { // To make the test faster, we will only test sync key generation once and // with a relatively small key. { + const isFips = hasFIPS(3); const ret = generateKeyPairSync('rsa', { - publicExponent: 3, - modulusLength: 512, + publicExponent: isFips ? 0x10001 : 3, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'pem' @@ -37,10 +39,10 @@ const { assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, pkcs1PubExp); - assertApproximateSize(publicKey, 162); + assertApproximateSize(publicKey, isFips ? 426 : 162); assert.strictEqual(typeof privateKey, 'string'); assert.match(privateKey, pkcs8Exp); - assertApproximateSize(privateKey, 512); + assertApproximateSize(privateKey, isFips ? 1704 : 512); testEncryptDecrypt(publicKey, privateKey); testSignVerify(publicKey, privateKey); diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index 111d3dcbfd4..dcce85b8377 100644 --- a/test/parallel/test-crypto-keygen.js +++ b/test/parallel/test-crypto-keygen.js @@ -14,8 +14,11 @@ const { } = require('crypto'); const { inspect } = require('util'); -const { hasOpenSSL3 } = require('../common/crypto'); -const isBoringSSL = process.features.openssl_is_boringssl; +const { + hasOpenSSL, + isBoringSSL: commonIsBoringSSL, +} = require('../common/crypto'); +const isBoringSSL = commonIsBoringSSL; // Test invalid parameter encoding. { @@ -379,7 +382,7 @@ const isBoringSSL = process.features.openssl_is_boringssl; let invalidExponentError = /bad e value/; if (isBoringSSL) { invalidExponentError = /BAD_E_VALUE/; - } else if (hasOpenSSL3) { + } else if (hasOpenSSL(3)) { invalidExponentError = /exponent/; } for (const publicExponent of [1, 1 + 0x10001]) { diff --git a/test/parallel/test-crypto-keyobject-brand-check.js b/test/parallel/test-crypto-keyobject-brand-check.js index ac0cf1b65f7..ed8e5bdae76 100644 --- a/test/parallel/test-crypto-keyobject-brand-check.js +++ b/test/parallel/test-crypto-keyobject-brand-check.js @@ -15,6 +15,7 @@ const { generateKeyPairSync, KeyObject, } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); const { types: { isKeyObject } } = require('node:util'); const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; @@ -25,7 +26,9 @@ function getter(proto, name) { { const secret = createSecretKey(Buffer.alloc(16)); - const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); + const { publicKey } = generateKeyPairSync('rsa', { + modulusLength: hasFIPS(3) ? 2048 : 1024, + }); const type = getter(KeyObject.prototype, 'type'); const symmetricKeySize = diff --git a/test/parallel/test-crypto-keyobject-clone-transfer.js b/test/parallel/test-crypto-keyobject-clone-transfer.js index 61cfcdc497f..2ac37ca201e 100644 --- a/test/parallel/test-crypto-keyobject-clone-transfer.js +++ b/test/parallel/test-crypto-keyobject-clone-transfer.js @@ -16,6 +16,7 @@ const { sign, verify, } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); const { MessageChannel, Worker } = require('node:worker_threads'); const { types: { isKeyObject } } = require('node:util'); @@ -87,7 +88,7 @@ function hmacDigest(key) { (async () => { const secret = createSecretKey(Buffer.alloc(16)); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, }); for (const key of [secret, publicKey, privateKey]) { diff --git a/test/parallel/test-crypto-keyobject-hidden-slots.js b/test/parallel/test-crypto-keyobject-hidden-slots.js index 1ea243ba0ab..b3db2068541 100644 --- a/test/parallel/test-crypto-keyobject-hidden-slots.js +++ b/test/parallel/test-crypto-keyobject-hidden-slots.js @@ -29,6 +29,7 @@ const { X509Certificate, } = require('node:crypto'); const { readFileSync } = require('node:fs'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); function updateFinal(cipher, data = Buffer.alloc(16)) { @@ -110,12 +111,13 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { } { + const modulusLength = hasFIPS(3) ? 2048 : 1024; const { publicKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength, }); const details = publicKey.asymmetricKeyDetails; - assert.strictEqual(details.modulusLength, 1024); + assert.strictEqual(details.modulusLength, modulusLength); assert.strictEqual(details.publicExponent, 65537n); details.modulusLength = 1; @@ -124,7 +126,7 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { const freshDetails = publicKey.asymmetricKeyDetails; assert.notStrictEqual(freshDetails, details); - assert.strictEqual(freshDetails.modulusLength, 1024); + assert.strictEqual(freshDetails.modulusLength, modulusLength); assert.strictEqual(freshDetails.publicExponent, 65537n); assert.strictEqual(freshDetails.extra, undefined); } @@ -150,7 +152,7 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { { const { privateKey, publicKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, }); const originalType = Object.getOwnPropertyDescriptor(KeyObject.prototype, 'type'); @@ -175,8 +177,10 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { verifier.update(data); assert.strictEqual(verifier.verify(publicKey, streamSignature), true); - const ciphertext = publicEncrypt(publicKey, data); - assert.deepStrictEqual(privateDecrypt(privateKey, ciphertext), data); + const options = hasFIPS(3) ? { oaepHash: 'sha256' } : {}; + const ciphertext = publicEncrypt({ key: publicKey, ...options }, data); + assert.deepStrictEqual( + privateDecrypt({ key: privateKey, ...options }, ciphertext), data); assert.strictEqual(publicKey.equals(createPublicKey(privateKey)), true); diff --git a/test/parallel/test-crypto-keyobject-no-own-symbols.js b/test/parallel/test-crypto-keyobject-no-own-symbols.js index f1539c6a0f7..94503244678 100644 --- a/test/parallel/test-crypto-keyobject-no-own-symbols.js +++ b/test/parallel/test-crypto-keyobject-no-own-symbols.js @@ -12,6 +12,7 @@ const { createSecretKey, generateKeyPairSync, } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); function assertNoOwnKeys(key) { assert.deepStrictEqual(Object.getOwnPropertySymbols(key), []); @@ -22,7 +23,7 @@ function assertNoOwnKeys(key) { { const secret = createSecretKey(Buffer.alloc(16)); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, }); for (const key of [secret, publicKey, privateKey]) { diff --git a/test/parallel/test-crypto-mac-cache-snapshot.js b/test/parallel/test-crypto-mac-cache-snapshot.js deleted file mode 100644 index 1fd37367d2a..00000000000 --- a/test/parallel/test-crypto-mac-cache-snapshot.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const { hasOpenSSL3 } = require('../common/crypto'); -if (!hasOpenSSL3 || process.features.openssl_is_boringssl) - common.skip('this test requires OpenSSL 3 EVP_MAC support'); - -const assert = require('node:assert'); -const { getMacs } = require('node:crypto'); -const fixtures = require('../common/fixtures'); -const tmpdir = require('../common/tmpdir'); -const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); - -if (!getMacs().includes('poly1305')) - common.skip('Poly1305 is not supported'); - -const entry = fixtures.path('snapshot', 'crypto-provider-mac-cache.js'); -const buildEnv = { - OPENSSL_CONF: fixtures.path( - 'openssl3-conf', 'legacy_provider_enabled.cnf'), -}; -const runEnv = { - OPENSSL_CONF: fixtures.path('openssl3-conf', 'default_only.cnf'), -}; - -tmpdir.refresh(); -buildSnapshot(entry, buildEnv); -const { stdout } = runWithSnapshot(undefined, runEnv); -assert.match(stdout, /provider MAC cache snapshot: ok/); diff --git a/test/parallel/test-crypto-mac-cache.js b/test/parallel/test-crypto-mac-cache.js deleted file mode 100644 index dfe99e943bb..00000000000 --- a/test/parallel/test-crypto-mac-cache.js +++ /dev/null @@ -1,307 +0,0 @@ -// Flags: --expose-internals --no-warnings -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const { hasOpenSSL3 } = require('../common/crypto'); -if (!hasOpenSSL3 || process.features.openssl_is_boringssl) - common.skip('this test requires OpenSSL 3 EVP_MAC support'); - -const assert = require('node:assert'); -const { once } = require('node:events'); -const { - createMac, - getFips, - getMacs, - setFips, -} = require('node:crypto'); -const { getMacCache } = require('internal/crypto/util'); -const { internalBinding } = require('internal/test/binding'); -const { Worker } = require('node:worker_threads'); - -const binding = internalBinding('crypto'); -const algorithm = 'poly1305'; -const key = Buffer.from( - '85d6be7857556d337f4452fe42d506a8' + - '0103808afb0db2fd4abff6af4149f51b', - 'hex', -); -const data = Buffer.from('Cryptographic Forum Research Group'); -const expected = 'a8061dc1305136c6c22b8baf0c0127a9'; -const originalFips = getFips(); - -function getAliasId(aliases, name) { - const normalized = name.toLowerCase(); - for (const [alias, id] of Object.entries(aliases)) { - if (alias.toLowerCase() === normalized) return id; - } - return undefined; -} - -try { - setFips(0); -} catch { - common.skip('FIPS mode cannot be disabled'); -} -if (getFips() !== 0) - common.skip('FIPS mode cannot be disabled'); - -const initialMacs = getMacs(); -if (!initialMacs.includes(algorithm)) - common.skip(`${algorithm} is not supported`); - -let fipsMacs; -let canToggleFips = false; -const generationBeforeFipsProbe = binding.getFipsCryptoGeneration(); -try { - setFips(1); -} catch { - // FIPS mode is optional, so the non-FIPS cache checks below still run. - assert.strictEqual( - binding.getFipsCryptoGeneration(), - generationBeforeFipsProbe, - ); -} -if (getFips() === 1) { - fipsMacs = getMacs(); - canToggleFips = true; -} -try { - setFips(0); -} catch { - canToggleFips = false; -} - -const generation = binding.getFipsCryptoGeneration(); -setFips(0); -assert.strictEqual(binding.getFipsCryptoGeneration(), generation); - -const expectedMacs = getMacs(); -const disposableMacs = getMacs(); -assert.notStrictEqual(disposableMacs, expectedMacs); -disposableMacs.length = 0; -disposableMacs.push('not-a-real-mac'); -assert.deepStrictEqual(getMacs(), expectedMacs); - -const aliases = binding.getCachedMacAliases(); -const initialAlgorithmId = getAliasId(aliases, algorithm); -assert.strictEqual(typeof initialAlgorithmId, 'number'); - -const macCache = getMacCache(); -const cacheName = Object.keys(macCache).find( - (name) => name.toLowerCase() === algorithm, -); -assert(cacheName); -const descriptor = Object.getOwnPropertyDescriptor(macCache, cacheName); -assert(descriptor); -assert.strictEqual(descriptor.value, initialAlgorithmId); -const sentinel = new Error('mac cache setter'); -const throwsSentinel = (err) => err === sentinel; - -function installThrowingMacCacheEntry(id) { - Object.defineProperty(macCache, cacheName, { - __proto__: null, - configurable: true, - enumerable: descriptor.enumerable, - get() { return id; }, - set() { throw sentinel; }, - }); -} - -installThrowingMacCacheEntry(-1); -assert.throws(() => createMac(cacheName, key), throwsSentinel); -Object.defineProperty(macCache, cacheName, descriptor); - -// OpenSSL exposes two spellings for each KMAC implementation. They must map -// to the same cached EVP_MAC rather than consume separate cache entries. -const kmac128Id = getAliasId(aliases, 'kmac128'); -const kmac128HyphenatedId = getAliasId(aliases, 'kmac-128'); -if (kmac128Id === undefined || kmac128HyphenatedId === undefined) { - common.printSkipMessage('KMAC-128 aliases are not available'); -} else { - assert.strictEqual(kmac128Id, kmac128HyphenatedId); - const kmacAlgorithm = 'KMAC128'; - const hyphenatedAlgorithm = 'KMAC-128'; - const kmacOptions = { outputLength: 32 }; - const kmacKey = Buffer.alloc(32, 0x42); - const kmacData = Buffer.from('cache alias test'); - assert.deepStrictEqual( - createMac(kmacAlgorithm, kmacKey, kmacOptions) - .update(kmacData).final(), - createMac(hyphenatedAlgorithm, kmacKey, kmacOptions) - .update(kmacData).final(), - ); - const aliasesAfterUse = binding.getCachedMacAliases(); - assert.strictEqual(getAliasId(aliasesAfterUse, 'kmac128'), kmac128Id); - assert.strictEqual( - getAliasId(aliasesAfterUse, 'kmac-128'), - kmac128Id, - ); -} - -if (!canToggleFips || fipsMacs.includes(algorithm)) { - common.printSkipMessage('FIPS cache invalidation cannot be exercised'); - try { - setFips(originalFips); - } catch { - // The process is about to exit and FIPS support is optional. - } -} else { - const liveMac = createMac(algorithm, key).update(data); - const worker = new Worker(` - 'use strict'; - const { - createMac, - getFips, - getMacs, - } = require('node:crypto'); - const { internalBinding } = require('internal/test/binding'); - const { parentPort, workerData } = require('node:worker_threads'); - - function getAliasId(aliases, name) { - const normalized = name.toLowerCase(); - for (const [alias, id] of Object.entries(aliases)) { - if (alias.toLowerCase() === normalized) return id; - } - return undefined; - } - - const binding = internalBinding('crypto'); - const key = Buffer.from(workerData.key); - const data = Buffer.from(workerData.data); - const liveMac = createMac(workerData.algorithm, key).update(data); - getMacs(); - const initialAlgorithmId = getAliasId( - binding.getCachedMacAliases(), - workerData.algorithm, - ); - parentPort.postMessage({ - phase: 'warm', - algorithmId: initialAlgorithmId, - generation: binding.getFipsCryptoGeneration(), - }); - - parentPort.on('message', (phase) => { - if (phase === 'fips-on') { - let errorCode; - try { - createMac(workerData.algorithm, key); - } catch (error) { - errorCode = error.code; - } - const macs = getMacs(); - parentPort.postMessage({ - phase, - algorithmId: getAliasId( - binding.getCachedMacAliases(), - workerData.algorithm, - ), - errorCode, - fips: getFips(), - generation: binding.getFipsCryptoGeneration(), - hasAlgorithm: macs.includes(workerData.algorithm), - tag: liveMac.final('hex'), - }); - } else if (phase === 'fips-off') { - const macs = getMacs(); - parentPort.postMessage({ - phase, - algorithmId: getAliasId( - binding.getCachedMacAliases(), - workerData.algorithm, - ), - fips: getFips(), - generation: binding.getFipsCryptoGeneration(), - hasAlgorithm: macs.includes(workerData.algorithm), - tag: createMac(workerData.algorithm, key) - .update(data).final('hex'), - }); - } else { - parentPort.close(); - } - }); - `, { - eval: true, - workerData: { algorithm, data, key }, - }); - worker.on('error', common.mustNotCall()); - - (async () => { - const exitPromise = once(worker, 'exit'); - try { - const [warm] = await once(worker, 'message'); - assert.strictEqual(warm.phase, 'warm'); - assert.strictEqual(typeof warm.algorithmId, 'number'); - assert.strictEqual(warm.generation, generation); - - installThrowingMacCacheEntry(descriptor.value); - try { - setFips(1); - assert.throws(() => createMac(cacheName, key), throwsSentinel); - installThrowingMacCacheEntry(-1); - assert.throws(() => createMac(cacheName, key), throwsSentinel); - } finally { - Object.defineProperty(macCache, cacheName, descriptor); - } - const enabledGeneration = binding.getFipsCryptoGeneration(); - assert.strictEqual(enabledGeneration, generation + 1n); - assert.strictEqual(getFips(), 1); - assert(!getMacs().includes(algorithm)); - assert.strictEqual( - getAliasId(binding.getCachedMacAliases(), algorithm), - undefined, - ); - assert.throws(() => createMac(algorithm, key), { - code: 'ERR_CRYPTO_INVALID_MAC', - }); - assert.strictEqual(liveMac.final('hex'), expected); - - let responsePromise = once(worker, 'message'); - worker.postMessage('fips-on'); - const [enabled] = await responsePromise; - assert.strictEqual(enabled.phase, 'fips-on'); - assert.strictEqual(enabled.algorithmId, undefined); - assert.strictEqual(enabled.errorCode, 'ERR_CRYPTO_INVALID_MAC'); - assert.strictEqual(enabled.fips, 1); - assert.strictEqual(enabled.generation, enabledGeneration); - assert.strictEqual(enabled.hasAlgorithm, false); - assert.strictEqual(enabled.tag, expected); - - setFips(0); - const disabledGeneration = binding.getFipsCryptoGeneration(); - assert.strictEqual(disabledGeneration, enabledGeneration + 1n); - assert.strictEqual(getFips(), 0); - assert(getMacs().includes(algorithm)); - const restoredAlgorithmId = getAliasId( - binding.getCachedMacAliases(), - algorithm, - ); - assert.strictEqual(typeof restoredAlgorithmId, 'number'); - assert.notStrictEqual(restoredAlgorithmId, initialAlgorithmId); - assert.strictEqual( - createMac(algorithm, key).update(data).final('hex'), - expected, - ); - - responsePromise = once(worker, 'message'); - worker.postMessage('fips-off'); - const [disabled] = await responsePromise; - assert.strictEqual(disabled.phase, 'fips-off'); - assert.strictEqual(disabled.fips, 0); - assert.strictEqual(disabled.generation, disabledGeneration); - assert.strictEqual(disabled.hasAlgorithm, true); - assert.strictEqual(typeof disabled.algorithmId, 'number'); - assert.notStrictEqual(disabled.algorithmId, warm.algorithmId); - assert.strictEqual(disabled.tag, expected); - - worker.postMessage('done'); - const [code] = await exitPromise; - assert.strictEqual(code, 0); - } finally { - if (worker.threadId !== -1) await worker.terminate(); - setFips(originalFips); - } - })().then(common.mustCall()); -} diff --git a/test/parallel/test-crypto-mac-errors.js b/test/parallel/test-crypto-mac-errors.js index 43e464bb737..8bc8cebfcf5 100644 --- a/test/parallel/test-crypto-mac-errors.js +++ b/test/parallel/test-crypto-mac-errors.js @@ -6,9 +6,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3) || process.features.openssl_is_boringssl) { +if (!hasOpenSSL(3) || isBoringSSL) { common.skip('OpenSSL 3 EVP_MAC support is required'); } diff --git a/test/parallel/test-crypto-mac-unsupported.js b/test/parallel/test-crypto-mac-unsupported.js index 68bb79f301e..1721e9d1c1a 100644 --- a/test/parallel/test-crypto-mac-unsupported.js +++ b/test/parallel/test-crypto-mac-unsupported.js @@ -6,9 +6,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (hasOpenSSL3 && !process.features.openssl_is_boringssl) { +if (hasOpenSSL(3) && !isBoringSSL) { common.skip('this test requires a build without EVP_MAC support'); } diff --git a/test/parallel/test-crypto-mac-vectors.js b/test/parallel/test-crypto-mac-vectors.js index ec9f0cffe87..19544ccf4ec 100644 --- a/test/parallel/test-crypto-mac-vectors.js +++ b/test/parallel/test-crypto-mac-vectors.js @@ -8,9 +8,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3) || process.features.openssl_is_boringssl) { +if (!hasOpenSSL(3) || isBoringSSL) { common.skip('OpenSSL 3 EVP_MAC support is required'); } diff --git a/test/parallel/test-crypto-mac.js b/test/parallel/test-crypto-mac.js index f41bd86509e..deb2a8a3d32 100644 --- a/test/parallel/test-crypto-mac.js +++ b/test/parallel/test-crypto-mac.js @@ -8,9 +8,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3) || process.features.openssl_is_boringssl) { +if (!hasOpenSSL(3) || isBoringSSL) { common.skip('OpenSSL 3 EVP_MAC support is required'); } @@ -28,20 +28,11 @@ const { Transform } = require('node:stream'); assert.strictEqual(crypto.Mac, undefined); -const firstMacs = getMacs(); -const secondMacs = getMacs(); +const macs = getMacs(); +assert(macs.every((name) => name === name.toLowerCase())); +assert(macs.every((name) => !/^\d+(?:\.\d+)+$/.test(name))); -assert.notStrictEqual(firstMacs, secondMacs); -assert.deepStrictEqual(firstMacs, [...firstMacs].sort()); -assert.strictEqual(firstMacs.length, new Set(firstMacs).size); -assert(firstMacs.every((name) => typeof name === 'string')); -assert(firstMacs.every((name) => name === name.toLowerCase())); -assert(firstMacs.every((name) => !/^\d+(?:\.\d+)+$/.test(name))); - -firstMacs.push('not-a-real-mac'); -assert(!getMacs().includes('not-a-real-mac')); - -const availableMacs = new Set(secondMacs); +const availableMacs = new Set(macs); if (!availableMacs.has('hmac')) { common.printSkipMessage('HMAC is not available from the active providers'); } else { diff --git a/test/parallel/test-crypto-negative-zero.js b/test/parallel/test-crypto-negative-zero.js index 0af9220569c..0e9525fcaf6 100644 --- a/test/parallel/test-crypto-negative-zero.js +++ b/test/parallel/test-crypto-negative-zero.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); function getOutcome(fn) { try { @@ -110,7 +110,7 @@ function assertSameErrorOrSuccess(actual, expected) { })); } -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { assert.strictEqual( crypto.createHash('shake128', { outputLength: -0 }).digest('hex'), '', diff --git a/test/parallel/test-crypto-no-algorithm.js b/test/parallel/test-crypto-no-algorithm.js index db781c66a6d..000ad3beabb 100644 --- a/test/parallel/test-crypto-no-algorithm.js +++ b/test/parallel/test-crypto-no-algorithm.js @@ -4,9 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL3) +if (!hasOpenSSL(3)) common.skip('this test requires OpenSSL 3.x'); const assert = require('node:assert/strict'); @@ -56,3 +56,21 @@ if (isMainThread) { assert(common.nodeProcessAborted(cp.status, cp.signal), `process did not abort, code:${cp.status} signal:${cp.signal}`); } + +// AIX keeps OpenSSL as V8's entropy source, so a DRBG that cannot be +// fetched still aborts at startup there. +if (!common.isAIX) { + // A configuration whose random section names a DRBG that cannot be + // fetched starts normally; the first crypto call fails, without a hang. + const fixtures = require('../common/fixtures'); + const { spawnSync } = require('node:child_process'); + const randomConf = fixtures.path('openssl3-conf', 'random_unavailable.cnf'); + const cp = spawnSync(process.execPath, + [ `--openssl-config=${randomConf}`, '-e', + 'require("node:crypto").randomBytes(8)' ], + { encoding: 'utf8' }); + assert(!common.nodeProcessAborted(cp.status, cp.signal), + `process aborted, code:${cp.status} signal:${cp.signal}`); + assert.strictEqual(cp.status, 1); + assert.match(cp.stderr, /unable to fetch drbg/); +} diff --git a/test/parallel/test-crypto-oneshot-hash-xof.js b/test/parallel/test-crypto-oneshot-hash-xof.js index b4363c31592..fd42e107edb 100644 --- a/test/parallel/test-crypto-oneshot-hash-xof.js +++ b/test/parallel/test-crypto-oneshot-hash-xof.js @@ -1,13 +1,15 @@ 'use strict'; + // This tests crypto.hash() works. const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const crypto = require('crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { common.skip('BoringSSL does not support XOF hash functions'); } diff --git a/test/parallel/test-crypto-padding.js b/test/parallel/test-crypto-padding.js index 6e7a7fffee5..dce34409400 100644 --- a/test/parallel/test-crypto-padding.js +++ b/test/parallel/test-crypto-padding.js @@ -26,7 +26,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); // Input data. const ODD_LENGTH_PLAIN = 'Hello node world!'; @@ -83,7 +83,7 @@ assert.strictEqual(enc(EVEN_LENGTH_PLAIN, true), EVEN_LENGTH_ENCRYPTED); assert.throws(function() { // Input must have block length %. enc(ODD_LENGTH_PLAIN, false); -}, hasOpenSSL3 ? { +}, hasOpenSSL(3) ? { message: /wrong[\s_]final[\s_]block[\s_]length/i, code: /ERR_OSSL(_EVP)?_WRONG_FINAL_BLOCK_LENGTH/, reason: /wrong[\s_]final[\s_]block[\s_]length/i, diff --git a/test/parallel/test-crypto-pbkdf2.js b/test/parallel/test-crypto-pbkdf2.js index 78b73ed6c4e..7cd1206f4f0 100644 --- a/test/parallel/test-crypto-pbkdf2.js +++ b/test/parallel/test-crypto-pbkdf2.js @@ -5,7 +5,16 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); + +const fips4 = hasFIPS(4); +const validPassword = fips4 ? 'password' : 'pass'; +const validSalt = fips4 ? '0123456789abcdef' : 'salt'; +const validIterations = fips4 ? 1000 : 8; +const validKeyLength = fips4 ? 16 : 8; function runPBKDF2(password, salt, iterations, keylen, hash) { const syncResult = @@ -19,6 +28,25 @@ function runPBKDF2(password, salt, iterations, keylen, hash) { return syncResult; } +function assertPBKDF2Fails(password, salt, iterations, keylen, hash) { + const expected = { message: 'PBKDF2 derivation failed' }; + assert.throws( + () => crypto.pbkdf2Sync(password, salt, iterations, keylen, hash), + expected, + ); + crypto.pbkdf2( + password, + salt, + iterations, + keylen, + hash, + common.mustCall((err, result) => { + assert.strictEqual(err?.message, expected.message); + assert.strictEqual(result, undefined); + }), + ); +} + function testPBKDF2(password, salt, iterations, keylen, expected, encoding) { const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256'); assert.strictEqual(actual.toString(encoding || 'latin1'), expected); @@ -28,36 +56,55 @@ function testPBKDF2(password, salt, iterations, keylen, expected, encoding) { // Test PBKDF2 with RFC 6070 test vectors (except #4) // -testPBKDF2('password', 'salt', 1, 20, - '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' + - '\x56\xc4\xf8\x37\xa8\x65\x48\xc9'); - -testPBKDF2('password', 'salt', 2, 20, - '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' + - '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e'); - -testPBKDF2('password', 'salt', 4096, 20, - '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' + - '\x84\x5c\x4c\x8d\x96\x28\x93\xa0'); - -testPBKDF2('passwordPASSWORDpassword', - 'saltSALTsaltSALTsaltSALTsaltSALTsalt', - 4096, - 25, - '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' + - '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c'); - -testPBKDF2('pass\0word', 'sa\0lt', 4096, 16, - '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' + - '\x0a\x86\x87'); - -testPBKDF2('password', 'salt', 32, 32, - '64c486c55d30d4c5a079b8823b7d7cb37ff0556f537da8410233bcec330ed956', - 'hex'); +if (fips4) { + testPBKDF2(validPassword, validSalt, validIterations, 32, + '8514638175a45bc45eb1f22f04ff7d27' + + 'f4f8be480498c455ff4b494ce8d1e7d2', + 'hex'); + + for (const args of [ + ['short', validSalt, validIterations], + [validPassword, 'short', validIterations], + [validPassword, validSalt, 999], + ]) { + assertPBKDF2Fails(...args, 32, 'sha256'); + } + assertPBKDF2Fails( + validPassword, validSalt, validIterations, 8, 'sha256'); +} else { + testPBKDF2('password', 'salt', 1, 20, + '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' + + '\x56\xc4\xf8\x37\xa8\x65\x48\xc9'); + + testPBKDF2('password', 'salt', 2, 20, + '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' + + '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e'); + + testPBKDF2('password', 'salt', 4096, 20, + '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' + + '\x84\x5c\x4c\x8d\x96\x28\x93\xa0'); + + testPBKDF2('passwordPASSWORDpassword', + 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, + 25, + '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' + + '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c'); + + testPBKDF2('pass\0word', 'sa\0lt', 4096, 16, + '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' + + '\x0a\x86\x87'); + + testPBKDF2('password', 'salt', 32, 32, + '64c486c55d30d4c5a079b8823b7d7cb3' + + '7ff0556f537da8410233bcec330ed956', + 'hex'); +} // Error path should not leak memory (check with valgrind). assert.throws( - () => crypto.pbkdf2('password', 'salt', 1, 20, 'sha1'), + () => crypto.pbkdf2( + validPassword, validSalt, validIterations, 20, 'sha1'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' @@ -66,7 +113,8 @@ assert.throws( for (const iterations of [-1, 0, 2147483648]) { assert.throws( - () => crypto.pbkdf2Sync('password', 'salt', iterations, 20, 'sha1'), + () => crypto.pbkdf2Sync( + validPassword, validSalt, iterations, 20, 'sha1'), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', @@ -77,7 +125,8 @@ for (const iterations of [-1, 0, 2147483648]) { ['str', null, undefined, [], {}].forEach((notNumber) => { assert.throws( () => { - crypto.pbkdf2Sync('password', 'salt', 1, notNumber, 'sha256'); + crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, notNumber, 'sha256'); }, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -89,7 +138,7 @@ for (const iterations of [-1, 0, 2147483648]) { [Infinity, -Infinity, NaN].forEach((input) => { assert.throws( () => { - crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + crypto.pbkdf2(validPassword, validSalt, validIterations, input, 'sha256', common.mustNotCall()); }, { code: 'ERR_OUT_OF_RANGE', @@ -102,7 +151,7 @@ for (const iterations of [-1, 0, 2147483648]) { [-1, 2147483648, 4294967296].forEach((input) => { assert.throws( () => { - crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + crypto.pbkdf2(validPassword, validSalt, validIterations, input, 'sha256', common.mustNotCall()); }, { code: 'ERR_OUT_OF_RANGE', @@ -119,14 +168,16 @@ for (const iterations of [-1, 0, 2147483648]) { let posError; let posResult; try { - posResult = crypto.pbkdf2Sync('password', 'salt', 1, 0, 'sha256'); + posResult = crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, 0, 'sha256'); } catch (err) { posError = err; } let negError; let negResult; try { - negResult = crypto.pbkdf2Sync('password', 'salt', 1, -0, 'sha256'); + negResult = crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, -0, 'sha256'); } catch (err) { negError = err; } @@ -136,15 +187,22 @@ for (const iterations of [-1, 0, 2147483648]) { assert.deepStrictEqual(negResult, posResult); } - crypto.pbkdf2('password', 'salt', 1, -0, 'sha256', common.mustCall()); + crypto.pbkdf2( + validPassword, validSalt, validIterations, -0, 'sha256', + common.mustCall()); } // Should not get FATAL ERROR with empty password and salt // https://github.com/nodejs/node/issues/8571 -crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed()); +if (fips4) { + assertPBKDF2Fails('', '', 1, 32, 'sha256'); +} else { + crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed()); +} assert.throws( - () => crypto.pbkdf2('password', 'salt', 8, 8, common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, validSalt, validIterations, 8, common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -153,7 +211,7 @@ assert.throws( }); assert.throws( - () => crypto.pbkdf2Sync('password', 'salt', 8, 8), + () => crypto.pbkdf2Sync(validPassword, validSalt, validIterations, 8), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -162,7 +220,7 @@ assert.throws( }); assert.throws( - () => crypto.pbkdf2Sync('password', 'salt', 8, 8, null), + () => crypto.pbkdf2Sync(validPassword, validSalt, validIterations, 8, null), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -171,7 +229,8 @@ assert.throws( }); [1, {}, [], true, undefined, null].forEach((input) => { assert.throws( - () => crypto.pbkdf2(input, 'salt', 8, 8, 'sha256', common.mustNotCall()), + () => crypto.pbkdf2( + input, validSalt, validIterations, 8, 'sha256', common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -179,7 +238,9 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2('pass', input, 8, 8, 'sha256', common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, input, validIterations, 8, 'sha256', + common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -187,7 +248,8 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync(input, 'salt', 8, 8, 'sha256'), + () => crypto.pbkdf2Sync( + input, validSalt, validIterations, 8, 'sha256'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -195,7 +257,8 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync('pass', input, 8, 8, 'sha256'), + () => crypto.pbkdf2Sync( + validPassword, input, validIterations, 8, 'sha256'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -206,7 +269,8 @@ assert.throws( ['test', {}, [], true, undefined, null].forEach((i) => { const received = common.invalidArgTypeHelper(i); assert.throws( - () => crypto.pbkdf2('pass', 'salt', i, 8, 'sha256', common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, validSalt, i, 8, 'sha256', common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -215,7 +279,7 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync('pass', 'salt', i, 8, 'sha256'), + () => crypto.pbkdf2Sync(validPassword, validSalt, i, 8, 'sha256'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -224,15 +288,32 @@ assert.throws( ); }); +if (fips4) { + assertPBKDF2Fails( + new Uint8Array(1), validSalt, validIterations, validKeyLength, 'sha256'); + assertPBKDF2Fails( + validPassword, new Uint8Array(1), validIterations, validKeyLength, + 'sha256'); +} + // Any TypedArray should work for password and salt. for (const SomeArray of [Uint8Array, Uint16Array, Uint32Array, Float32Array, Float64Array, ArrayBuffer, SharedArrayBuffer]) { - runPBKDF2(new SomeArray(10), 'salt', 8, 8, 'sha256'); - runPBKDF2('pass', new SomeArray(10), 8, 8, 'sha256'); + const length = fips4 ? 16 : 10; + const input = new SomeArray(length); + const bytes = ArrayBuffer.isView(input) ? + new Uint8Array(input.buffer, input.byteOffset, input.byteLength) : + new Uint8Array(input); + for (let index = 0; index < bytes.length; index++) + bytes[index] = index + 1; + runPBKDF2(input, validSalt, validIterations, validKeyLength, 'sha256'); + runPBKDF2(validPassword, input, validIterations, validKeyLength, 'sha256'); } assert.throws( - () => crypto.pbkdf2('pass', 'salt', 8, 8, 'md55', common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, validSalt, validIterations, 8, 'md55', + common.mustNotCall()), { code: 'ERR_CRYPTO_INVALID_DIGEST', name: 'TypeError', @@ -241,7 +322,8 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync('pass', 'salt', 8, 8, 'md55'), + () => crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, 8, 'md55'), { code: 'ERR_CRYPTO_INVALID_DIGEST', name: 'TypeError', @@ -249,7 +331,7 @@ assert.throws( } ); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { const kNotPBKDF2Supported = ['shake128', 'shake256']; crypto.getHashes() .filter((hash) => !kNotPBKDF2Supported.includes(hash)) @@ -261,7 +343,8 @@ if (!hasOpenSSL3) { { // This should not crash. assert.throws( - () => crypto.pbkdf2Sync('1', '2', 1, 1, '%'), + () => crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, 1, '%'), { code: 'ERR_CRYPTO_INVALID_DIGEST', name: 'TypeError', diff --git a/test/parallel/test-crypto-pkcs12.js b/test/parallel/test-crypto-pkcs12.js new file mode 100644 index 00000000000..0ec9a2f544d --- /dev/null +++ b/test/parallel/test-crypto-pkcs12.js @@ -0,0 +1,226 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const fixtures = require('../common/fixtures'); +const { hasOpenSSL3, hasFIPS } = require('../common/crypto'); + +const fips3 = hasFIPS(3); + +if (fips3) { + // The long-standing fixtures protect their contents with PKCS12KDF and + // 3DES, neither of which the FIPS provider offers, so they cannot be opened + // at all. Assert the recognizable error rather than skipping: this is the + // same path the legacy.pfx case below exercises. + for (const name of ['rsa_cert.pfx', 'agent1.pfx']) { + assert.throws( + () => crypto.parsePKCS12(fixtures.readKey(name), { passphrase: 'sample' }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + } + + // Reading the PBMAC1-protected FIPS fixtures needs OpenSSL 3.5 or newer, so + // below that there is nothing further this test can check. + if (!hasFIPS(3, 5)) return; +} + +// The FIPS fixtures use PBMAC1/PBKDF2 and AES-256 in place of PKCS12KDF and +// 3DES. Their passphrase is eight characters because the OpenSSL 4 FIPS +// provider rejects shorter ones outright, which would mask the errors the +// cases below are actually about. +const keyAndCert = fips3 ? + { name: 'agent1-fips.pfx', passphrase: 'password', additional: 1 } : + { name: 'rsa_cert.pfx', passphrase: 'sample', additional: 0 }; +const withCaCert = fips3 ? + { name: 'agent1-fips.pfx', passphrase: 'password' } : + { name: 'agent1.pfx', passphrase: 'sample' }; +const ecKeyAndCert = fips3 ? + { name: 'ec-fips.pfx', passphrase: 'password' } : + { name: 'ec.pfx', passphrase: '' }; +const certWithoutKey = fips3 ? + { name: 'cert-without-key-fips.pfx', passphrase: 'password' } : + { name: 'cert-without-key.pfx', passphrase: 'test' }; + +{ + // Round-trip: a bundle holding one key and its certificate. + const bundle = fixtures.readKey(keyAndCert.name); + const { privateKey, certificate, additionalCertificates } = + crypto.parsePKCS12(bundle, { passphrase: keyAndCert.passphrase }); + + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.ok(certificate instanceof crypto.X509Certificate); + assert.ok(Array.isArray(additionalCertificates)); + assert.strictEqual(additionalCertificates.length, keyAndCert.additional); + + // The parsed key and certificate must actually correspond. + assert.strictEqual(certificate.checkPrivateKey(privateKey), true); + + // PEM export -- the primary reason callers want this API at all. + const keyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }); + assert.match(keyPem, /^-----BEGIN PRIVATE KEY-----/); + assert.match(certificate.toString(), /^-----BEGIN CERTIFICATE-----/); + + // Round-trips back through createPrivateKey. + const reimported = crypto.createPrivateKey(keyPem); + assert.strictEqual(reimported.asymmetricKeyType, 'rsa'); + + // Passphrase accepted as a buffer as well as a string. + const { privateKey: key2 } = crypto.parsePKCS12( + bundle, { passphrase: Buffer.from(keyAndCert.passphrase) }); + assert.strictEqual(key2.type, 'private'); +} + +{ + // A bundle carrying a second certificate alongside the end-entity one. + const bundle = fixtures.readKey(withCaCert.name); + const { privateKey, certificate, additionalCertificates } = + crypto.parsePKCS12(bundle, { passphrase: withCaCert.passphrase }); + assert.strictEqual(privateKey.type, 'private'); + assert.ok(certificate instanceof crypto.X509Certificate); + assert.strictEqual(additionalCertificates.length, 1); + assert.ok(additionalCertificates[0] instanceof crypto.X509Certificate); + assert.strictEqual(certificate.checkPrivateKey(privateKey), true); +} + +{ + // EC key. + const bundle = fixtures.readKey(ecKeyAndCert.name); + const { privateKey, certificate } = + crypto.parsePKCS12(bundle, { passphrase: ecKeyAndCert.passphrase }); + assert.strictEqual(privateKey.asymmetricKeyType, 'ec'); + assert.strictEqual(certificate.checkPrivateKey(privateKey), true); +} + +if (!fips3) { + // ec.pfx carries an empty passphrase, which the FIPS provider will not + // accept at all, so this case has no FIPS counterpart. Omitting the + // passphrase rather than passing it empty must behave identically: OpenSSL + // tries both PKCS#12 password encodings, so either variant opens the file. + const { privateKey } = crypto.parsePKCS12(fixtures.readKey('ec.pfx')); + assert.strictEqual(privateKey.asymmetricKeyType, 'ec'); +} + +{ + // Wrong passphrase must be distinguishable from malformed input: OpenSSL + // reports it as a MAC verification failure of its own, and BoringSSL + // reports the same condition as an incorrect password. It is deliberately + // longer than the FIPS minimum password length so that this checks what it + // says it does under FIPS too. + const code = process.features.openssl_is_boringssl ? + 'ERR_OSSL_INCORRECT_PASSWORD' : 'ERR_OSSL_PKCS12_MAC_VERIFY_FAILURE'; + const bundle = fixtures.readKey(keyAndCert.name); + assert.throws( + () => crypto.parsePKCS12(bundle, { passphrase: 'wrongpassphrase' }), + { code }); +} + +{ + // Not a PKCS#12 structure at all. The error must be the same on either + // crypto library: OpenSSL rejects this in d2i_PKCS12_bio(), while + // BoringSSL's only copies the bytes and leaves every structural check to + // PKCS12_parse(), so the binding has to recognize both. + assert.throws( + () => crypto.parsePKCS12(Buffer.from('not a bundle'), { passphrase: 'x' }), + { code: 'ERR_CRYPTO_OPERATION_FAILED', message: /not a valid PKCS#12/ }); +} + +{ + // A bundle with no private key. PKCS12_parse() identifies the end-entity + // certificate by its association with the key, so with no key there is + // nothing to single out and everything arrives through + // `additionalCertificates` -- including the end-entity certificate itself. + const bundle = fixtures.readKey(certWithoutKey.name); + const { privateKey, certificate, additionalCertificates } = + crypto.parsePKCS12(bundle, { passphrase: certWithoutKey.passphrase }); + assert.strictEqual(privateKey, null); + assert.strictEqual(certificate, null); + assert.strictEqual(additionalCertificates.length, 1); + assert.ok(additionalCertificates[0] instanceof crypto.X509Certificate); +} + +if (hasOpenSSL3) { + // Legacy algorithms (RC2-40-CBC) throw a recognizable, actionable error + // rather than a bare OpenSSL string. Mirrors the behavior added for the + // TLS path in https://github.com/nodejs/node/pull/54485. + const bundle = fixtures.readKey('legacy.pfx'); + assert.throws( + () => crypto.parsePKCS12(bundle, { passphrase: 'legacy' }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); +} + +{ + // ArrayBuffer is a documented input type for both the bundle and the + // passphrase, and must behave the same as a view over the same bytes. + const buf = fixtures.readKey(keyAndCert.name); + const bundleAb = + buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + const passBuf = Buffer.from(keyAndCert.passphrase); + const passAb = + passBuf.buffer.slice(passBuf.byteOffset, + passBuf.byteOffset + passBuf.byteLength); + + const { privateKey, certificate } = + crypto.parsePKCS12(bundleAb, { passphrase: passAb }); + assert.strictEqual(privateKey.type, 'private'); + assert.ok(certificate instanceof crypto.X509Certificate); + assert.strictEqual(certificate.checkPrivateKey(privateKey), true); + + // DataView too. + const { privateKey: key2 } = crypto.parsePKCS12( + new DataView(bundleAb), { passphrase: keyAndCert.passphrase }); + assert.strictEqual(key2.type, 'private'); +} + +{ + // A passphrase reaches OpenSSL as a NUL-terminated C string, and + // PKCS12_parse() takes no length alongside it. Without a check, everything + // from an embedded NUL onwards is dropped and the bundle opens under the + // truncated prefix -- a passphrase of '\0junk' would unlock a bundle + // protected by ''. Reject the passphrase instead, whatever form it + // arrives in. + const bundle = fixtures.readKey(keyAndCert.name); + const withNul = `${keyAndCert.passphrase}\u0000junk`; + const withNulBuf = Buffer.from(withNul); + const withNulAb = withNulBuf.buffer.slice( + withNulBuf.byteOffset, withNulBuf.byteOffset + withNulBuf.byteLength); + + const passphrases = [ + withNul, + withNulBuf, + withNulAb, + new DataView(withNulAb), + new Uint8Array(withNulAb), + // A trailing NUL is no more representable than an interior one. + `${keyAndCert.passphrase}\u0000`, + // And neither is a passphrase that is nothing but a NUL, which must not + // be mistaken for the empty passphrase. + '\u0000', + Buffer.from([0]), + ]; + + for (const passphrase of passphrases) { + assert.throws( + () => crypto.parsePKCS12(bundle, { passphrase }), + { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.passphrase.*must not contain null bytes/, + }); + } + + // The prefix on its own still opens the bundle, which is precisely why the + // truncation had to be rejected rather than tolerated. + assert.strictEqual( + crypto.parsePKCS12(bundle, { passphrase: keyAndCert.passphrase }) + .privateKey.type, + 'private'); +} + +{ + // Argument validation. + assert.throws(() => crypto.parsePKCS12('a string'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => crypto.parsePKCS12(Buffer.alloc(0), 'nope'), + { code: 'ERR_INVALID_ARG_TYPE' }); +} diff --git a/test/parallel/test-crypto-pqc-encrypted-pkcs8.js b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js index b4a1b586d21..f4e1d781abc 100644 --- a/test/parallel/test-crypto-pqc-encrypted-pkcs8.js +++ b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js @@ -4,9 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3, 5) && !isBoringSSL) common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); @@ -21,7 +21,7 @@ const algorithms = new Set([ 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', ]); // BoringSSL does not support ML-KEM-512. -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { algorithms.delete('ml-kem-512'); } @@ -30,19 +30,32 @@ if (process.features.openssl_is_boringssl) { // ciphers like RC2, the optional PBKDF2 keyLength INTEGER branch in // the EncryptedPrivateKeyInfo parser. const availableCiphers = new Set(getCiphers()); +const passphrase = 'top secret'; const ciphers = [ 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', 'des-ede3-cbc', 'rc2-cbc', -].filter((c) => availableCiphers.has(c)); +].filter((cipher) => availableCiphers.has(cipher) && + (!hasFIPS(3) || cipher !== 'rc2-cbc')); + +if (hasFIPS(3)) { + const { privateKey } = generateKeyPairSync('ml-dsa-44'); + assert.throws(() => privateKey.export({ + type: 'pkcs8', + format: 'der', + cipher: 'rc2-cbc', + passphrase, + }), { code: 'ERR_OSSL_EVP_UNSUPPORTED' }); +} -const passphrase = 'top secret'; +const wrongPassphrase = 'wrong password'; const wrongPassphraseError = /bad decrypt|DECRYPTION_FAILED|BAD_DECRYPT|bad password|DECODE[ _]ERROR/i; // A wrong passphrase usually fails during cipher finalization, but CBC output // can have valid padding by chance. OpenSSL then parses the bad plaintext as // PKCS#8 and may report ASN.1 or decoder errors from the same failed import. function assertWrongPassphrase(fn) { - assert.throws(fn, (err) => wrongPassphraseError.test(err.message) || + assert.throws(fn, (err) => err.code === 'ERR_OSSL_BAD_DECRYPT' || + wrongPassphraseError.test(err.message) || err.code?.startsWith('ERR_OSSL_ASN1_') || err.code === 'ERR_OSSL_UNSUPPORTED'); } @@ -79,7 +92,7 @@ for (const asymmetricKeyType of algorithms) { key: encrypted, format, type: 'pkcs8', - passphrase: 'wrong', + passphrase: wrongPassphrase, })); } } @@ -128,7 +141,7 @@ for (const { alg, jwkFile, encBase } of fixtureCases) { key: encryptedFixture, format, type: 'pkcs8', - passphrase: 'wrong', + passphrase: wrongPassphrase, })); } } diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js index 883c2b71238..715b313849d 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js @@ -4,7 +4,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const { @@ -79,7 +79,7 @@ for (const [asymmetricKeyType, pubLen] of [ assertPublicKey(createPublicKey(key)); key.export({ format: 'der', type: 'pkcs8' }); if (hasSeed) { - assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private); + assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private_seed_only); const jwk = key.export({ format: 'jwk' }); assertPrivateJwk(jwk); assert.strictEqual(key.equals(createPrivateKey({ format: 'jwk', key: jwk })), true); @@ -100,7 +100,7 @@ for (const [asymmetricKeyType, pubLen] of [ } } - if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { + if (!hasOpenSSL(3, 5) && !isBoringSSL) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -120,7 +120,7 @@ for (const [asymmetricKeyType, pubLen] of [ [keys.private_seed_only, true, true], [keys.private_priv_only, false, false], ]) { - if (process.features.openssl_is_boringssl && !seedOnly) { + if (isBoringSSL && !seedOnly) { common.printSkipMessage('Skipping unsupported private key format test'); continue; } diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js index de0755ca652..14473e82e3f 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js @@ -4,7 +4,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const { @@ -79,7 +79,7 @@ for (const [asymmetricKeyType, pubLen] of [ assertPublicKey(createPublicKey(key)); key.export({ format: 'der', type: 'pkcs8' }); if (hasSeed) { - assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private); + assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private_seed_only); const jwk = key.export({ format: 'jwk' }); assertPrivateJwk(jwk); assert.strictEqual(key.equals(createPrivateKey({ format: 'jwk', key: jwk })), true); @@ -100,7 +100,7 @@ for (const [asymmetricKeyType, pubLen] of [ } } - if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { + if (!hasOpenSSL(3, 5) && !isBoringSSL) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -110,7 +110,7 @@ for (const [asymmetricKeyType, pubLen] of [ code: hasOpenSSL(3) ? 'ERR_OSSL_UNSUPPORTED' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); } - } else if (process.features.openssl_is_boringssl && asymmetricKeyType === 'ml-kem-512') { + } else if (isBoringSSL && asymmetricKeyType === 'ml-kem-512') { // BoringSSL does not support ML-KEM-512. assert.throws(() => createPublicKey(keys.public), { code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' }); @@ -123,7 +123,7 @@ for (const [asymmetricKeyType, pubLen] of [ assertPublicKey(publicKey); { - const entries = process.features.openssl_is_boringssl ? + const entries = isBoringSSL ? // BoringSSL only supports the seed-only PKCS#8 private key encoding. [[keys.private_seed_only, true]] : [ diff --git a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js index e6534c988c4..9f9b25bed8b 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js @@ -4,14 +4,14 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const { generateKeyPair, } = require('crypto'); -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { +if (!hasOpenSSL(3, 5) && !isBoringSSL) { for (const asymmetricKeyType of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', diff --git a/test/parallel/test-crypto-pqc-keygen-ml-kem.js b/test/parallel/test-crypto-pqc-keygen-ml-kem.js index 620f65c3a8d..bcc5e599668 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-kem.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-kem.js @@ -4,19 +4,19 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const { generateKeyPair, } = require('crypto'); -const algorithms = process.features.openssl_is_boringssl ? +const algorithms = isBoringSSL ? // BoringSSL does not support ML-KEM-512. ['ml-kem-768', 'ml-kem-1024'] : ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']; -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { +if (!hasOpenSSL(3, 5) && !isBoringSSL) { for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', @@ -72,7 +72,7 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { } } -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { assert.throws(() => generateKeyPair('ml-kem-512', common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', message: /The argument 'type' must be a supported key type/ diff --git a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js index 535e6a33d5c..e095269bf61 100644 --- a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js @@ -4,9 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3, 5) && !isBoringSSL) common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); @@ -39,7 +39,7 @@ for (const [asymmetricKeyType, sigLen] of [ [keys.private_seed_only, true], [keys.private_priv_only, false], ]) { - if (process.features.openssl_is_boringssl && !seedOnly) { + if (isBoringSSL && !seedOnly) { common.printSkipMessage('Skipping unsupported private key format test'); continue; } @@ -52,7 +52,7 @@ for (const [asymmetricKeyType, sigLen] of [ assert.strictEqual(verify(undefined, data, keys.public, Buffer.alloc(sigLen)), false); assert.strictEqual(verify(undefined, data, keys.public, signature), true); assert.strictEqual(verify(undefined, data, privateKey, signature), true); - const code = process.features.openssl_is_boringssl ? + const code = isBoringSSL ? 'ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED' : 'ERR_OSSL_INVALID_DIGEST'; assert.throws(() => sign('sha256', data, privateKey), { code }); assert.throws( @@ -72,7 +72,7 @@ for (const [asymmetricKeyType, sigLen] of [ })); })); - const message = process.features.openssl_is_boringssl ? /COMMAND_NOT_SUPPORTED/ : /invalid digest/; + const message = isBoringSSL ? /COMMAND_NOT_SUPPORTED/ : /invalid digest/; sign('sha256', data, privateKey, common.expectsError(message)); verify('sha256', data, keys.public, Buffer.alloc(sigLen), common.expectsError(message)); } diff --git a/test/parallel/test-crypto-prepare-asymmetric-cryptokey.js b/test/parallel/test-crypto-prepare-asymmetric-cryptokey.js new file mode 100644 index 00000000000..4701480eb88 --- /dev/null +++ b/test/parallel/test-crypto-prepare-asymmetric-cryptokey.js @@ -0,0 +1,50 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const assert = require('node:assert'); +const crypto = require('node:crypto'); +const fixtures = require('../common/fixtures'); +const { + prepareAsymmetricKey, + getCryptoKeyHandle, + kConsumePublic, + kConsumePrivate, + kCreatePublic, + kCreatePrivate, +} = require('internal/crypto/keys'); + +common.expectWarning({ + DeprecationWarning: { + DEP0203: 'Passing a CryptoKey to node:crypto functions is deprecated.', + }, +}); + +const privateKeyObject = crypto.createPrivateKey(fixtures.readKey('ec_p256_private.pem')); +const algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; +const privateKey = privateKeyObject.toCryptoKey(algorithm, true, ['sign']); +const publicKey = crypto.createPublicKey(privateKeyObject).toCryptoKey(algorithm, true, ['verify']); +const secretKey = crypto.createSecretKey(Buffer.alloc(16)).toCryptoKey('AES-CBC', true, ['encrypt']); + +for (const key of [privateKey, publicKey, secretKey]) { + for (const input of [key, { key, format: 'raw-public', asymmetricKeyType: 'invalid' }]) { + for (const context of [kConsumePublic, kConsumePrivate, kCreatePublic]) { + if (key === privateKey || (key === publicKey && context === kConsumePublic)) { + assert.strictEqual(prepareAsymmetricKey(input, context).data, getCryptoKeyHandle(key)); + } else { + const expected = context === kConsumePublic ? 'private or public' : 'private'; + assert.throws(() => prepareAsymmetricKey(input, context), { + code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + name: 'TypeError', + message: `Invalid key object type ${key.type}, expected ${expected}.`, + }); + } + } + assert.throws(() => prepareAsymmetricKey(input, kCreatePrivate), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } +} diff --git a/test/parallel/test-crypto-prepare-asymmetric-key.js b/test/parallel/test-crypto-prepare-asymmetric-key.js new file mode 100644 index 00000000000..de5ea1b483d --- /dev/null +++ b/test/parallel/test-crypto-prepare-asymmetric-key.js @@ -0,0 +1,101 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const assert = require('node:assert'); +const crypto = require('node:crypto'); +const fixtures = require('../common/fixtures'); +const { + prepareAsymmetricKey, + getKeyObjectHandle, + kConsumePublic, + kConsumePrivate, + kCreatePublic, + kCreatePrivate, +} = require('internal/crypto/keys'); + +const pem = fixtures.readKey('ec_p256_private.pem'); +const keyObject = crypto.createPrivateKey(pem); +const contexts = [kConsumePublic, kConsumePrivate, kCreatePublic, kCreatePrivate]; + +for (const input of [ + pem, + new Uint8Array(pem), + new DataView(Uint8Array.from(pem).buffer), + Uint8Array.from(pem).buffer, + new SharedArrayBuffer(pem.length), +]) { + Object.defineProperty(input, 'key', { get: common.mustNotCall() }); + Object.defineProperty(input, 'format', { get: common.mustNotCall() }); + Object.defineProperty(input, Symbol.toStringTag, { value: 'KeyObject' }); + for (const context of contexts) { + assert.strictEqual(prepareAsymmetricKey(input, context).data, input); + assert.strictEqual(prepareAsymmetricKey({ key: input }, context).data, input); + } +} + +for (const context of [kConsumePublic, kConsumePrivate, kCreatePublic]) { + const options = { key: keyObject, format: 'raw-public', asymmetricKeyType: 'invalid' }; + assert.strictEqual(prepareAsymmetricKey(options, context).data, getKeyObjectHandle(keyObject)); +} + +{ + const reads = []; + const values = { key: pem.toString(), encoding: 'utf8', format: 'pem' }; + const options = {}; + for (const name of ['key', 'encoding', 'format', 'properties', 'type', 'cipher', 'passphrase']) { + Object.defineProperty(options, name, { + get() { + reads.push(name); + return values[name]; + }, + }); + } + assert.deepStrictEqual(prepareAsymmetricKey(options, kConsumePrivate).data, pem); + assert.deepStrictEqual(reads, [ + 'key', 'encoding', 'format', 'properties', 'format', 'type', 'cipher', 'passphrase', 'encoding', + ]); +} + +{ + const input = { + key: pem.toString(), + encoding: 'invalid', + get format() { return 'pem'; }, + get type() { return assert.fail('String conversion must precede encoding option parsing'); }, + }; + assert.throws(() => prepareAsymmetricKey(input, kConsumePrivate), { code: 'ERR_UNKNOWN_ENCODING' }); +} + +{ + const raw = keyObject.export({ format: 'raw-private' }); + const input = { key: raw, format: 'raw-private', asymmetricKeyType: 'ec', namedCurve: 'P-256' }; + assert.strictEqual(prepareAsymmetricKey(input, kConsumePrivate).data, raw); + assert.throws(() => prepareAsymmetricKey({ ...input, key: raw.toString('hex') }, kConsumePrivate), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +for (const encoding of [undefined, 'buffer', 'utf8', 'hex', 'base64']) { + const inputEncoding = encoding === 'buffer' ? 'utf8' : encoding; + const passphrase = Buffer.from('password'); + const result = prepareAsymmetricKey({ + key: pem.toString(inputEncoding), + passphrase: passphrase.toString(inputEncoding), + encoding, + }, kConsumePrivate); + assert.deepStrictEqual(result.data, pem); + assert.deepStrictEqual(result.passphrase, passphrase); +} + +for (const [options, optionName] of [ + [{ format: 'invalid' }, 'format'], + [{ format: 'der', type: 'invalid' }, 'type'], +]) { + assert.throws(() => prepareAsymmetricKey({ key: pem, ...options }, kConsumePrivate, 'options.key'), { + code: 'ERR_INVALID_ARG_VALUE', + message: new RegExp(`options\\.key\\.${optionName}`), + }); +} diff --git a/test/parallel/test-crypto-private-decrypt-gh32240.js b/test/parallel/test-crypto-private-decrypt-gh32240.js index 1ff5b565d6d..a38fcba6775 100644 --- a/test/parallel/test-crypto-private-decrypt-gh32240.js +++ b/test/parallel/test-crypto-private-decrypt-gh32240.js @@ -14,29 +14,57 @@ const { privateDecrypt, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); -const pair = generateKeyPairSync('rsa', { modulusLength: 512 }); +const fips3 = hasFIPS(3); +const fips4 = hasFIPS(4); +const pair = generateKeyPairSync('rsa', { + modulusLength: fips3 ? 2048 : 512, +}); const expected = Buffer.from('shibboleth'); -const encrypted = publicEncrypt(pair.publicKey, expected); +const options = fips3 ? { oaepHash: 'sha256' } : {}; +const encrypted = publicEncrypt({ key: pair.publicKey, ...options }, expected); const pkey = pair.privateKey.export({ type: 'pkcs1', format: 'pem' }); -const pkeyEncrypted = - pair.privateKey.export({ +if (fips3) { + assert.throws(() => pair.privateKey.export({ type: 'pkcs1', format: 'pem', cipher: 'aes-128-cbc', passphrase: 'secret', + }), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); +} +if (fips4) { + assert.throws(() => pair.privateKey.export({ + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: 'secret', + }), { + code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + }); +} +const pkeyEncrypted = + pair.privateKey.export({ + type: fips3 ? 'pkcs8' : 'pkcs1', + format: 'pem', + cipher: fips3 ? 'aes-256-cbc' : 'aes-128-cbc', + passphrase: 'password', }); function decrypt(key) { - const decrypted = privateDecrypt(key, encrypted); + const decrypted = privateDecrypt({ key, ...options }, encrypted); assert.deepStrictEqual(decrypted, expected); } decrypt(pkey); -assert.throws(() => decrypt(pkeyEncrypted), hasOpenSSL3 ? +assert.throws(() => decrypt(pkeyEncrypted), hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto routines::interrupted or ' + 'cancelled' } : { code: 'ERR_MISSING_PASSPHRASE' }); diff --git a/test/parallel/test-crypto-provider-cipher-cache-snapshot.js b/test/parallel/test-crypto-provider-cache-snapshot.js similarity index 76% rename from test/parallel/test-crypto-provider-cipher-cache-snapshot.js rename to test/parallel/test-crypto-provider-cache-snapshot.js index 1afc5df8d94..522dedfc436 100644 --- a/test/parallel/test-crypto-provider-cipher-cache-snapshot.js +++ b/test/parallel/test-crypto-provider-cache-snapshot.js @@ -4,16 +4,16 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const assert = require('assert'); -const { hasOpenSSL3 } = require('../common/crypto'); +const assert = require('node:assert'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); -if (!hasOpenSSL3) +if (!hasOpenSSL(3) || isBoringSSL) common.skip('this test requires OpenSSL 3.x'); -const entry = fixtures.path('snapshot', 'crypto-provider-cipher-cache.js'); +const entry = fixtures.path('snapshot', 'crypto-provider-cache.js'); const buildEnv = { OPENSSL_CONF: fixtures.path( 'openssl3-conf', 'legacy_provider_enabled.cnf'), diff --git a/test/parallel/test-crypto-provider-cache.js b/test/parallel/test-crypto-provider-cache.js new file mode 100644 index 00000000000..4767d47dc52 --- /dev/null +++ b/test/parallel/test-crypto-provider-cache.js @@ -0,0 +1,364 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); +if (!hasOpenSSL(3) || isBoringSSL) + common.skip('this test requires OpenSSL 3 provider support'); + +const assert = require('node:assert'); +const crypto = require('node:crypto'); +const { once } = require('node:events'); +const { + isMainThread, + parentPort, + Worker, + workerData, +} = require('node:worker_threads'); +const { getMacCache } = require('internal/crypto/util'); +const { internalBinding } = require('internal/test/binding'); + +if (!isMainThread && !workerData?.cryptoCacheTest) + common.skip('crypto.setFips() is not supported in workers'); + +const binding = internalBinding('crypto'); +const getters = ['getCiphers', 'getHashes', 'getMacs', 'getCurves']; +const cipherAlgorithm = 'camellia-128-cbc-cts'; +const cipherKey = Buffer.alloc(16); +const iv = Buffer.alloc(16); +const plaintext = Buffer.alloc(32); +const hashAlgorithm = 'md5'; +const emptyHash = 'd41d8cd98f00b204e9800998ecf8427e'; +const macAlgorithm = 'poly1305'; +const macKey = Buffer.from( + '85d6be7857556d337f4452fe42d506a8' + + '0103808afb0db2fd4abff6af4149f51b', + 'hex', +); +const macData = Buffer.from('Cryptographic Forum Research Group'); +const expectedMac = 'a8061dc1305136c6c22b8baf0c0127a9'; +const curve = 'secp256k1'; + +function checkLists() { + const lists = {}; + for (const name of getters) { + const list = crypto[name](); + assert(list.every((entry) => typeof entry === 'string'), name); + assert.deepStrictEqual(list, [...list].sort(), name); + assert.strictEqual( + new Set(list.map((entry) => entry.toLowerCase())).size, + list.length, + name, + ); + const disposable = crypto[name](); + assert.notStrictEqual(disposable, list, name); + disposable.length = 0; + disposable.push('not-a-real-algorithm'); + assert.deepStrictEqual(crypto[name](), list, name); + lists[name] = list; + } + return lists; +} + +function getAliasId(aliases, name) { + const normalized = name.toLowerCase(); + for (const [alias, id] of Object.entries(aliases)) { + if (alias.toLowerCase() === normalized) return id; + } + return undefined; +} + +function checkMacAliases() { + // Both spellings must refer to one cached EVP_MAC. + const aliases = binding.getCachedMacAliases(); + const id = getAliasId(aliases, 'kmac128'); + const hyphenatedId = getAliasId(aliases, 'kmac-128'); + if (id === undefined || hyphenatedId === undefined) { + common.printSkipMessage('KMAC-128 aliases are not available'); + return; + } + assert.strictEqual(id, hyphenatedId); + const options = { outputLength: 32 }; + const key = Buffer.alloc(32, 0x42); + const data = Buffer.from('cache alias test'); + assert.deepStrictEqual( + crypto.createMac('KMAC128', key, options).update(data).final(), + crypto.createMac('KMAC-128', key, options).update(data).final(), + ); + const after = binding.getCachedMacAliases(); + assert.strictEqual(getAliasId(after, 'kmac128'), id); + assert.strictEqual(getAliasId(after, 'kmac-128'), id); +} + +function createFixtures(lists) { + const fixtures = {}; + if (lists.getCiphers.includes(cipherAlgorithm)) { + const info = crypto.getCipherInfo(cipherAlgorithm); + assert(info); + assert.deepStrictEqual( + crypto.getCipherInfo(cipherAlgorithm.toUpperCase()), info); + assert.deepStrictEqual(crypto.getCipherInfo(cipherAlgorithm), info); + fixtures.cipher = crypto.createCipheriv(cipherAlgorithm, cipherKey, iv); + } else { + common.printSkipMessage(`${cipherAlgorithm} is not supported`); + } + for (let i = 0; i < 2; i++) { + assert.strictEqual( + crypto.getCipherInfo('node-test-unknown-provider-cipher'), undefined); + } + fixtures.hash = lists.getHashes.includes(hashAlgorithm); + if (lists.getMacs.includes(macAlgorithm)) { + fixtures.mac = crypto.createMac(macAlgorithm, macKey).update(macData); + fixtures.macId = getAliasId(binding.getCachedMacAliases(), macAlgorithm); + assert.strictEqual(typeof fixtures.macId, 'number'); + } else { + common.printSkipMessage(`${macAlgorithm} is not supported`); + } + checkMacAliases(); + if (lists.getCurves.includes(curve)) { + fixtures.ecdh = crypto.createECDH(curve); + fixtures.ecdh.generateKeys(); + fixtures.peer = crypto.createECDH(curve).generateKeys(); + fixtures.ecdh.computeSecret(fixtures.peer); + } + return fixtures; +} + +function checkEnabled(fixtures, available) { + // Availability comes from a fresh environment. Exercise cached handles before + // refreshing the warmed JavaScript lists. + assert(!available.getCiphers.includes(cipherAlgorithm)); + assert(!available.getHashes.includes(hashAlgorithm)); + assert(!available.getCurves.includes(curve)); + if (fixtures.cipher !== undefined) { + assert.strictEqual(crypto.getCipherInfo(cipherAlgorithm), undefined); + assert.throws( + () => crypto.createCipheriv(cipherAlgorithm, cipherKey, iv), + { code: 'ERR_CRYPTO_UNKNOWN_CIPHER' }, + ); + const output = Buffer.concat([ + fixtures.cipher.update(plaintext), fixtures.cipher.final(), + ]); + assert.strictEqual(output.length, plaintext.length); + } + if (fixtures.mac !== undefined) { + if (!available.getMacs.includes(macAlgorithm)) { + assert.throws(() => crypto.createMac(macAlgorithm, macKey), { + code: 'ERR_CRYPTO_INVALID_MAC', + }); + assert.strictEqual( + getAliasId(binding.getCachedMacAliases(), macAlgorithm), undefined); + } + assert.strictEqual(fixtures.mac.final('hex'), expectedMac); + } + if (fixtures.ecdh !== undefined) { + // Existing keys and keys installed after the transition obey current policy. + const error = { code: 'ERR_CRYPTO_INVALID_KEYPAIR', name: 'RangeError' }; + assert.throws(() => fixtures.ecdh.computeSecret(fixtures.peer), error); + const installed = crypto.createECDH(curve); + installed.setPrivateKey(Buffer.from('cafebabe'.repeat(8), 'hex')); + assert.throws(() => installed.computeSecret(fixtures.peer), error); + } +} + +function checkDisabled(fixtures) { + if (fixtures.cipher !== undefined) { + assert(crypto.getCipherInfo(cipherAlgorithm)); + const cipher = crypto.createCipheriv(cipherAlgorithm, cipherKey, iv); + const output = Buffer.concat([cipher.update(plaintext), cipher.final()]); + assert.strictEqual(output.length, plaintext.length); + } + if (fixtures.hash) { + assert.strictEqual(crypto.createHash(hashAlgorithm).digest('hex'), emptyHash); + } + if (fixtures.mac !== undefined) { + assert.strictEqual( + crypto.createMac(macAlgorithm, macKey).update(macData).final('hex'), + expectedMac, + ); + const id = getAliasId(binding.getCachedMacAliases(), macAlgorithm); + assert.strictEqual(typeof id, 'number'); + assert.notStrictEqual(id, fixtures.macId); + } + if (fixtures.ecdh !== undefined) fixtures.ecdh.computeSecret(fixtures.peer); +} + +function setFips(value) { + const before = crypto.getFips(); + const generation = binding.getFipsCryptoGeneration(); + try { + crypto.setFips(value); + } catch (err) { + assert.strictEqual(crypto.getFips(), before); + assert.strictEqual(binding.getFipsCryptoGeneration(), generation); + throw err; + } + assert.strictEqual(crypto.getFips(), value); + assert.strictEqual( + binding.getFipsCryptoGeneration(), + generation + (before === value ? 0n : 1n), + ); +} + +// Cached IDs are written back through JavaScript properties. Setter failures +// must propagate for both a stale ID and an uncached (-1) ID. +function withMacCacheSetter(fixtures, toggle) { + if (fixtures.mac === undefined) return toggle(); + const cache = getMacCache(); + const name = Object.keys(cache).find( + (name) => name.toLowerCase() === macAlgorithm); + assert(name); + const descriptor = Object.getOwnPropertyDescriptor(cache, name); + assert.strictEqual(descriptor.value, fixtures.macId); + const sentinel = new Error('mac cache setter'); + function install(id) { + Object.defineProperty(cache, name, { + __proto__: null, + configurable: true, + enumerable: descriptor.enumerable, + get() { return id; }, + set() { throw sentinel; }, + }); + } + + function check() { + assert.throws(() => crypto.createMac(name, macKey), (err) => err === sentinel); + } + try { + install(-1); + check(); + install(descriptor.value); + toggle(); + check(); + install(-1); + check(); + } finally { + Object.defineProperty(cache, name, descriptor); + } +} + +if (!isMainThread) { + function reply(phase, lists) { + parentPort.postMessage({ + phase, + lists, + fips: crypto.getFips(), + generation: binding.getFipsCryptoGeneration(), + }); + } + if (workerData.listsOnly) { + parentPort.once('message', common.mustCall(() => { + reply('fresh', checkLists()); + })); + parentPort.postMessage('ready'); + } else { + let lists = checkLists(); + const fixtures = createFixtures(lists); + assert.throws(() => setFips(1), { + code: 'ERR_WORKER_UNSUPPORTED_OPERATION', + }); + assert.deepStrictEqual(checkLists(), lists); + reply('warm', lists); + parentPort.on('message', common.mustCallAtLeast(({ phase, available }) => { + if (phase === 'done') { + parentPort.close(); + return; + } + if (phase === 'fips-on') checkEnabled(fixtures, available); + if (phase === 'fips-off') checkDisabled(fixtures); + const next = checkLists(); + if (phase === 'unchanged') assert.deepStrictEqual(next, lists); + lists = next; + reply(phase, lists); + })); + } +} else { + async function main() { + const originalFips = crypto.getFips(); + const originalLists = checkLists(); + try { + setFips(0); + } catch (err) { + if (err.code !== 'ERR_CRYPTO_FIPS_FORCED') throw err; + assert.deepStrictEqual(checkLists(), originalLists); + common.printSkipMessage('FIPS mode cannot be disabled'); + return; + } + let worker; + let freshWorker; + try { + const defaultLists = checkLists(); + const fixtures = createFixtures(defaultLists); + worker = new Worker(__filename, { workerData: { cryptoCacheTest: true } }); + worker.on('error', common.mustNotCall()); + const exitPromise = once(worker, 'exit'); + function checkReply(message, phase, lists) { + assert.strictEqual(message.phase, phase); + assert.strictEqual(message.fips, crypto.getFips()); + assert.strictEqual( + message.generation, binding.getFipsCryptoGeneration()); + assert.deepStrictEqual(message.lists, lists); + } + + async function exchange(phase, lists) { + const response = once(worker, 'message'); + worker.postMessage({ phase, available: lists }); + const [message] = await response; + checkReply(message, phase, lists); + } + const [warm] = await once(worker, 'message'); + checkReply(warm, 'warm', defaultLists); + + setFips(0); + assert.deepStrictEqual(checkLists(), defaultLists); + await exchange('unchanged', defaultLists); + + // AIX uses OpenSSL entropy when initializing a worker's V8 isolate. + // Start it before enabling FIPS properties, which can succeed without a + // FIPS provider, but defer its first cache lookup until after the toggle. + freshWorker = new Worker(__filename, { + workerData: { cryptoCacheTest: true, listsOnly: true }, + }); + freshWorker.on('error', common.mustNotCall()); + const freshExit = once(freshWorker, 'exit'); + const [ready] = await once(freshWorker, 'message'); + assert.strictEqual(ready, 'ready'); + + withMacCacheSetter(fixtures, () => setFips(1)); + // A cold environment provides independent expectations for both native + // and JavaScript caches, including when no FIPS provider is installed. + const freshResponse = once(freshWorker, 'message'); + freshWorker.postMessage('read'); + const [fresh] = await freshResponse; + const [freshCode] = await freshExit; + assert.strictEqual(freshCode, 0); + const enabledLists = fresh.lists; + checkReply(fresh, 'fresh', enabledLists); + checkEnabled(fixtures, enabledLists); + assert.deepStrictEqual(checkLists(), enabledLists); + await exchange('fips-on', enabledLists); + + setFips(1); + assert.deepStrictEqual(checkLists(), enabledLists); + await exchange('unchanged', enabledLists); + + setFips(0); + checkDisabled(fixtures); + assert.deepStrictEqual(checkLists(), defaultLists); + await exchange('fips-off', defaultLists); + + worker.postMessage({ phase: 'done' }); + const [code] = await exitPromise; + assert.strictEqual(code, 0); + } finally { + if (freshWorker !== undefined && freshWorker.threadId !== -1) + await freshWorker.terminate(); + if (worker !== undefined && worker.threadId !== -1) await worker.terminate(); + setFips(originalFips); + } + } + main().then(common.mustCall()); +} diff --git a/test/parallel/test-crypto-provider-cipher-cache.js b/test/parallel/test-crypto-provider-cipher-cache.js deleted file mode 100644 index bde9988480e..00000000000 --- a/test/parallel/test-crypto-provider-cipher-cache.js +++ /dev/null @@ -1,183 +0,0 @@ -// Flags: --expose-internals --no-warnings -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const { hasOpenSSL3 } = require('../common/crypto'); -if (!hasOpenSSL3) - common.skip('this test requires OpenSSL 3.x'); - -const assert = require('assert'); -const { - createCipheriv, - getCipherInfo, - getCiphers, - getFips, - getHashes, - setFips, -} = require('crypto'); -const { internalBinding } = require('internal/test/binding'); -const { isMainThread, Worker } = require('worker_threads'); - -if (!isMainThread) - common.skip('crypto.setFips() is not supported in workers'); - -const algorithm = 'camellia-128-cbc-cts'; -const hashAlgorithm = 'md5'; -const originalFips = getFips(); -setFips(0); - -if (!getCiphers().includes(algorithm)) { - common.skip(`${algorithm} is not supported`); -} -assert(getHashes().includes(hashAlgorithm)); - -const binding = internalBinding('crypto'); -const generation = binding.getFipsCryptoGeneration(); -setFips(0); -assert.strictEqual(binding.getFipsCryptoGeneration(), generation); - -const ciphers = getCiphers(); -ciphers.length = 0; -assert(getCiphers().includes(algorithm)); - -const info = getCipherInfo(algorithm); -assert(info); -assert.deepStrictEqual(getCipherInfo(algorithm.toUpperCase()), info); -assert.deepStrictEqual(getCipherInfo(algorithm), info); -assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined); -assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined); - -const key = Buffer.alloc(16); -const iv = Buffer.alloc(16); -const plaintext = Buffer.alloc(32); -const liveCipher = createCipheriv(algorithm, key, iv); - -const worker = new Worker(` - 'use strict'; - const { - createHash, - createCipheriv, - getCipherInfo, - getCiphers, - getHashes, - } = require('crypto'); - const { internalBinding } = require('internal/test/binding'); - const { parentPort, workerData } = require('worker_threads'); - - const binding = internalBinding('crypto'); - const key = Buffer.from(workerData.key); - const iv = Buffer.from(workerData.iv); - const plaintext = Buffer.from(workerData.plaintext); - const liveCipher = createCipheriv(workerData.algorithm, key, iv); - - getHashes(); - getCiphers(); - getCipherInfo(workerData.algorithm); - parentPort.postMessage({ - phase: 'warm', - generation: binding.getFipsCryptoGeneration(), - }); - - parentPort.on('message', (phase) => { - if (phase === 'fips-on') { - let errorCode; - try { - createCipheriv(workerData.algorithm, key, iv); - } catch (error) { - errorCode = error.code; - } - const output = Buffer.concat([ - liveCipher.update(plaintext), - liveCipher.final(), - ]); - parentPort.postMessage({ - phase, - errorCode, - generation: binding.getFipsCryptoGeneration(), - hasCipher: getCiphers().includes(workerData.algorithm), - hasHash: getHashes().includes(workerData.hashAlgorithm), - hasInfo: getCipherInfo(workerData.algorithm) !== undefined, - outputLength: output.length, - }); - } else if (phase === 'fips-off') { - const cipher = createCipheriv(workerData.algorithm, key, iv); - const hash = createHash(workerData.hashAlgorithm).digest('hex'); - const output = Buffer.concat([ - cipher.update(plaintext), - cipher.final(), - ]); - parentPort.postMessage({ - phase, - generation: binding.getFipsCryptoGeneration(), - hasHash: getHashes().includes(workerData.hashAlgorithm), - hasCipher: getCiphers().includes(workerData.algorithm), - hasInfo: getCipherInfo(workerData.algorithm) !== undefined, - hash, - outputLength: output.length, - }); - } else { - parentPort.close(); - } - }); -`, { - eval: true, - workerData: { algorithm, hashAlgorithm, key, iv, plaintext }, -}); - -let enabledGeneration; -worker.on('message', common.mustCall((message) => { - if (message.phase === 'warm') { - assert.strictEqual(message.generation, generation); - - setFips(1); - enabledGeneration = binding.getFipsCryptoGeneration(); - assert.strictEqual(enabledGeneration, generation + 1n); - assert(!getCiphers().includes(algorithm)); - assert(!getHashes().includes(hashAlgorithm)); - assert.strictEqual(getCipherInfo(algorithm), undefined); - assert.throws(() => createCipheriv(algorithm, key, iv), { - code: 'ERR_CRYPTO_UNKNOWN_CIPHER', - }); - - const output = Buffer.concat([ - liveCipher.update(plaintext), - liveCipher.final(), - ]); - assert.strictEqual(output.length, plaintext.length); - worker.postMessage('fips-on'); - } else if (message.phase === 'fips-on') { - assert.strictEqual(message.generation, enabledGeneration); - assert.strictEqual(message.hasCipher, false); - assert.strictEqual(message.hasHash, false); - assert.strictEqual(message.hasInfo, false); - assert.strictEqual(message.errorCode, 'ERR_CRYPTO_UNKNOWN_CIPHER'); - assert.strictEqual(message.outputLength, plaintext.length); - - setFips(0); - assert.strictEqual( - binding.getFipsCryptoGeneration(), enabledGeneration + 1n); - assert(getHashes().includes(hashAlgorithm)); - assert(getCiphers().includes(algorithm)); - assert(getCipherInfo(algorithm)); - worker.postMessage('fips-off'); - } else { - assert.strictEqual(message.phase, 'fips-off'); - assert.strictEqual( - message.generation, binding.getFipsCryptoGeneration()); - assert.strictEqual(message.hasCipher, true); - assert.strictEqual(message.hasHash, true); - assert.strictEqual(message.hasInfo, true); - assert.strictEqual( - message.hash, - 'd41d8cd98f00b204e9800998ecf8427e', - ); - assert.strictEqual(message.outputLength, plaintext.length); - worker.postMessage('done'); - setFips(originalFips); - } -}, 3)); -worker.on('error', common.mustNotCall()); -worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0))); diff --git a/test/parallel/test-crypto-provider-hash-options.js b/test/parallel/test-crypto-provider-hash-options.js index 609d00d7f7b..ecccc02f01e 100644 --- a/test/parallel/test-crypto-provider-hash-options.js +++ b/test/parallel/test-crypto-provider-hash-options.js @@ -3,12 +3,14 @@ const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); + if (!common.hasCrypto) { common.skip('missing crypto'); } if (Number(process.versions.openssl.split('.')[0]) < 4 || - process.features.openssl_is_boringssl) { + isBoringSSL) { common.skip('OpenSSL 4 provider support is required'); } diff --git a/test/parallel/test-crypto-provider-hashes.js b/test/parallel/test-crypto-provider-hashes.js index 166efaa0d7f..5e688303910 100644 --- a/test/parallel/test-crypto-provider-hashes.js +++ b/test/parallel/test-crypto-provider-hashes.js @@ -26,9 +26,9 @@ const { sign, verify, } = require('node:crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL3 || process.features.openssl_is_boringssl) { +if (!hasOpenSSL(3) || isBoringSSL) { common.skip('OpenSSL 3 provider support is required'); } @@ -41,12 +41,6 @@ const { const hashes = getHashes(); const lowercaseHashes = hashes.map((name) => name.toLowerCase()); -const modifiedHashes = getHashes(); -modifiedHashes.length = 0; - -assert.deepStrictEqual(hashes, [...hashes].sort()); -assert.deepStrictEqual(getHashes(), hashes); -assert.strictEqual(new Set(lowercaseHashes).size, hashes.length); if (lowercaseHashes.includes('sha1')) { assert(hashes.includes('RSA-SHA1')); } diff --git a/test/parallel/test-crypto-publicDecrypt-fails-first-time.js b/test/parallel/test-crypto-publicDecrypt-fails-first-time.js index 1d64e08920c..21cc5f3ebce 100644 --- a/test/parallel/test-crypto-publicDecrypt-fails-first-time.js +++ b/test/parallel/test-crypto-publicDecrypt-fails-first-time.js @@ -7,15 +7,17 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { common.skip('only openssl3'); // https://github.com/nodejs/node/pull/42793#issuecomment-1107491901 } const assert = require('assert'); const crypto = require('crypto'); +const passphrase = 'password'; + const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { @@ -26,7 +28,7 @@ const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { type: 'pkcs8', format: 'pem', cipher: 'aes-128-ecb', - passphrase: 'abcdef' + passphrase } }); assert.notStrictEqual(privateKey.toString(), ''); @@ -35,7 +37,7 @@ const msg = 'The quick brown fox jumps over the lazy dog'; const encryptedString = crypto.privateEncrypt({ key: privateKey, - passphrase: 'abcdef' + passphrase }, Buffer.from(msg)).toString('base64'); const decryptedString = crypto.publicDecrypt(publicKey, Buffer.from(encryptedString, 'base64')).toString(); console.log(`Encrypted: ${encryptedString}`); diff --git a/test/parallel/test-crypto-raw-key-type-validation.js b/test/parallel/test-crypto-raw-key-type-validation.js new file mode 100644 index 00000000000..c4ed8f10f52 --- /dev/null +++ b/test/parallel/test-crypto-raw-key-type-validation.js @@ -0,0 +1,81 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { createPrivateKey, createPublicKey } = require('node:crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); + +const key = Buffer.alloc(32); +const formats = ['raw-public', 'raw-private', 'raw-seed']; + +// Recognized classical types reject raw encodings with a format error. +for (const asymmetricKeyType of ['rsa', 'rsa-pss', 'dsa', 'dh']) { + for (const format of formats) { + assert.throws(() => createPublicKey({ key, format, asymmetricKeyType }), { + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + }); + } +} + +// Only exact public names are accepted, excluding aliases and unexposed types. +for (const asymmetricKeyType of [ + 'RSA', 'RSA-PSS', 'DSA', 'DH', 'EC', 'sm2', 'SM2', 'unknown', + 'Ed25519', 'ED25519', 'Ed448', 'ED448', 'X25519', 'X448', + 'ML-DSA-44', 'Ml-Dsa-65', 'ML-DSA-87', + 'ML-KEM-512', 'Ml-Kem-768', 'ML-KEM-1024', 'SLH-DSA-SHA2-128s', + 'rsaEncryption', 'id-ecPublicKey', '1.3.101.112', '1.3.101.110', + 'ed25519 ', ' ml-dsa-44', + 'rsa\0suffix', 'ec\0suffix', 'ed25519\0suffix', 'ml-dsa-44\0suffix', +]) { + for (const format of formats) { + assert.throws(() => createPublicKey({ + key, format, asymmetricKeyType, namedCurve: 'P-256', + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: `Invalid asymmetricKeyType: ${asymmetricKeyType.split('\0')[0]}`, + }); + } +} + +assert.throws(() => createPublicKey({ + key, format: 'raw-seed', asymmetricKeyType: 'ec', namedCurve: 'P-256', +}), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + +// Recognized OKP and PQC names retain their raw-format restrictions. +for (const asymmetricKeyType of ['ed25519', 'x25519']) { + const options = { key, format: 'raw-private', asymmetricKeyType }; + if (asymmetricKeyType === 'x25519' && hasFIPS(3, 5)) { + assert.throws(() => createPrivateKey(options), { + code: 'ERR_INVALID_ARG_VALUE', message: 'Invalid key data', + }); + } else { + const imported = createPrivateKey(options); + assert.strictEqual(imported.asymmetricKeyType, asymmetricKeyType); + } + assert.throws(() => createPrivateKey({ + key, format: 'raw-seed', asymmetricKeyType, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); +} + +{ + const asymmetricKeyType = 'ml-dsa-44'; + if (hasOpenSSL(3, 5) || isBoringSSL) { + const imported = createPrivateKey({ + key, format: 'raw-seed', asymmetricKeyType, + }); + assert.strictEqual(imported.asymmetricKeyType, 'ml-dsa-44'); + assert.throws(() => createPrivateKey({ + key, format: 'raw-private', asymmetricKeyType, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } else { + for (const format of formats) { + assert.throws(() => createPublicKey({ key, format, asymmetricKeyType }), { + code: 'ERR_INVALID_ARG_VALUE', + message: 'Unsupported key type', + }); + } + } +} diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js index 35ad6730207..6e47d2a1865 100644 --- a/test/parallel/test-crypto-rsa-dsa.js +++ b/test/parallel/test-crypto-rsa-dsa.js @@ -9,7 +9,17 @@ const crypto = require('crypto'); const constants = crypto.constants; const fixtures = require('../common/fixtures'); -const { hasOpenSSL, hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, + isBoringSSL, +} = require('../common/crypto'); +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips30 = fips3 && !fips35; +const fips4 = hasFIPS(4); +const fipsDigestErrorCode = 'ERR_OSSL_DIGEST_NOT_ALLOWED'; +const wrongPassphrase = 'wrong-password'; // Test certificates const certPem = fixtures.readKey('rsa_cert.crt'); @@ -17,12 +27,14 @@ const keyPem = fixtures.readKey('rsa_private.pem'); const rsaKeySize = 2048; const rsaPubPem = fixtures.readKey('rsa_public.pem', 'ascii'); const rsaKeyPem = fixtures.readKey('rsa_private.pem', 'ascii'); -const rsaKeyPemEncrypted = fixtures.readKey('rsa_private_encrypted.pem', - 'ascii'); +// Fixed ciphertexts keep wrong passwords from occasionally producing valid +// padding and a decoder error instead of the expected bad decrypt. +const rsaKeyPemEncrypted = fixtures.readKey( + 'rsa_private_encrypted.pem', 'ascii'); const dsaPubPem = fixtures.readKey('dsa_public.pem', 'ascii'); const dsaKeyPem = fixtures.readKey('dsa_private.pem', 'ascii'); -const dsaKeyPemEncrypted = fixtures.readKey('dsa_private_encrypted.pem', - 'ascii'); +const dsaKeyPemEncrypted = fixtures.readKey( + 'dsa_private_encrypted.pem', 'ascii'); const rsaPkcs8KeyPem = fixtures.readKey('rsa_private_pkcs8.pem'); const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem'); @@ -37,20 +49,23 @@ const openssl1DecryptError = { library: 'digital envelope routines', }; -const decryptError = hasOpenSSL3 ? - { message: 'error:1C800064:Provider routines::bad decrypt' } : - process.features.openssl_is_boringssl ? { - message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', - code: 'ERR_OSSL_BAD_DECRYPT', - reason: 'BAD_DECRYPT', - function: 'OPENSSL_internal', - library: 'Cipher functions', - } : - openssl1DecryptError; - -const decryptPrivateKeyError = hasOpenSSL3 ? { +const decryptError = fips4 ? + { code: 'ERR_OSSL_BAD_DECRYPT' } : hasOpenSSL(3) ? + { message: 'error:1C800064:Provider routines::bad decrypt' } : + isBoringSSL ? { + message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', + code: 'ERR_OSSL_BAD_DECRYPT', + reason: 'BAD_DECRYPT', + function: 'OPENSSL_internal', + library: 'Cipher functions', + } : + openssl1DecryptError; + +const decryptPrivateKeyError = fips4 ? { + code: 'ERR_OSSL_BAD_DECRYPT', +} : hasOpenSSL(3) ? { message: 'error:1C800064:Provider routines::bad decrypt', -} : process.features.openssl_is_boringssl ? { +} : isBoringSSL ? { message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', } : openssl1DecryptError; @@ -156,7 +171,7 @@ function getBufferCopy(buf) { // Now with RSA_NO_PADDING. Plaintext needs to match key size. // OpenSSL 3.x has a rsa_check_padding that will cause an error if // RSA_NO_PADDING is used. - if (!hasOpenSSL3) { + if (!hasOpenSSL(3)) { { const plaintext = 'x'.repeat(rsaKeySize / 8); encryptedBuffer = crypto.privateEncrypt({ @@ -192,14 +207,14 @@ function getBufferCopy(buf) { assert.throws(() => { crypto.privateDecrypt({ key: rsaKeyPemEncrypted, - passphrase: 'wrong' + passphrase: wrongPassphrase }, bufferToEncrypt); }, decryptError); assert.throws(() => { crypto.publicEncrypt({ key: rsaKeyPemEncrypted, - passphrase: 'wrong' + passphrase: wrongPassphrase }, encryptedBuffer); }, decryptError); @@ -211,7 +226,7 @@ function getBufferCopy(buf) { assert.throws(() => { crypto.publicDecrypt({ key: rsaKeyPemEncrypted, - passphrase: Buffer.from('wrong') + passphrase: Buffer.from(wrongPassphrase) }, encryptedBuffer); }, decryptError); } @@ -336,7 +351,7 @@ function test_rsa(padding, encryptOaepHash, decryptOaepHash) { test_rsa('RSA_NO_PADDING'); test_rsa('RSA_PKCS1_OAEP_PADDING'); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { test_rsa('RSA_PKCS1_PADDING'); } else { common.printSkipMessage('Skipping unsupported RSA_PKCS1_PADDING test case'); @@ -349,8 +364,12 @@ test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha256'); test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha512', 'sha512'); assert.throws(() => { test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha512'); -}, { - code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR' +}, fips35 ? { + code: 'ERR_OSSL_EVP_PROVIDER_ASYM_CIPHER_FAILURE', +} : fips3 ? { + message: 'error:00000000:lib(0)::reason(0)', +} : { + code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR', }); // The following RSA-OAEP test cases were created using the WebCrypto API to @@ -416,8 +435,9 @@ for (const fn of [crypto.publicEncrypt, crypto.privateDecrypt]) { } // Test RSA key signing/verification -let rsaSign = crypto.createSign('SHA1'); -let rsaVerify = crypto.createVerify('SHA1'); +const rsaDigest = fips3 ? 'SHA256' : 'SHA1'; +let rsaSign = crypto.createSign(rsaDigest); +let rsaVerify = crypto.createVerify(rsaDigest); assert.ok(rsaSign); assert.ok(rsaVerify); @@ -428,36 +448,39 @@ const expectedSignature = fixtures.readKey( rsaSign.update(rsaPubPem); let rsaSignature = rsaSign.sign(rsaKeyPem, 'hex'); -assert.strictEqual(rsaSignature, expectedSignature); +if (!fips3) + assert.strictEqual(rsaSignature, expectedSignature); rsaVerify.update(rsaPubPem); assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); // Test RSA PKCS#8 key signing/verification -rsaSign = crypto.createSign('SHA1'); +rsaSign = crypto.createSign(rsaDigest); rsaSign.update(rsaPubPem); rsaSignature = rsaSign.sign(rsaPkcs8KeyPem, 'hex'); -assert.strictEqual(rsaSignature, expectedSignature); +if (!fips3) + assert.strictEqual(rsaSignature, expectedSignature); -rsaVerify = crypto.createVerify('SHA1'); +rsaVerify = crypto.createVerify(rsaDigest); rsaVerify.update(rsaPubPem); assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); // Test RSA key signing/verification with encrypted key -rsaSign = crypto.createSign('SHA1'); +rsaSign = crypto.createSign(rsaDigest); rsaSign.update(rsaPubPem); const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'password' }; rsaSignature = rsaSign.sign(signOptions, 'hex'); -assert.strictEqual(rsaSignature, expectedSignature); +if (!fips3) + assert.strictEqual(rsaSignature, expectedSignature); -rsaVerify = crypto.createVerify('SHA1'); +rsaVerify = crypto.createVerify(rsaDigest); rsaVerify.update(rsaPubPem); assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); -rsaSign = crypto.createSign('SHA1'); +rsaSign = crypto.createSign(rsaDigest); rsaSign.update(rsaPubPem); assert.throws(() => { - const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'wrong' }; + const signOptions = { key: rsaKeyPemEncrypted, passphrase: wrongPassphrase }; rsaSign.sign(signOptions, 'hex'); }, decryptPrivateKeyError); @@ -503,16 +526,17 @@ assert.throws(() => { // // Test DSA signing and verification // -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { const input = 'I AM THE WALRUS'; // DSA signatures vary across runs so there is no static string to verify // against. - const sign = crypto.createSign('SHA1'); + const dsaDigest = fips3 ? 'SHA256' : 'SHA1'; + const sign = crypto.createSign(dsaDigest); sign.update(input); const signature = sign.sign(dsaKeyPem, 'hex'); - const verify = crypto.createVerify('SHA1'); + const verify = crypto.createVerify(dsaDigest); verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); @@ -520,12 +544,18 @@ if (!process.features.openssl_is_boringssl) { // Test the legacy 'DSS1' name. const sign2 = crypto.createSign('DSS1'); sign2.update(input); - const signature2 = sign2.sign(dsaKeyPem, 'hex'); + if (fips30) { + assert.throws(() => sign2.sign(dsaKeyPem, 'hex'), { + code: fipsDigestErrorCode, + }); + } else { + const signature2 = sign2.sign(dsaKeyPem, 'hex'); - const verify2 = crypto.createVerify('DSS1'); - verify2.update(input); + const verify2 = crypto.createVerify('DSS1'); + verify2.update(input); - assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); + assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); + } } else { common.printSkipMessage('Skipping unsupported DSA test case'); } @@ -534,16 +564,17 @@ if (!process.features.openssl_is_boringssl) { // // Test DSA signing and verification with PKCS#8 private key // -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { const input = 'I AM THE WALRUS'; // DSA signatures vary across runs so there is no static string to verify // against. - const sign = crypto.createSign('SHA1'); + const dsaDigest = fips3 ? 'SHA256' : 'SHA1'; + const sign = crypto.createSign(dsaDigest); sign.update(input); const signature = sign.sign(dsaPkcs8KeyPem, 'hex'); - const verify = crypto.createVerify('SHA1'); + const verify = crypto.createVerify(dsaDigest); verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); @@ -558,22 +589,23 @@ if (!process.features.openssl_is_boringssl) { const input = 'I AM THE WALRUS'; { - const sign = crypto.createSign('SHA1'); + const sign = crypto.createSign(fips3 ? 'SHA256' : 'SHA1'); sign.update(input); assert.throws(() => { - sign.sign({ key: dsaKeyPemEncrypted, passphrase: 'wrong' }, 'hex'); + sign.sign({ key: dsaKeyPemEncrypted, passphrase: wrongPassphrase }, 'hex'); }, decryptPrivateKeyError); } -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { // DSA signatures vary across runs so there is no static string to verify // against. - const sign = crypto.createSign('SHA1'); + const dsaDigest = fips3 ? 'SHA256' : 'SHA1'; + const sign = crypto.createSign(dsaDigest); sign.update(input); const signOptions = { key: dsaKeyPemEncrypted, passphrase: 'password' }; const signature = sign.sign(signOptions, 'hex'); - const verify = crypto.createVerify('SHA1'); + const verify = crypto.createVerify(dsaDigest); verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); diff --git a/test/parallel/test-crypto-rsa-pkcs1-public-key.js b/test/parallel/test-crypto-rsa-pkcs1-public-key.js new file mode 100644 index 00000000000..ee37a17cce2 --- /dev/null +++ b/test/parallel/test-crypto-rsa-pkcs1-public-key.js @@ -0,0 +1,94 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { createPrivateKey, createPublicKey, sign, verify } = require('node:crypto'); +const fixtures = require('../common/fixtures'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); + +const privateKey = createPrivateKey(fixtures.readKey('rsa_private_2048.pem')); +const publicKey = createPublicKey(privateKey); +const der = publicKey.export({ format: 'der', type: 'pkcs1' }); +const pem = publicKey.export({ format: 'pem', type: 'pkcs1' }); +const data = Buffer.from('PKCS#1 public key import'); +const signature = sign('sha256', data, privateKey); + +for (const [key, format] of [ + [der, 'der'], + [Buffer.concat([der, Buffer.from('trailing data')]), 'der'], + [pem, 'pem'], + [`leading data\n${pem}trailing data\n`, 'pem'], +]) { + const imported = createPublicKey({ key, format, type: 'pkcs1' }); + assert.strictEqual(imported.type, 'public'); + assert.strictEqual(imported.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(imported.asymmetricKeyDetails, + publicKey.asymmetricKeyDetails); + assert.deepStrictEqual(imported.export({ format: 'der', type: 'pkcs1' }), der); + assert.strictEqual(imported.export({ format: 'pem', type: 'pkcs1' }), pem); + assert(verify('sha256', data, imported, signature)); +} + +// The public PKCS#1 decoder must reject truncated keys and other DER structures. +for (const invalid of [ + der.subarray(0, der.length - 1), + publicKey.export({ format: 'der', type: 'spki' }), +]) { + assert.throws(() => createPublicKey({ + key: invalid, format: 'der', type: 'pkcs1', + }), { name: 'Error' }); + assert.throws(() => createPublicKey( + `-----BEGIN RSA PUBLIC KEY-----\n${invalid.toString('base64')}\n` + + '-----END RSA PUBLIC KEY-----\n', + ), { name: 'Error' }); +} + +// Public-key creation continues to recognize PKCS#1 private keys separately. +const privateDer = privateKey.export({ format: 'der', type: 'pkcs1' }); +assert.deepStrictEqual(createPublicKey({ + key: privateDer, format: 'der', type: 'pkcs1', +}).export({ format: 'der', type: 'pkcs1' }), der); + +// Preserve the ASN.1 forms accepted by the legacy RSA BIGNUM decoder. These +// tiny keys exercise parsing only, without performing RSA operations. +if (!isBoringSSL) { + for (const hex of [ + '30800201110201030000', // Indefinite-length BER SEQUENCE. + '300702020011020103', // Redundant modulus padding. + '300702810111020103', // Non-minimal INTEGER length encoding. + '30800201110201030000ffff', // BER with trailing data. + ]) { + const imported = createPublicKey({ + key: Buffer.from(hex, 'hex'), format: 'der', type: 'pkcs1', + }); + assert.strictEqual(imported.type, 'public'); + assert.strictEqual(imported.asymmetricKeyType, 'rsa'); + } + + // OpenSSL 4 rejects empty INTEGERs in both legacy and provider decoders. + const emptyExponent = { + key: Buffer.from('30050201110200', 'hex'), format: 'der', type: 'pkcs1', + }; + if (hasOpenSSL(4)) { + assert.throws(() => createPublicKey(emptyExponent), { name: 'Error' }); + } else { + const imported = createPublicKey(emptyExponent); + assert.strictEqual(imported.type, 'public'); + assert.strictEqual(imported.asymmetricKeyType, 'rsa'); + } +} + +for (const hex of [ + '3080020111020103', // Missing BER end-of-contents marker. + '30800201110201030201010000', // Third INTEGER inside BER SEQUENCE. + '3006220111020103', // Constructed INTEGER. + '3006020111040103', // OCTET STRING in place of the exponent. + '3009020111020103020101', // Third INTEGER inside DER SEQUENCE. +]) { + assert.throws(() => createPublicKey({ + key: Buffer.from(hex, 'hex'), format: 'der', type: 'pkcs1', + }), { name: 'Error' }); +} diff --git a/test/parallel/test-crypto-rsa-pss-parameters.js b/test/parallel/test-crypto-rsa-pss-parameters.js new file mode 100644 index 00000000000..cda01e7a16d --- /dev/null +++ b/test/parallel/test-crypto-rsa-pss-parameters.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); +if (!hasOpenSSL(3) || isBoringSSL) + common.skip('requires OpenSSL 3 provider support'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { createPublicKey } = require('crypto'); + +const publicKey = createPublicKey( + fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem')); +const der = publicKey.export({ format: 'der', type: 'spki' }); +const saltOffset = der.indexOf(Buffer.from([0xa2, 3, 2, 1, 16])); +assert.notStrictEqual(saltOffset, -1); + +const publicDetails = { modulusLength: 2048, publicExponent: 65537n }; +const restrictedDetails = { + ...publicDetails, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256', + saltLength: 16, +}; + +function assertDetails(encoded, expected) { + const key = createPublicKey({ key: encoded, format: 'der', type: 'spki' }); + assert.strictEqual(key.asymmetricKeyType, 'rsa-pss'); + assert.deepStrictEqual(key.asymmetricKeyDetails, expected); +} + +assertDetails(der, restrictedDetails); + +for (const saltLength of [0, 32, 127, -1, -128]) { + const encoded = Buffer.from(der); + encoded.writeInt8(saltLength, saltOffset + 4); + assertDetails(encoded, saltLength < 0 ? publicDetails : { + ...restrictedDetails, + saltLength, + }); +} diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index 421ee4ce8f3..0f033e05d6d 100644 --- a/test/parallel/test-crypto-scrypt.js +++ b/test/parallel/test-crypto-scrypt.js @@ -5,6 +5,8 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); +const isFips = hasFIPS(3); if (typeof crypto.scrypt !== 'function' || typeof crypto.scryptSync !== 'function') common.skip('no scrypt support'); @@ -159,13 +161,20 @@ const badargs = [ }, ]; -for (const options of good) { - const { pass, salt, keylen, expected } = options; - const actual = crypto.scryptSync(pass, salt, keylen, options); - assert.strictEqual(actual.toString('hex'), expected); - crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => { +if (isFips) { + const expected = { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' }; + assert.throws(() => crypto.scryptSync('pass', 'salt', 1), expected); + assert.throws( + () => crypto.scrypt('pass', 'salt', 1, () => {}), expected); +} else { + for (const options of good) { + const { pass, salt, keylen, expected } = options; + const actual = crypto.scryptSync(pass, salt, keylen, options); assert.strictEqual(actual.toString('hex'), expected); - })); + crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => { + assert.strictEqual(actual.toString('hex'), expected); + })); + } } for (const options of bad) { @@ -191,8 +200,10 @@ for (const options of incompatibleOptions) { } for (const options of toobig) { - const expected = { - message: process.features.openssl_is_boringssl ? + const expected = isFips ? { + code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS', + } : { + message: isBoringSSL ? /Invalid scrypt params:.*(INVALID_PARAMETERS|MEMORY_LIMIT_EXCEEDED)/ : /Invalid scrypt params:.*memory limit exceeded/, code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS', @@ -203,7 +214,7 @@ for (const options of toobig) { expected); } -{ +if (!isFips) { const defaults = { N: 16384, p: 1, r: 8 }; const expected = crypto.scryptSync('pass', 'salt', 1, defaults); const actual = crypto.scryptSync('pass', 'salt', 1); @@ -229,10 +240,12 @@ for (const { args, expected } of badargs) { { // Values for maxmem that do not fit in 32 bits but that are still safe // integers should be allowed. - crypto.scrypt('', '', 4, { maxmem: 2 ** 52 }, - common.mustSucceed((actual) => { - assert.strictEqual(actual.toString('hex'), 'd72c87d0'); - })); + if (!isFips) { + crypto.scrypt('', '', 4, { maxmem: 2 ** 52 }, + common.mustSucceed((actual) => { + assert.strictEqual(actual.toString('hex'), 'd72c87d0'); + })); + } // Values that exceed Number.isSafeInteger should not be allowed. assert.throws(() => crypto.scryptSync('', '', 0, { maxmem: 2 ** 53 }), { @@ -240,7 +253,7 @@ for (const { args, expected } of badargs) { }); } -{ +if (!isFips) { // Regression test for https://github.com/nodejs/node/issues/28836. function testParameter(name, value) { @@ -299,5 +312,11 @@ for (const { args, expected } of badargs) { assert.deepStrictEqual(negResult, posResult); } - crypto.scrypt('', '', -0, common.mustCall()); + if (isFips) { + assert.throws( + () => crypto.scrypt('', '', -0, () => {}), + { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' }); + } else { + crypto.scrypt('', '', -0, common.mustCall()); + } } diff --git a/test/parallel/test-crypto-sec-level.js b/test/parallel/test-crypto-sec-level.js index f2c0e390062..ff32b29bb2b 100644 --- a/test/parallel/test-crypto-sec-level.js +++ b/test/parallel/test-crypto-sec-level.js @@ -2,6 +2,8 @@ 'use strict'; const common = require('../common'); + +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -15,7 +17,7 @@ const assert = require('assert'); // This test simply validates that we can get some value for the secLevel // when needed by tests. const secLevel = require('internal/crypto/util').getOpenSSLSecLevel(); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { assert.strictEqual(secLevel, 0); } else { assert.ok(secLevel >= 0 && secLevel <= 5); diff --git a/test/parallel/test-crypto-secure-heap.js b/test/parallel/test-crypto-secure-heap.js index 3845f49a474..9b09c2dadab 100644 --- a/test/parallel/test-crypto-secure-heap.js +++ b/test/parallel/test-crypto-secure-heap.js @@ -13,14 +13,19 @@ if (common.isASan) { common.skip('ASan does not play well with secure heap allocations'); } -if (process.features.openssl_is_boringssl) { +const { + isBoringSSL, + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); + +if (isBoringSSL) { common.skip('BoringSSL does not support secure heap'); } const assert = require('assert'); const { fork } = require('child_process'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); const { secureHeapUsed, createDiffieHellman, @@ -38,7 +43,8 @@ if (process.argv[2] === 'child') { assert.strictEqual(a.used, 0); { - const size = getFips() || hasOpenSSL3 ? 1024 : 256; + const size = hasFIPS(3) ? + 2048 : (getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); const dh1 = createDiffieHellman(size); const p1 = dh1.getPrime('buffer'); const dh2 = createDiffieHellman(p1, 'buffer'); @@ -60,6 +66,28 @@ if (process.argv[2] === 'child') { return; } +if (process.argv[2] === 'workers') { + // Eight Workers held alive at once. A 1 KiB secure heap has room for a + // few DRBGs only, so an isolate setup that drew its entropy through + // OpenSSL would fail for the later Workers and abort the process. + const { Worker } = require('worker_threads'); + const i32 = new Int32Array(new SharedArrayBuffer(4)); + let online = 0; + for (let i = 0; i < 8; i++) { + const worker = new Worker( + 'const { workerData } = require("worker_threads");' + + 'Atomics.wait(workerData.i32, 0, 0);', + { eval: true, workerData: { i32 } }); + worker.on('online', () => { + if (++online === 8) { + Atomics.store(i32, 0, 1); + Atomics.notify(i32, 0); + } + }); + } + return; +} + const child = fork( process.argv[1], ['child'], @@ -69,6 +97,19 @@ child.on('exit', common.mustCall((code) => { assert.strictEqual(code, 0); })); +// AIX keeps OpenSSL as V8's entropy source, so a Worker's isolate setup +// still draws on the secure heap there. +if (!common.isAIX) { + const child = fork( + process.argv[1], + ['workers'], + { execArgv: ['--secure-heap=1024', '--secure-heap-min=4'] }); + child.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0); + })); +} + { const child = fork(fixtures.path('a.js'), { execArgv: ['--secure-heap=3', '--secure-heap-min=3'], diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index 808c8d076a2..22a911f35e5 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -10,13 +10,26 @@ const crypto = require('crypto'); const fixtures = require('../common/fixtures'); const { hasOpenSSL, + hasFIPS, opensslCli, + isBoringSSL, } = require('../common/crypto'); // Test certificates const certPem = fixtures.readKey('rsa_cert.crt'); const keyPem = fixtures.readKey('rsa_private.pem'); const keySize = 2048; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips30 = fips3 && !fips35; +const fipsDigestErrorCode = 'ERR_OSSL_DIGEST_NOT_ALLOWED'; +const signingHash = fips3 ? 'SHA256' : 'SHA1'; + +if (fips30) { + assert.throws( + () => crypto.createSign('SHA1').update('Test123').sign(keyPem), + { code: fipsDigestErrorCode }); +} { const Sign = crypto.Sign; @@ -60,7 +73,7 @@ const keySize = 2048; Object.defineProperty(Object.prototype, 'opensslErrorStack', errorStack); assert.throws(() => { - crypto.createSign('SHA1') + crypto.createSign('SHA256') .update('Test123') .sign({ key: keyPem, @@ -68,7 +81,7 @@ const keySize = 2048; }); }, { message: hasOpenSSL(3) ? 'error:1C8000A5:Provider routines::illegal or unsupported padding mode' : - process.features.openssl_is_boringssl ? + isBoringSSL ? 'error:0600006d:public key routines:OPENSSL_internal:ILLEGAL_OR_UNSUPPORTED_PADDING_MODE' : 'bye, bye, error stack' }); @@ -99,15 +112,15 @@ assert.throws( // Test signing and verifying { - const s1 = crypto.createSign('SHA1') + const s1 = crypto.createSign(signingHash) .update('Test123') .sign(keyPem, 'base64'); - let s1stream = crypto.createSign('SHA1'); + let s1stream = crypto.createSign(signingHash); s1stream.end('Test123'); s1stream = s1stream.sign(keyPem, 'base64'); assert.strictEqual(s1, s1stream, `${s1} should equal ${s1stream}`); - const verified = crypto.createVerify('SHA1') + const verified = crypto.createVerify(signingHash) .update('Test') .update('123') .verify(certPem, s1, 'base64'); @@ -138,16 +151,16 @@ assert.throws( } { - const s3 = crypto.createSign('SHA1') + const s3 = crypto.createSign(signingHash) .update('Test123') .sign(keyPem, 'buffer'); - let verified = crypto.createVerify('SHA1') + let verified = crypto.createVerify(signingHash) .update('Test') .update('123') .verify(certPem, s3); assert.strictEqual(verified, true); - const verStream = crypto.createVerify('SHA1'); + const verStream = crypto.createVerify(signingHash); verStream.write('Tes'); verStream.write('t12'); verStream.end('3'); @@ -190,6 +203,17 @@ assert.throws( const data = Buffer.from('Test123'); + if (fips30) { + const streamOptions = { + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST, + }; + assert.throws( + () => crypto.createSign(algo).update(data).sign(streamOptions), + { code: fipsDigestErrorCode }); + } + signSaltLengths.forEach((signSaltLength) => { if (signSaltLength > max) { // If the salt length is too big, an Error should be thrown @@ -211,20 +235,23 @@ assert.throws( }, errMessage); } else { // Otherwise, a valid signature should be generated - const s4 = crypto.createSign(algo) - .update(data) - .sign({ - key: keyPem, - padding: crypto.constants.RSA_PKCS1_PSS_PADDING, - saltLength: signSaltLength - }); const s4_2 = crypto.sign(algo, data, { key: keyPem, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: signSaltLength }); + const signatures = [s4_2]; + if (!fips30) { + signatures.unshift(crypto.createSign(algo) + .update(data) + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + })); + } - [s4, s4_2].forEach((sig) => { + signatures.forEach((sig) => { let verified; verifySaltLengths.forEach((verifySaltLength) => { // Verification should succeed if and only if the salt length is @@ -281,7 +308,8 @@ assert.throws( }); } - testPSS('SHA1', 20); + if (!fips30) + testPSS('SHA1', 20); testPSS('SHA256', 32); } @@ -340,7 +368,7 @@ assert.throws( }); assert.throws(() => { - crypto.createSign('SHA1') + crypto.createSign('SHA256') .update('Test123') .sign({ key: keyPem, @@ -349,7 +377,7 @@ assert.throws( }, hasOpenSSL(3) ? { code: 'ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE', message: /illegal or unsupported padding mode/, - } : process.features.openssl_is_boringssl ? { + } : isBoringSSL ? { code: 'ERR_OSSL_EVP_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE', message: /ILLEGAL_OR_UNSUPPORTED_PADDING_MODE/, } : { @@ -365,7 +393,7 @@ assert.throws( // Test throws exception when key options is null { assert.throws(() => { - crypto.createSign('SHA1').update('Test123').sign(null, 'base64'); + crypto.createSign('SHA256').update('Test123').sign(null, 'base64'); }, { code: 'ERR_CRYPTO_SIGN_KEY_REQUIRED', name: 'Error' @@ -373,8 +401,8 @@ assert.throws( } { - const sign = crypto.createSign('SHA1'); - const verify = crypto.createVerify('SHA1'); + const sign = crypto.createSign('SHA256'); + const verify = crypto.createVerify('SHA256'); [1, [], {}, undefined, null, true, Infinity].forEach((input) => { const errObj = { @@ -433,7 +461,7 @@ for (const pair of [ raw: true }, { private: fixtures.readKey('ed448_private.pem', 'ascii'), public: fixtures.readKey('ed448_public.pem', 'ascii'), - skip: process.features.openssl_is_boringssl, + skip: isBoringSSL, algo: null, supportsContext: hasOpenSSL(3, 2), sigLen: 114, @@ -441,7 +469,7 @@ for (const pair of [ { private: fixtures.readKey('rsa_private_2048.pem', 'ascii'), public: fixtures.readKey('rsa_public_2048.pem', 'ascii'), skip: false, - algo: 'sha1', + algo: signingHash, sigLen: 256, raw: false }, ]) { @@ -450,6 +478,7 @@ for (const pair of [ continue; } const algo = pair.algo; + const keyType = crypto.createPrivateKey(pair.private).asymmetricKeyType; { const data = Buffer.from('Hello world'); @@ -521,15 +550,34 @@ for (const pair of [ const sig = crypto.sign(algo, data, { key: pair.private, context }); assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), true); assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false); + const mismatchedContext = { key: pair.public, context: crypto.randomBytes(30) }; + if (fips35 && keyType === 'ed25519') { + assert.throws(() => crypto.verify(algo, data, mismatchedContext, sig), { + code: 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION', + }); + } else { + assert.strictEqual( + crypto.verify(algo, data, mismatchedContext, sig), false); + } } { const context = new Uint8Array(32); - const sig = crypto.sign(algo, data, { key: pair.private, context }); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), false); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false); + if (fips35 && keyType === 'ed25519') { + assert.throws( + () => crypto.sign(algo, data, { key: pair.private, context }), + { code: 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION' }); + } else { + const sig = crypto.sign(algo, data, { key: pair.private, context }); + assert.strictEqual( + crypto.verify(algo, data, { key: pair.public }, sig), false); + assert.strictEqual( + crypto.verify(algo, data, { key: pair.public, context }, sig), true); + assert.strictEqual(crypto.verify(algo, data, { + key: pair.public, + context: crypto.randomBytes(30), + }, sig), false); + } } assert.throws(() => crypto.sign(algo, data, { key: pair.private, context: new Uint8Array(256) }), { @@ -572,20 +620,27 @@ if (hasOpenSSL(3, 2)) { { const context = Buffer.from('my context'); - const sig = crypto.sign(null, data, { key: privKey, context }); - assert.strictEqual(sig.length, 64); + if (fips35) { + assert.throws(() => crypto.sign(null, data, { key: privKey, context }), { + code: 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION', + }); + } else { + const sig = crypto.sign(null, data, { key: privKey, context }); + assert.strictEqual(sig.length, 64); - // Verify with matching context succeeds - assert.strictEqual(crypto.verify(null, data, { key: pubKey, context }, sig), true); + // Verify with matching context succeeds + assert.strictEqual( + crypto.verify(null, data, { key: pubKey, context }, sig), true); - // Verify without context fails (Ed25519ctx !== Ed25519 pure) - assert.strictEqual(crypto.verify(null, data, { key: pubKey }, sig), false); + // Verify without context fails (Ed25519ctx !== Ed25519 pure) + assert.strictEqual(crypto.verify(null, data, { key: pubKey }, sig), false); - // Verify with wrong context fails - assert.strictEqual(crypto.verify(null, data, { - key: pubKey, - context: Buffer.from('wrong'), - }, sig), false); + // Verify with wrong context fails + assert.strictEqual(crypto.verify(null, data, { + key: pubKey, + context: Buffer.from('wrong'), + }, sig), false); + } } { @@ -662,36 +717,39 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa { const data = Buffer.from('Hello world'); - const keys = [['ec-key.pem', 64], ['dsa_private_1025.pem', 40]]; + const dsaKey = fips3 ? + ['dsa_private.pem', 64] : ['dsa_private_1025.pem', 40]; + const keys = [['ec-key.pem', 64], dsaKey]; for (const [file, length] of keys) { - if (process.features.openssl_is_boringssl && file.startsWith('dsa_')) { + if (isBoringSSL && file.startsWith('dsa_')) { common.printSkipMessage(`Skipping unsupported ${file} test case`); continue; } const privKey = fixtures.readKey(file); + const digest = fips3 ? 'sha256' : 'sha1'; [ - crypto.createSign('sha1').update(data).sign(privKey), - crypto.sign('sha1', data, privKey), - crypto.sign('sha1', data, { key: privKey, dsaEncoding: 'der' }), + crypto.createSign(digest).update(data).sign(privKey), + crypto.sign(digest, data, privKey), + crypto.sign(digest, data, { key: privKey, dsaEncoding: 'der' }), ].forEach((sig) => { // Signature length variability due to DER encoding assert(sig.length >= length + 4 && sig.length <= length + 8); assert.strictEqual( - crypto.createVerify('sha1').update(data).verify(privKey, sig), + crypto.createVerify(digest).update(data).verify(privKey, sig), true ); - assert.strictEqual(crypto.verify('sha1', data, privKey, sig), true); + assert.strictEqual(crypto.verify(digest, data, privKey, sig), true); }); // Test (EC)DSA signature conversion. const opts = { key: privKey, dsaEncoding: 'ieee-p1363' }; - let sig = crypto.sign('sha1', data, opts); + let sig = crypto.sign(digest, data, opts); // Unlike DER signatures, IEEE P1363 signatures have a predictable length. assert.strictEqual(sig.length, length); - assert.strictEqual(crypto.verify('sha1', data, opts, sig), true); - assert.strictEqual(crypto.createVerify('sha1') + assert.strictEqual(crypto.verify(digest, data, opts, sig), true); + assert.strictEqual(crypto.createVerify(digest) .update(data) .verify(opts, sig), true); @@ -700,7 +758,7 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa sig = crypto.randomBytes(length + i); let result; try { - result = crypto.verify('sha1', data, opts, sig); + result = crypto.verify(digest, data, opts, sig); } catch (err) { assert.match(err.message, /asn1 encoding/); assert.strictEqual(err.library, 'asn1 encoding routines'); @@ -735,20 +793,20 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa } // Non-(EC)DSA keys should ignore the option. - const sig = crypto.sign('sha1', data, { + const sig = crypto.sign(signingHash, data, { key: keyPem, dsaEncoding: 'ieee-p1363' }); - assert.strictEqual(crypto.verify('sha1', data, certPem, sig), true); + assert.strictEqual(crypto.verify(signingHash, data, certPem, sig), true); assert.strictEqual( - crypto.verify('sha1', data, { + crypto.verify(signingHash, data, { key: certPem, dsaEncoding: 'ieee-p1363' }, sig), true ); assert.strictEqual( - crypto.verify('sha1', data, { + crypto.verify(signingHash, data, { key: certPem, dsaEncoding: 'der' }, sig), @@ -757,7 +815,7 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa for (const dsaEncoding of ['foo', null, {}, 5, true, NaN]) { assert.throws(() => { - crypto.sign('sha1', data, { + crypto.sign(signingHash, data, { key: certPem, dsaEncoding }); @@ -776,12 +834,13 @@ if (!opensslCli) { const privkey = fixtures.readKey('rsa_private_2048.pem'); const msg = 'Test123'; - const s5 = crypto.createSign('SHA256') - .update(msg) - .sign({ - key: privkey, - padding: crypto.constants.RSA_PKCS1_PSS_PADDING - }); + const options = { + key: privkey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + }; + const s5 = fips30 ? + crypto.sign('SHA256', Buffer.from(msg), options) : + crypto.createSign('SHA256').update(msg).sign(options); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); @@ -799,7 +858,7 @@ if (!opensslCli) { })); } -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { // Test RSA-PSS. { // This key pair does not restrict the message digest algorithm or salt @@ -811,8 +870,14 @@ if (!process.features.openssl_is_boringssl) { const privateKey = crypto.createPrivateKey(privatePem); for (const key of [privatePem, privateKey]) { - // Any algorithm should work. - for (const algo of ['sha1', 'sha256']) { + if (fips30) { + assert.throws(() => crypto.sign('sha1', 'foo', key), { + code: fipsDigestErrorCode, + }); + } + // Any algorithm should work unless SHA-1 signing is unavailable. + const algorithms = fips30 ? ['sha256'] : ['sha1', 'sha256']; + for (const algo of algorithms) { // Any salt length should work. for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { const signature = crypto.sign(algo, 'foo', { key, saltLength }); @@ -847,7 +912,9 @@ if (!process.features.openssl_is_boringssl) { // Signing with anything other than sha256 should fail. assert.throws(() => { crypto.sign('sha1', 'foo', key); - }, /digest not allowed/); + }, fips30 ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); // Signing with salt lengths less than 16 bytes should fail. for (const saltLength of [8, 10, 12]) { @@ -895,7 +962,9 @@ if (!process.features.openssl_is_boringssl) { for (const algo of ['sha1', 'sha256']) { assert.throws(() => { crypto.sign(algo, 'foo', key); - }, /digest not allowed/); + }, fips30 && algo === 'sha1' ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); } // sha512 should produce a valid signature. @@ -915,15 +984,24 @@ if (!process.features.openssl_is_boringssl) { // The sign function should not swallow OpenSSL errors. // Regression test for https://github.com/nodejs/node/issues/40794. { - assert.throws(() => { - const { privateKey } = crypto.generateKeyPairSync('rsa', { - modulusLength: 512 + if (fips3) { + assert.throws(() => crypto.generateKeyPairSync('rsa', { + modulusLength: 512, + }), { + name: 'Error', + message: 'error:020000AE:rsa routines::invalid modulus', }); - crypto.sign('sha512', 'message', privateKey); - }, { - code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY', - message: /digest too big for rsa key|DIGEST_TOO_BIG_FOR_RSA_KEY/ - }); + } else { + assert.throws(() => { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + crypto.sign('sha512', 'message', privateKey); + }, { + code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY', + message: /digest too big for rsa key|DIGEST_TOO_BIG_FOR_RSA_KEY/ + }); + } } { @@ -948,7 +1026,7 @@ if (!process.features.openssl_is_boringssl) { // Ed25519 and Ed448 must use the one-shot methods const keys = [{ privateKey: fixtures.readKey('ed25519_private.pem', 'ascii'), publicKey: fixtures.readKey('ed25519_public.pem', 'ascii') }]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { keys.push({ privateKey: fixtures.readKey('ed448_private.pem', 'ascii'), publicKey: fixtures.readKey('ed448_public.pem', 'ascii') }); } else { diff --git a/test/parallel/test-crypto-stream.js b/test/parallel/test-crypto-stream.js index ed0916b036a..9584e48f2cb 100644 --- a/test/parallel/test-crypto-stream.js +++ b/test/parallel/test-crypto-stream.js @@ -28,7 +28,7 @@ if (!common.hasCrypto) { const assert = require('assert'); const stream = require('stream'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); if (!crypto.getFips()) { // Small stream to buffer converter @@ -73,7 +73,7 @@ const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); const decipher = crypto.createDecipheriv('aes-128-cbc', badkey, iv); cipher.pipe(decipher) - .on('error', common.expectsError((hasOpenSSL3 || process.features.openssl_is_boringssl) ? { + .on('error', common.expectsError((hasOpenSSL(3) || isBoringSSL) ? { message: /bad[\s_]decrypt/i, library: /Provider routines|Cipher functions/, reason: /bad[\s_]decrypt/i, diff --git a/test/parallel/test-crypto-worker-thread.js b/test/parallel/test-crypto-worker-thread.js index d9030d5cfc1..42027f3f2b4 100644 --- a/test/parallel/test-crypto-worker-thread.js +++ b/test/parallel/test-crypto-worker-thread.js @@ -9,6 +9,7 @@ const { generateKeySync, generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; const assert = require('assert'); @@ -19,7 +20,7 @@ if (isMainThread) { (async () => { const secretKey = generateKeySync('aes', { length: 128 }); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024 + modulusLength: hasFIPS(3) ? 2048 : 1024 }); const cryptoKey = await subtle.generateKey( { name: 'AES-CBC', length: 128 }, false, ['encrypt']); diff --git a/test/parallel/test-crypto-x509.js b/test/parallel/test-crypto-x509.js index 353699cf911..c8f44372f97 100644 --- a/test/parallel/test-crypto-x509.js +++ b/test/parallel/test-crypto-x509.js @@ -19,7 +19,7 @@ const { const assert = require('assert'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const { readFileSync } = require('fs'); const cert = readFileSync(fixtures.path('keys', 'agent1-cert.pem')); @@ -28,8 +28,8 @@ const ca = readFileSync(fixtures.path('keys', 'ca1-cert.pem')); const privateKey = createPrivateKey(key); -if (!process.features.openssl_is_boringssl) { - const expectedPubkeys = hasOpenSSL3 ? [ +if (!isBoringSSL) { + const expectedPubkeys = hasOpenSSL(3) ? [ [ 'rsa_pss_cert_2048.pem', 292, @@ -88,7 +88,7 @@ emailAddress=ry@tinyclouds.org`; let infoAccessCheck = `OCSP - URI:http://ocsp.nodejs.org/ CA Issuers - URI:http://ca.nodejs.org/ca.cert`; -if (!hasOpenSSL3) +if (!hasOpenSSL(3)) infoAccessCheck += '\n'; const der = Buffer.from( @@ -156,7 +156,7 @@ const der = Buffer.from( assert.deepStrictEqual(x509.raw, der); - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { assert.deepStrictEqual(x509.validFromDate, new Date('2022-09-03T21:40:37Z')); assert.deepStrictEqual(x509.validToDate, new Date('2296-06-17T21:40:37Z')); } @@ -175,9 +175,21 @@ const der = Buffer.from( assert.strictEqual(x509.checkIP('127.0.0.1'), undefined); assert.strictEqual(x509.checkIP('::'), undefined); - assert.strictEqual(x509.checkHost('agent1'), 'agent1'); + // OpenSSL 4.1 no longer checks the subject DN by default. + // https://github.com/openssl/openssl/pull/31982 + for (const options of [undefined, { subject: 'default' }]) { + assert.strictEqual(x509.checkHost('agent1', options), + hasOpenSSL(4, 1) ? undefined : 'agent1'); + assert.strictEqual(x509.checkEmail('ry@tinyclouds.org', options), + hasOpenSSL(4, 1) ? undefined : 'ry@tinyclouds.org'); + } + assert.strictEqual(x509.checkHost('agent1', { subject: 'always' }), 'agent1'); + assert.strictEqual(x509.checkHost('agent1', { subject: 'never' }), undefined); assert.strictEqual(x509.checkHost('agent2'), undefined); - assert.strictEqual(x509.checkEmail('ry@tinyclouds.org'), 'ry@tinyclouds.org'); + assert.strictEqual(x509.checkEmail('ry@tinyclouds.org', { subject: 'always' }), + 'ry@tinyclouds.org'); + assert.strictEqual(x509.checkEmail('ry@tinyclouds.org', { subject: 'never' }), + undefined); assert.strictEqual(x509.checkEmail('sally@example.com'), undefined); assert.throws(() => x509.checkHost('agent\x001'), { code: 'ERR_INVALID_ARG_VALUE' @@ -399,10 +411,10 @@ tAt3hIKFD1bJt6c6WtMH2Su3syosWxmdmGk5ihslB00lvLpfj/wed8i3bkcB1doq UcXd/5qu2GhokrKU2cPttU+XAN2Om6a0 -----END CERTIFICATE-----`; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { const cert = new X509Certificate(certPem); assert.throws(() => cert.publicKey, { - message: hasOpenSSL3 ? /decode error/ : /wrong tag/, + message: hasOpenSSL(3) ? /decode error/ : /wrong tag/, name: 'Error' }); @@ -446,7 +458,7 @@ UidvpWWipVLZgK+oDks+bKTobcoXGW9oXobiIYqslXPy -----END CERTIFICATE-----`.trim(); const c1 = new X509Certificate(certPemUTCTime); - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { assert.deepStrictEqual(c1.validFromDate, new Date('1949-12-25T23:59:58Z')); assert.deepStrictEqual(c1.validToDate, new Date('1950-01-01T23:59:58Z')); } @@ -483,7 +495,7 @@ CWwQO8JZjJqFtqtuzy2n+gLCvqePgG/gmSqHOPm2ZbLW -----END CERTIFICATE-----`.trim(); const c2 = new X509Certificate(certPemGeneralizedTime); - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { assert.deepStrictEqual(c2.validFromDate, new Date('2049-12-26T00:00:01Z')); assert.deepStrictEqual(c2.validToDate, new Date('2050-01-02T00:00:01Z')); } diff --git a/test/parallel/test-crypto.js b/test/parallel/test-crypto.js index 46f4571b33d..22f3d3e5eb5 100644 --- a/test/parallel/test-crypto.js +++ b/test/parallel/test-crypto.js @@ -29,7 +29,8 @@ const assert = require('assert'); const crypto = require('crypto'); const tls = require('tls'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); +const isFips = hasFIPS(3); // Test Certificates const certPfx = fixtures.readKey('rsa_cert.pfx'); @@ -54,27 +55,35 @@ assert.throws(() => { }); // PFX tests -tls.createSecureContext({ pfx: certPfx, passphrase: 'sample' }); - -assert.throws(() => { - tls.createSecureContext({ pfx: certPfx }); -}, (err) => { - // Throws general Error, so there is no opensslErrorStack property. - return err instanceof Error && - err.name === 'Error' && - /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && - !('opensslErrorStack' in err); -}); +if (isFips) { + for (const passphrase of ['sample', undefined, 'test']) { + assert.throws( + () => tls.createSecureContext({ pfx: certPfx, passphrase }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + } +} else { + tls.createSecureContext({ pfx: certPfx, passphrase: 'sample' }); + + assert.throws(() => { + tls.createSecureContext({ pfx: certPfx }); + }, (err) => { + // Throws general Error, so there is no opensslErrorStack property. + return err instanceof Error && + err.name === 'Error' && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && + !('opensslErrorStack' in err); + }); -assert.throws(() => { - tls.createSecureContext({ pfx: certPfx, passphrase: 'test' }); -}, (err) => { - // Throws general Error, so there is no opensslErrorStack property. - return err instanceof Error && - err.name === 'Error' && - /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && - !('opensslErrorStack' in err); -}); + assert.throws(() => { + tls.createSecureContext({ pfx: certPfx, passphrase: 'test' }); + }, (err) => { + // Throws general Error, so there is no opensslErrorStack property. + return err instanceof Error && + err.name === 'Error' && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && + !('opensslErrorStack' in err); + }); +} assert.throws(() => { tls.createSecureContext({ pfx: 'sample', passphrase: 'test' }); @@ -145,7 +154,7 @@ assert(crypto.getHashes().includes('sha1')); assert(crypto.getHashes().includes('sha256')); assert(!crypto.getHashes().includes('SHA1')); assert(!crypto.getHashes().includes('SHA256')); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { assert(crypto.getHashes().includes('RSA-SHA1')); assert(!crypto.getHashes().includes('rsa-sha1')); } @@ -191,7 +200,7 @@ assert.throws( ); assert.throws( - () => crypto.createHmac('sha256', 'a secret').update('0', 'hex'), + () => crypto.createHmac('sha256', '0123456789abcdef').update('0', 'hex'), (error) => { assert.ok(!('opensslErrorStack' in error)); assert.throws(() => { throw error; }, encodingError); @@ -211,7 +220,11 @@ assert.throws(() => { ].join('\n'); crypto.createSign('SHA256').update('test').sign(priv); }, (err) => { - if (process.features.openssl_is_boringssl) { + if (isFips) { + assert.throws(() => { throw err; }, { + code: 'ERR_OSSL_INVALID_KEY_LENGTH', + }); + } else if (isBoringSSL) { // BoringSSL rejects the tiny RSA key while decoding it, before signing. assert.throws(() => { throw err; }, { name: 'Error', @@ -225,9 +238,9 @@ assert.throws(() => { assert(Array.isArray(err.opensslErrorStack)); assert(err.opensslErrorStack.length > 0); } else { - if (!hasOpenSSL3) + if (!hasOpenSSL(3)) assert.ok(!('opensslErrorStack' in err)); - assert.throws(() => { throw err; }, hasOpenSSL3 ? { + assert.throws(() => { throw err; }, hasOpenSSL(3) ? { name: 'Error', message: 'error:02000070:rsa routines::digest too big for rsa key', library: 'rsa routines', @@ -243,7 +256,7 @@ assert.throws(() => { return true; }); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { // The correct header inside `rsa_private_pkcs8_bad.pem` should have been // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- // instead of @@ -251,7 +264,7 @@ if (!hasOpenSSL3) { const sha1_privateKey = fixtures.readKey('rsa_private_pkcs8_bad.pem', 'ascii'); - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { // BoringSSL accepts the PKCS#8 payload despite the legacy PEM label. const signature = crypto.createSign('sha1').sign(sha1_privateKey); assert(Buffer.isBuffer(signature)); diff --git a/test/parallel/test-dns-channel-timeout.js b/test/parallel/test-dns-channel-timeout.js index 1e4dac54897..0c9c7c31cae 100644 --- a/test/parallel/test-dns-channel-timeout.js +++ b/test/parallel/test-dns-channel-timeout.js @@ -22,10 +22,11 @@ for (const ctor of [dns.Resolver, dns.promises.Resolver]) { for (const timeout of [-1, 0, 1]) new ctor({ timeout }); // OK } +// One attempt is enough to exercise the timeout without retry backoff. for (const timeout of [0, 1, 2]) { const server = dgram.createSocket('udp4'); server.bind(0, '127.0.0.1', common.mustCall(() => { - const resolver = new dns.Resolver({ timeout }); + const resolver = new dns.Resolver({ timeout, tries: 1 }); resolver.setServers([`127.0.0.1:${server.address().port}`]); resolver.resolve4('nodejs.org', common.mustCall((err) => { assert.throws(() => { throw err; }, { @@ -40,7 +41,7 @@ for (const timeout of [0, 1, 2]) { for (const timeout of [0, 1, 2]) { const server = dgram.createSocket('udp4'); server.bind(0, '127.0.0.1', common.mustCall(() => { - const resolver = new dns.promises.Resolver({ timeout }); + const resolver = new dns.promises.Resolver({ timeout, tries: 1 }); resolver.setServers([`127.0.0.1:${server.address().port}`]); resolver.resolve4('nodejs.org').catch(common.mustCall((err) => { assert.throws(() => { throw err; }, { diff --git a/test/parallel/test-filehandle-readablestream.js b/test/parallel/test-filehandle-readablestream.js index 46ad53a2001..1e2eb1c6b0e 100644 --- a/test/parallel/test-filehandle-readablestream.js +++ b/test/parallel/test-filehandle-readablestream.js @@ -114,6 +114,37 @@ const check = readFileSync(__filename, { encoding: 'utf8' }); await file.close(); })().then(common.mustCall()); +// Make sure 'byob' reader works with views into +// different parts of a single ArrayBuffer +(async () => { + const file = await open(__filename); + const dec = new TextDecoder(); + const readable = file.readableWebStream(); + const reader = readable.getReader({ mode: 'byob' }); + const size = (await file.stat()).size; + + let buff = new ArrayBuffer(size); + let offset = 0; + let result; + do { + result = await reader.read(new DataView(buff, offset, Math.min(100, buff.byteLength - offset))); + if (result.value !== undefined) { + buff = result.value.buffer; + offset += result.value.byteLength; + assert.ok(result.value.byteLength <= 100); + } + } while (!result.done && (offset < buff.byteLength)); + const data = dec.decode(new Uint8Array(buff)); + + assert.strictEqual(check, data); + + assert.throws(() => file.readableWebStream(), { + code: 'ERR_INVALID_STATE', + }); + + await file.close(); +})().then(common.mustCall()); + // Make sure a warning is logged if a non-'bytes' type is passed. (async () => { const file = await open(__filename); diff --git a/test/parallel/test-fs-cp-sync-dir-exists-error-on-exist.mjs b/test/parallel/test-fs-cp-sync-dir-exists-error-on-exist.mjs new file mode 100644 index 00000000000..bcc95476a9f --- /dev/null +++ b/test/parallel/test-fs-cp-sync-dir-exists-error-on-exist.mjs @@ -0,0 +1,32 @@ +// This tests that cpSync() throws if errorOnExist is true, force is false, +// and the destination directory already exists (even if contents don't +// conflict), matching the asynchronous cp() behavior. + +import '../common/index.mjs'; +import { nextdir } from '../common/fs.js'; +import assert from 'node:assert'; +import { cpSync, mkdirSync, writeFileSync } from 'node:fs'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); + +const src = nextdir(); +const dest = nextdir(); + +// Create source directory with a file +mkdirSync(src); +writeFileSync(`${src}/file.txt`, 'test'); + +// Create destination directory with a different (non-conflicting) file +mkdirSync(dest); +writeFileSync(`${dest}/other.txt`, 'existing'); + +// Should throw because dest directory already exists +assert.throws( + () => cpSync(src, dest, { + recursive: true, + errorOnExist: true, + force: false, + }), + { code: 'ERR_FS_CP_EEXIST' }, +); diff --git a/test/parallel/test-fs-promises-file-handle-read.js b/test/parallel/test-fs-promises-file-handle-read.js index 423f1778bf6..2974c9d4d57 100644 --- a/test/parallel/test-fs-promises-file-handle-read.js +++ b/test/parallel/test-fs-promises-file-handle-read.js @@ -112,6 +112,27 @@ async function validateReadLength(len) { } } +async function validateReadLengthCoercedFromString() { + // Align with fs.read / fs.readSync (`length |= 0`). A non-number length + // must not reach node::fs::Read (CHECK args[3]->IsInt32()). + const buf = Buffer.alloc(4); + const filePath = fixtures.path('x.txt'); + const fileHandle = await open(filePath, 'r'); + try { + const { bytesRead } = await fileHandle.read(buf, 0, '1', 0); + assert.strictEqual(bytesRead, 1); + const { bytesRead: bytesReadOptions } = await fileHandle.read({ + buffer: buf, + offset: 0, + length: '1', + position: 0, + }); + assert.strictEqual(bytesReadOptions, 1); + } finally { + await fileHandle.close(); + } +} + async function validateReadWithNoOptions(byte) { const buf = Buffer.alloc(byte); const filePath = fixtures.path('x.txt'); @@ -144,6 +165,7 @@ async function validateReadWithNoOptions(byte) { await validateReadWithPositionZero(); await validateReadLength(0); await validateReadLength(1); + await validateReadLengthCoercedFromString(); await validateReadWithNoOptions(0); await validateReadWithNoOptions(1); })().then(common.mustCall()); diff --git a/test/parallel/test-http-buffer-sanity.js b/test/parallel/test-http-buffer-sanity.js index a235f3793a4..e122976f0e3 100644 --- a/test/parallel/test-http-buffer-sanity.js +++ b/test/parallel/test-http-buffer-sanity.js @@ -55,6 +55,7 @@ const server = http.Server(common.mustCallAtLeast(function(req, res) { server.listen(0, common.mustCall(() => { const req = http.request({ + agent: false, port: server.address().port, method: 'POST', path: '/', diff --git a/test/parallel/test-http-byteswritten.js b/test/parallel/test-http-byteswritten.js index 003b7dfbd04..475176e6c97 100644 --- a/test/parallel/test-http-byteswritten.js +++ b/test/parallel/test-http-byteswritten.js @@ -51,5 +51,5 @@ const httpServer = http.createServer(common.mustCall(function(req, res) { })); httpServer.listen(0, function() { - http.get({ port: this.address().port }); + http.get({ port: this.address().port, agent: false }); }); diff --git a/test/parallel/test-http-catch-uncaughtexception.js b/test/parallel/test-http-catch-uncaughtexception.js index 1366b6e26ea..e9a78332925 100644 --- a/test/parallel/test-http-catch-uncaughtexception.js +++ b/test/parallel/test-http-catch-uncaughtexception.js @@ -14,7 +14,7 @@ const server = http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('bye'); }).listen(0, function() { - http.get({ port: this.address().port }, function(res) { + http.get({ port: this.address().port, agent: false }, function(res) { res.resume(); throw new Error('get did fail'); }).on('close', function() { diff --git a/test/parallel/test-http-client-abort-completed-keepalive.js b/test/parallel/test-http-client-abort-completed-keepalive.js new file mode 100644 index 00000000000..78d2f366bd7 --- /dev/null +++ b/test/parallel/test-http-client-abort-completed-keepalive.js @@ -0,0 +1,57 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +// Aborting a request whose exchange has already completed must not destroy the +// socket that is being released to the agent. socketErrorListener has been +// removed by responseKeepAlive() at that point, so the error would be emitted +// on a socket with no 'error' listener and crash the process. +// Refs: https://github.com/nodejs/node/issues/65938 + +const agent = new http.Agent({ keepAlive: true }); + +const server = http.createServer((req, res) => { + res.end('x'); +}); + +server.listen(0, '127.0.0.1', common.mustCall(() => { + const controller = new AbortController(); + + const req = http.get({ + port: server.address().port, + host: '127.0.0.1', + agent, + signal: controller.signal, + }, common.mustCall(async (res) => { + res.on('error', common.mustNotCall()); + + for await (const chunk of res) { + assert.strictEqual(chunk.length, 1); + assert.strictEqual(res.complete, true); + assert.strictEqual(req.writableFinished, true); + controller.abort(new Error('stop reading')); + break; + } + + // The socket must survive the abort and go back to the pool, and a + // subsequent request must be able to reuse it. + const res2 = await new Promise((resolve, reject) => { + const req2 = http.get({ + port: server.address().port, + host: '127.0.0.1', + agent, + }, resolve); + req2.on('error', reject); + }); + + let body = ''; + for await (const chunk of res2) body += chunk; + assert.strictEqual(body, 'x'); + + agent.destroy(); + server.close(); + })); + + req.on('error', common.mustNotCall()); +})); diff --git a/test/parallel/test-http-client-check-http-token.js b/test/parallel/test-http-client-check-http-token.js index ef2445ec66e..7ab9aa83d76 100644 --- a/test/parallel/test-http-client-check-http-token.js +++ b/test/parallel/test-http-client-check-http-token.js @@ -29,6 +29,6 @@ server.listen(0, common.mustCall(() => { }); expectedSuccesses.forEach((method) => { - http.request({ method, port: server.address().port }).end(); + http.request({ method, port: server.address().port, agent: false }).end(); }); })); diff --git a/test/parallel/test-http-client-encoding.js b/test/parallel/test-http-client-encoding.js index a4701cdbd0a..25349630728 100644 --- a/test/parallel/test-http-client-encoding.js +++ b/test/parallel/test-http-client-encoding.js @@ -29,6 +29,7 @@ const server = http.createServer((req, res) => { server.close(); }).listen(0, common.mustCall(() => { http.request({ + agent: false, port: server.address().port, encoding: 'utf8' }, common.mustCall((res) => { diff --git a/test/parallel/test-http-client-response-domain.js b/test/parallel/test-http-client-response-domain.js index 9975ca3f949..da3d3a09ff0 100644 --- a/test/parallel/test-http-client-response-domain.js +++ b/test/parallel/test-http-client-response-domain.js @@ -49,6 +49,7 @@ function test() { })); const req = http.get({ + agent: false, socketPath: common.PIPE, headers: { 'Content-Length': '1' }, method: 'POST', diff --git a/test/parallel/test-http-decoded-auth.js b/test/parallel/test-http-decoded-auth.js index 076c056253b..4f7847133f5 100644 --- a/test/parallel/test-http-decoded-auth.js +++ b/test/parallel/test-http-decoded-auth.js @@ -43,6 +43,6 @@ for (const testCase of testCases) { server.listen(0, function() { // make the request const url = new URL(`http://${testCase.username}:${testCase.password}@localhost:${this.address().port}`); - http.request(url).end(); + http.request(url, { agent: false }).end(); }); } diff --git a/test/parallel/test-http-default-port.js b/test/parallel/test-http-default-port.js index 2005487502f..874affcdf23 100644 --- a/test/parallel/test-http-default-port.js +++ b/test/parallel/test-http-default-port.js @@ -44,7 +44,6 @@ for (const { mod, createServer } of [ assert.strictEqual(req.headers['x-port'], `${server.address().port}`); res.writeHead(200); res.end('ok'); - server.close(); })).listen(0, common.mustCall(() => { mod.globalAgent.defaultPort = server.address().port; mod.get({ @@ -54,6 +53,7 @@ for (const { mod, createServer } of [ 'x-port': server.address().port } }, common.mustCall((res) => { + res.on('end', common.mustCall(() => server.close())); res.resume(); })); })); diff --git a/test/parallel/test-http-dont-set-default-headers-with-setHost.js b/test/parallel/test-http-dont-set-default-headers-with-setHost.js index e2a4e39c24b..41805112785 100644 --- a/test/parallel/test-http-dont-set-default-headers-with-setHost.js +++ b/test/parallel/test-http-dont-set-default-headers-with-setHost.js @@ -14,6 +14,7 @@ const server = http.createServer(common.mustCall(function(req, res) { })); server.listen(0, common.localhostIPv4, function() { http.request({ + agent: false, method: 'POST', host: common.localhostIPv4, port: this.address().port, diff --git a/test/parallel/test-http-dont-set-default-headers.js b/test/parallel/test-http-dont-set-default-headers.js index 3f73c11e511..0b8e4c58f56 100644 --- a/test/parallel/test-http-dont-set-default-headers.js +++ b/test/parallel/test-http-dont-set-default-headers.js @@ -17,6 +17,7 @@ const server = http.createServer(common.mustCall(function(req, res) { })); server.listen(0, common.localhostIPv4, function() { http.request({ + agent: false, method: 'POST', host: common.localhostIPv4, port: this.address().port, diff --git a/test/parallel/test-http-early-hints-invalid-argument.js b/test/parallel/test-http-early-hints-invalid-argument.js index edf613614bc..b426ca3e840 100644 --- a/test/parallel/test-http-early-hints-invalid-argument.js +++ b/test/parallel/test-http-early-hints-invalid-argument.js @@ -38,7 +38,7 @@ const testResBody = 'response content\n'; server.listen(0, common.mustCall(() => { const req = http.request({ - port: server.address().port, path: '/' + port: server.address().port, path: '/', agent: false }); req.end(); @@ -79,7 +79,7 @@ const testResBody = 'response content\n'; server.listen(0, common.mustCall(() => { const req = http.request({ - port: server.address().port, path: '/' + port: server.address().port, path: '/', agent: false }); req.end(); diff --git a/test/parallel/test-http-flush-headers.js b/test/parallel/test-http-flush-headers.js index aaff80971c4..c155720ef9d 100644 --- a/test/parallel/test-http-flush-headers.js +++ b/test/parallel/test-http-flush-headers.js @@ -16,5 +16,6 @@ server.listen(0, '127.0.0.1', common.mustCall(function() { port: this.address().port, }); req.setHeader('foo', 'bar'); + req.setHeader('Connection', 'close'); req.flushHeaders(); })); diff --git a/test/parallel/test-http-head-request.js b/test/parallel/test-http-head-request.js index 26d490d357d..a9fcb2c166b 100644 --- a/test/parallel/test-http-head-request.js +++ b/test/parallel/test-http-head-request.js @@ -35,6 +35,7 @@ function test(headers) { server.listen(0, common.mustCall(function() { const request = http.request({ + agent: false, port: this.address().port, method: 'HEAD', path: '/' diff --git a/test/parallel/test-http-hex-write.js b/test/parallel/test-http-hex-write.js index a3cbec6b36c..4162811276d 100644 --- a/test/parallel/test-http-hex-write.js +++ b/test/parallel/test-http-hex-write.js @@ -34,7 +34,7 @@ http.createServer(function(q, s) { s.end(); this.close(); }).listen(0, common.mustCall(function() { - http.request({ port: this.address().port }) + http.request({ port: this.address().port, agent: false }) .on('response', common.mustCall(function(res) { let data = ''; diff --git a/test/parallel/test-http-insecure-parser.js b/test/parallel/test-http-insecure-parser.js index 5262c323087..8a7c1eae318 100644 --- a/test/parallel/test-http-insecure-parser.js +++ b/test/parallel/test-http-insecure-parser.js @@ -11,6 +11,11 @@ const server = http.createServer(common.mustCallAtLeast((req, res) => { req.pipe(res); })); +// The malformed request intentionally has no valid Connection header. +// So we have to set an explicitly shorter-than-default timeout. +server.keepAliveTimeout = common.platformTimeout(100); +server.keepAliveTimeoutBuffer = 0; + server.listen(0, common.mustCall(function() { const bufs = []; const client = net.connect( diff --git a/test/parallel/test-http-keep-alive-drop-requests.js b/test/parallel/test-http-keep-alive-drop-requests.js index a69497a0c1a..8b1260c3b35 100644 --- a/test/parallel/test-http-keep-alive-drop-requests.js +++ b/test/parallel/test-http-keep-alive-drop-requests.js @@ -24,12 +24,20 @@ server.on('dropRequest', common.mustCall((request, socket) => { server.listen(0, common.mustCall(() => { const socket = net.connect(server.address().port); + let response = ''; socket.on('connect', common.mustCall(() => { request(socket); request(socket); })); - socket.on('data', common.mustCallAtLeast()); - socket.on('close', common.mustCall()); + socket.on('data', common.mustCallAtLeast((chunk) => { + response += chunk; + if (response.includes('HTTP/1.1 503 Service Unavailable')) + socket.end(); + })); + socket.on('close', common.mustCall(() => { + assert.match(response, /HTTP\/1\.1 200 OK/); + assert.match(response, /HTTP\/1\.1 503 Service Unavailable/); + })); })); server.maxRequestsPerSocket = 1; diff --git a/test/parallel/test-http-outgoing-end-types.js b/test/parallel/test-http-outgoing-end-types.js index 20b443bff2c..48372a98e81 100644 --- a/test/parallel/test-http-outgoing-end-types.js +++ b/test/parallel/test-http-outgoing-end-types.js @@ -14,5 +14,5 @@ const httpServer = http.createServer(common.mustCall(function(req, res) { })); httpServer.listen(0, common.mustCall(function() { - http.get({ port: this.address().port }); + http.get({ port: this.address().port, agent: false }); })); diff --git a/test/parallel/test-http-outgoing-finish-writable.js b/test/parallel/test-http-outgoing-finish-writable.js index e3c870164ba..e0d9b73702c 100644 --- a/test/parallel/test-http-outgoing-finish-writable.js +++ b/test/parallel/test-http-outgoing-finish-writable.js @@ -25,6 +25,7 @@ server.listen(0); server.on('listening', common.mustCall(function() { const clientRequest = http.request({ + agent: false, port: server.address().port, method: 'GET', path: '/' diff --git a/test/parallel/test-http-outgoing-finish.js b/test/parallel/test-http-outgoing-finish.js index 0f71cccdf81..f2378d9e05b 100644 --- a/test/parallel/test-http-outgoing-finish.js +++ b/test/parallel/test-http-outgoing-finish.js @@ -33,6 +33,7 @@ http.createServer(function(req, res) { this.close(); }).listen(0, function() { const req = http.request({ + agent: false, port: this.address().port, method: 'PUT' }); diff --git a/test/parallel/test-http-outgoing-properties.js b/test/parallel/test-http-outgoing-properties.js index 85c5b659a36..a831765322b 100644 --- a/test/parallel/test-http-outgoing-properties.js +++ b/test/parallel/test-http-outgoing-properties.js @@ -36,6 +36,7 @@ const OutgoingMessage = http.OutgoingMessage; server.on('listening', common.mustCall(function() { const clientRequest = http.request({ + agent: false, port: server.address().port, method: 'GET', path: '/' @@ -62,6 +63,7 @@ const OutgoingMessage = http.OutgoingMessage; server.on('listening', common.mustCall(() => { const req = http.request({ + agent: false, port: server.address().port, method: 'GET', path: '/' diff --git a/test/parallel/test-http-outgoing-write-types.js b/test/parallel/test-http-outgoing-write-types.js index 6257b87eea8..0f2c686d5a7 100644 --- a/test/parallel/test-http-outgoing-write-types.js +++ b/test/parallel/test-http-outgoing-write-types.js @@ -20,5 +20,5 @@ const httpServer = http.createServer(common.mustCall(function(req, res) { })); httpServer.listen(0, common.mustCall(function() { - http.get({ port: this.address().port }); + http.get({ port: this.address().port, agent: false }); })); diff --git a/test/parallel/test-http-pipeline-assertionerror-finish.js b/test/parallel/test-http-pipeline-assertionerror-finish.js index 7721cb7fea3..60915dd6069 100644 --- a/test/parallel/test-http-pipeline-assertionerror-finish.js +++ b/test/parallel/test-http-pipeline-assertionerror-finish.js @@ -27,7 +27,8 @@ const server = http .listen(0, function() { const s = net.connect(this.address().port); - const big = 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'.repeat(COUNT); + const big = 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'.repeat(COUNT - 1) + + 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n'; s.write(big); s.resume(); diff --git a/test/parallel/test-http-req-res-close.js b/test/parallel/test-http-req-res-close.js index 8a0a9e5ab3f..34312731d26 100644 --- a/test/parallel/test-http-req-res-close.js +++ b/test/parallel/test-http-req-res-close.js @@ -40,7 +40,7 @@ const assert = require('assert'); })); server.listen(0, common.mustCall(() => { - http.get({ port: server.address().port }, common.mustCall()); + http.get({ port: server.address().port, agent: false }, common.mustCall()); })); } @@ -81,7 +81,7 @@ const assert = require('assert'); })); server.listen(0, common.mustCall(() => { - http.get({ port: server.address().port }, common.mustCall()); + http.get({ port: server.address().port, agent: false }, common.mustCall()); })); } @@ -126,6 +126,6 @@ const assert = require('assert'); })); server.listen(0, common.mustCall(() => { - http.get({ port: server.address().port }, common.mustCall()); + http.get({ port: server.address().port, agent: false }, common.mustCall()); })); } diff --git a/test/parallel/test-http-request-arguments.js b/test/parallel/test-http-request-arguments.js index 5cdd514fd50..b08da9bc525 100644 --- a/test/parallel/test-http-request-arguments.js +++ b/test/parallel/test-http-request-arguments.js @@ -18,7 +18,7 @@ const http = require('http'); common.mustCall(() => { http.get( 'http://example.com/testpath', - { hostname: 'localhost', port: server.address().port }, + { hostname: 'localhost', port: server.address().port, agent: false }, common.mustCall((res) => { res.resume(); }) diff --git a/test/parallel/test-http-request-large-payload.js b/test/parallel/test-http-request-large-payload.js index 3be100b7404..08fada1381f 100644 --- a/test/parallel/test-http-request-large-payload.js +++ b/test/parallel/test-http-request-large-payload.js @@ -16,6 +16,7 @@ const server = http.createServer(function(req, res) { server.listen(0, function() { const req = http.request({ + agent: false, method: 'POST', port: this.address().port }); diff --git a/test/parallel/test-http-server-connection-list-when-close-deferred.js b/test/parallel/test-http-server-connection-list-when-close-deferred.js new file mode 100644 index 00000000000..af87e356161 --- /dev/null +++ b/test/parallel/test-http-server-connection-list-when-close-deferred.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); + +// Keep this case in a separate process from the immediate-close case so +// their modified parsers cannot be reused across cases. + +function request(server) { + http.get({ + agent: false, + port: server.address().port, + path: '/', + }, (res) => { + res.resume(); + }); +} + +const server = http.createServer(common.mustCallAtLeast((req, res) => { + // See `freeParser` in _http_common.js + const { parser } = req.socket; + parser.free = common.mustCall(() => { + setImmediate(common.mustCall(() => { + parser.close(); + })); + }); + req.socket.on('close', common.mustCall(() => { + setImmediate(common.mustCall(() => { + server.close(); + })); + })); + res.end('ok'); +})).listen(0, common.mustCall(() => { + request(server); +})); diff --git a/test/parallel/test-http-server-connection-list-when-close.js b/test/parallel/test-http-server-connection-list-when-close.js index a530b710c49..305755b14eb 100644 --- a/test/parallel/test-http-server-connection-list-when-close.js +++ b/test/parallel/test-http-server-connection-list-when-close.js @@ -5,6 +5,7 @@ const http = require('http'); function request(server) { http.get({ + agent: false, port: server.address().port, path: '/', }, (res) => { @@ -12,36 +13,14 @@ function request(server) { }); } -{ - const server = http.createServer(common.mustCallAtLeast((req, res) => { - // Hack to not remove parser out of server.connectionList - // See `freeParser` in _http_common.js - req.socket.parser.free = common.mustCall(); - req.socket.on('close', common.mustCall(() => { - server.close(); - })); - res.end('ok'); - })).listen(0, common.mustCall(() => { - request(server); +const server = http.createServer(common.mustCallAtLeast((req, res) => { + // Hack to not remove parser out of server.connectionList + // See `freeParser` in _http_common.js + req.socket.parser.free = common.mustCall(); + req.socket.on('close', common.mustCall(() => { + server.close(); })); -} - -{ - const server = http.createServer(common.mustCallAtLeast((req, res) => { - // See `freeParser` in _http_common.js - const { parser } = req.socket; - parser.free = common.mustCall(() => { - setImmediate(common.mustCall(() => { - parser.close(); - })); - }); - req.socket.on('close', common.mustCall(() => { - setImmediate(common.mustCall(() => { - server.close(); - })); - })); - res.end('ok'); - })).listen(0, common.mustCall(() => { - request(server); - })); -} + res.end('ok'); +})).listen(0, common.mustCall(() => { + request(server); +})); diff --git a/test/parallel/test-http-server-delete-parser.js b/test/parallel/test-http-server-delete-parser.js index 4215ee2f9df..6b5a3e13f50 100644 --- a/test/parallel/test-http-server-delete-parser.js +++ b/test/parallel/test-http-server-delete-parser.js @@ -14,6 +14,7 @@ const server = http.createServer(common.mustCall((req, res) => { server.listen(0, '127.0.0.1', common.mustCall(() => { const req = http.request({ + agent: false, port: server.address().port, host: '127.0.0.1', method: 'GET', diff --git a/test/parallel/test-http-server-multiheaders.js b/test/parallel/test-http-server-multiheaders.js index fea84a8d4a7..e15dbd0fcae 100644 --- a/test/parallel/test-http-server-multiheaders.js +++ b/test/parallel/test-http-server-multiheaders.js @@ -48,6 +48,7 @@ const server = http.createServer(common.mustCall((req, res) => { server.listen(0, function() { http.get({ + agent: false, host: 'localhost', port: this.address().port, path: '/', diff --git a/test/parallel/test-http-server-multiheaders2.js b/test/parallel/test-http-server-multiheaders2.js index 0408afa1b13..85f2fb09f93 100644 --- a/test/parallel/test-http-server-multiheaders2.js +++ b/test/parallel/test-http-server-multiheaders2.js @@ -100,6 +100,7 @@ const headers = [] server.listen(0, function() { http.get({ + agent: false, host: 'localhost', port: this.address().port, path: '/', diff --git a/test/parallel/test-http-set-timeout-server.js b/test/parallel/test-http-set-timeout-server.js index 4a381ec92cd..28a941ef8c5 100644 --- a/test/parallel/test-http-set-timeout-server.js +++ b/test/parallel/test-http-set-timeout-server.js @@ -192,7 +192,6 @@ test(function fastTimeout(cb) { const server = http.createServer(common.mustCall((req, res) => { req.on('timeout', common.mustNotCall()); - res.end(); connectionHandlerInvoked = true; invokeCallbackIfDone(); })); diff --git a/test/parallel/test-http-url.parse-auth-with-header-in-request.js b/test/parallel/test-http-url.parse-auth-with-header-in-request.js index ea5793ee18a..e4834c32a64 100644 --- a/test/parallel/test-http-url.parse-auth-with-header-in-request.js +++ b/test/parallel/test-http-url.parse-auth-with-header-in-request.js @@ -41,6 +41,7 @@ const server = http.createServer(function(request, response) { server.listen(0, function() { const testURL = url.parse(`http://asdf:qwer@localhost:${this.address().port}`); + testURL.agent = false; // The test here is if you set a specific authorization header in the // request we should not override that with basic auth testURL.headers = { diff --git a/test/parallel/test-http-url.parse-auth.js b/test/parallel/test-http-url.parse-auth.js index 2bb53115864..287c27b9eb9 100644 --- a/test/parallel/test-http-url.parse-auth.js +++ b/test/parallel/test-http-url.parse-auth.js @@ -42,6 +42,7 @@ server.listen(0, function() { const port = this.address().port; // username = "user", password = "pass:" const testURL = url.parse(`http://user:pass%3A@localhost:${port}`); + testURL.agent = false; // make the request http.request(testURL).end(); diff --git a/test/parallel/test-http-url.parse-basic.js b/test/parallel/test-http-url.parse-basic.js index d0c23097717..223d1d7af25 100644 --- a/test/parallel/test-http-url.parse-basic.js +++ b/test/parallel/test-http-url.parse-basic.js @@ -43,7 +43,6 @@ const server = http.createServer(function(request, response) { check(request); response.writeHead(200, {}); response.end('ok'); - server.close(); }); server.listen(0, common.mustCall(function() { @@ -54,5 +53,9 @@ server.listen(0, common.mustCall(function() { // Since there is a little magic with the agent // make sure that an http request uses the http.Agent assert.ok(clientRequest.agent instanceof http.Agent); + clientRequest.on('response', common.mustCall((response) => { + response.on('end', common.mustCall(() => server.close())); + response.resume(); + })); clientRequest.end(); })); diff --git a/test/parallel/test-http-url.parse-https.request.js b/test/parallel/test-http-url.parse-https.request.js index ff819adc2b8..e20c3a0ec7b 100644 --- a/test/parallel/test-http-url.parse-https.request.js +++ b/test/parallel/test-http-url.parse-https.request.js @@ -45,7 +45,6 @@ const server = https.createServer(httpsOptions, function(request, response) { check(request); response.writeHead(200, {}); response.end('ok'); - server.close(); }); server.listen(0, common.mustCall(function() { @@ -57,5 +56,9 @@ server.listen(0, common.mustCall(function() { // Since there is a little magic with the agent // make sure that the request uses the https.Agent assert.ok(clientRequest.agent instanceof https.Agent); + clientRequest.on('response', common.mustCall((response) => { + response.on('end', common.mustCall(() => server.close())); + response.resume(); + })); clientRequest.end(); })); diff --git a/test/parallel/test-http-url.parse-path.js b/test/parallel/test-http-url.parse-path.js index 25e4838c4af..04fe12a4ff1 100644 --- a/test/parallel/test-http-url.parse-path.js +++ b/test/parallel/test-http-url.parse-path.js @@ -40,6 +40,7 @@ const server = http.createServer(function(request, response) { server.listen(0, function() { const testURL = url.parse(`http://localhost:${this.address().port}/asdf`); + testURL.agent = false; // make the request http.request(testURL).end(); diff --git a/test/parallel/test-http-url.parse-post.js b/test/parallel/test-http-url.parse-post.js index db5ee78fe6e..447a1b6a3fd 100644 --- a/test/parallel/test-http-url.parse-post.js +++ b/test/parallel/test-http-url.parse-post.js @@ -47,6 +47,7 @@ const server = http.createServer(function(request, response) { server.listen(0, function() { testURL = url.parse(`http://localhost:${this.address().port}/asdf?qwer=zxcv`); + testURL.agent = false; testURL.method = 'POST'; // make the request diff --git a/test/parallel/test-http-url.parse-search.js b/test/parallel/test-http-url.parse-search.js index 0759c779d3f..80f435a6789 100644 --- a/test/parallel/test-http-url.parse-search.js +++ b/test/parallel/test-http-url.parse-search.js @@ -41,6 +41,7 @@ const server = http.createServer(function(request, response) { server.listen(0, function() { const port = this.address().port; const testURL = url.parse(`http://localhost:${port}/asdf?qwer=zxcv`); + testURL.agent = false; // make the request http.request(testURL).end(); diff --git a/test/parallel/test-http-write-callbacks.js b/test/parallel/test-http-write-callbacks.js index 1f90e5135be..3b29f7c2f5d 100644 --- a/test/parallel/test-http-write-callbacks.js +++ b/test/parallel/test-http-write-callbacks.js @@ -71,6 +71,7 @@ server.on('checkContinue', common.mustCall((req, res) => { server.listen(0, common.mustCall(function() { const req = http.request({ + agent: false, port: this.address().port, method: 'PUT', headers: { 'expect': '100-continue' } diff --git a/test/parallel/test-http-write-empty-string.js b/test/parallel/test-http-write-empty-string.js index 88eff08f766..05e97a4865c 100644 --- a/test/parallel/test-http-write-empty-string.js +++ b/test/parallel/test-http-write-empty-string.js @@ -39,7 +39,7 @@ const server = http.createServer(function(request, response) { }); server.listen(0, common.mustCall(() => { - http.get({ port: server.address().port }, common.mustCall((res) => { + http.get({ port: server.address().port, agent: false }, common.mustCall((res) => { let response = ''; assert.strictEqual(res.statusCode, 200); diff --git a/test/parallel/test-http-zero-length-write.js b/test/parallel/test-http-zero-length-write.js index dfaa7b92fb7..92905fd9755 100644 --- a/test/parallel/test-http-zero-length-write.js +++ b/test/parallel/test-http-zero-length-write.js @@ -75,7 +75,7 @@ const server = http.createServer(common.mustCall((req, res) => { })); server.listen(0, common.mustCall(function() { - const req = http.request({ port: this.address().port, method: 'POST' }); + const req = http.request({ port: this.address().port, method: 'POST', agent: false }); let actual = ''; req.on('response', common.mustCall((res) => { res.setEncoding('utf8'); diff --git a/test/parallel/test-http2-close-while-writing.js b/test/parallel/test-http2-close-while-writing.js index c0a05c4a8da..f8395002580 100644 --- a/test/parallel/test-http2-close-while-writing.js +++ b/test/parallel/test-http2-close-while-writing.js @@ -7,6 +7,7 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +const assert = require('assert'); const http2 = require('http2'); const key = fixtures.readKey('agent8-key.pem', 'binary'); @@ -23,11 +24,24 @@ let client_stream; server.on('session', common.mustCall(function(session) { session.on('stream', common.mustCall(function(stream) { - stream.resume(); + // Client destroys mid-stream without END_STREAM (clean RST code). + // Peer reset before END_STREAM surfaces as ERR_HTTP2_STREAM_ABORTED. + stream.on('error', common.mustNotCall()); + + // Every write dispatched before close must have its callback invoked. + let writes = 0; + let writeCallbacks = 0; stream.on('data', function() { - this.write(Buffer.alloc(1)); + writes++; + this.write(Buffer.alloc(1), () => { + writeCallbacks++; + }); process.nextTick(() => client_stream.destroy()); }); + stream.on('close', common.mustCall(() => { + assert.strictEqual(writeCallbacks, writes); + })); + stream.resume(); })); })); diff --git a/test/parallel/test-http2-session-destroy-stream-handler.js b/test/parallel/test-http2-session-destroy-stream-handler.js new file mode 100644 index 00000000000..35c5b16471e --- /dev/null +++ b/test/parallel/test-http2-session-destroy-stream-handler.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); + +// Regression test for https://github.com/nodejs/node/issues/64850 +// +// Destroying the session from a 'stream' handler runs (via nextTick drained +// from MakeCallback) while nghttp2 is still inside mem_recv. Close is deferred +// for that window; later HEADERS/DATA in the same buffer must not abort with +// Assertion failed: onread->IsFunction(). + +const STREAMS = 8; +const BODY = Buffer.alloc(2048, 'a'); +const ROUNDS = 40; + +const server = http2.createServer({ + settings: { maxConcurrentStreams: 4 }, +}); + +server.on('session', (session) => session.on('error', () => {})); + +server.on('stream', (stream) => { + stream.on('error', () => {}); + stream.session.destroy(); +}); + +server.listen(0, '127.0.0.1', common.mustCall(() => { + const port = server.address().port; + const origin = `http://127.0.0.1:${port}`; + let remaining = ROUNDS; + + const round = () => { + if (remaining-- <= 0) { + server.close(); + return; + } + + const session = http2.connect(origin); + session.on('error', () => {}); + session.on('close', () => setImmediate(round)); + + session.on('connect', () => { + for (let i = 0; i < STREAMS; i++) { + const stream = session.request({ + ':path': `/${i}`, + ':method': 'POST', + }); + stream.on('error', () => {}); + stream.resume(); + stream.end(BODY); + } + }); + }; + + round(); +})); diff --git a/test/parallel/test-https-agent-additional-options.js b/test/parallel/test-https-agent-additional-options.js index 000cb9d3d0c..eca5f147a41 100644 --- a/test/parallel/test-https-agent-additional-options.js +++ b/test/parallel/test-https-agent-additional-options.js @@ -6,17 +6,20 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); const https = require('https'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); const options = { key: fixtures.readKey('agent1-key.pem'), cert: fixtures.readKey('agent1-cert.pem'), ca: fixtures.readKey('ca1-cert.pem'), - minVersion: 'TLSv1.1', + minVersion: fips3 ? 'TLSv1.2' : 'TLSv1.1', }; -if (!process.features.openssl_is_boringssl) { - options.ciphers = 'ALL@SECLEVEL=0'; +if (!isBoringSSL) { + options.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'ALL@SECLEVEL=0'; } const server = https.Server(options, (req, res) => { @@ -33,8 +36,9 @@ function getBaseOptions(port) { servername: 'agent1', }; - if (!process.features.openssl_is_boringssl) { - baseOptions.ciphers = 'ALL@SECLEVEL=0'; + if (!isBoringSSL) { + baseOptions.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'ALL@SECLEVEL=0'; } return baseOptions; @@ -44,10 +48,11 @@ const updatedValues = new Map([ ['dhparam', fixtures.readKey('dh2048.pem')], ['ecdhCurve', 'secp384r1'], ['honorCipherOrder', true], - ['minVersion', 'TLSv1.1'], + ['minVersion', fips3 ? 'TLSv1.2' : 'TLSv1.1'], ['maxVersion', 'TLSv1.3'], ['secureOptions', crypto.constants.SSL_OP_CIPHER_SERVER_PREFERENCE], - ['secureProtocol', 'TLSv1_1_method'], + ['secureProtocol', fips3 ? + 'TLSv1_2_method' : 'TLSv1_1_method'], ['sessionIdContext', 'sessionIdContext'], ]); diff --git a/test/parallel/test-https-agent-pfx-object-array-reuse.js b/test/parallel/test-https-agent-pfx-object-array-reuse.js index 95134855e97..758c32e5400 100644 --- a/test/parallel/test-https-agent-pfx-object-array-reuse.js +++ b/test/parallel/test-https-agent-pfx-object-array-reuse.js @@ -6,21 +6,57 @@ if (!common.hasCrypto) const assert = require('assert'); const https = require('https'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); + +const onRequest = (req, res) => { + res.end(req.socket.getPeerCertificate().subject.CN); +}; +const requestHandler = fips3 && !fips35 ? + common.mustNotCall() : + common.mustCall(onRequest, fips3 ? 1 : 2); const server = https.createServer({ key: fixtures.readKey('agent2-key.pem'), cert: fixtures.readKey('agent2-cert.pem'), requestCert: true, rejectUnauthorized: false, -}, common.mustCall((req, res) => { - res.end(req.socket.getPeerCertificate().subject.CN); -}, 2)); +}, requestHandler); server.listen(0, common.mustCall(async () => { const agent = new https.Agent({ keepAlive: true, maxSockets: 1 }); const port = server.address().port; + if (fips3) { + await assert.rejects(request({ + agent, + port, + pfx: [{ buf: fixtures.readKey('agent1.pfx'), passphrase: 'sample' }], + }, false), { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + + if (!fips35) { + agent.destroy(); + server.close(); + return; + } + + const result = await request({ + agent, + port, + pfx: [{ + buf: fixtures.readKey('agent1-fips.pfx'), + passphrase: 'password', + }], + }); + assert.strictEqual(result.body, 'agent1'); + assert.strictEqual(result.reusedSocket, false); + agent.destroy(); + server.close(); + return; + } + const first = await request({ agent, port, @@ -41,19 +77,20 @@ server.listen(0, common.mustCall(async () => { server.close(); })); -function request(options) { +function request(options, expectResponse = true) { return new Promise((resolve, reject) => { - const req = https.get({ - ...options, - rejectUnauthorized: false, - }, common.mustCall((res) => { + const onResponse = expectResponse ? common.mustCall((res) => { let body = ''; res.setEncoding('utf8'); res.on('data', (chunk) => body += chunk); res.on('end', common.mustCall(() => { resolve({ body, reusedSocket: req.reusedSocket }); })); - })); + }) : common.mustNotCall(); + const req = https.get({ + ...options, + rejectUnauthorized: false, + }, onResponse); req.on('error', reject); }); } diff --git a/test/parallel/test-https-agent-session-eviction.js b/test/parallel/test-https-agent-session-eviction.js index de2790e0d0a..c4edc5b33db 100644 --- a/test/parallel/test-https-agent-session-eviction.js +++ b/test/parallel/test-https-agent-session-eviction.js @@ -8,10 +8,11 @@ if (!common.hasCrypto) { } const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const https = require('https'); -const { SSL_OP_NO_TICKET } = require('crypto').constants; +const { constants: { SSL_OP_NO_TICKET } } = require('crypto'); +const fips3 = hasFIPS(3); const options = { key: fixtures.readKey('agent1-key.pem'), @@ -19,11 +20,17 @@ const options = { secureOptions: SSL_OP_NO_TICKET, }; -if (!process.features.openssl_is_boringssl) { - options.ciphers = 'RSA@SECLEVEL=0'; +if (fips3) { + options.minVersion = 'TLSv1.3'; + options.maxVersion = 'TLSv1.3'; } -// Create TLS1.2 server +if (!isBoringSSL) { + options.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'RSA@SECLEVEL=0'; +} + +// Create the initial server and cache a session from it. https.createServer(options, function(req, res) { res.writeHead(200, { 'Connection': 'close' }); res.end('ohai'); @@ -47,9 +54,14 @@ function first(server) { req.end(); } -// Create TLS1 server +// Create a server constrained to a different TLS version. function faultyServer(port) { - options.secureProtocol = 'TLSv1_method'; + if (fips3) { + options.minVersion = 'TLSv1.2'; + options.maxVersion = 'TLSv1.2'; + } else { + options.secureProtocol = 'TLSv1_method'; + } https.createServer(options, function(req, res) { res.writeHead(200, { 'Connection': 'close' }); res.end('hello faulty'); @@ -62,14 +74,15 @@ function faultyServer(port) { function second(server, session) { const req = https.request({ port: server.address().port, - ciphers: (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + ciphers: fips3 ? 'ECDHE-RSA-AES256-GCM-SHA384' : + (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), rejectUnauthorized: false }, function(res) { res.resume(); }); - // Although we have a TLS 1.2 session to offer to the TLS 1.0 server, - // connection to the TLS 1.0 server should work. + // Offering the cached session to a server using another TLS version should + // not prevent a fresh connection. req.on('response', common.mustCall(function(res) { // The test is now complete for OpenSSL 1.1.0. server.close(); diff --git a/test/parallel/test-https-agent-session-reuse.js b/test/parallel/test-https-agent-session-reuse.js index c5b7b78b8e0..da110e4aeb7 100644 --- a/test/parallel/test-https-agent-session-reuse.js +++ b/test/parallel/test-https-agent-session-reuse.js @@ -1,11 +1,13 @@ 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); const assert = require('assert'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); return; } diff --git a/test/parallel/test-https-client-renegotiation-limit.js b/test/parallel/test-https-client-renegotiation-limit.js index 729176b7c1a..ee83c21c876 100644 --- a/test/parallel/test-https-client-renegotiation-limit.js +++ b/test/parallel/test-https-client-renegotiation-limit.js @@ -20,12 +20,14 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) { common.skip('missing crypto'); } -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { require('../common/boringssl').testRenegotiationUnsupported(); return; } diff --git a/test/parallel/test-https-drain.js b/test/parallel/test-https-drain.js index 5d7bf973645..b9a5c3d3bda 100644 --- a/test/parallel/test-https-drain.js +++ b/test/parallel/test-https-drain.js @@ -45,6 +45,7 @@ const server = https.createServer(options, function(req, res) { server.listen(0, common.mustCall(function() { let resumed = false; const req = https.request({ + agent: false, method: 'POST', port: this.address().port, rejectUnauthorized: false diff --git a/test/parallel/test-https-keep-alive-drop-requests.js b/test/parallel/test-https-keep-alive-drop-requests.js index cf22a4a2811..7eb9aacd3b3 100644 --- a/test/parallel/test-https-keep-alive-drop-requests.js +++ b/test/parallel/test-https-keep-alive-drop-requests.js @@ -39,11 +39,19 @@ server.listen(0, common.mustCall(() => { rejectUnauthorized: false }, common.mustCall(() => { + let response = ''; request(socket); request(socket); socket.on('error', common.mustNotCall()); - socket.on('data', common.mustCallAtLeast()); - socket.on('close', common.mustCall()); + socket.on('data', common.mustCallAtLeast((chunk) => { + response += chunk; + if (response.includes('HTTP/1.1 503 Service Unavailable')) + socket.end(); + })); + socket.on('close', common.mustCall(() => { + assert.match(response, /HTTP\/1\.1 200 OK/); + assert.match(response, /HTTP\/1\.1 503 Service Unavailable/); + })); }) ); })); diff --git a/test/parallel/test-https-options-boolean-check.js b/test/parallel/test-https-options-boolean-check.js index fa02a165b80..776e966211c 100644 --- a/test/parallel/test-https-options-boolean-check.js +++ b/test/parallel/test-https-options-boolean-check.js @@ -1,6 +1,8 @@ 'use strict'; const common = require('../common'); + +const { isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); if (!common.hasCrypto) @@ -41,7 +43,7 @@ const certDataView = toDataView(certBuff); const caArrDataView = toDataView(caCert); function filterBoringSSLKeyCertArrayCases(options, setName) { - if (!process.features.openssl_is_boringssl) + if (!isBoringSSL) return options; // The array-valued cases exercise multi-identity key/cert handling. diff --git a/test/parallel/test-https-pfx.js b/test/parallel/test-https-pfx.js index 3c0aa82b3db..2be91887207 100644 --- a/test/parallel/test-https-pfx.js +++ b/test/parallel/test-https-pfx.js @@ -29,6 +29,8 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const https = require('https'); +const { hasFIPS } = require('../common/crypto'); +const fips3 = hasFIPS(3); const pfx = fixtures.readKey('rsa_cert.pfx'); @@ -42,10 +44,23 @@ const options = { rejectUnauthorized: false }; +if (fips3) { + assert.throws(() => https.createServer(options), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + + if (!hasFIPS(3, 5)) { + return; + } + + options.pfx = fixtures.readKey('agent1-fips.pfx'); + options.passphrase = 'password'; +} + const server = https.createServer(options, common.mustCallAtLeast((req, res) => { - assert.strictEqual(req.socket.authorized, false); // not a client cert - assert.strictEqual(req.socket.authorizationError, - 'DEPTH_ZERO_SELF_SIGNED_CERT'); + assert.strictEqual(req.socket.authorized, fips3); + assert.strictEqual(req.socket.authorizationError, fips3 ? + null : 'DEPTH_ZERO_SELF_SIGNED_CERT'); res.writeHead(200); res.end('OK'); })); diff --git a/test/parallel/test-https-request-arguments.js b/test/parallel/test-https-request-arguments.js index 9dc80094be0..e68f757be81 100644 --- a/test/parallel/test-https-request-arguments.js +++ b/test/parallel/test-https-request-arguments.js @@ -32,6 +32,7 @@ const options = { 'https://example.com/testpath', { + agent: false, hostname: 'localhost', port: server.address().port, rejectUnauthorized: false diff --git a/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js b/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js index 667be03d58c..ec1b8dda8ca 100644 --- a/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js +++ b/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js @@ -13,9 +13,10 @@ if (!common.hasCrypto) common.skip('missing crypto'); const crypto = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); // See #37990 for details on why this is problematic with FIPS. -if (process.config.variables.openssl_is_fips) +if (crypto.getFips() === 1 && !hasOpenSSL(3)) common.skip('Skipping as test uses non-fips compliant EC curve'); // This test will fail for OpenSSL < 1.1.1h diff --git a/test/parallel/test-https-set-timeout-server.js b/test/parallel/test-https-set-timeout-server.js index e1b833611a1..a7e188c8d96 100644 --- a/test/parallel/test-https-set-timeout-server.js +++ b/test/parallel/test-https-set-timeout-server.js @@ -216,7 +216,6 @@ test(function fastTimeout(cb) { const server = https.createServer(serverOptions, common.mustCall( (req, res) => { req.on('timeout', common.mustNotCall()); - res.end(); connectionHandlerInvoked = true; invokeCallbackIfDone(); } diff --git a/test/parallel/test-https-truncate.js b/test/parallel/test-https-truncate.js index beed36cd7c0..eaaedea1afc 100644 --- a/test/parallel/test-https-truncate.js +++ b/test/parallel/test-https-truncate.js @@ -47,7 +47,7 @@ function httpsTest() { }); server.listen(0, function() { - const opts = { port: this.address().port, rejectUnauthorized: false }; + const opts = { port: this.address().port, rejectUnauthorized: false, agent: false }; https.get(opts).on('response', function(res) { test(res); }); diff --git a/test/parallel/test-https-unix-socket-self-signed.js b/test/parallel/test-https-unix-socket-self-signed.js index 9db92ac2aed..5a3b76e1179 100644 --- a/test/parallel/test-https-unix-socket-self-signed.js +++ b/test/parallel/test-https-unix-socket-self-signed.js @@ -21,6 +21,7 @@ const server = https.createServer(options, common.mustCall((req, res) => { server.listen(common.PIPE, common.mustCall(() => { https.get({ + agent: false, socketPath: common.PIPE, rejectUnauthorized: false }); diff --git a/test/parallel/test-internal-webidl-buffer-source.js b/test/parallel/test-internal-webidl-buffer-source.js index 9e522d7d7b8..b3158bd7d34 100644 --- a/test/parallel/test-internal-webidl-buffer-source.js +++ b/test/parallel/test-internal-webidl-buffer-source.js @@ -1,7 +1,7 @@ -// Flags: --expose-internals +// Flags: --expose-internals --experimental-wasm-rab-integration 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const { test } = require('node:test'); const vm = require('vm'); @@ -272,6 +272,92 @@ test('AllowSharedBufferSource handles growable shared buffers with explicit ' + } }); +test('Shared buffer growability checks do not read JavaScript properties', () => { + for (const [buffer, growable] of [ + [new SharedArrayBuffer(8), false], + [new SharedArrayBuffer(8, { maxByteLength: 8 }), true], + [new SharedArrayBuffer(8, { maxByteLength: 16 }), true], + [vm.runInNewContext('new SharedArrayBuffer(8)'), false], + [vm.runInNewContext('new SharedArrayBuffer(8, { maxByteLength: 16 })'), true], + ]) { + const view = new Uint8Array(buffer); + const dataView = new DataView(buffer); + for (const mode of ['shadow', 'getter', 'prototype']) { + if (mode === 'shadow') { + Object.defineProperty(buffer, 'growable', { + value: !growable, + configurable: true, + }); + } else if (mode === 'getter') { + Object.defineProperty(buffer, 'growable', { + get: common.mustNotCall('Unexpected growable getter'), + configurable: true, + }); + } else { + delete buffer.growable; + Object.setPrototypeOf(buffer, null); + } + + for (const value of [buffer, view, dataView]) { + if (growable) { + assert.throws(() => converters.AllowSharedBufferSource(value), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.AllowSharedBufferSource(value), value); + } + assert.strictEqual(converters.AllowSharedBufferSource(value, { + allowResizable: true, + }), value); + } + + if (growable) { + assert.throws(() => converters.Uint8Array(view, { allowShared: true }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.Uint8Array(view, { allowShared: true }), view); + } + assert.strictEqual(converters.Uint8Array(view, { + allowShared: true, + allowResizable: true, + }), view); + } + } +}); + +test('Shared WebAssembly buffer growability is checked per buffer', { + skip: typeof WebAssembly === 'undefined', +}, () => { + const memory = new WebAssembly.Memory({ initial: 1, maximum: 2, shared: true }); + for (const [buffer, growable] of [ + [memory.buffer, false], + [memory.toResizableBuffer(), true], + [memory.toFixedLengthBuffer(), false], + ]) { + for (const value of [buffer, new Uint8Array(buffer), new DataView(buffer)]) { + if (growable) { + assert.throws(() => converters.AllowSharedBufferSource(value), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.AllowSharedBufferSource(value), value); + } + assert.strictEqual(converters.AllowSharedBufferSource(value, { + allowResizable: true, + }), value); + } + const view = new Uint8Array(buffer); + if (growable) { + assert.throws(() => converters.Uint8Array(view, { allowShared: true }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.Uint8Array(view, { allowShared: true }), view); + } + } +}); + test('BufferSource rejects objects with a forged @@toStringTag', () => { const fake = { [Symbol.toStringTag]: 'Uint8Array' }; assert.throws( diff --git a/test/parallel/test-internal-webidl.js b/test/parallel/test-internal-webidl.js index 5084d80efd9..f52896d5210 100644 --- a/test/parallel/test-internal-webidl.js +++ b/test/parallel/test-internal-webidl.js @@ -1,7 +1,7 @@ // Flags: --expose-internals 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const vm = require('vm'); const webidl = require('internal/webidl'); @@ -544,10 +544,16 @@ for (const [prototype, value] of [ } { - class Example {} + class Example { + #brand; + + static is(value) { + return typeof value === 'object' && value !== null && #brand in value; + } + } const converter = webidl.createInterfaceConverter( 'Example', - Example.prototype); + Example.is); const example = new Example(); assert.strictEqual(converter(example), example); @@ -556,6 +562,42 @@ for (const [prototype, value] of [ code: 'ERR_INVALID_ARG_TYPE', message: 'Prefix: Context is not of type Example.', }); + assertInvalidArgType(() => converter({ __proto__: Example.prototype })); + assertInvalidArgType(() => converter(new Proxy(example, {}))); + Object.setPrototypeOf(example, null); + assert.strictEqual(converter(example), example); +} + +{ + const signal = AbortSignal.abort('reason'); + for (const value of [ + Object.create(AbortSignal.prototype, { aborted: { value: false } }), + { __proto__: signal }, + Object.create(AbortSignal.prototype, Object.getOwnPropertyDescriptors(signal)), + new Proxy(signal, {}), + ]) { + assertInvalidArgType(() => converters.AbortSignal(value)); + assertInvalidArgType(() => AbortSignal.any([value])); + } + + Object.setPrototypeOf(signal, null); + assert.strictEqual(converters.AbortSignal(signal), signal); + const composite = AbortSignal.any([signal]); + assert.strictEqual(composite.aborted, true); + assert.strictEqual(composite.reason, 'reason'); +} + +{ + const controller = new AbortController(); + Object.defineProperties(controller.signal, { + aborted: { get: common.mustNotCall('Unexpected aborted getter') }, + reason: { get: common.mustNotCall('Unexpected reason getter') }, + }); + const composite = AbortSignal.any([controller.signal]); + assert.strictEqual(composite.aborted, false); + controller.abort('reason'); + assert.strictEqual(composite.aborted, true); + assert.strictEqual(composite.reason, 'reason'); } { diff --git a/test/parallel/test-mark-promise-handled.js b/test/parallel/test-mark-promise-handled.js new file mode 100644 index 00000000000..c2c789310bb --- /dev/null +++ b/test/parallel/test-mark-promise-handled.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { markPromiseAsHandled } = require('node:util'); + +process.on('unhandledRejection', common.mustNotCall()); + +markPromiseAsHandled(Promise.reject(123)); + +{ + const { promise, reject } = Promise.withResolvers(); + markPromiseAsHandled(promise); + reject(123); +} + +{ + const { promise, reject } = Promise.withResolvers(); + reject(123); + markPromiseAsHandled(promise); +} + +assert.throws(() => markPromiseAsHandled(123), { + code: 'ERR_INVALID_ARG_TYPE', +}); diff --git a/test/parallel/test-perf-hooks-histogram-qrde-oracle.js b/test/parallel/test-perf-hooks-histogram-qrde-oracle.js new file mode 100644 index 00000000000..2a53db3d769 --- /dev/null +++ b/test/parallel/test-perf-hooks-histogram-qrde-oracle.js @@ -0,0 +1,157 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { createHistogram } = require('perf_hooks'); + +const oracle = JSON.parse(fixtures.readSync('qrde-r-oracle.json', 'utf8')); + +function buildHistogram(entries, options = {}) { + const histogram = createHistogram(options); + for (const [value, count] of entries) { + if (count === 1) { + histogram.record(value); + continue; + } + + const block = createHistogram(options); + block.record(value); + let remaining = count; + while (remaining > 0) { + if (remaining % 2 === 1) histogram.add(block); + remaining = Math.floor(remaining / 2); + if (remaining > 0) block.add(block); + } + } + return histogram; +} + +function assertMatchesOracle(definition, result) { + const expected = oracle.cases[definition.name]; + const span = result.quantiles.at(-1) - result.quantiles[0]; + const tolerance = Math.max(1e-10, span * definition.relativeTolerance); + assert.strictEqual(result.bucketCount, definition.bucketCount); + const indices = expected.indices ?? expected.probabilities.map((_, i) => i); + if (expected.probabilities !== undefined) { + assert.deepStrictEqual(result.probabilities, + new Float64Array(expected.probabilities)); + } + for (let i = 0; i < indices.length; i++) { + const index = indices[i]; + const difference = Math.abs(result.quantiles[index] - + expected.quantiles[i]); + assert.ok(difference <= tolerance, + `${definition.name} p${result.probabilities[index]}: ` + + `${result.quantiles[index]} != ${expected.quantiles[i]} ` + + `(difference ${difference}, tolerance ${tolerance})`); + } +} + +const exactTolerance = 2e-12; +const asymptoticTolerance = 2e-8; +const cases = [ + { + name: 'small-none', + bins: 10, + dequantize: 'none', + entries: [[1, 1], [2, 2], [4, 5], [16, 3], [100, 1]], + bucketCount: 5, + relativeTolerance: exactTolerance, + }, + { + name: 'small-all', + bins: 10, + dequantize: 'all', + entries: [[1, 1], [2, 2], [4, 5], [16, 3], [100, 1]], + bucketCount: 5, + relativeTolerance: exactTolerance, + }, + { + name: 'exact-support', + bins: 100, + dequantize: 'none', + entries: Array.from({ length: 1024 }, (_, index) => [index + 1, 1]), + bucketCount: 1024, + relativeTolerance: exactTolerance, + }, + { + name: 'tail-focused', + probabilities: [0, 0.5, 0.9, 0.99, 0.999, 0.9999, 1], + dequantize: 'none', + entries: Array.from({ length: 1024 }, (_, index) => [index + 1, 1]), + bucketCount: 1024, + relativeTolerance: exactTolerance, + }, + { + name: 'approximation-threshold', + bins: 1000, + dequantize: 'none', + entries: [[1, 26239], [131071, 973761]], + options: { highest: 131071, figures: 5 }, + bucketCount: 2, + relativeTolerance: asymptoticTolerance, + }, + { + name: 'multimodal-none', + bins: 100, + dequantize: 'none', + entries: [[1, 900000], [10, 90000], [100, 9000], [1000, 1000]], + bucketCount: 4, + relativeTolerance: asymptoticTolerance, + }, + { + name: 'multimodal-all', + bins: 100, + dequantize: 'all', + entries: [[1, 900000], [10, 90000], [100, 9000], [1000, 1000]], + bucketCount: 4, + relativeTolerance: asymptoticTolerance, + }, + { + name: 'wide-none', + bins: 1000, + dequantize: 'none', + entries: [ + [2 ** 20, 700000], + [2 ** 40, 200000], + [2 ** 52, 99999], + [Number.MAX_SAFE_INTEGER, 1], + ], + bucketCount: 4, + relativeTolerance: asymptoticTolerance, + }, + { + name: 'wide-hdr', + bins: 1000, + dequantize: 'hdr', + entries: [ + [2 ** 20, 700000], + [2 ** 40, 200000], + [2 ** 52, 99999], + [Number.MAX_SAFE_INTEGER, 1], + ], + bucketCount: 4, + relativeTolerance: asymptoticTolerance, + }, + { + name: 'huge-count', + bins: 3, + dequantize: 'none', + entries: [[1, 2 ** 50], [1000, 2 ** 51]], + bucketCount: 2, + relativeTolerance: asymptoticTolerance, + }, +]; + +(async () => { + assert.strictEqual(oracle.implementation, 'stats::pbeta/stats::dbeta'); + for (const definition of cases) { + const histogram = buildHistogram(definition.entries, definition.options); + const options = { dequantize: definition.dequantize }; + if (definition.probabilities === undefined) options.bins = definition.bins; + else options.probabilities = definition.probabilities; + const result = await histogram.qrde(options); + assertMatchesOracle(definition, result); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-perf-hooks-histogram-qrde-worker.js b/test/parallel/test-perf-hooks-histogram-qrde-worker.js new file mode 100644 index 00000000000..ba836651665 --- /dev/null +++ b/test/parallel/test-perf-hooks-histogram-qrde-worker.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { once } = require('events'); +const { Worker } = require('worker_threads'); + +const worker = new Worker(` + const { parentPort } = require('worker_threads'); + const { createHistogram } = require('perf_hooks'); + + const histogram = createHistogram({ highest: 200000, figures: 5 }); + for (let i = 1; i <= 100000; i++) histogram.record(i); + histogram.qrde({ bins: 1000, dequantize: 'all' }); + parentPort.postMessage('scheduled'); +`, { eval: true }); + +(async () => { + assert.deepStrictEqual(await once(worker, 'message'), ['scheduled']); + assert.strictEqual(await worker.terminate(), 1); +})().then(common.mustCall()); diff --git a/test/parallel/test-perf-hooks-histogram-qrde.js b/test/parallel/test-perf-hooks-histogram-qrde.js new file mode 100644 index 00000000000..40eb1e71f1e --- /dev/null +++ b/test/parallel/test-perf-hooks-histogram-qrde.js @@ -0,0 +1,249 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { createHistogram } = require('perf_hooks'); + +function assertClose(actual, expected, tolerance = 1e-12) { + assert.ok(Math.abs(actual - expected) <= tolerance, + `${actual} != ${expected}`); +} + +function recordRepeated(histogram, options, value, count) { + const block = createHistogram(options); + block.record(value); + while (count > 0) { + if (count % 2 === 1) histogram.add(block); + count = Math.floor(count / 2); + if (count > 0) block.add(block); + } +} + +(async () => { + const empty = createHistogram(); + const emptyResult = await empty.qrde(); + assert.strictEqual(Object.getPrototypeOf(emptyResult), null); + const defaultProbabilities = new Float64Array(101); + for (let i = 0; i <= 100; i++) defaultProbabilities[i] = i / 100; + assert.deepStrictEqual(emptyResult.probabilities, defaultProbabilities); + assert.deepStrictEqual(emptyResult.quantiles, new Float64Array()); + assert.deepStrictEqual(emptyResult.densities, new Float64Array()); + assert.strictEqual(emptyResult.count, 0n); + assert.strictEqual(emptyResult.bucketCount, 0); + assert.strictEqual(emptyResult.corrections, 0); + assert.strictEqual(emptyResult.dequantize, 'hdr'); + + assert.throws(() => empty.qrde.call({}), { + code: 'ERR_INVALID_THIS', + }); + assert.throws(() => empty.qrde(null), { + code: 'ERR_INVALID_ARG_TYPE', + }); + assert.throws(() => empty.qrde({ bins: 0 }), { + code: 'ERR_OUT_OF_RANGE', + }); + assert.throws(() => empty.qrde({ bins: 1001 }), { + code: 'ERR_OUT_OF_RANGE', + }); + assert.throws(() => empty.qrde({ + bins: 10, + probabilities: [0, 1], + }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => empty.qrde({ probabilities: [0] }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => empty.qrde({ probabilities: [0.1, 1] }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => empty.qrde({ probabilities: [0, 0.9] }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => empty.qrde({ probabilities: [0, 0.5, 0.5, 1] }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => empty.qrde({ probabilities: [0, 1.1, 1] }), { + code: 'ERR_OUT_OF_RANGE', + }); + assert.throws(() => empty.qrde({ probabilities: new Array(1002) }), { + code: 'ERR_OUT_OF_RANGE', + }); + assert.throws(() => empty.qrde({ dequantize: true }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.throws(() => empty.qrde({ cache: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + + const sample = createHistogram(); + sample.record(3); + sample.record(4); + sample.record(7); + const pending = sample.qrde({ bins: 10, dequantize: 'none' }); + assert.ok(pending instanceof Promise); + const result = await pending; + assert.ok(result.quantiles instanceof Float64Array); + assert.ok(result.densities instanceof Float64Array); + const tenBinProbabilities = new Float64Array(11); + for (let i = 0; i <= 10; i++) tenBinProbabilities[i] = i / 10; + assert.deepStrictEqual(result.probabilities, tenBinProbabilities); + assert.strictEqual(result.quantiles.length, 11); + assert.strictEqual(result.densities.length, 10); + assert.strictEqual(result.count, 3n); + assert.strictEqual(result.bucketCount, 3); + assert.strictEqual(result.corrections, 0); + assert.strictEqual(result.dequantize, 'none'); + assertClose(result.quantiles[5], 122 / 27); + for (let i = 0; i < result.densities.length; i++) { + const width = result.quantiles[i + 1] - result.quantiles[i]; + assertClose(result.densities[i] * width, 0.1); + } + + const customInput = [0, 0.2, 0.5, 0.9, 1]; + const customPending = sample.qrde({ + probabilities: customInput, + dequantize: 'none', + }); + customInput[1] = 0.4; + const custom = await customPending; + assert.deepStrictEqual(custom.probabilities, + new Float64Array([0, 0.2, 0.5, 0.9, 1])); + assert.strictEqual(custom.quantiles.length, 5); + assert.strictEqual(custom.densities.length, 4); + assertClose(custom.quantiles[1], result.quantiles[2]); + assertClose(custom.quantiles[2], result.quantiles[5]); + assertClose(custom.quantiles[3], result.quantiles[9]); + for (let i = 0; i < custom.densities.length; i++) { + const width = custom.quantiles[i + 1] - custom.quantiles[i]; + const mass = custom.probabilities[i + 1] - custom.probabilities[i]; + assertClose(custom.densities[i] * width, mass); + } + + const pointMass = createHistogram(); + for (let i = 0; i < 100; i++) pointMass.record(100); + + const pointMassNone = + await pointMass.qrde({ bins: 10, dequantize: 'none' }); + const pointMassHdr = await pointMass.qrde({ bins: 10 }); + assert.deepStrictEqual(pointMassNone.quantiles, + new Float64Array(11).fill(100)); + assert.deepStrictEqual(pointMassHdr.quantiles, pointMassNone.quantiles); + assert.ok(pointMassNone.densities.every((value) => value === Infinity)); + assert.ok(pointMassHdr.densities.every((value) => value === Infinity)); + + const pointMassAll = + await pointMass.qrde({ bins: 10, dequantize: 'all' }); + for (let i = 0; i <= 10; i++) { + assertClose(pointMassAll.quantiles[i], 99.5 + i / 10); + } + for (const density of pointMassAll.densities) assertClose(density, 1); + assert.strictEqual(pointMassAll.corrections, 0); + + const wideBucket = createHistogram(); + for (let i = 0; i < 100; i++) wideBucket.record(100_000); + const [wideNone, wideHdr, wideAll] = await Promise.all([ + wideBucket.qrde({ bins: 10, dequantize: 'none' }), + wideBucket.qrde({ bins: 10, dequantize: 'hdr' }), + wideBucket.qrde({ bins: 10, dequantize: 'all' }), + ]); + assert.ok(wideNone.densities.every((value) => value === Infinity)); + assert.ok(wideHdr.densities.every(Number.isFinite)); + assert.deepStrictEqual(wideHdr.quantiles, wideAll.quantiles); + + const snapshot = createHistogram(); + snapshot.record(3); + snapshot.record(4); + snapshot.record(7); + const snapshotPending = + snapshot.qrde({ bins: 1000, dequantize: 'none' }); + snapshot.record(1000); + const snapshotResult = await snapshotPending; + assert.strictEqual(snapshotResult.count, 3n); + assert.strictEqual(snapshotResult.quantiles.at(-1), 7); + + const cachedSnapshot = createHistogram(); + cachedSnapshot.record(3); + cachedSnapshot.record(4); + cachedSnapshot.record(7); + const cachedPending = cachedSnapshot.qrde({ bins: 1000, cache: true }); + cachedSnapshot.record(1000); + const cachedResult = await cachedPending; + assert.strictEqual(cachedResult.count, 3n); + assert.strictEqual(cachedResult.quantiles.at(-1), 7); + const refreshedCache = await cachedSnapshot.qrde({ + probabilities: [0, 0.5, 1], + cache: true, + }); + assert.strictEqual(refreshedCache.count, 4n); + assert.strictEqual(refreshedCache.quantiles.at(-1), 1000); + const reusedCache = await cachedSnapshot.qrde({ bins: 4, cache: true }); + assert.strictEqual(reusedCache.count, 4n); + assert.strictEqual(reusedCache.quantiles.at(-1), 1000); + cachedSnapshot.reset(); + const invalidatedCache = await cachedSnapshot.qrde({ cache: true }); + assert.strictEqual(invalidatedCache.count, 0n); + + const arithmeticCache = createHistogram(); + arithmeticCache.record(1); + await arithmeticCache.qrde({ cache: true }); + const operand = createHistogram(); + operand.record(2); + arithmeticCache.add(operand); + assert.strictEqual( + (await arithmeticCache.qrde({ cache: true })).count, 2n); + arithmeticCache.subtract(operand); + assert.strictEqual( + (await arithmeticCache.qrde({ cache: true })).count, 1n); + arithmeticCache.recordCorrected(10, 5); + assert.strictEqual( + (await arithmeticCache.qrde({ cache: true })).count, 3n); + + const exactTails = createHistogram(); + for (let i = 1; i <= 1024; i++) exactTails.record(i); + const [exactNone, exactAll] = await Promise.all([ + exactTails.qrde({ bins: 100, dequantize: 'none' }), + exactTails.qrde({ bins: 100, dequantize: 'all' }), + ]); + assert.strictEqual(exactNone.bucketCount, 1024); + assertClose(exactNone.quantiles[1], 10.740000001653616, 1e-9); + assertClose(exactNone.quantiles[99], 1014.2599999983464, 1e-9); + assertClose(exactAll.quantiles[1], exactNone.quantiles[1]); + assertClose(exactAll.quantiles[99], exactNone.quantiles[99]); + + const tinyUpperTail = createHistogram({ highest: 32768, figures: 4 }); + for (let i = 1; i <= 31; i++) tinyUpperTail.record(i); + tinyUpperTail.record(16384); + const tinyUpperTailResult = + await tinyUpperTail.qrde({ bins: 2, dequantize: 'none' }); + assertClose(tinyUpperTailResult.quantiles[1], + 16.500000000000938, 2e-13); + + const largeCount = createHistogram(); + largeCount.record(1); + largeCount.record(3); + for (let i = 0; i < 52; i++) { + const copy = createHistogram(); + copy.add(largeCount); + largeCount.add(copy); + } + largeCount.record(1); + const largeCountResult = + await largeCount.qrde({ bins: 2, dequantize: 'none' }); + assert.strictEqual(largeCountResult.count, (1n << 53n) + 1n); + assertClose(largeCountResult.quantiles[1], 2); + + // Exercise correction across the exact-to-asymptotic beta CDF threshold. + const correctionOptions = { highest: 131071, figures: 5 }; + const correction = createHistogram(correctionOptions); + recordRepeated(correction, correctionOptions, 1, 26239); + recordRepeated(correction, correctionOptions, 131071, 973761); + const count = 1_000_000; + const threshold = (1 - Math.sqrt(1 - 100_000 / (count + 1))) / 2; + const corrected = await correction.qrde({ + probabilities: [0, threshold - 1e-10, threshold + 1e-10, 1], + dequantize: 'none', + }); + assert.strictEqual(corrected.corrections, 1); + assert.strictEqual(corrected.quantiles[1], corrected.quantiles[2]); +})().then(common.mustCall()); diff --git a/test/parallel/test-perf-hooks-histogram-stats.js b/test/parallel/test-perf-hooks-histogram-stats.js index 581579d1268..151b43e08c2 100644 --- a/test/parallel/test-perf-hooks-histogram-stats.js +++ b/test/parallel/test-perf-hooks-histogram-stats.js @@ -653,6 +653,60 @@ const { createHistogram, importHistogram } = require('perf_hooks'); 0x09, 0x18, 0x40, // 9 (countsLen) = 64 0x0a, 0x82, 0x18, 0x64, 0x01, // 10 (counts) = [100, 1] ])), { code: 'ERR_INVALID_ARG_VALUE' }); + + // --- Normalization offset validation --- + + // lowest=1, highest=100, figures=1 produces counts_len=64. An offset + // at or beyond counts_len leaves normalize_index() out of bounds after + // its single wrap adjustment and would corrupt memory on a later + // record(), so it must be rejected. + assert.throws(() => importHistogram(new Uint8Array([ + 0xa4, // map(4) + 0x02, 0x18, 0x64, // 2 (highest) = 100 + 0x03, 0x01, // 3 (figures) = 1 + 0x09, 0x18, 0x40, // 9 (countsLen) = 64 + 0x07, 0x18, 0x40, // 7 (normOffset) = 64 + ])), { code: 'ERR_INVALID_ARG_VALUE' }); + // Offset just beyond the accepted range. + assert.throws(() => importHistogram(new Uint8Array([ + 0xa4, // map(4) + 0x02, 0x18, 0x64, // 2 (highest) = 100 + 0x03, 0x01, // 3 (figures) = 1 + 0x09, 0x18, 0x40, // 9 (countsLen) = 64 + 0x07, 0x18, 0x65, // 7 (normOffset) = 101 + ])), { code: 'ERR_INVALID_ARG_VALUE' }); + // Offset that cannot be represented as int32_t (2**32). Without the + // representability check the static_cast would wrap to 0 and accept it. + assert.throws(() => importHistogram(new Uint8Array([ + 0xa4, // map(4) + 0x02, 0x18, 0x64, // 2 (highest) = 100 + 0x03, 0x01, // 3 (figures) = 1 + 0x09, 0x18, 0x40, // 9 (countsLen) = 64 + 0x07, 0x1b, 0x00, 0x00, 0x00, 0x01, // 7 (normOffset) = 2**32 + 0x00, 0x00, 0x00, 0x00, + ])), { code: 'ERR_INVALID_ARG_VALUE' }); + + // Valid offsets within [0, counts_len) are still accepted: 0 and the + // highest valid offset (counts_len - 1). + const offsetZero = importHistogram(new Uint8Array([ + 0xa4, // map(4) + 0x02, 0x18, 0x64, // 2 (highest) = 100 + 0x03, 0x01, // 3 (figures) = 1 + 0x09, 0x18, 0x40, // 9 (countsLen) = 64 + 0x07, 0x00, // 7 (normOffset) = 0 + ])); + assert.strictEqual(offsetZero.count, 0); + const offsetMax = importHistogram(new Uint8Array([ + 0xa4, // map(4) + 0x02, 0x18, 0x64, // 2 (highest) = 100 + 0x03, 0x01, // 3 (figures) = 1 + 0x09, 0x18, 0x40, // 9 (countsLen) = 64 + 0x07, 0x18, 0x3f, // 7 (normOffset) = 63 + ])); + assert.strictEqual(offsetMax.count, 0); + // The imported histogram with the max valid offset is recordable. + offsetMax.record(1); + assert.strictEqual(offsetMax.count, 1); } // --------------------------------------------------------------------------- diff --git a/test/parallel/test-perf-hooks-sliding-window-histogram-fast-calls.js b/test/parallel/test-perf-hooks-sliding-window-histogram-fast-calls.js new file mode 100644 index 00000000000..1097920f5f7 --- /dev/null +++ b/test/parallel/test-perf-hooks-sliding-window-histogram-fast-calls.js @@ -0,0 +1,31 @@ +// Flags: --expose-internals --no-warnings --allow-natives-syntax +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { internalBinding } = require('internal/test/binding'); +const { + createSlidingWindowHistogram, +} = require('perf_hooks'); + +const histogram = createSlidingWindowHistogram({ + chunks: 2, + recordsPerChunk: 1, +}); + +function record() { + histogram.record(1); +} + +eval('%PrepareFunctionForOptimization(histogram.record)'); +record(); +eval('%OptimizeFunctionOnNextCall(histogram.record)'); +record(); + +assert.strictEqual(histogram.snapshot().count, 2); + +if (common.isDebug) { + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual( + getV8FastApiCallCount('histogram.slidingWindow.record'), 1); +} diff --git a/test/parallel/test-perf-hooks-sliding-window-histogram.js b/test/parallel/test-perf-hooks-sliding-window-histogram.js new file mode 100644 index 00000000000..3ee1ca4ea43 --- /dev/null +++ b/test/parallel/test-perf-hooks-sliding-window-histogram.js @@ -0,0 +1,195 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { setTimeout: delay } = require('timers/promises'); +const { MessageChannel } = require('worker_threads'); +const { + createSlidingWindowHistogram, +} = require('perf_hooks'); + +{ + const histogram = createSlidingWindowHistogram({ + chunks: 3, + recordsPerChunk: 2, + highest: 100, + }); + + assert.strictEqual(histogram.constructor.name, 'SlidingWindowHistogram'); + assert.strictEqual(histogram.recordDelta, undefined); + assert.strictEqual(histogram.snapshot().count, 0); + + for (let value = 1; value <= 6; value++) histogram.record(value); + + const full = histogram.snapshot(); + assert.strictEqual(full.count, 6); + assert.strictEqual(full.min, 1); + assert.strictEqual(full.max, 6); + assert.strictEqual(full.record, undefined); + + histogram.record(7); + let current = histogram.snapshot(); + assert.strictEqual(current.count, 5); + assert.strictEqual(current.min, 3); + assert.strictEqual(current.max, 7); + + histogram.record(8); + histogram.record(9); + current = histogram.snapshot(); + assert.strictEqual(current.count, 5); + assert.strictEqual(current.min, 5); + assert.strictEqual(current.max, 9); + + // Materialized snapshots do not change with the sliding window. + assert.strictEqual(full.count, 6); + assert.strictEqual(full.min, 1); + assert.strictEqual(full.max, 6); + + histogram.reset(); + assert.strictEqual(histogram.snapshot().count, 0); + histogram.record(10n); + assert.strictEqual(histogram.snapshot().maxBigInt, 10n); + for (const value of [0n, 2n ** 63n]) { + assert.throws(() => histogram.record(value), { + code: 'ERR_OUT_OF_RANGE', + }); + } + + assert.throws(() => new histogram.constructor(), { + code: 'ERR_ILLEGAL_CONSTRUCTOR', + }); + assert.throws(() => histogram.record.call({}, 1), { + code: 'ERR_INVALID_THIS', + }); + assert.throws(() => histogram.snapshot.call({}), { + code: 'ERR_INVALID_THIS', + }); + assert.throws(() => histogram.reset.call({}), { + code: 'ERR_INVALID_THIS', + }); + assert.throws(() => structuredClone(histogram), { + name: 'DataCloneError', + }); + + const { port1, port2 } = new MessageChannel(); + assert.throws(() => port1.postMessage(histogram), { + name: 'DataCloneError', + }); + assert.throws(() => port1.postMessage(histogram, [histogram]), { + name: 'DataCloneError', + }); + port1.close(); + port2.close(); +} + +{ + const histogram = createSlidingWindowHistogram({ + chunks: 2, + recordsPerChunk: 1, + highest: 10, + }); + + // Out-of-range recording attempts count toward count-based rotation. + histogram.record(11); + histogram.record(1); + let current = histogram.snapshot(); + assert.strictEqual(current.count, 1); + assert.strictEqual(current.exceeds, 1); + + histogram.record(2); + current = histogram.snapshot(); + assert.strictEqual(current.count, 2); + assert.strictEqual(current.exceeds, 0); +} + +{ + for (const options of [ + undefined, + null, + {}, + { chunks: 2 }, + { chunks: 2, chunkDuration: 1, recordsPerChunk: 1 }, + ]) { + assert.throws(() => createSlidingWindowHistogram(options), { + code: options?.chunks === undefined ? + 'ERR_INVALID_ARG_TYPE' : 'ERR_INVALID_ARG_VALUE', + }); + } + + for (const chunks of [0, 1025, 1.5, '2']) { + assert.throws(() => createSlidingWindowHistogram({ + chunks, + recordsPerChunk: 1, + }), { + code: typeof chunks === 'number' ? + 'ERR_OUT_OF_RANGE' : 'ERR_INVALID_ARG_TYPE', + }); + } + + for (const chunkDuration of [0, 1.5, 18_446_744_073_710]) { + assert.throws(() => createSlidingWindowHistogram({ + chunks: 2, + chunkDuration, + }), { code: 'ERR_OUT_OF_RANGE' }); + } + + for (const recordsPerChunk of [0, 1.5, Number.MAX_SAFE_INTEGER + 1]) { + assert.throws(() => createSlidingWindowHistogram({ + chunks: 2, + recordsPerChunk, + }), { code: 'ERR_OUT_OF_RANGE' }); + } + + assert.throws(() => createSlidingWindowHistogram({ + chunks: 2, + recordsPerChunk: 1, + lowest: 10, + highest: 10, + }), { code: 'ERR_OUT_OF_RANGE' }); + + for (const [name, value] of [ + ['lowest', 0n], + ['lowest', 2n ** 63n], + ['highest', 0n], + ['highest', 2n ** 63n], + ]) { + assert.throws(() => createSlidingWindowHistogram({ + chunks: 2, + recordsPerChunk: 1, + [name]: value, + }), { code: 'ERR_OUT_OF_RANGE' }); + } + + for (const bounds of [ + { lowest: 1n }, + { lowest: 1n, highest: 100 }, + { lowest: 1, highest: 100n }, + ]) { + const histogram = createSlidingWindowHistogram({ + chunks: 1, + recordsPerChunk: 1, + ...bounds, + }); + histogram.record(1); + assert.strictEqual(histogram.snapshot().count, 1); + } +} + +(async () => { + const histogram = createSlidingWindowHistogram({ + chunks: 1, + chunkDuration: 100, + highest: 100, + }); + + histogram.record(1); + assert.strictEqual(histogram.snapshot().count, 1); + + await delay(common.platformTimeout(200)); + assert.strictEqual(histogram.snapshot().count, 0); + + histogram.record(2); + const current = histogram.snapshot(); + assert.strictEqual(current.count, 1); + assert.strictEqual(current.min, 2); +})().then(common.mustCall()); diff --git a/test/parallel/test-permission-openssl-store.js b/test/parallel/test-permission-openssl-store.js index ca2f240a952..f97657051ca 100644 --- a/test/parallel/test-permission-openssl-store.js +++ b/test/parallel/test-permission-openssl-store.js @@ -4,8 +4,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); -if (!hasOpenSSL3) +const { hasOpenSSL } = require('../common/crypto'); +if (!hasOpenSSL(3)) common.skip('requires OpenSSL 3.x'); // Verifies the openssl.store permission: allowed when --allow-openssl-store is diff --git a/test/parallel/test-process-env-allowed-flags-are-documented.js b/test/parallel/test-process-env-allowed-flags-are-documented.js index 9bc3cc98199..10c3ca2a893 100644 --- a/test/parallel/test-process-env-allowed-flags-are-documented.js +++ b/test/parallel/test-process-env-allowed-flags-are-documented.js @@ -5,7 +5,7 @@ const common = require('../common'); const assert = require('assert'); const fs = require('fs'); const path = require('path'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); const rootDir = path.resolve(__dirname, '..', '..'); const cliMd = path.join(rootDir, 'doc', 'api', 'cli.md'); @@ -44,7 +44,7 @@ for (const line of [...nodeOptionsLines, ...v8OptionsLines]) { } } -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { documented.delete('--openssl-legacy-provider'); documented.delete('--openssl-shared-config'); } @@ -58,8 +58,8 @@ const conditionalOpts = [ filter: (opt) => { return [ '--openssl-config', - hasOpenSSL3 ? '--openssl-legacy-provider' : '', - hasOpenSSL3 ? '--openssl-shared-config' : '', + hasOpenSSL(3) ? '--openssl-legacy-provider' : '', + hasOpenSSL(3) ? '--openssl-shared-config' : '', '--tls-cipher-list', '--use-bundled-ca', '--use-openssl-ca', diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js index b670ff37ac6..7f54beeea67 100644 --- a/test/parallel/test-process-versions.js +++ b/test/parallel/test-process-versions.js @@ -87,12 +87,12 @@ assert.match( assert.match(process.versions.modules, /^\d+$/); if (common.hasCrypto) { - const { hasOpenSSL3 } = require('../common/crypto'); + const { hasOpenSSL } = require('../common/crypto'); assert.match(process.versions.ncrypto, commonTemplate); if (process.config.variables.node_shared_openssl) { assert.ok(process.versions.openssl); } else { - const versionRegex = hasOpenSSL3 ? + const versionRegex = hasOpenSSL(3) ? // The following also matches a development version of OpenSSL 3.x which // can be in the format '3.0.0-alpha4-dev'. This can be handy when // building and linking against the main development branch of OpenSSL. diff --git a/test/parallel/test-quic-h3-uni-stream-limit-start-failure.mjs b/test/parallel/test-quic-h3-uni-stream-limit-start-failure.mjs new file mode 100644 index 00000000000..78f78ac1d58 --- /dev/null +++ b/test/parallel/test-quic-h3-uni-stream-limit-start-failure.mjs @@ -0,0 +1,38 @@ +// Flags: --experimental-quic --no-warnings + +// An HTTP/3 session must cleanly fail if the peer advertises fewer than +// the 3 unidirectional streams that HTTP/3 needs for control and QPACK. + +import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +const serverEndpoint = await listen(async (serverSession) => { + await serverSession.closed; +}, { + sni: { '*': { keys: [key], certs: [cert] } }, + // No uni streams allowed: + transportParams: { initialMaxStreamsUni: 0 }, + onheaders: mustNotCall(), +}); + +// Expect the client to cleanly fail - not crash the process +await assert.rejects(async () => { + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; +}, { code: 'ERR_QUIC_TRANSPORT_ERROR' }); + +await serverEndpoint.close(); diff --git a/test/parallel/test-runner-execution-ordered-bypass.mjs b/test/parallel/test-runner-execution-ordered-bypass.mjs index ac1c97ee007..75e9eb51d7d 100644 --- a/test/parallel/test-runner-execution-ordered-bypass.mjs +++ b/test/parallel/test-runner-execution-ordered-bypass.mjs @@ -1,8 +1,10 @@ // Flags: --no-warnings -import '../common/index.mjs'; +import { mustCall, platformTimeout } from '../common/index.mjs'; import * as fixtures from '../common/fixtures.mjs'; import assert from 'node:assert'; +import { once } from 'node:events'; +import { createServer } from 'node:net'; import { test, run } from 'node:test'; const files = [ @@ -10,15 +12,29 @@ const files = [ fixtures.path('test-runner', 'execution-ordered-bypass', 'fast-fail.mjs'), ]; -test('execution-ordered events bypass FileTest declaration-order buffer', async () => { +test('execution-ordered events bypass FileTest declaration-order buffer', { + timeout: platformTimeout(30_000), +}, async (t) => { + const { promise: fastCompleted, resolve: releaseSlow } = Promise.withResolvers(); + const server = createServer(mustCall((socket) => { + t.after(() => socket.destroy()); + fastCompleted.then(mustCall(() => { + socket.end(); + })); + })); + t.after(() => server.close()); + await once(server.listen(0, '127.0.0.1'), 'listening'); + // Concurrency must be a number so the runner does not collapse it to 1 on // single-core CI runners (where `concurrency: true` resolves to // `availableParallelism() - 1`). Without two slots the runner spawns the - // files sequentially and fast-fail never starts while slow is sleeping. + // files sequentially and fast-fail never starts while slow is waiting. const stream = run({ files, isolation: 'process', concurrency: 2, + argv: [String(server.address().port)], + signal: t.signal, }); const events = []; @@ -27,6 +43,9 @@ test('execution-ordered events bypass FileTest declaration-order buffer', async if (data.name === 'slow' || data.name === 'fast-fail') { events.push(`complete:${data.name}`); } + if (data.name === 'fast-fail') { + releaseSlow(); + } }); stream.on('test:fail', (data) => { diff --git a/test/parallel/test-runner-v8-deserializer.mjs b/test/parallel/test-runner-v8-deserializer.mjs index 7f2c0155c97..3a4db367ca6 100644 --- a/test/parallel/test-runner-v8-deserializer.mjs +++ b/test/parallel/test-runner-v8-deserializer.mjs @@ -85,6 +85,15 @@ describe('v8 deserializer', common.mustCall(() => { assert.deepStrictEqual(reported, [reportedDiagnosticEvent]); }); + it('should serialize a repeated object as independent messages', async () => { + const repeatedChunks = await toArray(serializer([diagnosticEvent, diagnosticEvent])); + const reported = await collectReported(repeatedChunks); + assert.deepStrictEqual(reported, [ + reportedDiagnosticEvent, + reportedDiagnosticEvent, + ]); + }); + it('should deserialize a serialized chunk after non-serialized chunk', async () => { const reported = await collectReported([Buffer.concat([Buffer.from('unknown'), ...chunks])]); assert.deepStrictEqual(reported, [ diff --git a/test/parallel/test-sqlite-backup.mjs b/test/parallel/test-sqlite-backup.mjs index f995ae3ca72..ee5f54fd909 100644 --- a/test/parallel/test-sqlite-backup.mjs +++ b/test/parallel/test-sqlite-backup.mjs @@ -269,6 +269,16 @@ test('throws if URL is not file: scheme', (t) => { }); }); +test('throws if the URL-like path has an unparsable href', (t) => { + const database = new DatabaseSync(':memory:'); + + t.after(() => { database.close(); }); + + t.assert.throws(() => { + backup(database, { href: 'not a url' }); + }, { code: 'ERR_INVALID_URL' }); +}); + test('database backup fails when dest file is not writable', { skip: isRoot }, async (t) => { const readonlyDestDb = nextDb(); writeFileSync(readonlyDestDb, '', { mode: 0o444 }); diff --git a/test/parallel/test-sqlite-database-sync.js b/test/parallel/test-sqlite-database-sync.js index 08a636c9cbd..e570ffde779 100644 --- a/test/parallel/test-sqlite-database-sync.js +++ b/test/parallel/test-sqlite-database-sync.js @@ -51,6 +51,12 @@ suite('DatabaseSync() constructor', () => { }); }); + test('throws if the URL-like path has an unparsable href', (t) => { + t.assert.throws(() => { + new DatabaseSync({ href: 'not a url' }); + }, { code: 'ERR_INVALID_URL' }); + }); + test('throws if options is provided but is not an object', (t) => { t.assert.throws(() => { new DatabaseSync('foo', null); diff --git a/test/parallel/test-stream-duplex-from.js b/test/parallel/test-stream-duplex-from.js index e12599fed17..a553a90f96e 100644 --- a/test/parallel/test-stream-duplex-from.js +++ b/test/parallel/test-stream-duplex-from.js @@ -418,3 +418,19 @@ function makeATestWritableStream(writeFunc) { })); r.destroy(expectedErr); } + +// Regression for https://github.com/nodejs/node/issues/55077: +// An AsyncFunction passed to Duplex.from() that returns without consuming its +// input must still allow pipeline() to complete and destroy the upstream. +{ + const r = Readable.from(['foo', 'bar', 'baz']); + pipeline( + r, + Duplex.from(async function() { + // Intentionally do not consume the async iterable input. + }), + common.mustCall(() => { + assert.strictEqual(r.destroyed, true); + }), + ); +} diff --git a/test/parallel/test-stream-iter-broadcast-basic.js b/test/parallel/test-stream-iter-broadcast-basic.js index 3dbd3ce9751..4135aea6d97 100644 --- a/test/parallel/test-stream-iter-broadcast-basic.js +++ b/test/parallel/test-stream-iter-broadcast-basic.js @@ -350,6 +350,18 @@ async function testLateJoinerSeesBufferedData() { assert.strictEqual(result, 'before-join'); } +async function testLateJoinerAfterDetachSeesBufferedData() { + const { writer, broadcast: bc } = broadcast({ budget: 16384 }); + const first = bc.push()[Symbol.asyncIterator](); + + writer.writeSync('before-detach'); + await first.return(); + + const second = bc.push(); + writer.endSync(); + assert.strictEqual(await text(second), 'before-detach'); +} + async function testOverlappingNextKeepsEarlierRead() { const { writer, broadcast: bc } = broadcast(); const it = bc.push()[Symbol.asyncIterator](); @@ -403,5 +415,6 @@ Promise.all([ testFailDetachesConsumers(), testWriterFailIdempotent(), testLateJoinerSeesBufferedData(), + testLateJoinerAfterDetachSeesBufferedData(), testOverlappingNextKeepsEarlierRead(), ]).then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-broadcast-from.js b/test/parallel/test-stream-iter-broadcast-from.js index 39d92c2aef4..928d4d472f0 100644 --- a/test/parallel/test-stream-iter-broadcast-from.js +++ b/test/parallel/test-stream-iter-broadcast-from.js @@ -122,8 +122,8 @@ async function testBroadcastFromCancelWhileBlocked() { async function* slowSource() { const enc = new TextEncoder(); yield [enc.encode('chunk1')]; - // Simulate a long delay - the cancel should unblock this - await new Promise((resolve) => setTimeout(resolve, 10000)); + // Simulate a long delay without keeping the cancelled source alive. + await new Promise((resolve) => setTimeout(resolve, 10000).unref()); yield [enc.encode('chunk2')]; sourceFinished = true; } diff --git a/test/parallel/test-stream-iter-share-from.js b/test/parallel/test-stream-iter-share-from.js index 806e3087630..69b782ce560 100644 --- a/test/parallel/test-stream-iter-share-from.js +++ b/test/parallel/test-stream-iter-share-from.js @@ -221,23 +221,29 @@ async function testShareDropNewest() { // ============================================================================= async function testShareStrictBackpressure() { - async function* source() { - for (let i = 0; i < 10; i++) { - yield [new Uint8Array(16384)]; + for (const transformed of [false, true]) { + async function* source() { + for (let i = 0; i < 10; i++) { + yield [new Uint8Array(16384)]; + } } + const shared = share(source(), { + budget: 32768, + backpressure: 'strict', + }); + const consumer = transformed ? + shared.pull((chunks) => chunks) : shared.pull(); + const fast = consumer[Symbol.asyncIterator](); + // This consumer prevents the buffer from being trimmed. + shared.pull(); + + await fast.next(); + await fast.next(); + await assert.rejects(fast.next(), { code: 'ERR_OUT_OF_RANGE' }); + assert.strictEqual(shared.consumerCount, 1); + assert.strictEqual((await fast.next()).done, true); + shared.cancel(); } - const shared = share(source(), { budget: 32768, backpressure: 'strict' }); - const fast = shared.pull(); - // Create a second consumer that never reads — this prevents buffer trimming - shared.pull(); - - // The fast consumer's pulls will eventually cause the buffer to exceed - // the budget (since the slow consumer prevents trimming), - // triggering an ERR_OUT_OF_RANGE error. - await assert.rejects(async () => { - // eslint-disable-next-line no-unused-vars - for await (const _ of fast) { /* consume */ } - }, { code: 'ERR_OUT_OF_RANGE' }); } Promise.all([ diff --git a/test/parallel/test-stream-readable-to-web-termination.js b/test/parallel/test-stream-readable-to-web-termination.js index f30cf721e14..8a15fa3f5fd 100644 --- a/test/parallel/test-stream-readable-to-web-termination.js +++ b/test/parallel/test-stream-readable-to-web-termination.js @@ -42,3 +42,25 @@ const { setTimeout: delay } = require('timers/promises'); assert.deepStrictEqual(closeResult, { value: undefined, done: true }); })().then(common.mustCall()); } + +// Cancelling a web ReadableStream while the underlying Readable is actively +// producing data should not throw ERR_INVALID_STATE. The 'data' handler in +// newReadableStreamFromStreamReadable must check wasCanceled before calling +// controller.enqueue(). See: https://github.com/nodejs/node/issues/54205 +{ + const readable = new Readable({ + read() { + this.push(Buffer.alloc(1024)); + }, + }); + + const webStream = Readable.toWeb(readable); + const reader = webStream.getReader(); + + (async () => { + await reader.read(); + await reader.read(); + reader.releaseLock(); + await webStream.cancel(); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-tick-processor-arguments.js b/test/parallel/test-tick-processor-arguments.js index 406b13b676d..a2d99192f97 100644 --- a/test/parallel/test-tick-processor-arguments.js +++ b/test/parallel/test-tick-processor-arguments.js @@ -19,6 +19,17 @@ const files = fs.readdirSync(tmpdir.path); const logfile = files.find((name) => /\.log$/.test(name)); assert(logfile); +// Drop the shared-library entries: the tick processor resolves the C++ +// symbols of every listed library through nm (and c++filt on macOS), which is +// slow on builds that link many shared libraries and depends on the host +// toolchain. This test only checks that CLI arguments reach the tick +// processor; C++ symbol resolution is covered by test/tick-processor. +const logpath = tmpdir.resolve(logfile); +fs.writeFileSync(logpath, fs.readFileSync(logpath, 'utf8') + .split('\n') + .filter((line) => !line.startsWith('shared-library,')) + .join('\n')); + // Make sure that the --preprocess argument is passed through correctly, // as an example flag listed in deps/v8/tools/tickprocessor.js. // Any of the other flags there should work for this test too, if --preprocess diff --git a/test/parallel/test-tls-alert-handling.js b/test/parallel/test-tls-alert-handling.js index c319e766ce8..1b80571e70e 100644 --- a/test/parallel/test-tls-alert-handling.js +++ b/test/parallel/test-tls-alert-handling.js @@ -6,7 +6,8 @@ if (!common.hasCrypto) { } const { - hasOpenSSL3, + hasOpenSSL, + isBoringSSL, } = require('../common/crypto'); const assert = require('assert'); @@ -38,7 +39,7 @@ const errorHandler = common.mustCall((err) => { assert.match(err.code, /ERR_SSL_(WRONG_VERSION_NUMBER|PACKET_LENGTH_TOO_LONG|BAD_RECORD_TYPE)/); assert.strictEqual(err.library, 'SSL routines'); - if (!hasOpenSSL3 && !process.features.openssl_is_boringssl) + if (!hasOpenSSL(3) && !isBoringSSL) assert.strictEqual(err.function, 'ssl3_get_record'); assert.match(err.reason, /wrong[\s_]version[\s_]number|packet[\s_]length[\s_]too[\s_]long|bad[\s_]record[\s_]type/i); @@ -99,7 +100,7 @@ function sendBADTLSRecord() { assert.match(err.code, /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) + if (!hasOpenSSL(3) && !isBoringSSL) 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); diff --git a/test/parallel/test-tls-alert.js b/test/parallel/test-tls-alert.js index 64b7080e39b..5c229d7e05c 100644 --- a/test/parallel/test-tls-alert.js +++ b/test/parallel/test-tls-alert.js @@ -27,7 +27,9 @@ if (!common.hasCrypto) { const { hasOpenSSL, + hasFIPS, opensslCli, + isBoringSSL, } = require('../common/crypto'); if (!opensslCli) { @@ -43,12 +45,19 @@ function loadPEM(n) { return fixtures.readKey(`${n}.pem`); } -const server = tls.Server({ - secureProtocol: 'TLSv1_2_server_method', +const serverOptions = { key: loadPEM('agent2-key'), - cert: loadPEM('agent2-cert') -}, null).listen(0, common.mustCall(() => { - if (process.features.openssl_is_boringssl) { + cert: loadPEM('agent2-cert'), +}; +if (hasFIPS(3)) { + serverOptions.minVersion = 'TLSv1.3'; + serverOptions.maxVersion = 'TLSv1.3'; +} else { + serverOptions.secureProtocol = 'TLSv1_2_server_method'; +} + +const server = tls.Server(serverOptions, null).listen(0, common.mustCall(() => { + if (isBoringSSL) { let gotClientError = false; let gotServerError = false; function maybeClose() { @@ -75,8 +84,9 @@ const server = tls.Server({ return; } - const args = ['s_client', '-quiet', '-tls1_1', - '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + const args = ['s_client', '-quiet', hasFIPS(3) ? '-tls1_2' : '-tls1_1', + '-cipher', hasFIPS(3) ? 'DEFAULT' : + (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `127.0.0.1:${server.address().port}`]; execFile(opensslCli, args, common.mustCall((err, _, stderr) => { diff --git a/test/parallel/test-tls-alpn-callback-sync-end-newsession.js b/test/parallel/test-tls-alpn-callback-sync-end-newsession.js new file mode 100644 index 00000000000..6e20e4d112a --- /dev/null +++ b/test/parallel/test-tls-alpn-callback-sync-end-newsession.js @@ -0,0 +1,56 @@ +'use strict'; + +// A shutdown deferred off the SSL library's stack must not run while the +// 'newSession' callback is still outstanding. Make sure it's deferred +// correctly so the connection still cleanly closes. + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); +const { SSL_OP_NO_TICKET } = require('crypto').constants; + +const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), + // The only config that consistently fires newSession on both OpenSSL & + // BoringSSL is TLS v1.2 + session id resumption (tickets disabled): + minVersion: 'TLSv1.2', + maxVersion: 'TLSv1.2', + secureOptions: SSL_OP_NO_TICKET, + ALPNCallback: common.mustCall(function({ protocols }) { + this.end(); + return protocols[0]; + }), +}); + +// Answering asynchronously holds EncOut() while the shutdown is replayed. +server.on('newSession', common.mustCall((id, data, callback) => { + setImmediate(callback); +})); + +server.on('tlsClientError', common.mustNotCall()); +server.on('secureConnection', common.mustCall((socket) => { + socket.on('error', common.mustNotCall()); +})); + +server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + ALPNProtocols: ['a'], + rejectUnauthorized: false, + }, common.mustCall(() => { + assert.strictEqual(client.alpnProtocol, 'a'); + })); + + client.on('end', common.mustCall()); + client.on('close', common.mustCall((hadError) => { + assert.strictEqual(hadError, false); + server.close(); + })); + client.on('error', common.mustNotCall()); +})); diff --git a/test/parallel/test-tls-alpn-callback-sync-end.js b/test/parallel/test-tls-alpn-callback-sync-end.js new file mode 100644 index 00000000000..96c63998390 --- /dev/null +++ b/test/parallel/test-tls-alpn-callback-sync-end.js @@ -0,0 +1,53 @@ +'use strict'; + +// Ending a server TLSSocket synchronously from inside an ALPNCallback must +// finish the handshake and then shut the connection down cleanly, rather than +// dropping the underlying socket part way through it. + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); + +function test(maxVersion) { + const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), + maxVersion, + ALPNCallback: common.mustCall(function({ protocols }) { + this.end(); + return protocols[0]; + }), + }); + + server.on('tlsClientError', common.mustNotCall()); + server.on('secureConnection', common.mustCall((socket) => { + socket.on('error', common.mustNotCall()); + })); + + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + ALPNProtocols: ['a'], + rejectUnauthorized: false, + maxVersion, + }, common.mustCall(() => { + assert.strictEqual(client.alpnProtocol, 'a'); + })); + + // A clean close_notify, not a truncated connection. + client.on('end', common.mustCall()); + client.on('close', common.mustCall((hadError) => { + assert.strictEqual(hadError, false); + server.close(); + })); + client.on('error', common.mustNotCall()); + })); +} + +test('TLSv1.2'); +test('TLSv1.3'); diff --git a/test/parallel/test-tls-alpn-callback-sync-write.js b/test/parallel/test-tls-alpn-callback-sync-write.js new file mode 100644 index 00000000000..d7e2d9098c4 --- /dev/null +++ b/test/parallel/test-tls-alpn-callback-sync-write.js @@ -0,0 +1,49 @@ +'use strict'; + +// Writing to a server TLSSocket synchronously from inside an ALPNCallback, +// which the TLS library invokes on its own stack mid-handshake, must not break +// the connection; the data must be delivered once the handshake ends. + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); + +const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), + ALPNCallback: common.mustCall(function({ protocols }) { + // The write cannot complete until the handshake does, but it must be + // accepted and eventually flushed rather than dropped or encrypted into + // the middle of the handshake. + this.write('from-mid-handshake', common.mustCall()); + return protocols[0]; + }), +}); + +server.on('tlsClientError', common.mustNotCall()); +server.on('secureConnection', common.mustCall((socket) => { + assert.strictEqual(socket.alpnProtocol, 'a'); + socket.on('error', common.mustNotCall()); +})); + +server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + ALPNProtocols: ['a', 'b'], + rejectUnauthorized: false, + }, common.mustCall(() => { + assert.strictEqual(client.alpnProtocol, 'a'); + + client.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'from-mid-handshake'); + client.end(); + server.close(); + })); + })); + client.on('error', common.mustNotCall()); +})); diff --git a/test/parallel/test-tls-cert-ext-encoding.js b/test/parallel/test-tls-cert-ext-encoding.js index 154e0cdcf02..973e9fad32e 100644 --- a/test/parallel/test-tls-cert-ext-encoding.js +++ b/test/parallel/test-tls-cert-ext-encoding.js @@ -3,9 +3,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); -if (hasOpenSSL3) +if (hasOpenSSL(3)) // TODO(danbev) This test fails with the following error: // error:0D00008F:asn1 encoding routines::no matching choice type // diff --git a/test/parallel/test-tls-client-auth.js b/test/parallel/test-tls-client-auth.js index 517054c6e29..da06156e2e0 100644 --- a/test/parallel/test-tls-client-auth.js +++ b/test/parallel/test-tls-client-auth.js @@ -6,7 +6,7 @@ const fixtures = require('../common/fixtures'); if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const { assert, connect, keys, tls @@ -111,7 +111,7 @@ if (tls.DEFAULT_MAX_VERSION === 'TLSv1.3') connect({ // and sends a fatal Alert to the client that the client discovers there has // been a fatal error. pair.client.conn.once('error', common.mustCall((err) => { - const expectedErr = process.features.openssl_is_boringssl ? + const expectedErr = isBoringSSL ? 'ERR_SSL_TLSV1_ALERT_CERTIFICATE_REQUIRED' : 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'; assert.strictEqual(err.code, expectedErr); diff --git a/test/parallel/test-tls-client-getephemeralkeyinfo.js b/test/parallel/test-tls-client-getephemeralkeyinfo.js index 82572c4e497..f1c179db8fa 100644 --- a/test/parallel/test-tls-client-getephemeralkeyinfo.js +++ b/test/parallel/test-tls-client-getephemeralkeyinfo.js @@ -3,13 +3,18 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +const { + isBoringSSL, + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').testEphemeralKeyInfo(); return; } const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); const { X509Certificate } = require('crypto'); @@ -17,12 +22,14 @@ const tls = require('tls'); const key = fixtures.readKey('agent2-key.pem'); const cert = fixtures.readKey('agent2-cert.pem'); +const fips3 = hasFIPS(3); +const rejectsXCurves = hasFIPS(3, 5); function loadDHParam(n) { return fixtures.readKey(`dh${n}.pem`); } -function test(size, type, name, cipher) { +function test(size, type, name, cipher, expectError = false) { assert(cipher); const options = { @@ -49,39 +56,62 @@ function test(size, type, name, cipher) { } } - const server = tls.createServer(options, common.mustCall((conn) => { - assert.strictEqual(conn.getEphemeralKeyInfo(), null); - conn.end(); - })); + if (rejectsXCurves && (name === 'X25519' || name === 'X448')) { + assert.throws(() => tls.createServer(options), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + return; + } + + const onConnection = expectError ? common.mustNotCall() : + common.mustCall((conn) => { + assert.strictEqual(conn.getEphemeralKeyInfo(), null); + conn.end(); + }); + const server = tls.createServer(options, onConnection); server.on('close', common.mustSucceed()); server.listen(0, common.mustCall(() => { + const onSecureConnect = expectError ? common.mustNotCall() : + common.mustCall(function() { + const ekeyinfo = client.getEphemeralKeyInfo(); + assert.strictEqual(ekeyinfo.type, type); + assert.strictEqual(ekeyinfo.size, size); + assert.strictEqual(ekeyinfo.name, name); + server.close(); + }); const client = tls.connect({ port: server.address().port, rejectUnauthorized: false - }, common.mustCall(function() { - const ekeyinfo = client.getEphemeralKeyInfo(); - assert.strictEqual(ekeyinfo.type, type); - assert.strictEqual(ekeyinfo.size, size); - assert.strictEqual(ekeyinfo.name, name); - server.close(); - })); - client.on('secureConnect', common.mustCall()); + }, onSecureConnect); + if (expectError) { + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_BAD_DH_VALUE'); + server.close(); + })); + } else { + client.on('secureConnect', common.mustCall()); + } })); } -test(undefined, undefined, undefined, 'AES256-SHA256'); +if (!fips3) + test(undefined, undefined, undefined, 'AES256-SHA256'); test('auto', 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); -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'); +if (fips3 && !hasOpenSSL(4)) { + test(2048, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384', true); } else { - test(3072, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); + 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'); + } + test(2048, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); } -test(2048, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); test(256, 'ECDH', 'prime256v1', 'ECDHE-RSA-AES256-GCM-SHA384'); test(521, 'ECDH', 'secp521r1', 'ECDHE-RSA-AES256-GCM-SHA384'); test(253, 'ECDH', 'X25519', 'ECDHE-RSA-AES256-GCM-SHA384'); @@ -96,6 +126,17 @@ function testTLS13Group(size, type, name) { maxVersion: 'TLSv1.3', }; + const unsupportedFipsGroup = + (rejectsXCurves && name === 'X25519') || + (hasFIPS(4) && + (name === 'curveSM2' || name === 'curveSM2MLKEM768')); + if (unsupportedFipsGroup) { + assert.throws(() => tls.createServer(options), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + return; + } + const server = tls.createServer(options, common.mustCall((conn) => { assert.strictEqual(conn.getEphemeralKeyInfo(), null); conn.end(); @@ -121,6 +162,8 @@ function testTLS13Group(size, type, name) { })); } +if (fips3) + testTLS13Group(256, 'ECDH', 'prime256v1'); testTLS13Group(253, 'ECDH', 'X25519'); if (hasOpenSSL(3, 5)) { diff --git a/test/parallel/test-tls-client-mindhsize.js b/test/parallel/test-tls-client-mindhsize.js index d777a9bfa97..f08be448dd1 100644 --- a/test/parallel/test-tls-client-mindhsize.js +++ b/test/parallel/test-tls-client-mindhsize.js @@ -13,7 +13,9 @@ 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 { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); +const fips3 = hasFIPS(3); +const fips4 = hasFIPS(4); const key = fixtures.readKey('agent2-key.pem'); const cert = fixtures.readKey('agent2-cert.pem'); @@ -29,7 +31,7 @@ function test(size, err, next, minDHSizeOverride) { const options = { key: key, cert: cert, - dhparam: loadDHParam(size), + dhparam: size === 'auto' ? 'auto' : loadDHParam(size), ciphers: 'DHE-RSA-AES128-GCM-SHA256' }; @@ -60,7 +62,8 @@ function test(size, err, next, minDHSizeOverride) { if (err) { client.on('error', common.mustCall((e) => { nerror++; - assert.strictEqual(e.code, 'ERR_TLS_DH_PARAM_SIZE'); + assert.strictEqual(e.code, fips3 && !fips4 ? + 'ERR_SSL_BAD_DH_VALUE' : 'ERR_TLS_DH_PARAM_SIZE'); server.close(); })); } @@ -85,8 +88,12 @@ function testDHE3072() { test(3072, false, null); } -if (!process.features.openssl_is_boringssl) { - if (hasOpenSSL(4, 0)) { +if (!isBoringSSL) { + if (fips3 && !fips4) { + // The FIPS provider rejects explicit DH parameters without a validated + // subgroup, while OpenSSL's built-in FFDHE group remains available. + testDHE2048(true, () => test('auto', false, null, 2048)); + } else 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. @@ -122,7 +129,7 @@ for (const minDHSize of [true, false, null, undefined, {}, [], '', '1']) { }); } -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { process.on('exit', function() { assert.strictEqual(nsuccess, 1); assert.strictEqual(nerror, 1); diff --git a/test/parallel/test-tls-client-reject.js b/test/parallel/test-tls-client-reject.js index cff0aabc89a..90e3127ba47 100644 --- a/test/parallel/test-tls-client-reject.js +++ b/test/parallel/test-tls-client-reject.js @@ -20,7 +20,9 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -31,7 +33,7 @@ const fixtures = require('../common/fixtures'); const options = { key: fixtures.readKey('rsa_private.pem'), cert: fixtures.readKey('rsa_cert.crt'), - ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), + ...(isBoringSSL ? { maxVersion: 'TLSv1.2' } : {}), }; const server = tls.createServer(options, function(socket) { @@ -48,7 +50,7 @@ function unauthorized() { port: server.address().port, servername: 'localhost', rejectUnauthorized: false, - ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), + ...(isBoringSSL ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(function() { let _data; assert(!socket.authorized); @@ -70,7 +72,7 @@ function rejectUnauthorized() { console.log('reject unauthorized'); const socket = tls.connect(server.address().port, { servername: 'localhost', - ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), + ...(isBoringSSL ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustNotCall()); socket.on('data', common.mustNotCall()); socket.on('error', common.mustCall(function(err) { @@ -84,7 +86,7 @@ function rejectUnauthorizedUndefined() { const socket = tls.connect(server.address().port, { servername: 'localhost', rejectUnauthorized: undefined, - ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), + ...(isBoringSSL ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustNotCall()); socket.on('data', common.mustNotCall()); socket.on('error', common.mustCall(function(err) { @@ -98,7 +100,7 @@ function authorized() { const socket = tls.connect(server.address().port, { ca: [fixtures.readKey('rsa_cert.crt')], servername: 'localhost', - ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), + ...(isBoringSSL ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(function() { console.log('... authorized'); assert(socket.authorized); diff --git a/test/parallel/test-tls-client-renegotiation-13.js b/test/parallel/test-tls-client-renegotiation-13.js index 80c4753d065..33e74af13ed 100644 --- a/test/parallel/test-tls-client-renegotiation-13.js +++ b/test/parallel/test-tls-client-renegotiation-13.js @@ -5,7 +5,7 @@ const common = require('../common'); if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); @@ -32,7 +32,7 @@ connect({ assert.strictEqual(client.getProtocol(), 'TLSv1.3'); const ok = client.renegotiate({}, common.mustCall((err) => { - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { assert.throws(() => { throw err; }, { message: 'TLS session renegotiation is unsupported by this TLS ' + 'implementation', @@ -40,7 +40,7 @@ connect({ }); } else { assert.throws(() => { throw err; }, { - message: hasOpenSSL3 ? + message: hasOpenSSL(3) ? 'error:0A00010A:SSL routines::wrong ssl version' : 'error:1420410A:SSL routines:SSL_renegotiate:wrong ssl version', code: 'ERR_SSL_WRONG_SSL_VERSION', diff --git a/test/parallel/test-tls-client-renegotiation-limit.js b/test/parallel/test-tls-client-renegotiation-limit.js index 9b7f62865b3..b712e6f7d9e 100644 --- a/test/parallel/test-tls-client-renegotiation-limit.js +++ b/test/parallel/test-tls-client-renegotiation-limit.js @@ -25,13 +25,13 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { opensslCli } = require('../common/crypto'); +const { opensslCli, isBoringSSL } = require('../common/crypto'); if (!opensslCli) { common.skip('node compiled without OpenSSL CLI.'); } -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { require('../common/boringssl').testRenegotiationUnsupported(); return; } diff --git a/test/parallel/test-tls-connect-single-use-context.js b/test/parallel/test-tls-connect-single-use-context.js new file mode 100644 index 00000000000..e135bff4f25 --- /dev/null +++ b/test/parallel/test-tls-connect-single-use-context.js @@ -0,0 +1,132 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +// tls.connect() builds a SecureContext per connection and marks it as single +// use, so that the underlying SSL_CTX is released as soon as the socket +// closes instead of when the JS wrapper happens to be garbage collected. +// Regression test for https://github.com/nodejs/node/issues/66002, where the +// flag was lost on its way to the context and nothing was ever closed. + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); +const { createMockedLookup } = require('../common/dns'); + +const key = fixtures.readKey('agent1-key.pem'); +const cert = fixtures.readKey('agent1-cert.pem'); + +const server = tls.createServer({ key, cert }, (conn) => conn.end()); + +// Bound to a single address so that the addresses the handle-swap case +// retries through are refused rather than answered by this server. +server.listen(0, '127.0.0.1', common.mustCall(() => { + connectWithOwnContext(common.mustCall(() => { + connectWithSharedContext(common.mustCall(() => { + connectAcrossHandleSwaps(common.mustCall(() => { + connectWithKeepAlive(common.mustCall(() => server.close())); + })); + })); + })); +})); + +// _destroySSL() runs from the immediate queue, after the 'close' event. +function afterDestroySSL(socket, fn) { + socket.on('close', common.mustCall(() => setImmediate(fn))); +} + +function connectWithOwnContext(done) { + const socket = tls.connect({ + host: '127.0.0.1', + port: server.address().port, + rejectUnauthorized: false, + }, common.mustCall(() => { + const secureContext = socket.ssl._secureContext; + assert.strictEqual(secureContext.singleUse, true); + assert.notStrictEqual(secureContext.context, null); + + afterDestroySSL(socket, common.mustCall(() => { + assert.strictEqual(secureContext.context, null); + done(); + })); + })); +} + +function connectWithSharedContext(done) { + // A context passed in by the user may outlive the connection, so it must + // not be marked single use, and it must still work for the next socket. + const secureContext = tls.createSecureContext(); + let remaining = 2; + + (function connectOnce() { + const socket = tls.connect({ + host: '127.0.0.1', + port: server.address().port, + rejectUnauthorized: false, + secureContext, + }, common.mustCall(() => { + assert.strictEqual(socket.ssl._secureContext, secureContext); + assert.strictEqual(secureContext.singleUse, undefined); + + afterDestroySSL(socket, common.mustCall(() => { + assert.notStrictEqual(secureContext.context, null); + if (--remaining === 0) done(); + else connectOnce(); + })); + })); + })(); +} + +function connectAcrossHandleSwaps(done) { + // autoSelectFamily reinitializes the handle on every failed attempt, and the + // successive TLSWraps share the socket's context. Releasing it with the old + // handle leaves the next attempt without a context. Two failing addresses + // are needed: the close happens on the first swap, and the next swap is what + // trips over it. + const socket = tls.connect({ + host: 'example.org', + port: server.address().port, + rejectUnauthorized: false, + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout: + common.defaultAutoSelectFamilyAttemptTimeout, + lookup: createMockedLookup('::1', '127.0.0.2', '127.0.0.1'), + }, common.mustCall(() => { + // `ssl` is cleared while the handle is swapped, so read the context from + // the handle the socket ended up with. + const secureContext = socket._handle._secureContext; + assert.strictEqual(secureContext.singleUse, true); + assert.notStrictEqual(secureContext.context, null); + + afterDestroySSL(socket, common.mustCall(() => { + assert.strictEqual(secureContext.context, null); + done(); + })); + })); +} + +function connectWithKeepAlive(done) { + // tls.connect() only asks for a single-use context when TCP keepalive is + // off, so a client that enables it leaves its context to the garbage + // collector. Pinned here as the current behaviour: whether the context + // should be released early in this case too is the open question in the + // issue, and is deliberately not decided by this change. + const socket = tls.connect({ + host: '127.0.0.1', + port: server.address().port, + rejectUnauthorized: false, + keepAlive: true, + }, common.mustCall(() => { + const secureContext = socket.ssl._secureContext; + assert.strictEqual(secureContext.singleUse, undefined); + assert.notStrictEqual(secureContext.context, null); + + afterDestroySSL(socket, common.mustCall(() => { + assert.notStrictEqual(secureContext.context, null); + done(); + })); + })); +} diff --git a/test/parallel/test-tls-dhe.js b/test/parallel/test-tls-dhe.js index 83af6daccbd..6f54bbd9766 100644 --- a/test/parallel/test-tls-dhe.js +++ b/test/parallel/test-tls-dhe.js @@ -26,7 +26,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -if (process.features.openssl_is_boringssl) { +const { isBoringSSL } = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').assertFiniteFieldDheUnsupported(); return; } @@ -34,6 +36,7 @@ if (process.features.openssl_is_boringssl) { const { opensslCli, hasOpenSSL, + hasFIPS, } = require('../common/crypto'); // OpenSSL has a set of security levels which affect what algorithms @@ -62,7 +65,7 @@ const dheCipher = 'DHE-RSA-AES128-SHA256'; const ecdheCipher = 'ECDHE-RSA-AES128-SHA256'; const ciphers = `${dheCipher}:${ecdheCipher}`; -if (secLevel < 2) { +if (secLevel < 2 && !hasFIPS(3)) { // Test will emit a warning because the DH parameter size is < 2048 bits // when the test is run on versions lower than OpenSSL32 common.expectWarning('SecurityWarning', @@ -74,7 +77,7 @@ function loadDHParam(n) { return fixtures.readKey(keyname); } -function test(dhparam, keylen, expectedCipher) { +function test(dhparam, keylen, expectedCipher, expectedError) { const options = { key, cert, @@ -84,12 +87,29 @@ function test(dhparam, keylen, expectedCipher) { }; const server = tls.createServer(options, (conn) => conn.end()); + if (typeof expectedError === 'string' || Array.isArray(expectedError)) { + server.once('tlsClientError', common.mustCall((err) => { + if (Array.isArray(expectedError)) { + assert.ok(expectedError.includes(err.code), err); + } else { + assert.strictEqual(err.code, expectedError); + } + })); + } server.listen(0, '127.0.0.1', common.mustCall(() => { const args = ['s_client', '-connect', `127.0.0.1:${server.address().port}`, '-cipher', `${ciphers}:@SECLEVEL=1`]; - execFile(opensslCli, args, common.mustSucceed((stdout) => { + execFile(opensslCli, args, common.mustCall((err, stdout, stderr) => { + if (expectedError) { + assert.strictEqual(err?.code, 1); + if (expectedError instanceof RegExp) assert.match(stderr, expectedError); + server.close(); + return; + } + + assert.ifError(err); assert(keylen === null || // s_client < OpenSSL 3.5 stdout.includes(`Server Temp Key: DH, ${keylen} bits`) || @@ -103,10 +123,10 @@ function test(dhparam, keylen, expectedCipher) { return once(server, 'close'); } -function testCustomParam(keylen, expectedCipher) { +function testCustomParam(keylen, expectedCipher, expectedError) { const dhparam = loadDHParam(keylen); if (keylen === 'error') keylen = null; - return test(dhparam, keylen, expectedCipher); + return test(dhparam, keylen, expectedCipher, expectedError); } (async () => { @@ -140,14 +160,29 @@ function testCustomParam(keylen, expectedCipher) { // 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); + if (hasFIPS(3)) { + if (hasFIPS(4)) { + await test(loadDHParam(3072), 2048, dheCipher); + await testCustomParam(2048, dheCipher); + } else { + const errorCode = hasFIPS(3, 5) ? + [ + 'ERR_SSL_INVALID_KEY_LENGTH', + 'ERR_SSL_SSL/TLS_ALERT_ILLEGAL_PARAMETER', + ] : 'ERR_SSL_INTERNAL_ERROR'; + await testCustomParam(3072, null, errorCode); + await testCustomParam(2048, null, errorCode); + } } else { - await testCustomParam(3072, dheCipher); + 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); } - await testCustomParam(2048, dheCipher); // Invalid DHE parameters are discarded. Prior to OpenSSL 4.0 this // disabled DHE and ECDHE was negotiated; since 4.0, FFDHE-2048 is used. diff --git a/test/parallel/test-tls-dhparam-auto-boringssl.js b/test/parallel/test-tls-dhparam-auto-boringssl.js index 54f2190d1a9..d2334c7feb0 100644 --- a/test/parallel/test-tls-dhparam-auto-boringssl.js +++ b/test/parallel/test-tls-dhparam-auto-boringssl.js @@ -1,9 +1,11 @@ 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); -if (!process.features.openssl_is_boringssl) +if (!isBoringSSL) common.skip('only applies to BoringSSL builds'); const assert = require('assert'); diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js index 84a6ead4a54..1934becc2a6 100644 --- a/test/parallel/test-tls-disable-renegotiation.js +++ b/test/parallel/test-tls-disable-renegotiation.js @@ -1,5 +1,7 @@ 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const fixtures = require('../common/fixtures'); @@ -8,7 +10,7 @@ const fixtures = require('../common/fixtures'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { require('../common/boringssl').testRenegotiationUnsupported(); return; } diff --git a/test/parallel/test-tls-ecdh-multiple.js b/test/parallel/test-tls-ecdh-multiple.js index ed60044197d..cc74130a2e3 100644 --- a/test/parallel/test-tls-ecdh-multiple.js +++ b/test/parallel/test-tls-ecdh-multiple.js @@ -8,7 +8,12 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { opensslCli, hasOpenSSL } = require('../common/crypto'); +const { + opensslCli, + hasOpenSSL, + hasFIPS, + isBoringSSL, +} = require('../common/crypto'); const crypto = require('crypto'); if (!opensslCli) { @@ -19,14 +24,16 @@ const assert = require('assert'); const tls = require('tls'); const { execFile } = require('child_process'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); 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 = process.features.openssl_is_boringssl || hasOpenSSL(4, 0) ? +// The FIPS provider and OpenSSL 4.0 disable support for deprecated elliptic +// curves from RFC 8422 (including secp256k1) by default. +const ecdhCurve = isBoringSSL || + hasOpenSSL(4, 0) || hasFIPS(3) ? 'prime256v1:secp521r1' : 'secp256k1:prime256v1:secp521r1'; @@ -61,13 +68,22 @@ const server = tls.createServer(options, (conn) => { 'prime192v3', ]; - // Brainpool is not supported in FIPS mode. - if (crypto.getFips()) { + // Setting a Brainpool group on a TLS context is deferred by OpenSSL, so + // exercise the prohibited key operation directly under FIPS properties. + if (fips3) { + if (hasFIPS(3, 5)) { + assert.throws( + () => crypto.createECDH('brainpoolP256r1').generateKeys(), + { code: 'ERR_CRYPTO_OPERATION_FAILED' }); + } else { + unsupportedCurves.push('brainpoolP256r1'); + } + } else if (crypto.getFips() === 1) { unsupportedCurves.push('brainpoolP256r1'); } // Deprecated RFC 8422 curves are disabled by default in OpenSSL 4.0. - if (process.features.openssl_is_boringssl || hasOpenSSL(4, 0)) { + if (isBoringSSL || hasOpenSSL(4, 0)) { unsupportedCurves.push('secp256k1'); } diff --git a/test/parallel/test-tls-empty-sni-context.js b/test/parallel/test-tls-empty-sni-context.js index 6ecdfbeecbe..7d8bf61239a 100644 --- a/test/parallel/test-tls-empty-sni-context.js +++ b/test/parallel/test-tls-empty-sni-context.js @@ -3,7 +3,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); @@ -26,7 +26,7 @@ const server = tls.createServer(options, (c) => { }, common.mustNotCall()); c.on('error', common.mustCall((err) => { - const expectedErr = process.features.openssl_is_boringssl ? + const expectedErr = isBoringSSL ? 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR' : hasOpenSSL(4, 0) ? 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; diff --git a/test/parallel/test-tls-env-extra-ca-with-options.js b/test/parallel/test-tls-env-extra-ca-with-options.js index 8f04decf670..5775284eea4 100644 --- a/test/parallel/test-tls-env-extra-ca-with-options.js +++ b/test/parallel/test-tls-env-extra-ca-with-options.js @@ -8,7 +8,9 @@ if (!common.hasCrypto) const assert = require('node:assert'); const tls = require('node:tls'); const { fork } = require('node:child_process'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); const tests = [ { @@ -28,13 +30,26 @@ const tests = [ crl: fixtures.readKey('ca2-crl.pem') } }, - { +]; + +if (fips3) { + assert.throws(() => tls.createSecureContext({ + pfx: fixtures.readKey('agent1.pfx'), + passphrase: 'sample', + }), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); +} + +if (!fips3 || hasFIPS(3, 5)) { + tests.push({ clientOptions: { - pfx: fixtures.readKey('agent1.pfx'), - passphrase: 'sample' + pfx: fixtures.readKey(fips3 ? + 'agent1-fips.pfx' : 'agent1.pfx'), + passphrase: fips3 ? 'password' : 'sample' } - }, -]; + }); +} if (process.argv[2]) { const testNumber = parseInt(process.argv[2], 10); diff --git a/test/parallel/test-tls-finished.js b/test/parallel/test-tls-finished.js index b23b4567d27..5a33257cebc 100644 --- a/test/parallel/test-tls-finished.js +++ b/test/parallel/test-tls-finished.js @@ -1,6 +1,8 @@ 'use strict'; const common = require('../common'); + +const { isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); if (!common.hasCrypto) @@ -21,7 +23,7 @@ const pem = (n) => fixtures.readKey(`${n}.pem`); const server = tls.createServer({ key: pem('agent1-key'), cert: pem('agent1-cert'), - ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), + ...(isBoringSSL ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall((alice) => { msg.server = { alice: alice.getFinished(), @@ -34,7 +36,7 @@ server.listen(0, common.mustCall(() => { const bob = tls.connect({ port: server.address().port, rejectUnauthorized: false, - ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), + ...(isBoringSSL ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(() => { msg.client = { alice: bob.getPeerFinished(), diff --git a/test/parallel/test-tls-getcipher.js b/test/parallel/test-tls-getcipher.js index 2d4de5639af..cb8f1ba826b 100644 --- a/test/parallel/test-tls-getcipher.js +++ b/test/parallel/test-tls-getcipher.js @@ -20,7 +20,9 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL: commonIsBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -36,7 +38,7 @@ const options = { honorCipherOrder: true }; -const isBoringSSL = process.features.openssl_is_boringssl; +const isBoringSSL = commonIsBoringSSL; let clients = 0; const expectedClients = isBoringSSL ? 1 : 2; const server = tls.createServer(options, common.mustCall(() => { diff --git a/test/parallel/test-tls-getprotocol.js b/test/parallel/test-tls-getprotocol.js index 2945ff99b5a..e3e0a1fa241 100644 --- a/test/parallel/test-tls-getprotocol.js +++ b/test/parallel/test-tls-getprotocol.js @@ -3,7 +3,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); // This test ensures that `getProtocol` returns the right protocol // from a TLS connection @@ -11,6 +11,7 @@ const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); let clientConfigs = [ { @@ -27,7 +28,7 @@ let clientConfigs = [ }, ]; -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { // Remove the TLSv1 and TLSv1.1 cases. BoringSSL does not negotiate those // legacy protocols in this configuration; keep TLSv1.2 to cover getProtocol() // on a successful BoringSSL TLS handshake. @@ -41,28 +42,51 @@ const serverConfig = { cert: fixtures.readKey('agent2-cert.pem') }; -if (!process.features.openssl_is_boringssl) { - serverConfig.ciphers = 'RSA@SECLEVEL=0'; +if (!isBoringSSL) { + serverConfig.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'RSA@SECLEVEL=0'; } -const server = tls.createServer(serverConfig, common.mustCall(clientConfigs.length)) +const expectedConnections = fips3 ? 1 : clientConfigs.length; +const server = tls.createServer(serverConfig, common.mustCall(expectedConnections)); + +if (fips3) { + server.on('tlsClientError', common.mustCall((err) => { + assert.ok([ + 'ERR_SSL_NO_SUITABLE_DIGEST_ALGORITHM', + 'ERR_SSL_UNEXPECTED_MESSAGE', + ].includes(err.code), err); + }, 2)); +} + +server .listen(0, common.localhostIPv4, common.mustCall(function() { - let connected = 0; + let completed = 0; + function done() { + if (++completed === clientConfigs.length) + server.close(); + } + for (const v of clientConfigs) { - tls.connect({ + const shouldConnect = !fips3 || v.version === 'TLSv1.2'; + const client = tls.connect({ host: common.localhostIPv4, port: server.address().port, ciphers: v.ciphers, rejectUnauthorized: false, secureProtocol: v.secureProtocol - }, common.mustCall(function() { + }, shouldConnect ? common.mustCall(function() { assert.strictEqual(this.getProtocol(), v.version); this.on('end', common.mustCall()); this.on('close', common.mustCall(function() { assert.strictEqual(this.getProtocol(), null); + done(); })).end(); - if (++connected === clientConfigs.length) - server.close(); - })); + }) : common.mustNotCall()); + + if (!shouldConnect) { + client.on('error', common.mustCall((err) => assert(err.code))); + client.on('close', common.mustCall(done)); + } } })); diff --git a/test/parallel/test-tls-honorcipherorder.js b/test/parallel/test-tls-honorcipherorder.js index d86a59aa4cd..fcaed53353a 100644 --- a/test/parallel/test-tls-honorcipherorder.js +++ b/test/parallel/test-tls-honorcipherorder.js @@ -8,6 +8,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const mustCall = common.mustCall; const tls = require('tls'); const util = require('util'); @@ -16,7 +17,7 @@ const util = require('util'); // default method is updated in the future const SSL_Method = 'TLSv1_2_method'; const localhost = '127.0.0.1'; -const config = process.features.openssl_is_boringssl ? { +const config = isBoringSSL || hasFIPS(3) ? { serverCiphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', clientPreferenceCiphers: diff --git a/test/parallel/test-tls-invalid-pfx.js b/test/parallel/test-tls-invalid-pfx.js index c16858f0f78..749d9e8b9f7 100644 --- a/test/parallel/test-tls-invalid-pfx.js +++ b/test/parallel/test-tls-invalid-pfx.js @@ -7,6 +7,7 @@ const fixtures = require('../common/fixtures'); const { assert, connect, keys } = require(fixtures.path('tls-connect')); +const { hasFIPS } = require('../common/crypto'); const invalidPfx = fixtures.readKey('cert-without-key.pfx'); @@ -18,6 +19,10 @@ connect({ }, server: keys.agent1 }, common.mustCall((e, pair, cleanup) => { - assert.strictEqual(e.message, 'Unable to load private key from PFX data'); + if (hasFIPS(3)) { + assert.strictEqual(e.code, 'ERR_CRYPTO_UNSUPPORTED_OPERATION'); + } else { + assert.strictEqual(e.message, 'Unable to load private key from PFX data'); + } cleanup(); })); diff --git a/test/parallel/test-tls-key-mismatch.js b/test/parallel/test-tls-key-mismatch.js index 797c7c171dc..8f60ef0520c 100644 --- a/test/parallel/test-tls-key-mismatch.js +++ b/test/parallel/test-tls-key-mismatch.js @@ -27,13 +27,13 @@ if (!common.hasCrypto) { } const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); -const errorMessageRegex = process.features.openssl_is_boringssl ? +const errorMessageRegex = isBoringSSL ? /^Error: error:0b000074:X\.509 certificate routines:OPENSSL_internal:KEY_VALUES_MISMATCH$/ : - hasOpenSSL3 ? + hasOpenSSL(3) ? /^Error: error:05800074:x509 certificate routines::key values mismatch$/ : /^Error: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch$/; diff --git a/test/parallel/test-tls-keylog-sync-write.js b/test/parallel/test-tls-keylog-sync-write.js new file mode 100644 index 00000000000..5e806ae62e7 --- /dev/null +++ b/test/parallel/test-tls-keylog-sync-write.js @@ -0,0 +1,55 @@ +'use strict'; + +// The 'keylog' event is emitted from the TLS library's own stack, part way +// through the handshake. Writing to the socket from the handler must not +// corrupt the connection; the data must arrive intact. + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); + +const PAYLOAD = 'from-keylog'; + +const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), +}, common.mustCall((socket) => { + socket.on('error', common.mustNotCall()); + + const onPayload = common.mustCall(() => { + assert.strictEqual(received, PAYLOAD); + socket.end(); + server.close(); + }); + + let received = ''; + socket.on('data', (data) => { + received += data; + if (received.length >= PAYLOAD.length) onPayload(); + }); +})); + +server.on('tlsClientError', common.mustNotCall()); + +server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + }); + + // 'keylog' fires once per secret derived, so the count is version dependent. + // Write from the first one only, to keep what the server expects exact. + let written = false; + client.on('keylog', common.mustCallAtLeast(() => { + if (written) return; + written = true; + client.write(PAYLOAD, common.mustCall()); + })); + + client.on('error', common.mustNotCall()); +})); diff --git a/test/parallel/test-tls-legacy-pfx.js b/test/parallel/test-tls-legacy-pfx.js index 5106217718d..77f9c093502 100644 --- a/test/parallel/test-tls-legacy-pfx.js +++ b/test/parallel/test-tls-legacy-pfx.js @@ -4,9 +4,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { common.skip('OpenSSL legacy failures are only testable with OpenSSL 3+'); } diff --git a/test/parallel/test-tls-max-send-fragment.js b/test/parallel/test-tls-max-send-fragment.js index 2e319fcdaea..d389cf50d3d 100644 --- a/test/parallel/test-tls-max-send-fragment.js +++ b/test/parallel/test-tls-max-send-fragment.js @@ -20,7 +20,9 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); if (!common.hasCrypto) @@ -64,7 +66,7 @@ const server = tls.createServer({ // both out-of-range values and reports success, so assert that difference // explicitly instead of using a truthiness shortcut. const acceptsOutOfRangeFragmentSize = - process.features.openssl_is_boringssl; + isBoringSSL; assert.strictEqual(c.setMaxSendFragment(511), acceptsOutOfRangeFragmentSize); assert.strictEqual(c.setMaxSendFragment(16385), diff --git a/test/parallel/test-tls-min-max-version.js b/test/parallel/test-tls-min-max-version.js index 88dce9f4b8e..932ad7f0f21 100644 --- a/test/parallel/test-tls-min-max-version.js +++ b/test/parallel/test-tls-min-max-version.js @@ -5,15 +5,17 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -if (process.features.openssl_is_boringssl) { +const { + isBoringSSL, + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').testLegacyProtocolUnsupported(); return; } -const { - hasOpenSSL, - hasOpenSSL3, -} = require('../common/crypto'); const fixtures = require('../common/fixtures'); const { inspect } = require('util'); @@ -29,8 +31,24 @@ const DEFAULT_MAX_VERSION = tls.DEFAULT_MAX_VERSION; function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { assert(proto || cerr || serr, 'test missing any expectations'); + const legacyProtocols = new Set([ + 'TLSv1', + 'TLSv1.1', + 'TLSv1_method', + 'TLSv1_1_method', + ]); + const expectedLegacyProtocol = proto === 'TLSv1' || proto === 'TLSv1.1'; + const legacyOnlyConfiguration = [cprot, sprot, cmax, smax] + .some((value) => legacyProtocols.has(value)); + const fipsLegacyFailure = hasFIPS(3) && + (expectedLegacyProtocol || (!proto && legacyOnlyConfiguration)); + + if (hasFIPS(3) && expectedLegacyProtocol) { + proto = undefined; + } + let ciphers; - if (hasOpenSSL3 && (proto === 'TLSv1' || proto === 'TLSv1.1' || + if (hasOpenSSL(3) && (proto === 'TLSv1' || proto === 'TLSv1.1' || proto === 'TLSv1_1_method' || proto === 'TLSv1_method' || sprot === 'TLSv1_1_method' || sprot === 'TLSv1_method')) { if (serr !== 'ERR_SSL_UNSUPPORTED_PROTOCOL') @@ -65,6 +83,27 @@ function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { console.log('test:', u(cmin), u(cmax), u(cprot), u(smin), u(smax), u(sprot), u(ciphers), 'expect', u(proto), u(cerr), u(serr)); console.log(' ', where); + if (fipsLegacyFailure) { + const errors = [pair.client.err, pair.server.err].filter(Boolean); + assert(errors.length > 0); + const expectedCodes = new Set([ + 'ERR_SSL_NO_PROTOCOLS_AVAILABLE', + 'ERR_SSL_NO_SUITABLE_DIGEST_ALGORITHM', + 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE', + 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', + 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNEXPECTED_MESSAGE', + 'ERR_SSL_UNSUPPORTED_PROTOCOL', + 'ERR_SSL_VERSION_TOO_LOW', + 'ERR_SSL_WRONG_VERSION_NUMBER', + ]); + if (hasFIPS(4)) + expectedCodes.add('ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE'); + for (const error of errors) + assert(expectedCodes.has(error.code), error); + return cleanup(); + } + if (!proto) { console.log('client', pair.client.err ? pair.client.err.code : undefined); console.log('server', pair.server.err ? pair.server.err.code : undefined); @@ -139,9 +178,9 @@ test(U, U, 'TLS_method', U, U, 'TLSv1_method', 'TLSv1'); // OpenSSL 1.1.1 and 3.0 use a different error code and alert (sent to the // client) when no protocols are enabled on the server. -const NO_PROTOCOLS_AVAILABLE_SERVER = hasOpenSSL3 ? +const NO_PROTOCOLS_AVAILABLE_SERVER = hasOpenSSL(3) ? 'ERR_SSL_NO_PROTOCOLS_AVAILABLE' : 'ERR_SSL_INTERNAL_ERROR'; -const NO_PROTOCOLS_AVAILABLE_SERVER_ALERT = hasOpenSSL3 ? +const NO_PROTOCOLS_AVAILABLE_SERVER_ALERT = hasOpenSSL(3) ? 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION' : 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR'; // SSLv23 also means "any supported protocol" greater than the default diff --git a/test/parallel/test-tls-multi-key.js b/test/parallel/test-tls-multi-key.js index 0a9c6f108bf..cc52eb9e65d 100644 --- a/test/parallel/test-tls-multi-key.js +++ b/test/parallel/test-tls-multi-key.js @@ -27,7 +27,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +const { isBoringSSL, hasFIPS } = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').assertMultiKeyUnsupported(); return; } @@ -143,6 +145,17 @@ test({ }); function test(options) { + if (hasFIPS(3) && options.pfx) { + const serverOptions = { ...options }; + delete serverOptions.rsaCN; + delete serverOptions.eccCN; + delete serverOptions.client; + assert.throws(() => tls.createServer(serverOptions), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + return; + } + const rsaCN = options.rsaCN || 'agent1'; const eccCN = options.eccCN || 'agent2'; const clientTrustRoots = options.client.ca; diff --git a/test/parallel/test-tls-multi-pfx.js b/test/parallel/test-tls-multi-pfx.js index fec697cd3b7..d8a533049b2 100644 --- a/test/parallel/test-tls-multi-pfx.js +++ b/test/parallel/test-tls-multi-pfx.js @@ -3,7 +3,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +const { isBoringSSL, hasFIPS } = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').testMultiPfxSelectionDifference(); return; } @@ -11,8 +13,10 @@ if (process.features.openssl_is_boringssl) { const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); +const fips4 = hasFIPS(4); -const options = { +const legacyOptions = { pfx: [ { buf: fixtures.readKey('agent1.pfx'), @@ -22,6 +26,40 @@ const options = { ] }; +if (fips3) { + assert.throws(() => tls.createServer(legacyOptions), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + + if (!hasFIPS(3, 5)) { + return; + } +} + +const fipsPfx = [ + { + buf: fixtures.readKey('agent1-fips.pfx'), + passphrase: 'password', + }, + { + buf: fixtures.readKey('ec-fips.pfx'), + passphrase: 'password', + }, +]; + +if (fips4) { + for (const { buf } of fipsPfx) { + assert.throws(() => tls.createServer({ + pfx: buf, + passphrase: 'sample', + }), { + message: 'password strength too weak', + }); + } +} + +const options = fips3 ? { pfx: fipsPfx } : legacyOptions; + const ciphers = []; const server = tls.createServer(options, function(conn) { diff --git a/test/parallel/test-tls-no-cert-required.js b/test/parallel/test-tls-no-cert-required.js index 499ab2dfd14..36da97526b6 100644 --- a/test/parallel/test-tls-no-cert-required.js +++ b/test/parallel/test-tls-no-cert-required.js @@ -20,7 +20,9 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -28,7 +30,7 @@ const assert = require('assert'); const tls = require('tls'); // Omitting the cert or pfx option to tls.createServer() should not throw. -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { // AECDH-NULL-SHA is a no-authentication/no-encryption cipher and hence // does not need a certificate. BoringSSL does not provide that anonymous // cipher suite, so only this cipher-specific no-cert case is skipped. diff --git a/test/parallel/test-tls-options-boolean-check.js b/test/parallel/test-tls-options-boolean-check.js index f7dd7bb102f..4366502aacb 100644 --- a/test/parallel/test-tls-options-boolean-check.js +++ b/test/parallel/test-tls-options-boolean-check.js @@ -1,6 +1,8 @@ 'use strict'; const common = require('../common'); + +const { isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); if (!common.hasCrypto) @@ -41,7 +43,7 @@ const certDataView = toDataView(certBuff); const caArrDataView = toDataView(caCert); function filterBoringSSLKeyCertArrayCases(options, setName) { - if (!process.features.openssl_is_boringssl) + if (!isBoringSSL) return options; // The array-valued cases exercise multi-identity key/cert handling. diff --git a/test/parallel/test-tls-passphrase.js b/test/parallel/test-tls-passphrase.js index 4372da249bb..6e1b50c0bb1 100644 --- a/test/parallel/test-tls-passphrase.js +++ b/test/parallel/test-tls-passphrase.js @@ -26,6 +26,7 @@ if (!common.hasCrypto) const assert = require('assert'); const tls = require('tls'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); const passKey = fixtures.readKey('rsa_private_encrypted.pem'); @@ -224,6 +225,10 @@ server.listen(0, common.mustCall(function() { })).unref(); const errMessageDecrypt = /bad[ _]decrypt/i; +// TLS supplies an empty password when the passphrase is omitted. OpenSSL 4 +// FIPS rejects it during PBKDF2 password-length checks, before decryption. +const missingPassphraseError = hasFIPS(4) ? + { code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK' } : errMessageDecrypt; // Missing passphrase assert.throws(function() { @@ -233,7 +238,7 @@ assert.throws(function() { cert: cert, rejectUnauthorized: false }); -}, errMessageDecrypt); +}, missingPassphraseError); assert.throws(function() { tls.connect({ @@ -242,7 +247,7 @@ assert.throws(function() { cert: cert, rejectUnauthorized: false }); -}, errMessageDecrypt); +}, missingPassphraseError); assert.throws(function() { tls.connect({ @@ -251,14 +256,14 @@ assert.throws(function() { cert: cert, rejectUnauthorized: false }); -}, errMessageDecrypt); +}, missingPassphraseError); // Invalid passphrase assert.throws(function() { tls.connect({ port: server.address().port, key: passKey, - passphrase: 'invalid', + passphrase: 'wrong-password', cert: cert, rejectUnauthorized: false }); @@ -268,7 +273,7 @@ assert.throws(function() { tls.connect({ port: server.address().port, key: [passKey], - passphrase: 'invalid', + passphrase: 'wrong-password', cert: cert, rejectUnauthorized: false }); @@ -278,7 +283,7 @@ assert.throws(function() { tls.connect({ port: server.address().port, key: [{ pem: passKey }], - passphrase: 'invalid', + passphrase: 'wrong-password', cert: cert, rejectUnauthorized: false }); @@ -287,7 +292,7 @@ assert.throws(function() { assert.throws(function() { tls.connect({ port: server.address().port, - key: [{ pem: passKey, passphrase: 'invalid' }], + key: [{ pem: passKey, passphrase: 'wrong-password' }], passphrase: 'password', // Valid but unused cert: cert, rejectUnauthorized: false diff --git a/test/parallel/test-tls-pfx-authorizationerror.js b/test/parallel/test-tls-pfx-authorizationerror.js index 53fcc0b16b5..e115eea80fe 100644 --- a/test/parallel/test-tls-pfx-authorizationerror.js +++ b/test/parallel/test-tls-pfx-authorizationerror.js @@ -10,14 +10,38 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); +const { hasFIPS } = require('../common/crypto'); -const pfx = fixtures.readKey('agent1.pfx'); +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips4 = hasFIPS(4); +const pfx = fixtures.readKey(fips35 ? 'agent1-fips.pfx' : 'agent1.pfx'); +const passphrase = fips35 ? 'password' : 'sample'; + +if (fips3) { + assert.throws(() => tls.createServer({ + pfx: fixtures.readKey('agent1.pfx'), + passphrase: 'sample', + }), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + + if (!fips35) { + return; + } + + if (fips4) { + assert.throws(() => tls.createServer({ pfx, passphrase: 'sample' }), { + message: 'password strength too weak', + }); + } +} const server = tls .createServer( { pfx: pfx, - passphrase: 'sample', + passphrase, requestCert: true, rejectUnauthorized: false }, @@ -33,7 +57,7 @@ const server = tls { port: this.address().port, pfx: pfx, - passphrase: 'sample', + passphrase, rejectUnauthorized: false }, common.mustCall(() => { diff --git a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js index 16ae56eef8a..7c2d9804372 100644 --- a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js +++ b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js @@ -10,11 +10,12 @@ // throw exceptions during validation. const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { require('../common/boringssl').testPskTls13Unsupported(); return; } diff --git a/test/parallel/test-tls-psk-circuit.js b/test/parallel/test-tls-psk-circuit.js index c9c93d53350..9cd4a68676e 100644 --- a/test/parallel/test-tls-psk-circuit.js +++ b/test/parallel/test-tls-psk-circuit.js @@ -5,12 +5,13 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -if (process.features.openssl_is_boringssl) { +const { isBoringSSL, hasOpenSSL } = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').testPskTls13Unsupported(); return; } -const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); @@ -69,9 +70,12 @@ test({ psk: USERS.UserA, identity: 'UserA' }, { minVersion: 'TLSv1.3' }); test({ psk: USERS.UserB, identity: 'UserB' }); test({ psk: USERS.UserB, identity: 'UserB' }, { minVersion: 'TLSv1.3' }); // Unrecognized user should fail handshake -const expectedHandshakeErr = hasOpenSSL(4, 0) ? - 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? - 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; +// OpenSSL 4.1 uses the same alert as for an invalid binder when no certificate +// is available: https://github.com/openssl/openssl/pull/31026 +const expectedHandshakeErr = hasOpenSSL(4, 1) ? + 'ERR_SSL_TLSV1_ALERT_DECRYPT_ERROR' : hasOpenSSL(4, 0) ? + 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? + 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; test({ psk: USERS.UserB, identity: 'UserC' }, {}, expectedHandshakeErr); // Recognized user but incorrect secret should fail handshake const expectedIllegalParameterErr = hasOpenSSL(3, 4) ? 'ERR_SSL_TLSV1_ALERT_DECRYPT_ERROR' : diff --git a/test/parallel/test-tls-psk-server.js b/test/parallel/test-tls-psk-server.js index 692550fc1c1..29d164f019d 100644 --- a/test/parallel/test-tls-psk-server.js +++ b/test/parallel/test-tls-psk-server.js @@ -5,7 +5,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -if (process.features.openssl_is_boringssl) { +const { isBoringSSL } = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').testPskTls13Unsupported(); return; } diff --git a/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js b/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js index cca22067a0f..b013d9f02c7 100644 --- a/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js +++ b/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js @@ -1,10 +1,12 @@ 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { require('../common/boringssl').assertOpenSSLSecurityLevelsUnsupported(); return; } diff --git a/test/parallel/test-tls-server-verify.js b/test/parallel/test-tls-server-verify.js index 439e3213103..c7487f593eb 100644 --- a/test/parallel/test-tls-server-verify.js +++ b/test/parallel/test-tls-server-verify.js @@ -26,7 +26,7 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { opensslCli } = require('../common/crypto'); +const { opensslCli, isBoringSSL } = require('../common/crypto'); if (!opensslCli) { common.skip('node compiled without OpenSSL CLI.'); @@ -125,7 +125,7 @@ let testCases = ] }, ]; -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { // Remove the delayed client-certificate verification case. It depends on TLS // renegotiation to request a client certificate after the initial handshake, // but BoringSSL does not support caller-initiated renegotiation. diff --git a/test/parallel/test-tls-session-cache.js b/test/parallel/test-tls-session-cache.js index ae560e56798..54b7eeca2e9 100644 --- a/test/parallel/test-tls-session-cache.js +++ b/test/parallel/test-tls-session-cache.js @@ -26,7 +26,9 @@ if (!common.hasCrypto) { } const { hasOpenSSL, + hasFIPS, opensslCli, + isBoringSSL: commonIsBoringSSL, } = require('../common/crypto'); if (!opensslCli) { @@ -37,7 +39,7 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); const { spawn } = require('child_process'); -const isBoringSSL = process.features.openssl_is_boringssl; +const isBoringSSL = commonIsBoringSSL; doTest({ tickets: false }, function() { doTest({ tickets: true }, function() { @@ -59,7 +61,8 @@ function doTest(testOptions, callback) { secureProtocol: 'TLS_method', // BoringSSL supports the RSA cipher selector, but not OpenSSL's // cipher-string policy command syntax. - ciphers: isBoringSSL ? 'RSA' : 'RSA@SECLEVEL=0' + ciphers: hasFIPS(3) ? 'ECDHE-RSA-AES256-GCM-SHA384' : + (isBoringSSL ? 'RSA' : 'RSA@SECLEVEL=0') }; let requestCount = 0; let resumeCount = 0; @@ -108,8 +111,9 @@ function doTest(testOptions, callback) { server.listen(0, common.mustCall(function() { const args = [ 's_client', - isBoringSSL ? '-tls1_2' : '-tls1', - '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + isBoringSSL || hasFIPS(3) ? '-tls1_2' : '-tls1', + '-cipher', hasFIPS(3) ? 'ECDHE-RSA-AES256-GCM-SHA384' : + (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `localhost:${this.address().port}`, '-servername', 'ohgod', '-key', fixtures.path('keys/rsa_private.pem'), diff --git a/test/parallel/test-tls-set-ciphers-error.js b/test/parallel/test-tls-set-ciphers-error.js index b79bd512ffe..102186f9e39 100644 --- a/test/parallel/test-tls-set-ciphers-error.js +++ b/test/parallel/test-tls-set-ciphers-error.js @@ -7,7 +7,7 @@ if (!common.hasCrypto) const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); { const options = { @@ -21,7 +21,7 @@ const { hasOpenSSL } = require('../common/crypto'); assert.throws(() => tls.createServer(options, common.mustNotCall()), /no[_ ]cipher[_ ]match/i); options.ciphers = 'TLS_not_a_cipher'; - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { tls.createServer(options).close(); } else { assert.throws(() => tls.createServer(options, common.mustNotCall()), diff --git a/test/parallel/test-tls-set-ciphers.js b/test/parallel/test-tls-set-ciphers.js index 82a19bb9e90..57fb3599112 100644 --- a/test/parallel/test-tls-set-ciphers.js +++ b/test/parallel/test-tls-set-ciphers.js @@ -6,10 +6,10 @@ if (!common.hasCrypto) { const { hasOpenSSL, - hasOpenSSL3, + hasFIPS, } = require('../common/crypto'); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { common.skip('missing crypto, or OpenSSL version lower than 3'); } @@ -96,57 +96,83 @@ if (hasOpenSSL(4, 0)) { expectedTLSAlertError = 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE'; } -// Have shared ciphers. -test(U, 'AES256-SHA', 'AES256-SHA'); -test('AES256-SHA', U, 'AES256-SHA'); - -test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); -test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); -test('TLS_AES_256_GCM_SHA384:!TLS_CHACHA20_POLY1305_SHA256', U, 'TLS_AES_256_GCM_SHA384'); - -// Do not have shared ciphers. -test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', - U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); - -test('AES256-SHA', 'AES256-SHA256', U, expectedTLSAlertError, - 'ERR_SSL_NO_SHARED_CIPHER'); -test('AES256-SHA:TLS_AES_256_GCM_SHA384', - 'TLS_CHACHA20_POLY1305_SHA256:AES256-SHA256', - U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); - -// Cipher order ignored, TLS1.3 chosen before TLS1.2. -test('AES256-SHA:TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); -test(U, 'AES256-SHA:TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); - -// Cipher order ignored, TLS1.3 before TLS1.2 and -// cipher suites are not disabled if TLS ciphers are set only -// TODO: maybe these tests should be reworked so maxVersion clamping -// is done explicitly and not implicitly in the test() function -test('AES256-SHA', U, 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); -test(U, 'AES256-SHA', 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); - -// TLS_AES_128_CCM_8_SHA256 & TLS_AES_128_CCM_SHA256 are not enabled by -// default, but work. -// However, for OpenSSL32 AES_128 is not enabled due to the -// default security level -if (!hasOpenSSL(3, 2)) { - test('TLS_AES_128_CCM_8_SHA256', U, - U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', 'ERR_SSL_NO_SHARED_CIPHER'); - - test('TLS_AES_128_CCM_8_SHA256', 'TLS_AES_128_CCM_8_SHA256', - 'TLS_AES_128_CCM_8_SHA256'); +if (hasFIPS(3)) { + const tls12Cipher = 'ECDHE-RSA-AES256-GCM-SHA384'; + + // FIPS-approved TLS 1.2 and TLS 1.3 cipher suites work. + test(U, tls12Cipher, tls12Cipher); + test(tls12Cipher, U, tls12Cipher); + test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); + + // The FIPS provider rejects ChaCha20-Poly1305. + test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + U, expectedTLSAlertError, 'ERR_SSL_NO_CIPHERS_AVAILABLE'); + + // Invalid cipher values are still validated before provider selection. + test(9, tls12Cipher, U, 'ERR_INVALID_ARG_TYPE', U); + test(tls12Cipher, 9, U, U, 'ERR_INVALID_ARG_TYPE'); + test(':', tls12Cipher, U, 'ERR_INVALID_ARG_VALUE', U); + test(tls12Cipher, ':', U, U, 'ERR_INVALID_ARG_VALUE'); + + // Empty and null values continue to select the defaults. + test('TLS_AES_256_GCM_SHA384', '', 'TLS_AES_256_GCM_SHA384'); + test('', 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test(null, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384', null, 'TLS_AES_256_GCM_SHA384'); +} else { + // Have shared ciphers. + test(U, 'AES256-SHA', 'AES256-SHA'); + test('AES256-SHA', U, 'AES256-SHA'); + + test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384:!TLS_CHACHA20_POLY1305_SHA256', U, 'TLS_AES_256_GCM_SHA384'); + + // Do not have shared ciphers. + test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); + + test('AES256-SHA', 'AES256-SHA256', U, expectedTLSAlertError, + 'ERR_SSL_NO_SHARED_CIPHER'); + test('AES256-SHA:TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256:AES256-SHA256', + U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); + + // Cipher order ignored, TLS1.3 chosen before TLS1.2. + test('AES256-SHA:TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); + test(U, 'AES256-SHA:TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + + // Cipher order ignored, TLS1.3 before TLS1.2 and + // cipher suites are not disabled if TLS ciphers are set only + // TODO: maybe these tests should be reworked so maxVersion clamping + // is done explicitly and not implicitly in the test() function + test('AES256-SHA', U, 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); + test(U, 'AES256-SHA', 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); + + // TLS_AES_128_CCM_8_SHA256 & TLS_AES_128_CCM_SHA256 are not enabled by + // default, but work. + // However, for OpenSSL32 AES_128 is not enabled due to the + // default security level + if (!hasOpenSSL(3, 2)) { + test('TLS_AES_128_CCM_8_SHA256', U, + U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', 'ERR_SSL_NO_SHARED_CIPHER'); + + test('TLS_AES_128_CCM_8_SHA256', 'TLS_AES_128_CCM_8_SHA256', + 'TLS_AES_128_CCM_8_SHA256'); + } + + // Invalid cipher values + test(9, 'AES256-SHA', U, 'ERR_INVALID_ARG_TYPE', U); + test('AES256-SHA', 9, U, U, 'ERR_INVALID_ARG_TYPE'); + test(':', 'AES256-SHA', U, 'ERR_INVALID_ARG_VALUE', U); + test('AES256-SHA', ':', U, U, 'ERR_INVALID_ARG_VALUE'); + + // Using '' is synonymous for "use default ciphers" + test('TLS_AES_256_GCM_SHA384', '', 'TLS_AES_256_GCM_SHA384'); + test('', 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + + // Using null should be treated the same as undefined. + test(null, 'AES256-SHA', 'AES256-SHA'); + test('AES256-SHA', null, 'AES256-SHA'); } - -// Invalid cipher values -test(9, 'AES256-SHA', U, 'ERR_INVALID_ARG_TYPE', U); -test('AES256-SHA', 9, U, U, 'ERR_INVALID_ARG_TYPE'); -test(':', 'AES256-SHA', U, 'ERR_INVALID_ARG_VALUE', U); -test('AES256-SHA', ':', U, U, 'ERR_INVALID_ARG_VALUE'); - -// Using '' is synonymous for "use default ciphers" -test('TLS_AES_256_GCM_SHA384', '', 'TLS_AES_256_GCM_SHA384'); -test('', 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); - -// Using null should be treated the same as undefined. -test(null, 'AES256-SHA', 'AES256-SHA'); -test('AES256-SHA', null, 'AES256-SHA'); diff --git a/test/parallel/test-tls-set-default-ca-certificates-recovery.js b/test/parallel/test-tls-set-default-ca-certificates-recovery.js index ea6f98d5686..d4154a2da62 100644 --- a/test/parallel/test-tls-set-default-ca-certificates-recovery.js +++ b/test/parallel/test-tls-set-default-ca-certificates-recovery.js @@ -3,6 +3,7 @@ // This tests error recovery and fallback behavior for tls.setDefaultCACertificates() const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); @@ -27,7 +28,7 @@ function testRecovery(expectedCerts) { { const invalidCert = '-----BEGIN CERTIFICATE-----\nvalid cert content\n-----END CERTIFICATE-----'; assert.throws(() => tls.setDefaultCACertificates([fixtureCert, invalidCert]), { - code: process.features.openssl_is_boringssl ? + code: isBoringSSL ? 'ERR_OSSL_PEM_ASN.1_ENCODING_ROUTINES' : 'ERR_OSSL_PEM_ASN1_LIB', }); diff --git a/test/parallel/test-tls-set-sigalgs.js b/test/parallel/test-tls-set-sigalgs.js index e1bf8b93f8a..ca111e83ef2 100644 --- a/test/parallel/test-tls-set-sigalgs.js +++ b/test/parallel/test-tls-set-sigalgs.js @@ -3,7 +3,7 @@ const common = require('../common'); if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); // Test sigalgs: option for TLS. @@ -41,7 +41,7 @@ function test(csigalgs, ssigalgs, shared_sigalgs, cerr, serr) { assert(pair.client.conn); // BoringSSL's OpenSSL-compatible SSL_get_shared_sigalgs() API always // returns zero, so a successful handshake still reports an empty list. - const expectedSharedSigalgs = process.features.openssl_is_boringssl ? + const expectedSharedSigalgs = isBoringSSL ? [] : shared_sigalgs; assert.deepStrictEqual( @@ -74,7 +74,7 @@ test('RSA-PSS+SHA256:RSA-PSS+SHA512:ECDSA+SHA256', const handshakeErr = hasOpenSSL(4, 0) ? 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; -const noSharedSigalgsErr = process.features.openssl_is_boringssl ? +const noSharedSigalgsErr = isBoringSSL ? 'ERR_SSL_NO_COMMON_SIGNATURE_ALGORITHMS' : 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'; test('RSA-PSS+SHA384', 'ECDSA+SHA256', diff --git a/test/parallel/test-tls-ticket-cluster.js b/test/parallel/test-tls-ticket-cluster.js index f183b53f24c..fa3c23c2a6e 100644 --- a/test/parallel/test-tls-ticket-cluster.js +++ b/test/parallel/test-tls-ticket-cluster.js @@ -20,11 +20,13 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); return; } diff --git a/test/parallel/test-tls-ticket.js b/test/parallel/test-tls-ticket.js index 8316f5e8da8..b6aa7a6de9c 100644 --- a/test/parallel/test-tls-ticket.js +++ b/test/parallel/test-tls-ticket.js @@ -20,7 +20,9 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; + const common = require('../common'); +const { isBoringSSL } = require('../common/crypto'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -30,7 +32,7 @@ const net = require('net'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); -if (process.features.openssl_is_boringssl && +if (isBoringSSL && tls.DEFAULT_MAX_VERSION !== 'TLSv1.2') { require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); return; diff --git a/test/parallel/test-tls-write-error.js b/test/parallel/test-tls-write-error.js index 8a8d820a09c..08b943aab71 100644 --- a/test/parallel/test-tls-write-error.js +++ b/test/parallel/test-tls-write-error.js @@ -5,7 +5,9 @@ if (!common.hasCrypto) const { TestTLSSocket, ccs } = require('../common/tls'); const fixtures = require('../common/fixtures'); +const assert = require('assert'); const https = require('https'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); // Regression test for an use-after-free bug in the TLS implementation that // would occur when `SSL_write()` failed. @@ -18,14 +20,23 @@ const opts = { key: server_key, cert: server_cert, }; +const rejectsClientHello = hasFIPS(3) && !hasFIPS(3, 5); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { opts.ciphers = 'ALL@SECLEVEL=0'; } const server = https.createServer(opts, (req, res) => { res.write('hello'); -}).listen(0, common.mustCall(() => { +}); + +if (rejectsClientHello) { + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_WRONG_SIGNATURE_TYPE'); + })); +} + +server.listen(0, common.mustCall(() => { const client = new TestTLSSocket(server_cert); client.connect({ @@ -37,6 +48,12 @@ const server = https.createServer(opts, (req, res) => { })); client.once('data', common.mustCall((buf) => { + if (rejectsClientHello) { + client.end(); + server.close(); + return; + } + let remaining = buf; do { remaining = client.parseTLSFrame(remaining); diff --git a/test/parallel/test-trace-env.js b/test/parallel/test-trace-env.js index 7a7b80fa4c1..4d1a8165277 100644 --- a/test/parallel/test-trace-env.js +++ b/test/parallel/test-trace-env.js @@ -19,8 +19,8 @@ spawnSyncAndAssert(process.execPath, ['--trace-env', fixtures.path('empty.js')], if (common.hasCrypto) { assert.match(output, /get "NODE_EXTRA_CA_CERTS"/); - const { hasOpenSSL3 } = require('../common/crypto'); - if (hasOpenSSL3) { + const { hasOpenSSL } = require('../common/crypto'); + if (hasOpenSSL(3)) { assert.match(output, /get "OPENSSL_CONF"/); } } diff --git a/test/parallel/test-urlpattern.js b/test/parallel/test-urlpattern.js index 0ba84cae3f8..ae0a0356222 100644 --- a/test/parallel/test-urlpattern.js +++ b/test/parallel/test-urlpattern.js @@ -5,6 +5,20 @@ require('../common'); const assert = require('assert'); const { URLPattern } = require('url'); +{ + assert.deepStrictEqual( + Object.getOwnPropertyDescriptor(URLPattern.prototype, Symbol.toStringTag), + { + configurable: true, + enumerable: false, + value: 'URLPattern', + writable: false, + }); + assert.strictEqual( + Object.prototype.toString.call(new URLPattern()), + '[object URLPattern]'); +} + // Verify that if an error is thrown while accessing any of the // init options, the error is appropriately propagated. assert.throws(() => { diff --git a/test/parallel/test-util-debounce.js b/test/parallel/test-util-debounce.js new file mode 100644 index 00000000000..15d93e61041 --- /dev/null +++ b/test/parallel/test-util-debounce.js @@ -0,0 +1,381 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { createHook } = require('node:async_hooks'); +const { setTimeout } = require('node:timers/promises'); +const { debounce } = require('node:util'); +const { TIMEOUT_MAX } = require('internal/timers'); + +for (const value of [undefined, null, true, 0, 'fn', {}, [], Symbol()]) { + assert.throws(() => debounce(value, 1), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +for (const value of [undefined, null, true, '1', {}, [], Symbol()]) { + assert.throws(() => debounce(() => {}, value), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +for (const value of [-1, 0.5, NaN, Infinity, TIMEOUT_MAX + 1]) { + assert.throws(() => debounce(() => {}, value), { + code: 'ERR_OUT_OF_RANGE', + }); +} + +for (const value of [null, true, 1, 'options', []]) { + assert.throws(() => debounce(() => {}, 1, value), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +assert.throws(() => debounce(() => {}, 1, { rejectOnCancel: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => debounce(() => {}, 1, { leading: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => debounce(() => {}, 1, { signal: {} }), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +{ + const reason = new Error('already aborted'); + assert.throws( + () => debounce(() => {}, 1, { signal: AbortSignal.abort(reason) }), + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); +} + +(async () => { + { + const debounced = debounce(common.mustCall(function(...args) { + assert.strictEqual(this, debounced); + assert.strictEqual(debounced.pending, null); + assert.deepStrictEqual(args, ['last', 2]); + return args[1]; + }), 100); + + assert.strictEqual(typeof debounced.cancel, 'function'); + assert.strictEqual(typeof debounced.flush, 'function'); + assert.strictEqual(typeof debounced.ref, 'function'); + assert.strictEqual(typeof debounced.unref, 'function'); + assert.strictEqual(debounced.unref(), debounced); + assert.strictEqual(debounced.ref(), debounced); + assert.strictEqual(debounced.pending, null); + assert.strictEqual(debounced.pendingCount, 0); + + const first = debounced('first', 1); + const second = debounced('last', 2); + assert(first instanceof Promise); + assert(second instanceof Promise); + assert.notStrictEqual(first, second); + assert.strictEqual(debounced.pending, second); + assert.strictEqual(debounced.pendingCount, 2); + assert.strictEqual(debounced.unref(), debounced); + assert.strictEqual(debounced.ref(), debounced); + + debounced.flush(); + assert.strictEqual(debounced.pending, null); + assert.strictEqual(debounced.pendingCount, 0); + debounced.flush(); + assert.deepStrictEqual(await Promise.all([first, second]), [2, 2]); + } + + { + const values = []; + const debounced = debounce(common.mustCall((value) => { + values.push(value); + return value; + }, 3), 100, { leading: true }); + + const first = debounced(1); + assert.deepStrictEqual(values, [1]); + assert.strictEqual(debounced.pending, null); + assert.strictEqual(debounced.pendingCount, 0); + + // With no trailing call pending, flush does not end the debounce window. + debounced.flush(); + const second = debounced(2); + const third = debounced(3); + assert.deepStrictEqual(values, [1]); + assert.strictEqual(debounced.pending, third); + assert.strictEqual(debounced.pendingCount, 2); + + debounced.flush(); + assert.deepStrictEqual(values, [1, 3]); + assert.deepStrictEqual( + await Promise.all([first, second, third]), + [1, 3, 3], + ); + + const fourth = debounced(4); + assert.deepStrictEqual(values, [1, 3, 4]); + assert.strictEqual(await fourth, 4); + debounced.cancel(); + } + + { + const debounced = debounce(common.mustCall((value) => value), 1, { + leading: true, + }); + assert.strictEqual(await debounced(42), 42); + assert.strictEqual(debounced.pending, null); + await setTimeout(10); + } + + { + let recursive; + const values = []; + const debounced = debounce(common.mustCall((value) => { + values.push(value); + if (value === 1) recursive = debounced(2); + return value; + }, 2), 100); + + const first = debounced(1); + debounced.flush(); + assert.deepStrictEqual(values, [1]); + assert.strictEqual(debounced.pending, recursive); + assert.strictEqual(await first, 1); + + debounced.flush(); + assert.deepStrictEqual(values, [1, 2]); + assert.strictEqual(await recursive, 2); + } + + { + let recursive; + const values = []; + const debounced = debounce(common.mustCall((value) => { + values.push(value); + if (value === 1) recursive = debounced(2); + return value; + }, 2), 100, { leading: true }); + + const first = debounced(1); + assert.deepStrictEqual(values, [1]); + assert.strictEqual(debounced.pending, recursive); + assert.strictEqual(await first, 1); + + debounced.flush(); + assert.deepStrictEqual(values, [1, 2]); + assert.strictEqual(await recursive, 2); + } + + { + let recursive; + const values = []; + const debounced = debounce(common.mustCall((value) => { + values.push(value); + if (value === 2) recursive = debounced(3); + return value; + }, 3), 100, { leading: true }); + + const first = debounced(1); + const second = debounced(2); + debounced.flush(); + assert.deepStrictEqual(values, [1, 2, 3]); + assert.deepStrictEqual( + await Promise.all([first, second, recursive]), + [1, 2, 3], + ); + assert.strictEqual(debounced.pending, null); + debounced.cancel(); + } + + { + const debounced = debounce(common.mustCall(async (value) => { + await Promise.resolve(); + return value; + }), 100); + const result = debounced(42); + debounced.flush(); + assert.strictEqual(await result, 42); + } + + { + const expected = new Error('failure'); + const debounced = debounce(common.mustCall(() => { throw expected; }), 100); + const result = debounced(); + debounced.flush(); + await assert.rejects(result, (error) => error === expected); + } + + { + const debounced = debounce(common.mustNotCall(), 100); + const first = debounced(); + const second = debounced(); + const reason = new Error('cancelled'); + debounced.cancel(reason); + assert.strictEqual(debounced.pending, null); + debounced.cancel(); + await assert.rejects( + first, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + await assert.rejects( + second, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + } + + { + const debounced = debounce(common.mustCall((value) => value), 100, { + rejectOnCancel: true, + }); + const first = debounced(1); + const firstRejection = assert.rejects(first, { + code: 'ABORT_ERR', + name: 'AbortError', + }); + const second = debounced(2); + assert.strictEqual(debounced.pendingCount, 1); + await firstRejection; + debounced.flush(); + assert.strictEqual(await second, 2); + } + + { + const reason = new Error('stop'); + const controller = new AbortController(); + const debounced = debounce(common.mustNotCall(), 100, { + signal: controller.signal, + }); + const result = debounced(); + controller.abort(reason); + await assert.rejects( + result, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + assert.strictEqual(debounced.pending, null); + } + + { + const reason = new Error('stop future calls'); + const controller = new AbortController(); + const debounced = debounce(common.mustNotCall(), 100, { + signal: controller.signal, + }); + controller.abort(reason); + await assert.rejects( + debounced(), + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + await assert.rejects( + debounced(), + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + assert.strictEqual(debounced.pending, null); + assert.strictEqual(debounced.pendingCount, 0); + } + + { + const reason = new Error('abort during leading call'); + const controller = new AbortController(); + const state = {}; + controller.signal.addEventListener('abort', common.mustCall(() => { + state.result = state.debounced(); + })); + state.debounced = debounce(common.mustNotCall(), 100, { + leading: true, + signal: controller.signal, + }); + controller.abort(reason); + await assert.rejects( + state.result, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + } + + { + const reason = new Error('abort during flush'); + const controller = new AbortController(); + const state = {}; + controller.signal.addEventListener('abort', common.mustCall(() => { + state.debounced.flush(); + })); + state.debounced = debounce(common.mustNotCall(), 100, { + signal: controller.signal, + }); + const result = state.debounced(); + controller.abort(reason); + await assert.rejects( + result, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + } + + { + const controller = new AbortController(); + const debounced = debounce(common.mustCall(() => 1), 100, { + signal: controller.signal, + }); + const result = debounced(); + debounced.flush(); + controller.abort(); + assert.strictEqual(await result, 1); + } + + { + const debounced = debounce(common.mustCall((value) => value), 1); + assert.strictEqual(await debounced(42), 42); + } + + { + const debounced = debounce(common.mustCall((value) => value, 2), 100); + const first = debounced(1); + debounced.flush(); + assert.strictEqual(await first, 1); + const second = debounced(2); + debounced.flush(); + assert.strictEqual(await second, 2); + } + + { + function original(first, second) { + return first + second; + } + const debounced = debounce(original, 100); + assert.strictEqual(debounced.name, original.name); + assert.strictEqual(debounced.length, original.length); + } + + { + let timeoutCount = 0; + const hook = createHook({ + init(_asyncId, type) { + if (type === 'Timeout') timeoutCount++; + }, + }); + const debounced = debounce(common.mustCall((value) => value), 100); + hook.enable(); + const first = debounced(1); + const second = debounced(2); + const third = debounced(3); + hook.disable(); + assert.strictEqual(timeoutCount, 1); + debounced.flush(); + assert.deepStrictEqual(await Promise.all([first, second, third]), [3, 3, 3]); + } + + { + const debounced = debounce(common.mustNotCall(), 100); + debounced(); + const pending = debounced(); + const { promise, resolve } = Promise.withResolvers(); + process.once('unhandledRejection', common.mustCall((error, unhandled) => { + assert.strictEqual(error.code, 'ABORT_ERR'); + assert.strictEqual(unhandled, pending); + resolve(); + })); + debounced.cancel(); + await promise; + } + + // Give canceled timers time to expose any accidental extra invocation. + await setTimeout(110); +})().then(common.mustCall()); diff --git a/test/parallel/test-util-throttle-timing.js b/test/parallel/test-util-throttle-timing.js new file mode 100644 index 00000000000..56b6c7716a9 --- /dev/null +++ b/test/parallel/test-util-throttle-timing.js @@ -0,0 +1,170 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { mock } = require('node:test'); +const { setImmediate } = require('node:timers/promises'); +const { internalBinding } = require('internal/test/binding'); + +// Throttle uses the libuv clock as well as timers. Control both before loading +// the implementation, which captures setTimeout and clearTimeout. +mock.timers.enable({ apis: ['Date', 'setTimeout'] }); +mock.method(internalBinding('timers'), 'getLibuvNow', () => Date.now()); +const { throttle } = require('node:util'); + +process.on('unhandledRejection', common.mustNotCall()); + +(async () => { + { + const values = []; + const times = []; + const start = Date.now(); + const throttled = throttle(common.mustCall(function(value) { + assert.strictEqual(this, throttled); + values.push(value); + times.push(Date.now() - start); + return value; + }, 5), 2, 40); + + assert.strictEqual(typeof throttled.cancel, 'function'); + assert.strictEqual(typeof throttled.hasImmediateCapacity, 'function'); + assert.strictEqual(typeof throttled.ref, 'function'); + assert.strictEqual(typeof throttled.unref, 'function'); + assert.strictEqual(throttled.pending, null); + assert.strictEqual(throttled.pendingCount, 0); + assert.strictEqual(throttled.activeCount, 0); + assert.strictEqual(throttled.hasImmediateCapacity(), true); + + const first = throttled(1); + const second = throttled(2); + const third = throttled(3); + const fourth = throttled(4); + const fifth = throttled(5); + + assert(first instanceof Promise); + assert(second instanceof Promise); + assert.notStrictEqual(first, second); + assert.deepStrictEqual(values, [1, 2]); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + assert.strictEqual(throttled.pending, fifth); + assert.strictEqual(throttled.pendingCount, 3); + assert.strictEqual(throttled.unref(), throttled); + assert.strictEqual(throttled.ref(), throttled); + + mock.timers.tick(39); + assert.deepStrictEqual(values, [1, 2]); + mock.timers.tick(1); + assert.deepStrictEqual(values, [1, 2, 3, 4]); + assert.strictEqual(throttled.pendingCount, 1); + mock.timers.tick(39); + assert.deepStrictEqual(values, [1, 2, 3, 4]); + mock.timers.tick(1); + + assert.deepStrictEqual( + await Promise.all([first, second, third, fourth, fifth]), + [1, 2, 3, 4, 5], + ); + assert.deepStrictEqual(values, [1, 2, 3, 4, 5]); + assert.strictEqual(throttled.pending, null); + assert.strictEqual(throttled.pendingCount, 0); + assert.strictEqual(throttled.activeCount, 0); + assert.deepStrictEqual(times, [0, 0, 40, 40, 80]); + } + + { + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + return value; + }, 2), 1, 20, { maxPending: 1 }); + const first = throttled(1); + const second = throttled(2); + const dropped = throttled(3); + + assert.deepStrictEqual(values, [1]); + assert.strictEqual(throttled.pendingCount, 1); + await setImmediate(); + await assert.rejects(dropped, { code: 'ERR_THROTTLED' }); + mock.timers.tick(19); + assert.deepStrictEqual(values, [1]); + mock.timers.tick(1); + assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]); + assert.deepStrictEqual(values, [1, 2]); + } + + { + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + return value; + }, 3), 2, 30, { overflow: 'drop' }); + const first = throttled(1); + const second = throttled(2); + await assert.rejects(throttled(3), { code: 'ERR_THROTTLED' }); + assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]); + + mock.timers.tick(29); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + await assert.rejects(throttled(4), { code: 'ERR_THROTTLED' }); + mock.timers.tick(1); + assert.strictEqual(throttled.hasImmediateCapacity(), true); + assert.strictEqual(await throttled(5), 5); + assert.deepStrictEqual(values, [1, 2, 5]); + } + + // A rolling window releases one slot at a time when timers run on schedule. + // If dispatch is delayed until both slots expire, both calls may run together. + for (const delayed of [false, true]) { + const times = []; + const start = Date.now(); + const throttled = throttle(common.mustCall((value) => { + times.push(Date.now() - start); + return value; + }, 4), 2, 80, { strict: true }); + + const first = throttled(1); + mock.timers.tick(40); + const second = throttled(2); + const third = throttled(3); + const fourth = throttled(4); + + mock.timers.tick(39); + assert.deepStrictEqual(times, [0, 40]); + if (delayed) { + mock.timers.tick(41); + } else { + mock.timers.tick(1); + assert.deepStrictEqual(times, [0, 40, 80]); + mock.timers.tick(39); + assert.deepStrictEqual(times, [0, 40, 80]); + mock.timers.tick(1); + } + + assert.deepStrictEqual( + await Promise.all([first, second, third, fourth]), + [1, 2, 3, 4], + ); + assert.deepStrictEqual(times, delayed ? [0, 40, 120, 120] : [0, 40, 80, 120]); + } + + { + let recursive; + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + if (value === 1) recursive = throttled(2); + return value; + }, 2), 1, 20); + + const first = throttled(1); + assert.deepStrictEqual(values, [1]); + assert.strictEqual(throttled.pending, recursive); + assert.strictEqual(throttled.pendingCount, 1); + mock.timers.tick(19); + assert.deepStrictEqual(values, [1]); + mock.timers.tick(1); + assert.deepStrictEqual(await Promise.all([first, recursive]), [1, 2]); + assert.deepStrictEqual(values, [1, 2]); + } +})().then(common.mustCall()).finally(() => mock.reset()); diff --git a/test/parallel/test-util-throttle.js b/test/parallel/test-util-throttle.js new file mode 100644 index 00000000000..b22fb4630e7 --- /dev/null +++ b/test/parallel/test-util-throttle.js @@ -0,0 +1,345 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { createHook } = require('node:async_hooks'); +const { setImmediate } = require('node:timers/promises'); +const { throttle } = require('node:util'); +const { TIMEOUT_MAX } = require('internal/timers'); + +process.on('unhandledRejection', common.mustNotCall()); + +for (const value of [undefined, null, true, 0, 'fn', {}, [], Symbol()]) { + assert.throws(() => throttle(value, 1, 1), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +for (const value of [undefined, null, true, '1', {}, [], Symbol()]) { + assert.throws(() => throttle(() => {}, value, 1), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +for (const value of [-1, 0, 0.5, NaN, Infinity]) { + assert.throws(() => throttle(() => {}, value, 1), { + code: 'ERR_OUT_OF_RANGE', + }); +} + +for (const value of [undefined, null, true, '1', {}, [], Symbol()]) { + assert.throws(() => throttle(() => {}, 1, value), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +for (const value of [-1, 0.5, NaN, Infinity, TIMEOUT_MAX + 1]) { + assert.throws(() => throttle(() => {}, 1, value), { + code: 'ERR_OUT_OF_RANGE', + }); +} + +for (const value of [null, true, 1, 'options', []]) { + assert.throws(() => throttle(() => {}, 1, 1, value), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +assert.throws(() => throttle(() => {}, 1, 1, { signal: {} }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => throttle(() => {}, 1, 1, { strict: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +for (const value of [null, true, 0, 'invalid', {}, []]) { + assert.throws(() => throttle(() => {}, 1, 1, { overflow: value }), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} + +for (const option of ['concurrency', 'maxPending']) { + for (const value of [null, true, '1', {}, [], Symbol()]) { + assert.throws(() => throttle(() => {}, 1, 1, { [option]: value }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } +} + +for (const value of [-1, 0, 0.5, NaN, -Infinity]) { + assert.throws(() => throttle(() => {}, 1, 1, { concurrency: value }), { + code: 'ERR_OUT_OF_RANGE', + }); +} + +for (const value of [-1, 0.5, NaN, -Infinity]) { + assert.throws(() => throttle(() => {}, 1, 1, { maxPending: value }), { + code: 'ERR_OUT_OF_RANGE', + }); +} + +// Explicit Infinity values are accepted. +throttle(() => {}, 1, 1, { + concurrency: Infinity, + maxPending: Infinity, +}); + +{ + const reason = new Error('already aborted'); + assert.throws( + () => throttle(() => {}, 1, 1, { + signal: AbortSignal.abort(reason), + }), + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); +} + +// Keep windows open across synchronous assertions, even if the process is +// descheduled. Window expiration is covered in test-util-throttle-timing.js. +(async () => { + { + let running = 0; + let maxRunning = 0; + let timeoutCount = 0; + const releases = []; + const values = []; + const hook = createHook({ + init(_asyncId, type) { + if (type === 'Timeout') timeoutCount++; + }, + }); + const throttled = throttle(common.mustCall((value) => { + running++; + maxRunning = Math.max(maxRunning, running); + values.push(value); + const { promise, resolve } = Promise.withResolvers(); + releases.push(() => { + running--; + resolve(value); + }); + return promise; + }, 4), 10, 1_000, { concurrency: 2 }); + + hook.enable(); + const calls = [ + throttled(1), + throttled(2), + throttled(3), + throttled(4), + ]; + hook.disable(); + + assert.deepStrictEqual(values, [1, 2]); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + assert.strictEqual(throttled.activeCount, 2); + assert.strictEqual(throttled.pendingCount, 2); + assert.strictEqual(timeoutCount, 0); + + releases[0](); + await setImmediate(); + assert.deepStrictEqual(values, [1, 2, 3]); + assert.strictEqual(throttled.activeCount, 2); + assert.strictEqual(throttled.pendingCount, 1); + + releases[1](); + await setImmediate(); + assert.deepStrictEqual(values, [1, 2, 3, 4]); + assert.strictEqual(throttled.activeCount, 2); + assert.strictEqual(throttled.pendingCount, 0); + + releases[2](); + releases[3](); + assert.deepStrictEqual(await Promise.all(calls), [1, 2, 3, 4]); + assert.strictEqual(throttled.activeCount, 0); + assert.strictEqual(maxRunning, 2); + } + + { + const deferred = Promise.withResolvers(); + const throttled = throttle(common.mustCall(() => deferred.promise), 10, 100, { + concurrency: 1, + overflow: 'drop', + }); + const first = throttled(); + const dropped = throttled(); + + assert.strictEqual(throttled.activeCount, 1); + assert.strictEqual(throttled.pendingCount, 0); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + await setImmediate(); + await assert.rejects(dropped, { code: 'ERR_THROTTLED' }); + deferred.resolve('result'); + assert.strictEqual(await first, 'result'); + assert.strictEqual(throttled.activeCount, 0); + } + + { + let timeoutCount = 0; + const hook = createHook({ + init(_asyncId, type) { + if (type === 'Timeout') timeoutCount++; + }, + }); + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + return value; + }, 3), 2, TIMEOUT_MAX, { overflow: 'drop' }); + + hook.enable(); + const first = throttled(1); + const second = throttled(2); + const dropped = throttled(3); + hook.disable(); + + assert.deepStrictEqual(values, [1, 2]); + assert.strictEqual(timeoutCount, 0); + assert.strictEqual(throttled.pending, null); + assert.strictEqual(throttled.pendingCount, 0); + await setImmediate(); + await assert.rejects(dropped, { code: 'ERR_THROTTLED' }); + assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]); + + throttled.cancel(); + assert.strictEqual(await throttled(4), 4); + assert.deepStrictEqual(values, [1, 2, 4]); + } + + { + const reason = new Error('cancelled'); + const throttled = throttle(common.mustCall((value) => value, 2), 1, TIMEOUT_MAX); + const first = throttled(1); + const second = throttled(2); + const third = throttled(3); + const secondRejection = assert.rejects( + second, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + const thirdRejection = assert.rejects( + third, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + + throttled.cancel(reason); + assert.strictEqual(throttled.pending, null); + assert.strictEqual(throttled.pendingCount, 0); + assert.strictEqual(await first, 1); + await Promise.all([secondRejection, thirdRejection]); + + // Canceling resets the limiter, so the next call can run immediately. + assert.strictEqual(await throttled(4), 4); + throttled.cancel(); + } + + { + const reason = new Error('stop'); + const controller = new AbortController(); + const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX, { + signal: controller.signal, + }); + const first = throttled(1); + const second = throttled(2); + const secondRejection = assert.rejects( + second, + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + + controller.abort(reason); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + assert.strictEqual(await first, 1); + await secondRejection; + assert.strictEqual(throttled.pending, null); + assert.strictEqual(throttled.pendingCount, 0); + await assert.rejects( + throttled(3), + (error) => error.code === 'ABORT_ERR' && error.cause === reason, + ); + } + + { + const expected = new Error('failure'); + const throttled = throttle(common.mustCall((value) => { + if (value === 2) throw expected; + return value; + }, 2), 1, 20); + + assert.strictEqual(await throttled(1), 1); + await assert.rejects(throttled(2), (error) => error === expected); + } + + { + const expected = new Error('async failure'); + const throttled = throttle(common.mustCall(async () => { + throw expected; + }), 1, 0); + await assert.rejects(throttled(), (error) => error === expected); + } + + { + function original(first, second) { + return first + second; + } + const throttled = throttle(original, 1, 0); + assert.strictEqual(throttled.name, original.name); + assert.strictEqual(throttled.length, original.length); + assert.strictEqual(await throttled(1, 2), 3); + } + + { + const values = []; + const throttled = throttle(common.mustCall((value) => { + values.push(value); + return value; + }, 5), 1, 0); + const calls = []; + for (let i = 0; i < 5; i++) calls.push(throttled(i)); + assert.deepStrictEqual(values, [0, 1, 2, 3, 4]); + assert.deepStrictEqual(await Promise.all(calls), [0, 1, 2, 3, 4]); + } + + { + let timeoutCount = 0; + const hook = createHook({ + init(_asyncId, type) { + if (type === 'Timeout') timeoutCount++; + }, + }); + const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX); + + hook.enable(); + assert.strictEqual(throttled.hasImmediateCapacity(), true); + const call = throttled(1); + assert.strictEqual(throttled.hasImmediateCapacity(), false); + if (throttled.hasImmediateCapacity()) throttled(2); + hook.disable(); + + assert.strictEqual(timeoutCount, 0); + assert.strictEqual(throttled.pendingCount, 0); + assert.strictEqual(await call, 1); + } + + { + let timeoutCount = 0; + const hook = createHook({ + init(_asyncId, type) { + if (type === 'Timeout') timeoutCount++; + }, + }); + const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX); + hook.enable(); + const first = throttled(1); + const second = throttled(2); + const third = throttled(3); + hook.disable(); + const secondRejection = assert.rejects(second, { code: 'ABORT_ERR' }); + const thirdRejection = assert.rejects(third, { code: 'ABORT_ERR' }); + + assert.strictEqual(timeoutCount, 1); + assert.strictEqual(throttled.unref(), throttled); + assert.strictEqual(throttled.ref(), throttled); + throttled.cancel(); + assert.strictEqual(await first, 1); + await Promise.all([secondRejection, thirdRejection]); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js index 316d706e7b7..8ac5125b276 100644 --- a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js +++ b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js @@ -6,10 +6,17 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); -async function test(algorithmName, keyLength, ivLength, format = 'raw') { +async function test( + algorithmName, + keyLength, + ivLength, + format = 'raw', + causeMessage, +) { const key = await subtle.importKey( format, new Uint8Array(keyLength), @@ -21,19 +28,40 @@ async function test(algorithmName, keyLength, ivLength, format = 'raw') { const data = new Uint8Array(32); data.buffer.transfer(); + const expected = causeMessage === undefined ? + { name: 'OperationError' } : + (err) => err.name === 'OperationError' && + err.cause?.message === causeMessage; await assert.rejects( subtle.decrypt({ name: algorithmName, iv: new Uint8Array(ivLength) }, key, data), - { name: 'OperationError' }, + expected, ); } const tests = [ test('AES-GCM', 32, 12), - test('ChaCha20-Poly1305', 32, 12, 'raw-secret'), ]; +if (fips3) { + tests.push(assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(32), + 'ChaCha20-Poly1305', + false, + ['encrypt', 'decrypt']), + { name: 'NotSupportedError' })); +} else { + tests.push(test('ChaCha20-Poly1305', 32, 12, 'raw-secret')); +} + if (hasOpenSSL(3)) { - tests.push(test('AES-OCB', 32, 12, 'raw-secret')); + tests.push(test( + 'AES-OCB', + 32, + 12, + 'raw-secret', + fips3 ? 'error:0308010C:digital envelope routines::unsupported' : undefined)); } Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-constructors.js b/test/parallel/test-webcrypto-constructors.js index 782265edc29..3d13b6c92bb 100644 --- a/test/parallel/test-webcrypto-constructors.js +++ b/test/parallel/test-webcrypto-constructors.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips4 = hasFIPS(4); // Test CryptoKey constructor { @@ -152,19 +154,41 @@ const notSubtle = Reflect.construct(function() {}, [], SubtleCrypto); } { - subtle.importKey( - 'raw', - globalThis.crypto.getRandomValues(new Uint8Array(4)), - 'PBKDF2', - false, - ['deriveKey'], - ).then((key) => { + const keyData = globalThis.crypto.getRandomValues( + new Uint8Array(fips4 ? 8 : 4)); + const importedKeys = [ + subtle.importKey('raw', keyData, 'PBKDF2', false, ['deriveKey']), + ]; + if (fips4) { + importedKeys.push( + subtle.importKey( + 'raw', + globalThis.crypto.getRandomValues(new Uint8Array(4)), + 'PBKDF2', + false, + ['deriveKey'])); + } + + Promise.all(importedKeys).then(async ([key, weakKey]) => { subtle.importKey = common.mustNotCall(); - return subtle.deriveKey({ + if (fips4) { + await assert.rejects(subtle.deriveKey({ + name: 'PBKDF2', + hash: 'SHA-512', + salt: new Uint8Array(), + iterations: 5, + }, weakKey, { + name: 'AES-GCM', + length: 256, + }, true, ['encrypt', 'decrypt']), { name: 'OperationError' }); + } + + await subtle.deriveKey({ name: 'PBKDF2', hash: 'SHA-512', - salt: globalThis.crypto.getRandomValues(new Uint8Array()), - iterations: 5, + salt: globalThis.crypto.getRandomValues( + new Uint8Array(fips4 ? 16 : 0)), + iterations: fips4 ? 1000 : 5, }, key, { name: 'AES-GCM', length: 256 diff --git a/test/parallel/test-webcrypto-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js index 327c6a6f154..5f5f1761a4c 100644 --- a/test/parallel/test-webcrypto-crypto-job-mode.js +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -7,7 +7,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const { types: { isCryptoKey } } = require('util'); const { internalBinding } = require('internal/test/binding'); const { @@ -211,7 +211,7 @@ async function withObjectPrototypeSetters(names, fn) { } } - if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + if (hasOpenSSL(3, 5) || isBoringSSL) { const pair = await subtle.generateKey( { name: 'ML-KEM-768' }, true, diff --git a/test/parallel/test-webcrypto-cryptokey-brand-check.js b/test/parallel/test-webcrypto-cryptokey-brand-check.js index 3fe8aaa181a..9174aebe8f0 100644 --- a/test/parallel/test-webcrypto-cryptokey-brand-check.js +++ b/test/parallel/test-webcrypto-cryptokey-brand-check.js @@ -1,13 +1,10 @@ 'use strict'; -// The four CryptoKey prototype getters (`type`, `extractable`, -// `algorithm`, `usages`) are user-configurable per Web IDL, so they -// can be invoked with an arbitrary `this`. The native callbacks that -// implement them must brand-check their receiver and throw cleanly -// (ERR_INVALID_THIS) rather than crashing the process or returning -// garbage. This test exercises four progressively more hostile -// receiver shapes, including subverting `instanceof` via -// `Symbol.hasInstance`, to make sure the C++ brand check holds. +// CryptoKey prototype getters and methods can be invoked with an +// arbitrary `this`. They must brand-check their receiver and throw +// cleanly (ERR_INVALID_THIS) rather than crashing the process or +// returning garbage. This test exercises invalid receiver shapes, +// including subverting `instanceof` via `Symbol.hasInstance`. // // It also verifies that `util.types.isCryptoKey()` cannot be fooled // by prototype spoofing. @@ -17,7 +14,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('node:assert'); -const { types: { isCryptoKey } } = require('node:util'); +const { inspect, types: { isCryptoKey } } = require('node:util'); const { subtle } = globalThis.crypto; (async () => { @@ -29,22 +26,16 @@ const { subtle } = globalThis.crypto; const CryptoKey = key.constructor; - // Capture the underlying prototype getters once, so that subsequent + // Capture the underlying prototype members once, so that subsequent // tampering with `CryptoKey.prototype` cannot affect what we call. - const getters = { - type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get, - extractable: - Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get, - algorithm: - Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get, - usages: - Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get, - }; + const descriptors = Object.getOwnPropertyDescriptors(CryptoKey.prototype); // Sanity: each getter works on a real CryptoKey. - Object.entries(getters).forEach(([name, getter]) => { - assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`); - }); + for (const name of Reflect.ownKeys(descriptors)) { + const { get } = descriptors[name]; + if (get !== undefined) + Reflect.apply(get, key, []); + } assert.strictEqual(isCryptoKey(key), true); assert.strictEqual(Object.hasOwn(CryptoKey, 'getSlots'), false); const internalProto = Object.getPrototypeOf(key); @@ -54,37 +45,53 @@ const { subtle } = globalThis.crypto; assert.strictEqual(Object.getPrototypeOf(internalProto), CryptoKey.prototype); const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; + const invalidArgType = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }; + + async function assertInvalidReceiver(receiver) { + for (const name of Reflect.ownKeys(descriptors)) { + if (name === 'constructor') continue; + const descriptor = descriptors[name]; + const args = name === inspect.custom ? [0, {}] : []; + for (const kind of ['get', 'set', 'value']) { + const member = descriptor[kind]; + if (typeof member !== 'function') continue; + await assert.rejects( + async () => Reflect.apply(member, receiver, args), + invalidThis, + `CryptoKey.${String(name)} (${kind})`, + ); + } + } + } // Plain object receiver. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call({}), invalidThis); - }); + await assertInvalidReceiver({}); // Null-prototype object receiver. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call({ __proto__: null }), invalidThis); - }); + await assertInvalidReceiver({ __proto__: null }); // Primitive receiver. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(1), invalidThis); - }); + await assertInvalidReceiver(1); // Null. - Object.entries(getters).forEach(([, getter]) => { - // eslint-disable-next-line no-useless-call - assert.throws(() => getter.call(null), invalidThis); - }); + await assertInvalidReceiver(null); // Undefined. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(), invalidThis); - }); + await assertInvalidReceiver(undefined); // Function - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(function() {}), invalidThis); - }); + await assertInvalidReceiver(function() {}); + + const revoked = Proxy.revocable(key, {}); + revoked.revoke(); + for (const receiver of [ + { __proto__: CryptoKey.prototype }, + { __proto__: key }, + new Proxy(key, {}), + revoked.proxy, + ]) { + await assertInvalidReceiver(receiver); + } // Prototype spoofing with InternalCryptoKey.prototype must not pass // util.types.isCryptoKey(). @@ -94,10 +101,10 @@ const { subtle } = globalThis.crypto; assert.strictEqual(isCryptoKey(spoofed), false); await assert.rejects( subtle.sign('HMAC', spoofed, Buffer.from('payload')), - invalidThis); + invalidArgType); await assert.rejects( subtle.exportKey('jwk', spoofed), - invalidThis); + invalidArgType); // Subvert `instanceof CryptoKey` via Symbol.hasInstance, then // invoke the native getters on a forged object. The C++ tag @@ -110,9 +117,7 @@ const { subtle } = globalThis.crypto; const fake = { foo: 'bar' }; assert.strictEqual(fake instanceof CryptoKey, true); assert.strictEqual(isCryptoKey(fake), false); - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(fake), invalidThis); - }); + await assertInvalidReceiver(fake); // Subverted `instanceof` plus a real BaseObject of a different // kind (a Buffer) as the receiver. Without the C++ tag check @@ -120,13 +125,11 @@ const { subtle } = globalThis.crypto; const buf = Buffer.alloc(16); assert.strictEqual(buf instanceof CryptoKey, true); assert.strictEqual(isCryptoKey(buf), false); - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(buf), invalidThis); - }); + await assertInvalidReceiver(buf); // The real CryptoKey continues to work after all of the above. - assert.strictEqual(getters.type.call(key), 'secret'); - assert.strictEqual(getters.extractable.call(key), true); - assert.strictEqual(getters.algorithm.call(key).name, 'HMAC'); - assert.deepStrictEqual(getters.usages.call(key), ['sign']); + assert.strictEqual(descriptors.type.get.call(key), 'secret'); + assert.strictEqual(descriptors.extractable.get.call(key), true); + assert.strictEqual(descriptors.algorithm.get.call(key).name, 'HMAC'); + assert.deepStrictEqual(descriptors.usages.get.call(key), ['sign']); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-clone-transfer.js b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js index 4983e1c0bda..2567ac69454 100644 --- a/test/parallel/test-webcrypto-cryptokey-clone-transfer.js +++ b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js @@ -17,6 +17,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('node:assert'); +const { KeyObject } = require('node:crypto'); const { inspect } = require('node:util'); const { once } = require('node:events'); const { Worker, MessageChannel } = require('node:worker_threads'); @@ -320,6 +321,17 @@ async function checkRsaPssTransferToWorker({ publicKey, privateKey }) { { name: 'AES-GCM', iv }, k, ciphertext); assert.deepStrictEqual(Buffer.from(decrypted), plaintext); } + + const bytes = new Uint8Array(await subtle.exportKey('raw', key)); + const nullPrototypeClone = structuredClone(key); + Object.setPrototypeOf(nullPrototypeClone, null); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey('raw', nullPrototypeClone)), bytes); + const typeGetter = Object.getOwnPropertyDescriptor(key.constructor.prototype, 'type').get; + const customInspect = key[inspect.custom]; + assert.strictEqual(typeGetter.call(nullPrototypeClone), 'secret'); + assert.strictEqual(typeof customInspect.call(nullPrototypeClone, 0, {}), 'string'); + assert.deepStrictEqual(KeyObject.from(structuredClone(key)).export(), Buffer.from(bytes)); } // ECDSA keypair (public extractable, private non-extractable) diff --git a/test/parallel/test-webcrypto-cryptokey-hidden-slots.js b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js index 792a1a59c4c..75b977073da 100644 --- a/test/parallel/test-webcrypto-cryptokey-hidden-slots.js +++ b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js @@ -22,6 +22,7 @@ if (!common.hasCrypto) const assert = require('node:assert'); const { createHmac, + getFips, KeyObject, sign: cryptoSign, verify: cryptoVerify, @@ -50,7 +51,7 @@ common.expectWarning({ const { publicKey: rsaPublicKey } = await subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index ebd1b4683fc..56feda2c7bd 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -13,7 +13,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { createSecretKey } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; function assertSameSet(actual, expected, msg) { @@ -50,6 +50,10 @@ function assertSameSet(actual, expected, msg) { expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] }, ]; + if (hasFIPS(3)) + symmetric.splice(symmetric.findIndex(({ algorithm }) => + algorithm.name === 'ChaCha20-Poly1305'), 1); + if (hasOpenSSL(3)) { symmetric.push({ algorithm: { name: 'AES-OCB', length: 128 }, @@ -107,7 +111,11 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + if (hasFIPS(3)) + asymmetric.splice(asymmetric.findIndex(({ algorithm }) => + algorithm.name === 'X25519'), 1); + + if (hasOpenSSL(3, 5) || isBoringSSL) { asymmetric.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], @@ -289,7 +297,7 @@ function assertSameSet(actual, expected, msg) { assert.deepStrictEqual(imported.usages, ['sign']); })()); - if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + if (hasOpenSSL(3, 5) || isBoringSSL) { // ML-DSA JWK roundtrip. tests.push((async () => { const { privateKey } = await subtle.generateKey( @@ -322,12 +330,17 @@ function assertSameSet(actual, expected, msg) { // ChaCha20-Poly1305 raw-secret import. tests.push((async () => { - const key = await subtle.importKey( + const imported = subtle.importKey( 'raw-secret', new Uint8Array(32), { name: 'ChaCha20-Poly1305' }, true, ['decrypt', 'encrypt', 'decrypt', 'encrypt']); + if (hasFIPS(3)) { + await assert.rejects(imported, { name: 'NotSupportedError' }); + return; + } + const key = await imported; assertSameSet(key.usages, ['encrypt', 'decrypt']); assert.strictEqual(key.usages.length, 2); })()); @@ -477,7 +490,11 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + if (hasFIPS(3)) + jwkPairVectors.splice(jwkPairVectors.findIndex(({ algorithm }) => + algorithm.name === 'X25519'), 1); + + if (hasOpenSSL(3, 5) || isBoringSSL) { jwkPairVectors.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], diff --git a/test/parallel/test-webcrypto-derivebits-argon2.js b/test/parallel/test-webcrypto-derivebits-argon2.js index e2b465ab206..129b716c591 100644 --- a/test/parallel/test-webcrypto-derivebits-argon2.js +++ b/test/parallel/test-webcrypto-derivebits-argon2.js @@ -5,10 +5,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); +if (hasFIPS(3)) + common.skip('Argon2 is not available in FIPS mode'); const assert = require('assert'); const { createSecretKey } = require('crypto'); diff --git a/test/parallel/test-webcrypto-derivebits-cfrg.js b/test/parallel/test-webcrypto-derivebits-cfrg.js index 757c8127253..d2fbb2998ae 100644 --- a/test/parallel/test-webcrypto-derivebits-cfrg.js +++ b/test/parallel/test-webcrypto-derivebits-cfrg.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const kTests = [ { @@ -20,7 +22,7 @@ const kTests = [ }, ]; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { kTests.push( { name: 'X448', @@ -77,6 +79,15 @@ async function prepareKeys() { Object.keys(keys).map(async (name) => { const { size, result, privateKey, publicKey } = keys[name]; + if (rejectsXCurves) { + await assert.rejects( + subtle.deriveBits({ name, public: publicKey }, privateKey, 8 * size), + (err) => err.name === 'OperationError' && + err.cause?.message === + 'error:03000096:digital envelope routines::operation not supported for this keytype'); + return; + } + { // Good parameters const bits = await subtle.deriveBits({ diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index 539440ea7c3..fafd6a7afa5 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; function getDeriveKeyInfo(name, length, hash, ...usages) { @@ -28,7 +28,7 @@ const kDerivedKeyTypes = [ ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ]; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { kDerivedKeyTypes.push( ['HMAC', 256, 'SHA3-256', 'sign', 'verify'], ['HMAC', 256, 'SHA3-384', 'sign', 'verify'], @@ -91,7 +91,7 @@ const kDerivations = { empty: '9e4b719033742101e90f1ad61e2ff3b4' + '256863667296d74389f1f02af2c4e6a6' }, - ...(!process.features.openssl_is_boringssl ? { + ...(!isBoringSSL ? { 'SHA3-256': { normal: '386b0693d7a58c4ddf01b49bfbbd2fa87c6f911991543995170ba20ed28df599', empty: 'd029bc828b6c6c8bb16ce3d25f5058f19c7d2517745e11c5d65c6d242e82e47f', @@ -131,7 +131,7 @@ const kDerivations = { empty: 'c8e12774135305c9147f2cc4766e5ead' + '25d8f457b9a1953d52677361ced558fb' }, - ...(!process.features.openssl_is_boringssl ? { + ...(!isBoringSSL ? { 'SHA3-256': { normal: '9befc557f5baf4075b5fb38c014b41b92ab7534150baf64201069e8807d0e83d', empty: '54d1fa1aa7cad99dab0622b772170e775c103756183bac36a228fd817a98a3f6', @@ -173,7 +173,7 @@ const kDerivations = { empty: 'e579d1f9e7f08e6f990ffcfcce1ed201' + 'c5e37e62cdf606f0ba4aca80427fbc44' }, - ...(!process.features.openssl_is_boringssl ? { + ...(!isBoringSSL ? { 'SHA3-256': { normal: '24f38fd1905554b7cbf8395cc3976292d11ce24a0b3131da0fd4b109832d27e3', empty: '33d0a5151c0f52e4bb7fb67cf7a17063127624dc3e685903f49ebb07872084d1', @@ -213,7 +213,7 @@ const kDerivations = { empty: 'b4f7e7557674d501cbfbc0148ad800c0' + '750189fe295a2aca5e1bf4122c85edf9' }, - ...(!process.features.openssl_is_boringssl ? { + ...(!isBoringSSL ? { 'SHA3-256': { normal: 'fe32459f7339dd2e8df6c6fc874ed9e81e3b7aad669edad9b71196f53ed95b12', empty: '04519be1eb94079c91306cc5b21946b3de6a78ad35ec83d4f4a37bafbda678d7', diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js index 545dcab5849..e1e3da6ce26 100644 --- a/test/parallel/test-webcrypto-derivebits.js +++ b/test/parallel/test-webcrypto-derivebits.js @@ -7,7 +7,11 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const requiresLongHkdfInputs = hasFIPS(3) && !hasFIPS(3, 5); +const rejectsWeakPbkdf2Inputs = hasFIPS(4); +const rejectsXCurves = hasFIPS(3, 5); // This is only a partial test. The WebCrypto Web Platform Tests // will provide much greater coverage. @@ -56,12 +60,20 @@ const { subtle } = globalThis.crypto; } const kTests = [ - ['hello', 'there', 'my friend', 'SHA-256', 512, - '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e329' + - '41ef21bdeb0fa87b6b6bfa5c54466aa5bf76cdc2685fb' + - 'a4408ea5b94c049fe035649b46f92fdc519374'], - ['hello', 'there', 'my friend', 'SHA-384', 128, - 'e36cf2cf943d8f3a88adb80f478745c3'], + [requiresLongHkdfInputs ? 'hello hello hello' : 'hello', + 'there', requiresLongHkdfInputs ? 'my friend indeed' : 'my friend', + 'SHA-256', 512, + requiresLongHkdfInputs ? + 'bc2b7841512a6f4563f723c317909ac305ddbfbdec1daf0055d0587b5db8d635' + + 'a22f97b0dfbcc12dcd2d096123385227b16e95e5bccc0d6751491f38c5e48428' : + '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e329' + + '41ef21bdeb0fa87b6b6bfa5c54466aa5bf76cdc2685fb' + + 'a4408ea5b94c049fe035649b46f92fdc519374'], + [requiresLongHkdfInputs ? 'hello hello hello' : 'hello', + 'there', requiresLongHkdfInputs ? 'my friend indeed' : 'my friend', + 'SHA-384', 128, + requiresLongHkdfInputs ? 'ee2d1d7dc759c26f2ab8ee6d7cfa0c23' : + 'e36cf2cf943d8f3a88adb80f478745c3'], ]; const tests = Promise.all(kTests.map((args) => test(...args))); @@ -88,17 +100,24 @@ const { subtle } = globalThis.crypto; } const kTests = [ - ['hello', 'there', 10, 'SHA-256', 512, - 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7' + - 'ce7678b4cb16fad88098110a83e71f4483ce73203f7a64' + - '719d293280f780f9fafdcf46925c5c0588b3'], - ['hello', 'there', 5, 'SHA-384', 128, - '201509b012c9cd2fbe7ea938f0c509b3'], + ['password', 'there there here', 1000, 'SHA-256', 512, + '8802c34ee684a523f9304a6335394c0a5f02350d51383d' + + '17d3cf89fa0808591ddede3c832fe4691c7f361ade53b9' + + '36bf94347055bcf86fd662abe038fb945d17'], + ['password', 'there there here', 2000, 'SHA-384', 128, + '7c650b88798cea1a390802a6f97e05b0'], ]; const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); + + if (rejectsWeakPbkdf2Inputs) { + assert.rejects( + test('hello', 'there', 10, 'SHA-256', 512), + { name: 'OperationError' }) + .then(common.mustCall()); + } } // Test PBKDF2 rejects iteration counts beyond the native signed int range @@ -145,10 +164,20 @@ const { subtle } = globalThis.crypto; assert.deepStrictEqual(secret1, secret2); } - test('X25519').then(common.mustCall()); - if (!process.features.openssl_is_boringssl) { - test('X448').then(common.mustCall()); + if (rejectsXCurves) { + for (const name of ['X25519', 'X448']) { + assert.rejects( + test(name), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + .then(common.mustCall()); + } } else { - common.printSkipMessage('Skipping unsupported X448 test case'); + test('X25519').then(common.mustCall()); + if (!isBoringSSL) { + test('X448').then(common.mustCall()); + } else { + common.printSkipMessage('Skipping unsupported X448 test case'); + } } } diff --git a/test/parallel/test-webcrypto-derivekey-cfrg.js b/test/parallel/test-webcrypto-derivekey-cfrg.js index c5a5b1f3518..133292cbeff 100644 --- a/test/parallel/test-webcrypto-derivekey-cfrg.js +++ b/test/parallel/test-webcrypto-derivekey-cfrg.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const kTests = [ { @@ -20,7 +22,7 @@ const kTests = [ }, ]; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { kTests.push( { name: 'X448', @@ -80,6 +82,15 @@ async function prepareKeys() { Object.keys(keys).map(async (name) => { const { result, privateKey, publicKey } = keys[name]; + if (rejectsXCurves) { + await assert.rejects( + subtle.deriveKey({ name, public: publicKey }, privateKey, ...otherArgs), + (err) => err.name === 'OperationError' && + err.cause?.message === + 'error:03000096:digital envelope routines::operation not supported for this keytype'); + return; + } + { // Good parameters const key = await subtle.deriveKey({ diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index f9323bca2ca..325c4472d80 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -5,11 +5,13 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const { subtle } = globalThis.crypto; const { KeyObject } = require('crypto'); +const rejectsXCurves = hasFIPS(3, 5); +const fips4 = hasFIPS(4); // This is only a partial test. The WebCrypto Web Platform Tests // will provide much greater coverage. @@ -74,24 +76,24 @@ const { KeyObject } = require('crypto'); } const kTests = [ - ['hello', 'there', 'my friend', 'SHA-1', - '365ca5d3f42d050c74302e420c83975327950f1913a151eecd00526bf52614a0'], - ['hello', 'there', 'my friend', 'SHA-256', - '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e32941ef21bdeb0fa87b6b6'], - ['hello', 'there', 'my friend', 'SHA-384', - 'e36cf2cf943d8f3a88adb80f478745c336ac811b1a86d03a7d10eb0b6b52295c'], - ['hello', 'there', 'my friend', 'SHA-512', - '1e42d43fcacba361716f65853bd5f3c479f679612f0180eab3c51ed6c9d2b47d'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-1', + 'aac1ecdc73147af6a418393da6875bff5f566c0a473e25d54b4dfc3cb7cb2ace'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-256', + 'bc2b7841512a6f4563f723c317909ac305ddbfbdec1daf0055d0587b5db8d635'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-384', + 'ee2d1d7dc759c26f2ab8ee6d7cfa0c2313e82650a4514673c867063dc1849040'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-512', + 'a7abd704d0be364c6d4a530b6f93fcaff95474a2eee5a127ff86c5d095a2a812'], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { kTests.push( - ['hello', 'there', 'my friend', 'SHA3-256', - '2a49a3b6fb219117af9e251c6c65f16600cbca13bd0be6e70d96b0b9fa4cf3fd'], - ['hello', 'there', 'my friend', 'SHA3-384', - '0437bb59b95f2db2c7684c0b439028cb0fdd6f0f5d03b9f489066a87ae147221'], - ['hello', 'there', 'my friend', 'SHA3-512', - '3bbc469d38214371921e52c6f147e96cb7eb370421a81f53dea8b4851dfb8bce'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA3-256', + '89b3751df2ada85322a57ec82f7d0a5c233c6def91c92e681bc5118bd5768dca'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA3-384', + 'b4fa7b9929a595bbaa370eb959b194c1232d5a329abd02a5fa166a1424962fcf'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA3-512', + 'ac5d90a6bc848961e78a491887539b29c532a9c0d0b39cec464df071a63e0061'], ); } else { common.printSkipMessage('Skipping unsupported SHA-3 test cases'); @@ -127,30 +129,36 @@ const { KeyObject } = require('crypto'); } const kTests = [ - ['hello', 'there', 5, 'SHA-1', - 'f8f65a5fd92c9b74916083a7e9b0001c46bc89e2a14c48014cf1e0e1dbabf635'], - ['hello', 'there', 5, 'SHA-256', - '2e575eae24267db32106c7dba01615e5417557e8c5cf33ba15a311cb0c2907ee'], - ['hello', 'there', 5, 'SHA-384', - '201509b012c9cd2fbe7ea938f0c509b36ecb140f38bf9130e96923f55f46756d'], - ['hello', 'there', 5, 'SHA-512', - '2e8d981741f98193e0af9c79870af0e985089341221edad9a130d297eae1984b'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-1', + 'b747604ca226287ccae90d8d8c119645a80d1154625a56b2debb3f9b172eb134'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-256', + '3cc64f6cfcbdb9c42b63b471016f17d1966b70934b4719a12ce95382940252f2'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-384', + '5ce64241beef3a3931dbfac6eef7303b5bdbea13449d4eeb4f89c3e9f9357c65'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-512', + '12790ce09027db067d680670f4dc704715b5120d139e8fde810afc34fb66f9f1'], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { kTests.push( - ['hello', 'there', 5, 'SHA3-256', - '0aed29b61b3ca3978aea34a9793276574ea997b69e8d03727438199f90571649'], - ['hello', 'there', 5, 'SHA3-384', - '7aa4a274aa19b4623c5d3091c4b06355de85ff6f25e53a83e3126cbb86ae68df'], - ['hello', 'there', 5, 'SHA3-512', - '4d909c47a81c625f866d1f9406248e6bc3c7ea89225fbccf1f08820254c9ef56'] + ['hello hello hello', 'my friend indeed', 1000, 'SHA3-256', + '0f69b46660cba27b95215d5676492c64ed6abf6d426669a4a02b0ca3a1c36c11'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA3-384', + 'a2e86a2d4cdf9844d70ae37f71302356ce2b9a899f5d778fc9af64d32e351d70'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA3-512', + '03431052c37d626ae3fc1df582ff2a4d610642fc27e1b8130ca5980c0b0756ac'] ); } else { common.printSkipMessage('Skipping unsupported SHA-3 test cases'); } - const tests = Promise.all(kTests.map((args) => test(...args))); + const promises = kTests.map((args) => test(...args)); + if (fips4) { + promises.push(assert.rejects( + test('hello', 'there', 5, 'SHA-256', ''), + { name: 'OperationError' })); + } + const tests = Promise.all(promises); tests.then(common.mustCall()); } @@ -167,7 +175,7 @@ const { KeyObject } = require('crypto'); [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { vectors.push( [{ name: 'HMAC', hash: 'SHA3-256', length: 256 }, 'sign', 256], [{ name: 'HMAC', hash: 'SHA3-384', length: 384 }, 'sign', 384], @@ -227,7 +235,7 @@ const { KeyObject } = require('crypto'); [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { vectors.push( [{ name: 'HMAC', hash: 'SHA3-256', length: 256 }, 'sign', 256], [{ name: 'HMAC', hash: 'SHA3-384', length: 384 }, 'sign', 384], @@ -254,8 +262,18 @@ const { KeyObject } = require('crypto'); (async () => { for (const [derivedKeyAlgorithm, usage, expected] of vectors) { const derived = await subtle.deriveKey( - { name: 'PBKDF2', salt: new Uint8Array([]), hash: 'SHA-256', iterations: 20 }, - await subtle.importKey('raw', new Uint8Array([]), { name: 'PBKDF2' }, false, ['deriveKey']), + { + name: 'PBKDF2', + salt: new Uint8Array(16), + hash: 'SHA-256', + iterations: 1000, + }, + await subtle.importKey( + 'raw', + new Uint8Array(8), + { name: 'PBKDF2' }, + false, + ['deriveKey']), derivedKeyAlgorithm, false, [usage]); @@ -265,23 +283,33 @@ const { KeyObject } = require('crypto'); })().then(common.mustCall()); } -if (hasOpenSSL(3)) { +if (hasOpenSSL(3) && !hasFIPS()) { (async () => { const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; const usages = ['sign']; for (const [algorithm, baseKeyAlgorithm] of [ [ - { name: 'HKDF', salt: new Uint8Array(), info: new Uint8Array(), hash: 'SHA-256' }, + { + name: 'HKDF', + salt: new Uint8Array(16), + info: new Uint8Array(), + hash: 'SHA-256', + }, { name: 'HKDF' }, ], [ - { name: 'PBKDF2', salt: new Uint8Array(), hash: 'SHA-256', iterations: 20 }, + { + name: 'PBKDF2', + salt: new Uint8Array(16), + hash: 'SHA-256', + iterations: 1000, + }, { name: 'PBKDF2' }, ], ]) { const baseKey = await subtle.importKey( 'raw', - new Uint8Array(), + new Uint8Array(baseKeyAlgorithm.name === 'HKDF' ? 16 : 8), baseKeyAlgorithm, false, ['deriveKey']); @@ -293,11 +321,11 @@ if (hasOpenSSL(3)) { usages); assert.strictEqual(derived.algorithm.length, 0); - const signature = await subtle.sign({ + const signature = subtle.sign({ name: 'KMAC128', outputLength: 256, }, derived, new Uint8Array()); - assert.strictEqual(signature.byteLength, 32); + assert.strictEqual((await signature).byteLength, 32); } })().then(common.mustCall()); } @@ -333,10 +361,20 @@ if (hasOpenSSL(3)) { assert.deepStrictEqual(raw1, raw2); } - test('X25519').then(common.mustCall()); - if (!process.features.openssl_is_boringssl) { - test('X448').then(common.mustCall()); + if (rejectsXCurves) { + for (const name of ['X25519', 'X448']) { + assert.rejects( + test(name), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + .then(common.mustCall()); + } } else { - common.printSkipMessage('Skipping unsupported X448 test case'); + test('X25519').then(common.mustCall()); + if (!isBoringSSL) { + test('X448').then(common.mustCall()); + } else { + common.printSkipMessage('Skipping unsupported X448 test case'); + } } } diff --git a/test/parallel/test-webcrypto-digest-turboshake-rfc.js b/test/parallel/test-webcrypto-digest-turboshake-rfc.js index 271fde76ab2..46220465412 100644 --- a/test/parallel/test-webcrypto-digest-turboshake-rfc.js +++ b/test/parallel/test-webcrypto-digest-turboshake-rfc.js @@ -5,6 +5,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasFIPS } = require('../common/crypto'); + +if (hasFIPS()) + common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode'); + const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-digest-turboshake.js b/test/parallel/test-webcrypto-digest-turboshake.js index a6f4b2d50f9..bd09362caa2 100644 --- a/test/parallel/test-webcrypto-digest-turboshake.js +++ b/test/parallel/test-webcrypto-digest-turboshake.js @@ -5,6 +5,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasFIPS } = require('../common/crypto'); + +if (hasFIPS()) + common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode'); + const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 8e1b6797ee8..47a56a912d6 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -9,7 +9,8 @@ const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); +const fips = hasFIPS(); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -18,7 +19,7 @@ const kTests = [ ['SHA-512', ['sha512'], 512], ]; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { kTests.push( [{ name: 'cSHAKE128', outputLength: 256 }, ['shake128', { outputLength: 256 >> 3 }], 256], [{ name: 'cSHAKE256', outputLength: 512 }, ['shake256', { outputLength: 512 >> 3 }], 512], @@ -149,7 +150,7 @@ const kDigestedData = { '60b22aab8d36a4c2a3affdb71234f49276737c575ddf7' + '4d14054cbd6fdb98fd0ddcbcb46f91ad76b6ee' }, - ...(!process.features.openssl_is_boringssl ? { + ...(!isBoringSSL ? { 'cshake128': { empty: '7f9c2ba4e88f827d616045507605853ed73b8093f6e' + 'fbc88eb1a6eacfa66ef26', @@ -290,6 +291,8 @@ if (getHashes().includes('shake128')) { message: 'Unsupported CShakeParams functionName', }); + if (fips) return; + await assert.rejects( subtle.digest( { @@ -407,7 +410,8 @@ if (getHashes().includes('shake128')) { nistCShakeSample1.data)); const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); assert.strictEqual(truncated.byteLength, expected.byteLength); - assert.deepStrictEqual(truncated.subarray(0, 31), expected.subarray(0, 31)); + assert.deepStrictEqual( + truncated.subarray(0, 31), expected.subarray(0, 31)); assert.strictEqual(truncated[31] & 0b00000001, 0); assert.strictEqual(truncated[31] | 0b00000001, expected[31]); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-encap-decap-ml-kem.js b/test/parallel/test-webcrypto-encap-decap-ml-kem.js index e31be79e6bf..128cb0d9575 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -6,9 +6,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3, 5) && !isBoringSSL) common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); @@ -289,7 +289,7 @@ async function testDecapsulateBits({ name, publicKeyPem, privateKeyPem, results const variations = []; for (const vector of vectors) { - if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + if (isBoringSSL && vector.name === 'ML-KEM-512') { common.printSkipMessage(`Skipping unsupported ${vector.name} test`); continue; } diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js index d7a7dca6584..14cc0127188 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -8,6 +8,7 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { @@ -237,6 +238,14 @@ if (hasOpenSSL(3)) { } = require('../fixtures/crypto/aes_ocb')(); (async function() { + if (getFips() === 1) { + await assert.rejects( + testEncrypt(passing[0]), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + return; + } + const variations = []; passing.forEach((vector) => { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js index 723fd26ea57..45225115fd5 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -6,8 +6,19 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +if (hasFIPS(3)) { + assert.rejects( + subtle.generateKey( + { name: 'ChaCha20-Poly1305' }, + false, + ['encrypt', 'decrypt']), + { name: 'NotSupportedError' }).then(common.mustCall()); + return; +} + async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { // Using a copy of plaintext to prevent tampering of the original plaintext = Buffer.from(plaintext); diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index c4ca52862fe..ae8cd7fcfdb 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -6,7 +6,8 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests @@ -58,7 +59,7 @@ const { subtle } = globalThis.crypto; } // Test Encrypt/Decrypt RSA-OAEP w/ SHA-3 -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); async function test() { @@ -207,7 +208,15 @@ if (hasOpenSSL(3)) { Buffer.from(buf).toString('hex')); } - test().then(common.mustCall()); + if (getFips() === 1) { + assert.rejects( + test(), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + .then(common.mustCall()); + } else { + test().then(common.mustCall()); + } } else { common.printSkipMessage('Skipping unsupported AES-OCB test cases'); } diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index cd71906f7c7..921bd394d2f 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -8,7 +8,9 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const keyData = { 'Ed25519': { @@ -94,7 +96,7 @@ const testVectors = [ }, ]; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { testVectors.push( { name: 'Ed448', @@ -404,9 +406,19 @@ async function testImportRaw({ name, publicUsages }) { for (const extractable of [true, false]) { tests.push(testImportSpki(vector, extractable)); tests.push(testImportPkcs8(vector, extractable)); - tests.push(testImportJwk(vector, extractable)); + if (rejectsXCurves && vector.name.startsWith('X')) { + tests.push(assert.rejects( + testImportJwk(vector, extractable), + { name: 'DataError' })); + } else { + tests.push(testImportJwk(vector, extractable)); + } + } + if (rejectsXCurves && vector.name.startsWith('X')) { + tests.push(assert.rejects(testImportRaw(vector), { name: 'DataError' })); + } else { + tests.push(testImportRaw(vector)); } - tests.push(testImportRaw(vector)); } await Promise.all(tests); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index ee6eaba7a36..440530a52a7 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -5,9 +5,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3, 5) && !isBoringSSL) common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); @@ -104,7 +104,7 @@ async function testImportPkcs8({ name, privateUsages }, extractable) { extractable, privateUsages); } catch (err) { - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { assert.strictEqual(err.name, 'DataError'); assert.strictEqual(err.cause.code, 'ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED'); common.printSkipMessage('Skipping unsupported private key format test'); @@ -522,7 +522,7 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { } })().then(common.mustCall()); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { (async function() { for (const { name, privateUsages } of testVectors) { const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index 0437b9ff198..100d330e0b4 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -5,9 +5,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3, 5) && !isBoringSSL) common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); @@ -104,7 +104,7 @@ async function testImportPkcs8({ name, privateUsages }, extractable) { extractable, privateUsages); } catch (err) { - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { assert.strictEqual(err.name, 'DataError'); assert.strictEqual(err.cause.code, 'ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED'); @@ -455,7 +455,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) (async function() { const tests = []; for (const vector of testVectors) { - if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + if (isBoringSSL && vector.name === 'ML-KEM-512') { common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); continue; } @@ -529,7 +529,7 @@ if (!process.features.openssl_is_boringssl) { } })().then(common.mustCall()); -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { (async function() { for (const { name, privateUsages } of testVectors) { const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); diff --git a/test/parallel/test-webcrypto-export-import-rsa.js b/test/parallel/test-webcrypto-export-import-rsa.js index fee2e910f05..4674b124fc3 100644 --- a/test/parallel/test-webcrypto-export-import-rsa.js +++ b/test/parallel/test-webcrypto-export-import-rsa.js @@ -1,6 +1,8 @@ 'use strict'; const common = require('../common'); + +const { isBoringSSL } = require('../common/crypto'); const fixtures = require('../common/fixtures'); if (!common.hasCrypto) @@ -19,7 +21,7 @@ const hashes = [ 'SHA-512', ]; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { hashes.push( 'SHA3-256', 'SHA3-384', diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index c7399c69d9c..385ae8c65e9 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -6,11 +6,16 @@ const fixtures = require('../common/fixtures'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const { subtle } = globalThis.crypto; -const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto'); +const { + createPrivateKey, + createPublicKey, + createSecretKey, + getFips, +} = require('crypto'); { async function test() { @@ -281,66 +286,68 @@ if (hasOpenSSL(3)) { { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); { - const importedZeroImplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - name, - true, - ['sign', 'verify']); - const importedZeroImplicitRaw = - await subtle.exportKey('raw-secret', importedZeroImplicit); - assert.strictEqual(importedZeroImplicit.algorithm.length, 0); - assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); + if (getFips() !== 1) { + const importedZeroImplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array(), + name, + true, + ['sign', 'verify']); + const importedZeroImplicitRaw = + await subtle.exportKey('raw-secret', importedZeroImplicit); + assert.strictEqual(importedZeroImplicit.algorithm.length, 0); + assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); - const importedZeroExplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - { name, length: 0 }, - true, - ['sign', 'verify']); - const importedZeroExplicitRaw = - await subtle.exportKey('raw-secret', importedZeroExplicit); - assert.strictEqual(importedZeroExplicit.algorithm.length, 0); - assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); - - await assert.rejects( - subtle.importKey( + const importedZeroExplicit = await subtle.importKey( 'raw-secret', - new Uint8Array([0xff]), + new Uint8Array(), { name, length: 0 }, true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); - - const generated = await subtle.generateKey( - { name, length: 9 }, - true, - ['sign', 'verify']); - const generatedRaw = await subtle.exportKey('raw-secret', generated); - assert.strictEqual(generated.algorithm.length, 9); - assert.strictEqual(generatedRaw.byteLength, 2); - assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + ['sign', 'verify']); + const importedZeroExplicitRaw = + await subtle.exportKey('raw-secret', importedZeroExplicit); + assert.strictEqual(importedZeroExplicit.algorithm.length, 0); + assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 0 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + + const generated = await subtle.generateKey( + { name, length: 9 }, + true, + ['sign', 'verify']); + const generatedRaw = await subtle.exportKey('raw-secret', generated); + assert.strictEqual(generated.algorithm.length, 9); + assert.strictEqual(generatedRaw.byteLength, 2); + assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); - const importedExplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff]), - { name, length: 9 }, - true, - ['sign', 'verify']); - const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); - assert.strictEqual(importedExplicit.algorithm.length, 9); - assert.deepStrictEqual( - new Uint8Array(importedExplicitRaw), - new Uint8Array([0xff, 0x80])); - - await assert.rejects( - subtle.importKey( + const importedExplicit = await subtle.importKey( 'raw-secret', - new Uint8Array([0xff]), + new Uint8Array([0xff, 0xff]), { name, length: 9 }, true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); + ['sign', 'verify']); + const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); + assert.strictEqual(importedExplicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(importedExplicitRaw), + new Uint8Array([0xff, 0x80])); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 9 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + } } } @@ -397,7 +404,7 @@ if (hasOpenSSL(3)) { async function test() { const { publicKey, privateKey } = await subtle.generateKey({ name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-384' }, true, ['sign', 'verify']); @@ -511,7 +518,7 @@ if (hasOpenSSL(3)) { } // SHA-3 hashes and JWK "alg" -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { const rsa = fixtures.readKey('rsa_private_2048.pem'); const privateKey = createPrivateKey(rsa); const publicKey = createPublicKey(privateKey); diff --git a/test/parallel/test-webcrypto-fips-exceptions.mjs b/test/parallel/test-webcrypto-fips-exceptions.mjs new file mode 100644 index 00000000000..d9c26a1674d --- /dev/null +++ b/test/parallel/test-webcrypto-fips-exceptions.mjs @@ -0,0 +1,199 @@ +// Flags: --expose-internals + +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { createRequire } from 'node:module'; +import { hasFIPS } from '../common/crypto.js'; + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!hasFIPS(3)) + common.skip('requires OpenSSL >= 3 in FIPS mode'); + +const require = createRequire(import.meta.url); +const { internalBinding } = require('internal/test/binding'); +const { getCryptoKeyHandle } = require('internal/crypto/keys'); +const { + CShakeJob, + KangarooTwelveJob, + KmacJob, + TurboShakeJob, + kCryptoJobWebCrypto, + kSignJobModeSign, +} = internalBinding('crypto'); +const { subtle } = globalThis.crypto; +const { SubtleCrypto } = globalThis; +const data = new Uint8Array(); + +async function assertFipsException(operation, algorithm, fn, message) { + assert.strictEqual(SubtleCrypto.supports(operation, algorithm), false); + await assert.rejects(fn(), { + name: 'NotSupportedError', + message, + }); +} + +for (const algorithm of [ + { name: 'turboshake128', outputLength: 128 }, + { name: 'TurboSHAKE256', outputLength: 256 }, + { name: 'KT128', outputLength: 128 }, + { name: 'KT256', outputLength: 256, customization: data }, +]) { + await assertFipsException( + 'digest', + algorithm, + () => subtle.digest(algorithm, data), + 'Unrecognized algorithm name'); +} + +for (const createJob of [ + () => new TurboShakeJob( + kCryptoJobWebCrypto, 'TurboSHAKE128', 0x1f, 16, data), + () => new KangarooTwelveJob( + kCryptoJobWebCrypto, 'KT128', undefined, 16, data), + () => new CShakeJob( + kCryptoJobWebCrypto, + 'cSHAKE128', + data, + Buffer.from('KMAC'), + undefined, + 128), +]) { + assert.throws(createJob, { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + message: 'Unsupported crypto operation', + }); +} + +const emptyCShake = { + name: 'cSHAKE128', + outputLength: 256, + customization: data, + functionName: data, +}; +assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true); + +for (const length of [1, 513]) { + const algorithm = { + name: 'cSHAKE128', + outputLength: 256, + customization: new Uint8Array(length), + }; + await assertFipsException( + 'digest', + algorithm, + () => subtle.digest(algorithm, data), + 'Unsupported CShakeParams customization'); +} + +const functionName = { + name: 'cSHAKE256', + outputLength: 256, + functionName: Buffer.from('KMAC'), +}; +await assertFipsException( + 'digest', + functionName, + () => subtle.digest(functionName, data), + 'Unsupported CShakeParams functionName'); + +const bothCShakeParams = { + ...functionName, + customization: new Uint8Array(1), +}; +await assertFipsException( + 'digest', + bothCShakeParams, + () => subtle.digest(bothCShakeParams, data), + 'Unsupported CShakeParams customization'); + +for (const length of [0, 24, 33]) { + const algorithm = { name: 'KMAC128', length }; + await assertFipsException( + 'generateKey', + algorithm, + () => subtle.generateKey(algorithm, false, ['sign', 'verify']), + 'Invalid key length'); + await assertFipsException( + 'importKey', + algorithm, + () => subtle.importKey( + 'raw-secret', + new Uint8Array(length === 24 ? 4 : Math.ceil(length / 8)), + algorithm, + false, + ['sign', 'verify']), + 'Invalid key length'); +} + +const minimumKmac = { name: 'KMAC128', length: 32 }; +assert.strictEqual( + SubtleCrypto.supports('generateKey', minimumKmac), true); +assert.strictEqual( + SubtleCrypto.supports('importKey', minimumKmac), true); +await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(5), + minimumKmac, + false, + ['sign', 'verify']), { + name: 'DataError', + message: 'Invalid key length', + }); + +for (const length of [0, 3]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(length), + 'KMAC128', + false, + ['sign', 'verify']), { + name: 'NotSupportedError', + message: 'Invalid key length', + }); +} +const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(4), + 'KMAC128', + false, + ['sign', 'verify']); +assert.strictEqual(key.algorithm.length, 32); + +await assert.rejects( + new KmacJob( + kCryptoJobWebCrypto, + kSignJobModeSign, + getCryptoKeyHandle(key), + 'KMAC128', + undefined, + 32, + 9, + data, + undefined).run(), + (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual(err.cause?.name, 'Error'); + assert.strictEqual(err.cause?.message, 'Deriving bits failed'); + return true; + }); + +const minimumOutput = { name: 'KMAC128', outputLength: 8 }; +assert.strictEqual(SubtleCrypto.supports('sign', minimumOutput), true); +assert.strictEqual(SubtleCrypto.supports('verify', minimumOutput), true); +for (const outputLength of [0, 9]) { + const algorithm = { name: 'KMAC128', outputLength }; + await assertFipsException( + 'sign', + algorithm, + () => subtle.sign(algorithm, key, data), + 'Invalid KmacParams outputLength'); + await assertFipsException( + 'verify', + algorithm, + () => subtle.verify(algorithm, key, data, data), + 'Invalid KmacParams outputLength'); +} diff --git a/test/parallel/test-webcrypto-get-public-key.mjs b/test/parallel/test-webcrypto-get-public-key.mjs index 622ec4adca6..06a4b13d3de 100644 --- a/test/parallel/test-webcrypto-get-public-key.mjs +++ b/test/parallel/test-webcrypto-get-public-key.mjs @@ -9,8 +9,10 @@ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const { kSupportedAlgorithms } = require('internal/crypto/util'); +const { hasFIPS } = require('../common/crypto'); const { SubtleCrypto } = globalThis; const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const RSA_KEY_GEN = { modulusLength: 2048, @@ -54,6 +56,14 @@ for (const name of ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']) { keyGeneration[name] = vector(name, ['decapsulateBits'], ['encapsulateBits']); } +for (const name of [ + 'MLKEM768-P256', + 'MLKEM768-X25519', + 'MLKEM1024-P384', +]) { + keyGeneration[name] = vector(name, ['decapsulateBits'], ['encapsulateBits']); +} + const unsupportedGetPublicKeyAlgorithms = new Set([ 'AES-CBC', 'AES-CTR', @@ -80,6 +90,15 @@ for (const name of Object.keys(kSupportedAlgorithms.exportKey)) { assert.strictEqual(SubtleCrypto.supports('getPublicKey', name), true); + if (rejectsXCurves && + (name === 'X25519' || name === 'X448')) { + await assert.rejects( + subtle.generateKey(test.algorithm, false, test.privateUsages), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + continue; + } + const { privateKey } = await subtle.generateKey( test.algorithm, false, test.privateUsages); const usages = test.publicUsages; diff --git a/test/parallel/test-webcrypto-kem-hybrids.js b/test/parallel/test-webcrypto-kem-hybrids.js new file mode 100644 index 00000000000..0d1ec410c16 --- /dev/null +++ b/test/parallel/test-webcrypto-kem-hybrids.js @@ -0,0 +1,689 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3, 5)) + common.skip('requires OpenSSL >= 3.5'); + +const assert = require('assert'); +const { once } = require('events'); +const { KeyObject } = require('crypto'); +const { MessageChannel } = require('worker_threads'); +const { subtle } = globalThis.crypto; +const { SubtleCrypto } = globalThis; + +if (hasFIPS()) { + (async () => { + for (const name of [ + 'MLKEM768-P256', + 'MLKEM768-X25519', + 'MLKEM1024-P384', + ]) { + for (const operation of [ + 'generateKey', + 'importKey', + 'exportKey', + 'getPublicKey', + 'encapsulateBits', + 'decapsulateBits', + ]) { + assert.strictEqual(SubtleCrypto.supports(operation, name), false); + } + assert.strictEqual( + SubtleCrypto.supports('encapsulateKey', name, 'HKDF'), + false); + assert.strictEqual( + SubtleCrypto.supports('decapsulateKey', name, 'HKDF'), + false); + await assert.rejects( + subtle.generateKey( + name, + true, + ['encapsulateBits', 'decapsulateBits']), + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); + } + })().then(common.mustCall()); + return; +} + +const vectors = [ + { + name: 'MLKEM768-P256', + seed: '0000000000000000000000000000000000000000000000000000000000000000', + publicKey: '3d209f716752f6408e7f89bceef97ac388530045377927644ef046c0a7cae978c8841a0133aa' + + 'c4f1e1a7027277f671219cf58b85d29c8fec08edd432e787a3cf9936fe0026a113cb9efb1d72' + + '14049527bfe2141ea170b0294a59403ab0ce16760a8baa95b823cbb8aacdcc17ef32775223c7' + + '91e3740163941f9bb3f63346bef1c050c31f932c62719429aff14c2bd438ab135bed692d56c7' + + '7c04cbbffd6335b578318b513771e84b14ea821262141ca006ccb8bf2500aa1008970f216fe7' + + 'f1ae34125aa290492c069a189222adc322f97649c762c7d3128ad3bb2667971d0744014bc3b6' + + '7445cbcd0b3e7ea69fb1cb9f9c331f97487920187292926d04a25a2650abbd44982bb0c3c630' + + '1fe6a61330d24d8a3c7021dc3e3392c79a139b37613bba67a2984298507b84a4d61eef18acfb' + + '979af2d39caa4c0db4513815359d76fc378c63a7f4f3053b17168d0221cf0c2eec5514ba235f' + + '81d04d67c3b5c518094917671c26a7c046457533cc32844581277a03eb065c4529a779a9a587' + + '8f2aac3f81db9ed3d8c9345697058cbb99d379bca16d8fdb61d129960390524791b9d3e501b9' + + '00bd1e5002e095be06c23f1fb212f5801f24b6b28c0c5493d246d02aa29fa3acfbe15ac4e212' + + 'eb0b6f69ebbea259a2703aa4c308224bdb741c65c7a5d4bff788279507bbfe513d7aa5694e7b' + + '3cdf62ab36432742d4a0ca9b3570ba742fa803b46989c8526ea586cc4fc32866143b79601725' + + 'fa545fd280b404530318bbc3371194710b6d74beaa629eb18a36a953b75915ae96999ba5c88c' + + 'dc56a46861c50032c9b630bcc1445a30878979bc55a2c0955bf399b231203b90c651b6afe0e2' + + '42b5a543250b142f7291ed753d816098f7913302a8ce91641716623d4fc2ac6772aa5f367404' + + '2b7c4a18a2186289a4ac4e200774596ca03e6798c7506b984999db6ac142586bae0799f1e776' + + 'f9f5247dc574d8556ddf9bbbc4ca3643263457f74248010d62d4311268360aecb4902b450bf2' + + '050ecb8ba7a92820d233f5a14ed31225a1d17ca6f19e825894cfb1807d922cbd60761134be41' + + '9144bcf72006366a4460137ad9136c113f05eb54c409520edc72e4150cc3a24b0f819eec11bb' + + 'd19ca9645b0810a60b4a8a9e9c3955396a1653955b047bcf4f98433c27236c570d75f809e44a' + + 'af2dc33665826351872c293350ab324518c8c0c80b521c80c81a56bdc968a5650315a830c8bb' + + '17532c62ccc23b1d46412c256b224fd4674491803501d0143125c7577239689965b6989ca561' + + '793c0f85c62a9e13487da17662a7188c70b1040a67ed4c3f85e74e3691822fb96314d6134fe6' + + 'a626b3cbe1461d62a7b573b2cc75579ffa22967e36ceb2a1aa0b71875a22751d706b72ca9ecd' + + '0c8100ad0aa58009a5c83fffe91759e6baa0a9345af99fe3b69509dbc84032868844ab3f65bb' + + '1df8beadf36442e48e339c967023a525411544c789a2f04dacd06ffef78302210450b931f6b4' + + 'c32aab34a3f5260b810f4c9a946fc22d3baabaa80ba8d9955d6dc35e8609b4256b482cdc9d89' + + '77c1a47a354e7c527fdb1672e166917b95cd6351820261daab361f8a2dcbb240c55abd6a8105' + + 'e5291b427b566d731e6b7047189cff20d8b120e0b3e72472d1b0086812200fd3698e23f06e4f' + + '4e08bbb54cc2049c039c845be659999c8fa48d7f62327c146cf1bc0b0bb1b91b30174b7bc220' + + 'd422023bff6b0dee263532c503f3982e4d3e27071b855578a9a9aa63b8a8c339bf', + ciphertext: 'd81018a94f8078e02105beaa814e003390befa4589bb614f77397af42d8e8150796f2c88a4ef' + + 'ca81b8cf93c0ae3716c54ec1b045e3875f38c2dd12d7f717bd7fb701a9fecda5ed8b764c9a35' + + 'd4a5c1d8930f6071f653eebb2d1afa77debb8302d16f17e0f5f3920a71a4d49beafa0e1c7e44' + + '3f8abca64a65a9e81a97e7357bf902573363c0e1a12e5228036828e3f759121fada92441fe33' + + '4e85d79347e470d2fed945541d832c54baaa3cb7526c3853954db4f73547cc7c27fd38398bfa' + + '7704952cb841e38b270e4db7435f0ee22f57d7ad3270bd0c88e71b4b864cf2277c65daa10a6d' + + 'ad4c7abecd95cc4ebec39c08404b522e4ecc1545713f76bebd3b5a0f2feb3461936065dbd13f' + + '6a1f61e1b142a2af2e5a482ba2c50cf0317049c0b3bfd6d5e9240eba9111d2030fdea17e33b6' + + '524020d30b0c4f8069285f3a6ca267d287d01e827d8422bf5426e11688bfc73756af1841b1c8' + + '7e126cb50c914b5b2b8673488ad3b074cad77a3840eb12dd688f313ee1e9ff8c479a678f2763' + + '56fc9d65e1d5b4c1e9855b4175db144f7767c12061769190fe6b5e51563b91f94d131a2b796b' + + 'd2980ed0dab4ae7a7110e920007a757158a5eb8662cbf89ddffe9d8196821313cdc00108853f' + + 'c4746b111d5b56da638d8ed2973918960f5dfe93ead3ae521e957cec3c8d843e8fce234c70ad' + + '055177f235439d6098bdd771b1cfcfadaab4f50a7378185c62409f383c8ff658c2a2af66498c' + + 'fd81e962766ac6b774e88424fb4f331837d0a28502708477caf8780a156d723f68fca791e1cd' + + '2397bfc2b24c77c765d9b2af36f732d52107517efd8157b283b440a613f756c364ca108971a8' + + '878199a93f260baec3e850033cc032c2e53f823576affb4d3b116e2d16049152c35aaa263ab3' + + '76f0ad5ede6a749607a283e3016e62191c0e8fde33e718cd989591c9a205d608d99fcb8a7471' + + '603d716cb01b56328d7d880aec2851f4e6d8b5016c25647e9026ebb441543e8012dbfcf078d4' + + '012b8c39184dd64f3821b4774ae4e36365f8baf2bd1f6667c017a1e65ff8a1554458fb3f367c' + + '02721752bfa56fc7fd566ae95ffb208f919ef12f4cf8a2fdd141a8df559bddb7b8d1f04ee6d4' + + 'cf7805d142989caf216dfae985faaab9974f6d9f8aa1129084db8db912b1655f595ffbaa6649' + + '1ab4655fd734cfd4bb0c0289d4bcc8fc5e9943b351cb147c8db059a24004d1c3e3bb4c14a881' + + 'e5101acb736c65c5d579acb67ee85a560277b43338fe79d34b772c5da001da3b5a3383dd8131' + + '9a0b4542e6d7e46eed5314cc70eb231de27b6e760db598ba19995cf69be0e4458e35f3f274ac' + + 'a2455d43fe3344e183c6dc47c857dbe9907b41e41006d91b25adcafc098fe66f7554be8dad49' + + '3c4f4b1dbf7a51464139db474afab5572f92a2232b59be56a72c0505149dae5cde1e60287703' + + '7de7802b5f6fa47a4c9a3e52d6ca15339920254e9ffb53c7b834cc0288ed9905a1841e9390ea' + + '94a8898bd4c6b6d6027e4d43c7867242515bbeefe12340fc04428a824ea7cf56ad2a64ed368b' + + '71315d80cee846007cff1d2eea2c3f0f921537304ae598f98dd10d1f102811a4e2d161c3fd8b' + + 'bb193d4b25bee950ac839c0f9d', + sharedSecret: '9bd018e869bb01b63fb8f5da374a73d347ea14cb2bc570b13d0908e2288ec456', + }, + { + name: 'MLKEM768-X25519', + seed: '0000000000000000000000000000000000000000000000000000000000000000', + publicKey: '3d209f716752f6408e7f89bceef97ac388530045377927644ef046c0a7cae978c8841a0133aa' + + 'c4f1e1a7027277f671219cf58b85d29c8fec08edd432e787a3cf9936fe0026a113cb9efb1d72' + + '14049527bfe2141ea170b0294a59403ab0ce16760a8baa95b823cbb8aacdcc17ef32775223c7' + + '91e3740163941f9bb3f63346bef1c050c31f932c62719429aff14c2bd438ab135bed692d56c7' + + '7c04cbbffd6335b578318b513771e84b14ea821262141ca006ccb8bf2500aa1008970f216fe7' + + 'f1ae34125aa290492c069a189222adc322f97649c762c7d3128ad3bb2667971d0744014bc3b6' + + '7445cbcd0b3e7ea69fb1cb9f9c331f97487920187292926d04a25a2650abbd44982bb0c3c630' + + '1fe6a61330d24d8a3c7021dc3e3392c79a139b37613bba67a2984298507b84a4d61eef18acfb' + + '979af2d39caa4c0db4513815359d76fc378c63a7f4f3053b17168d0221cf0c2eec5514ba235f' + + '81d04d67c3b5c518094917671c26a7c046457533cc32844581277a03eb065c4529a779a9a587' + + '8f2aac3f81db9ed3d8c9345697058cbb99d379bca16d8fdb61d129960390524791b9d3e501b9' + + '00bd1e5002e095be06c23f1fb212f5801f24b6b28c0c5493d246d02aa29fa3acfbe15ac4e212' + + 'eb0b6f69ebbea259a2703aa4c308224bdb741c65c7a5d4bff788279507bbfe513d7aa5694e7b' + + '3cdf62ab36432742d4a0ca9b3570ba742fa803b46989c8526ea586cc4fc32866143b79601725' + + 'fa545fd280b404530318bbc3371194710b6d74beaa629eb18a36a953b75915ae96999ba5c88c' + + 'dc56a46861c50032c9b630bcc1445a30878979bc55a2c0955bf399b231203b90c651b6afe0e2' + + '42b5a543250b142f7291ed753d816098f7913302a8ce91641716623d4fc2ac6772aa5f367404' + + '2b7c4a18a2186289a4ac4e200774596ca03e6798c7506b984999db6ac142586bae0799f1e776' + + 'f9f5247dc574d8556ddf9bbbc4ca3643263457f74248010d62d4311268360aecb4902b450bf2' + + '050ecb8ba7a92820d233f5a14ed31225a1d17ca6f19e825894cfb1807d922cbd60761134be41' + + '9144bcf72006366a4460137ad9136c113f05eb54c409520edc72e4150cc3a24b0f819eec11bb' + + 'd19ca9645b0810a60b4a8a9e9c3955396a1653955b047bcf4f98433c27236c570d75f809e44a' + + 'af2dc33665826351872c293350ab324518c8c0c80b521c80c81a56bdc968a5650315a830c8bb' + + '17532c62ccc23b1d46412c256b224fd4674491803501d0143125c7577239689965b6989ca561' + + '793c0f85c62a9e13487da17662a7188c70b1040a67ed4c3f85e74e3691822fb96314d6134fe6' + + 'a626b3cbe1461d62a7b573b2cc75579ffa22967e36ceb2a1aa0b71875a22751d706b72ca9ecd' + + '0c8100ad0aa58009a5c83fffe91759e6baa0a9345af99fe3b69509dbc84032868844ab3f65bb' + + '1df8beadf36442e48e339c967023a525411544c789a2f04dacd06ffef78302210450b931f6b4' + + 'c32aab34a3f5260b810f4c9a946fc22d3baabaa80ba8d9955d6dc35e8609b4256b482cdc9d89' + + '77c1a47a354e7c527fdb1672e166917b95cd6351820261daab361f8a2dcbb240c55abd6a8105' + + 'e5291b427b566d731e6b7047189cff20d8b120e0b3e72472d1b0086812200fd3698e23f06e4f' + + '4e08bbb54cc2f63601b7f85accfeea2d17964c66b5194b0f08e18519faaee194e3c102823062', + ciphertext: 'd81018a94f8078e02105beaa814e003390befa4589bb614f77397af42d8e8150796f2c88a4ef' + + 'ca81b8cf93c0ae3716c54ec1b045e3875f38c2dd12d7f717bd7fb701a9fecda5ed8b764c9a35' + + 'd4a5c1d8930f6071f653eebb2d1afa77debb8302d16f17e0f5f3920a71a4d49beafa0e1c7e44' + + '3f8abca64a65a9e81a97e7357bf902573363c0e1a12e5228036828e3f759121fada92441fe33' + + '4e85d79347e470d2fed945541d832c54baaa3cb7526c3853954db4f73547cc7c27fd38398bfa' + + '7704952cb841e38b270e4db7435f0ee22f57d7ad3270bd0c88e71b4b864cf2277c65daa10a6d' + + 'ad4c7abecd95cc4ebec39c08404b522e4ecc1545713f76bebd3b5a0f2feb3461936065dbd13f' + + '6a1f61e1b142a2af2e5a482ba2c50cf0317049c0b3bfd6d5e9240eba9111d2030fdea17e33b6' + + '524020d30b0c4f8069285f3a6ca267d287d01e827d8422bf5426e11688bfc73756af1841b1c8' + + '7e126cb50c914b5b2b8673488ad3b074cad77a3840eb12dd688f313ee1e9ff8c479a678f2763' + + '56fc9d65e1d5b4c1e9855b4175db144f7767c12061769190fe6b5e51563b91f94d131a2b796b' + + 'd2980ed0dab4ae7a7110e920007a757158a5eb8662cbf89ddffe9d8196821313cdc00108853f' + + 'c4746b111d5b56da638d8ed2973918960f5dfe93ead3ae521e957cec3c8d843e8fce234c70ad' + + '055177f235439d6098bdd771b1cfcfadaab4f50a7378185c62409f383c8ff658c2a2af66498c' + + 'fd81e962766ac6b774e88424fb4f331837d0a28502708477caf8780a156d723f68fca791e1cd' + + '2397bfc2b24c77c765d9b2af36f732d52107517efd8157b283b440a613f756c364ca108971a8' + + '878199a93f260baec3e850033cc032c2e53f823576affb4d3b116e2d16049152c35aaa263ab3' + + '76f0ad5ede6a749607a283e3016e62191c0e8fde33e718cd989591c9a205d608d99fcb8a7471' + + '603d716cb01b56328d7d880aec2851f4e6d8b5016c25647e9026ebb441543e8012dbfcf078d4' + + '012b8c39184dd64f3821b4774ae4e36365f8baf2bd1f6667c017a1e65ff8a1554458fb3f367c' + + '02721752bfa56fc7fd566ae95ffb208f919ef12f4cf8a2fdd141a8df559bddb7b8d1f04ee6d4' + + 'cf7805d142989caf216dfae985faaab9974f6d9f8aa1129084db8db912b1655f595ffbaa6649' + + '1ab4655fd734cfd4bb0c0289d4bcc8fc5e9943b351cb147c8db059a24004d1c3e3bb4c14a881' + + 'e5101acb736c65c5d579acb67ee85a560277b43338fe79d34b772c5da001da3b5a3383dd8131' + + '9a0b4542e6d7e46eed5314cc70eb231de27b6e760db598ba19995cf69be0e4458e35f3f274ac' + + 'a2455d43fe3344e183c6dc47c857dbe9907b41e41006d91b25adcafc098fe66f7554be8dad49' + + '3c4f4b1dbf7a51464139db474afab5572f92a2232b59be56a72c0505149dae5cde1e60287703' + + '7de7802b5f6fa47a4c9a3e52d6ca15339920254e9ffb53c7b834cc0288ed9905a1841e9390ea' + + '94a8898bd4c6b6d6027e4d43c7867242515bbeefe12340fc6b3d57762f8badb69433f9c6d060' + + 'f85f5e5c6b6803a816d141c075f63541ad10', + sharedSecret: 'e5ba94031ea6efd69c09c254f6d9783136ba6037e2d4c43bcccf19d6f3f4343a', + }, + { + name: 'MLKEM1024-P384', + seed: '0000000000000000000000000000000000000000000000000000000000000000', + publicKey: 'a10bc8b554cd51980cdbbccc3041420fd320fe8b74c7a84278c63c17070dc231b61ab269b9d6' + + '77d920261186654b4571f51797d5c342b8070bc6c92bca16adecc631e4e94c7508b111730c74' + + '9c73e2d6a6f97155cb269ccc06a71a21bef3d269463c935048a7f4636c7b320073709023f7b0' + + '4d0530571a9a6f718280870bb63875d3f599bc229b95869cd5bb5d26640856d40b828198fdf2' + + 'c099998ffdf772e462336c521cd326b5e4997bd95c135c57bd02c7afa80a2923d510951778ee' + + '5125b2aa18f90445453b85789224725b259279698ac9426c882baabc38d4fb3a3f6831180918' + + 'b9825e0e418154d78aebab5e7e7066e69b2567476bf1177fe079a38298be6f01b098c33851ab' + + '25312b52e32a5750c2b73d293c0b810473b310aaf062f19914c7377b2e90388f575bf5e68534' + + '53b95a74aa18d62d4ae37e6996a48ab5217488a92d7b01e315c50b68204143792afc4f8367c0' + + 'ce065ab32014bdb5515fe0594608aad1218994724afaaaa2df0355f46666b6e02a387b6d3da4' + + '713edb610bb048c3a2078b800e9ea483f2009c96d24c71b2cbc8e1200c0277383c5c27895e29' + + '8c3607701ce58702a91903274a041408234cb0021ef2b1c5131419b444dc84b89d147d1fe43c' + + '43f676d906735d9ca2a59c2232d97fd4aa1ae2bb3d1b170ca553cb2574954fdc6689fac623cb' + + 'aa31982d82424d5a564fef7a8ba51b44df15053b2b45bec4aa1ed49929123daf754175c59382' + + '58c608b24d062042ab4bbee5e553a5ea627521738ae5ab2e06bd98b020787b2f5fa51eb4c46c' + + '2bf90e55a49560340667f88ac41432b7f551dfd98c037c79f79b41b985a8b1f51345550cd816' + + '714362040778c43e378a288394bd028c8c31b5a904bc4a5648a596035cb38f0e276e12c9a96f' + + '8425056b05a136642dd2cb75463036485ba1a50539e420e1e31dfac529cad6c68ec067467494' + + '73e050a4ac92b7199beceb239b6c12c8e716b66607aeca64a5850b01f99d0b176a7759781ed7' + + '7cb1ba40d17ac5c6cb06c942c002c2cf6efcb121f10ad2a45ff781426e7104cbdca73b81865a' + + 'b22b00ba834355ae485a262f354248932c2be178369a3dd7e2428fdc379346ab2b754c43db65' + + '7460cb09c5c48b5810cb7a5c6156cf87440c9e36a4869a8ac458b382fc178915a9ce1bcdda7c' + + '48807c207e656ffb80bf33e32bc8c7b20ef60572612ceac99ad1c56ce5a764b29b74c17a5b51' + + '0b1afcb18a1afc35c12ac213725325f9b7a2eb338fe4c0080c31a58a995db7027d900e785448' + + '87f90ada467d0e383c119c5399310bc6735874e8804ff6c2bae57f2c3357cb627033c12a5924' + + 'b20ce5abf113172bd2b77086cac543811793bba71734c9f005ac2656460bc30a442b38872575' + + '8a623e37ba6e293abfb84f344229f373c214ca776a7c05adc465fed93b9cf77f0022ab71f1ad' + + 'de369dd8f420a58c057c14cc18dc47da7c12b086473eab419652967001c4e42a381c8ba539a8' + + '75d21a9945133bab9bc1e53a600de77cbfb2aeab6b19ced4c6eaa8998ee6a1577255f7132d80' + + 'a32d6c0c6ec44c9c4b28699a645bb0bc958e00275077925309519b0824c7000dfa61912ec049' + + '063a067d00b059053e508a5bfee63473869c8a8510af898cd7572854f5c38af96f5f97a73726' + + '32ea7bb4b6fb831c612af71191ff9806b379bcd43c6059b7b1f953741444af713c155d962722' + + 'b947aa23a32a89b356a6a7508aad63968c1dea78ff18aac27a89aa7b42b0d7481dd3cc649421' + + 'e51397782218ac5441760ba51a0328d66b436fec32d7aa4d68e0cad1bc14f7241c903480f809' + + '983fc2c30d93138cf63b59bc737ac08192893d039187a811bef3d3209eb7b8d1e05b5b251cef' + + '760a210b2732867ab32049ba3c354e3858aee7b71df792924730d8e842e484122b50677b0a30' + + '6e61cf21b62091da18b937192936a09e5a418cf78b666157dd477af1c36a12320129522840e3' + + '70941157808782a5335b0ac10d70e1beafd401074b84b9826cc58aad217bae0f419b2da89613' + + '3272d8f22c6f420fcc738fccc1082fc93c7df0994c6bcf2cc8a29037b6bb2b4bcef4b0ee8caf' + + '8506bc5ecba082a56806c1cede0b944338a69a668254c1150ae05030e256b2b67661ba027d97' + + '576da613ac8c7c29051f1240b96b0c127e264d5e1dbbfe9561a567d5c9103673b446b3ccea6c' + + '5f7f34f09348a5d4a58b0498871dc940ee97b50c0336f9a60c3299f99560ac70657a27befa70' + + '2265ce590583e04a28326092d3dea2118dd1df5e81d7d3014ec4b5ce67dcb45ef001769dd5d5' + + 'ada76934d38d740924712bfae672169d8f8744c151346d285fbb653f83aa0f', + ciphertext: 'dc63d18bb9715fb6e3ba71cb439fcd3377a75305cc9b144e6758bf5794a272e6b4a0da33234c' + + '0ac1bb5b4e60e4c82eb1fb780d59e4e4616641a0595ba031e3ae69d971dcd5fff14e21731a8e' + + '1a221f46c7820d214630b707fa1b0de3a484698f3d49e0a75f1212b8c42d330dd909f15eac04' + + '02f19ee77fba9447e1c44304b0d8c371c17c5549fdbdec1e0a2e7be9f577d7a4b5b2618d9ba6' + + '7ab95a0297cd5c5a13c89cc5a57cbd9a8ae38d66455c9a3d2bc55b498775fee2f6dc224d376d' + + '5f526a8354c8ed724f60337e900b85627972383e1fd987d407a8834005814a4fdc94c947e5f3' + + '471459288cfb127952b3208f10c914200bbaac5fcebd2bc9e2848492bab17b9288ca8b81d1c2' + + 'ac9522dcc0b6d5f51e10f3afbb5d65fbf919edef6323c4e92c6b0690c10db25a9182de9e919e' + + 'a1b3e65ae6150635d5180ebd7d23a2264828bc3ee1fd34dba1924ad0db30c747e05baa9148f1' + + 'a032769c685e04665fd802a79c4624f69a9198a426eac1b217d903cdacf8844e73365f3a219a' + + '700dda27edf6bea33602617c5fd105b301b884bfaaa1163b791ec09f82523fef65c87b75ed06' + + '3ceb127729b82c8712e1f41b547d095f55ee71f3f8b47a306cb5d9bdd817854c74a42eebf934' + + 'a1136dea3fbc546ad8ce51b3171913722f08b0261d197590342bfe4108dcb08c62a98610cbfb' + + '8d3b2831f56dcac2220e29a5811f38f0824f21a6cbebc64fd89a09b110dffbe03799ffc74fe5' + + '65c80dbf6a66acd7bfd14cb90acba03405a7982d4c1c68caa75f8b72e4dd6401d7dce4db4f6b' + + '820a7886a604b66b4e5b9eea5e5eddc2bca458a25977bd1f02874c5d9daf2baf56b3040f24ce' + + '7fe14cc14d61c7960db4decb37d9779c8e36d69a7763066d8c1149312d26887a693dc222daa8' + + '92dd00cd8f3a558cf605e4c65c011c2e9f0d671ba10af2bb90ee0351ae5078eb7878399ec9eb' + + '4ace87a68269618bda12a7aed6fda0385496c5d10ac36b35255f4a31edfa8a2c516b65c63431' + + '013ed4909ec7a787a5efb9d3c3887b80ac18a44934b6559bd8a84b18e86fa1b0b9e1d9f92ba4' + + '95ba5595d82e5095612b79e805154bf428a7071662c7cefb6450165c6f8f6954c37219bff4a4' + + '9894a8aa37f940a40f4ec942c281e6c47ea408199927a724ff1c7460fc8fd47a98d0c9d4d1f0' + + '7994d8084f6e084935ad7c2985282fabd5ca13b942e10d35278f4ff4cb1cb96f3c862410e791' + + '44a46b4db1a3c3d4d63018ec5c01ca48cb67081482e7d434b4abe5fa3071f2fbb533f745602b' + + '0da6183b28e6c5dfa42dab7ae0bbbf7638e106be1bd7312cba399e08c96dbd69a128a2face2d' + + '4a02951533a25e82fe63d0aaaa2e8c75150215c93ab06c22f9cab8d1cae7424f8baa09b3260e' + + 'cfa3c7c8d55a276b4b317f72ec86b1b145a63aca83ef8c1204d8ab0c96ea3f742de39db47020' + + '616e139285814f188029ace4587f14cf12b5ed81086d8213cf8cb578341e04e16f519b77ff4c' + + '2644a5732639d658d0c4eaf992bd7dbd5011b700a5fa63dc1b24a84a3c80656bab5705dc3a74' + + '312c80e8bdb24a7ac6e27bcb8c07ece62c6e5777dd3dc0657181f440c7524d907dd27950bcb2' + + '52aef7f8cbf453cee3fe3143a665072c787cea76de323aa41537df2f3a40a518a694b918953b' + + 'de8d57084e32d3b1fdcf9d153e73f02624beaf6ebe23e6828a6a489583494f3cd790fc96bb6f' + + '5d8b198402965e2e668e6581e7cf1c8a47a92198388f2b4cd38df660f0ddd48ad126819c4435' + + 'af3a12c89113d778ac544fd8079cb8aaa97d2ff1b608da574c4dcd87f4979390de3be405f0e4' + + '7788dd0b01662805079fd73c64e9278c036544add3694c838bfcfb08c8a5efb09549442123ea' + + 'a59fa30fbb9198105f6be00163bac076193f6721c539714108bbfae167f5db8085c5838618f3' + + '2a968bbb25c40645a17c17b9bec64aea45832eec5adc25b53e677f67566fbf5ce2d9193a06bd' + + '9b477e601d589b25f422defc49105252cd9ca6adcbb36be8a01a8472b4d463f655be14ccff9b' + + '0571a2048e31c14b9b23e2d43fafa3f85ece6fd41896cc5c68993dbaa926f285ec94c72887de' + + '9564881d735c05f83aa474b3d4cd133a630ac63850771cb5270f6cb7a391170d66af3e4901b6' + + 'eb0253f3f34ef57d6babd97aa99ce718c3bcb53ff13d4028a0c943bb9681106ce176242cccb7' + + '5df1d3f8d3706e5b068b042c3154d5e6292581b36499e6b069b9a490aa67f0675390539da855' + + '5e6a4e8a35a86fdfea83e1387bf4acc650ec1edae7c99aa3a48306ee1d1a5e513c0c6901f64d' + + '0a3ee285de3c11d49f90cd4323dafda14832f0d8b760c0e5a48633c967cfaf', + sharedSecret: '8c028c6ea72a1c59408e2b15dd8fed8008517e861cd2329b159bda1919ea656c', + }, +]; + +const lengths = { + 'MLKEM768-P256': { publicKey: 1249, ciphertext: 1153, groupElement: 65 }, + 'MLKEM768-X25519': { publicKey: 1216, ciphertext: 1120, groupElement: 32 }, + 'MLKEM1024-P384': { publicKey: 1665, ciphertext: 1665, groupElement: 97 }, +}; + +async function roundTripViaMessageChannel(key) { + const { port1, port2 } = new MessageChannel(); + port1.postMessage(key); + const [received] = await once(port2, 'message'); + port1.close(); + port2.close(); + return received; +} + +function buffers(vector) { + return { + seed: Buffer.from(vector.seed, 'hex'), + publicKey: Buffer.from(vector.publicKey, 'hex'), + ciphertext: Buffer.from(vector.ciphertext, 'hex'), + sharedSecret: Buffer.from(vector.sharedSecret, 'hex'), + }; +} + +async function testAlgorithmConfigPrototypePollution() { + const properties = [ + 'encapsulationKeyLength', + 'ciphertextLength', + 'groupOrder', + 'namedCurve', + ]; + const descriptors = new Map(); + + for (const property of properties) { + descriptors.set( + property, + Object.getOwnPropertyDescriptor(Object.prototype, property)); + Object.defineProperty(Object.prototype, property, { + __proto__: null, + configurable: true, + get: common.mustNotCall(`Object.prototype.${property} getter`), + set: common.mustNotCall(`Object.prototype.${property} setter`), + }); + } + + try { + await subtle.generateKey( + 'MLKEM768-X25519', + true, + ['encapsulateBits', 'decapsulateBits']); + } finally { + for (const property of properties) { + const descriptor = descriptors.get(property); + if (descriptor === undefined) { + delete Object.prototype[property]; + } else { + Object.defineProperty(Object.prototype, property, descriptor); + } + } + } +} + +async function testGeneratedRoundTrip(vector) { + const algorithm = { name: vector.name }; + const { ciphertext } = buffers(vector); + const keyPair = await subtle.generateKey( + algorithm, + true, + ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); + assert.strictEqual(Object.getPrototypeOf(keyPair), Object.prototype); + assert.deepStrictEqual(Object.keys(keyPair), ['publicKey', 'privateKey']); + const { privateKey, publicKey } = keyPair; + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.algorithm.name, algorithm.name); + assert.strictEqual(privateKey.algorithm.name, algorithm.name); + assert.deepStrictEqual(publicKey.usages, ['encapsulateKey', 'encapsulateBits']); + assert.deepStrictEqual(privateKey.usages, ['decapsulateKey', 'decapsulateBits']); + + for (const key of [publicKey, privateKey]) { + assert.throws(() => KeyObject.from(key), { + code: 'ERR_INVALID_ARG_VALUE', + message: /cannot be represented by a single KeyObject/, + }); + } + + const encapsulated = await subtle.encapsulateBits(algorithm, publicKey); + assert.strictEqual(encapsulated.sharedKey.byteLength, 32); + assert.strictEqual(encapsulated.ciphertext.byteLength, ciphertext.byteLength); + + const decapsulated = await subtle.decapsulateBits( + algorithm, + privateKey, + encapsulated.ciphertext); + assert(Buffer.from(decapsulated).equals(Buffer.from(encapsulated.sharedKey))); + + const encapsulatedKey = await subtle.encapsulateKey( + algorithm, + publicKey, + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign']); + const decapsulatedKey = await subtle.decapsulateKey( + algorithm, + privateKey, + encapsulatedKey.ciphertext, + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign']); + assert(KeyObject.from(encapsulatedKey.sharedKey) + .export() + .equals(KeyObject.from(decapsulatedKey).export())); +} + +async function testVectorRoundTrip(vector) { + const algorithm = { name: vector.name }; + const { seed, publicKey, ciphertext, sharedSecret } = buffers(vector); + assert.strictEqual(seed.byteLength, 32); + assert.strictEqual(publicKey.byteLength, lengths[vector.name].publicKey); + assert.strictEqual(ciphertext.byteLength, lengths[vector.name].ciphertext); + assert.strictEqual(sharedSecret.byteLength, 32); + + const privateKey = await subtle.importKey( + 'raw-seed', + seed, + algorithm, + true, + ['decapsulateBits']); + const publicKeyFromPrivate = await subtle.getPublicKey( + privateKey, + ['encapsulateBits']); + assert(Buffer.from(await subtle.exportKey('raw-public', publicKeyFromPrivate)) + .equals(publicKey)); + + const jwk = await subtle.exportKey('jwk', privateKey); + assert.strictEqual(jwk.kty, 'AKP'); + assert.strictEqual(jwk.alg, algorithm.name); + assert(Buffer.from(jwk.pub, 'base64url').equals(publicKey)); + assert(Buffer.from(jwk.priv, 'base64url').equals(seed)); + + const publicJwk = await subtle.exportKey('jwk', publicKeyFromPrivate); + assert.deepStrictEqual(publicJwk, { + kty: 'AKP', + alg: algorithm.name, + pub: jwk.pub, + key_ops: ['encapsulateBits'], + ext: true, + }); + const jwkPublicKey = await subtle.importKey( + 'jwk', + publicJwk, + algorithm, + true, + ['encapsulateBits']); + assert(Buffer.from(await subtle.exportKey('raw-public', jwkPublicKey)) + .equals(publicKey)); + + const jwkPrivateKey = await subtle.importKey( + 'jwk', + jwk, + algorithm, + true, + ['decapsulateBits']); + const vectorSharedSecret = await subtle.decapsulateBits( + algorithm, + jwkPrivateKey, + ciphertext); + assert(Buffer.from(vectorSharedSecret).equals(sharedSecret)); + + const implicitRejectionCiphertext = Buffer.from(ciphertext); + const pqCiphertextLength = + ciphertext.byteLength - lengths[vector.name].groupElement; + implicitRejectionCiphertext.fill(0, 0, pqCiphertextLength); + // Preserve the valid traditional group element so this only exercises + // ML-KEM implicit rejection. + assert(implicitRejectionCiphertext.subarray(pqCiphertextLength) + .equals(ciphertext.subarray(pqCiphertextLength))); + + const implicitRejectionSharedSecret = Buffer.from( + await subtle.decapsulateBits( + algorithm, + privateKey, + implicitRejectionCiphertext)); + assert.strictEqual(implicitRejectionSharedSecret.byteLength, 32); + assert(!implicitRejectionSharedSecret.equals(sharedSecret)); + assert(implicitRejectionSharedSecret.equals(Buffer.from( + await subtle.decapsulateBits( + algorithm, + privateKey, + implicitRejectionCiphertext)))); + + const publicKeyOnly = await subtle.importKey( + 'raw-public', + publicKey, + algorithm, + true, + ['encapsulateBits']); + const clonedPrivateKey = structuredClone(privateKey); + const clonedPublicKey = structuredClone(publicKeyOnly); + const portPrivateKey = await roundTripViaMessageChannel(privateKey); + const portPublicKey = await roundTripViaMessageChannel(publicKeyOnly); + assert.deepStrictEqual(clonedPrivateKey, privateKey); + assert.deepStrictEqual(clonedPublicKey, publicKeyOnly); + assert.deepStrictEqual(portPrivateKey, privateKey); + assert.deepStrictEqual(portPublicKey, publicKeyOnly); + assert(Buffer.from(await subtle.exportKey('raw-seed', clonedPrivateKey)) + .equals(seed)); + assert(Buffer.from(await subtle.exportKey('raw-seed', portPrivateKey)) + .equals(seed)); + assert(Buffer.from((await subtle.exportKey('jwk', clonedPrivateKey)).priv, 'base64url') + .equals(seed)); + assert(Buffer.from((await subtle.exportKey('jwk', portPrivateKey)).priv, 'base64url') + .equals(seed)); + + const clonedPublicKeyFromPrivate = await subtle.getPublicKey( + clonedPrivateKey, + ['encapsulateBits']); + assert(Buffer.from(await subtle.exportKey('raw-public', clonedPublicKeyFromPrivate)) + .equals(publicKey)); + + const clonedVectorSharedSecret = await subtle.decapsulateBits( + algorithm, + clonedPrivateKey, + ciphertext); + assert(Buffer.from(clonedVectorSharedSecret).equals(sharedSecret)); + const portVectorSharedSecret = await subtle.decapsulateBits( + algorithm, + portPrivateKey, + ciphertext); + assert(Buffer.from(portVectorSharedSecret).equals(sharedSecret)); + + const { publicKey: differentPublicKey } = await subtle.generateKey( + algorithm, + true, + ['encapsulateBits', 'decapsulateBits']); + assert.notDeepStrictEqual(publicKeyOnly, differentPublicKey); + + const encapsulated = await subtle.encapsulateBits(algorithm, publicKeyOnly); + const decapsulated = await subtle.decapsulateBits( + algorithm, + privateKey, + encapsulated.ciphertext); + assert(Buffer.from(decapsulated).equals(Buffer.from(encapsulated.sharedKey))); + const clonedDecapsulated = await subtle.decapsulateBits( + algorithm, + clonedPrivateKey, + encapsulated.ciphertext); + assert(Buffer.from(clonedDecapsulated).equals(Buffer.from(encapsulated.sharedKey))); + + const clonedEncapsulated = await subtle.encapsulateBits(algorithm, clonedPublicKey); + const originalDecapsulated = await subtle.decapsulateBits( + algorithm, + privateKey, + clonedEncapsulated.ciphertext); + assert(Buffer.from(originalDecapsulated).equals(Buffer.from(clonedEncapsulated.sharedKey))); + + const portEncapsulated = await subtle.encapsulateBits(algorithm, portPublicKey); + const portDecapsulated = await subtle.decapsulateBits( + algorithm, + privateKey, + portEncapsulated.ciphertext); + assert(Buffer.from(portDecapsulated).equals(Buffer.from(portEncapsulated.sharedKey))); +} + +async function testFailures(vector) { + const algorithm = { name: vector.name }; + const { seed, publicKey, ciphertext } = buffers(vector); + const privateKey = await subtle.importKey( + 'raw-seed', + seed, + algorithm, + true, + ['decapsulateBits']); + const publicKeyOnly = await subtle.getPublicKey(privateKey, ['encapsulateBits']); + const jwk = await subtle.exportKey('jwk', privateKey); + + // JWK key usage validation precedes `key_ops` validation. + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: ['encapsulateBits', 'encapsulateBits'] }, + algorithm, + true, + ['encapsulateBits']), + { + name: 'SyntaxError', + message: `Unsupported key usage for ${vector.name} key`, + }); + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: ['decapsulateBits', 'decapsulateBits'] }, + algorithm, + true, + ['decapsulateBits']), + { name: 'DataError', message: 'Duplicate key operation' }); + + await assert.rejects( + subtle.importKey('raw-public', Buffer.alloc(publicKey.byteLength - 1), algorithm, true, ['encapsulateBits']), + { name: 'DataError' }); + await assert.rejects( + subtle.importKey('raw-seed', Buffer.alloc(seed.byteLength - 1), algorithm, true, ['decapsulateBits']), + { name: 'DataError' }); + await assert.rejects( + subtle.importKey('raw-public', publicKey, algorithm, true, ['decapsulateBits']), + { name: 'SyntaxError' }); + await assert.rejects( + subtle.importKey('raw-seed', seed, algorithm, true, ['encapsulateBits']), + { name: 'SyntaxError' }); + await assert.rejects( + subtle.importKey('raw', publicKey, algorithm, true, ['encapsulateBits']), + { name: 'NotSupportedError' }); + await assert.rejects( + subtle.importKey('raw-private', seed, algorithm, true, ['decapsulateBits']), + { name: 'NotSupportedError' }); + await assert.rejects( + subtle.exportKey('spki', publicKeyOnly), + { name: 'NotSupportedError' }); + await assert.rejects( + subtle.exportKey('pkcs8', privateKey), + { name: 'NotSupportedError' }); + await assert.rejects( + subtle.decapsulateBits(algorithm, privateKey, Buffer.alloc(ciphertext.byteLength - 1)), + { name: 'OperationError' }); + + const badCiphertext = Buffer.from(ciphertext); + badCiphertext.fill( + 0, + badCiphertext.byteLength - lengths[vector.name].groupElement); + await assert.rejects( + subtle.decapsulateBits(algorithm, privateKey, badCiphertext), + { name: 'OperationError' }); + + if (vector.name !== 'MLKEM768-X25519') { + const groupElementLength = lengths[vector.name].groupElement; + const nonCanonicalPublicKey = Buffer.from(publicKey); + const publicKeyPointOffset = + nonCanonicalPublicKey.byteLength - groupElementLength; + assert.strictEqual(nonCanonicalPublicKey[publicKeyPointOffset], 0x04); + nonCanonicalPublicKey[publicKeyPointOffset] = + 0x06 | (nonCanonicalPublicKey[nonCanonicalPublicKey.byteLength - 1] & 1); + await assert.rejects( + subtle.importKey( + 'raw-public', + nonCanonicalPublicKey, + algorithm, + true, + ['encapsulateBits']), + { name: 'DataError' }); + + const nonCanonicalCiphertext = Buffer.from(ciphertext); + const ciphertextPointOffset = + nonCanonicalCiphertext.byteLength - groupElementLength; + assert.strictEqual(nonCanonicalCiphertext[ciphertextPointOffset], 0x04); + nonCanonicalCiphertext[ciphertextPointOffset] = + 0x06 | (nonCanonicalCiphertext[nonCanonicalCiphertext.byteLength - 1] & 1); + await assert.rejects( + subtle.decapsulateBits(algorithm, privateKey, nonCanonicalCiphertext), + { name: 'OperationError' }); + } + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, alg: 'ML-KEM-768' }, + algorithm, + true, + ['decapsulateBits']), + { name: 'DataError' }); + + const badPublicKey = Buffer.from(publicKey); + badPublicKey[0] ^= 1; + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, pub: badPublicKey.toString('base64url') }, + algorithm, + true, + ['decapsulateBits']), + { name: 'DataError' }); +} + +function testSupports(vector) { + const algorithm = { name: vector.name }; + assert(SubtleCrypto.supports('generateKey', algorithm)); + assert(SubtleCrypto.supports('importKey', algorithm)); + assert(SubtleCrypto.supports('exportKey', algorithm)); + assert(SubtleCrypto.supports('getPublicKey', algorithm)); + assert(SubtleCrypto.supports('encapsulateBits', algorithm)); + assert(SubtleCrypto.supports('decapsulateBits', algorithm)); + assert(SubtleCrypto.supports('encapsulateKey', algorithm, 'HKDF')); + assert(SubtleCrypto.supports('decapsulateKey', algorithm, 'HKDF')); + assert(!SubtleCrypto.supports('sign', algorithm)); +} + +(async () => { + await testAlgorithmConfigPrototypePollution(); + for (const vector of vectors) { + testSupports(vector); + await testGeneratedRoundTrip(vector); + await testVectorRoundTrip(vector); + await testFailures(vector); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index c1125412892..33716095751 100644 --- a/test/parallel/test-webcrypto-keygen-kmac.js +++ b/test/parallel/test-webcrypto-keygen-kmac.js @@ -5,7 +5,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); @@ -13,38 +13,45 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { subtle } = globalThis.crypto; +const fips = hasFIPS(); const usages = ['sign', 'verify']; async function test(name, length) { - length ??= name === 'KMAC128' ? 128 : 256; - const key = await subtle.generateKey({ - name, - length, - }, true, usages); - - assert(key); - assert(isCryptoKey(key)); - - assert.strictEqual(key.type, 'secret'); - assert.strictEqual(key.toString(), '[object CryptoKey]'); - assert.strictEqual(key.extractable, true); - assert.deepStrictEqual(key.usages, usages); - assert.strictEqual(key.algorithm.name, name); - assert.strictEqual(key.algorithm.length, length); - assert.strictEqual(key.algorithm, key.algorithm); - assert.strictEqual(key.usages, key.usages); - - const raw = await subtle.exportKey('raw-secret', key); - assert.strictEqual(raw.byteLength, Math.ceil(length / 8)); + const expectedLength = length ?? (name === 'KMAC128' ? 128 : 256); + const algorithm = { name }; + if (length !== undefined) + algorithm.length = length; + + if (fips && length !== undefined && + (length < 32 || length % 8 !== 0)) return; + + const generatedKey = await subtle.generateKey(algorithm, true, usages); + + assert(generatedKey); + assert(isCryptoKey(generatedKey)); + + assert.strictEqual(generatedKey.type, 'secret'); + assert.strictEqual(generatedKey.toString(), '[object CryptoKey]'); + assert.strictEqual(generatedKey.extractable, true); + assert.deepStrictEqual(generatedKey.usages, usages); + assert.strictEqual(generatedKey.algorithm.name, name); + assert.strictEqual(generatedKey.algorithm.length, expectedLength); + assert.strictEqual(generatedKey.algorithm, generatedKey.algorithm); + assert.strictEqual(generatedKey.usages, generatedKey.usages); + + const raw = await subtle.exportKey('raw-secret', generatedKey); + assert.strictEqual(raw.byteLength, Math.ceil(expectedLength / 8)); } const kTests = [ ['KMAC128', 0], + ['KMAC128', 32], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], ['KMAC256', 0], + ['KMAC256', 32], ['KMAC256', 128], ['KMAC256', 256], ['KMAC256'], diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 86d480740bb..951ec07484b 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -6,7 +6,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); @@ -17,6 +17,8 @@ const { } = require('crypto'); const { subtle } = globalThis.crypto; const rsaMinimumModulusLength = getFips() === 1 ? 2048 : 512; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); const { bigIntArrayToUnsignedBigInt } = require('internal/crypto/util'); @@ -71,7 +73,7 @@ const vectors = { }, 'RSASSA-PKCS1-v1_5': { algorithm: { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -83,7 +85,7 @@ const vectors = { }, 'RSA-PSS': { algorithm: { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -95,7 +97,7 @@ const vectors = { }, 'RSA-OAEP': { algorithm: { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -156,7 +158,7 @@ const vectors = { }, }; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { vectors.Ed448 = { result: 'CryptoKeyPair', usages: [ @@ -198,7 +200,7 @@ if (hasOpenSSL(3)) { } } -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { vectors[name] = { result: 'CryptoKeyPair', @@ -249,6 +251,21 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // Test bad usages { async function test(name) { + if (fips3 && name === 'ChaCha20-Poly1305') { + await assert.rejects( + subtle.generateKey({ name }, true, []), + { name: 'NotSupportedError' }); + return; + } + + if (fips35 && (name === 'X25519' || name === 'X448')) { + await assert.rejects( + subtle.generateKey({ name }, true, ['deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + return; + } + await assert.rejects( subtle.generateKey( { @@ -469,7 +486,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const kTests = [ [ 'RSASSA-PKCS1-v1_5', - 1024, + getFips() === 1 ? 2048 : 1024, Buffer.from([1, 0, 1]), 'SHA-1', ['sign'], @@ -477,7 +494,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { ], [ 'RSA-PSS', - 1024, + getFips() === 1 ? 2048 : 1024, Buffer.from([1, 0, 1]), 'SHA-256', ['sign'], @@ -486,22 +503,37 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { ]; - if (!process.features.openssl_is_boringssl) { - kTests.push( - [ - 'RSA-OAEP', - 1024, - Buffer.from([3]), - 'SHA3-256', - ['decrypt', 'unwrapKey'], - ['encrypt', 'wrapKey'], - ], - ); + let fipsExponentTest; + if (!isBoringSSL) { + if (fips3) { + fipsExponentTest = assert.rejects( + subtle.generateKey({ + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: Buffer.from([3]), + hash: 'SHA3-256', + }, true, ['decrypt', 'unwrapKey', 'encrypt', 'wrapKey']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:020000B2:rsa routines::pub exponent out of range'); + } else { + kTests.push( + [ + 'RSA-OAEP', + 1024, + Buffer.from([3]), + 'SHA3-256', + ['decrypt', 'unwrapKey'], + ['encrypt', 'wrapKey'], + ], + ); + } } else { common.printSkipMessage('Skipping unsupported SHA-3 test case'); } const tests = kTests.map((args) => test(...args)); + if (fipsExponentTest !== undefined) + tests.push(fipsExponentTest); Promise.all(tests).then(common.mustCall()); } @@ -690,7 +722,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { [1024, 'SHA-512', ['sign', 'verify']], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { kTests.push( [256, 'SHA3-256', ['sign', 'verify']], [384, 'SHA3-384', ['sign', 'verify']], @@ -722,6 +754,13 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); // Test OKP Key Generation { + async function testFipsUnsupported(name) { + await assert.rejects( + subtle.generateKey({ name }, true, ['deriveKey', 'deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } + async function test( name, privateUsages, @@ -769,7 +808,7 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); ], ]; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { kTests.push( [ 'Ed448', @@ -786,13 +825,18 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); common.printSkipMessage('Skipping unsupported Curve448 test cases'); } - const tests = kTests.map((args) => test(...args)); + const tests = kTests.map((args) => { + const [name] = args; + if (fips35 && (name === 'X25519' || name === 'X448')) + return testFipsUnsupported(name); + return test(...args); + }); Promise.all(tests).then(common.mustCall()); } // Test ML-DSA Key Generation -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { async function test( name, privateUsages, @@ -835,7 +879,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { } // Test ML-KEM Key Generation -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { async function test( name, privateUsages, @@ -872,7 +916,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const kTests = ['ML-KEM-768', 'ML-KEM-1024']; - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { kTests.unshift('ML-KEM-512'); } else { common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index da5df46390b..7e58f7e0802 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -24,7 +24,21 @@ if (!common.hasCrypto) common.skip('missing crypto'); const require = createRequire(import.meta.url); const { kSupportedAlgorithms } = require('internal/crypto/util'); +const { getFips } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips4 = hasFIPS(4); +const fips35UnavailableKeyGeneration = new Set([ + 'X25519', + 'X448', +]); +const fips3UnavailableDerivation = new Set([ + 'Argon2d', + 'Argon2i', + 'Argon2id', +]); Promise.prototype.then = common.mustNotCall('Promise.prototype.then'); @@ -333,7 +347,7 @@ function algorithm(name, params = {}) { function rsaAlgorithm(name) { return algorithm(name, { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }); @@ -864,6 +878,19 @@ for (const name of [ })); } +for (const name of [ + 'MLKEM768-P256', + 'MLKEM768-X25519', + 'MLKEM1024-P384', +]) { + addFixture(name, kemFixture({ + name, + generateAlgorithm: algorithm(name), + importAlgorithm: algorithm(name), + spki: false, + })); +} + for (const name of ['HKDF', 'PBKDF2']) { addFixture(name, kdfFixture({ name, @@ -877,8 +904,8 @@ for (const name of ['HKDF', 'PBKDF2']) { }) : algorithm(name, { hash: 'SHA-256', - salt: new Uint8Array(8), - iterations: 1, + salt: new Uint8Array(fips4 ? 16 : 8), + iterations: fips4 ? 1000 : 1, }), })); } @@ -1040,6 +1067,24 @@ for (const [name, operations] of supportedAlgorithms) { assert(fixture, `missing prototype pollution fixture for ${name}`); const ctx = { __proto__: null }; + if (fips3 && fips3UnavailableDerivation.has(name)) { + await fixture.importKey(ctx); + await assert.rejects( + fixture.deriveBits(ctx), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + continue; + } + if ((fips3 && name === 'ChaCha20-Poly1305') || + (fips35 && fips35UnavailableKeyGeneration.has(name))) { + const expected = name === 'ChaCha20-Poly1305' ? + { name: 'NotSupportedError' } : + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'; + await assert.rejects(fixture.generateKey(ctx), expected); + continue; + } + for (const operation of operationOrder) { if (!operations.has(operation)) continue; @@ -1047,7 +1092,17 @@ for (const [name, operations] of supportedAlgorithms) { typeof fixture[operation], 'function', `missing prototype pollution coverage for ${name} ${operation}`); - await fixture[operation](ctx); + if (fips3 && name === 'AES-OCB' && + (operation === 'encrypt' || operation === 'decrypt')) { + if (operation === 'decrypt') + ctx.ciphertext = new Uint8Array(); + await assert.rejects( + fixture[operation](ctx), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } else { + await fixture[operation](ctx); + } } if (typeof fixture.getPublicKey === 'function' && @@ -1085,17 +1140,22 @@ for (const name of getKeyLengthAlgorithms) { continue; } - await assertCryptoKeyResult(`get key length ${name}`, () => + const deriveKey = () => assertCryptoKeyResult(`get key length ${name}`, () => subtle.deriveKey( algorithm('PBKDF2', { hash: 'SHA-256', - salt: new Uint8Array(8), - iterations: 1, + salt: new Uint8Array(fips4 ? 16 : 8), + iterations: fips4 ? 1000 : 1, }), pbkdf2Key, target.algorithm, true, target.usages)); + if (fips3 && name === 'ChaCha20-Poly1305') { + await assert.rejects(deriveKey(), { name: 'NotSupportedError' }); + } else { + await deriveKey(); + } } // Keep one explicit unwrapKey('jwk') negative case: the parsed object must not diff --git a/test/parallel/test-webcrypto-prototype-pollution.mjs b/test/parallel/test-webcrypto-prototype-pollution.mjs index a7104c2b7ae..5ed77fc6021 100644 --- a/test/parallel/test-webcrypto-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-prototype-pollution.mjs @@ -142,16 +142,24 @@ if (supports('digest', 'cSHAKE128')) { outputLength: 256, customization: new Uint8Array([1, 2, 3]), }; - const expected = new Uint8Array(await subtle.digest(algorithm, data)); - const plain = new Uint8Array( - await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data)); - assert.notDeepStrictEqual(expected, plain); - await withPoisoned(poisonTypedArrayByteLength(0), - common.mustCall(async () => { - assert.deepStrictEqual( - new Uint8Array(await subtle.digest(algorithm, data)), - expected); - })); + if (getFips() === 1) { + await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() => + assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams customization', + }))); + } else { + const expected = new Uint8Array(await subtle.digest(algorithm, data)); + const plain = new Uint8Array( + await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data)); + assert.notDeepStrictEqual(expected, plain); + await withPoisoned(poisonTypedArrayByteLength(0), + common.mustCall(async () => { + assert.deepStrictEqual( + new Uint8Array(await subtle.digest(algorithm, data)), + expected); + })); + } } } @@ -290,17 +298,17 @@ await withPoisoned( // enforceRangeOptions(): [EnforceRange] uses IntegerPart, not round-half-even. { const key = await subtle.importKey( - 'raw-secret', new Uint8Array(4), 'PBKDF2', false, ['deriveBits']); + 'raw-secret', new Uint8Array(32), 'PBKDF2', false, ['deriveBits']); const pbkdf2 = (iterations) => subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations, - }, key, 8); + }, key, 112); - const expected = new Uint8Array(await pbkdf2(1)); + const expected = new Uint8Array(await pbkdf2(1000)); await withPoisoned(inherited('clamp', true), common.mustCall(async () => { - assert.deepStrictEqual(new Uint8Array(await pbkdf2(1.5)), expected); + assert.deepStrictEqual(new Uint8Array(await pbkdf2(1000.5)), expected); })); } diff --git a/test/parallel/test-webcrypto-raw-format-aliases.js b/test/parallel/test-webcrypto-raw-format-aliases.js index 94e9474fde1..bce0c353904 100644 --- a/test/parallel/test-webcrypto-raw-format-aliases.js +++ b/test/parallel/test-webcrypto-raw-format-aliases.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); function getAlgorithmName(algorithm) { return typeof algorithm === 'string' ? algorithm : algorithm.name; @@ -50,7 +52,7 @@ async function assertPublicKeyDoesNotAcceptRawSecret( importUsages); } -Promise.all([ +const tests = [ assertSecretKeyDoesNotAcceptRawPublic('HKDF'), assertSecretKeyDoesNotAcceptRawPublic('PBKDF2'), assertPublicKeyDoesNotAcceptRawSecret( @@ -65,8 +67,18 @@ Promise.all([ 'Ed25519', ['sign', 'verify'], ['verify']), - assertPublicKeyDoesNotAcceptRawSecret( +]; + +if (rejectsXCurves) { + tests.push(assert.rejects( + subtle.generateKey('X25519', true, ['deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported')); +} else { + tests.push(assertPublicKeyDoesNotAcceptRawSecret( 'X25519', ['deriveBits'], - []), -]).then(common.mustCall()); + [])); +} + +Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-ecdsa.js b/test/parallel/test-webcrypto-sign-verify-ecdsa.js index eb7814efa55..94e3eff02eb 100644 --- a/test/parallel/test-webcrypto-sign-verify-ecdsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ecdsa.js @@ -6,7 +6,10 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsSha1Signing = hasFIPS(3) && !hasFIPS(3, 5); const vectors = require('../fixtures/crypto/ecdsa')(); @@ -50,7 +53,7 @@ async function testVerify({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -173,7 +176,7 @@ async function testSign({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -229,6 +232,32 @@ async function testSign({ name, for (let i = 0; i < vectors.length; ++i) { const vector = vectors[i]; + if (rejectsSha1Signing && vector.hash === 'SHA-1') { + const publicKey = await subtle.importKey( + 'spki', + vector.publicKeyBuffer, + { name: vector.name, namedCurve: vector.namedCurve }, + false, + ['verify']); + const privateKey = await subtle.importKey( + 'pkcs8', + vector.privateKeyBuffer, + { name: vector.name, namedCurve: vector.namedCurve }, + false, + ['sign']); + assert(await subtle.verify( + { name: vector.name, hash: vector.hash }, + publicKey, + vector.signature, + vector.plaintext)); + await assert.rejects( + subtle.sign( + { name: vector.name, hash: vector.hash }, + privateKey, + vector.plaintext), + { name: 'OperationError' }); + continue; + } variations.push(testVerify(vector)); variations.push(testSign(vector)); } diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index b35e94df44c..2c1b0bd4400 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -5,7 +5,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); const crypto = require('crypto'); @@ -31,7 +31,7 @@ const smallOrderVerifyVectors = [ }, ]; -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { smallOrderVerifyVectors.push({ name: 'Ed448', publicKey: Buffer.concat([Buffer.from([1]), Buffer.alloc(56)]), @@ -92,7 +92,7 @@ async function testVerify({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -217,7 +217,7 @@ async function testSign({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-sign-verify-hmac.js b/test/parallel/test-webcrypto-sign-verify-hmac.js index ac3841fad79..bbe5dd49810 100644 --- a/test/parallel/test-webcrypto-sign-verify-hmac.js +++ b/test/parallel/test-webcrypto-sign-verify-hmac.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; const vectors = require('../fixtures/crypto/hmac')(); @@ -35,7 +36,7 @@ async function testVerify({ hash, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -126,7 +127,7 @@ async function testSign({ hash, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index f93fc293b2a..ac0b738bcd5 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -5,16 +5,38 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); const assert = require('assert'); const { subtle } = globalThis.crypto; +const fips = hasFIPS(); +const fips4 = hasFIPS(4); const vectors = require('../fixtures/crypto/kmac')(); +function isFipsProviderUnsupported(err) { + return err.name === 'OperationError' && + err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'; +} + +function usesNonFipsImplementation({ key, keyLength, outputLength }) { + const keyLengthInBits = keyLength ?? key.byteLength * 8; + return outputLength === 0 || + outputLength % 8 !== 0 || + keyLengthInBits < 32 || + keyLengthInBits % 8 !== 0; +} + +function isFips4Incompatible({ key, keyLength, outputLength }) { + const keyLengthInBits = keyLength ?? key.byteLength * 8; + return keyLengthInBits < 128 || + keyLengthInBits % 8 !== 0 || + outputLength % 8 !== 0; +} + async function testVerify({ algorithm, key, keyLength, @@ -193,8 +215,17 @@ async function testSign({ algorithm, const variations = []; for (const vector of vectors) { - variations.push(testVerify(vector)); - variations.push(testSign(vector)); + if (fips && usesNonFipsImplementation(vector)) continue; + + if (fips4 && isFips4Incompatible(vector)) { + variations.push(assert.rejects( + testVerify(vector), isFipsProviderUnsupported)); + variations.push(assert.rejects( + testSign(vector), isFipsProviderUnsupported)); + } else { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + } } await Promise.all(variations); @@ -209,45 +240,66 @@ async function testSign({ algorithm, ['sign', 'verify']); const algorithm = { name: 'KMAC128', - outputLength: 9, + outputLength: fips ? 16 : 9, customization: new Uint8Array(), }; const data = new Uint8Array([1, 2, 3]); const signature = await subtle.sign(algorithm, key, data); assert.strictEqual(signature.byteLength, 2); - assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); + if (!fips) + assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); assert(await subtle.verify(algorithm, key, signature, data)); - const signature16 = new Uint8Array(await subtle.sign({ - ...algorithm, - outputLength: 16, - }, key, data)); - signature16[1] &= 0b10000000; - assert.notDeepStrictEqual(new Uint8Array(signature), signature16); + if (fips) { + const signature128 = await subtle.sign({ + ...algorithm, + outputLength: 128, + }, key, data); + assert.strictEqual(signature128.byteLength, 16); + assert(await subtle.verify({ + ...algorithm, + outputLength: 128, + }, key, signature128, data)); + } else { + const signature16 = new Uint8Array(await subtle.sign({ + ...algorithm, + outputLength: 16, + }, key, data)); + signature16[1] &= 0b10000000; + assert.notDeepStrictEqual(new Uint8Array(signature), signature16); + } const invalidSignature = new Uint8Array(signature); - invalidSignature[1] |= 0b00000001; + if (fips) + invalidSignature[0] ^= 0b00000001; + else + invalidSignature[1] |= 0b00000001; assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); - const nonByteKey = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff, 0xff, 0xff]), - { name: 'KMAC128', length: 25 }, - false, - ['sign', 'verify']); - const nonByteKeySignature = await subtle.sign({ - ...algorithm, - outputLength: 16, - }, nonByteKey, data); - assert.strictEqual(nonByteKeySignature.byteLength, 2); - assert(await subtle.verify({ - ...algorithm, - outputLength: 16, - }, nonByteKey, nonByteKeySignature, data)); + if (!fips) { + const nonByteKey = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff, 0xff, 0xff]), + { name: 'KMAC128', length: 25 }, + false, + ['sign', 'verify']); + const nonByteKeySignature = subtle.sign({ + ...algorithm, + outputLength: 16, + }, nonByteKey, data); + const result = await nonByteKeySignature; + assert.strictEqual(result.byteLength, 2); + assert(await subtle.verify({ + ...algorithm, + outputLength: 16, + }, nonByteKey, result, data)); + } })().then(common.mustCall()); (async function() { + if (fips) return; + const data = new Uint8Array([1, 2, 3]); for (const name of ['KMAC128', 'KMAC256']) { @@ -265,9 +317,10 @@ async function testSign({ algorithm, assert.strictEqual(key.algorithm.length, keyData.byteLength * 8); const algorithm = { name, outputLength: 256 }; - const signature = await subtle.sign(algorithm, key, data); - assert.strictEqual(signature.byteLength, 32); - assert(await subtle.verify(algorithm, key, signature, data)); + const signature = subtle.sign(algorithm, key, data); + const result = await signature; + assert.strictEqual(result.byteLength, 32); + assert(await subtle.verify(algorithm, key, result, data)); } } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index 67f90d2a0e5..6925c0782f8 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -5,9 +5,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3, 5) && !isBoringSSL) common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); @@ -43,7 +43,7 @@ async function testVerify({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -154,7 +154,7 @@ async function testSign({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 3f8a916846c..8ba0257b177 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -6,7 +6,10 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); +const rejectsSha1Signing = hasFIPS(3) && !hasFIPS(3, 5); const rsa_pkcs = require('../fixtures/crypto/rsa_pkcs'); const rsa_pss = require('../fixtures/crypto/rsa_pss'); @@ -194,6 +197,23 @@ async function testSign({ }); } +async function testFipsSignRejected({ + algorithm, + hash, + privateKeyBuffer, + plaintext, +}) { + const privateKey = await subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + ['sign']); + await assert.rejects( + subtle.sign(algorithm, privateKey, plaintext), + { name: 'OperationError' }); +} + async function testSaltLength(keyLength, hash, hLen) { const { publicKey, privateKey } = await subtle.generateKey({ name: 'RSA-PSS', @@ -234,25 +254,43 @@ async function testSaltLength(keyLength, hash, hLen) { rsa_pkcs().forEach((vector) => { variations.push(testVerify(vector)); - variations.push(testSign(vector)); + variations.push(rejectsSha1Signing && vector.hash === 'SHA-1' ? + testFipsSignRejected(vector) : testSign(vector)); }); rsa_pss().forEach((vector) => { variations.push(testVerify(vector)); - variations.push(testSign(vector)); + variations.push(rejectsSha1Signing && vector.hash === 'SHA-1' ? + testFipsSignRejected(vector) : testSign(vector)); }); - for (const keyLength of [1024, 2048]) { + if (fips3) { + variations.push(assert.rejects( + subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, false, ['sign', 'verify']), + { + name: 'OperationError', + message: 'algorithm.modulusLength must be at least 2048', + })); + } + + for (const keyLength of fips3 ? [2048] : [1024, 2048]) { for (const [hash, hLen] of [ ['SHA-1', 20], ['SHA-256', 32], ['SHA-384', 48], ['SHA-512', 64], - ...(!process.features.openssl_is_boringssl ? [ + ...(!isBoringSSL ? [ ['SHA3-256', 32], ['SHA3-384', 48], ['SHA3-512', 64], ] : []), ]) { + if (rejectsSha1Signing && hash === 'SHA-1') + continue; variations.push(testSaltLength(keyLength, hash, hLen)); } } diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 0a6f5cffe7b..295f1378238 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -5,9 +5,10 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests @@ -19,7 +20,7 @@ const { subtle } = globalThis.crypto; const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, ['sign', 'verify']); @@ -151,7 +152,7 @@ if (hasOpenSSL(3)) { } // Test Sign/Verify Ed448 -if (!process.features.openssl_is_boringssl) { +if (!isBoringSSL) { async function test(data) { const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ @@ -173,7 +174,7 @@ if (!process.features.openssl_is_boringssl) { } // Test Sign/Verify ML-DSA -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { +if (hasOpenSSL(3, 5) || isBoringSSL) { async function test(name, data) { const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index d0a88f0057b..15874102a22 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -4,6 +4,11 @@ if (!common.hasCrypto) common.skip('missing crypto'); import * as assert from 'node:assert'; +import { hasFIPS } from '../common/crypto.js'; + +if (hasFIPS(3)) + common.skip('SubtleCrypto.supports() does not reflect FIPS provider availability'); + const { SubtleCrypto } = globalThis; const sources = [ @@ -66,6 +71,9 @@ function getSharedKeyLength(alg) { case 'ml-kem-512': case 'ml-kem-768': case 'ml-kem-1024': + case 'mlkem768-p256': + case 'mlkem768-x25519': + case 'mlkem1024-p384': return 256; } } diff --git a/test/parallel/test-webcrypto-webidl-brand.js b/test/parallel/test-webcrypto-webidl-brand.js new file mode 100644 index 00000000000..d4d78568490 --- /dev/null +++ b/test/parallel/test-webcrypto-webidl-brand.js @@ -0,0 +1,38 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; +const { CryptoKey } = require('internal/crypto/keys'); +const { converters } = require('internal/crypto/webidl'); + +async function main() { + const bytes = new Uint8Array(16); + const key = await subtle.importKey('raw', bytes, 'AES-GCM', true, ['encrypt']); + + for (const value of [ + { __proto__: CryptoKey.prototype }, + { __proto__: key }, + Object.create(CryptoKey.prototype, Object.getOwnPropertyDescriptors(key)), + new Proxy(key, {}), + ]) { + assert.throws(() => converters.CryptoKey(value), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + await assert.rejects(subtle.exportKey('raw', value), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + } + + Object.setPrototypeOf(key, null); + assert.strictEqual(converters.CryptoKey(key), key); + assert.deepStrictEqual(new Uint8Array(await subtle.exportKey('raw', key)), bytes); +} + +main().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 2078ec617a1..f557f2ae473 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -5,10 +5,13 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); const kWrappingData = { 'RSA-OAEP': { @@ -35,7 +38,7 @@ const kWrappingData = { wrap: { iv: new Uint8Array(16), additionalData: new Uint8Array(16), - tagLength: 64 + tagLength: fips3 ? 128 : 64 }, pair: false }, @@ -54,7 +57,10 @@ const kWrappingData = { } }; -if (hasOpenSSL(3)) { +if (fips3) + delete kWrappingData['ChaCha20-Poly1305']; + +if (hasOpenSSL(3) && !fips3) { kWrappingData['AES-OCB'] = { generate: { length: 128 }, wrap: { @@ -87,7 +93,7 @@ async function generateKeysToWrap() { { algorithm: { name: 'RSASSA-PKCS1-v1_5', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -98,7 +104,7 @@ async function generateKeysToWrap() { { algorithm: { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -109,7 +115,7 @@ async function generateKeysToWrap() { { algorithm: { name: 'RSA-OAEP', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -200,7 +206,7 @@ async function generateKeysToWrap() { }, ]; - if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + if (hasOpenSSL(3, 5) || isBoringSSL) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { parameters.push({ algorithm: { name }, @@ -211,7 +217,7 @@ async function generateKeysToWrap() { } } - if (!process.features.openssl_is_boringssl) { + if (!isBoringSSL) { parameters.push( { algorithm: { @@ -234,6 +240,20 @@ async function generateKeysToWrap() { common.printSkipMessage('Skipping unsupported Curve test cases'); } + if (fips3) { + const unsupported = new Set([ + 'ChaCha20-Poly1305', + ]); + if (fips35) { + unsupported.add('X25519'); + unsupported.add('X448'); + } + for (let i = parameters.length - 1; i >= 0; --i) { + if (unsupported.has(parameters[i].algorithm.name)) + parameters.splice(i, 1); + } + } + const allkeys = await Promise.all(parameters.map(async (params) => { const usages = 'usages' in params ? params.usages : @@ -360,6 +380,37 @@ function testWrapping(name, keys) { } (async function() { + if (fips3) { + await assert.rejects( + subtle.generateKey( + { name: 'ChaCha20-Poly1305' }, true, ['wrapKey']), + { name: 'NotSupportedError' }); + + if (fips35) { + for (const name of ['X25519', 'X448']) { + await assert.rejects( + subtle.generateKey({ name }, true, ['deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } + } + + const wrappingKey = await subtle.generateKey( + { name: 'AES-OCB', length: 128 }, true, ['wrapKey']); + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256', length: 256 }, + true, + ['sign']); + await assert.rejects( + subtle.wrapKey( + 'raw', + key, + wrappingKey, + { name: 'AES-OCB', iv: new Uint8Array(15), tagLength: 128 }), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } + await generateWrappingKeys(); const keys = await generateKeysToWrap(); const variations = []; @@ -434,7 +485,7 @@ async function testNonByteLengthWrapUnwrap({ implicitAlgorithm: hmacAlgorithm, }); - if (hasOpenSSL(3)) { + if (hasOpenSSL(3) && getFips() !== 1) { const kmacAlgorithm = { name: 'KMAC128' }; const kmacKey = await subtle.importKey( 'raw-secret', diff --git a/test/parallel/test-webstreams-writer-closed-abort-pending.js b/test/parallel/test-webstreams-writer-closed-abort-pending.js new file mode 100644 index 00000000000..b03ccbe3ef9 --- /dev/null +++ b/test/parallel/test-webstreams-writer-closed-abort-pending.js @@ -0,0 +1,42 @@ +'use strict'; + +// The writer's closed promise stays pending until the sink's abort() +// algorithm settles, even when it is first observed after the stream has +// already reached the 'errored' state. + +const common = require('../common'); +const assert = require('assert'); +const { setImmediate: immediate } = require('timers/promises'); +const { WritableStream } = require('stream/web'); + +async function main() { + const error = new Error('boom'); + const { promise: abortComplete, resolve: finishAbort } = Promise.withResolvers(); + const ws = new WritableStream({ + abort: common.mustCall((reason) => { + assert.strictEqual(reason, error); + return abortComplete; + }), + }); + const writer = ws.getWriter(); + const aborted = writer.abort(error); + + // Lets the stream finish erroring and call the sink's abort(). + await immediate(); + + let closedSettled = false; + const closed = writer.closed.catch(common.mustCall((reason) => { + closedSettled = true; + assert.strictEqual(reason, error); + })); + + await immediate(); + assert.strictEqual(closedSettled, false); + + finishAbort(); + await aborted; + await closed; + assert.strictEqual(closedSettled, true); +} + +main().then(common.mustCall()); diff --git a/test/parallel/test-whatwg-transformstream-parked-write-error.js b/test/parallel/test-whatwg-transformstream-parked-write-error.js new file mode 100644 index 00000000000..b457fec573f --- /dev/null +++ b/test/parallel/test-whatwg-transformstream-parked-write-error.js @@ -0,0 +1,57 @@ +'use strict'; + +require('../common'); +const { test } = require('node:test'); +const assert = require('node:assert'); +const { TransformStream } = require('stream/web'); +const { setImmediate } = require('timers/promises'); + +// A transform sink write arriving while the stream has backpressure is +// parked until backpressure clears. These cases complete a parked write +// while the writable side is already erroring. +// +// The setImmediate() lets the start algorithm settle so the write below +// reaches the sink and parks (backpressure is set until the readable +// side pulls). + +test('readable.cancel() rejects a parked write with the cancel reason', async () => { + const stream = new TransformStream(); + const writer = stream.writable.getWriter(); + await setImmediate(); + + const reason = new Error('cancelled'); + const write = writer.write('parked'); + await stream.readable.cancel(reason); + await assert.rejects(write, (err) => err === reason); +}); + +test('controller.error() rejects a parked write with the stored error', async () => { + let controller; + const stream = new TransformStream({ + start(c) { controller = c; }, + }); + const writer = stream.writable.getWriter(); + await setImmediate(); + + const reason = new Error('boom'); + const write = writer.write('parked'); + controller.error(reason); + await assert.rejects(write, (err) => err === reason); + await assert.rejects(writer.closed, (err) => err === reason); +}); + +test('controller.terminate() rejects a parked write', async () => { + let controller; + const stream = new TransformStream({ + start(c) { controller = c; }, + }); + const writer = stream.writable.getWriter(); + await setImmediate(); + + const write = writer.write('parked'); + controller.terminate(); + await assert.rejects(write, { + name: 'TypeError', + message: /terminated/, + }); +}); diff --git a/test/parallel/test-x509-escaping.js b/test/parallel/test-x509-escaping.js index ab91e334555..47901140cac 100644 --- a/test/parallel/test-x509-escaping.js +++ b/test/parallel/test-x509-escaping.js @@ -10,7 +10,7 @@ const { X509Certificate } = require('crypto'); const tls = require('tls'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); // Test that all certificate chains provided by the reporter are rejected. { @@ -59,8 +59,8 @@ const { hasOpenSSL3 } = require('../common/crypto'); 'IP Address:8.8.8.8', 'IP Address:8.8.4.4', // For backward-compatibility, include invalid IP address lengths. - hasOpenSSL3 ? 'IP Address:' : 'IP Address:', - hasOpenSSL3 ? 'IP Address:' : 'IP Address:', + hasOpenSSL(3) ? 'IP Address:' : 'IP Address:', + hasOpenSSL(3) ? 'IP Address:' : 'IP Address:', // IPv6 addresses are represented as OpenSSL does. 'IP Address:A0B:C0D:E0F:0:0:0:7A7B:7C7D', // Regular email addresses don't require escaping. @@ -88,22 +88,22 @@ const { hasOpenSSL3 } = require('../common/crypto'); // This is an OID that will likely never be assigned to anything, thus // OpenSSL should not know it. 'Registered ID:1.3.9999.12.34', - hasOpenSSL3 ? + hasOpenSSL(3) ? 'othername:XmppAddr:abc123' : 'othername:', - hasOpenSSL3 ? + hasOpenSSL(3) ? 'othername:"XmppAddr:abc123\\u002c DNS:good.example.com"' : 'othername:', - hasOpenSSL3 ? + hasOpenSSL(3) ? 'othername:"XmppAddr:good.example.com\\u0000abc123"' : 'othername:', // This is unsupported because the OID is not recognized. 'othername:', - hasOpenSSL3 ? 'othername:SRVName:abc123' : 'othername:', + hasOpenSSL(3) ? 'othername:SRVName:abc123' : 'othername:', // This is unsupported because it is an SRVName with a UTF8String value, // which is not allowed for SRVName. 'othername:', - hasOpenSSL3 ? + hasOpenSSL(3) ? 'othername:"SRVName:abc\\u0000def"' : 'othername:', ]; @@ -173,7 +173,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); ], }, }, - hasOpenSSL3 ? { + hasOpenSSL(3) ? { text: 'OCSP - othername:XmppAddr:good.example.com\n' + 'OCSP - othername:\n' + 'OCSP - othername:SRVName:abc123', @@ -196,7 +196,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); ], }, }, - hasOpenSSL3 ? { + hasOpenSSL(3) ? { text: 'OCSP - othername:"XmppAddr:good.example.com\\u0000abc123"', legacy: { 'OCSP - othername': [ @@ -222,7 +222,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); // Test the subjectAltName property of the X509Certificate API. const cert = new X509Certificate(pem); assert.strictEqual(cert.infoAccess, - `${expected.text}${hasOpenSSL3 ? '' : '\n'}`); + `${expected.text}${hasOpenSSL(3) ? '' : '\n'}`); // Test that the certificate obtained by checkServerIdentity has the correct // subjectaltname property. @@ -439,7 +439,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); // The hostname is the CN, but not a SAN entry. const servername = 'good.example.com'; - const cnFallback = process.features.openssl_is_boringssl ? undefined : + const cnFallback = isBoringSSL ? undefined : servername; const certX509 = new X509Certificate(cert); assert.strictEqual(certX509.subject, `CN=${servername}`); @@ -485,7 +485,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); assert.strictEqual(certX509.subjectAltName, 'IP Address:1.2.3.4'); // The newer X509Certificate API allows customizing this behavior: - const cnFallback = process.features.openssl_is_boringssl ? undefined : + const cnFallback = isBoringSSL ? undefined : servername; assert.strictEqual(certX509.checkHost(servername), cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'default' }), diff --git a/test/parallel/test-zlib-zstd-dictionary.js b/test/parallel/test-zlib-zstd-dictionary.js index 41b2ab90238..b40107b50c0 100644 --- a/test/parallel/test-zlib-zstd-dictionary.js +++ b/test/parallel/test-zlib-zstd-dictionary.js @@ -40,3 +40,24 @@ for (const dict of [arrayBuffer, uint8, dataView]) { const decompressed = zlib.zstdDecompressSync(compressed, { dictionary: dict }); assert.strictEqual(decompressed.toString(), input.toString()); } + +for (const dictionary of [null, 'string', 123, true, {}, [1, 2, 3]]) { + const options = { dictionary }; + const expected = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }; + + assert.throws(() => zlib.createZstdCompress(options), expected); + assert.throws(() => zlib.createZstdDecompress(options), expected); + assert.throws(() => zlib.zstdCompressSync(input, options), expected); + assert.throws(() => zlib.zstdDecompressSync(input, options), expected); + assert.throws( + () => zlib.zstdCompress(input, options, common.mustNotCall()), + expected, + ); + assert.throws( + () => zlib.zstdDecompress(input, options, common.mustNotCall()), + expected, + ); +} diff --git a/test/parallel/test-zlib-zstd-reset.js b/test/parallel/test-zlib-zstd-reset.js new file mode 100644 index 00000000000..839669bc630 --- /dev/null +++ b/test/parallel/test-zlib-zstd-reset.js @@ -0,0 +1,68 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { finished } = require('stream/promises'); +const test = require('node:test'); +const zlib = require('zlib'); + +const dictionary = Buffer.from( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +); +const input = Buffer.from( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(100), +); + +async function collect(stream, ...data) { + const chunks = []; + stream.on('data', (chunk) => chunks.push(chunk)); + for (let i = 0; i < data.length - 1; i++) { + stream.write(data[i]); + } + stream.end(data[data.length - 1]); + await finished(stream); + return Buffer.concat(chunks); +} + +test('ZstdCompress reset preserves its initial options', async () => { + const options = { + dictionary, + pledgedSrcSize: input.length, + params: { + [zlib.constants.ZSTD_c_compressionLevel]: 19, + [zlib.constants.ZSTD_c_checksumFlag]: 1, + }, + }; + const expected = await collect(zlib.createZstdCompress(options), input); + const reset = zlib.createZstdCompress(options); + reset.reset(); + + assert.deepStrictEqual(await collect(reset, input), expected); +}); + +test('ZstdDecompress reset preserves its dictionary', async () => { + const compressed = zlib.zstdCompressSync(input, { dictionary }); + const decompress = zlib.createZstdDecompress({ dictionary }); + decompress.reset(); + + assert.deepStrictEqual(await collect(decompress, compressed), input); +}); + +test('ZstdDecompress reset preserves its parameters', async () => { + const compressed = await collect(zlib.createZstdCompress({ + params: { + [zlib.constants.ZSTD_c_windowLog]: 11, + }, + }), Buffer.alloc(2048), Buffer.alloc(2048)); + const decompress = zlib.createZstdDecompress({ + params: { + [zlib.constants.ZSTD_d_windowLogMax]: 10, + }, + }); + decompress.reset(); + + await assert.rejects(collect(decompress, compressed), { + code: 'ZSTD_error_frameParameter_windowTooLarge', + }); +}); diff --git a/test/pummel/test-crypto-argon2-nonblocking-constructor.js b/test/pummel/test-crypto-argon2-nonblocking-constructor.js index 37cb5363d43..4e0ff50c3fa 100644 --- a/test/pummel/test-crypto-argon2-nonblocking-constructor.js +++ b/test/pummel/test-crypto-argon2-nonblocking-constructor.js @@ -4,10 +4,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); +if (hasFIPS(3)) + common.skip('Argon2 is not available in FIPS mode'); // Regression test for https://github.com/nodejs/node/issues/62861. // `AdditionalConfig` used to invoke the full Argon2 KDF synchronously inside diff --git a/test/pummel/test-crypto-dh-hash.js b/test/pummel/test-crypto-dh-hash.js index 03b4a9c831a..e428df491a3 100644 --- a/test/pummel/test-crypto-dh-hash.js +++ b/test/pummel/test-crypto-dh-hash.js @@ -30,9 +30,9 @@ if (common.isPi()) { common.skip('Too slow for Raspberry Pi devices'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { common.skip('Too slow when dynamically linked against OpenSSL 1.1.1'); } diff --git a/test/pummel/test-crypto-dh-keys.js b/test/pummel/test-crypto-dh-keys.js index 8aa1e30e354..cbfec26d14a 100644 --- a/test/pummel/test-crypto-dh-keys.js +++ b/test/pummel/test-crypto-dh-keys.js @@ -32,12 +32,22 @@ if (common.isPi()) { const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS, isBoringSSL } = require('../common/crypto'); for (const name of ['modp1', 'modp2', 'modp5', 'modp14', 'modp15', 'modp16', 'modp17']) { // modp1 is 768 bits, FIPS requires >= 1024. // BoringSSL does not support modp1 or modp2. - if ((name === 'modp1' && crypto.getFips()) || - (process.features.openssl_is_boringssl && + if (hasFIPS(3) && ['modp1', 'modp2', 'modp5'].includes(name)) { + const parameters = crypto.getDiffieHellman(name); + const group = crypto.createDiffieHellman( + parameters.getPrime(), parameters.getGenerator()); + assert.throws(() => group.generateKeys(), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + continue; + } + if ((name === 'modp1' && crypto.getFips() === 1) || + (isBoringSSL && (name === 'modp1' || name === 'modp2'))) { common.printSkipMessage(`Skipping unsupported ${name} test case`); continue; diff --git a/test/pummel/test-crypto-pqc-sign-verify-slh-dsa.mjs b/test/pummel/test-crypto-pqc-sign-verify-slh-dsa.mjs index 772d9bab6f6..52b018e7028 100644 --- a/test/pummel/test-crypto-pqc-sign-verify-slh-dsa.mjs +++ b/test/pummel/test-crypto-pqc-sign-verify-slh-dsa.mjs @@ -13,6 +13,9 @@ import { promisify } from 'node:util'; import { randomBytes, sign, verify } from 'node:crypto'; import fixtures from '../common/fixtures.js'; +const pSign = promisify(sign); +const pVerify = promisify(verify); + function getKeyFileName(type, suffix) { return `${type.replaceAll('-', '_')}_${suffix}.pem`; } @@ -37,6 +40,8 @@ for (const [asymmetricKeyType, sigLen] of [ }; const data = randomBytes(32); + // Start the async signature before the sync work to overlap the two. + const signaturePromise = pSign(undefined, data, keys.private); // sync { @@ -48,9 +53,7 @@ for (const [asymmetricKeyType, sigLen] of [ // async { - const pSign = promisify(sign); - const pVerify = promisify(verify); - const signature = await pSign(undefined, data, keys.private); + const signature = await signaturePromise; assert.strictEqual(signature.byteLength, sigLen); assert.strictEqual(await pVerify(undefined, randomBytes(32), keys.public, signature), false); assert.strictEqual(await pVerify(undefined, data, keys.public, signature), true); diff --git a/test/pummel/test-dh-regr.js b/test/pummel/test-dh-regr.js index 961bea3246f..8a2e71745a3 100644 --- a/test/pummel/test-dh-regr.js +++ b/test/pummel/test-dh-regr.js @@ -32,17 +32,33 @@ if (common.isPi()) { const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); -// FIPS requires length >= 1024 but we use 512/256 in this test to keep it from -// taking too long and timing out in CI. -const length = crypto.getFips() ? 1024 : hasOpenSSL3 ? 512 : 256; +let iterations = 2000; +if (hasFIPS(3)) { + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + }); -const p = crypto.createDiffieHellman(length).getPrime(); + // Keep a lower iteration count for FIPS jobs. + iterations = 100; +} + +let createDH; +if (hasOpenSSL(3)) { + // OpenSSL 3 recognizes named groups without validating their primes. + createDH = () => crypto.getDiffieHellman('modp14'); +} else { + // Other backends validate each peer's parameters, so keep them small. + const length = crypto.getFips() === 1 ? 1024 : 256; + const prime = crypto.createDiffieHellman(length).getPrime(); + createDH = () => crypto.createDiffieHellman(prime); +} -for (let i = 0; i < 2000; i++) { - const a = crypto.createDiffieHellman(p); - const b = crypto.createDiffieHellman(p); +for (let i = 0; i < iterations; i++) { + const a = createDH(); + const b = createDH(); a.generateKeys(); b.generateKeys(); diff --git a/test/pummel/test-webcrypto-derivebits-pbkdf2.js b/test/pummel/test-webcrypto-derivebits-pbkdf2.js index bfb01ac0c94..9e6cdcad8ab 100644 --- a/test/pummel/test-webcrypto-derivebits-pbkdf2.js +++ b/test/pummel/test-webcrypto-derivebits-pbkdf2.js @@ -11,7 +11,9 @@ if (common.isPi()) { } const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips4 = hasFIPS(4); function getDeriveKeyInfo(name, length, hash, ...usages) { return [{ name, length, hash }, usages]; @@ -632,6 +634,19 @@ async function testWrongKeyType( Object.keys(kDerivations[size][saltSize][hash]) .forEach((iterations) => { const args = [baseKeys, size, saltSize, hash, iterations | 0]; + if (fips4 && + (size === 'empty' || saltSize !== 'long' || iterations < 1000)) { + variations.push(assert.rejects( + testDeriveBits(...args), { name: 'OperationError' })); + kDerivedKeyTypes.forEach((keyType) => { + const keyArgs = getDeriveKeyInfo(...keyType); + variations.push(assert.rejects( + testDeriveKey(...args, ...keyArgs), + { name: 'OperationError' })); + }); + return; + } + variations.push(testDeriveBits(...args)); variations.push(testDeriveBitsBadLengths(...args)); variations.push(testDeriveBitsBadHash(...args)); @@ -674,12 +689,17 @@ async function testWrongKeyType( // https://github.com/w3c/webcrypto/pull/380 { - crypto.subtle.importKey('raw', new Uint8Array(0), 'PBKDF2', false, ['deriveBits']).then((key) => { + crypto.subtle.importKey( + 'raw', + new Uint8Array(fips4 ? 8 : 0), + 'PBKDF2', + false, + ['deriveBits']).then((key) => { return crypto.subtle.deriveBits({ name: 'PBKDF2', hash: { name: 'SHA-256' }, - iterations: 10, - salt: new Uint8Array(0), + iterations: fips4 ? 1000 : 10, + salt: new Uint8Array(fips4 ? 16 : 0), }, key, 0); }).then((bits) => { assert.deepStrictEqual(bits, new ArrayBuffer(0)); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 5db4a776315..ef48f457a87 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -139,6 +139,7 @@ function testInitialized(req, ctor_name) { if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check const crypto = require('crypto'); + const { hasFIPS } = require('../common/crypto'); // The handle for PBKDF2 and RandomBytes isn't returned by the function call, // so need to check it from the callback. @@ -152,7 +153,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check testInitialized(this, 'RandomBytesJob'); })); - if (typeof internalBinding('crypto').ScryptJob === 'function') { + if (typeof internalBinding('crypto').ScryptJob === 'function' && + !hasFIPS(3)) { crypto.scrypt('password', 'salt', 8, common.mustCall(function() { testInitialized(this, 'ScryptJob'); })); diff --git a/test/sequential/test-http2-timeout-large-write-file.js b/test/sequential/test-http2-timeout-large-write-file.js index c0675ced2c1..fb95ee01466 100644 --- a/test/sequential/test-http2-timeout-large-write-file.js +++ b/test/sequential/test-http2-timeout-large-write-file.js @@ -27,7 +27,6 @@ const minReadSize = 500000; const serverTimeout = common.platformTimeout(500); let offsetTimeout = common.platformTimeout(100); let didReceiveData = false; - const content = Buffer.alloc(writeSize, 0x44); const filepath = tmpdir.resolve('http2-large-write.tmp'); fs.writeFileSync(filepath, content, 'binary'); @@ -52,8 +51,10 @@ server.on('timeout', common.mustCallAtLeast(() => { }, 0)); server.listen(0, common.mustCall(() => { - const client = http2.connect(`https://localhost:${server.address().port}`, - { rejectUnauthorized: false }); + const client = http2.connect(`https://localhost:${server.address().port}`, { + rejectUnauthorized: false, + settings: { initialWindowSize: 65535 }, + }); const req = client.request({ ':path': '/' }); req.end(); diff --git a/test/sequential/test-http2-timeout-large-write.js b/test/sequential/test-http2-timeout-large-write.js index ea5fd1973b7..b924c175d6e 100644 --- a/test/sequential/test-http2-timeout-large-write.js +++ b/test/sequential/test-http2-timeout-large-write.js @@ -23,7 +23,6 @@ const minReadSize = 500000; const serverTimeout = common.platformTimeout(500); let offsetTimeout = common.platformTimeout(100); let didReceiveData = false; - const server = http2.createSecureServer({ key: fixtures.readKey('agent1-key.pem'), cert: fixtures.readKey('agent1-cert.pem'), @@ -49,8 +48,10 @@ server.setTimeout(serverTimeout); server.on('timeout', onTimeout); server.listen(0, common.mustCall(() => { - const client = http2.connect(`https://localhost:${server.address().port}`, - { rejectUnauthorized: false }); + const client = http2.connect(`https://localhost:${server.address().port}`, { + rejectUnauthorized: false, + settings: { initialWindowSize: 65535 }, + }); const req = client.request({ ':path': '/' }); req.end(); diff --git a/test/sequential/test-https-connect-localport.js b/test/sequential/test-https-connect-localport.js index 10d358c8a92..041becdc644 100644 --- a/test/sequential/test-https-connect-localport.js +++ b/test/sequential/test-https-connect-localport.js @@ -26,7 +26,9 @@ const assert = require('assert'); family: 4, localPort: common.PORT, rejectUnauthorized: false, - }, common.mustCall(() => { + agent: false, + }, common.mustCall((res) => { + res.resume(); assert.strictEqual(req.socket.localPort, common.PORT); assert.strictEqual(req.socket.remotePort, port); })); diff --git a/test/sequential/test-net-connect-econnrefused.js b/test/sequential/test-net-connect-econnrefused.js index 67f5820221c..c51e99e59b8 100644 --- a/test/sequential/test-net-connect-econnrefused.js +++ b/test/sequential/test-net-connect-econnrefused.js @@ -32,7 +32,7 @@ let rounds = 1; let reqs = 0; let port; -const server = net.createServer().listen(0, common.mustCall(() => { +const server = net.createServer().listen(0, common.localhostIPv4, common.mustCall(() => { port = server.address().port; server.close(common.mustCall(pummel)); })); @@ -40,17 +40,14 @@ const server = net.createServer().listen(0, common.mustCall(() => { function pummel() { let pending; for (pending = 0; pending < ATTEMPTS_PER_ROUND; pending++) { - net.createConnection({ port, autoSelectFamily: false }).on('error', common.mustCallAtLeast((error) => { - // Family autoselection might be skipped if only a single address is returned by DNS. - const actualError = Array.isArray(error.errors) ? error.errors[0] : error; - + net.createConnection({ host: common.localhostIPv4, port }).on('error', common.mustCall((error) => { console.log('pending', pending, 'rounds', rounds); - assert.strictEqual(actualError.code, 'ECONNREFUSED'); + assert.strictEqual(error.code, 'ECONNREFUSED'); if (--pending > 0) return; if (rounds === ROUNDS) return check(); rounds++; pummel(); - }, 0)); + })); reqs++; } } diff --git a/test/sequential/test-perf-hooks-histogram-heapdump.js b/test/sequential/test-perf-hooks-histogram-heapdump.js new file mode 100644 index 00000000000..cf310eb973b --- /dev/null +++ b/test/sequential/test-perf-hooks-histogram-heapdump.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + createJSHeapSnapshot, + validateByRetainingPathFromNodes, +} = require('../common/heap'); +const { + createHistogram, + createSlidingWindowHistogram, +} = require('perf_hooks'); + +(async () => { + const uncached = createHistogram(); + const cached = createHistogram(); + cached.record(1); + cached.record(1000); + await cached.qrde({ cache: true }); + + const sliding = createSlidingWindowHistogram({ + chunks: 2, + recordsPerChunk: 1, + }); + + const nodes = createJSHeapSnapshot(); + const snapshots = validateByRetainingPathFromNodes( + nodes, + 'Node / Histogram', + [{ node_name: 'Node / qrde_snapshot', edge_name: 'qrde_snapshot' }], + ); + assert.strictEqual(snapshots.length, 1); + assert.ok(snapshots[0].self_size > 0); + + const windows = validateByRetainingPathFromNodes( + nodes, + 'Node / SlidingWindowHistogram', + [], + ); + for (const [edgeName, nodeName] of [ + ['chunks', 'Node / chunks'], + ['generations', 'Node / generations'], + ['spare', 'Node / Histogram'], + ]) { + validateByRetainingPathFromNodes(windows, 'Node / SlidingWindowHistogram', [ + { node_name: nodeName, edge_name: edgeName }, + ]); + } + + // Keep all three wrappers live through snapshot generation. + assert.strictEqual(uncached.count, 0); + assert.strictEqual(cached.count, 2); + assert.strictEqual(sliding.snapshot().count, 0); +})().then(common.mustCall()); diff --git a/test/sequential/test-pipe.js b/test/sequential/test-pipe.js index 7515e4c705b..39b11e17d51 100644 --- a/test/sequential/test-pipe.js +++ b/test/sequential/test-pipe.js @@ -93,7 +93,10 @@ function startClient() { port: common.PORT, method: 'GET', path: '/', - headers: { 'content-length': buffer.length }, + headers: { + 'connection': 'close', + 'content-length': buffer.length, + }, }, common.mustCall((res) => { res.setEncoding('utf8'); res.on('data', common.mustCall((string) => { diff --git a/test/sequential/test-tls-psk-client.js b/test/sequential/test-tls-psk-client.js index 2eb6228f79f..5cb429f47de 100644 --- a/test/sequential/test-tls-psk-client.js +++ b/test/sequential/test-tls-psk-client.js @@ -5,7 +5,9 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -if (process.features.openssl_is_boringssl) { +const { isBoringSSL } = require('../common/crypto'); + +if (isBoringSSL) { require('../common/boringssl').testPskTls13Unsupported(); return; } diff --git a/test/test-runner/test-output-junit-quote.mjs b/test/test-runner/test-output-junit-quote.mjs new file mode 100644 index 00000000000..6d1769eea83 --- /dev/null +++ b/test/test-runner/test-output-junit-quote.mjs @@ -0,0 +1,11 @@ +// Test that the output of test-runner/output/junit_quote.js matches +// test-runner/output/junit_quote.snapshot +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawnAndAssert, junitTransform, ensureCwdIsProjectRoot } from '../common/assertSnapshot.js'; + +ensureCwdIsProjectRoot(); +await spawnAndAssert( + fixtures.path('test-runner/output/junit_quote.js'), + junitTransform, +); diff --git a/test/test-runner/test-run-watch-cwd-isolation-none-argv.mjs b/test/test-runner/test-run-watch-cwd-isolation-none-argv.mjs index 1066f8fc5d6..a08677300b8 100644 --- a/test/test-runner/test-run-watch-cwd-isolation-none-argv.mjs +++ b/test/test-runner/test-run-watch-cwd-isolation-none-argv.mjs @@ -2,7 +2,8 @@ // parent process argv when spawning the watch child. import * as common from '../common/index.mjs'; import assert from 'node:assert'; -import { writeFileSync } from 'node:fs'; +import { setTimeout } from 'node:timers/promises'; +import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { run } from 'node:test'; import tmpdir from '../common/tmpdir.js'; @@ -11,12 +12,15 @@ import { skipIfNoWatch } from '../common/watch.js'; skipIfNoWatch(); tmpdir.refresh(); -writeFileSync(join(tmpdir.path, 'test.js'), ` +await writeFile(join(tmpdir.path, 'test.js'), ` const test = require('node:test'); test('test ran from cwd', () => {}); `); +// Add some delay to ensure the OS sends the FS events before watch mode is started. +await setTimeout(common.platformTimeout(100)); + const passed = []; const controller = new AbortController(); const stream = run({ @@ -24,18 +28,20 @@ const stream = run({ watch: true, signal: controller.signal, isolation: 'none', -}).on('data', function({ type }) { - if (type === 'test:watch:drained') { - stream.removeAllListeners('test:fail'); - stream.removeAllListeners('test:pass'); - controller.abort(); - } +}).on('data', ({ type }) => { + if (type !== 'test:watch:drained') return; + + stream.removeAllListeners('test:fail'); + stream.removeAllListeners('test:pass'); + controller.abort(); }); -stream.on('test:fail', common.mustNotCall()); -stream.on('test:pass', common.mustCall((data) => passed.push(data.name), 1)); -// eslint-disable-next-line no-unused-vars -for await (const _ of stream); +stream.on('test:watch:restarted', common.mustNotCall('test:watch:restarted')); +stream.on('test:fail', common.mustNotCall('test:fail')); +stream.on('test:pass', common.mustCall((data) => passed.push(data.name))); + +// eslint-disable-next-line no-empty-pattern +for await (const {} of stream); // Validate the expected test ran by name: assert.deepStrictEqual(passed, ['test ran from cwd']); diff --git a/test/test-runner/test-run-watch-cwd-isolation-none.mjs b/test/test-runner/test-run-watch-cwd-isolation-none.mjs index ce65bfb5e41..bbb1a141a4b 100644 --- a/test/test-runner/test-run-watch-cwd-isolation-none.mjs +++ b/test/test-runner/test-run-watch-cwd-isolation-none.mjs @@ -1,27 +1,28 @@ // Test run({ watch: true, cwd, isolation: 'none' }) runs with different cwd while in watch mode and isolation none import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; import { run } from 'node:test'; -import tmpdir from '../common/tmpdir.js'; -import { refreshForTestRunnerWatch, skipIfNoWatch } from '../common/watch.js'; +import { skipIfNoWatch } from '../common/watch.js'; skipIfNoWatch(); -refreshForTestRunnerWatch(); const controller = new AbortController(); const stream = run({ - cwd: tmpdir.path, + cwd: fixtures.path('test-runner-watch'), watch: true, signal: controller.signal, isolation: 'none', -}).on('data', function({ type }) { - if (type === 'test:watch:drained') { - stream.removeAllListeners('test:fail'); - stream.removeAllListeners('test:pass'); - controller.abort(); - } +}).on('data', ({ type }) => { + if (type !== 'test:watch:drained') return; + + stream.removeAllListeners('test:fail'); + stream.removeAllListeners('test:pass'); + controller.abort(); }); -stream.on('test:fail', common.mustNotCall()); -stream.on('test:pass', common.mustCall(1)); -// eslint-disable-next-line no-unused-vars -for await (const _ of stream); +stream.on('test:watch:restarted', common.mustNotCall('test:watch:restarted')); +stream.on('test:fail', common.mustNotCall('test:fail')); +stream.on('test:pass', common.mustCall()); + +// eslint-disable-next-line no-empty-pattern +for await (const {} of stream); diff --git a/test/test-runner/test-runner.status b/test/test-runner/test-runner.status index 7d2d59d3540..7ad99e20a87 100644 --- a/test/test-runner/test-runner.status +++ b/test/test-runner/test-runner.status @@ -6,11 +6,7 @@ prefix test-runner [true] # This section applies to all platforms -# https://github.com/nodejs/node/pull/54888#issuecomment-2351128116 -# TODO(pmarchini): This test is failing when isolation is set to none and must be fixed. -test-watch-create-isolation-none: SKIP - -[$system==macos && $arch==x64] -# https://github.com/nodejs/node/issues/54534#issuecomment-5423551021 +[$system==win32] +# https://github.com/nodejs/node/issues/66056 test-run-watch-cwd-isolation-none: PASS, FLAKY test-run-watch-cwd-isolation-none-argv: PASS, FLAKY diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index db856a75cca..d0459aea524 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -1,14 +1,21 @@ 'use strict'; -const { hasOpenSSL } = require('../../common/crypto.js'); +const { + hasOpenSSL, + hasFIPS, + isBoringSSL, +} = require('../../common/crypto.js'); const conditionalFileSkips = {}; const conditionalSubtestSkips = {}; function skip(...files) { for (const file of files) { + const provider = isBoringSSL ? + 'BoringSSL' : + `OpenSSL ${process.versions.openssl}${hasFIPS(3) ? ' FIPS mode' : ''}`; conditionalFileSkips[file] = { - 'skip': 'Unsupported in ' + (process.features.openssl_is_boringssl ? 'BoringSSL' : `OpenSSL ${process.versions.openssl}`), + 'skip': `Unsupported in ${provider}`, }; } } @@ -37,13 +44,13 @@ if (!hasOpenSSL(3, 0)) { 'sign_verify/kmac.tentative.https.any.js'); } -if (!hasOpenSSL(3, 2)) { +if (!hasOpenSSL(3, 2) || hasFIPS(3)) { skip( 'derive_bits_keys/argon2.tentative.https.any.js', 'import_export/Argon2_importKey.tentative.https.any.js'); } -if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { +if (!hasOpenSSL(3, 5) && !isBoringSSL) { skip( 'encap_decap/encap_decap_bits.tentative.https.any.js', 'encap_decap/encap_decap_keys.tentative.https.any.js', @@ -65,7 +72,7 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { ]); } -if (process.features.openssl_is_boringssl) { +if (isBoringSSL) { skip( 'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js', 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js', @@ -94,8 +101,90 @@ if (process.features.openssl_is_boringssl) { ['supports-modern.tentative.https.any.js', /ml-kem-512/i]); } -skipSubtests( - ['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]); +if (hasFIPS(3)) { + skip( + 'encrypt_decrypt/aes_ocb.tentative.https.any.js', + 'encrypt_decrypt/chacha20_poly1305.tentative.https.any.js', + 'generateKey/failures_chacha20_poly1305.tentative.https.any.js', + 'generateKey/successes_chacha20_poly1305.tentative.https.any.js', + 'import_export/ChaCha20-Poly1305_importKey.tentative.https.any.js', + 'serialization/chacha20-poly1305.tentative.https.any.js'); + + skipSubtests( + [ + 'supports-modern.tentative.https.any.js', + /(?:ChaCha20-Poly1305|^supports returns (?:true|false) for algorithm objects with (?:valid|invalid) parameters$)/, + ], + [ + 'wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js', + /(?=.*(?:RSASSA-PKCS1-v1_5|RSA-PSS|RSA-OAEP) private key)(?=.*non-extractable)/, + ]); +} + +if (hasFIPS()) { + skip( + 'digest/kangarootwelve.tentative.https.any.js', + 'digest/turboshake.tentative.https.any.js'); +} + +// OpenSSL 3.0 through 3.3 reject SHA-1 signature generation in FIPS mode. +// OpenSSL 3.4 permits it for legacy use cases while marking the operation as +// non-approved through a per-operation FIPS indicator. Node does not expose +// that indicator, so the round-trip tests succeed. +if (hasFIPS(3) && !hasOpenSSL(3, 4)) { + skipSubtests( + ['sign_verify/ecdsa.https.any.js', /with SHA-1.*round trip$/], + ['sign_verify/rsa_pkcs.https.any.js', /with SHA-1.*round trip$/], + ['sign_verify/rsa_pss.https.any.js', /with SHA-1.*round trip$/]); +} + +if (hasFIPS(3, 5)) { + skip( + 'derive_bits_keys/cfrg_curves_bits_curve25519.https.any.js', + 'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js', + 'derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js', + 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js', + 'generateKey/successes_X25519.https.any.js', + 'generateKey/successes_X448.tentative.https.any.js', + 'import_export/okp_importKey_X25519.https.any.js', + 'import_export/okp_importKey_X448.tentative.https.any.js', + 'import_export/okp_importKey_failures_X25519.https.any.js', + 'import_export/okp_importKey_failures_X448.tentative.https.any.js', + 'serialization/x25519.https.any.js', + 'serialization/x448.tentative.https.any.js'); + + skipSubtests( + [ + 'derive_bits_keys/derived_bits_length.https.any.js', + /^X25519 derivation/, + ], + ['getPublicKey.tentative.https.any.js', /(?:X25519|X448)/], + [ + 'import_export/raw_format_aliases.tentative.https.any.js', + /(?:X25519|X448)/, + ], + [ + 'supports.tentative.https.any.js', + /(?:X25519|^deriveKey promise tests$|^supports validates the ECDH public key$)/, + ], + [ + 'wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js', + /(?=.*(?:X25519|X448))(?=.*(?:jwk|as non-extractable using pkcs8))/, + ]); +} + +if (hasFIPS(4)) { + skipSubtests( + [ + 'derive_bits_keys/pbkdf2.https.any.js', + /(?:empty password|(?:short|empty) salt|with 1 iterations)/, + ]); +} + +if (!hasFIPS()) { + skipSubtests( + ['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]); +} function assertNoOverlap(fileSkips, subtestSkips) { const subtestSkipFiles = new Set(Object.keys(subtestSkips)); diff --git a/test/wpt/test-user-timing.js b/test/wpt/test-user-timing.js index 151c06a86d0..57db5837bbe 100644 --- a/test/wpt/test-user-timing.js +++ b/test/wpt/test-user-timing.js @@ -1,9 +1,31 @@ 'use strict'; +const assert = require('assert'); +const { basename } = require('path'); const { WPTRunner } = require('../common/wpt'); const runner = new WPTRunner('user-timing'); runner.pretendGlobalThisAs('Window'); +runner.setScriptModifier((script) => { + if (basename(script.filename) !== 'mark.any.js') return; + + // A scheduling pause between mark() and now() can exceed any fixed + // tolerance. Check that the mark falls between the surrounding clock + // readings instead, leaving the upstream fixture unchanged. + // https://github.com/nodejs/node/issues/40449 + function replace(from, to) { + assert(script.code.includes(from), `Unexpected contents of ${script.filename}`); + script.code = script.code.replaceAll(from, to); + } + + replace('var expectedTimes = new Array();', + 'var beforeTimes = [];\nvar expectedTimes = new Array();'); + replace('self.performance.mark("mark");', + 'beforeTimes.push(self.performance.now());\n self.performance.mark("mark");'); + replace('assert_approx_equals(entries[index].startTime, expectedTimes[index], testThreshold);', + 'assert_between_inclusive(entries[index].startTime, beforeTimes[index], expectedTimes[index]);'); +}); + runner.runJsTests(); diff --git a/test/wpt/wpt.status b/test/wpt/wpt.status index d20d108ac99..de3b3024627 100644 --- a/test/wpt/wpt.status +++ b/test/wpt/wpt.status @@ -5,8 +5,6 @@ prefix wpt # sample-test : PASS,FLAKY [true] # This section applies to all platforms -# https://github.com/nodejs/node/issues/40449 -test-user-timing: PASS,FLAKY [$system==win32] diff --git a/tools/actions/commit-queue.sh b/tools/actions/commit-queue.sh index e260773d14a..0c3c70da89f 100755 --- a/tools/actions/commit-queue.sh +++ b/tools/actions/commit-queue.sh @@ -72,7 +72,7 @@ commit_queue_failed() { Add https://github.com/nodejs/node/labels/commit-queue-squash to land it as one commit, or https://github.com/nodejs/node/labels/commit-queue-rebase to land the commits separately.' else if [ -z "$reported_failure" ]; then - reported_failure=$(grep -e '✘' -e '⚠' output | tail -n 10) + reported_failure=$(grep -e '✘' -e '✖' -e '⚠' output | tail -n 10) fi if [ -z "$reported_failure" ]; then reported_failure=$(tail -n 10 output) diff --git a/tools/actions/start-ci.sh b/tools/actions/start-ci.sh index d4d19b92082..a9ae07a365e 100755 --- a/tools/actions/start-ci.sh +++ b/tools/actions/start-ci.sh @@ -6,6 +6,20 @@ REQUEST_CI_LABEL="request-ci" REQUEST_CI_FAILED_LABEL="request-ci-failed" cqurl="${GITHUB_SERVER_URL:?}/${GITHUB_REPOSITORY:?}/actions/runs/${GITHUB_RUN_ID:?}" +escape_code_block_or_line() { + case $1 in + *" +"*|'') fence='```' sep=' +' ;; + *[![:space:]]*) fence='`' sep=' ' ;; + *) fence='`' sep='' ;; + esac + while case $1 in *"$fence"*) ;; *) false ;; esac; do + fence=$fence'`' + done + printf '%s%s%s%s%s\n' "$fence" "$sep" "$1" "$sep" "$fence" +} + for pr in "$@"; do gh -R "$GITHUB_REPOSITORY" pr edit "$pr" --remove-label "$REQUEST_CI_LABEL" @@ -18,7 +32,28 @@ for pr in "$@"; do # Do we need to reset? gh -R "$GITHUB_REPOSITORY" pr edit "$pr" --add-label "$REQUEST_CI_FAILED_LABEL" - body="
Failed to start CI
$(cat output)
$cqurl
" + reported_failure=$(grep -e '✘' -e '✖' -e '⚠' -e 'ℹ' output | tail -n 10) + if [ -z "$reported_failure" ]; then + reported_failure=$(tail -n 10 output) + fi + if [ -z "$reported_failure" ]; then + reported_failure='No failure reason was reported.' + fi + failure_body=$(escape_code_block_or_line "$reported_failure") + raw_output=$(cat output) + + body="### Failed to start CI + +$failure_body + +
+Full Auto Start CI output + +$(escape_code_block_or_line "$raw_output") + +
+ +[View workflow run]($cqurl)" echo "$body" gh -R "$GITHUB_REPOSITORY" pr comment "$pr" --body "$body" diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 33607cbf2bd..b8c9df280b3 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -226,6 +226,8 @@ const customTypesMap = { 'perf_hooks.html#class-performanceobserver', 'PerformanceObserverEntryList': 'perf_hooks.html#class-performanceobserverentrylist', + 'SlidingWindowHistogram': + 'perf_hooks.html#class-slidingwindowhistogram', 'readline.Interface': 'readline.html#class-readlineinterface', diff --git a/tools/eslint/package-lock.json b/tools/eslint/package-lock.json index d9f1b6c5e68..52bd25321ad 100644 --- a/tools/eslint/package-lock.json +++ b/tools/eslint/package-lock.json @@ -8,18 +8,18 @@ "name": "eslint-tools", "version": "0.0.0", "dependencies": { - "@babel/core": "^8.0.1", - "@babel/eslint-parser": "^8.0.1", + "@babel/core": "^8.0.5", + "@babel/eslint-parser": "^8.0.5", "@babel/plugin-syntax-import-defer": "^8.0.1", "@babel/plugin-syntax-import-source": "^8.0.1", "@eslint/js": "^10.0.1", "@eslint/markdown": "^8.0.3", "@stylistic/eslint-plugin": "^5.10.0", - "eslint": "^10.9.1", + "eslint": "^10.10.0", "eslint-formatter-tap": "^9.0.1", - "eslint-plugin-jsdoc": "^64.2.1", - "eslint-plugin-regexp": "^3.2.0", - "globals": "^17.11.0" + "eslint-plugin-jsdoc": "^64.3.10", + "eslint-plugin-regexp": "^3.3.0", + "globals": "^17.12.0" } }, "node_modules/@babel/code-frame": { @@ -36,28 +36,28 @@ } }, "node_modules/@babel/compat-data": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz", - "integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz", + "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==", "license": "MIT", "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/core": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz", - "integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.5.tgz", + "integrity": "sha512-2/oWkgTbBYoqioCWAE4XJobOrzwxTDa5/XjDP3tJ1BhDr/owcd9qnXBp4xc3/2G5X4bvXM04nrxbRJxFcXAxFQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^8.0.0", - "@babel/generator": "^8.0.0", - "@babel/helper-compilation-targets": "^8.0.0", - "@babel/helpers": "^8.0.0", - "@babel/parser": "^8.0.0", + "@babel/generator": "^8.0.5", + "@babel/helper-compilation-targets": "^8.0.5", + "@babel/helpers": "^8.0.5", + "@babel/parser": "^8.0.5", "@babel/template": "^8.0.0", - "@babel/traverse": "^8.0.0", - "@babel/types": "^8.0.0", + "@babel/traverse": "^8.0.5", + "@babel/types": "^8.0.5", "@types/gensync": "^1.0.5", "convert-source-map": "^2.0.0", "empathic": "^2.0.1", @@ -65,7 +65,7 @@ "import-meta-resolve": "^4.2.0", "json5": "^2.2.3", "obug": "^2.1.1", - "semver": "^7.7.3" + "verkit": "^0.3.2" }, "engines": { "node": "^22.18.0 || >=24.11.0" @@ -76,14 +76,14 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-8.0.1.tgz", - "integrity": "sha512-2javO8pAQv/ld6sS6OcxoLAlzZEZy+xm99bnoAfMhzKSumKhdF5wylpbZB7XTorWr3KLPtx5K95eduJPOy1mzA==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-8.0.5.tgz", + "integrity": "sha512-q4c+FpoJ/FKeiLnZ9Os3vWnSlZw3dBmlTUx6vuITRqbd8yn4hgUB0/OOJ+gi5lt+mOx/0IW2rXINR98JJllfag==", "license": "MIT", "dependencies": { "eslint-scope": "^9.1.0", "eslint-visitor-keys": "^5.0.0", - "semver": "^7.7.3" + "verkit": "^0.3.2" }, "engines": { "node": "^22.18.0 || >=24.11.0" @@ -94,15 +94,15 @@ } }, "node_modules/@babel/generator": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", - "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.5.tgz", + "integrity": "sha512-f/TuhuMAxJqhwxEGNsJrswuG9VHmh0oNFoQoo6TbpgtFAz9wYZXcTAcWZMHfp7ljesr0RG04bp3Aos9GI59L7w==", "license": "MIT", "dependencies": { - "@babel/parser": "^8.0.0", - "@babel/types": "^8.0.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", + "@babel/parser": "^8.0.5", + "@babel/types": "^8.0.5", + "@jridgewell/gen-mapping": "0.4.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.31", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" }, @@ -111,16 +111,16 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz", - "integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.5.tgz", + "integrity": "sha512-Qk8ahMGooH5mz6uuhoDvfZGkUf/Mf3RTBucVVl4MKx4LKMTv872TeW8O92h15iVtlN8wAROBIpI1aV6x1z0LCQ==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^8.0.0", + "@babel/compat-data": "^8.0.5", "@babel/helper-validator-option": "^8.0.0", "browserslist": "^4.24.0", "lru-cache": "^11.0.0", - "semver": "^7.7.3" + "verkit": "^0.3.2" }, "engines": { "node": "^22.18.0 || >=24.11.0" @@ -157,9 +157,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.2.tgz", - "integrity": "sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", "license": "MIT", "engines": { "node": "^22.18.0 || >=24.11.0" @@ -175,25 +175,25 @@ } }, "node_modules/@babel/helpers": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz", - "integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.5.tgz", + "integrity": "sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==", "license": "MIT", "dependencies": { "@babel/template": "^8.0.0", - "@babel/types": "^8.0.0" + "@babel/types": "^8.0.5" }, "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0.tgz", - "integrity": "sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.5.tgz", + "integrity": "sha512-51RXvQNFakaS0bTpYiGkxNbUVwkPO4kONv6EVLorZABxsx+KZ6Z7uSYvi/wmKS/+X+rfj9RvOw0/ZNh+cmI0Rw==", "license": "MIT", "dependencies": { - "@babel/types": "^8.0.0" + "@babel/types": "^8.0.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -247,17 +247,17 @@ } }, "node_modules/@babel/traverse": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.0.tgz", - "integrity": "sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.5.tgz", + "integrity": "sha512-XFfnuvapSc/vJOcUO7kwORSvpBIvraofKEZ2dhT0PjiF21BRCD7YbAFC8UEeDJNeLoQz82/gVqzgX5hCzkCbdg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^8.0.0", - "@babel/generator": "^8.0.0", + "@babel/generator": "^8.0.5", "@babel/helper-globals": "^8.0.0", - "@babel/parser": "^8.0.0", + "@babel/parser": "^8.0.5", "@babel/template": "^8.0.0", - "@babel/types": "^8.0.0", + "@babel/types": "^8.0.5", "obug": "^2.1.1" }, "engines": { @@ -265,41 +265,51 @@ } }, "node_modules/@babel/types": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0.tgz", - "integrity": "sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.5.tgz", + "integrity": "sha512-eVdMqi3ej5aHhyQ2Si6yD2cAWeV8FJK9UrhK5aL0Sd8hu5GhT+YswhVNbVheOGVYMg8kuGuMaUpkB3stjj4z8A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.0" + "@babel/helper-validator-identifier": "^8.0.4" }, "engines": { "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@es-joy/jsdoccomment": { - "version": "0.95.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.95.1.tgz", - "integrity": "sha512-LO/RI08Fo9bhXwB7Od9G+1j3eSNq63+ZS5CQO8YLXHbDg6kx6S/DhTeY0+Fc9uZrjK1zZSyTx8Sg5gv5DIoCnA==", + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.9", - "@typescript-eslint/types": "^8.67.0", - "comment-parser": "1.4.8", - "esquery": "^1.7.0", - "jsdoc-type-pratt-parser": "~9.1.2" - }, - "engines": { - "node": "^22.22.2 || >=24.15.0" + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" } }, - "node_modules/@es-joy/jsdoccomment/node_modules/jsdoc-type-pratt-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-9.1.2.tgz", - "integrity": "sha512-9EXymowgk1mb9RY1VxuwKc+AhaxfBk2CV0dWxgGM+l5RURTtiUoAx7MlKwcsiVcEXK5HEPa7FeH/tsRpqjEPRg==", + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.9" + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.97.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.97.0.tgz", + "integrity": "sha512-EP8uoFfh6+GsdGCduYtmWAW0h7AO+Ayik9Vh5YbA2r/3N6lmJKkCNZX+q3QBXC1K6ixjQ/9igF2b7WVvLm063g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9", + "@typescript-eslint/types": "^8.69.0", + "comment-parser": "1.4.8", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~9.2.1" }, "engines": { "node": "^22.22.2 || >=24.15.0" @@ -446,9 +456,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "license": "Apache-2.0", "dependencies": { "@eslint/core": "^1.2.1", @@ -520,12 +530,12 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz", + "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", "@jridgewell/trace-mapping": "^0.3.24" } }, @@ -539,9 +549,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -554,6 +564,28 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, "node_modules/@sindresorhus/base62": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", @@ -682,10 +714,64 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/@typescript-eslint/types": { - "version": "8.69.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", - "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -695,6 +781,73 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -757,9 +910,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", - "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "version": "2.11.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", + "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -781,9 +934,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", "funding": [ { "type": "opencollective", @@ -800,11 +953,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -813,6 +966,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001810", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", @@ -950,9 +1116,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.420", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", - "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "version": "1.5.429", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.429.tgz", + "integrity": "sha512-/1ENIE3cx4HTIx4IfPZFaOunJmsrSVTnj6coXoRVbiJUbkeTyFkJvBeWGkdgh08OhFbxYLMT1kbkwFVSarq6Ow==", "license": "ISC" }, "node_modules/empathic": { @@ -986,9 +1152,9 @@ } }, "node_modules/eslint": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", - "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", "license": "MIT", "workspaces": [ "packages/*" @@ -999,7 +1165,7 @@ "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", + "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -1014,7 +1180,7 @@ "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", @@ -1056,13 +1222,14 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "64.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.2.1.tgz", - "integrity": "sha512-6GpSYxLPcbMw38S94Cngrgs1Zv8yLinQS1O17OxJVZ6deLbrMRCERUYQKceweSqEuG2kx5Amn4l3aKPkCp4geQ==", + "version": "64.3.10", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.3.10.tgz", + "integrity": "sha512-oevWxylnQJm/f4S5WfvqG/pdmX9Ndsjpfm51QpTFfG8MF92999sp4hm4/GxyboaAQRFngLRDL4iKHZz5hPs4Xw==", "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.95.1", + "@es-joy/jsdoccomment": "~0.97.0", "@es-joy/resolve.exports": "1.2.0", + "@typescript-eslint/utils": "^8.69.0", "are-docs-informative": "^0.1.1", "comment-parser": "1.4.8", "debug": "^4.4.3", @@ -1113,9 +1280,9 @@ } }, "node_modules/eslint-plugin-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-3.2.0.tgz", - "integrity": "sha512-4yq47CnLxyfHFKJEo5kvNaiJ0aDtBxSJWk4M2LiamplUXdWxdlzDYqImb/ZVCp+2BqkQjBAmw4Xc07Q8SXVDZg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-3.3.0.tgz", + "integrity": "sha512-TT0JTQbW7CRKohntpGAsMdQ1K3MsmJK4gdAhKJtIwlJ0JvuYxauymFgJdvqxIY4lqNj0EuRZWVrymQtEdJYEFg==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -1282,16 +1449,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" + "flat-cache": "^6.1.23" } }, "node_modules/find-up": { @@ -1311,22 +1492,20 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "license": "ISC" }, "node_modules/format": { @@ -1365,9 +1544,9 @@ } }, "node_modules/globals": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", - "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", "license": "MIT", "engines": { "node": ">=18" @@ -1376,6 +1555,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "license": "MIT" + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -1454,9 +1651,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -1500,12 +1697,6 @@ "node": ">=6" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1547,12 +1738,12 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/levn": { @@ -1594,9 +1785,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -2488,9 +2679,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", - "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "license": "MIT", "engines": { "node": ">=18" @@ -2503,9 +2694,9 @@ "license": "MIT" }, "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -2631,6 +2822,24 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "license": "MIT" + }, "node_modules/refa": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", @@ -2737,6 +2946,22 @@ "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "license": "CC0-1.0" }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/to-valid-identifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", @@ -2753,6 +2978,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2765,6 +3002,20 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", @@ -2841,9 +3092,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", - "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "funding": [ { "type": "opencollective", @@ -2879,6 +3130,18 @@ "punycode": "^2.1.0" } }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/tools/eslint/package.json b/tools/eslint/package.json index c91990303d3..8357472e24c 100644 --- a/tools/eslint/package.json +++ b/tools/eslint/package.json @@ -3,17 +3,17 @@ "version": "0.0.0", "private": true, "dependencies": { - "@babel/core": "^8.0.1", - "@babel/eslint-parser": "^8.0.1", + "@babel/core": "^8.0.5", + "@babel/eslint-parser": "^8.0.5", "@babel/plugin-syntax-import-defer": "^8.0.1", "@babel/plugin-syntax-import-source": "^8.0.1", "@eslint/js": "^10.0.1", "@eslint/markdown": "^8.0.3", "@stylistic/eslint-plugin": "^5.10.0", - "eslint": "^10.9.1", + "eslint": "^10.10.0", "eslint-formatter-tap": "^9.0.1", - "eslint-plugin-jsdoc": "^64.2.1", - "eslint-plugin-regexp": "^3.2.0", - "globals": "^17.11.0" + "eslint-plugin-jsdoc": "^64.3.10", + "eslint-plugin-regexp": "^3.3.0", + "globals": "^17.12.0" } } diff --git a/tools/lint-md/package-lock.json b/tools/lint-md/package-lock.json index 1ab4268328b..ea6663dc369 100644 --- a/tools/lint-md/package-lock.json +++ b/tools/lint-md/package-lock.json @@ -326,9 +326,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", diff --git a/tools/pgo/README.md b/tools/pgo/README.md index 9277315508c..234f20e638a 100644 --- a/tools/pgo/README.md +++ b/tools/pgo/README.md @@ -1,7 +1,6 @@ # Node.js PGO Training Scripts -Training workloads for Profile-Guided Optimization (PGO) builds using -Clang/LLVM (including Clang-CL on Windows). +Training workloads for Profile-Guided Optimization (PGO) builds. ## What is PGO? @@ -10,21 +9,36 @@ branch prediction, code layout), typically improving throughput by 5-20%. The process has three phases: -1. **Instrument** — Build with `-fprofile-generate` (produces `.profraw` files) +1. **Instrument** — Build with `-fprofile-generate` 2. **Train** — Run representative workloads to collect profile data -3. **Optimize** — Merge `.profraw` → `node.profdata` via `llvm-profdata`, - then rebuild with `-fprofile-use` +3. **Optimize** — Rebuild with `-fprofile-use` -## Quick Start +## Platform Support -From a VS Developer Command Prompt: +| Platform | Supported toolchains | Driver | +| -------- | -------------------- | ------------------------- | +| Windows | Clang-CL | `vcbuild.bat` + `pgo.ps1` | +| Linux | GCC | `configure` + `make` | +| macOS | — | — | + +The two supported flows differ in how profile data is collected. Clang writes +one `.profraw` file per process, which must be merged into a single +`.profdata` before the optimize phase. GCC's libgcov instead merges counters +into `.gcda` files next to each object file as each process exits, so there is +no merge step. + +Clang on Linux and macOS are not supported yet. + +## Quick Start: Windows + +From a VS Developer Command Prompt, at the repo root: ```powershell # Step 1: Build the instrumented binary vcbuild.bat pgo-generate -# Step 2: Run workloads and merge profile data -.\pgo.ps1 +# Step 2: Run workloads to collect profile data +powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1 # Step 3: Build the optimized binary vcbuild.bat pgo-use @@ -33,16 +47,51 @@ vcbuild.bat pgo-use `pgo.ps1` expects the instrumented binary at `Release\node.exe` (produced by step 1) and writes `node.profdata` to the repo root (consumed by step 3). +The script is unsigned, so the default execution policy refuses to run it +without `-ExecutionPolicy Bypass`. Use `pwsh` in place of `powershell` on +PowerShell 7. + ```powershell # Optionally set a longer training duration (default: 15s per script) -.\pgo.ps1 -Duration 30 +powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1 -Duration 30 +``` + +## Quick Start: Linux + +```bash +# Step 1: Build the instrumented binary +./configure --enable-pgo-generate +make + +# Step 2: Run workloads to collect profile data +./out/Release/node tools/pgo/pgo-run-all.js --duration=15 --verbose + +# Step 3: Build the optimized binary +./configure --enable-pgo-use +make ``` +Step 2 needs no driver script. Each object file gets one counter file beside +it, with the same basename and a `.gcda` extension: + +```text +out/Release/obj/src/node_base.node_binding.o # from step 1 +out/Release/obj/src/node_base.node_binding.gcda # from step 2 +``` + +Keep `out/` intact between steps 1 and 3. GCC records the `.gcda` path into +each object at compile time, so `make clean` or `make distclean` discards the +training data and step 3 silently produces an ordinary build. + +The build passes `-fprofile-correction`, which is required here. Counter +updates from the worker threads and the libuv thread pool race with each +other, and GCC treats the resulting inconsistent profile as an error unless +told to smooth it out. + ## Training Scripts All scripts use only Node.js built-in modules (no npm dependencies). -Each script is run as a separate process via `fork()`, producing its own -`.profraw` file. +Each script is run as a separate process via `fork()`. | Script | What it exercises | | ------------------------ | ------------------------------------------------------------- | @@ -65,13 +114,13 @@ workloads). When used with `pgo.ps1`, this is handled automatically. ```bash # Run all scripts -node tools/pgo/pgo-run-all.js --duration=15 --verbose +./out/Release/node tools/pgo/pgo-run-all.js --duration=15 --verbose # Run specific scripts -node tools/pgo/pgo-run-all.js --scripts=http-server,json,crypto --duration=30 +./out/Release/node tools/pgo/pgo-run-all.js --scripts=http-server,json,crypto --duration=30 # Show help -node tools/pgo/pgo-run-all.js --help +./out/Release/node tools/pgo/pgo-run-all.js --help ``` Each script reads the `PGO_TRAINING_DURATION` environment variable (in @@ -82,6 +131,7 @@ automatically from the `--duration` flag (in seconds). ``` tools/pgo/ +├── pgo.ps1 # Windows training driver (collect + merge) ├── pgo-run-all.js # Training orchestrator ├── pgo-http-server.js # HTTP server + client workload ├── pgo-json.js # JSON parse/stringify workload diff --git a/pgo.ps1 b/tools/pgo/pgo.ps1 similarity index 88% rename from pgo.ps1 rename to tools/pgo/pgo.ps1 index 39f5352463e..1cfedddd9e6 100644 --- a/pgo.ps1 +++ b/tools/pgo/pgo.ps1 @@ -4,9 +4,12 @@ # (Release\node.exe) and merges the resulting .profraw files into # node.profdata for use with -fprofile-use. # -# Usage (from a VS Developer Command Prompt): -# .\pgo.ps1 # Run workloads (15s each) and merge -# .\pgo.ps1 -Duration 30 # Run workloads (30s each) and merge +# Usage (from a VS Developer Command Prompt, at the repo root): +# powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1 +# powershell -ExecutionPolicy Bypass -File .\tools\pgo\pgo.ps1 -Duration 30 +# +# The script is unsigned, so the default execution policy blocks it without +# -ExecutionPolicy Bypass. Default duration is 15s per workload. # # Prerequisites: # - Release\node.exe must be an instrumented build (built with pgo-generate) @@ -22,6 +25,10 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# The instrumented binary and the merged profile both live at the repo root, +# two levels up from tools\pgo. common.gypi reads node.profdata from there. +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path + # --------------------------------------------------------------------------- # Locate llvm-profdata shipped with Visual Studio's LLVM toolset # --------------------------------------------------------------------------- @@ -64,13 +71,13 @@ function Find-LlvmProfdata { # Validate prerequisites # --------------------------------------------------------------------------- -$instrumentedNode = Join-Path $PSScriptRoot "Release\node.exe" +$instrumentedNode = Join-Path $repoRoot "Release\node.exe" if (-not (Test-Path $instrumentedNode)) { Write-Error "Instrumented binary not found: $instrumentedNode`nBuild with: vcbuild.bat pgo-generate" exit 1 } -$pgoRunAll = Join-Path $PSScriptRoot "tools\pgo\pgo-run-all.js" +$pgoRunAll = Join-Path $PSScriptRoot "pgo-run-all.js" if (-not (Test-Path $pgoRunAll)) { Write-Error "PGO training script not found: $pgoRunAll" exit 1 @@ -90,7 +97,7 @@ Write-Host "`n=== STEP 1: Collect PGO profiles ===" -ForegroundColor Cyan # Directory that will receive .profraw files from the instrumented binary. # %p (PID) and %m (module hash) keep concurrent/fork'd processes from colliding. -$profileDir = Join-Path $PSScriptRoot "pgo-profiles" +$profileDir = Join-Path $repoRoot "pgo-profiles" if (Test-Path $profileDir) { Remove-Item -Recurse -Force $profileDir @@ -137,7 +144,7 @@ $totalSize = ($profrawFiles | Measure-Object -Property Length -Sum).Sum $totalSizeMB = [math]::Round($totalSize / 1MB, 1) Write-Host "Found $($profrawFiles.Count) .profraw file(s), ${totalSizeMB} MB total" -$profdata = Join-Path $PSScriptRoot "node.profdata" +$profdata = Join-Path $repoRoot "node.profdata" $mergeArgs = @("merge", "--output=$profdata") + ($profrawFiles | Select-Object -ExpandProperty FullName) $mergeStopwatch = [System.Diagnostics.Stopwatch]::StartNew() diff --git a/tools/test.py b/tools/test.py index 0bdfa5de799..1495182fcc0 100755 --- a/tools/test.py +++ b/tools/test.py @@ -741,10 +741,11 @@ def RunProcess(context, timeout, args, **rest): timed_out = True else: exit_code = process.poll() - time.sleep(sleep_time) - sleep_time = sleep_time * SLEEP_TIME_FACTOR - if sleep_time > MAX_SLEEP_TIME: - sleep_time = MAX_SLEEP_TIME + if exit_code is None: + time.sleep(sleep_time) + sleep_time = sleep_time * SLEEP_TIME_FACTOR + if sleep_time > MAX_SLEEP_TIME: + sleep_time = MAX_SLEEP_TIME return (process, exit_code, timed_out) @@ -1846,7 +1847,7 @@ def should_keep(case): print() sys.stderr.write("--- Total time: %s ---\n" % FormatTime(duration)) timed_tests = [ t for t in cases_to_run if not t.duration is None ] - timed_tests.sort(key=lambda x: x.duration) + timed_tests.sort(key=lambda x: x.duration, reverse=True) for i, entry in enumerate(timed_tests[:20], start=1): t = FormatTimedelta(entry.duration) sys.stderr.write("%4i (%s) %s\n" % (i, t, entry.GetLabel())) diff --git a/tools/v8/fetch_deps.py b/tools/v8/fetch_deps.py index 055ffb50eaf..2d72127f955 100755 --- a/tools/v8/fetch_deps.py +++ b/tools/v8/fetch_deps.py @@ -9,9 +9,6 @@ Usage: fetch_deps.py """ -# for py2/py3 compatibility -from __future__ import print_function - import os import subprocess import sys diff --git a/tools/v8/node_common.py b/tools/v8/node_common.py index f873065c1df..f9b3074e3e1 100755 --- a/tools/v8/node_common.py +++ b/tools/v8/node_common.py @@ -3,11 +3,9 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -# for py2/py3 compatibility -from __future__ import print_function import os -import pipes +import shlex import shutil import stat import subprocess @@ -29,8 +27,8 @@ def _Get(v8_path): print("Checking out depot_tools.") # shell=True needed on Windows to resolve git.bat. subprocess.check_call("git clone {} {}".format( - pipes.quote(DEPOT_TOOLS_URL), - pipes.quote(depot_tools)), shell=True) + shlex.quote(DEPOT_TOOLS_URL), + shlex.quote(depot_tools)), shell=True) # Using check_output to hide warning messages. subprocess.check_output( [sys.executable, gclient_path, "metrics", "--opt-out"], diff --git a/tools/v8_gypfiles/features.gypi b/tools/v8_gypfiles/features.gypi index 15bcb05dc55..373448fccfb 100644 --- a/tools/v8_gypfiles/features.gypi +++ b/tools/v8_gypfiles/features.gypi @@ -464,9 +464,21 @@ 'defines': ['V8_ENABLE_SEEDED_ARRAY_INDEX_HASH',], }], ['dcheck_always_on!=0', { - 'defines': ['DEBUG',], + 'defines': [ + 'DEBUG', + 'V8_LOGGING_LEVEL=2', # Print file, line, message on fatal checks + ], }, { 'defines': ['NDEBUG',], + 'configurations': { + 'Debug': { + 'defines': ['V8_LOGGING_LEVEL=2',], + }, + 'Release': { + # Only log message (without file or line) on fatal checks + 'defines': ['V8_LOGGING_LEVEL=1',], + }, + }, }], ['v8_enable_verify_csa==1', { 'defines': ['ENABLE_VERIFY_CSA',], diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index eb40d33c513..b31bd413e0b 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -125,6 +125,8 @@ declare namespace InternalCryptoBinding { algorithm: object | undefined, usagesMask: number, extractable: boolean, + secondaryHandle?: KeyObjectHandle, + seedData?: ArrayBuffer | SharedArrayBuffer | ArrayBufferView, ): CryptoKey; } interface CryptoKeyPair { @@ -380,19 +382,19 @@ declare namespace InternalCryptoBinding { ): CryptoJobWebCrypto>; } - interface NidKeyPairGenJobConstructor { + interface NamedKeyPairGenJobConstructor { new< M extends CryptoJobRegularMode, PublicFormat extends PublicKeyFormat = undefined, PrivateFormat extends PrivateKeyFormat = undefined, >( mode: M, - nid: number, + name: string, ...encoding: KeyPairEncodingArgs ): CryptoJobForMode>; new( mode: CryptoJobWebCryptoMode, - nid: number, + name: string, algorithm: object, publicUsagesMask: number, privateUsagesMask: number, @@ -605,6 +607,8 @@ declare namespace InternalCryptoBinding { algorithm: object | undefined, usagesMask: number, extractable: boolean, + secondaryHandle?: KeyObjectHandle, + seedData?: ArrayBuffer | SharedArrayBuffer | ArrayBufferView, ): CryptoKey; } @@ -619,6 +623,8 @@ declare namespace InternalCryptoBinding { algorithm: object, usagesMask: number, handle: KeyObjectHandle, + secondaryHandle: KeyObjectHandle | undefined, + seedData: Buffer | undefined, ]; type CreateNativeKeyObjectClassCallback = @@ -781,6 +787,12 @@ declare namespace InternalCryptoBinding { InternalX509Certificate: InternalX509CertificateConstructor, ]; + type PKCS12ParseResult = [ + privateKey: KeyObjectHandle | null, + certificate: X509CertificateHandle | null, + additionalCertificates: X509CertificateHandle[], + ]; + interface CipherInfo { name: string; nid: number; @@ -819,7 +831,8 @@ export interface CryptoBinding { KEMEncapsulateJob?: InternalCryptoBinding.KEMEncapsulateJobConstructor; KangarooTwelveJob: InternalCryptoBinding.KangarooTwelveJobConstructor; KmacJob: InternalCryptoBinding.KmacJobConstructor; - NidKeyPairGenJob: InternalCryptoBinding.NidKeyPairGenJobConstructor; + getPqcKeyTypes(): string[]; + NamedKeyPairGenJob: InternalCryptoBinding.NamedKeyPairGenJobConstructor; PBKDF2Job: InternalCryptoBinding.PBKDF2JobConstructor; RandomBytesJob: InternalCryptoBinding.RandomBytesJobConstructor; RandomPrimeJob: InternalCryptoBinding.RandomPrimeJobConstructor; @@ -871,28 +884,6 @@ export interface CryptoBinding { Sign: new () => InternalCryptoBinding.SignHandle; Verify: new () => InternalCryptoBinding.VerifyHandle; - EVP_PKEY_ED25519: number; - EVP_PKEY_ED448: number; - EVP_PKEY_ML_DSA_44: number; - EVP_PKEY_ML_DSA_65: number; - EVP_PKEY_ML_DSA_87: number; - EVP_PKEY_ML_KEM_512: number; - EVP_PKEY_ML_KEM_768: number; - EVP_PKEY_ML_KEM_1024: number; - EVP_PKEY_SLH_DSA_SHA2_128F: number; - EVP_PKEY_SLH_DSA_SHA2_128S: number; - EVP_PKEY_SLH_DSA_SHA2_192F: number; - EVP_PKEY_SLH_DSA_SHA2_192S: number; - EVP_PKEY_SLH_DSA_SHA2_256F: number; - EVP_PKEY_SLH_DSA_SHA2_256S: number; - EVP_PKEY_SLH_DSA_SHAKE_128F: number; - EVP_PKEY_SLH_DSA_SHAKE_128S: number; - EVP_PKEY_SLH_DSA_SHAKE_192F: number; - EVP_PKEY_SLH_DSA_SHAKE_192S: number; - EVP_PKEY_SLH_DSA_SHAKE_256F: number; - EVP_PKEY_SLH_DSA_SHAKE_256S: number; - EVP_PKEY_X25519: number; - EVP_PKEY_X448: number; OPENSSL_EC_EXPLICIT_CURVE: number; OPENSSL_EC_NAMED_CURVE: number; RSA_PKCS1_PSS_PADDING: number; @@ -1014,6 +1005,10 @@ export interface CryptoBinding { functionName?: InternalCryptoBinding.OptionalBufferSource, customization?: InternalCryptoBinding.OptionalBufferSource, ): string | InternalCryptoBinding.Buffer; + parsePKCS12( + bundle: InternalCryptoBinding.ByteSource, + passphrase?: InternalCryptoBinding.ByteSource, + ): InternalCryptoBinding.PKCS12ParseResult; parseX509(data: InternalCryptoBinding.ByteSource): InternalCryptoBinding.X509CertificateHandle; privateDecrypt: InternalCryptoBinding.PublicKeyCipher; privateEncrypt: InternalCryptoBinding.PublicKeyCipher; diff --git a/typings/internalBinding/performance.d.ts b/typings/internalBinding/performance.d.ts index fa9a3810fc7..cf3ef0a664f 100644 --- a/typings/internalBinding/performance.d.ts +++ b/typings/internalBinding/performance.d.ts @@ -76,6 +76,20 @@ declare namespace InternalPerformanceBinding { subtract(other: Histogram): number; } + class SlidingWindowHistogram { + constructor( + lowest: number | bigint, + highest: number | bigint, + figures: number, + chunks: number, + timeBased: boolean, + rotateAt: bigint, + ); + record(value: number | bigint): void; + snapshot(): Histogram; + reset(): void; + } + interface Constants { NODE_PERFORMANCE_GC_MAJOR: number; NODE_PERFORMANCE_GC_MINOR: number; @@ -116,6 +130,8 @@ type PerformanceObserverCallback = export interface PerformanceBinding { Histogram: typeof InternalPerformanceBinding.Histogram; + SlidingWindowHistogram: + typeof InternalPerformanceBinding.SlidingWindowHistogram; constants: InternalPerformanceBinding.Constants; observerCounts: Uint32Array; milestones: Float64Array; @@ -129,6 +145,7 @@ export interface PerformanceBinding { samplePerIteration: boolean, ): InternalPerformanceBinding.ELDHistogram; markBootstrapComplete(): void; - uvMetricsInfo(): [number, number, number]; + uvMetricsInfo(): void; + uvMetricsBuffer: Int32Array; now(): number; } diff --git a/typings/internalBinding/util.d.ts b/typings/internalBinding/util.d.ts index a3026b5a030..4c3eeca8ed8 100644 --- a/typings/internalBinding/util.d.ts +++ b/typings/internalBinding/util.d.ts @@ -47,6 +47,7 @@ export interface UtilBinding { styleText(format: Array | string, text: string): string; isInsideNodeModules(frameLimit?: number): boolean; constructSharedArrayBuffer(length?: number): SharedArrayBuffer; + getSharedArrayBufferGrowable(this: SharedArrayBuffer): boolean; constants: { kPending: 0;