From 356dbd3de98272deb18aa50367ca1d22915a595b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 13 Sep 2026 11:58:51 +0200 Subject: [PATCH 01/11] tools: use self-repository references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve local actions and reusable workflows from the running workflow commit with $/ references, independent of checkout paths. Remove the tarball job's action-only checkout and the WPT action checkout/copy workaround, which are no longer needed. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66013 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Xuguang Mei Reviewed-By: Ulises Gascón --- .github/workflows/build-tarball.yml | 5 ----- .github/workflows/daily-wpt-fyi.yml | 22 ++-------------------- .github/workflows/stress-test.yml | 2 +- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml index e7007953048..1c9eeeff7f2 100644 --- a/.github/workflows/build-tarball.yml +++ b/.github/workflows/build-tarball.yml @@ -68,11 +68,6 @@ jobs: SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} SCCACHE_IDLE_TIMEOUT: '0' steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: .github/actions/install-clang - sparse-checkout-cone-mode: false - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/.github/workflows/daily-wpt-fyi.yml b/.github/workflows/daily-wpt-fyi.yml index df894b236cc..afc04b5d462 100644 --- a/.github/workflows/daily-wpt-fyi.yml +++ b/.github/workflows/daily-wpt-fyi.yml @@ -105,33 +105,15 @@ jobs: else echo "UNDICI_WPT=legacy" >> $GITHUB_ENV fi - # Checkout composite actions from the default branch since the - # version-specific checkout above overwrites .github/actions/ - - name: Checkout undici WPT actions - if: ${{ env.WPT_REPORT != '' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - sparse-checkout: | - .github/actions/undici-wpt-current - .github/actions/undici-wpt-legacy - sparse-checkout-cone-mode: false - persist-credentials: false - path: _wpt_actions - clean: false - - name: Place undici WPT actions - if: ${{ env.WPT_REPORT != '' }} - run: | - mkdir -p .github/actions - cp -r _wpt_actions/.github/actions/undici-wpt-* .github/actions/ - name: Run undici WPT (current) if: ${{ env.UNDICI_WPT == 'current' }} - uses: ./.github/actions/undici-wpt-current + uses: $/.github/actions/undici-wpt-current with: undici-version: ${{ env.UNDICI_VERSION }} wpt-report: ${{ env.WPT_REPORT }} - name: Run undici WPT (legacy) if: ${{ env.UNDICI_WPT == 'legacy' }} - uses: ./.github/actions/undici-wpt-legacy + uses: $/.github/actions/undici-wpt-legacy with: undici-version: ${{ env.UNDICI_VERSION }} wpt-report: ${{ env.WPT_REPORT }} diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml index 581b3f84883..1672fd6f445 100644 --- a/.github/workflows/stress-test.yml +++ b/.github/workflows/stress-test.yml @@ -60,7 +60,7 @@ jobs: path: node - name: Install Clang ${{ env.CLANG_VERSION }} if: runner.os == 'Linux' - uses: ./node/.github/actions/install-clang + uses: $/.github/actions/install-clang with: clang-version: ${{ env.CLANG_VERSION }} - name: Set up Xcode ${{ env.XCODE_VERSION }} From c9dbf5b3029a0bdd41c9f38643b9e1905fafc5da Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 23 Sep 2026 10:50:56 +0200 Subject: [PATCH 02/11] crypto: use current FIPS state for availability Refresh conditional algorithm registration on FIPS changes and use the current state for parameter restrictions. Use per-algorithm availability checks in normalization and native named key generation. Remove getPqcKeyTypes(). Enable supports() tests under FIPS and cover state transitions in warmed workers. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66160 Reviewed-By: James M Snell Reviewed-By: Matteo Collina --- deps/ncrypto/ncrypto.cc | 6 - deps/ncrypto/ncrypto.h | 2 - lib/internal/crypto/keygen.js | 21 +-- lib/internal/crypto/util.js | 151 +++++++++++------- lib/internal/crypto/webcrypto.js | 1 - lib/internal/crypto/webidl.js | 10 +- node.gyp | 2 - src/crypto/README.md | 10 +- src/crypto/crypto_keygen.cc | 18 ++- src/crypto/crypto_keys.cc | 12 +- src/crypto/crypto_pqc.cc | 22 --- src/crypto/crypto_pqc.h | 15 -- src/crypto/crypto_sig.cc | 1 - src/node_crypto.h | 1 - test/fixtures/webcrypto/supports-level-2.mjs | 70 +++++--- .../webcrypto/supports-modern-algorithms.mjs | 100 ++++++------ .../webcrypto/supports-secure-curves.mjs | 51 +++--- test/fixtures/webcrypto/supports-sha3.mjs | 75 ++++----- .../test-crypto-key-objects-to-crypto-key.js | 8 +- test/parallel/test-crypto-key-objects.js | 3 +- test/parallel/test-crypto-key-store.js | 3 +- ...-crypto-keygen-async-elliptic-curve-jwk.js | 16 +- test/parallel/test-crypto-keygen-eddsa.js | 33 +++- test/parallel/test-crypto-keygen-raw.js | 34 ++-- test/parallel/test-crypto-provider-cache.js | 21 +++ ...-webcrypto-aead-decrypt-detached-buffer.js | 16 +- .../test-webcrypto-deduplicate-usages.js | 36 ++++- .../test-webcrypto-derivebits-cfrg.js | 24 ++- .../test-webcrypto-derivebits-hkdf.js | 8 +- test/parallel/test-webcrypto-derivebits.js | 3 +- .../parallel/test-webcrypto-derivekey-cfrg.js | 24 ++- test/parallel/test-webcrypto-derivekey.js | 3 +- .../test-webcrypto-encrypt-decrypt-aes.js | 3 +- .../test-webcrypto-encrypt-decrypt.js | 3 +- .../test-webcrypto-export-import-cfrg.js | 33 ++-- test/parallel/test-webcrypto-keygen.js | 10 +- .../test-webcrypto-raw-format-aliases.js | 3 +- test/parallel/test-webcrypto-supports-fips.js | 135 ++++++++++++++++ test/parallel/test-webcrypto-supports.mjs | 4 - test/parallel/test-webcrypto-wrap-unwrap.js | 13 +- test/wpt/status/WebCryptoAPI.cjs | 6 + typings/internalBinding/crypto.d.ts | 2 +- 42 files changed, 607 insertions(+), 405 deletions(-) delete mode 100644 src/crypto/crypto_pqc.cc delete mode 100644 src/crypto/crypto_pqc.h create mode 100644 test/parallel/test-webcrypto-supports-fips.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index c48d168531e..e005fd957fd 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -3251,12 +3251,6 @@ const KeyAlgorithm* KeyAlgorithm::FromName(const char* name) { 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; } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index f3e8cd10e5d..9a501a551b4 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -1121,8 +1121,6 @@ class KeyAlgorithm final { // 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 { diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 3f127a472ec..223a09a7623 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -4,7 +4,6 @@ const { FunctionPrototypeCall, ObjectDefineProperty, SafeArrayIterator, - StringPrototypeToLowerCase, } = primordials; const { @@ -12,7 +11,6 @@ const { DsaKeyPairGenJob, EcKeyPairGenJob, NamedKeyPairGenJob, - getPqcKeyTypes, RsaKeyPairGenJob, SecretKeyGenJob, kCryptoJobAsync, @@ -160,17 +158,6 @@ function parseKeyEncoding(keyType, options = kEmptyObject) { ]; } -const namedKeyPairs = { - '__proto__': null, - 'ed25519': 'Ed25519', - 'ed448': 'Ed448', - 'x25519': 'X25519', - 'x448': 'X448', -}; -for (const name of new SafeArrayIterator(getPqcKeyTypes())) { - namedKeyPairs[StringPrototypeToLowerCase(name)] = name; -} - function createJob(mode, type, options) { validateString(type, 'type'); @@ -339,12 +326,8 @@ function createJob(mode, type, options) { generator == null ? 2 : generator, ...encoding); } - default: { - if (namedKeyPairs[type] === undefined) { - throw new ERR_INVALID_ARG_VALUE('type', type, 'must be a supported key type'); - } - return new NamedKeyPairGenJob(mode, namedKeyPairs[type], ...encoding); - } + default: + return new NamedKeyPairGenJob(mode, type, ...encoding); } } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 4f9fb720161..38fa0a1c5d2 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -50,10 +50,19 @@ const { getFipsCrypto, getFipsCryptoGeneration, KmacJob, - getPqcKeyTypes, + isKeyAlgorithmAvailable, } = internalBinding('crypto'); -const isFips = getFipsCrypto() === 1; +let fips; +let fipsGeneration; +function isFips() { + const generation = getFipsCryptoGeneration(); + if (fipsGeneration !== generation) { + fips = getFipsCrypto() === 1; + fipsGeneration = generation; + } + return fips; +} const { getOptionValue } = require('internal/options'); @@ -122,6 +131,9 @@ let _hashCache; let _macCache; if (isBuildingSnapshot()) { addSerializeCallback(() => { + fips = undefined; + fipsGeneration = undefined; + supportedAlgorithmsGeneration = undefined; _hashCache = undefined; _macCache = undefined; }); @@ -491,53 +503,69 @@ const kAlgorithmDefinitions = { }, }; -// Conditionally supported algorithms -const pqcKeyTypes = getPqcKeyTypes(); - -const conditionalAlgorithms = { - 'AES-OCB': !!hasAesOcbMode, - 'Argon2d': !!Argon2Job, - 'Argon2i': !!Argon2Job, - 'Argon2id': !!Argon2Job, - 'ChaCha20-Poly1305': process.features.openssl_is_boringssl || - ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), - 'cSHAKE128': !process.features.openssl_is_boringssl || - ArrayPrototypeIncludes(getHashes(), 'shake128'), - 'cSHAKE256': !process.features.openssl_is_boringssl || - ArrayPrototypeIncludes(getHashes(), 'shake256'), - 'Ed448': !process.features.openssl_is_boringssl, - 'KMAC128': !!KmacJob, - 'KMAC256': !!KmacJob, - 'KT128': !isFips, - 'KT256': !isFips, - 'ML-DSA-44': ArrayPrototypeIncludes(pqcKeyTypes, 'ML-DSA-44'), - 'ML-DSA-65': ArrayPrototypeIncludes(pqcKeyTypes, 'ML-DSA-65'), - 'ML-DSA-87': ArrayPrototypeIncludes(pqcKeyTypes, 'ML-DSA-87'), - 'ML-KEM-512': ArrayPrototypeIncludes(pqcKeyTypes, 'ML-KEM-512'), - 'ML-KEM-768': ArrayPrototypeIncludes(pqcKeyTypes, 'ML-KEM-768'), - 'ML-KEM-1024': ArrayPrototypeIncludes(pqcKeyTypes, 'ML-KEM-1024'), - 'MLKEM768-P256': !isFips && ArrayPrototypeIncludes(pqcKeyTypes, 'ML-KEM-768') && - (!process.features.openssl_is_boringssl || - (ArrayPrototypeIncludes(getHashes(), 'sha3-256') && - ArrayPrototypeIncludes(getHashes(), 'shake256'))), - 'MLKEM768-X25519': !isFips && ArrayPrototypeIncludes(pqcKeyTypes, 'ML-KEM-768') && - (!process.features.openssl_is_boringssl || - (ArrayPrototypeIncludes(getHashes(), 'sha3-256') && - ArrayPrototypeIncludes(getHashes(), 'shake256'))), - 'MLKEM1024-P384': !isFips && ArrayPrototypeIncludes(pqcKeyTypes, 'ML-KEM-1024') && - (!process.features.openssl_is_boringssl || - (ArrayPrototypeIncludes(getHashes(), 'sha3-256') && - ArrayPrototypeIncludes(getHashes(), 'shake256'))), - 'SHA3-256': !process.features.openssl_is_boringssl || - ArrayPrototypeIncludes(getHashes(), 'sha3-256'), - 'SHA3-384': !process.features.openssl_is_boringssl || - ArrayPrototypeIncludes(getHashes(), 'sha3-384'), - 'SHA3-512': !process.features.openssl_is_boringssl || - ArrayPrototypeIncludes(getHashes(), 'sha3-512'), - 'TurboSHAKE128': !isFips, - 'TurboSHAKE256': !isFips, - 'X448': !process.features.openssl_is_boringssl, -}; +function has(algorithms, name) { + return ArrayPrototypeIncludes(algorithms, name); +} + +// Re-evaluated when the active FIPS state changes. +function getConditionalAlgorithms() { + const mlKem768 = isKeyAlgorithmAvailable('ML-KEM-768'); + const mlKem1024 = isKeyAlgorithmAvailable('ML-KEM-1024'); + const ciphers = getCiphers(); + const hashes = getHashes(); + const macs = getMacs(); + + const fips = isFips(); + + return { + 'AES-OCB': !!hasAesOcbMode && + (has(ciphers, 'aes-128-ocb') || + has(ciphers, 'aes-192-ocb') || + has(ciphers, 'aes-256-ocb')), + 'Argon2d': !!Argon2Job && !fips, + 'Argon2i': !!Argon2Job && !fips, + 'Argon2id': !!Argon2Job && !fips, + 'ChaCha20-Poly1305': process.features.openssl_is_boringssl || + has(ciphers, 'chacha20-poly1305'), + 'cSHAKE128': has(hashes, 'shake128'), + 'cSHAKE256': has(hashes, 'shake256'), + 'Ed25519': isKeyAlgorithmAvailable('Ed25519'), + 'Ed448': isKeyAlgorithmAvailable('Ed448'), + 'KMAC128': !!KmacJob && has(macs, 'kmac128'), + 'KMAC256': !!KmacJob && has(macs, 'kmac256'), + 'KT128': !fips, + 'KT256': !fips, + 'ML-DSA-44': isKeyAlgorithmAvailable('ML-DSA-44'), + 'ML-DSA-65': isKeyAlgorithmAvailable('ML-DSA-65'), + 'ML-DSA-87': isKeyAlgorithmAvailable('ML-DSA-87'), + 'ML-KEM-512': isKeyAlgorithmAvailable('ML-KEM-512'), + 'ML-KEM-768': mlKem768, + 'ML-KEM-1024': mlKem1024, + 'MLKEM768-P256': !fips && mlKem768 && + (!process.features.openssl_is_boringssl || + (has(hashes, 'sha3-256') && + has(hashes, 'shake256'))), + 'MLKEM768-X25519': !fips && mlKem768 && + (!process.features.openssl_is_boringssl || + (has(hashes, 'sha3-256') && + has(hashes, 'shake256'))), + 'MLKEM1024-P384': !fips && mlKem1024 && + (!process.features.openssl_is_boringssl || + (has(hashes, 'sha3-256') && + has(hashes, 'shake256'))), + 'SHA-1': has(hashes, 'sha1'), + 'SHA-256': has(hashes, 'sha256'), + 'SHA-384': has(hashes, 'sha384'), + 'SHA-512': has(hashes, 'sha512'), + 'SHA3-256': has(hashes, 'sha3-256'), + 'SHA3-384': has(hashes, 'sha3-384'), + 'SHA3-512': has(hashes, 'sha3-512'), + 'TurboSHAKE128': !fips, + 'TurboSHAKE256': !fips, + 'X25519': isKeyAlgorithmAvailable('X25519'), + 'X448': isKeyAlgorithmAvailable('X448'), + }; +} // Experimental algorithms that emit warnings. const experimentalAlgorithmsWithWarnings = [ @@ -575,6 +603,7 @@ const experimentalAlgorithmsWithWarnings = [ // Also builds a parallel Map per operation // for O(1) case-insensitive algorithm name lookup in normalizeAlgorithm. function createSupportedAlgorithms(algorithmDefs) { + const conditionalAlgorithms = getConditionalAlgorithms(); // Detached below rather than declared `__proto__: null`: V8 puts that // literal form in dictionary mode, slowing every registry lookup. const result = {}; @@ -622,8 +651,16 @@ function createSupportedAlgorithms(algorithmDefs) { return { algorithms: result, nameMap }; } -const { algorithms: kSupportedAlgorithms, nameMap: kAlgorithmNameMap } = - createSupportedAlgorithms(kAlgorithmDefinitions); +let supportedAlgorithms; +let supportedAlgorithmsGeneration; +function getSupportedAlgorithms() { + const generation = getFipsCryptoGeneration(); + if (supportedAlgorithmsGeneration !== generation) { + supportedAlgorithms = createSupportedAlgorithms(kAlgorithmDefinitions); + supportedAlgorithmsGeneration = generation; + } + return supportedAlgorithms; +} const simpleAlgorithmDictionaries = { AesCbcParams: { iv: 'BufferSource' }, @@ -685,7 +722,7 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } function validateKmacKeyLength(length) { - if ((length < 32 || length % 8) && isFips) + if ((length < 32 || length % 8) && isFips()) throw lazyDOMException('Invalid key length', 'NotSupportedError'); } @@ -745,16 +782,16 @@ function normalizeAlgorithm(algorithm, op) { webidl ??= require('internal/crypto/webidl'); - // 1. - const registeredAlgorithms = kSupportedAlgorithms[op]; // 2. 3. const initialAlg = webidl.converters.Algorithm(algorithm, kNormalizeAlgorithmOpts); + const { algorithms, nameMap } = getSupportedAlgorithms(); + const registeredAlgorithms = algorithms[op]; // 4. let algName = initialAlg.name; // 5. Case-insensitive lookup via pre-built Map (O(1) instead of O(n)). - const canonicalName = kAlgorithmNameMap[op]?.get( + const canonicalName = nameMap[op]?.get( StringPrototypeToUpperCase(algName)); if (canonicalName === undefined) throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); @@ -1229,7 +1266,9 @@ module.exports = { toBuf, kNamedCurveAliases, - kSupportedAlgorithms, + get kSupportedAlgorithms() { + return getSupportedAlgorithms().algorithms; + }, isFips, normalizeAlgorithm, normalizeHashName, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 754452f3b6e..4d9d3c4a557 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1775,7 +1775,6 @@ class SubtleCrypto { } // Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-supports - // TODO(panva): Make supports() account for the active FIPS state. static supports(operation, algorithm, lengthOrAdditionalAlgorithm = null) { webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'supports' on 'SubtleCrypto'"; diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 645dd413f4c..913a9cc157c 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -42,9 +42,6 @@ const { type, } = require('internal/webidl'); -const { getFipsCrypto } = internalBinding('crypto'); -const kRsaKeyGenMinimumModulusLength = getFipsCrypto() === 1 ? 2048 : 512; - function validateByteLength(buf, name, target) { if (getBufferSourceByteLength(buf) !== target) { throw lazyDOMException( @@ -168,6 +165,7 @@ const dictRsaKeyGenParams = [ converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (modulusLength) => { + const kRsaKeyGenMinimumModulusLength = isFips() ? 2048 : 512; if (modulusLength < kRsaKeyGenMinimumModulusLength) { throw lazyDOMException( `algorithm.modulusLength must be at least ${kRsaKeyGenMinimumModulusLength}`, @@ -294,7 +292,7 @@ function validateCShakeFunctionName(V) { const length = getBufferSourceByteLength(V); if (length === 0) return; - if (!isFips) { + if (!isFips()) { const bytes = getBufferSourceBytes(V); for (let i = 0; i < kCShakeFunctionNames.length; i++) { const functionName = kCShakeFunctionNames[i]; @@ -314,7 +312,7 @@ function validateCShakeFunctionName(V) { } function validateCShakeCustomization(V) { - if (isFips && getBufferSourceByteLength(V) !== 0) + if (isFips() && getBufferSourceByteLength(V) !== 0) throw lazyDOMException( 'Unsupported CShakeParams customization', 'NotSupportedError'); @@ -778,7 +776,7 @@ converters.KmacParams = createDictionaryConverter( converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V) => { - if ((V === 0 || V % 8) && isFips) + if ((V === 0 || V % 8) && isFips()) throw lazyDOMException( 'Invalid KmacParams outputLength', 'NotSupportedError'); diff --git a/node.gyp b/node.gyp index c6ae477ce8f..f829825ceca 100644 --- a/node.gyp +++ b/node.gyp @@ -394,7 +394,6 @@ 'src/crypto/crypto_cipher.cc', 'src/crypto/crypto_context.cc', 'src/crypto/crypto_ec.cc', - 'src/crypto/crypto_pqc.cc', 'src/crypto/crypto_kem.cc', 'src/crypto/crypto_hmac.cc', 'src/crypto/crypto_kmac.cc', @@ -433,7 +432,6 @@ 'src/crypto/crypto_tls.h', 'src/crypto/crypto_context.h', 'src/crypto/crypto_ec.h', - 'src/crypto/crypto_pqc.h', 'src/crypto/crypto_hkdf.h', 'src/crypto/crypto_pbkdf2.h', 'src/crypto/crypto_sig.h', diff --git a/src/crypto/README.md b/src/crypto/README.md index e4988647a55..fbda94ae4b3 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -46,7 +46,6 @@ following table: | `crypto_keys` | Utilities for using and generating secret, private, and public keys. | | `crypto_mac` | Provider-generic MAC implementations. | | `crypto_pbkdf2` | PBKDF2 key / bit generation implementation. | -| `crypto_pqc` | Post-quantum algorithm enumeration. | | `crypto_rsa` | RSA Key Generation functions. | | `crypto_scrypt` | Scrypt key / bit generation implementation. | | `crypto_sig` | General digital signature and verification utilities. | @@ -205,10 +204,11 @@ Public input validation remains specific to each API: PQC JWK `alg` values use exact canonical names such as `ML-DSA-44`, while raw imports require exact public `asymmetricKeyType` values such as `ml-dsa-44`. -The internal JavaScript binding exposes `getPqcKeyTypes()` for the available -known PQC algorithm names in their canonical spelling. Named key generation -passes algorithm names to `NamedKeyPairGenJob`, which resolves the name to a static -`KeyAlgorithm` descriptor. Asymmetric key IDs are not exposed to JavaScript. +The internal JavaScript binding exposes `isKeyAlgorithmAvailable()` to check +whether a known key algorithm is available from the current backend. Named key +generation passes algorithm names to `NamedKeyPairGenJob`, which resolves the +name to a static `KeyAlgorithm` descriptor. Asymmetric key IDs are not exposed to +JavaScript. Real EC curve and ASN.1/OID NIDs still have their own uses. The EC generation path keeps Ed/X algorithm descriptors separate from curve NIDs while preserving diff --git a/src/crypto/crypto_keygen.cc b/src/crypto/crypto_keygen.cc index 7e273eb57a5..1da910690b1 100644 --- a/src/crypto/crypto_keygen.cc +++ b/src/crypto/crypto_keygen.cc @@ -9,6 +9,7 @@ #include "v8.h" #include +#include namespace node { @@ -19,6 +20,7 @@ using v8::JustVoid; using v8::Local; using v8::Maybe; using v8::MaybeLocal; +using v8::Nothing; using v8::Object; using v8::Uint32; using v8::Value; @@ -40,8 +42,20 @@ Maybe NamedKeyPairGenTraits::AdditionalConfig( NamedKeyPairGenConfig* params) { CHECK(args[*offset]->IsString()); Utf8Value name(args.GetIsolate(), args[*offset]); - params->params.algorithm = ncrypto::KeyAlgorithm::FromName(*name); - CHECK_NOT_NULL(params->params.algorithm); + const auto* algorithm = ncrypto::KeyAlgorithm::FromName(*name); + // Traditional key generation accepts lowercase key types; Web Crypto passes + // normalized algorithm names and checks availability during normalization. + if (algorithm == nullptr || (!algorithm->isOkp() && !algorithm->isPqc()) || + (mode != kCryptoJobWebCrypto && + (std::string_view(*name, name.length()) != algorithm->keyTypeName() || + !algorithm->isAvailable()))) { + THROW_ERR_INVALID_ARG_VALUE( + Environment::GetCurrent(args), + "The argument 'type' must be a supported key type. Received '%s'", + *name); + return Nothing(); + } + params->params.algorithm = algorithm; *offset += 1; diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 448c419fae3..fcd8f1d1aae 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -51,6 +51,13 @@ using v8::Value; namespace crypto { namespace { +void IsKeyAlgorithmAvailable(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsString()); + const Utf8Value name(args.GetIsolate(), args[0]); + const auto* algorithm = KeyAlgorithm::FromName(*name); + args.GetReturnValue().Set(algorithm != nullptr && algorithm->isAvailable()); +} + Maybe GetKeyFormatAndTypeFromJs( const FunctionCallbackInfo& args, unsigned int* offset, @@ -2233,7 +2240,8 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatPKCS8); NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatSPKI); NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatJWK); - SetMethod(context, target, "getPqcKeyTypes", GetPqcKeyTypes); + SetMethodNoSideEffect( + context, target, "isKeyAlgorithmAvailable", IsKeyAlgorithmAvailable); NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1); NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8); NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI); @@ -2254,7 +2262,7 @@ void Initialize(Environment* env, Local target) { void RegisterExternalReferences(ExternalReferenceRegistry* registry) { KeyObjectHandle::RegisterExternalReferences(registry); - registry->Register(GetPqcKeyTypes); + registry->Register(IsKeyAlgorithmAvailable); } } // namespace Keys diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc deleted file mode 100644 index 17d6855ada7..00000000000 --- a/src/crypto/crypto_pqc.cc +++ /dev/null @@ -1,22 +0,0 @@ -#include "crypto/crypto_pqc.h" -#include "ncrypto.h" -#include "util-inl.h" -#include "v8.h" - -namespace node { - -using v8::Value; - -namespace crypto { - -void GetPqcKeyTypes(const v8::FunctionCallbackInfo& args) { - v8::LocalVector names(args.GetIsolate()); - ncrypto::KeyAlgorithm::ForEachPqc( - [&](const ncrypto::KeyAlgorithm& algorithm) { - names.push_back(OneByteString(args.GetIsolate(), algorithm.name())); - }); - args.GetReturnValue().Set( - v8::Array::New(args.GetIsolate(), names.data(), names.size())); -} -} // namespace crypto -} // namespace node diff --git a/src/crypto/crypto_pqc.h b/src/crypto/crypto_pqc.h deleted file mode 100644 index 23cdcdc0512..00000000000 --- a/src/crypto/crypto_pqc.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef SRC_CRYPTO_CRYPTO_PQC_H_ -#define SRC_CRYPTO_CRYPTO_PQC_H_ - -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#include "v8.h" - -namespace node { -namespace crypto { -void GetPqcKeyTypes(const v8::FunctionCallbackInfo& args); -} // namespace crypto -} // namespace node - -#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#endif // SRC_CRYPTO_CRYPTO_PQC_H_ diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index 7a6798c7816..1b3fdcd0002 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -3,7 +3,6 @@ #include "base_object-inl.h" #include "crypto/crypto_ec.h" #include "crypto/crypto_keys.h" -#include "crypto/crypto_pqc.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" diff --git a/src/node_crypto.h b/src/node_crypto.h index e15e5958808..92d35b844f9 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -51,7 +51,6 @@ #include "crypto/crypto_mac.h" #include "crypto/crypto_pbkdf2.h" #include "crypto/crypto_pkcs12.h" -#include "crypto/crypto_pqc.h" #include "crypto/crypto_random.h" #include "crypto/crypto_rsa.h" #include "crypto/crypto_scrypt.h" diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index db1f6327396..2e2014bb5f1 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -1,4 +1,4 @@ -import { getFips } from 'node:crypto'; +import { generateKeyPairSync, getFips } from 'node:crypto'; const { subtle } = globalThis.crypto; const RSA_MINIMUM_MODULUS_LENGTH = getFips() === 1 ? 2048 : 512; @@ -8,10 +8,28 @@ const RSA_KEY_GEN = { publicExponent: new Uint8Array([1, 0, 1]) }; -const [ECDH, X25519] = await Promise.all([ - subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits', 'deriveKey']), - subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']), -]); +// Determine availability through traditional crypto, independently of supports(). +export function generateNamedKeyPair(name) { + let pair; + try { + pair = generateKeyPairSync(name.toLowerCase()); + } catch (err) { + if (err.code !== 'ERR_INVALID_ARG_VALUE') throw err; + return; + } + const derive = name.startsWith('X'); + return { + publicKey: pair.publicKey.toCryptoKey(name, true, derive ? [] : ['verify']), + privateKey: pair.privateKey.toCryptoKey(name, false, derive ? ['deriveBits', 'deriveKey'] : ['sign']), + }; +} + +export const ECDH = await subtle.generateKey( + { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits', 'deriveKey']); +export const X25519 = generateNamedKeyPair('X25519'); +export const Ed25519 = generateNamedKeyPair('Ed25519'); +const hasX25519 = X25519 !== undefined; +const hasEd25519 = Ed25519 !== undefined; export const vectors = { 'encrypt': [ @@ -32,7 +50,7 @@ export const vectors = { [false, 'Invalid'], [false, 'SHA-1'], - [true, 'Ed25519'], + [hasEd25519, 'Ed25519'], [true, 'RSASSA-PKCS1-v1_5'], @@ -59,8 +77,8 @@ export const vectors = { [false, 'Invalid'], [false, 'HKDF'], [false, 'PBKDF2'], - [true, 'X25519'], - [true, 'Ed25519'], + [hasX25519, 'X25519'], + [hasEd25519, 'Ed25519'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], @@ -128,20 +146,20 @@ export const vectors = { [false, { name: 'PBKDF2', hash: 'SHA-256', salt: Buffer.alloc(0), iterations: 1 }, 'HKDF'], - [true, - { name: 'X25519', public: X25519.publicKey }, + [hasX25519, + { name: 'X25519', public: X25519?.publicKey }, { name: 'AES-CBC', length: 128 }], [false, - { name: 'X25519', public: X25519.publicKey }, + { name: 'X25519', public: X25519?.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], - [true, - { name: 'X25519', public: X25519.publicKey }, + [hasX25519, + { name: 'X25519', public: X25519?.publicKey }, { name: 'HMAC', hash: 'SHA-256', length: 256 }], [false, - { name: 'X25519', public: X25519.publicKey }, + { name: 'X25519', public: X25519?.publicKey }, { name: 'HMAC', hash: 'SHA-256', length: 257 }], - [true, - { name: 'X25519', public: X25519.publicKey }, + [hasX25519, + { name: 'X25519', public: X25519?.publicKey }, 'HKDF'], [true, { name: 'ECDH', public: ECDH.publicKey }, @@ -165,10 +183,10 @@ export const vectors = { { name: 'ECDH', public: ECDH.publicKey }, 'HKDF'], [false, - { name: 'X25519', public: X25519.publicKey }, + { name: 'X25519', public: X25519?.publicKey }, 'SHA-256'], [false, - { name: 'X25519', public: X25519.publicKey }, + { name: 'X25519', public: X25519?.publicKey }, 'AES-CBC'], ], 'deriveBits': [ @@ -201,17 +219,17 @@ export const vectors = { [false, { name: 'ECDH', public: ECDH.privateKey }], [false, 'ECDH'], - [true, { name: 'X25519', public: X25519.publicKey }], - [true, { name: 'X25519', public: X25519.publicKey }, 256], - [false, { name: 'X25519', public: X25519.publicKey }, 257], - [false, { name: 'X25519', public: X25519.privateKey }], + [hasX25519, { name: 'X25519', public: X25519?.publicKey }], + [hasX25519, { name: 'X25519', public: X25519?.publicKey }, 256], + [false, { name: 'X25519', public: X25519?.publicKey }, 257], + [false, { name: 'X25519', public: X25519?.privateKey }], [false, 'X25519'], ], 'importKey': [ [false, 'SHA-1'], [false, 'Invalid'], - [true, 'X25519'], - [true, 'Ed25519'], + [hasX25519, 'X25519'], + [hasEd25519, 'Ed25519'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], @@ -246,8 +264,8 @@ export const vectors = { [true, 'AES-CBC'], [true, 'AES-GCM'], [true, 'AES-KW'], - [true, 'Ed25519'], - [true, 'X25519'], + [hasEd25519, 'Ed25519'], + [hasX25519, 'X25519'], ], 'wrapKey': [ [true, 'AES-KW'], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 67fad15f7f3..b01ebce3e5a 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -1,63 +1,65 @@ import * as crypto from 'node:crypto' import { hasOpenSSL, isBoringSSL } from '../../common/crypto.js' +import { X25519, Ed25519 } from './supports-level-2.mjs' +import { X448, Ed448 } from './supports-secure-curves.mjs' const boringSSL = isBoringSSL; +const fips = crypto.getFips() === 1; +const chacha = boringSSL || crypto.getCiphers().includes('chacha20-poly1305'); const pqc = hasOpenSSL(3, 5) || boringSSL; -const argon2 = hasOpenSSL(3, 2); +const argon2 = hasOpenSSL(3, 2) && !fips; 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 ocb = hasOpenSSL(3) && crypto.getCiphers().includes('aes-128-ocb'); +const kmac = hasOpenSSL(3) && crypto.getMacs().includes('kmac128'); +const hybridKems = !fips && pqc && (!boringSSL || (sha3 && shake256)); -const { subtle } = globalThis.crypto; -const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); export const vectors = { 'digest': [ [false, 'cSHAKE128'], [shake128, { name: 'cSHAKE128', outputLength: 128 }], [shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake128 && kmac, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], + [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('SHAKE') }], - [shake128 && kmac, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], + [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(513) }], [shake128, { name: 'cSHAKE128', outputLength: 127 }], [false, 'cSHAKE256'], [shake256, { name: 'cSHAKE256', outputLength: 256 }], [shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake256 && kmac, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], + [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('SHAKE') }], - [shake256 && kmac, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], + [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(513) }], [shake256, { name: 'cSHAKE256', outputLength: 255 }], [false, 'TurboSHAKE128'], - [true, { name: 'TurboSHAKE128', outputLength: 128 }], - [true, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], + [!fips, { name: 'TurboSHAKE128', outputLength: 128 }], + [!fips, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], [false, { name: 'TurboSHAKE128', outputLength: 0 }], [false, { name: 'TurboSHAKE128', outputLength: 127 }], [false, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x00 }], [false, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x80 }], [false, 'TurboSHAKE256'], - [true, { name: 'TurboSHAKE256', outputLength: 256 }], - [true, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x07 }], + [!fips, { name: 'TurboSHAKE256', outputLength: 256 }], + [!fips, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x07 }], [false, { name: 'TurboSHAKE256', outputLength: 0 }], [false, { name: 'TurboSHAKE256', outputLength: 255 }], [false, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x00 }], [false, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x80 }], [false, 'KT128'], - [true, { name: 'KT128', outputLength: 128 }], - [true, { name: 'KT128', outputLength: 128, customization: Buffer.alloc(0) }], - [true, { name: 'KT128', outputLength: 128, customization: Buffer.alloc(512) }], + [!fips, { name: 'KT128', outputLength: 128 }], + [!fips, { name: 'KT128', outputLength: 128, customization: Buffer.alloc(0) }], + [!fips, { name: 'KT128', outputLength: 128, customization: Buffer.alloc(512) }], [false, { name: 'KT128', outputLength: 128, customization: Buffer.alloc(513) }], [false, { name: 'KT128', outputLength: 0 }], [false, { name: 'KT128', outputLength: 127 }], [false, 'KT256'], - [true, { name: 'KT256', outputLength: 256 }], - [true, { name: 'KT256', outputLength: 256, customization: Buffer.alloc(0) }], - [true, { name: 'KT256', outputLength: 256, customization: Buffer.alloc(512) }], + [!fips, { name: 'KT256', outputLength: 256 }], + [!fips, { name: 'KT256', outputLength: 256, customization: Buffer.alloc(0) }], + [!fips, { name: 'KT256', outputLength: 256, customization: Buffer.alloc(512) }], [false, { name: 'KT256', outputLength: 256, customization: Buffer.alloc(513) }], [false, { name: 'KT256', outputLength: 0 }], [false, { name: 'KT256', outputLength: 255 }], @@ -84,9 +86,9 @@ export const vectors = { [false, 'KMAC128'], [false, 'KMAC256'], [kmac, { name: 'KMAC128', outputLength: 256 }], - [kmac, { name: 'KMAC128', outputLength: 255 }], + [kmac && !fips, { name: 'KMAC128', outputLength: 255 }], [kmac, { name: 'KMAC256', outputLength: 256 }], - [kmac, { name: 'KMAC256', outputLength: 255 }], + [kmac && !fips, { name: 'KMAC256', outputLength: 255 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], @@ -95,7 +97,7 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [true, 'ChaCha20-Poly1305'], + [chacha, 'ChaCha20-Poly1305'], [hybridKems, 'MLKEM768-P256'], [hybridKems, 'MLKEM768-X25519'], [hybridKems, 'MLKEM1024-P384'], @@ -107,10 +109,10 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac, { name: 'KMAC128', length: 0 }], - [kmac, { name: 'KMAC256', length: 0 }], - [kmac, { name: 'KMAC128', length: 1 }], - [kmac, { name: 'KMAC256', length: 1 }], + [kmac && !fips, { name: 'KMAC128', length: 0 }], + [kmac && !fips, { name: 'KMAC256', length: 0 }], + [kmac && !fips, { name: 'KMAC128', length: 1 }], + [kmac && !fips, { name: 'KMAC256', length: 1 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -119,7 +121,7 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [true, 'ChaCha20-Poly1305'], + [chacha, 'ChaCha20-Poly1305'], [hybridKems, 'MLKEM768-P256'], [hybridKems, 'MLKEM768-X25519'], [hybridKems, 'MLKEM1024-P384'], @@ -131,10 +133,10 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac, { name: 'KMAC128', length: 0 }], - [kmac, { name: 'KMAC256', length: 0 }], - [kmac, { name: 'KMAC128', length: 1 }], - [kmac, { name: 'KMAC256', length: 1 }], + [kmac && !fips, { name: 'KMAC128', length: 0 }], + [kmac && !fips, { name: 'KMAC256', length: 0 }], + [kmac && !fips, { name: 'KMAC128', length: 1 }], + [kmac && !fips, { name: 'KMAC256', length: 1 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -143,7 +145,7 @@ export const vectors = { [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [true, 'ChaCha20-Poly1305'], + [chacha, 'ChaCha20-Poly1305'], [hybridKems, 'MLKEM768-P256'], [hybridKems, 'MLKEM768-X25519'], [hybridKems, 'MLKEM1024-P384'], @@ -158,10 +160,10 @@ export const vectors = { [true, 'RSA-OAEP'], [true, 'RSA-PSS'], [true, 'RSASSA-PKCS1-v1_5'], - [true, 'X25519'], - [!boringSSL, 'X448'], - [true, 'Ed25519'], - [!boringSSL, 'Ed448'], + [!!X25519, 'X25519'], + [!!X448, 'X448'], + [!!Ed25519, 'Ed25519'], + [!!Ed448, 'Ed448'], [true, 'ECDH'], [true, 'ECDSA'], [pqc, 'ML-DSA-44'], @@ -186,14 +188,14 @@ export const vectors = { [false, 'KMAC256'], ], 'deriveKey': [ - [argon2, - { name: 'X25519', public: X25519.publicKey }, + [argon2 && !!X25519, + { name: 'X25519', public: X25519?.publicKey }, 'Argon2d'], - [argon2, - { name: 'X25519', public: X25519.publicKey }, + [argon2 && !!X25519, + { name: 'X25519', public: X25519?.publicKey }, 'Argon2i'], - [argon2, - { name: 'X25519', public: X25519.publicKey }, + [argon2 && !!X25519, + { name: 'X25519', public: X25519?.publicKey }, 'Argon2id'], ], 'deriveBits': [ @@ -215,9 +217,9 @@ export const vectors = { [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 16777215, memory: 8, passes: 1 }, 32], ], 'encrypt': [ - [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], + [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(16) }], - [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], + [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], [false, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', iv: Buffer.alloc(15) }], @@ -242,14 +244,14 @@ export const vectors = { [pqc, 'ML-KEM-768', 'AES-GCM'], [pqc, 'ML-KEM-768', 'AES-CTR'], [pqc, 'ML-KEM-768', 'AES-CBC'], - [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc && chacha, 'ML-KEM-768', 'ChaCha20-Poly1305'], [pqc, 'ML-KEM-768', 'HKDF'], [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [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 }], + [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], @@ -274,14 +276,14 @@ export const vectors = { [pqc, 'ML-KEM-768', 'AES-GCM'], [pqc, 'ML-KEM-768', 'AES-CTR'], [pqc, 'ML-KEM-768', 'AES-CBC'], - [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc && chacha, 'ML-KEM-768', 'ChaCha20-Poly1305'], [pqc, 'ML-KEM-768', 'HKDF'], [pqc, 'ML-KEM-768', 'PBKDF2'], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [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 }], + [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], diff --git a/test/fixtures/webcrypto/supports-secure-curves.mjs b/test/fixtures/webcrypto/supports-secure-curves.mjs index fa3f1d36521..52dd1c95995 100644 --- a/test/fixtures/webcrypto/supports-secure-curves.mjs +++ b/test/fixtures/webcrypto/supports-secure-curves.mjs @@ -1,62 +1,55 @@ -import { hasOpenSSL, isBoringSSL } from '../../common/crypto.js' +import { hasOpenSSL } from '../../common/crypto.js'; +import { generateNamedKeyPair, X25519 } from './supports-level-2.mjs'; const supportsContext = hasOpenSSL(3, 2); - -const { subtle } = globalThis.crypto; - -const boringSSL = isBoringSSL; - -const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); -let X448; -let Ed448; -if (!boringSSL) { - X448 = await subtle.generateKey('X448', false, ['deriveBits', 'deriveKey']) - Ed448 = await subtle.generateKey('Ed448', false, ['sign', 'verify']) -} +export const X448 = generateNamedKeyPair('X448'); +export const Ed448 = generateNamedKeyPair('Ed448'); +const hasX448 = X448 !== undefined; +const hasEd448 = Ed448 !== undefined; export const vectors = { 'sign': [ - [!boringSSL, 'Ed448'], - [!boringSSL, { name: 'Ed448', context: Buffer.alloc(0) }], - [!boringSSL && supportsContext, { name: 'Ed448', context: Buffer.alloc(32) }], - [!boringSSL && supportsContext, { name: 'Ed448', context: Buffer.alloc(255) }], + [hasEd448, 'Ed448'], + [hasEd448, { name: 'Ed448', context: Buffer.alloc(0) }], + [hasEd448 && supportsContext, { name: 'Ed448', context: Buffer.alloc(32) }], + [hasEd448 && supportsContext, { name: 'Ed448', context: Buffer.alloc(255) }], [false, { name: 'Ed448', context: Buffer.alloc(256) }], ], 'generateKey': [ - [!boringSSL, 'X448'], - [!boringSSL, 'Ed448'], + [hasX448, 'X448'], + [hasEd448, 'Ed448'], ], 'deriveKey': [ - [!boringSSL, + [hasX448, { name: 'X448', public: X448?.publicKey }, { name: 'AES-CBC', length: 128 }], [false, { name: 'X448', public: X448?.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], - [!boringSSL, + [hasX448, { name: 'X448', public: X448?.publicKey }, { name: 'HMAC', hash: 'SHA-256', length: 448 }], [false, { name: 'X448', public: X448?.publicKey }, { name: 'HMAC', hash: 'SHA-256', length: 449 }], - [!boringSSL, + [hasX448, { name: 'X448', public: X448?.publicKey }, 'HKDF'], ], 'deriveBits': [ - [!boringSSL, { name: 'X448', public: X448?.publicKey }], - [!boringSSL, { name: 'X448', public: X448?.publicKey }, 448], + [hasX448, { name: 'X448', public: X448?.publicKey }], + [hasX448, { name: 'X448', public: X448?.publicKey }, 448], [false, { name: 'X448', public: X448?.publicKey }, 449], - [false, { name: 'X448', public: X25519.publicKey }], + [false, { name: 'X448', public: X25519?.publicKey }], [false, { name: 'X448', public: X448?.privateKey }], [false, 'X448'], ], 'importKey': [ - [!boringSSL, 'X448'], - [!boringSSL, 'Ed448'], + [hasX448, 'X448'], + [hasEd448, 'Ed448'], ], 'exportKey': [ - [!boringSSL, 'Ed448'], - [!boringSSL, 'X448'], + [hasEd448, 'Ed448'], + [hasX448, 'X448'], ], }; diff --git a/test/fixtures/webcrypto/supports-sha3.mjs b/test/fixtures/webcrypto/supports-sha3.mjs index fe256dcfb16..699b72bb11b 100644 --- a/test/fixtures/webcrypto/supports-sha3.mjs +++ b/test/fixtures/webcrypto/supports-sha3.mjs @@ -1,33 +1,28 @@ -import { isBoringSSL } from '../../common/crypto.js' +import { getHashes } from 'node:crypto'; +import { ECDH, X25519 } from './supports-level-2.mjs'; -const { subtle } = globalThis.crypto; - -const boringSSL = isBoringSSL; +const hashes = getHashes(); +const hasSha3 = hashes.includes('sha3-256'); const RSA_KEY_GEN = { modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]) }; -const [ECDH, X25519] = await Promise.all([ - subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits', 'deriveKey']), - subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']), -]); - export const vectors = { 'digest': [ - [!boringSSL, 'SHA3-256'], - [!boringSSL, 'SHA3-384'], - [!boringSSL, 'SHA3-512'], + [hasSha3, 'SHA3-256'], + [hashes.includes('sha3-384'), 'SHA3-384'], + [hashes.includes('sha3-512'), 'SHA3-512'], ], 'generateKey': [ - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], - [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [hasSha3, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [hasSha3, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [hasSha3, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], // This interaction is not defined for now. @@ -35,28 +30,28 @@ export const vectors = { [false, { name: 'HMAC', hash: 'SHA3-256' }], ], 'deriveKey': [ - [!boringSSL, + [hasSha3, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, { name: 'AES-CBC', length: 128 }], - [!boringSSL, + [hasSha3, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 'HKDF'], - [!boringSSL, + [hasSha3, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, { name: 'AES-CBC', length: 128 }], - [!boringSSL, + [hasSha3, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 'HKDF'], - [!boringSSL, - { name: 'X25519', public: X25519.publicKey }, + [hasSha3 && X25519 !== undefined, + { name: 'X25519', public: X25519?.publicKey }, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [!boringSSL, + [hasSha3, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], @@ -69,34 +64,34 @@ export const vectors = { { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, { name: 'HMAC', hash: 'SHA3-256' }], [false, - { name: 'X25519', public: X25519.publicKey }, + { name: 'X25519', public: X25519?.publicKey }, { name: 'HMAC', hash: 'SHA3-256' }], [false, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA3-256' }], ], 'deriveBits': [ - [!boringSSL, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8], - [!boringSSL, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 0], + [hasSha3, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8], + [hasSha3, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 0], [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, null], [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 7], - [!boringSSL, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 8], - [!boringSSL, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 0], + [hasSha3, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 8], + [hasSha3, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 0], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 0 }, 8], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, null], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 7], ], 'importKey': [ - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], - [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256' }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [hasSha3, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [hasSha3, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [hasSha3, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256' }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [hasSha3, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], ], 'get key length': [ 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 a8fe94f1dd4..eeb5c90c166 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -250,8 +250,7 @@ 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$/, + code: 'ERR_INVALID_ARG_VALUE', }); return []; } @@ -307,9 +306,12 @@ function invalidAsymmetricKeyType(name, invalidAlgorithm) { const { publicKey } = generateKeyPairSync(name.toLowerCase()); assert.throws(() => { publicKey.toCryptoKey(invalidAlgorithm, true, []); - }, { + }, invalidAlgorithm in kSupportedAlgorithms.importKey ? { name: 'DataError', message: 'Invalid key type' + } : { + name: 'NotSupportedError', + message: 'Unrecognized algorithm name', }); } diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index f4af79e31b5..c2cf0dc6cdc 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -1228,8 +1228,7 @@ if (!isBoringSSL) { const first = generateKeyPairSync('ed25519'); if (rejectsXCurves) { assert.throws(() => generateKeyPairSync('x25519'), { - name: 'Error', - message: /:digital envelope routines::unsupported$/, + code: 'ERR_INVALID_ARG_VALUE', }); } else { const second = generateKeyPairSync('x25519'); diff --git a/test/parallel/test-crypto-key-store.js b/test/parallel/test-crypto-key-store.js index e777440f2c0..d5636f350dc 100644 --- a/test/parallel/test-crypto-key-store.js +++ b/test/parallel/test-crypto-key-store.js @@ -86,8 +86,7 @@ const data = Buffer.from('hello store'); { if (hasFIPS(3, 5)) { assert.throws(() => generateKeyPairSync('x25519'), { - name: 'Error', - message: /:digital envelope routines::unsupported$/, + code: 'ERR_INVALID_ARG_VALUE', }); } else { const alice = generateKeyPairSync('x25519'); 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 84ecccd55ee..bd097f04eca 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js @@ -18,19 +18,21 @@ const rejectsXCurves = hasFIPS(3, 5); common.printSkipMessage(`Skipping unsupported ${type} test case`); continue; } - generateKeyPair(type, { + const options = { publicKeyEncoding: { format: 'jwk' }, privateKeyEncoding: { format: 'jwk' } - }, common.mustCall((err, publicKey, privateKey) => { - if (rejectsXCurves && type.startsWith('x')) { - assert.strictEqual(err?.message, 'error:0308010C:digital envelope routines::unsupported'); - return; - } - assert.ifError(err); + }; + if (rejectsXCurves && type.startsWith('x')) { + assert.throws(() => generateKeyPair(type, options, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + }); + continue; + } + generateKeyPair(type, options, common.mustSucceed((publicKey, privateKey) => { assert.strictEqual(typeof publicKey, 'object'); assert.strictEqual(typeof privateKey, 'object'); assert.strictEqual(publicKey.x, privateKey.x); diff --git a/test/parallel/test-crypto-keygen-eddsa.js b/test/parallel/test-crypto-keygen-eddsa.js index ff17d03e752..a9faf39d0ab 100644 --- a/test/parallel/test-crypto-keygen-eddsa.js +++ b/test/parallel/test-crypto-keygen-eddsa.js @@ -7,23 +7,40 @@ if (!common.hasCrypto) const assert = require('assert'); const { generateKeyPair, + generateKeyPairSync, } = require('crypto'); const { hasFIPS, isBoringSSL } = require('../common/crypto'); const rejectsXCurves = hasFIPS(3, 5); +// Named key generation accepts only supported, lowercase public key types. +for (const type of [ + 'toString', 'constructor', 'sm2', 'ml-kem-999', + 'Ed25519', 'X25519', 'ML-KEM-768', 'ML-DSA-44', 'SLH-DSA-SHA2-128f', +]) { + const error = { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: `The argument 'type' must be a supported key type. Received '${type}'`, + }; + assert.throws(() => generateKeyPairSync(type), error); + assert.throws(() => generateKeyPair(type, common.mustNotCall()), error); +} + // Test EdDSA key generation. { for (const keyType of ['ed25519', 'ed448', 'x25519', 'x448']) { - if (isBoringSSL && keyType.endsWith('448')) { - common.printSkipMessage(`Skipping unsupported ${keyType} test case`); + if ((isBoringSSL && keyType.endsWith('448')) || + (rejectsXCurves && keyType.startsWith('x'))) { + const error = { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: `The argument 'type' must be a supported key type. Received '${keyType}'`, + }; + assert.throws(() => generateKeyPairSync(keyType), error); + assert.throws(() => generateKeyPair(keyType, common.mustNotCall()), error); continue; } - 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); + generateKeyPair(keyType, common.mustSucceed((publicKey, privateKey) => { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, keyType); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); diff --git a/test/parallel/test-crypto-keygen-raw.js b/test/parallel/test-crypto-keygen-raw.js index 0006e5e1535..9c6b85a1fc2 100644 --- a/test/parallel/test-crypto-keygen-raw.js +++ b/test/parallel/test-crypto-keygen-raw.js @@ -17,19 +17,18 @@ const rejectsXCurves = hasFIPS(3, 5); // Test generateKeyPairSync with raw encoding for EdDSA/ECDH key types. { - const types = ['ed25519', 'x25519']; - if (!isBoringSSL) { - types.push('ed448', 'x448'); - } + const types = ['ed25519', 'x25519', 'ed448', 'x448']; for (const type of types) { const options = { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, }; - if (rejectsXCurves && type.startsWith('x')) { + if ((isBoringSSL && type.endsWith('448')) || + (rejectsXCurves && type.startsWith('x'))) { assert.throws(() => generateKeyPairSync(type, options), { - name: 'Error', - message: 'error:0308010C:digital envelope routines::unsupported', + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: `The argument 'type' must be a supported key type. Received '${type}'`, }); continue; } @@ -59,22 +58,23 @@ const rejectsXCurves = hasFIPS(3, 5); // Test async generateKeyPair with raw encoding for EdDSA/ECDH key types. { - const types = ['ed25519', 'x25519']; - if (!isBoringSSL) { - types.push('ed448', 'x448'); - } + const types = ['ed25519', 'x25519', 'ed448', 'x448']; for (const type of types) { const options = { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, }; + if ((isBoringSSL && type.endsWith('448')) || + (rejectsXCurves && type.startsWith('x'))) { + assert.throws(() => generateKeyPair(type, options, common.mustNotCall()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: `The argument 'type' must be a supported key type. Received '${type}'`, + }); + continue; + } 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); + common.mustSucceed((publicKey, privateKey) => { assert(Buffer.isBuffer(publicKey)); assert(Buffer.isBuffer(privateKey)); })); diff --git a/test/parallel/test-crypto-provider-cache.js b/test/parallel/test-crypto-provider-cache.js index 4767d47dc52..bbfee76b615 100644 --- a/test/parallel/test-crypto-provider-cache.js +++ b/test/parallel/test-crypto-provider-cache.js @@ -93,7 +93,26 @@ function checkMacAliases() { assert.strictEqual(getAliasId(after, 'kmac-128'), id); } +function checkNamedKeygen() { + for (const name of ['ML-KEM-768', 'Ed25519', 'Ed448', 'X25519', 'X448']) { + const type = name.toLowerCase(); + if (binding.isKeyAlgorithmAvailable(name)) { + const { publicKey, privateKey } = crypto.generateKeyPairSync(type); + assert.strictEqual(publicKey.asymmetricKeyType, type); + assert.strictEqual(privateKey.asymmetricKeyType, type); + } else { + const error = { + name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', + message: `The argument 'type' must be a supported key type. Received '${type}'`, + }; + assert.throws(() => crypto.generateKeyPairSync(type), error); + assert.throws(() => crypto.generateKeyPair(type, common.mustNotCall()), error); + } + } +} + function createFixtures(lists) { + checkNamedKeygen(); const fixtures = {}; if (lists.getCiphers.includes(cipherAlgorithm)) { const info = crypto.getCipherInfo(cipherAlgorithm); @@ -128,6 +147,7 @@ function createFixtures(lists) { } function checkEnabled(fixtures, available) { + checkNamedKeygen(); // Availability comes from a fresh environment. Exercise cached handles before // refreshing the warmed JavaScript lists. assert(!available.getCiphers.includes(cipherAlgorithm)); @@ -165,6 +185,7 @@ function checkEnabled(fixtures, available) { } function checkDisabled(fixtures) { + checkNamedKeygen(); if (fixtures.cipher !== undefined) { assert(crypto.getCipherInfo(cipherAlgorithm)); const cipher = crypto.createCipheriv(cipherAlgorithm, cipherKey, iv); diff --git a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js index 8ac5125b276..53db81496b2 100644 --- a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js +++ b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js @@ -15,8 +15,13 @@ async function test( keyLength, ivLength, format = 'raw', - causeMessage, ) { + if (fips3 && algorithmName === 'AES-OCB') { + await assert.rejects( + subtle.importKey(format, new Uint8Array(keyLength), algorithmName, false, ['decrypt']), + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); + return; + } const key = await subtle.importKey( format, new Uint8Array(keyLength), @@ -28,13 +33,9 @@ async function test( 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), - expected, + { name: 'OperationError' }, ); } @@ -60,8 +61,7 @@ if (hasOpenSSL(3)) { 'AES-OCB', 32, 12, - 'raw-secret', - fips3 ? 'error:0308010C:digital envelope routines::unsupported' : undefined)); + 'raw-secret')); } Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index 56feda2c7bd..f8d4dbd7a83 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -71,7 +71,14 @@ function assertSameSet(actual, expected, msg) { for (const { algorithm, usages, expected } of symmetric) { tests.push((async () => { - const key = await subtle.generateKey(algorithm, true, usages); + const generated = subtle.generateKey(algorithm, true, usages); + if (hasFIPS(3) && algorithm.name === 'AES-OCB') { + await assert.rejects(generated, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }); + return; + } + const key = await generated; assertSameSet(key.usages, expected, `generateKey ${algorithm.name}`); assert.strictEqual(key.usages.length, expected.length, @@ -228,12 +235,19 @@ function assertSameSet(actual, expected, msg) { // Argon2 only supports raw-secret import. if (hasOpenSSL(3, 2)) { tests.push((async () => { - const key = await subtle.importKey( + const imported = subtle.importKey( 'raw-secret', new Uint8Array(16), 'Argon2id', false, ['deriveBits', 'deriveBits']); + if (hasFIPS(3)) { + await assert.rejects(imported, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }); + return; + } + const key = await imported; assertSameSet(key.usages, ['deriveBits'], 'importKey raw-secret Argon2id'); assert.strictEqual(key.usages.length, 1); @@ -348,12 +362,19 @@ function assertSameSet(actual, expected, msg) { // AES-OCB raw-secret import. if (hasOpenSSL(3)) { tests.push((async () => { - const key = await subtle.importKey( + const imported = subtle.importKey( 'raw-secret', new Uint8Array(16), { name: 'AES-OCB' }, true, ['decrypt', 'encrypt', 'decrypt', 'encrypt']); + if (hasFIPS(3)) { + await assert.rejects(imported, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }); + return; + } + const key = await imported; assertSameSet(key.usages, ['encrypt', 'decrypt']); assert.strictEqual(key.usages.length, 2); })()); @@ -451,7 +472,14 @@ function assertSameSet(actual, expected, msg) { for (const { algorithm, usages, expected } of jwkVectors) { tests.push((async () => { - const key = await subtle.generateKey(algorithm, true, usages); + const generated = subtle.generateKey(algorithm, true, usages); + if (hasFIPS(3) && algorithm.name === 'AES-OCB') { + await assert.rejects(generated, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }); + return; + } + const key = await generated; const jwk = await subtle.exportKey('jwk', key); assertSameSet(jwk.key_ops, expected, `jwk key_ops for ${algorithm.name}`); diff --git a/test/parallel/test-webcrypto-derivebits-cfrg.js b/test/parallel/test-webcrypto-derivebits-cfrg.js index d2fbb2998ae..faacfb6922a 100644 --- a/test/parallel/test-webcrypto-derivebits-cfrg.js +++ b/test/parallel/test-webcrypto-derivebits-cfrg.js @@ -45,10 +45,7 @@ async function prepareKeys() { const keys = {}; await Promise.all( kTests.map(async ({ name, size, pkcs8, spki, result }) => { - const [ - privateKey, - publicKey, - ] = await Promise.all([ + const imported = [ subtle.importKey( 'pkcs8', Buffer.from(pkcs8, 'hex'), @@ -61,7 +58,14 @@ async function prepareKeys() { { name }, true, []), - ]); + ]; + if (rejectsXCurves) { + await Promise.all(imported.map((promise) => assert.rejects(promise, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }))); + return; + } + const [privateKey, publicKey] = await Promise.all(imported); keys[name] = { privateKey, publicKey, @@ -74,20 +78,12 @@ async function prepareKeys() { (async function() { const keys = await prepareKeys(); + if (rejectsXCurves) return; await Promise.all( 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 fafd6a7afa5..4fb372fd3a8 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, isBoringSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; function getDeriveKeyInfo(name, length, hash, ...usages) { @@ -606,6 +606,12 @@ async function testWrongKeyType( kDerivedKeyTypes.forEach((keyType) => { const keyArgs = getDeriveKeyInfo(...keyType); + if (hasFIPS() && keyType[0] === 'AES-OCB') { + variations.push(assert.rejects(testDeriveKey(...args, ...keyArgs), { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + })); + return; + } variations.push(testDeriveKey(...args, ...keyArgs)); variations.push(testDeriveKeyBadHash(...args, ...keyArgs)); variations.push(testDeriveKeyBadUsage( diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js index b81669f95de..ee4de1c26a2 100644 --- a/test/parallel/test-webcrypto-derivebits.js +++ b/test/parallel/test-webcrypto-derivebits.js @@ -168,8 +168,7 @@ const rejectsXCurves = hasFIPS(3, 5); for (const name of ['X25519', 'X448']) { assert.rejects( test(name), - (err) => err.name === 'OperationError' && - err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }) .then(common.mustCall()); } } else { diff --git a/test/parallel/test-webcrypto-derivekey-cfrg.js b/test/parallel/test-webcrypto-derivekey-cfrg.js index 133292cbeff..130e2ad4cc8 100644 --- a/test/parallel/test-webcrypto-derivekey-cfrg.js +++ b/test/parallel/test-webcrypto-derivekey-cfrg.js @@ -44,10 +44,7 @@ async function prepareKeys() { const keys = {}; await Promise.all( kTests.map(async ({ name, size, pkcs8, spki, result }) => { - const [ - privateKey, - publicKey, - ] = await Promise.all([ + const imported = [ subtle.importKey( 'pkcs8', Buffer.from(pkcs8, 'hex'), @@ -60,7 +57,14 @@ async function prepareKeys() { { name }, true, []), - ]); + ]; + if (rejectsXCurves) { + await Promise.all(imported.map((promise) => assert.rejects(promise, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }))); + return; + } + const [privateKey, publicKey] = await Promise.all(imported); keys[name] = { privateKey, publicKey, @@ -73,6 +77,7 @@ async function prepareKeys() { (async function() { const keys = await prepareKeys(); + if (rejectsXCurves) return; const otherArgs = [ { name: 'HMAC', hash: 'SHA-256', length: 256 }, true, @@ -82,15 +87,6 @@ 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 325c4472d80..c567edfbb4e 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -365,8 +365,7 @@ if (hasOpenSSL(3) && !hasFIPS()) { for (const name of ['X25519', 'X448']) { assert.rejects( test(name), - (err) => err.name === 'OperationError' && - err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }) .then(common.mustCall()); } } else { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js index 14cc0127188..bdf1a1d6666 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -241,8 +241,7 @@ if (hasOpenSSL(3)) { if (getFips() === 1) { await assert.rejects( testEncrypt(passing[0]), - (err) => err.name === 'OperationError' && - err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); return; } diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index ae8cd7fcfdb..0f3341db64b 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -211,8 +211,7 @@ if (hasOpenSSL(3)) { if (getFips() === 1) { assert.rejects( test(), - (err) => err.name === 'OperationError' && - err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }) .then(common.mustCall()); } else { test().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index 70594c36507..f892755e13c 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -412,18 +412,18 @@ async function testImportRaw({ name, publicUsages }) { const tests = []; for (const vector of testVectors) { for (const extractable of [true, false]) { - tests.push(testImportSpki(vector, extractable)); - tests.push(testImportPkcs8(vector, extractable)); - if (rejectsXCurves && vector.name.startsWith('X')) { - tests.push(assert.rejects( - testImportJwk(vector, extractable), - { name: 'DataError' })); - } else { - tests.push(testImportJwk(vector, extractable)); + for (const test of [testImportSpki, testImportPkcs8, testImportJwk]) { + const imported = test(vector, extractable); + tests.push(rejectsXCurves && vector.name.startsWith('X') ? + assert.rejects(imported, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }) : imported); } } if (rejectsXCurves && vector.name.startsWith('X')) { - tests.push(assert.rejects(testImportRaw(vector), { name: 'DataError' })); + tests.push(assert.rejects(testImportRaw(vector), { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + })); } else { tests.push(testImportRaw(vector)); } @@ -452,7 +452,9 @@ async function testImportRaw({ name, publicUsages }) { { name }, true, [invalidUsage]), - { name: 'SyntaxError', message: /Unsupported key usage/ }); + rejectsXCurves && isKeyAgreement ? + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' } : + { name: 'SyntaxError', message: /Unsupported key usage/ }); const validUsage = privateUsages[0]; await assert.rejects( @@ -462,7 +464,9 @@ async function testImportRaw({ name, publicUsages }) { { name }, true, [validUsage]), - { name: 'DataError', message: 'Duplicate key operation' }); + rejectsXCurves && isKeyAgreement ? + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' } : + { name: 'DataError', message: 'Duplicate key operation' }); } })().then(common.mustCall()); @@ -476,15 +480,18 @@ async function testImportRaw({ name, publicUsages }) { ['Ed25519', ['verify'], ['sign']], ['X25519', [], ['deriveBits']], ]) { + const error = rejectsXCurves && name.startsWith('X') ? + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' } : + { message: /Invalid key type/ }; assert.rejects(subtle.importKey( 'spki', rsaPublic.export({ format: 'der', type: 'spki' }), { name }, - true, publicUsages), { message: /Invalid key type/ }).then(common.mustCall()); + true, publicUsages), error).then(common.mustCall()); assert.rejects(subtle.importKey( 'pkcs8', rsaPrivate.export({ format: 'der', type: 'pkcs8' }), { name }, - true, privateUsages), { message: /Invalid key type/ }).then(common.mustCall()); + true, privateUsages), error).then(common.mustCall()); } } diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 951ec07484b..30744bcdbe5 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -251,18 +251,17 @@ if (hasOpenSSL(3, 5) || isBoringSSL) { // Test bad usages { async function test(name) { - if (fips3 && name === 'ChaCha20-Poly1305') { + if (fips3 && (name === 'ChaCha20-Poly1305' || name === 'AES-OCB')) { await assert.rejects( subtle.generateKey({ name }, true, []), - { name: 'NotSupportedError' }); + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); 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'); + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); return; } @@ -757,8 +756,7 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); 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'); + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); } async function test( diff --git a/test/parallel/test-webcrypto-raw-format-aliases.js b/test/parallel/test-webcrypto-raw-format-aliases.js index bce0c353904..49a09713f49 100644 --- a/test/parallel/test-webcrypto-raw-format-aliases.js +++ b/test/parallel/test-webcrypto-raw-format-aliases.js @@ -72,8 +72,7 @@ const tests = [ if (rejectsXCurves) { tests.push(assert.rejects( subtle.generateKey('X25519', true, ['deriveBits']), - (err) => err.name === 'OperationError' && - err.cause?.message === 'error:0308010C:digital envelope routines::unsupported')); + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' })); } else { tests.push(assertPublicKeyDoesNotAcceptRawSecret( 'X25519', diff --git a/test/parallel/test-webcrypto-supports-fips.js b/test/parallel/test-webcrypto-supports-fips.js new file mode 100644 index 00000000000..ae5d1d4b32d --- /dev/null +++ b/test/parallel/test-webcrypto-supports-fips.js @@ -0,0 +1,135 @@ +// 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('requires OpenSSL 3'); + +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 { normalizeAlgorithm } = require('internal/crypto/util'); +const { subtle } = globalThis.crypto; + +if (!isMainThread && !workerData?.supportsFips) + common.skip('crypto.setFips() is not supported in workers'); + +async function check() { + const fips = crypto.getFips() === 1; + const turbo = { name: 'TurboSHAKE128', outputLength: 128 }; + if (fips) { + const error = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; + assert.throws(() => normalizeAlgorithm(turbo, 'digest'), error); + await assert.rejects(subtle.digest(turbo, new Uint8Array()), error); + } else { + assert.strictEqual(normalizeAlgorithm(turbo, 'digest').name, turbo.name); + assert.strictEqual((await subtle.digest(turbo, new Uint8Array())).byteLength, 16); + } + assert.strictEqual(SubtleCrypto.supports('digest', turbo), !fips); + for (const [algorithm, usages] of [ + [{ name: 'AES-OCB', length: 128 }, ['encrypt']], + [{ name: 'X25519' }, ['deriveBits']], + ]) { + const supported = SubtleCrypto.supports('generateKey', algorithm); + const generated = subtle.generateKey(algorithm, true, usages); + if (supported) { + await generated; + } else { + await assert.rejects(generated, { + name: 'NotSupportedError', message: 'Unrecognized algorithm name', + }); + } + } + for (const name of ['ECDH', 'ECDSA']) { + for (const namedCurve of ['P-256', 'P-384', 'P-521']) { + const algorithm = { name, namedCurve }; + for (const operation of ['generateKey', 'importKey']) { + assert.strictEqual(SubtleCrypto.supports(operation, algorithm), true); + } + } + } + const rsa = { + name: 'RSA-PSS', modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', + }; + assert.strictEqual(SubtleCrypto.supports('generateKey', rsa), !fips); + if (fips) { + assert.throws(() => normalizeAlgorithm(rsa, 'generateKey'), { + name: 'OperationError', message: 'algorithm.modulusLength must be at least 2048', + }); + } else { + assert.strictEqual(normalizeAlgorithm(rsa, 'generateKey').modulusLength, 1024); + } + const hashes = crypto.getHashes(); + const salt = new Uint8Array(16); + const hashError = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; + for (const [name, alias] of [ + ['SHA-1', 'sha1'], ['SHA-256', 'sha256'], ['SHA-384', 'sha384'], ['SHA-512', 'sha512'], + ['SHA3-256', 'sha3-256'], ['SHA3-384', 'sha3-384'], ['SHA3-512', 'sha3-512'], + ]) { + const available = hashes.includes(alias); + for (const hash of [name, { name }]) { + const hmac = { name: 'HMAC', hash, length: 256 }; + const hkdf = { name: 'HKDF', hash, salt, info: new Uint8Array() }; + for (const [operations, algorithm] of [ + [['digest'], hash], + [['generateKey', 'importKey'], { ...rsa, modulusLength: 2048, hash }], + [['generateKey', 'importKey'], hmac], + [['sign', 'verify'], { name: 'ECDSA', hash }], + [['deriveBits'], hkdf], + [['deriveBits'], { name: 'PBKDF2', hash, salt, iterations: 1 }], + ]) { + for (const operation of operations) { + const length = operation === 'deriveBits' ? 128 : undefined; + assert.strictEqual(SubtleCrypto.supports(operation, algorithm, length), available); + if (!available) + assert.throws(() => normalizeAlgorithm(algorithm, operation), hashError); + } + } + assert.strictEqual(SubtleCrypto.supports('deriveKey', hkdf, hmac), available); + if (!available) + assert.throws(() => normalizeAlgorithm(hmac, 'get key length'), hashError); + } + } +} + +if (!isMainThread) { + parentPort.on('message', async () => { + await check(); + parentPort.postMessage(crypto.getFips()); + }); + parentPort.postMessage('ready'); +} else { + (async () => { + const originalFips = crypto.getFips(); + await check(); + try { + crypto.setFips(0); + } catch (err) { + if (err.code !== 'ERR_CRYPTO_FIPS_FORCED') throw err; + common.printSkipMessage('FIPS mode cannot be disabled'); + return; + } + let worker; + try { + // Start the worker before enabling FIPS: isolate initialization can need + // OpenSSL entropy, which is unavailable without an active FIPS provider. + worker = new Worker(__filename, { workerData: { supportsFips: true } }); + worker.on('error', common.mustNotCall()); + await once(worker, 'message'); + for (const fips of [0, 1, 0]) { + crypto.setFips(fips); + await check(); + const response = once(worker, 'message'); + worker.postMessage('check'); + const [actual] = await response; + assert.strictEqual(actual, fips); + } + } finally { + if (worker !== undefined) await worker.terminate(); + crypto.setFips(originalFips); + } + })().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index 15874102a22..7b98cf13c0d 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -4,10 +4,6 @@ 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; diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index ed0bba774fa..b1198964330 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -390,13 +390,13 @@ function testWrapping(name, keys) { 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'); + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); } } - const wrappingKey = await subtle.generateKey( - { name: 'AES-OCB', length: 128 }, true, ['wrapKey']); + await assert.rejects( + subtle.generateKey({ name: 'AES-OCB', length: 128 }, true, ['wrapKey']), + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); const key = await subtle.generateKey( { name: 'HMAC', hash: 'SHA-256', length: 256 }, true, @@ -405,10 +405,9 @@ function testWrapping(name, keys) { subtle.wrapKey( 'raw', key, - wrappingKey, + key, { name: 'AES-OCB', iv: new Uint8Array(15), tagLength: 128 }), - (err) => err.name === 'OperationError' && - err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }); } await generateWrappingKeys(); diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 56de2929fd1..581ebf8f380 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -113,9 +113,13 @@ if (hasFIPS(3)) { skip( 'encrypt_decrypt/aes_ocb.tentative.https.any.js', 'encrypt_decrypt/chacha20_poly1305.tentative.https.any.js', + 'generateKey/failures_AES-OCB.tentative.https.any.js', 'generateKey/failures_chacha20_poly1305.tentative.https.any.js', + 'generateKey/successes_AES-OCB.tentative.https.any.js', 'generateKey/successes_chacha20_poly1305.tentative.https.any.js', + 'import_export/AES-OCB_importKey.tentative.https.any.js', 'import_export/ChaCha20-Poly1305_importKey.tentative.https.any.js', + 'serialization/aes-ocb.tentative.https.any.js', 'serialization/chacha20-poly1305.tentative.https.any.js'); skipSubtests( @@ -153,6 +157,8 @@ if (hasFIPS(3, 5)) { 'derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js', 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js', 'generateKey/failures_Hybrid-KEM.tentative.https.any.js', + 'generateKey/failures_X25519.https.any.js', + 'generateKey/failures_X448.tentative.https.any.js', 'generateKey/successes_Hybrid-KEM.tentative.https.any.js', 'generateKey/successes_X25519.https.any.js', 'generateKey/successes_X448.tentative.https.any.js', diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 67a67d90fed..af6455f886f 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -832,7 +832,6 @@ export interface CryptoBinding { KEMEncapsulateJob?: InternalCryptoBinding.KEMEncapsulateJobConstructor; KangarooTwelveJob: InternalCryptoBinding.KangarooTwelveJobConstructor; KmacJob: InternalCryptoBinding.KmacJobConstructor; - getPqcKeyTypes(): string[]; NamedKeyPairGenJob: InternalCryptoBinding.NamedKeyPairGenJobConstructor; PBKDF2Job: InternalCryptoBinding.PBKDF2JobConstructor; RandomBytesJob: InternalCryptoBinding.RandomBytesJobConstructor; @@ -988,6 +987,7 @@ export interface CryptoBinding { getHashes(): string[]; getMacs(): string[]; isCryptoKey(key: unknown): boolean; + isKeyAlgorithmAvailable(name: string): boolean; isKeyObject(key: unknown): boolean; isX509Certificate(value: unknown): boolean; getKeyObjectSlots(key: object): InternalCryptoBinding.KeyObjectSlots; From 4d3f5662b02527c9df77f518cf35cf1d14317661 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:12:55 +0200 Subject: [PATCH 03/11] crypto: use internal random buffer bounds Read buffer lengths and element sizes from internal slots in randomFillSync() and randomFill(). Shadowed properties can otherwise skip filling, change the selected range, or fail a native bounds check. Use the intrinsic byte length for the getRandomValues() quota check and keep its delegation to randomFillSync(). Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66237 Reviewed-By: James M Snell Reviewed-By: Aviv Keller --- lib/internal/crypto/random.js | 61 +++++++-- .../test-crypto-randomfill-properties.js | 120 ++++++++++++++++++ ...st-webcrypto-getrandomvalues-properties.js | 31 +++++ 3 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 test/parallel/test-crypto-randomfill-properties.js create mode 100644 test/parallel/test-webcrypto-getrandomvalues-properties.js diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index c1ab718b16f..494578e9075 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -10,6 +10,7 @@ const { BigInt, BigIntPrototypeToString, DataView, + DataViewPrototypeGetByteLength, DataViewPrototypeGetUint8, DateNow, FunctionPrototypeBind, @@ -22,6 +23,10 @@ const { StringPrototypePadStart, TypedArrayPrototypeFill, TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetLength, + TypedArrayPrototypeGetSymbolToStringTag, + Uint8Array, } = primordials; const { @@ -59,6 +64,8 @@ const { const { isArrayBufferView, isAnyArrayBuffer, + isDataView, + isSharedArrayBuffer, isTypedArray, isFloat16Array, isFloat32Array, @@ -78,6 +85,35 @@ const { const kMaxInt32 = 2 ** 31 - 1; const kMaxPossibleLength = MathMin(kMaxLength, kMaxInt32); +function getByteLength(buf) { + if (isArrayBufferView(buf)) { + return isDataView(buf) ? + DataViewPrototypeGetByteLength(buf) : TypedArrayPrototypeGetByteLength(buf); + } + if (isSharedArrayBuffer(buf)) + return TypedArrayPrototypeGetByteLength(new Uint8Array(buf)); + return ArrayBufferPrototypeGetByteLength(buf); +} + +function getElementSize(buf) { + switch (TypedArrayPrototypeGetSymbolToStringTag(buf)) { + case 'Int16Array': + case 'Uint16Array': + case 'Float16Array': + return 2; + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + return 4; + case 'Float64Array': + case 'BigInt64Array': + case 'BigUint64Array': + return 8; + default: + return 1; + } +} + function assertOffset(offset, elementSize, length) { validateNumber(offset, 'offset'); offset *= elementSize; @@ -134,14 +170,15 @@ function randomFillSync(buf, offset = 0, size) { buf); } - const elementSize = buf.BYTES_PER_ELEMENT || 1; + const elementSize = getElementSize(buf); + const byteLength = getByteLength(buf); - offset = assertOffset(offset, elementSize, buf.byteLength); + offset = assertOffset(offset, elementSize, byteLength); if (size === undefined) { - size = buf.byteLength - offset; + size = byteLength - offset; } else { - size = assertSize(size, elementSize, offset, buf.byteLength); + size = assertSize(size, elementSize, offset, byteLength); } if (size === 0) @@ -168,26 +205,27 @@ function randomFill(buf, offset, size, callback) { buf); } - const elementSize = buf.BYTES_PER_ELEMENT || 1; + const elementSize = getElementSize(buf); if (typeof offset === 'function') { callback = offset; offset = 0; // Size is a length here, assertSize() call turns it into a number of bytes - size = buf.length; + size = isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : undefined; } else if (typeof size === 'function') { callback = size; - size = (buf.length ?? buf.byteLength) - offset; + size = (isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : getByteLength(buf)) - offset; } else { validateFunction(callback, 'callback'); } - offset = assertOffset(offset, elementSize, buf.byteLength); + const byteLength = getByteLength(buf); + offset = assertOffset(offset, elementSize, byteLength); if (size === undefined) { - size = buf.byteLength - offset; + size = byteLength - offset; } else { - size = assertSize(size, elementSize, offset, buf.byteLength); + size = assertSize(size, elementSize, offset, byteLength); } if (size === 0) { @@ -337,7 +375,8 @@ function getRandomValues(data) { 'The data argument must be an integer-type TypedArray', 'TypeMismatchError'); } - if (data.byteLength > 65536) { + const byteLength = TypedArrayPrototypeGetByteLength(data); + if (byteLength > 65536) { throw lazyDOMException( 'The requested length exceeds 65,536 bytes', 'QuotaExceededError'); diff --git a/test/parallel/test-crypto-randomfill-properties.js b/test/parallel/test-crypto-randomfill-properties.js new file mode 100644 index 00000000000..07066b5d462 --- /dev/null +++ b/test/parallel/test-crypto-randomfill-properties.js @@ -0,0 +1,120 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { randomFill, randomFillSync } = require('crypto'); + +const typedArrays = [ + Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, + Int32Array, Uint32Array, Float32Array, Float64Array, + BigInt64Array, BigUint64Array, +]; +if (globalThis.Float16Array !== undefined) + typedArrays.push(globalThis.Float16Array); + +const factories = []; +for (const Backing of [ArrayBuffer, SharedArrayBuffer]) { + for (const Type of [...typedArrays, Buffer, DataView]) { + const elementSize = Type.BYTES_PER_ELEMENT || 1; + factories.push({ + name: `${Type.name} on ${Backing.name}`, + elementSize, + make(length = 64) { + const backing = new Backing(length + 64); + const input = Type === Buffer ? Buffer.from(backing, 32, length) : + new Type(backing, 32, length / elementSize); + return { input, backing, bytes: new Uint8Array(backing), start: 32, length }; + }, + }); + } + factories.push({ + name: Backing.name, + elementSize: 1, + make(length = 64) { + const input = new Backing(length); + return { input, backing: input, bytes: new Uint8Array(input), start: 0, length }; + }, + }); +} + +function shadowMetadata(target, mode) { + const values = { + byteLength: mode === 'zero' ? 0 : 4096, + length: mode === 'zero' ? 0 : 4096, + BYTES_PER_ELEMENT: mode === 'zero' ? 0 : 4096, + buffer: new ArrayBuffer(0), + byteOffset: mode === 'zero' ? 0 : 4096, + [Symbol.toStringTag]: 'NotAnArrayBufferView', + }; + for (const key of Reflect.ownKeys(values)) { + Object.defineProperty(target, key, mode === 'getters' ? + { get: common.mustNotCall(`Unexpected ${String(key)} getter`) } : + { value: values[key] }); + } +} + +function checkFilled({ input, bytes, start }, result, offset, size, label) { + assert.strictEqual(result, input, label); + assert(bytes.subarray(0, start + offset).every((byte) => byte === 0), label); + assert(bytes.subarray(start + offset + size).every((byte) => byte === 0), label); + // Every nonempty test range contains at least 32 random bytes. Detect a + // no-op without assuming that any particular generated byte is nonzero. + if (size !== 0) + assert(bytes.subarray(start + offset, start + offset + size).some((byte) => byte !== 0), label); +} + +for (const factory of factories) { + const { elementSize } = factory; + const ranges = [ + { args: [], offset: 0, size: 64 }, + { args: [16 / elementSize], offset: 16, size: 48 }, + { args: [16 / elementSize, 32 / elementSize], offset: 16, size: 32 }, + { args: [16 / elementSize, undefined], offset: 16, size: 48 }, + { args: [64 / elementSize], offset: 64, size: 0 }, + ]; + for (const mode of ['getters', 'zero', 'inflated']) { + for (const { args, offset, size } of ranges) { + for (const async of [false, true]) { + const value = factory.make(); + shadowMetadata(value.input, mode); + if (value.backing !== value.input) + shadowMetadata(value.backing, mode); + const label = `${factory.name}, ${mode}, ${async ? 'async' : 'sync'}, ${args}`; + if (async) { + assert.strictEqual(randomFill(value.input, ...args, common.mustSucceed((result) => { + checkFilled(value, result, offset, size, label); + })), undefined); + } else { + checkFilled(value, randomFillSync(value.input, ...args), offset, size, label); + } + } + } + } + + // Inflated public bounds must not let an invalid range reach the native job. + // Keep the real element width here so each range exceeds the actual buffer. + const units = 64 / elementSize; + for (const args of [[units + 1], [0, units + 1], [units, 1]]) { + const { input } = factory.make(); + Object.defineProperties(input, { + byteLength: { value: 4096 }, + length: { value: 4096 }, + }); + assert.throws(() => randomFillSync(input, ...args), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => randomFill(input, ...args, common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE', + }); + } + + for (const args of [[], [0], [0, 0], [0, undefined]]) { + const value = factory.make(0); + shadowMetadata(value.input, 'inflated'); + checkFilled(value, randomFillSync(value.input, ...args), 0, 0, factory.name); + assert.strictEqual(randomFill(value.input, ...args, common.mustSucceed((result) => { + checkFilled(value, result, 0, 0, factory.name); + })), undefined); + } +} diff --git a/test/parallel/test-webcrypto-getrandomvalues-properties.js b/test/parallel/test-webcrypto-getrandomvalues-properties.js new file mode 100644 index 00000000000..c9aca5c9276 --- /dev/null +++ b/test/parallel/test-webcrypto-getrandomvalues-properties.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const webcrypto = globalThis.crypto; + +for (const Type of [Uint8Array, Uint16Array, Uint32Array, BigUint64Array]) { + const storage = new Uint8Array(128); + const view = new Type(storage.buffer, 32, 32 / Type.BYTES_PER_ELEMENT); + for (const name of ['buffer', 'byteOffset', 'byteLength', 'BYTES_PER_ELEMENT']) { + Object.defineProperty(view, name, { get: common.mustNotCall() }); + Object.defineProperty(storage.buffer, name, { get: common.mustNotCall() }); + } + assert.strictEqual(webcrypto.getRandomValues(view), view); + assert(storage.subarray(32, 64).some((byte) => byte !== 0)); + assert(storage.subarray(0, 32).every((byte) => byte === 0)); + assert(storage.subarray(64).every((byte) => byte === 0)); +} + +const oversized = new Uint8Array(65537); +Object.defineProperty(oversized, 'byteLength', { value: 0 }); +assert.throws(() => webcrypto.getRandomValues(oversized), { + name: 'QuotaExceededError', +}); + +const empty = new Uint8Array(0); +Object.defineProperty(empty, 'byteLength', { value: 65537 }); +assert.strictEqual(webcrypto.getRandomValues(empty), empty); From 2db0ced91e13216ff934d500dd3a801a393734bb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 25 Sep 2026 16:25:32 +0200 Subject: [PATCH 04/11] test: reduce expensive crypto test variants Skip redundant PBKDF2 WPT variants during regular CI. Keep the full matrix when producing a WPT report. Retain the RSA WPT variants because v24 cannot skip whole variants without hanging empty test runs. Use fewer scrypt, PBKDF2, and Argon2 derivations in parallel tests. Reuse RSA key material across OAEP hash and PSS salt tests. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66244 Reviewed-By: James M Snell Reviewed-By: Xuguang Mei --- test/parallel/test-crypto-argon2.js | 18 ++- test/parallel/test-crypto-pbkdf2.js | 24 ++-- test/parallel/test-crypto-scrypt.js | 62 ++++----- .../test-webcrypto-derivebits-argon2.js | 2 +- .../test-webcrypto-encrypt-decrypt.js | 130 ++++++------------ .../test-webcrypto-sign-verify-rsa.js | 64 +++++---- test/wpt/status/WebCryptoAPI.cjs | 10 ++ 7 files changed, 145 insertions(+), 165 deletions(-) diff --git a/test/parallel/test-crypto-argon2.js b/test/parallel/test-crypto-argon2.js index 98367bab547..127c66019f8 100644 --- a/test/parallel/test-crypto-argon2.js +++ b/test/parallel/test-crypto-argon2.js @@ -11,13 +11,15 @@ if (!hasOpenSSL(3, 2)) const assert = require('node:assert'); const crypto = require('node:crypto'); -function runArgon2(algorithm, options) { +function runArgon2(algorithm, options, testAsync = false) { const syncResult = crypto.argon2Sync(algorithm, options); - crypto.argon2(algorithm, options, - common.mustSucceed((asyncResult) => { - assert.deepStrictEqual(asyncResult, syncResult); - })); + if (testAsync) { + crypto.argon2(algorithm, options, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(asyncResult, syncResult); + })); + } return syncResult; } @@ -126,9 +128,11 @@ const bad = [ assert.deepStrictEqual(omitted, explicitEmpty); } -for (const [algorithm, overrides, expected] of good) { +// The RFC vectors exercise both APIs for each Argon2 algorithm. Other vectors +// check distinct options without repeating the derivation asynchronously. +for (const [index, [algorithm, overrides, expected]] of good.entries()) { const parameters = { ...defaults, ...overrides }; - const actual = runArgon2(algorithm, parameters); + const actual = runArgon2(algorithm, parameters, index < 3); assert.strictEqual(actual.toString('hex'), expected); } diff --git a/test/parallel/test-crypto-pbkdf2.js b/test/parallel/test-crypto-pbkdf2.js index 7cd1206f4f0..5083396f733 100644 --- a/test/parallel/test-crypto-pbkdf2.js +++ b/test/parallel/test-crypto-pbkdf2.js @@ -16,14 +16,16 @@ const validSalt = fips4 ? '0123456789abcdef' : 'salt'; const validIterations = fips4 ? 1000 : 8; const validKeyLength = fips4 ? 16 : 8; -function runPBKDF2(password, salt, iterations, keylen, hash) { +function runPBKDF2(password, salt, iterations, keylen, hash, testAsync = true) { const syncResult = crypto.pbkdf2Sync(password, salt, iterations, keylen, hash); - crypto.pbkdf2(password, salt, iterations, keylen, hash, - common.mustSucceed((asyncResult) => { - assert.deepStrictEqual(asyncResult, syncResult); - })); + if (testAsync) { + crypto.pbkdf2(password, salt, iterations, keylen, hash, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(asyncResult, syncResult); + })); + } return syncResult; } @@ -47,20 +49,22 @@ function assertPBKDF2Fails(password, salt, iterations, keylen, hash) { ); } -function testPBKDF2(password, salt, iterations, keylen, expected, encoding) { - const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256'); +function testPBKDF2(password, salt, iterations, keylen, expected, encoding, + testAsync = false) { + const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256', testAsync); assert.strictEqual(actual.toString(encoding || 'latin1'), expected); } // -// Test PBKDF2 with RFC 6070 test vectors (except #4) +// Test PBKDF2 with RFC 6070 test vectors (except #4). Run one known answer +// through the callback API; the rest only need synchronous derivation. // if (fips4) { testPBKDF2(validPassword, validSalt, validIterations, 32, '8514638175a45bc45eb1f22f04ff7d27' + 'f4f8be480498c455ff4b494ce8d1e7d2', - 'hex'); + 'hex', true); for (const args of [ ['short', validSalt, validIterations], @@ -82,7 +86,7 @@ if (fips4) { 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'); + '\x84\x5c\x4c\x8d\x96\x28\x93\xa0', 'latin1', true); testPBKDF2('passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index 0f033e05d6d..e4a4a071558 100644 --- a/test/parallel/test-crypto-scrypt.js +++ b/test/parallel/test-crypto-scrypt.js @@ -43,6 +43,7 @@ const good = [ N: 1024, p: 16, r: 8, + testAsync: true, expected: 'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' + '2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640', @@ -65,32 +66,11 @@ const good = [ cost: 16, parallelization: 1, blockSize: 1, + testAsync: true, expected: '77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442' + 'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906', }, - { - pass: 'password', - salt: 'NaCl', - keylen: 64, - cost: 1024, - parallelization: 16, - blockSize: 8, - expected: - 'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' + - '2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640', - }, - { - pass: 'pleaseletmein', - salt: 'SodiumChloride', - keylen: 64, - cost: 16384, - parallelization: 1, - blockSize: 8, - expected: - '7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2' + - 'd5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887', - }, ]; // Test vectors that contain invalid parameters. @@ -171,9 +151,11 @@ if (isFips) { 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); - })); + if (options.testAsync) { + crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => { + assert.strictEqual(actual.toString('hex'), expected); + })); + } } } @@ -215,12 +197,11 @@ for (const options of toobig) { } 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); - assert.deepStrictEqual(actual.toString('hex'), expected.toString('hex')); - crypto.scrypt('pass', 'salt', 1, common.mustSucceed((actual) => { - assert.deepStrictEqual(actual.toString('hex'), expected.toString('hex')); + const expected = '4cac4540'; + const actual = crypto.scryptSync('pass', 'salt', 4); + assert.strictEqual(actual.toString('hex'), expected); + crypto.scrypt('pass', 'salt', 4, common.mustSucceed((actual) => { + assert.strictEqual(actual.toString('hex'), expected); })); } @@ -241,9 +222,9 @@ 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. if (!isFips) { - crypto.scrypt('', '', 4, { maxmem: 2 ** 52 }, + crypto.scrypt('', '', 4, { N: 16, maxmem: 2 ** 52 }, common.mustSucceed((actual) => { - assert.strictEqual(actual.toString('hex'), 'd72c87d0'); + assert.strictEqual(actual.toString('hex'), 'e2b18837'); })); } @@ -258,9 +239,12 @@ if (!isFips) { function testParameter(name, value) { let accessCount = 0; + // This regression checks getter access, so the derivation can be cheap. + const options = name === 'cost' ? { cost: 16 } : { N: 16 }; // Find out how often the value is accessed. crypto.scryptSync('', '', 1, { + ...options, get [name]() { accessCount++; return value; @@ -270,6 +254,7 @@ if (!isFips) { // Try to crash the process on the last access. assert.throws(() => { crypto.scryptSync('', '', 1, { + ...options, get [name]() { if (--accessCount === 0) return ''; @@ -282,7 +267,7 @@ if (!isFips) { } [ - ['N', 16384], ['cost', 16384], + ['N', 16], ['cost', 16], ['r', 8], ['blockSize', 8], ['p', 1], ['parallelization', 1], ].forEach((arg) => testParameter(...arg)); @@ -292,17 +277,18 @@ if (!isFips) { // IsInt32() assertion. Assert that `-0` produces the same outcome as // `+0` (which differs by OpenSSL build). { + const options = { N: 16 }; let posError; let posResult; try { - posResult = crypto.scryptSync('', '', 0); + posResult = crypto.scryptSync('', '', 0, options); } catch (err) { posError = err; } let negError; let negResult; try { - negResult = crypto.scryptSync('', '', -0); + negResult = crypto.scryptSync('', '', -0, options); } catch (err) { negError = err; } @@ -314,9 +300,9 @@ if (!isFips) { if (isFips) { assert.throws( - () => crypto.scrypt('', '', -0, () => {}), + () => crypto.scrypt('', '', -0, options, () => {}), { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' }); } else { - crypto.scrypt('', '', -0, common.mustCall()); + crypto.scrypt('', '', -0, options, common.mustCall()); } } diff --git a/test/parallel/test-webcrypto-derivebits-argon2.js b/test/parallel/test-webcrypto-derivebits-argon2.js index 129b716c591..a7fbed77f25 100644 --- a/test/parallel/test-webcrypto-derivebits-argon2.js +++ b/test/parallel/test-webcrypto-derivebits-argon2.js @@ -80,7 +80,7 @@ for (const { algorithm, length, password, params, tag } of vectors) { const result = await subtle.deriveBits(parameters, key, length); assert.deepStrictEqual(result, tag); } - { + if (algorithm === 'Argon2id') { const derivedKeyType = { name: 'HMAC', length, hash: 'SHA-256' }; const key = createSecretKey(password) diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index 0f3341db64b..8c851682432 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -13,95 +13,55 @@ const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests // will provide much greater coverage. -// Test Encrypt/Decrypt RSA-OAEP w/ SHA-2 -{ +async function testRSAOaep(publicKey, privateKey) { const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); - - async function test() { - const ec = new TextEncoder(); - const { publicKey, privateKey } = await subtle.generateKey({ - name: 'RSA-OAEP', - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: 'SHA-384', - }, true, ['encrypt', 'decrypt']); - - const ciphertext = await subtle.encrypt({ - name: 'RSA-OAEP', - label: ec.encode('a label') - }, publicKey, buf); - - const plaintext = await subtle.decrypt({ - name: 'RSA-OAEP', - label: ec.encode('a label') - }, privateKey, ciphertext); - - assert.strictEqual( - Buffer.from(plaintext).toString('hex'), - Buffer.from(buf).toString('hex')); - - await assert.rejects(() => subtle.encrypt({ - name: 'RSA-OAEP', - }, privateKey, buf), { - name: 'InvalidAccessError', - message: 'Unable to use this key to encrypt' - }); - - await assert.rejects(() => subtle.decrypt({ - name: 'RSA-OAEP', - }, publicKey, ciphertext), { - name: 'InvalidAccessError', - message: 'Unable to use this key to decrypt' - }); - } - - test().then(common.mustCall()); + const label = new TextEncoder().encode('a label'); + const ciphertext = await subtle.encrypt({ name: 'RSA-OAEP', label }, publicKey, buf); + const plaintext = await subtle.decrypt({ name: 'RSA-OAEP', label }, privateKey, ciphertext); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + + await assert.rejects(() => subtle.encrypt({ + name: 'RSA-OAEP', + }, privateKey, buf), { + name: 'InvalidAccessError', + message: 'Unable to use this key to encrypt' + }); + + await assert.rejects(() => subtle.decrypt({ + name: 'RSA-OAEP', + }, publicKey, ciphertext), { + name: 'InvalidAccessError', + message: 'Unable to use this key to decrypt' + }); } -// Test Encrypt/Decrypt RSA-OAEP w/ SHA-3 -if (!isBoringSSL) { - const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); - - async function test() { - const ec = new TextEncoder(); - const { publicKey, privateKey } = await subtle.generateKey({ - name: 'RSA-OAEP', - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: 'SHA3-384', - }, true, ['encrypt', 'decrypt']); - - const ciphertext = await subtle.encrypt({ - name: 'RSA-OAEP', - label: ec.encode('a label') - }, publicKey, buf); - - const plaintext = await subtle.decrypt({ - name: 'RSA-OAEP', - label: ec.encode('a label') - }, privateKey, ciphertext); - - assert.strictEqual( - Buffer.from(plaintext).toString('hex'), - Buffer.from(buf).toString('hex')); - - await assert.rejects(() => subtle.encrypt({ - name: 'RSA-OAEP', - }, privateKey, buf), { - name: 'InvalidAccessError', - message: 'Unable to use this key to encrypt' - }); - - await assert.rejects(() => subtle.decrypt({ - name: 'RSA-OAEP', - }, publicKey, ciphertext), { - name: 'InvalidAccessError', - message: 'Unable to use this key to decrypt' - }); +(async function() { + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-384', + }, true, ['encrypt', 'decrypt']); + + await testRSAOaep(publicKey, privateKey); + + if (!isBoringSSL) { + // Import the same key material with SHA-3 to avoid another RSA keygen. + const [spki, pkcs8] = await Promise.all([ + subtle.exportKey('spki', publicKey), + subtle.exportKey('pkcs8', privateKey), + ]); + const algorithm = { name: 'RSA-OAEP', hash: 'SHA3-384' }; + const [sha3PublicKey, sha3PrivateKey] = await Promise.all([ + subtle.importKey('spki', spki, algorithm, false, ['encrypt']), + subtle.importKey('pkcs8', pkcs8, algorithm, false, ['decrypt']), + ]); + await testRSAOaep(sha3PublicKey, sha3PrivateKey); } - - test().then(common.mustCall()); -} +})().then(common.mustCall()); // Test Encrypt/Decrypt AES-CTR { diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 8ba0257b177..c64793390dd 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -214,13 +214,11 @@ async function testFipsSignRejected({ { name: 'OperationError' }); } -async function testSaltLength(keyLength, hash, hLen) { - const { publicKey, privateKey } = await subtle.generateKey({ - name: 'RSA-PSS', - modulusLength: keyLength, - publicExponent: new Uint8Array([1, 0, 1]), - hash, - }, false, ['sign', 'verify']); +async function testSaltLength(keyLength, hash, hLen, spki, pkcs8) { + const [publicKey, privateKey] = await Promise.all([ + subtle.importKey('spki', spki, { name: 'RSA-PSS', hash }, false, ['verify']), + subtle.importKey('pkcs8', pkcs8, { name: 'RSA-PSS', hash }, false, ['sign']), + ]); const data = Buffer.from('Hello, world!'); const max = keyLength / 8 - hLen - 2; @@ -249,6 +247,39 @@ async function testSaltLength(keyLength, hash, hLen) { } } +async function testSaltLengths(keyLength) { + // Reuse the same RSA key material across hashes. The salt boundary depends + // on the modulus length and digest size, not on a newly generated modulus. + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: keyLength, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, true, ['sign', 'verify']); + const [spki, pkcs8] = await Promise.all([ + subtle.exportKey('spki', publicKey), + subtle.exportKey('pkcs8', privateKey), + ]); + + const variations = []; + for (const [hash, hLen] of [ + ['SHA-1', 20], + ['SHA-256', 32], + ['SHA-384', 48], + ['SHA-512', 64], + ...(!isBoringSSL ? [ + ['SHA3-256', 32], + ['SHA3-384', 48], + ['SHA3-512', 64], + ] : []), + ]) { + if (rejectsSha1Signing && hash === 'SHA-1') + continue; + variations.push(testSaltLength(keyLength, hash, hLen, spki, pkcs8)); + } + await Promise.all(variations); +} + (async function() { const variations = []; @@ -277,23 +308,8 @@ async function testSaltLength(keyLength, hash, hLen) { })); } - 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], - ...(!isBoringSSL ? [ - ['SHA3-256', 32], - ['SHA3-384', 48], - ['SHA3-512', 64], - ] : []), - ]) { - if (rejectsSha1Signing && hash === 'SHA-1') - continue; - variations.push(testSaltLength(keyLength, hash, hLen)); - } - } + for (const keyLength of fips3 ? [2048] : [1024, 2048]) + variations.push(testSaltLengths(keyLength)); await Promise.all(variations); })().then(common.mustCall()); diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 581ebf8f380..ff58519e2b3 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -194,6 +194,16 @@ if (hasFIPS(3, 5)) { ]); } +if (process.env.WPT_REPORT === undefined) { + // Retain one 100000-iteration deriveKey per PBKDF2 vector; deriveBits still + // checks every password, salt, and hash, and cheaper vectors cover all + // derived key types. + skipSubtests([ + 'derive_bits_keys/pbkdf2.https.any.js', + /^Derived key of type (?!name: AES-CBC length: 256\s+using\b).* using .*, SHA-(?:1|256|384|512), with 100000 iterations$/, + ]); +} + if (hasFIPS(4)) { skipSubtests( [ From dc5c98d0fc3572725fcda7d745d8d74d3e82fb3f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:23:31 +0200 Subject: [PATCH 05/11] crypto: separate conversion from validation Convert algorithm dictionaries on the original receiver, with name read once. Validate normalized parameters in their operation steps after the method-level key checks and generation usage checks. This also makes Argon2 validation use converted parallelism and keeps later dictionary conversion errors ahead of semantic parameter errors. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66237 Backport-PR-URL: https://github.com/nodejs/node/pull/66391 Reviewed-By: Richard Lau Reviewed-By: Xuguang Mei --- lib/internal/crypto/aes.js | 7 +- lib/internal/crypto/argon2.js | 2 + lib/internal/crypto/cfrg.js | 3 + lib/internal/crypto/chacha20_poly1305.js | 2 + lib/internal/crypto/diffiehellman.js | 2 + lib/internal/crypto/ec.js | 3 + lib/internal/crypto/hash.js | 2 + lib/internal/crypto/hkdf.js | 2 + lib/internal/crypto/mac.js | 15 ++-- lib/internal/crypto/ml_dsa.js | 3 + lib/internal/crypto/rsa.js | 2 + lib/internal/crypto/util.js | 15 +++- lib/internal/crypto/webcrypto.js | 10 ++- lib/internal/crypto/webidl.js | 89 ++++++++++++++----- .../test-webcrypto-crypto-job-mode.js | 12 +-- test/parallel/test-webcrypto-fips-refresh.js | 37 ++++++++ .../test-webcrypto-normalization-boundary.js | 79 ++++++++++++++++ test/parallel/test-webcrypto-supports-fips.js | 10 ++- test/parallel/test-webcrypto-webidl.js | 10 ++- 19 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 test/parallel/test-webcrypto-fips-refresh.js create mode 100644 test/parallel/test-webcrypto-normalization-boundary.js diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 3280ddff845..dd95748dcab 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -25,6 +25,7 @@ const { getUsagesMask, jobPromise, getBufferSourceByteLength, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -173,6 +174,7 @@ function asyncAesOcbCipher(mode, key, data, algorithm) { } function aesCipher(mode, key, data, algorithm) { + validateAlgorithm(algorithm, 'encrypt'); switch (algorithm.name) { case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm); case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm); @@ -185,8 +187,9 @@ function aesCipher(mode, key, data, algorithm) { function aesGenerateKey(algorithm, extractable, usages) { const { name, length } = algorithm; - const usagesSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages[name], name)); + const usagesSet = validateKeyUsages(usages, kUsages[name], name); + validateAlgorithm(algorithm, 'generateKey'); + validateUsagesNotEmpty(usagesSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index bf9eaedd85a..844f9dc94a3 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -29,6 +29,7 @@ const { const { getArrayBufferOrView, jobPromise, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -203,6 +204,7 @@ function validateArgon2DeriveBitsLength(length) { function argon2DeriveBits(algorithm, baseKey, length) { validateArgon2DeriveBitsLength(length); + validateAlgorithm(algorithm, 'deriveBits'); const type = { '__proto__': null, diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 026fa925a95..fe37f5c9f2b 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -25,6 +25,7 @@ const { getUsagesMask, jobPromise, toUsagesSet, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -192,6 +193,8 @@ function eddsaSignVerify(key, data, algorithm, signature) { if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + validateAlgorithm(algorithm, mode === kSignJobModeSign ? 'sign' : 'verify'); + return jobPromise(() => new SignJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 45071cded21..32bbcfbf0b7 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -9,6 +9,7 @@ const { const { getUsagesMask, jobPromise, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -36,6 +37,7 @@ function validateKeyLength(length) { } function c20pCipher(mode, key, data, algorithm) { + validateAlgorithm(algorithm, 'encrypt'); return jobPromise(() => new ChaCha20Poly1305CipherJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 16ee3fda7b5..03b62faa082 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -66,6 +66,7 @@ const { numBitsToBytes, toBuf, truncateToBitLength, + validateAlgorithm, kHandle, } = require('internal/crypto/util'); @@ -368,6 +369,7 @@ function diffieHellman(options, callback) { // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. function ecdhDeriveBits(algorithm, baseKey, length) { + validateAlgorithm(algorithm, 'deriveBits'); const { 'public': key } = algorithm; if (getCryptoKeyType(baseKey) !== 'private') { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 7f277addd87..bb857be74c8 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -31,6 +31,7 @@ const { getUsagesMask, jobPromise, normalizeHashName, + validateAlgorithm, kNamedCurveAliases, toUsagesSet, } = require('internal/crypto/util'); @@ -72,6 +73,7 @@ function ecGenerateKey(algorithm, extractable, usages) { const { name, namedCurve } = algorithm; const allowedUsages = kUsages[name]; const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); + validateAlgorithm(algorithm, 'generateKey'); const keyAlgorithm = { name, namedCurve }; const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); @@ -139,6 +141,7 @@ function ecImportKey( usages, ) { const { name, namedCurve } = algorithm; + validateAlgorithm(algorithm, 'importKey'); let handle; const allowedUsages = kUsages[name]; diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 62bec966060..20a5b8db862 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -28,6 +28,7 @@ const { normalizeHashName, numBitsToBytes, truncateToBitLength, + validateAlgorithm, validateMaxBufferLength, kHandle, getCachedHashId, @@ -259,6 +260,7 @@ Hmac.prototype._transform = Hash.prototype._transform; function asyncDigest(algorithm, data) { validateMaxBufferLength(data, 'data'); + validateAlgorithm(algorithm, 'digest'); switch (algorithm.name) { case 'SHA-1': diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 8673f9b4681..bf8a296bc4b 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -29,6 +29,7 @@ const { normalizeHashName, toBuf, validateByteSource, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -182,6 +183,7 @@ function validateHkdfDeriveBitsLength(length, hash) { function hkdfDeriveBits(algorithm, baseKey, length) { const { hash, salt, info } = algorithm; validateHkdfDeriveBitsLength(length, hash); + validateAlgorithm(algorithm, 'deriveBits'); if (length === 0) return PromiseResolve(new ArrayBuffer(0)); diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 3297297abd5..742e1ebe590 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -21,6 +21,7 @@ const { numBitsToBytes, truncateToBitLength, validateKmacKeyLength, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -71,11 +72,12 @@ function hmacGenerateKey(algorithm, extractable, usages) { const { hash, name, - length = getBlockSize(hash.name), } = algorithm; - const usageSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages, name)); + const usageSet = validateKeyUsages(usages, kUsages, name); + validateAlgorithm(algorithm, 'generateKey'); + const { length = getBlockSize(hash.name) } = algorithm; + validateUsagesNotEmpty(usageSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -95,8 +97,9 @@ function kmacGenerateKey(algorithm, extractable, usages) { }[name], } = algorithm; - const usageSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages, name)); + const usageSet = validateKeyUsages(usages, kUsages, name); + validateAlgorithm(algorithm, 'generateKey'); + validateUsagesNotEmpty(usageSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -114,6 +117,7 @@ function macImportKey( usages, ) { const isHmac = algorithm.name === 'HMAC'; + validateAlgorithm(algorithm, 'importKey'); const usagesSet = validateKeyUsages( usages, kUsages, algorithm.name); let handle; @@ -181,6 +185,7 @@ function hmacSignVerify(key, data, algorithm, signature) { } function kmacSignVerify(key, data, algorithm, signature) { + validateAlgorithm(algorithm, 'sign'); const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new KmacJob( kCryptoJobWebCrypto, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 3a566b369d8..621a377d47d 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -28,6 +28,7 @@ const { getUsagesMask, jobPromise, toUsagesSet, + validateAlgorithm, getBufferSourceByteLength, } = require('internal/crypto/util'); @@ -211,6 +212,8 @@ function mlDsaSignVerify(key, data, algorithm, signature) { if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + validateAlgorithm(algorithm, mode === kSignJobModeSign ? 'sign' : 'verify'); + return jobPromise(() => new SignJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index d8e589f9e03..8fa3ecdff1b 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -34,6 +34,7 @@ const { jobPromise, jobPromiseThen, normalizeHashName, + validateAlgorithm, validateMaxBufferLength, toUsagesSet, } = require('internal/crypto/util'); @@ -113,6 +114,7 @@ function rsaKeyGenerate( const allowedUsages = kUsages[name]; const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); + validateAlgorithm(algorithm, 'generateKey'); const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent); let firstNonzeroByte = 0; while (publicExponent[firstNonzeroByte] === 0) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 38fa0a1c5d2..51953b7e523 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -813,8 +813,8 @@ function normalizeAlgorithm(algorithm, op) { return { name: algName }; // 6. - const normalizedAlgorithm = webidl.converters[desiredType]( - { __proto__: algorithm, name: algName }, + const normalizedAlgorithm = webidl.algorithmConverters[desiredType]( + algorithm, kNormalizeAlgorithmOpts, ); // 7. @@ -850,6 +850,16 @@ function normalizeAlgorithm(algorithm, op) { return normalizedAlgorithm; } +function validateAlgorithm(algorithm, op) { + // HMAC's get key length operation handles zero with a TypeError, while + // HmacImportParams otherwise uses the import operation's DataError. + if (op === 'get key length' && algorithm.name === 'HMAC') + return; + webidl ??= require('internal/crypto/webidl'); + const desiredType = getSupportedAlgorithms().algorithms[op]?.[algorithm.name]; + webidl.validators[desiredType]?.(algorithm); +} + function getDataViewOrTypedArrayBuffer(V) { return isDataView(V) ? DataViewPrototypeGetBuffer(V) : TypedArrayPrototypeGetBuffer(V); @@ -1275,6 +1285,7 @@ module.exports = { hasAnyNotIn, validateByteSource, validateKeyOps, + validateAlgorithm, jobPromise, jobPromiseThen, cleanupWebCryptoResult, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 4d9d3c4a557..d05d37ebd8b 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -41,6 +41,7 @@ const { codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -70,6 +71,7 @@ const { normalizeHashName, numBitsToBytes, prepareWebCryptoResult, + validateAlgorithm, validateMaxBufferLength, getOptionalByteLength, } = require('internal/crypto/util'); @@ -318,7 +320,9 @@ function deriveBitsImpl(algorithm, baseKey, length = null) { } } -function getKeyLength({ name, length, hash }) { +function getKeyLength(algorithm) { + validateAlgorithm(algorithm, 'get key length'); + const { name, length, hash } = algorithm; switch (name) { case 'AES-CTR': case 'AES-CBC': @@ -330,6 +334,8 @@ function getKeyLength({ name, length, hash }) { return length; case 'HMAC': + if (length === 0) + throw new ERR_INVALID_ARG_VALUE('algorithm.length', length, 'must not be zero'); if (length === undefined) { return getBlockSize(hash?.name); } @@ -1877,6 +1883,7 @@ class SubtleCrypto { normalizeAlgorithm(algorithm, 'get shared key length'); sharedKeyLength = getSharedKeyLength(normalizedAlgorithm); normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey'); + validateAlgorithm(normalizedAdditionalAlgorithm, 'importKey'); } catch { return false; } @@ -1905,6 +1912,7 @@ function check(op, alg, length) { let normalizedAlgorithm; try { normalizedAlgorithm = normalizeAlgorithm(alg, op); + validateAlgorithm(normalizedAlgorithm, op); } catch { if (op === 'wrapKey') { return check('encrypt', alg); diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 913a9cc157c..f8bc4818f75 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -2,9 +2,12 @@ const { ArrayPrototypeIncludes, + ArrayPrototypePush, + ArrayPrototypeToSorted, MathPow, NumberParseInt, ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeToLowerCase, @@ -134,6 +137,41 @@ const dictAlgorithm = [ converters.Algorithm = createDictionaryConverter( 'Algorithm', dictAlgorithm); +const validators = { __proto__: null }; +const algorithmConverters = { __proto__: null }; + +// Algorithm.name was converted when selecting the registered dictionary. +// Convert the remaining members on the original object, and keep operation +// validation separate from Web IDL conversion. +function createAlgorithmDictionaryConverter(name, dictionaries) { + const members = []; + const fullMembers = [dictAlgorithm]; + const checks = []; + for (let i = 1; i < dictionaries.length; i++) { + const dictionary = []; + const sorted = ArrayPrototypeToSorted(dictionaries[i], (a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + for (let j = 0; j < sorted.length; j++) { + const member = sorted[j]; + if (ObjectPrototypeHasOwnProperty(member, 'validator')) { + ArrayPrototypePush(checks, { key: member.key, validator: member.validator }); + } + ArrayPrototypePush(dictionary, { ...member, validator: undefined }); + } + ArrayPrototypePush(members, dictionary); + ArrayPrototypePush(fullMembers, dictionary); + } + validators[name] = (algorithm) => { + for (let i = 0; i < checks.length; i++) { + const check = checks[i]; + const value = algorithm[check.key]; + if (value !== undefined) + check.validator(value, algorithm); + } + }; + algorithmConverters[name] = createDictionaryConverter(name, members); + return createDictionaryConverter(name, fullMembers); +} + // TODO(panva): Reject resizable backing stores in a semver-major with: // converters.BigInteger = webidl.Uint8Array; converters.BigInteger = (V, opts = kEmptyObject) => { @@ -196,13 +234,13 @@ const dictRsaKeyGenParams = [ }, ]; -converters.RsaKeyGenParams = createDictionaryConverter( +converters.RsaKeyGenParams = createAlgorithmDictionaryConverter( 'RsaKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, ]); -converters.RsaHashedKeyGenParams = createDictionaryConverter( +converters.RsaHashedKeyGenParams = createAlgorithmDictionaryConverter( 'RsaHashedKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, @@ -215,7 +253,7 @@ converters.RsaHashedKeyGenParams = createDictionaryConverter( ], ]); -converters.RsaHashedImportParams = createDictionaryConverter( +converters.RsaHashedImportParams = createAlgorithmDictionaryConverter( 'RsaHashedImportParams', [ dictAlgorithm, [ @@ -229,7 +267,7 @@ converters.RsaHashedImportParams = createDictionaryConverter( converters.NamedCurve = converters.DOMString; -converters.EcKeyImportParams = createDictionaryConverter( +converters.EcKeyImportParams = createAlgorithmDictionaryConverter( 'EcKeyImportParams', [ dictAlgorithm, [ @@ -242,7 +280,7 @@ converters.EcKeyImportParams = createDictionaryConverter( ], ]); -converters.EcKeyGenParams = createDictionaryConverter( +converters.EcKeyGenParams = createAlgorithmDictionaryConverter( 'EcKeyGenParams', [ dictAlgorithm, [ @@ -255,7 +293,7 @@ converters.EcKeyGenParams = createDictionaryConverter( ], ]); -converters.AesKeyGenParams = createDictionaryConverter( +converters.AesKeyGenParams = createAlgorithmDictionaryConverter( 'AesKeyGenParams', [ dictAlgorithm, [ @@ -319,7 +357,7 @@ function validateCShakeCustomization(V) { validateMaxBufferLength(V, 'CShakeParams.customization', 512); } -converters.RsaPssParams = createDictionaryConverter( +converters.RsaPssParams = createAlgorithmDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, [ @@ -332,7 +370,7 @@ converters.RsaPssParams = createDictionaryConverter( ], ]); -converters.RsaOaepParams = createDictionaryConverter( +converters.RsaOaepParams = createAlgorithmDictionaryConverter( 'RsaOaepParams', [ dictAlgorithm, [ @@ -343,7 +381,7 @@ converters.RsaOaepParams = createDictionaryConverter( ], ]); -converters.EcdsaParams = createDictionaryConverter( +converters.EcdsaParams = createAlgorithmDictionaryConverter( 'EcdsaParams', [ dictAlgorithm, [ @@ -368,7 +406,7 @@ const kHmacDictionaries = [ ]; for (let i = 0; i < kHmacDictionaries.length; i++) { const { 0: name, 1: zeroError } = kHmacDictionaries[i]; - converters[name] = createDictionaryConverter( + converters[name] = createAlgorithmDictionaryConverter( name, [ dictAlgorithm, [ @@ -431,7 +469,7 @@ converters.JsonWebKey = createDictionaryConverter( simpleDomStringKey('priv'), ]); -converters.HkdfParams = createDictionaryConverter( +converters.HkdfParams = createAlgorithmDictionaryConverter( 'HkdfParams', [ dictAlgorithm, [ @@ -454,7 +492,7 @@ converters.HkdfParams = createDictionaryConverter( ], ]); -converters.CShakeParams = createDictionaryConverter( +converters.CShakeParams = createAlgorithmDictionaryConverter( 'CShakeParams', [ dictAlgorithm, [ @@ -478,7 +516,7 @@ converters.CShakeParams = createDictionaryConverter( ], ]); -converters.Pbkdf2Params = createDictionaryConverter( +converters.Pbkdf2Params = createAlgorithmDictionaryConverter( 'Pbkdf2Params', [ dictAlgorithm, [ @@ -501,7 +539,7 @@ converters.Pbkdf2Params = createDictionaryConverter( ], ]); -converters.AesDerivedKeyParams = createDictionaryConverter( +converters.AesDerivedKeyParams = createAlgorithmDictionaryConverter( 'AesDerivedKeyParams', [ dictAlgorithm, [ @@ -515,7 +553,7 @@ converters.AesDerivedKeyParams = createDictionaryConverter( ], ]); -converters.AesCbcParams = createDictionaryConverter( +converters.AesCbcParams = createAlgorithmDictionaryConverter( 'AesCbcParams', [ dictAlgorithm, [ @@ -528,7 +566,7 @@ converters.AesCbcParams = createDictionaryConverter( ], ]); -converters.AeadParams = createDictionaryConverter( +converters.AeadParams = createAlgorithmDictionaryConverter( 'AeadParams', [ dictAlgorithm, [ @@ -592,7 +630,7 @@ converters.AeadParams = createDictionaryConverter( ], ]); -converters.AesCtrParams = createDictionaryConverter( +converters.AesCtrParams = createAlgorithmDictionaryConverter( 'AesCtrParams', [ dictAlgorithm, [ @@ -620,7 +658,7 @@ converters.AesCtrParams = createDictionaryConverter( converters.CryptoKey = createInterfaceConverter( 'CryptoKey', isCryptoKey); -converters.EcdhKeyDeriveParams = createDictionaryConverter( +converters.EcdhKeyDeriveParams = createAlgorithmDictionaryConverter( 'EcdhKeyDeriveParams', [ dictAlgorithm, [ @@ -642,7 +680,7 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter( ], ]); -converters.ContextParams = createDictionaryConverter( +converters.ContextParams = createAlgorithmDictionaryConverter( 'ContextParams', [ dictAlgorithm, [ @@ -676,7 +714,7 @@ converters.ContextParams = createDictionaryConverter( ], ]); -converters.Argon2Params = createDictionaryConverter( +converters.Argon2Params = createAlgorithmDictionaryConverter( 'Argon2Params', [ dictAlgorithm, [ @@ -753,7 +791,7 @@ converters.Argon2Params = createDictionaryConverter( const kKmacDictionaries = ['KmacKeyGenParams', 'KmacImportParams']; for (let i = 0; i < kKmacDictionaries.length; i++) { const name = kKmacDictionaries[i]; - converters[name] = createDictionaryConverter( + converters[name] = createAlgorithmDictionaryConverter( name, [ dictAlgorithm, [ @@ -767,7 +805,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) { ]); } -converters.KmacParams = createDictionaryConverter( +converters.KmacParams = createAlgorithmDictionaryConverter( 'KmacParams', [ dictAlgorithm, [ @@ -790,7 +828,7 @@ converters.KmacParams = createDictionaryConverter( ], ]); -converters.KangarooTwelveParams = createDictionaryConverter( +converters.KangarooTwelveParams = createAlgorithmDictionaryConverter( 'KangarooTwelveParams', [ dictAlgorithm, [ @@ -812,7 +850,7 @@ converters.KangarooTwelveParams = createDictionaryConverter( ], ]); -converters.TurboShakeParams = createDictionaryConverter( +converters.TurboShakeParams = createAlgorithmDictionaryConverter( 'TurboShakeParams', [ dictAlgorithm, [ @@ -842,6 +880,9 @@ converters.TurboShakeParams = createDictionaryConverter( ]); module.exports = { + // Spread into fast-property objects before detaching their prototypes. + algorithmConverters: ObjectSetPrototypeOf({ ...algorithmConverters }, null), converters, requiredArguments, + validators: ObjectSetPrototypeOf({ ...validators }, null), }; diff --git a/test/parallel/test-webcrypto-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js index 5f5f1761a4c..c050ad5c26b 100644 --- a/test/parallel/test-webcrypto-crypto-job-mode.js +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -15,10 +15,8 @@ const { } = require('internal/crypto/keys'); const { getUsagesMask, + jobPromise, } = require('internal/crypto/util'); -const { - aesCipher, -} = require('internal/crypto/aes'); const { AESCipherJob, @@ -135,11 +133,13 @@ async function withObjectPrototypeSetters(names, fn) { Buffer.alloc(15)), /Invalid initialization vector/); - const promise = aesCipher( + const promise = jobPromise(() => new AESCipherJob( + kCryptoJobWebCrypto, kWebCryptoCipherEncrypt, - key, + getCryptoKeyHandle(key), Buffer.alloc(16), - { name: 'AES-CBC', iv: Buffer.alloc(15) }); + kKeyVariantAES_CBC_128, + Buffer.alloc(15))); assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); await assert.rejects(promise, (err) => { diff --git a/test/parallel/test-webcrypto-fips-refresh.js b/test/parallel/test-webcrypto-fips-refresh.js new file mode 100644 index 00000000000..71459099356 --- /dev/null +++ b/test/parallel/test-webcrypto-fips-refresh.js @@ -0,0 +1,37 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { getFips, setFips } = require('crypto'); +const { internalBinding } = require('internal/test/binding'); +const { getOptionValue } = require('internal/options'); +if (!internalBinding('crypto').testFipsCrypto()) + common.skip('requires an active FIPS provider'); +if (getOptionValue('--force-fips')) + common.skip('FIPS mode cannot be changed when forced'); + +const initial = getFips(); +try { + for (const fips of [false, true, false]) { + setFips(fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'TurboSHAKE128', outputLength: 128, + }), !fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'KT128', outputLength: 128, + }), !fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array(1), + }), !fips); + assert.strictEqual(SubtleCrypto.supports('generateKey', { + name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + }), !fips); + } +} finally { + setFips(initial); +} diff --git a/test/parallel/test-webcrypto-normalization-boundary.js b/test/parallel/test-webcrypto-normalization-boundary.js new file mode 100644 index 00000000000..20102c32e6a --- /dev/null +++ b/test/parallel/test-webcrypto-normalization-boundary.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const data = new Uint8Array(16); + const key = await subtle.importKey('raw', data, 'AES-CBC', false, ['encrypt']); + class Parameters { + #iv = new Uint8Array(16); + get name() { return 'AES-CBC'; } + get iv() { return this.#iv; } + } + await subtle.encrypt(new Parameters(), key, data); + + const reads = []; + const algorithm = new Proxy({ name: 'AES-CBC', iv: data }, { + get: common.mustCall((target, member, receiver) => { + assert.strictEqual(receiver, algorithm); + reads.push(member); + return Reflect.get(target, member, receiver); + }, 2), + }); + await subtle.encrypt(algorithm, key, data); + assert.deepStrictEqual(reads, ['name', 'iv']); + + const decryptKey = await subtle.importKey('raw', data, 'AES-CBC', false, ['decrypt']); + await assert.rejects(subtle.encrypt({ name: 'AES-CBC', iv: new Uint8Array(8) }, + decryptKey, data), { name: 'InvalidAccessError' }); + await assert.rejects(subtle.generateKey({ name: 'AES-CBC', length: 100 }, false, ['sign']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'HMAC', hash: 'SHA-256', length: 0 }, false, ['encrypt']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'AES-CBC', length: 64 }, false, []), + { name: 'OperationError' }); + await assert.rejects(subtle.generateKey({ name: 'HMAC', hash: 'SHA-256', length: 0 }, false, []), + { name: 'OperationError' }); + if (SubtleCrypto.supports('digest', 'SHA3-256')) { + const hmac = { name: 'HMAC', hash: 'SHA3-256' }; + await assert.rejects(subtle.generateKey(hmac, false, ['encrypt']), { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey(hmac, false, []), { name: 'NotSupportedError' }); + } + await assert.rejects(subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-999' }, false, ['encrypt']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-999' }, false, []), + { name: 'NotSupportedError' }); + await assert.rejects(subtle.generateKey({ + name: 'RSA-PSS', modulusLength: 0, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', + }, false, []), { name: 'OperationError' }); + await assert.rejects(subtle.encrypt({ name: 'AES-CTR', counter: new Uint8Array(8), length: NaN }, + key, data), TypeError); + assert.strictEqual(SubtleCrypto.supports('encrypt', { name: 'AES-CBC', iv: new Uint8Array(8) }), false); + + const hkdf = { name: 'HKDF', hash: 'SHA-256', salt: data, info: data }; + const base = await subtle.importKey('raw', data, 'HKDF', false, ['deriveKey']); + const noDeriveKey = await subtle.importKey('raw', data, 'HKDF', false, ['deriveBits']); + const hmac = { name: 'HMAC', hash: 'SHA-256', length: 0 }; + await assert.rejects(subtle.deriveKey(hkdf, base, hmac, false, ['sign']), TypeError); + await assert.rejects(subtle.deriveKey(hkdf, noDeriveKey, hmac, false, ['sign']), + { name: 'InvalidAccessError' }); + await assert.rejects(subtle.deriveKey(hkdf, noDeriveKey, { name: 'AES-GCM', length: 100 }, false, ['encrypt']), + { name: 'InvalidAccessError' }); + + if (SubtleCrypto.supports('importKey', 'Argon2id')) { + const base = await subtle.importKey('raw-secret', data, 'Argon2id', false, ['deriveBits']); + const parameters = { name: 'Argon2id', nonce: data, passes: 1, memory: 8, parallelism: 1 }; + const expected = await subtle.deriveBits(parameters, base, 256); + let conversions = 0; + const parallelism = { valueOf() { conversions++; return 1.5; } }; + assert.deepStrictEqual(await subtle.deriveBits({ ...parameters, parallelism }, base, 256), expected); + assert.strictEqual(conversions, 1); + assert.strictEqual(SubtleCrypto.supports('deriveBits', { ...parameters, parallelism: 1.5 }, 256), true); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-supports-fips.js b/test/parallel/test-webcrypto-supports-fips.js index ae5d1d4b32d..e08551c65f3 100644 --- a/test/parallel/test-webcrypto-supports-fips.js +++ b/test/parallel/test-webcrypto-supports-fips.js @@ -50,21 +50,23 @@ async function check() { } } } + const hashes = crypto.getHashes(); + const hashError = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; const rsa = { name: 'RSA-PSS', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }; assert.strictEqual(SubtleCrypto.supports('generateKey', rsa), !fips); if (fips) { - assert.throws(() => normalizeAlgorithm(rsa, 'generateKey'), { + // Hash normalization precedes the RSA operation's modulus validation. + const error = hashes.includes('sha256') ? { name: 'OperationError', message: 'algorithm.modulusLength must be at least 2048', - }); + } : hashError; + await assert.rejects(subtle.generateKey(rsa, true, ['sign']), error); } else { assert.strictEqual(normalizeAlgorithm(rsa, 'generateKey').modulusLength, 1024); } - const hashes = crypto.getHashes(); const salt = new Uint8Array(16); - const hashError = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; for (const [name, alias] of [ ['SHA-1', 'sha1'], ['SHA-256', 'sha256'], ['SHA-384', 'sha384'], ['SHA-512', 'sha512'], ['SHA3-256', 'sha3-256'], ['SHA3-384', 'sha3-384'], ['SHA3-512', 'sha3-512'], diff --git a/test/parallel/test-webcrypto-webidl.js b/test/parallel/test-webcrypto-webidl.js index d91099aed2b..372d2c0b51b 100644 --- a/test/parallel/test-webcrypto-webidl.js +++ b/test/parallel/test-webcrypto-webidl.js @@ -649,7 +649,9 @@ function assertJsonWebKey(actual, expected) { }); } - assert.throws(() => converters.Argon2Params({ ...good, passes: 0 }, opts), { + const zeroPasses = converters.Argon2Params({ ...good, passes: 0 }, opts); + assert.strictEqual(zeroPasses.passes, 0); + assert.throws(() => webidl.validators.Argon2Params(zeroPasses), { name: 'OperationError', message: 'passes must be > 0', }); @@ -734,11 +736,13 @@ function assertJsonWebKey(actual, expected) { }; assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good); - assert.throws(() => converters.Argon2Params({ + const excessiveParallelism = converters.Argon2Params({ ...good, parallelism: maxParallelism + 1, memory: 8 * (maxParallelism + 1), - }, opts), { + }, opts); + assert.strictEqual(excessiveParallelism.parallelism, maxParallelism + 1); + assert.throws(() => webidl.validators.Argon2Params(excessiveParallelism), { name: 'OperationError', message: 'parallelism must be > 0 and <= 16777215', }); From dfec95023d0dbde5f760bf97a5778fe1e9c87295 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 23 Sep 2026 13:34:11 +0200 Subject: [PATCH 06/11] crypto: use backend cSHAKE and KMAC Require cSHAKE and KMAC output lengths and KMAC key lengths to be multiples of 8 bits. KMAC keys must be at least 32 bits. Share these restrictions between operations and supports. Use OpenSSL's KMAC provider for all supported inputs and its cSHAKE implementation for non-empty function names or customization strings. Keep using SHAKE when both cSHAKE parameters are empty. Remove the custom Keccak framing, partial-bit handling, and short-key fallback. Document the OpenSSL 4.0 requirement for non-empty cSHAKE parameters and reject customization strings containing null bytes. Keep the documented 512-byte customization limit and let backend failures reach callers. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66237 Backport-PR-URL: https://github.com/nodejs/node/pull/66391 Reviewed-By: Richard Lau Reviewed-By: Xuguang Mei --- doc/api/webcrypto.md | 27 +- lib/internal/crypto/hash.js | 28 +- lib/internal/crypto/mac.js | 1 - lib/internal/crypto/util.js | 2 +- lib/internal/crypto/webcrypto.js | 12 +- lib/internal/crypto/webidl.js | 25 +- src/crypto/crypto_hash.cc | 352 ------------------ src/crypto/crypto_hash.h | 67 ---- src/crypto/crypto_kmac.cc | 97 +---- src/crypto/crypto_kmac.h | 3 +- test/fixtures/crypto/kmac.js | 55 --- .../webcrypto/supports-modern-algorithms.mjs | 48 ++- .../test-crypto-key-objects-to-crypto-key.js | 15 +- .../test-webcrypto-aes-kw-short-input.js | 16 +- test/parallel/test-webcrypto-derivekey.js | 18 +- test/parallel/test-webcrypto-digest.js | 100 +++-- test/parallel/test-webcrypto-export-import.js | 82 ++-- .../test-webcrypto-fips-exceptions.mjs | 72 ++-- test/parallel/test-webcrypto-fips-refresh.js | 6 +- .../test-webcrypto-keccak-byte-alignment.js | 90 +++++ test/parallel/test-webcrypto-keygen-kmac.js | 18 +- .../test-webcrypto-prototype-pollution.mjs | 9 +- .../test-webcrypto-sign-verify-kmac.js | 126 ++----- test/parallel/test-webcrypto-wrap-unwrap.js | 38 +- typings/internalBinding/crypto.d.ts | 13 - 25 files changed, 367 insertions(+), 953 deletions(-) create mode 100644 test/parallel/test-webcrypto-keccak-byte-alignment.js diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 458e5fedaab..bc4a43dec98 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -1829,6 +1829,9 @@ changes: description: Renamed `cShakeParams.length` to `cShakeParams.outputLength`. --> +When both `functionName` and `customization` are empty or `undefined`, cSHAKE is +equivalent to plain SHAKE. + #### `cShakeParams.name` -* Type: {number} represents the requested output length in bits. +* Type: {number} represents the requested output length in bits. Must be a + multiple of 8. #### `cShakeParams.functionName` @@ -1858,9 +1862,10 @@ changes: * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} The `functionName` member represents the NIST function-name byte string used to -domain-separate functions built on top of cSHAKE. Accepted values are: +domain-separate functions built on top of cSHAKE. Non-empty values require +OpenSSL 4.0 or later. Accepted values are: -* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE +* empty or `undefined` * the ASCII byte sequence `'KMAC'` * the ASCII byte sequence `'TupleHash'` * the ASCII byte sequence `'ParallelHash'` @@ -1877,11 +1882,11 @@ changes: * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The `customization` member represents the customization data. Accepted -values are: +The `customization` member represents the customization data. Non-empty values +require OpenSSL 4.0 or later. Accepted values are: -* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE -* up to 512 bytes of arbitrary data +* empty or `undefined` +* up to 512 bytes of data without null bytes ### Class: `EcdhKeyDeriveParams` @@ -2331,7 +2336,7 @@ added: v24.8.0 * Type: {number} The optional number of bits in the KMAC key. This is optional and should -be omitted for most cases. +be omitted for most cases. The key length must be at least 32 and a multiple of 8. #### `kmacImportParams.name` @@ -2381,7 +2386,8 @@ added: v24.8.0 The number of bits to generate for the KMAC key. If omitted, the length will be determined by the KMAC algorithm used. -This is optional and should be omitted for most cases. +This is optional and should be omitted for most cases. Must be at least 32 and a +multiple of 8. #### `kmacKeyGenParams.name` @@ -2415,7 +2421,8 @@ added: v24.8.0 added: v24.15.0 --> -* Type: {number} represents the requested output length in bits. +* Type: {number} represents the requested output length in bits. Must be a + multiple of 8. #### `kmacParams.customization` diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 20a5b8db862..617fafe091d 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -6,12 +6,10 @@ const { StringPrototypeReplace, StringPrototypeToLowerCase, Symbol, - TypedArrayPrototypeGetBuffer, TypedArrayPrototypeIncludes, } = primordials; const { - CShakeJob, Hash: _Hash, HashJob, Hmac: _Hmac, @@ -24,10 +22,7 @@ const { const { getStringOption, jobPromise, - jobPromiseThen, normalizeHashName, - numBitsToBytes, - truncateToBitLength, validateAlgorithm, validateMaxBufferLength, kHandle, @@ -43,7 +38,6 @@ const { } = require('internal/crypto/keys'); const { - lazyDOMException, normalizeEncoding, encodingsMap, isPendingDeprecation, @@ -286,30 +280,20 @@ function asyncDigest(algorithm, data) { const outputLength = algorithm.outputLength; if (getOptionalByteLength(algorithm.functionName) || getOptionalByteLength(algorithm.customization)) { - if (CShakeJob === undefined) { - throw lazyDOMException( - 'Non-empty CShakeParams functionName or customization is not supported', - 'NotSupportedError'); - } - - return jobPromise(() => new CShakeJob( + return jobPromise(() => new HashJob( kCryptoJobWebCrypto, - algorithm.name, + StringPrototypeToLowerCase(algorithm.name), data, + outputLength, algorithm.functionName, - algorithm.customization, - outputLength)); + algorithm.customization)); } - const bits = jobPromise(() => new HashJob( + return jobPromise(() => new HashJob( kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, - numBitsToBytes(outputLength) * 8)); - if (outputLength % 8 === 0) - return bits; - return jobPromiseThen(bits, (bits) => - TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits))); + outputLength)); } case 'TurboSHAKE128': // Fall through diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 742e1ebe590..ed2be2b7fdb 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -193,7 +193,6 @@ function kmacSignVerify(key, data, algorithm, signature) { getCryptoKeyHandle(key), algorithm.name, algorithm.customization, - getCryptoKeyAlgorithm(key).length, algorithm.outputLength, data, signature)); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 51953b7e523..3633bce9cb1 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -722,7 +722,7 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } function validateKmacKeyLength(length) { - if ((length < 32 || length % 8) && isFips()) + if (length < 32 || length % 8 !== 0) throw lazyDOMException('Invalid key length', 'NotSupportedError'); } diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index d05d37ebd8b..3d3b2cf7e94 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -24,7 +24,6 @@ const { } = primordials; const { - CShakeJob, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, @@ -73,7 +72,6 @@ const { prepareWebCryptoResult, validateAlgorithm, validateMaxBufferLength, - getOptionalByteLength, } = require('internal/crypto/util'); const { @@ -1926,15 +1924,7 @@ function check(op, alg, length) { } switch (op) { - case 'digest': { - if ((normalizedAlgorithm.name === 'cSHAKE128' || - normalizedAlgorithm.name === 'cSHAKE256') && - (getOptionalByteLength(normalizedAlgorithm.functionName) || - getOptionalByteLength(normalizedAlgorithm.customization))) { - return CShakeJob !== undefined; - } - return true; - } + case 'digest': case 'decapsulate': case 'decrypt': case 'encapsulate': diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index f8bc4818f75..64fb0a8a226 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -11,15 +11,13 @@ const { StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeToLowerCase, + TypedArrayPrototypeIncludes, } = primordials; const { lazyDOMException, kEmptyObject, } = require('internal/util'); -const { - isUint32, -} = require('internal/validators'); const { getCryptoKeyAlgorithm, getCryptoKeyType, @@ -30,9 +28,9 @@ const { validateMaxBufferLength, getBufferSourceByteLength, getBufferSourceBytes, + getHashes, isFips, kNamedCurveAliases, - numBitsToBytes, validateKmacKeyLength, } = require('internal/crypto/util'); const { @@ -317,20 +315,20 @@ function validateZeroLength(parameterName) { } function validateCShakeOutputLength(V) { - if (!isUint32(numBitsToBytes(V) * 8)) { + if (V % 8 !== 0) { throw lazyDOMException( 'Invalid CShakeParams outputLength', - 'OperationError'); + 'NotSupportedError'); } } const kCShakeFunctionNames = ['KMAC', 'TupleHash', 'ParallelHash']; -function validateCShakeFunctionName(V) { +function validateCShakeFunctionName(V, dict) { const length = getBufferSourceByteLength(V); if (length === 0) return; - if (!isFips()) { + if (ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name))) { const bytes = getBufferSourceBytes(V); for (let i = 0; i < kCShakeFunctionNames.length; i++) { const functionName = kCShakeFunctionNames[i]; @@ -349,12 +347,17 @@ function validateCShakeFunctionName(V) { 'NotSupportedError'); } -function validateCShakeCustomization(V) { - if (isFips() && getBufferSourceByteLength(V) !== 0) +function validateCShakeCustomization(V, dict) { + if (getBufferSourceByteLength(V) === 0) return; + if (!ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name))) throw lazyDOMException( 'Unsupported CShakeParams customization', 'NotSupportedError'); validateMaxBufferLength(V, 'CShakeParams.customization', 512); + if (TypedArrayPrototypeIncludes(getBufferSourceBytes(V), 0)) + throw lazyDOMException( + 'Unsupported CShakeParams customization', + 'NotSupportedError'); } converters.RsaPssParams = createAlgorithmDictionaryConverter( @@ -814,7 +817,7 @@ converters.KmacParams = createAlgorithmDictionaryConverter( converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V) => { - if ((V === 0 || V % 8) && isFips()) + if (V % 8 !== 0 || (V === 0 && isFips())) throw lazyDOMException( 'Invalid KmacParams outputLength', 'NotSupportedError'); diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 2cf971347b9..65735042ec4 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,21 +7,15 @@ #include "threadpoolwork-inl.h" #include "v8.h" -#if OPENSSL_WITH_EVP_MAC -#include #include -#endif #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK #include #endif #include -#include #include #include -#include -#include #include #include @@ -566,9 +560,6 @@ void Hash::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "oneShotDigest", OneShotDigest); HashJob::Initialize(env, target); -#if OPENSSL_WITH_EVP_MAC - CShakeJob::Initialize(env, target); -#endif } void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -580,9 +571,6 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(OneShotDigest); HashJob::RegisterExternalReferences(registry); -#if OPENSSL_WITH_EVP_MAC - CShakeJob::RegisterExternalReferences(registry); -#endif } // new Hash(algorithm, xofLen, algorithmId, algorithmCache[, functionName, @@ -870,345 +858,5 @@ bool HashTraits::DeriveBits(Environment* env, return true; } -#if OPENSSL_WITH_EVP_MAC -namespace { - -static constexpr std::array kEmptyString = {}; -static constexpr size_t kKeccakKmac128Rate = 168; -static constexpr size_t kKeccakKmac256Rate = 136; -static constexpr size_t kMaxCShakeCustomizationSize = 512; - -struct EncodedLength { - std::array data; - size_t size; -}; - -struct EncodedStringInput { - const void* data; - size_t byte_length; - size_t bit_length; -}; - -struct KeccakKmacXof { - ncrypto::EVPMDCtxPointer ctx; - size_t rate; -}; - -size_t EncodedLengthSize(size_t value) { - size_t size = 1; - size_t remaining = value; - while (remaining >>= CHAR_BIT) size++; - return size + 1; -} - -bool AddSize(size_t a, size_t b, size_t* out) { - if (a > std::numeric_limits::max() - b) return false; - *out = a + b; - return true; -} - -EncodedLength EncodeLength(size_t value, bool left) { - const size_t value_size = EncodedLengthSize(value) - 1; - EncodedLength encoded = {{}, value_size + 1}; - - if (left) encoded.data[0] = static_cast(value_size); - for (size_t n = 0; n < value_size; n++) { - const size_t shift = CHAR_BIT * (value_size - n - 1); - encoded.data[(left ? 1 : 0) + n] = - static_cast(value >> shift); - } - if (!left) encoded.data[value_size] = static_cast(value_size); - - return encoded; -} - -bool DigestUpdate(ncrypto::EVPMDCtxPointer* ctx, - const void* data, - size_t size) { - if (size == 0) return true; - return ctx->digestUpdate(ncrypto::Buffer{ - .data = data, - .len = size, - }); -} - -bool DigestUpdateZeros(ncrypto::EVPMDCtxPointer* ctx, size_t size) { - static constexpr std::array zeros = {}; - while (size > 0) { - const size_t chunk = std::min(size, zeros.size()); - if (!DigestUpdate(ctx, zeros.data(), chunk)) return false; - size -= chunk; - } - return true; -} - -bool EncodedStringSize(size_t byte_length, size_t bit_length, size_t* size) { - return AddSize(EncodedLengthSize(bit_length), byte_length, size); -} - -bool ByteLengthToBitLength(size_t byte_length, size_t* bit_length) { - if (byte_length > std::numeric_limits::max() / CHAR_BIT) { - return false; - } - *bit_length = byte_length * CHAR_BIT; - return true; -} - -KeccakKmacXof NewKeccakKmacXof(bool use_128_bits) { - // OpenSSL 3.x exposes the cSHAKE/KMAC suffix primitive as KECCAK-KMAC-*. - const char* digest_name = use_128_bits ? OSSL_DIGEST_NAME_KECCAK_KMAC128 - : OSSL_DIGEST_NAME_KECCAK_KMAC256; - auto digest = std::unique_ptr{ - EVP_MD_fetch(nullptr, digest_name, nullptr), EVP_MD_free}; - if (!digest) return {}; - - auto ctx = ncrypto::EVPMDCtxPointer::New(); - if (!ctx.digestInit(digest.get())) return {}; - - return { - .ctx = std::move(ctx), - .rate = use_128_bits ? kKeccakKmac128Rate : kKeccakKmac256Rate, - }; -} - -bool ToEncodedStringInput(const void* data, - size_t byte_length, - EncodedStringInput* input) { - if (byte_length > 0 && data == nullptr) return false; - - size_t bit_length; - if (!ByteLengthToBitLength(byte_length, &bit_length)) return false; - - *input = { - .data = byte_length == 0 ? kEmptyString.data() : data, - .byte_length = byte_length, - .bit_length = bit_length, - }; - return true; -} - -bool DigestUpdateEncodedLength(ncrypto::EVPMDCtxPointer* ctx, - size_t value, - bool left) { - const EncodedLength encoded = EncodeLength(value, left); - return DigestUpdate(ctx, encoded.data.data(), encoded.size); -} - -bool DigestUpdateEncodedString(ncrypto::EVPMDCtxPointer* ctx, - const void* data, - size_t byte_length, - size_t bit_length) { - return DigestUpdateEncodedLength(ctx, bit_length, true) && - DigestUpdate(ctx, data, byte_length); -} - -bool DigestUpdateBytepad(ncrypto::EVPMDCtxPointer* ctx, - size_t width, - const void* data, - size_t byte_length, - size_t bit_length, - const void* data2 = nullptr, - size_t byte_length2 = 0, - size_t bit_length2 = 0) { - if (width == 0) return false; - - size_t encoded_size; - size_t written = EncodedLengthSize(width); - if (!EncodedStringSize(byte_length, bit_length, &encoded_size) || - !AddSize(written, encoded_size, &written)) { - return false; - } - if (data2 != nullptr) { - if (!EncodedStringSize(byte_length2, bit_length2, &encoded_size) || - !AddSize(written, encoded_size, &written)) { - return false; - } - } - - size_t padded_size; - if (!AddSize(written, width - 1, &padded_size)) return false; - padded_size = padded_size / width * width; - DCHECK_GE(padded_size, written); - const size_t padding = padded_size - written; - - return DigestUpdateEncodedLength(ctx, width, true) && - DigestUpdateEncodedString(ctx, data, byte_length, bit_length) && - (data2 == nullptr || - DigestUpdateEncodedString(ctx, data2, byte_length2, bit_length2)) && - DigestUpdateZeros(ctx, padding); -} - -} // namespace - -CShakeConfig::CShakeConfig(CShakeConfig&& other) noexcept - : in(std::move(other.in)), - function_name(std::move(other.function_name)), - customization(std::move(other.customization)), - variant(other.variant), - length(other.length) {} - -CShakeConfig& CShakeConfig::operator=(CShakeConfig&& other) noexcept { - if (&other == this) return *this; - this->~CShakeConfig(); - return *new (this) CShakeConfig(std::move(other)); -} - -void CShakeConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TraitTrackInline(in, "in"); - tracker->TraitTrackInline(function_name, "function_name"); - tracker->TraitTrackInline(customization, "customization"); -} - -MaybeLocal CShakeTraits::EncodeOutput(Environment* env, - const CShakeConfig& params, - ByteSource* out) { - return out->ToArrayBuffer(env); -} - -Maybe CShakeTraits::AdditionalConfig( - CryptoJobMode mode, - const FunctionCallbackInfo& args, - unsigned int offset, - CShakeConfig* params) { - Environment* env = Environment::GetCurrent(args); - - if (IsFipsEnabled()) { - THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); - return Nothing(); - } - - CHECK(args[offset]->IsString()); // Algorithm name - Utf8Value algorithm_name(env->isolate(), args[offset]); - std::string_view algorithm_str = algorithm_name.ToStringView(); - - if (algorithm_str == "cSHAKE128") { - params->variant = CShakeVariant::CSHAKE128; - } else if (algorithm_str == "cSHAKE256") { - params->variant = CShakeVariant::CSHAKE256; - } else { - UNREACHABLE(); - } - - ArrayBufferOrViewContents data(args[offset + 1]); - if (!data.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "data is too big"); - return Nothing(); - } - params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - - if (!args[offset + 2]->IsUndefined()) { - ArrayBufferOrViewContents function_name(args[offset + 2]); - if (!function_name.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "functionName is too big"); - return Nothing(); - } - params->function_name = IsCryptoJobAsync(mode) - ? function_name.ToCopy() - : function_name.ToByteSource(); - } - - if (!args[offset + 3]->IsUndefined()) { - ArrayBufferOrViewContents customization(args[offset + 3]); - if (!customization.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); - return Nothing(); - } - params->customization = IsCryptoJobAsync(mode) - ? customization.ToCopy() - : customization.ToByteSource(); - } - - CHECK(args[offset + 4]->IsUint32()); // Length - params->length = args[offset + 4].As()->Value(); - - return JustVoid(); -} - -bool CShakeTraits::DeriveBits(Environment* env, - const CShakeConfig& params, - ByteSource* out, - CryptoJobMode mode) { - CShakeParams cshake_params = { - .variant = params.variant, - .function_name_data = params.function_name.data(), - .function_name_size = params.function_name.size(), - .customization_data = params.customization.data(), - .customization_size = params.customization.size(), - .bytepad_input = nullptr, - .input_data = params.in.data(), - .input_size = params.in.size(), - .append_output_length = false, - .length = params.length, - }; - return DeriveCShakeBits(cshake_params, out); -} - -bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out) { - if (params.customization_size > kMaxCShakeCustomizationSize) { - return false; - } - - if (params.length == 0) { - *out = ByteSource(); - return true; - } - - auto xof = NewKeccakKmacXof(params.variant == CShakeVariant::CSHAKE128); - if (!xof.ctx) return false; - auto ctx = std::move(xof.ctx); - - EncodedStringInput function_name; - EncodedStringInput customization; - if (!ToEncodedStringInput(params.function_name_data, - params.function_name_size, - &function_name) || - !ToEncodedStringInput(params.customization_data, - params.customization_size, - &customization)) { - return false; - } - - if (!DigestUpdateBytepad(&ctx, - xof.rate, - function_name.data, - function_name.byte_length, - function_name.bit_length, - customization.data, - customization.byte_length, - customization.bit_length)) { - return false; - } - - if (params.bytepad_input != nullptr && - !DigestUpdateBytepad(&ctx, - xof.rate, - params.bytepad_input->data, - params.bytepad_input->byte_length, - params.bytepad_input->bit_length)) { - return false; - } - - if (!DigestUpdate(&ctx, params.input_data, params.input_size)) { - return false; - } - - if (params.append_output_length && - !DigestUpdateEncodedLength(&ctx, params.length, false)) { - return false; - } - - const size_t length_bytes = - NumBitsToBytes(static_cast(params.length)); - auto data = ctx.digestFinal(length_bytes); - if (!data) [[unlikely]] - return false; - - DCHECK(!data.isSecure()); - *out = ByteSource::Allocated(data.release()); - if (params.length % CHAR_BIT != 0) TruncateToBitLength(params.length, out); - return true; -} -#endif // OPENSSL_WITH_EVP_MAC - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 2bab4c24bd5..ad6c439afaa 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -85,73 +85,6 @@ struct HashTraits final { using HashJob = DeriveBitsJob; -#if OPENSSL_WITH_EVP_MAC -enum class CShakeVariant { CSHAKE128, CSHAKE256 }; - -struct CShakeBytepadInput final { - const void* data; - size_t byte_length; - size_t bit_length; -}; - -struct CShakeParams final { - CShakeVariant variant; - const void* function_name_data; - size_t function_name_size; - const void* customization_data; - size_t customization_size; - const CShakeBytepadInput* bytepad_input; - const void* input_data; - size_t input_size; - bool append_output_length; - uint32_t length; // Output length in bits -}; - -bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out); - -struct CShakeConfig final : public MemoryRetainer { - ByteSource in; - ByteSource function_name; - ByteSource customization; - CShakeVariant variant; - uint32_t length; // Output length in bits - - CShakeConfig() = default; - - explicit CShakeConfig(CShakeConfig&& other) noexcept; - - CShakeConfig& operator=(CShakeConfig&& other) noexcept; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(CShakeConfig) - SET_SELF_SIZE(CShakeConfig) -}; - -struct CShakeTraits final { - using AdditionalParameters = CShakeConfig; - static constexpr const char* JobName = "CShakeJob"; - static constexpr AsyncWrap::ProviderType Provider = - AsyncWrap::PROVIDER_HASHREQUEST; - - static v8::Maybe AdditionalConfig( - CryptoJobMode mode, - const v8::FunctionCallbackInfo& args, - unsigned int offset, - CShakeConfig* params); - - static bool DeriveBits(Environment* env, - const CShakeConfig& params, - ByteSource* out, - CryptoJobMode mode); - - static v8::MaybeLocal EncodeOutput(Environment* env, - const CShakeConfig& params, - ByteSource* out); -}; - -using CShakeJob = DeriveBitsJob; -#endif // OPENSSL_WITH_EVP_MAC - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index 292dedf6ccc..1e29a247454 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -1,14 +1,11 @@ #include "crypto/crypto_kmac.h" #include "async_wrap-inl.h" -#include "crypto/crypto_hash.h" #include "node_internals.h" #include "threadpoolwork-inl.h" #if OPENSSL_WITH_EVP_MAC #include #include -#include -#include #include #include "crypto/crypto_keys.h" #include "crypto/crypto_sig.h" @@ -26,7 +23,6 @@ using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; -using v8::Number; using v8::Object; using v8::Uint32; using v8::Value; @@ -38,7 +34,6 @@ KmacConfig::KmacConfig(KmacConfig&& other) noexcept signature(std::move(other.signature)), customization(std::move(other.customization)), variant(other.variant), - key_length(other.key_length), length(other.length) {} KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { @@ -96,27 +91,18 @@ Maybe KmacTraits::AdditionalConfig( } // If undefined, params->customization remains uninitialized (size 0). - CHECK(args[offset + 4]->IsNumber()); // Key length - double key_length = args[offset + 4].As()->Value(); - if (!(key_length >= 0) || - key_length > static_cast(std::numeric_limits::max())) { - THROW_ERR_OUT_OF_RANGE(env, "key length is too big"); - return Nothing(); - } - params->key_length = static_cast(key_length); + CHECK(args[offset + 4]->IsUint32()); // Length + params->length = args[offset + 4].As()->Value(); - CHECK(args[offset + 5]->IsUint32()); // Length - params->length = args[offset + 5].As()->Value(); - - ArrayBufferOrViewContents data(args[offset + 6]); + ArrayBufferOrViewContents data(args[offset + 5]); if (!data.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - if (!args[offset + 7]->IsUndefined()) { - ArrayBufferOrViewContents signature(args[offset + 7]); + if (!args[offset + 6]->IsUndefined()) { + ArrayBufferOrViewContents signature(args[offset + 6]); if (!signature.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); @@ -128,85 +114,17 @@ Maybe KmacTraits::AdditionalConfig( return JustVoid(); } -namespace { - -static constexpr std::array kKmacFunctionName = { - 'K', 'M', 'A', 'C'}; -static constexpr size_t kKmacMinOpenSSLKeySize = 4; -// Keep the bit-aware path within OpenSSL's KMAC provider limits. -static constexpr size_t kKmacMaxOpenSSLKeySize = 512; -static constexpr size_t kKmacMaxOpenSSLCustomizationSize = 512; -static constexpr size_t kKmacMaxOpenSSLOutputSize = 0xffffff / CHAR_BIT; - -bool KmacParamsWithinOpenSSLLimits(const KmacConfig& params, - size_t key_size, - size_t length_bytes) { - return key_size <= kKmacMaxOpenSSLKeySize && - NumBitsToBytes(params.key_length) <= kKmacMaxOpenSSLKeySize && - params.customization.size() <= kKmacMaxOpenSSLCustomizationSize && - length_bytes <= kKmacMaxOpenSSLOutputSize; -} - -bool DeriveBitsWithCShake(const KmacConfig& params, - const void* key_data, - size_t key_size, - ByteSource* out) { - if (IsFipsEnabled()) return false; - - const size_t key_length_bytes = NumBitsToBytes(params.key_length); - if (key_size < key_length_bytes) return false; - - CShakeBytepadInput key_input = { - .data = key_data, - .byte_length = key_length_bytes, - .bit_length = params.key_length, - }; - CShakeParams cshake_params = { - .variant = params.variant == KmacVariant::KMAC128 - ? CShakeVariant::CSHAKE128 - : CShakeVariant::CSHAKE256, - .function_name_data = kKmacFunctionName.data(), - .function_name_size = kKmacFunctionName.size(), - .customization_data = params.customization.data(), - .customization_size = params.customization.size(), - .bytepad_input = &key_input, - .input_data = params.data.data(), - .input_size = params.data.size(), - .append_output_length = true, - .length = params.length, - }; - return DeriveCShakeBits(cshake_params, out); -} - -} // namespace - bool KmacTraits::DeriveBits(Environment* env, const KmacConfig& params, ByteSource* out, CryptoJobMode mode) { - const bool truncate_to_bit_length = params.length % CHAR_BIT != 0; - const size_t length_bytes = - NumBitsToBytes(static_cast(params.length)); + if (params.length % CHAR_BIT != 0) return false; + const size_t length_bytes = params.length / CHAR_BIT; // Get the key data. const void* key_data = params.key.GetSymmetricKey(); size_t key_size = params.key.GetSymmetricKeySize(); - if (!KmacParamsWithinOpenSSLLimits(params, key_size, length_bytes)) { - return false; - } - - if (params.length == 0) { - *out = ByteSource(); - return true; - } - - // OpenSSL's EVP_MAC provider rejects KMAC keys shorter than 4 bytes. - if (params.length % CHAR_BIT != 0 || params.key_length % CHAR_BIT != 0 || - key_size < kKmacMinOpenSSLKeySize) { - return DeriveBitsWithCShake(params, key_data, key_size, out); - } - // Fetch the KMAC algorithm auto mac = EVPMacPointer::Fetch((params.variant == KmacVariant::KMAC128) ? OSSL_MAC_NAME_KMAC128 @@ -260,7 +178,6 @@ bool KmacTraits::DeriveBits(Environment* env, auto buffer = result.release(); *out = ByteSource::Allocated(buffer.data, buffer.len); - if (truncate_to_bit_length) TruncateToBitLength(params.length, out); return true; } diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 65617fd7623..951b67da887 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -21,8 +21,7 @@ struct KmacConfig final : public MemoryRetainer { ByteSource signature; ByteSource customization; KmacVariant variant; - size_t key_length; // Key length in bits - uint32_t length; // Output length in bits + uint32_t length; // Output length in bits KmacConfig() = default; diff --git a/test/fixtures/crypto/kmac.js b/test/fixtures/crypto/kmac.js index ed265bb9c9f..cc1870af2bb 100644 --- a/test/fixtures/crypto/kmac.js +++ b/test/fixtures/crypto/kmac.js @@ -114,61 +114,6 @@ module.exports = function() { 0x76, 0xfc, 0x89, 0x65, ]), }, - { - // KMAC128 with a short key, generated with OpenSSL's KECCAK-KMAC128 - // digest over independently encoded NIST SP 800-185 framing. - algorithm: 'KMAC128', - key: Buffer.from([0x00, 0x01, 0x02]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: Buffer.from('Node.js'), - outputLength: 256, - expected: Buffer.from([ - 0xfb, 0x7c, 0xbb, 0xa2, 0xa1, 0x0d, 0x1a, 0x87, 0x9a, 0x9f, 0x96, 0x8c, - 0x58, 0x9d, 0x2a, 0xfe, 0x4a, 0x9b, 0xbf, 0x03, 0x7e, 0x85, 0x2c, 0xac, - 0x05, 0xdd, 0x78, 0x5b, 0x78, 0xd6, 0x57, 0x1c, - ]), - }, - { - // KMAC256 with a short key, generated with OpenSSL's KECCAK-KMAC256 - // digest over independently encoded NIST SP 800-185 framing. - algorithm: 'KMAC256', - key: Buffer.from([0x00, 0x01, 0x02]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: Buffer.from('Node.js'), - outputLength: 512, - expected: Buffer.from([ - 0x2c, 0xcf, 0x20, 0xde, 0xd8, 0xc9, 0x6d, 0xb0, 0x5f, 0x15, 0xe0, 0xb3, - 0xce, 0x5d, 0xf0, 0x45, 0xc7, 0xd7, 0x4e, 0xfe, 0x18, 0xee, 0x36, 0xa8, - 0xe4, 0x9a, 0x37, 0xfb, 0xc2, 0xb1, 0xbb, 0xfd, 0xad, 0xf5, 0xb7, 0x89, - 0xd3, 0xa4, 0xbc, 0xb3, 0xa8, 0x28, 0x8e, 0x9f, 0x25, 0xe6, 0x8d, 0x5b, - 0x4a, 0x01, 0x0b, 0x90, 0xae, 0x6d, 0x2b, 0xfc, 0xf1, 0xb6, 0xbb, 0x82, - 0x34, 0x8b, 0x51, 0xd9, - ]), - }, - { - // KMAC128 with a non-byte-aligned output length. The second byte has its - // unused low bits cleared after squeezing a 9-bit result. - algorithm: 'KMAC128', - key: Buffer.from([0x00, 0x01, 0x02, 0x03]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: undefined, - outputLength: 9, - expected: Buffer.from([0x63, 0x80]), - }, - { - // KMAC128 with a non-byte-aligned key length. The raw key is already - // truncated to 25 bits, matching WebCrypto import semantics. - algorithm: 'KMAC128', - key: Buffer.from([0xff, 0xff, 0xff, 0x80]), - keyLength: 25, - data: Buffer.from([0x01, 0x02, 0x03]), - customization: undefined, - outputLength: 128, - expected: Buffer.from([ - 0x25, 0xea, 0xc7, 0x06, 0x82, 0x47, 0x7e, 0x3c, 0x9b, 0xf0, 0xf1, 0x51, - 0x87, 0x46, 0x40, 0x0c, - ]), - }, ]; return vectors; diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index b01ebce3e5a..8d58397783e 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -11,6 +11,8 @@ const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2) && !fips; const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); +const cshake128 = crypto.getHashes().includes('cshake128'); +const cshake256 = crypto.getHashes().includes('cshake256'); const sha3 = crypto.getHashes().includes('sha3-256'); const ocb = hasOpenSSL(3) && crypto.getCiphers().includes('aes-128-ocb'); const kmac = hasOpenSSL(3) && crypto.getMacs().includes('kmac128'); @@ -22,19 +24,21 @@ export const vectors = { [false, 'cSHAKE128'], [shake128, { name: 'cSHAKE128', outputLength: 128 }], [shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], + [cshake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('SHAKE') }], - [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], + [cshake128, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1, 1) }], + [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(513) }], - [shake128, { name: 'cSHAKE128', outputLength: 127 }], + [false, { name: 'cSHAKE128', outputLength: 127 }], [false, 'cSHAKE256'], [shake256, { name: 'cSHAKE256', outputLength: 256 }], [shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], + [cshake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('SHAKE') }], - [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], + [cshake256, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1, 1) }], + [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(513) }], - [shake256, { name: 'cSHAKE256', outputLength: 255 }], + [false, { name: 'cSHAKE256', outputLength: 255 }], [false, 'TurboSHAKE128'], [!fips, { name: 'TurboSHAKE128', outputLength: 128 }], [!fips, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], @@ -86,9 +90,9 @@ export const vectors = { [false, 'KMAC128'], [false, 'KMAC256'], [kmac, { name: 'KMAC128', outputLength: 256 }], - [kmac && !fips, { name: 'KMAC128', outputLength: 255 }], + [false, { name: 'KMAC128', outputLength: 255 }], [kmac, { name: 'KMAC256', outputLength: 256 }], - [kmac && !fips, { name: 'KMAC256', outputLength: 255 }], + [false, { name: 'KMAC256', outputLength: 255 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], @@ -109,10 +113,14 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac && !fips, { name: 'KMAC128', length: 0 }], - [kmac && !fips, { name: 'KMAC256', length: 0 }], - [kmac && !fips, { name: 'KMAC128', length: 1 }], - [kmac && !fips, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC128', length: 0 }], + [false, { name: 'KMAC256', length: 0 }], + [false, { name: 'KMAC128', length: 1 }], + [false, { name: 'KMAC128', length: 24 }], + [kmac, { name: 'KMAC128', length: 32 }], + [false, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC256', length: 24 }], + [kmac, { name: 'KMAC256', length: 32 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -133,10 +141,14 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac && !fips, { name: 'KMAC128', length: 0 }], - [kmac && !fips, { name: 'KMAC256', length: 0 }], - [kmac && !fips, { name: 'KMAC128', length: 1 }], - [kmac && !fips, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC128', length: 0 }], + [false, { name: 'KMAC256', length: 0 }], + [false, { name: 'KMAC128', length: 1 }], + [false, { name: 'KMAC128', length: 24 }], + [kmac, { name: 'KMAC128', length: 32 }], + [false, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC256', length: 24 }], + [kmac, { name: 'KMAC256', length: 32 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -251,7 +263,7 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], - [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], @@ -283,7 +295,7 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], - [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], 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 eeb5c90c166..6024029da1b 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -14,7 +14,6 @@ const { } = 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) => { @@ -132,19 +131,11 @@ function genericSecretVectors(name) { ]; } -function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { +function macInvalid(algorithm, invalidLengthMessage, isKmac = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - if (allowZeroKey && !fips) { - const zeroKey = createSecretKey(Buffer.alloc(0)) - .toCryptoKey(algorithm, true, usages); - assert.strictEqual(zeroKey.algorithm.length, 0); - - const explicitZeroKey = createSecretKey(Buffer.alloc(0)) - .toCryptoKey({ ...algorithm, length: 0 }, true, usages); - assert.strictEqual(explicitZeroKey.algorithm.length, 0); - } else if (allowZeroKey) { + if (isKmac) { for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) { assert.throws(() => { createSecretKey(Buffer.alloc(0)) @@ -170,7 +161,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { assert.throws( () => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages), - allowZeroKey && fips ? { + isKmac ? { name: 'NotSupportedError', message: 'Invalid key length', } : { diff --git a/test/parallel/test-webcrypto-aes-kw-short-input.js b/test/parallel/test-webcrypto-aes-kw-short-input.js index 09fe0268ab9..e11e19360de 100644 --- a/test/parallel/test-webcrypto-aes-kw-short-input.js +++ b/test/parallel/test-webcrypto-aes-kw-short-input.js @@ -6,18 +6,13 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { getFips } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; (async () => { const keyToWrap = await subtle.importKey( 'raw', new Uint8Array(16), 'AES-GCM', true, ['encrypt']); - let emptyKey; - if (hasOpenSSL(3) && getFips() !== 1) { - emptyKey = await subtle.importKey( - 'raw-secret', new Uint8Array(0), 'KMAC128', true, ['sign']); - } + const shortKey = await subtle.importKey( + 'raw', new Uint8Array(8), { name: 'HMAC', hash: 'SHA-256' }, true, ['sign']); for (const length of [128, 192, 256]) { const wrappingKey = await subtle.generateKey( @@ -31,11 +26,8 @@ const { subtle } = globalThis.crypto; 'HKDF', false, ['deriveBits']), { name: 'OperationError' }); } - if (emptyKey !== undefined) { - await assert.rejects(subtle.wrapKey( - 'raw-secret', emptyKey, wrappingKey, 'AES-KW'), - { name: 'OperationError' }); - } + await assert.rejects(subtle.wrapKey( + 'raw', shortKey, wrappingKey, 'AES-KW'), { name: 'OperationError' }); const wrapped = await subtle.wrapKey( 'raw', keyToWrap, wrappingKey, 'AES-KW'); diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index c567edfbb4e..675b740d939 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -283,7 +283,7 @@ const fips4 = hasFIPS(4); })().then(common.mustCall()); } -if (hasOpenSSL(3) && !hasFIPS()) { +if (hasOpenSSL(3)) { (async () => { const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; const usages = ['sign']; @@ -313,19 +313,9 @@ if (hasOpenSSL(3) && !hasFIPS()) { baseKeyAlgorithm, false, ['deriveKey']); - const derived = await subtle.deriveKey( - algorithm, - baseKey, - derivedKeyAlgorithm, - false, - usages); - assert.strictEqual(derived.algorithm.length, 0); - - const signature = subtle.sign({ - name: 'KMAC128', - outputLength: 256, - }, derived, new Uint8Array()); - assert.strictEqual((await signature).byteLength, 32); + await assert.rejects( + subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages), + { name: 'NotSupportedError', message: 'Invalid key length' }); } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 47a56a912d6..89bd86bd928 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -9,8 +9,7 @@ const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); -const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); -const fips = hasFIPS(); +const { isBoringSSL } = require('../common/crypto'); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -265,16 +264,16 @@ if (getHashes().includes('shake128')) { new Uint8Array(0), ); - const digest = await subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)); - assert.strictEqual(digest.byteLength, 1); - assert.strictEqual(new Uint8Array(digest)[0] & 0b00000001, 0); + await assert.rejects( + subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)), + { name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' }); await assert.rejects( subtle.digest( { name: 'cSHAKE128', outputLength: 0xffffffff }, Buffer.alloc(1)), { - name: 'OperationError', + name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength', }); @@ -291,22 +290,53 @@ if (getHashes().includes('shake128')) { message: 'Unsupported CShakeParams functionName', }); - if (fips) return; - - await assert.rejects( - subtle.digest( - { - name: 'cSHAKE128', + for (const name of ['cSHAKE128', 'cSHAKE256']) { + const supported = getHashes().includes(name.toLowerCase()); + assert.deepStrictEqual( + await subtle.digest({ + name, outputLength: 256, - customization: Buffer.alloc(513), - }, - Buffer.alloc(1)), - { - name: 'OperationError', - message: 'CShakeParams.customization must be at most 512 bytes', - }); - - if (!hasOpenSSL(3)) return; + functionName: Buffer.alloc(0), + customization: Buffer.alloc(0), + }, Buffer.alloc(1)), + await subtle.digest({ name, outputLength: 256 }, Buffer.alloc(1))); + + await assert.rejects( + subtle.digest({ + name, + outputLength: 256, + customization: Buffer.alloc(513, 1), + }, Buffer.alloc(1)), + supported ? { + name: 'OperationError', + message: 'CShakeParams.customization must be at most 512 bytes', + } : { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams customization', + }); + + await assert.rejects( + subtle.digest({ + name, + outputLength: 256, + customization: Buffer.from([0x61, 0x00, 0x62]), + }, Buffer.alloc(1)), + { name: 'NotSupportedError', message: 'Unsupported CShakeParams customization' }); + + for (const params of [ + { functionName: Buffer.from('KMAC') }, + { customization: Buffer.from('Node.js') }, + ]) { + const algorithm = { name, outputLength: 256, ...params }; + if (supported) { + assert.strictEqual((await subtle.digest(algorithm, Buffer.alloc(1))).byteLength, 32); + } else { + await assert.rejects(subtle.digest(algorithm, Buffer.alloc(1)), { + name: 'NotSupportedError', + }); + } + } + } const nistCShakeShortInput = Buffer.from('00010203', 'hex'); const nistCShakeLongInput = @@ -400,19 +430,23 @@ if (getHashes().includes('shake128')) { 'ca6f88db415829', }, ]) { - assert.strictEqual( - Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), - expected); + if (getHashes().includes(algorithm.name.toLowerCase())) { + assert.strictEqual( + Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), + expected); + } else { + await assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + }); + } } - const truncated = Buffer.from(await subtle.digest( - { ...nistCShakeSample1.algorithm, outputLength: 255 }, - 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.strictEqual(truncated[31] & 0b00000001, 0); - assert.strictEqual(truncated[31] | 0b00000001, expected[31]); + if (getHashes().includes('cshake128')) { + await assert.rejects( + subtle.digest( + { ...nistCShakeSample1.algorithm, outputLength: 255 }, + nistCShakeSample1.data), + { name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' }); + } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 385ae8c65e9..63fdde05f1e 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -285,69 +285,35 @@ if (hasOpenSSL(3)) { [/* empty usages */]), { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); - { - 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); + for (const algorithm of [name, { name, length: 0 }]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', new Uint8Array(), algorithm, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + } - 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( - '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); + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 0 }, + true, + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + + await assert.rejects( + subtle.generateKey({ name, length: 9 }, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); - const importedExplicit = await subtle.importKey( + for (const byteLength of [1, 2]) { + await assert.rejects( + subtle.importKey( 'raw-secret', - new Uint8Array([0xff, 0xff]), + new Uint8Array(byteLength).fill(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( - 'raw-secret', - new Uint8Array([0xff]), - { name, length: 9 }, - true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); - } + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); } } diff --git a/test/parallel/test-webcrypto-fips-exceptions.mjs b/test/parallel/test-webcrypto-fips-exceptions.mjs index d9c26a1674d..ebbec86e688 100644 --- a/test/parallel/test-webcrypto-fips-exceptions.mjs +++ b/test/parallel/test-webcrypto-fips-exceptions.mjs @@ -12,10 +12,10 @@ if (!hasFIPS(3)) common.skip('requires OpenSSL >= 3 in FIPS mode'); const require = createRequire(import.meta.url); +const { getHashes } = require('node:crypto'); const { internalBinding } = require('internal/test/binding'); const { getCryptoKeyHandle } = require('internal/crypto/keys'); const { - CShakeJob, KangarooTwelveJob, KmacJob, TurboShakeJob, @@ -52,13 +52,6 @@ for (const createJob of [ 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', @@ -66,48 +59,34 @@ for (const createJob of [ }); } -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', +for (const name of ['cSHAKE128', 'cSHAKE256']) { + const emptyCShake = { + name, outputLength: 256, - customization: new Uint8Array(length), + customization: data, + functionName: data, }; - await assertFipsException( - 'digest', - algorithm, - () => subtle.digest(algorithm, data), - 'Unsupported CShakeParams customization'); + assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true); + assert.strictEqual((await subtle.digest(emptyCShake, data)).byteLength, 32); + + for (const params of [ + { customization: Buffer.from('Node.js') }, + { functionName: Buffer.from('KMAC') }, + { functionName: Buffer.from('KMAC'), customization: Buffer.from('Node.js') }, + ]) { + const algorithm = { name, outputLength: 256, ...params }; + const supported = getHashes().includes(name.toLowerCase()); + assert.strictEqual(SubtleCrypto.supports('digest', algorithm), supported); + if (supported) { + assert.strictEqual((await subtle.digest(algorithm, data)).byteLength, 32); + } else { + await assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + }); + } + } } -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( @@ -170,7 +149,6 @@ await assert.rejects( getCryptoKeyHandle(key), 'KMAC128', undefined, - 32, 9, data, undefined).run(), diff --git a/test/parallel/test-webcrypto-fips-refresh.js b/test/parallel/test-webcrypto-fips-refresh.js index 71459099356..4eff258b72b 100644 --- a/test/parallel/test-webcrypto-fips-refresh.js +++ b/test/parallel/test-webcrypto-fips-refresh.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { getFips, setFips } = require('crypto'); +const { getFips, getHashes, setFips } = require('crypto'); const { internalBinding } = require('internal/test/binding'); const { getOptionValue } = require('internal/options'); if (!internalBinding('crypto').testFipsCrypto()) @@ -25,8 +25,8 @@ try { name: 'KT128', outputLength: 128, }), !fips); assert.strictEqual(SubtleCrypto.supports('digest', { - name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array(1), - }), !fips); + name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array([1]), + }), getHashes().includes('cshake128')); assert.strictEqual(SubtleCrypto.supports('generateKey', { name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), diff --git a/test/parallel/test-webcrypto-keccak-byte-alignment.js b/test/parallel/test-webcrypto-keccak-byte-alignment.js new file mode 100644 index 00000000000..ce3201210eb --- /dev/null +++ b/test/parallel/test-webcrypto-keccak-byte-alignment.js @@ -0,0 +1,90 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createSecretKey, subtle } = require('crypto'); +const { SubtleCrypto } = globalThis; +const unsupported = { name: 'NotSupportedError' }; + +(async () => { + const data = new Uint8Array([1, 2, 3]); + for (const name of ['cSHAKE128', 'cSHAKE256']) { + if (!SubtleCrypto.supports('digest', { name, outputLength: 128 })) + continue; + + for (const customization of [undefined, new Uint8Array([1])]) { + if (!SubtleCrypto.supports('digest', { name, outputLength: 128, customization })) + continue; + + for (let remainder = 1; remainder < 8; remainder++) { + const algorithm = { name, outputLength: 128 + remainder, customization }; + assert.strictEqual(SubtleCrypto.supports('digest', algorithm), false); + await assert.rejects(subtle.digest(algorithm, data), unsupported); + } + + const digest = await subtle.digest({ name, outputLength: 128, customization }, data); + assert.strictEqual(digest.byteLength, 16); + } + + // Check the uint32 boundary without allocating a large digest. + assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength: 0xfffffff8 }), true); + for (const outputLength of [0xfffffff9, 0xffffffff]) { + assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength }), false); + await assert.rejects(subtle.digest({ name, outputLength }, data), unsupported); + } + } + + const baseKey = await subtle.importKey('raw-secret', data, 'HKDF', false, ['deriveKey']); + const derivation = { name: 'HKDF', hash: 'SHA-256', salt: data, info: data }; + const raw = Buffer.alloc(32, 1); + const secretKey = createSecretKey(raw); + + for (const name of ['KMAC128', 'KMAC256']) { + if (!SubtleCrypto.supports('importKey', name)) + continue; + + const key = await subtle.importKey('raw-secret', raw, name, false, ['sign', 'verify']); + for (let remainder = 1; remainder < 8; remainder++) { + const algorithm = { name, length: 248 + remainder }; + assert.strictEqual(SubtleCrypto.supports('generateKey', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('importKey', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('deriveKey', derivation, algorithm), false); + await assert.rejects(subtle.generateKey(algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.importKey('raw-secret', raw, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.importKey( + 'jwk', { kty: 'oct', k: raw.toString('base64url') }, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.deriveKey(derivation, baseKey, algorithm, false, ['sign']), unsupported); + assert.throws(() => secretKey.toCryptoKey(algorithm, false, ['sign']), unsupported); + + const params = { name, outputLength: 248 + remainder }; + assert.strictEqual(SubtleCrypto.supports('sign', params), false); + assert.strictEqual(SubtleCrypto.supports('verify', params), false); + await assert.rejects(subtle.sign(params, key, data), unsupported); + await assert.rejects(subtle.verify(params, key, raw, data), unsupported); + } + } + + if (SubtleCrypto.supports('generateKey', 'ML-KEM-768')) { + const { publicKey, privateKey } = await subtle.generateKey( + 'ML-KEM-768', false, ['encapsulateBits', 'encapsulateKey', 'decapsulateKey']); + const { ciphertext } = await subtle.encapsulateBits('ML-KEM-768', publicKey); + for (const name of ['KMAC128', 'KMAC256']) { + if (!SubtleCrypto.supports('importKey', name)) + continue; + + const algorithm = { name, length: 255 }; + assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', algorithm), false); + await assert.rejects(subtle.encapsulateKey( + 'ML-KEM-768', publicKey, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.decapsulateKey( + 'ML-KEM-768', privateKey, ciphertext, algorithm, false, ['sign']), unsupported); + + assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', { name, length: 256 }), true); + assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', { name, length: 256 }), true); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index 33716095751..999e70df6a6 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 { hasFIPS, hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); @@ -13,7 +13,6 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { subtle } = globalThis.crypto; -const fips = hasFIPS(); const usages = ['sign', 'verify']; @@ -23,9 +22,6 @@ async function test(name, length) { 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); @@ -45,12 +41,10 @@ async function test(name, length) { } const kTests = [ - ['KMAC128', 0], ['KMAC128', 32], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], - ['KMAC256', 0], ['KMAC256', 32], ['KMAC256', 128], ['KMAC256', 256], @@ -60,3 +54,13 @@ const kTests = [ const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); + +(async () => { + for (const name of ['KMAC128', 'KMAC256']) { + for (const length of [0, 8, 16, 24]) { + await assert.rejects( + subtle.generateKey({ name, length }, true, usages), + { name: 'NotSupportedError', message: 'Invalid key length' }); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-prototype-pollution.mjs b/test/parallel/test-webcrypto-prototype-pollution.mjs index bb8a8e20d3c..45b5dfc8389 100644 --- a/test/parallel/test-webcrypto-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-prototype-pollution.mjs @@ -13,8 +13,7 @@ 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 { hasOpenSSL } = require('../common/crypto'); +const { getFips, getHashes } = require('node:crypto'); const { subtle } = globalThis.crypto; const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype); @@ -134,15 +133,15 @@ if (supports('digest', 'cSHAKE128')) { message: /Unsupported CShakeParams functionName/, }))); - // asyncDigest() picks the cSHAKE job over plain SHAKE on a non-empty + // asyncDigest() picks cSHAKE over plain SHAKE on a non-empty // customization. - if (hasOpenSSL(3)) { + { const algorithm = { name: 'cSHAKE128', outputLength: 256, customization: new Uint8Array([1, 2, 3]), }; - if (getFips() === 1) { + if (!getHashes().includes('cshake128')) { await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() => assert.rejects(subtle.digest(algorithm, data), { name: 'NotSupportedError', diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index ac0b738bcd5..5829ab3685c 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -5,38 +5,16 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasFIPS, hasOpenSSL } = require('../common/crypto'); +const { 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, @@ -215,17 +193,8 @@ async function testSign({ algorithm, const variations = []; for (const vector of vectors) { - 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)); - } + variations.push(testVerify(vector)); + variations.push(testSign(vector)); } await Promise.all(variations); @@ -240,87 +209,54 @@ async function testSign({ algorithm, ['sign', 'verify']); const algorithm = { name: 'KMAC128', - outputLength: fips ? 16 : 9, + outputLength: 16, customization: new Uint8Array(), }; const data = new Uint8Array([1, 2, 3]); const signature = await subtle.sign(algorithm, key, data); assert.strictEqual(signature.byteLength, 2); - if (!fips) - assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); assert(await subtle.verify(algorithm, key, signature, data)); - 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 signature128 = await subtle.sign({ + ...algorithm, + outputLength: 128, + }, key, data); + assert.strictEqual(signature128.byteLength, 16); + assert(await subtle.verify({ + ...algorithm, + outputLength: 128, + }, key, signature128, data)); const invalidSignature = new Uint8Array(signature); - if (fips) - invalidSignature[0] ^= 0b00000001; - else - invalidSignature[1] |= 0b00000001; + invalidSignature[0] ^= 0b00000001; assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); - if (!fips) { - const nonByteKey = await subtle.importKey( + const nonByteOutput = { ...algorithm, outputLength: 9 }; + await assert.rejects( + subtle.sign(nonByteOutput, key, data), + { name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' }); + await assert.rejects( + subtle.verify(nonByteOutput, key, signature, data), + { name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' }); + + await assert.rejects( + 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)); - } + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); })().then(common.mustCall()); (async function() { - if (fips) return; - - const data = new Uint8Array([1, 2, 3]); - for (const name of ['KMAC128', 'KMAC256']) { - for (const keyData of [ - new Uint8Array(), - new Uint8Array([1]), - new Uint8Array([1, 2, 3]), - ]) { - const key = await subtle.importKey( - 'raw-secret', - keyData, - { name }, - true, - ['sign', 'verify']); - assert.strictEqual(key.algorithm.length, keyData.byteLength * 8); - - const algorithm = { name, outputLength: 256 }; - const signature = subtle.sign(algorithm, key, data); - const result = await signature; - assert.strictEqual(result.byteLength, 32); - assert(await subtle.verify(algorithm, key, result, data)); + for (const byteLength of [0, 1, 2, 3]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', new Uint8Array(byteLength), name, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); } } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index b1198964330..84024462204 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -485,20 +485,30 @@ async function testNonByteLengthWrapUnwrap({ }); if (hasOpenSSL(3) && getFips() !== 1) { - const kmacAlgorithm = { name: 'KMAC128' }; - const kmacKey = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff]), - { ...kmacAlgorithm, length: 9 }, - true, - ['sign', 'verify']); - await testNonByteLengthWrapUnwrap({ - key: kmacKey, - formats: ['raw-secret', 'jwk'], - rawFormat: 'raw-secret', - explicitAlgorithm: { ...kmacAlgorithm, length: 9 }, - implicitAlgorithm: kmacAlgorithm, - }); + for (const name of ['KMAC128', 'KMAC256']) { + const keyData = new Uint8Array(32).fill(0xff); + const kmacKey = await subtle.importKey( + 'raw-secret', keyData, name, true, ['sign', 'verify']); + const wrappingKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, true, ['wrapKey', 'unwrapKey']); + + for (const [i, format] of ['raw-secret', 'jwk'].entries()) { + const wrapAlgorithm = { name: 'AES-GCM', iv: new Uint8Array(12).fill(i) }; + const wrapped = await subtle.wrapKey(format, kmacKey, wrappingKey, wrapAlgorithm); + await assert.rejects( + subtle.unwrapKey( + format, wrapped, wrappingKey, wrapAlgorithm, + { name, length: 255 }, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + + const unwrapped = await subtle.unwrapKey( + format, wrapped, wrappingKey, wrapAlgorithm, + { name, length: 256 }, true, ['sign', 'verify']); + assert.strictEqual(unwrapped.algorithm.length, 256); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey('raw-secret', unwrapped)), keyData); + } + } } })().then(common.mustCall()); diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index af6455f886f..e12016ec916 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -182,17 +182,6 @@ declare namespace InternalCryptoBinding { ): CryptoJobWebCrypto; } - interface CShakeJobConstructor { - new( - mode: CryptoJobWebCryptoMode, - algorithm: string, - data: ByteSource, - functionName: OptionalByteSource, - customization: OptionalByteSource, - outputLength: number, - ): CryptoJobWebCrypto; - } - interface ChaCha20Poly1305CipherJobConstructor { new( mode: CryptoJobWebCryptoMode, @@ -375,7 +364,6 @@ declare namespace InternalCryptoBinding { key: KeyObjectHandle, algorithm: string, customization: OptionalByteSource, - keyLength: number, outputLength: number, data: ByteSource, ...signature: MacJobSignatureArgs @@ -818,7 +806,6 @@ declare namespace InternalCryptoBinding { export interface CryptoBinding { AESCipherJob: InternalCryptoBinding.AESCipherJobConstructor; Argon2Job: InternalCryptoBinding.Argon2JobConstructor; - CShakeJob?: InternalCryptoBinding.CShakeJobConstructor; ChaCha20Poly1305CipherJob: InternalCryptoBinding.ChaCha20Poly1305CipherJobConstructor; CheckPrimeJob: InternalCryptoBinding.CheckPrimeJobConstructor; DHBitsJob: InternalCryptoBinding.DHBitsJobConstructor; From 847f77e25d637ad7948d7982ee8e7a3968190663 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Sat, 3 Oct 2026 11:32:22 +0300 Subject: [PATCH 07/11] crypto: improve random synchronous number generation performance Instead of creating a RandomBytesJob object, it calls CSPRNG() directly now. Assisted-by: Claude Code Signed-off-by: Mert Can Altin PR-URL: https://github.com/nodejs/node/pull/66348 Reviewed-By: Filip Skokan Reviewed-By: James M Snell Signed-off-by: Filip Skokan --- lib/internal/crypto/random.js | 12 ++----- src/crypto/crypto_random.cc | 33 +++++++++++++++++++ ...est-crypto-randomfillsync-trace-sync-io.js | 23 +++++++++++++ typings/internalBinding/crypto.d.ts | 1 + 4 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-crypto-randomfillsync-trace-sync-io.js diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index 494578e9075..3c78bd68022 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -35,6 +35,7 @@ const { CheckPrimeJob, kCryptoJobAsync, kCryptoJobSync, + randomFillSync: randomFillSyncImpl, secureBuffer, } = internalBinding('crypto'); @@ -184,16 +185,7 @@ function randomFillSync(buf, offset = 0, size) { if (size === 0) return buf; - const job = new RandomBytesJob( - kCryptoJobSync, - buf, - offset, - size); - - const err = job.run()[0]; - if (err) - throw err; - + randomFillSyncImpl(buf, offset, size); return buf; } diff --git a/src/crypto/crypto_random.cc b/src/crypto/crypto_random.cc index de4c69c6cfc..d7cba36895a 100644 --- a/src/crypto/crypto_random.cc +++ b/src/crypto/crypto_random.cc @@ -211,13 +211,46 @@ MaybeLocal CheckPrimeTraits::EncodeOutput(Environment* env, } namespace Random { +static void RandomFillSync(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(IsAnyBufferSource(args[0])); // Buffer to fill + CHECK(args[1]->IsUint32()); // Offset + CHECK(args[2]->IsUint32()); // Size + + ArrayBufferOrViewContents in(args[0]); + + const uint32_t byte_offset = args[1].As()->Value(); + const uint32_t size = args[2].As()->Value(); + CHECK_GE(byte_offset + size, byte_offset); // Overflow check. + CHECK_LE(byte_offset + size, in.size()); // Bounds check. + + env->PrintSyncTrace(); + if (ERR_peek_error() != 0) ERR_clear_error(); + if (ncrypto::CSPRNG(in.data() + byte_offset, size)) { + if (ERR_peek_error() != 0) ERR_clear_error(); + return; + } + + CryptoErrorStore errors; + errors.Capture(); + if (errors.Empty()) { + errors.Insert(NodeCryptoError::DERIVING_BITS_FAILED); + } + Local exception; + if (errors.ToException(env).ToLocal(&exception)) { + env->isolate()->ThrowException(exception); + } +} + void Initialize(Environment* env, Local target) { + SetMethod(env->context(), target, "randomFillSync", RandomFillSync); RandomBytesJob::Initialize(env, target); RandomPrimeJob::Initialize(env, target); CheckPrimeJob::Initialize(env, target); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(RandomFillSync); RandomBytesJob::RegisterExternalReferences(registry); RandomPrimeJob::RegisterExternalReferences(registry); CheckPrimeJob::RegisterExternalReferences(registry); diff --git a/test/parallel/test-crypto-randomfillsync-trace-sync-io.js b/test/parallel/test-crypto-randomfillsync-trace-sync-io.js new file mode 100644 index 00000000000..8f9461f19f3 --- /dev/null +++ b/test/parallel/test-crypto-randomfillsync-trace-sync-io.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { spawnSync } = require('child_process'); + +// randomFillSync() should be reported by --trace-sync-io when it runs after +// the first event loop turn. + +if (process.argv[2] === 'child') { + setImmediate(() => { + require('crypto').randomFillSync(Buffer.alloc(16)); + }); + return; +} + +const { stderr, status } = spawnSync(process.execPath, + ['--trace-sync-io', __filename, 'child'], + { encoding: 'utf8' }); +assert.strictEqual(status, 0); +assert.match(stderr, /WARNING: Detected use of sync API[\s\S]*randomFillSync/); diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index e12016ec916..22ee303f599 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -1002,6 +1002,7 @@ export interface CryptoBinding { privateEncrypt: InternalCryptoBinding.PublicKeyCipher; publicDecrypt: InternalCryptoBinding.PublicKeyCipher; publicEncrypt: InternalCryptoBinding.PublicKeyCipher; + randomFillSync(buf: ArrayBufferLike | ArrayBufferView, offset: number, size: number): void; resetRootCertStore(): void; secureBuffer(length: number): Uint8Array | undefined; secureHeapUsed(): bigint | undefined; From 7b21b9bccc5cb25b828ff4f0b4945e1ae71977df Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 26 Sep 2026 13:15:24 +0200 Subject: [PATCH 08/11] test: scope Blob WPT failures to big-endian The typed array Blob constructor tests expect little-endian byte order. Mark them as expected failures only on big-endian platforms, so failures on little-endian platforms remain visible. Signed-off-by: Filip Skokan Assisted-by: Claude, Codex PR-URL: https://github.com/nodejs/node/pull/66322 Reviewed-By: Daeyeon Jeong --- test/wpt/status/FileAPI/blob.cjs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/wpt/status/FileAPI/blob.cjs b/test/wpt/status/FileAPI/blob.cjs index bf40df1b842..e7d2cdfe23d 100644 --- a/test/wpt/status/FileAPI/blob.cjs +++ b/test/wpt/status/FileAPI/blob.cjs @@ -23,16 +23,19 @@ module.exports = { 'Blob-constructor-dom.window.js': { skip: 'Depends on DOM API', }, - 'Blob-constructor.any.js': { - fail: { - flaky: [ - 'Passing typed arrays as elements of the blobParts array should work.', - 'Passing a Float16Array as element of the blobParts array should work.', - 'Passing a Float64Array as element of the blobParts array should work.', - 'Passing BigInt typed arrays as elements of the blobParts array should work.', - ], + ...(os.endianness() === 'BE' ? { + 'Blob-constructor.any.js': { + fail: { + note: 'The expected bytes assume a little-endian platform', + expected: [ + 'Passing typed arrays as elements of the blobParts array should work.', + 'Passing a Float16Array as element of the blobParts array should work.', + 'Passing a Float64Array as element of the blobParts array should work.', + 'Passing BigInt typed arrays as elements of the blobParts array should work.', + ], + }, }, - }, + } : {}), 'Blob-in-worker.worker.js': { skip: 'Depends on Web Workers API', }, From 23f394af72647411122afc7155e8da3f8442d654 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 26 Sep 2026 13:22:26 +0200 Subject: [PATCH 09/11] test: honor allowed WPT promise rejections Honor allow_uncaught_exception for unhandled promise rejections in the WPT worker path. Apply setup settings before nested callbacks, preserve settings after results start, and apply promise_setup settings when its callback runs. Keep rejection failures in single_test mode. Cover the worker runner and remove the flaky expectation for the Web Locks test that deliberately rejects. Refs: https://github.com/nodejs/node/issues/59142 Signed-off-by: Filip Skokan Assisted-by: Claude, Codex PR-URL: https://github.com/nodejs/node/pull/66322 Reviewed-By: Daeyeon Jeong --- test/common/wpt/rejections.js | 65 +++++++ test/common/wpt/worker.js | 2 + .../test-common-wpt-allowed-rejections.js | 172 ++++++++++++++++++ test/wpt/status/web-locks.json | 8 - 4 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 test/common/wpt/rejections.js create mode 100644 test/parallel/test-common-wpt-allowed-rejections.js diff --git a/test/common/wpt/rejections.js b/test/common/wpt/rejections.js new file mode 100644 index 00000000000..f0cda7a4c60 --- /dev/null +++ b/test/common/wpt/rejections.js @@ -0,0 +1,65 @@ +'use strict'; + +// Node.js does not dispatch the global unhandledrejection event used by +// testharness.js. Ignore rejections only when the harness settings allow them. +module.exports = function honorAllowedRejections() { + const { setup, promise_setup, add_result_callback, add_completion_callback } = globalThis; + let configurable = true; + let allowed = false; + let singleTest = false; + const ignoreRejection = () => {}; + + // The harness ignores configuration changes once results have started. + add_result_callback(() => { configurable = false; }); + add_completion_callback(() => { configurable = false; }); + + function apply(properties) { + if (properties) { + if (Object.prototype.propertyIsEnumerable.call(properties, 'allow_uncaught_exception')) { + allowed = properties.allow_uncaught_exception; + } + if (Object.prototype.propertyIsEnumerable.call(properties, 'single_test') && properties.single_test) { + singleTest = true; + } + } + process.removeListener('unhandledRejection', ignoreRejection); + // In single_test mode the harness still fails the implicit test when + // uncaught errors are allowed. Retain Node's failure path for that mode. + if (allowed && !singleTest) { + process.on('unhandledRejection', ignoreRejection); + } + } + + globalThis.setup = function(funcOrProperties, maybeProperties) { + let func; + let properties; + if (arguments.length === 2) { + func = funcOrProperties; + properties = maybeProperties; + } else if (funcOrProperties instanceof Function) { + func = funcOrProperties; + properties = {}; + } else { + properties = funcOrProperties; + } + + // This callback runs only when setup was accepted, before any nested + // setup calls made by the test's callback can override its settings. + return setup.call(this, () => { + apply(properties); + if (func) func(); + }, properties); + }; + + globalThis.promise_setup = function(func, properties) { + if (typeof func !== 'function') { + return promise_setup.call(this, func, properties); + } + return promise_setup.call(this, () => { + // promise_setup applies settings when its queued callback executes. + // Unlike setup, it still invokes that callback after results start. + if (configurable) apply(properties); + return func(); + }, properties); + }; +}; diff --git a/test/common/wpt/worker.js b/test/common/wpt/worker.js index 4927a3a7ec7..c3a915662fa 100644 --- a/test/common/wpt/worker.js +++ b/test/common/wpt/worker.js @@ -2,6 +2,7 @@ const { runInNewContext, runInThisContext } = require('vm'); const { setFlagsFromString } = require('v8'); +const honorAllowedRejections = require('./rejections'); const { parentPort, workerData } = require('worker_threads'); const { ResourceLoader } = require(workerData.wptRunner); @@ -34,6 +35,7 @@ if (workerData.initScript) { runInThisContext(workerData.harness.code, { filename: workerData.harness.filename, }); +honorAllowedRejections(); // If there are skip patterns, wrap test functions to prevent execution of // matching tests. This must happen after testharness.js is loaded but before diff --git a/test/parallel/test-common-wpt-allowed-rejections.js b/test/parallel/test-common-wpt-allowed-rejections.js new file mode 100644 index 00000000000..2441fd29242 --- /dev/null +++ b/test/parallel/test-common-wpt-allowed-rejections.js @@ -0,0 +1,172 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { Worker } = require('worker_threads'); + +const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js'); +const harness = { + code: fs.readFileSync(harnessPath, 'utf8'), + filename: harnessPath, +}; + +async function check({ + name, + code, + allowed, + passes = 1, + message = /uncaught rejection probe/, +}) { + const label = name; + const script = { + filename: fixtures.path('wpt-rejection-probe.js'), + code, + }; + const workerData = { + testRelativePath: 'rejection-probe.any.js', + wptRunner: path.join(__dirname, '../common/wpt.js'), + wptPath: 'compression', + harness, + scriptsToRun: [script], + }; + + let result; + const statuses = []; + const worker = new Worker(path.join(__dirname, '../common/wpt/worker.js'), { + workerData, + }); + worker.on('message', (message) => { + if (message.type === 'result') { + statuses.push(message.result.status); + } else if (message.type === 'completion') { + result = message.status; + worker.terminate(); + } else { + assert.fail(`Unexpected message type: ${message.type}`); + } + }); + worker.on('error', (error) => { + result = { status: 1, message: error.message }; + }); + const finished = new Promise((resolve) => worker.once('exit', resolve)); + const timeout = setTimeout(() => { + worker.terminate(); + assert.fail(`WPT rejection probe did not finish: ${label}`); + }, common.platformTimeout(10_000)); + + try { + await finished; + } finally { + clearTimeout(timeout); + } + + assert(result, label); + assert.strictEqual(result.status, allowed ? 0 : 1, label); + if (allowed) { + assert.deepStrictEqual(statuses, Array(passes).fill(0), label); + } else { + assert.match(result.message, message, label); + } +} + +(async () => { + for (const [setup, allowed] of [ + ['', false], + ['setup({ allow_uncaught_exception: false });', false], + ['setup({ allow_uncaught_exception: true });', true], + ['setup(() => {}, { allow_uncaught_exception: true });', true], + ['setup(null, { allow_uncaught_exception: true });', true], + ['setup({ allow_uncaught_exception: true }); setup({});', true], + ['setup({ allow_uncaught_exception: true }); setup({ allow_uncaught_exception: false });', false], + ['setup(Object.defineProperty({}, "allow_uncaught_exception", { value: true }));', false], + ]) { + await check({ + name: setup || 'default settings', + code: `${setup} + const t = async_test('rejection probe'); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0);`, + allowed, + }); + } + + for (const allowed of [false, true]) { + await check({ + name: `nested setup allows rejection: ${allowed}`, + code: `setup(() => setup({ allow_uncaught_exception: ${allowed} }), + { allow_uncaught_exception: ${!allowed} }); + const t = async_test('rejection probe'); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0);`, + allowed, + }); + + await check({ + name: `late setup preserves allowance: ${allowed}`, + code: `setup({ allow_uncaught_exception: ${allowed} }); + const t = async_test('rejection probe'); + test(() => {}, 'first result'); + setup(() => assert_unreached('late setup callback'), + { allow_uncaught_exception: ${!allowed} }); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0);`, + allowed, + passes: 2, + }); + + await check({ + name: `promise_setup applies deferred allowance: ${allowed}`, + code: `promise_setup(async () => {}, { allow_uncaught_exception: ${allowed} }); + setup({ allow_uncaught_exception: ${!allowed} }); + promise_test(async () => { + Promise.reject(new Error('uncaught rejection probe')); + await new Promise(resolve => setTimeout(resolve, 0)); + }, 'rejection probe');`, + allowed, + }); + + await check({ + name: `late promise_setup preserves allowance: ${allowed}`, + code: `setup({ allow_uncaught_exception: ${allowed} }); + const t = async_test('rejection probe'); + test(() => {}, 'first result'); + promise_setup(async () => { + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(() => t.done(), 0); + }, { allow_uncaught_exception: ${!allowed} });`, + allowed, + passes: 2, + }); + + await check({ + name: `single_test still fails with allowance: ${allowed}`, + code: `setup({ single_test: true, allow_uncaught_exception: ${allowed} }); + setup({ single_test: false }); + Promise.reject(new Error('uncaught rejection probe')); + setTimeout(done, 0);`, + allowed: false, + }); + } + + for (const setup of [ + 'setup(() => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });', + 'promise_setup(async () => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });', + ]) { + await check({ + name: setup, + code: `${setup}\npromise_test(async () => {}, 'setup must succeed first');`, + allowed: false, + message: /setup failure/, + }); + } + + await check({ + name: 'unexpected rejection fails even with a permanently pending test', + code: `async_test('pending forever'); + Promise.reject(new Error('uncaught rejection probe'));`, + allowed: false, + }); +})().then(common.mustCall()); diff --git a/test/wpt/status/web-locks.json b/test/wpt/status/web-locks.json index 97a0a7900d6..f7774a38fa7 100644 --- a/test/wpt/status/web-locks.json +++ b/test/wpt/status/web-locks.json @@ -1,12 +1,4 @@ { - "held.https.any.js": { - "fail": { - "note": "Flaky on ppc, linux x64 and s390x #59142", - "flaky": [ - "Error: this uncaught rejection is expected" - ] - } - }, "idlharness.https.any.js": { "fail": { "expected": [ From 74297a06ae975f232a7cfd874c8ff6607af98909 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 30 Sep 2026 14:05:15 +0200 Subject: [PATCH 10/11] crypto: remove KEM key locking ncrypto::KEM creates a new EVP_PKEY_CTX per operation. Parameters, entropy, scratch buffers, and outputs are private to each call. ML-KEM expansion finishes during import, and operations read an immutable key. EC and X25519 retain read-only recipient keys. RSA synchronizes mutable caches internally. Remove the outer locks serializing jobs that share a KeyObject or CryptoKey. Cover cold keys and worker clones across RSA, EC, X25519, and ML-KEM, checking every resulting shared secret. Refs: https://docs.openssl.org/3.5/man7/openssl-threads/ Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66413 Reviewed-By: James M Snell --- src/crypto/crypto_kem.cc | 2 - test/parallel/test-crypto-kem-shared-key.js | 140 ++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-crypto-kem-shared-key.js diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index 85d0c65247b..10c7d96ebd3 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -117,7 +117,6 @@ KEMEncapsulateJob::KEMEncapsulateJob(Environment* env, void KEMEncapsulateJob::DoThreadPoolWork() { ncrypto::ClearErrorOnReturn clear_error_on_return; AdditionalParams* params = CryptoJob::params(); - Mutex::ScopedLock lock(params->key.mutex()); auto result = ncrypto::KEM::Encapsulate(params->key.GetAsymmetricKey()); if (result) { out_.emplace(); @@ -228,7 +227,6 @@ bool KEMDecapsulateTraits::DeriveBits(Environment* env, const KEMConfiguration& params, ByteSource* out, CryptoJobMode mode) { - Mutex::ScopedLock lock(params.key.mutex()); const auto& private_key = params.key.GetAsymmetricKey(); return DoKEMDecapsulate(env, private_key, params.ciphertext, out, mode); diff --git a/test/parallel/test-crypto-kem-shared-key.js b/test/parallel/test-crypto-kem-shared-key.js new file mode 100644 index 00000000000..da28d5b4134 --- /dev/null +++ b/test/parallel/test-crypto-kem-shared-key.js @@ -0,0 +1,140 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createPrivateKey, + createPublicKey, + encapsulate, + decapsulate, +} = require('crypto'); +const { once } = require('events'); +const { promisify } = require('util'); +const { Worker, parentPort, workerData } = require('worker_threads'); +const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); +const fixtures = require('../common/fixtures'); +const { subtle } = globalThis.crypto; + +if (!hasOpenSSL(3) && !isBoringSSL) + common.skip('requires OpenSSL >= 3 or BoringSSL'); + +const encapsulateAsync = promisify(encapsulate); +const decapsulateAsync = promisify(decapsulate); + +async function encapsulateKeys(keys) { + const operations = Array.from({ length: 4 }, () => + encapsulateAsync(keys.publicKey)); + operations.push(encapsulate(keys.publicKey)); + if (keys.publicCryptoKey) { + operations.push(subtle.encapsulateBits('ML-KEM-768', keys.publicCryptoKey)); + } + + const results = await Promise.all(operations); + for (const { sharedKey, ciphertext } of results) { + assert.strictEqual(sharedKey.byteLength, keys.sharedSecretLength); + assert.strictEqual(ciphertext.byteLength, keys.ciphertextLength); + } + return results; +} + +async function decapsulateKeys(keys, results) { + const operations = results.map(common.mustCall(async ({ sharedKey, ciphertext }) => { + const result = await decapsulateAsync(keys.privateKey, ciphertext); + assert.deepStrictEqual(result, Buffer.from(sharedKey)); + }, results.length)); + + for (const { sharedKey, ciphertext } of results) { + assert.deepStrictEqual( + decapsulate(keys.privateKey, ciphertext), Buffer.from(sharedKey)); + if (keys.privateCryptoKey) { + operations.push(subtle.decapsulateBits( + 'ML-KEM-768', keys.privateCryptoKey, ciphertext, + ).then((result) => { + assert.deepStrictEqual(Buffer.from(result), Buffer.from(sharedKey)); + })); + } + } + + await Promise.all(operations); +} + +function waitForStart(barrier, phase) { + parentPort.postMessage(phase); + Atomics.wait(barrier, phase, 0); +} + +async function runWorker() { + const barrier = new Int32Array(workerData.barrier); + waitForStart(barrier, 0); + const results = await encapsulateKeys(workerData.keys); + waitForStart(barrier, 1); + await decapsulateKeys(workerData.keys, results); +} + +async function testKeys(keys) { + const barrier = new Int32Array(new SharedArrayBuffer(8)); + const workers = Array.from({ length: 3 }, () => new Worker(__filename, { + workerData: { test: 'kem-shared-key', keys, barrier: barrier.buffer }, + })); + const exits = workers.map((worker) => once(worker, 'exit')); + + // Synchronize each phase before the backing keys have been used for it. + const ready = await Promise.all( + workers.map((worker) => once(worker, 'message'))); + for (const [phase] of ready) + assert.strictEqual(phase, 0); + const decapsulateReady = workers.map((worker) => once(worker, 'message')); + Atomics.store(barrier, 0, 1); + Atomics.notify(barrier, 0); + const results = await encapsulateKeys(keys); + + for (const [phase] of await Promise.all(decapsulateReady)) + assert.strictEqual(phase, 1); + Atomics.store(barrier, 1, 1); + Atomics.notify(barrier, 1); + await decapsulateKeys(keys, results); + + for (const [code] of await Promise.all(exits)) + assert.strictEqual(code, 0); +} + +async function runMain() { + const types = []; + if (hasOpenSSL(3)) { + types.push(['rsa', 'rsa', 256, 256]); + } + if (hasOpenSSL(3, 2) && !hasFIPS(3)) { + types.push(['ec', 'ec_p256', 32, 65], ['x25519', 'x25519', 32, 32]); + } + if (hasOpenSSL(3, 5) || isBoringSSL) { + types.push(['ml-kem-768', 'ml_kem_768', 32, 1088]); + } + + for (const [type, prefix, sharedSecretLength, ciphertextLength] of types) { + const suffix = type === 'rsa' ? '_2048' : ''; + const privateSuffix = type === 'ml-kem-768' ? '_seed_only' : suffix; + const publicKey = createPublicKey(fixtures.readKey( + `${prefix}_public${suffix}.pem`)); + const privateKey = createPrivateKey(fixtures.readKey( + `${prefix}_private${privateSuffix}.pem`)); + const keys = { + publicKey, privateKey, sharedSecretLength, ciphertextLength, + }; + if (type === 'ml-kem-768') { + keys.publicCryptoKey = publicKey.toCryptoKey( + 'ML-KEM-768', false, ['encapsulateBits']); + keys.privateCryptoKey = privateKey.toCryptoKey( + 'ML-KEM-768', false, ['decapsulateBits']); + } + await testKeys(keys); + } +} + +if (workerData?.test === 'kem-shared-key') { + runWorker().then(common.mustCall()); +} else { + runMain().then(common.mustCall()); +} From 14bd81907c7fe0f249d4a9318ecb4171baba8ba9 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 30 Sep 2026 14:05:16 +0200 Subject: [PATCH 11/11] crypto: remove key export and metadata locks Raw and JWK getters return independently owned bytes or BIGNUMs. RSA metadata snapshots public parameters and PSS restrictions. EC fallback paths reconstruct or duplicate the source key. EC PKCS8 export changes encoding flags only on its own clone. Early OpenSSL 3 key downgrading mutated shared provider fields. Current OpenSSL publishes separate legacy and provider conversion caches under internal read/write locks. Ordinary DER/PEM export, equality, signing, and key derivation already access the same keys without these locks. Remove export and metadata acquisitions, including their coverage of V8 allocation and encoding, and remove the now-unused shared mutex storage. Retain shared immutable key ownership. The concurrent reuse test checks that EC PKCS8 export leaves the original flags intact. Refs: https://github.com/nodejs/node/pull/36825 Refs: https://docs.openssl.org/3.5/man7/openssl-threads/ Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66413 Reviewed-By: James M Snell --- src/crypto/crypto_ec.cc | 4 - src/crypto/crypto_keys.cc | 35 +++----- src/crypto/crypto_keys.h | 17 ++-- src/crypto/crypto_rsa.cc | 2 - .../test-crypto-key-reuse-concurrent.js | 86 ++++++++++++++++++- 5 files changed, 103 insertions(+), 41 deletions(-) diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 1056f58d042..e1461a6079f 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -485,7 +485,6 @@ WebCryptoKeyExportStatus EC_Raw_Export(const KeyObjectData& key_data, ByteSource* out) { const auto& m_pkey = key_data.GetAsymmetricKey(); CHECK(m_pkey); - Mutex::ScopedLock lock(key_data.mutex()); if (!m_pkey.isA(KeyAlgorithm::EC)) { switch (key_data.GetKeyType()) { @@ -573,7 +572,6 @@ WebCryptoKeyExportStatus ECKeyExportTraits::DoExport( // the header but it is a somewhat silly hoop to jump through because // the header is for all practical purposes a static 26 byte sequence // where only the second byte changes. - Mutex::ScopedLock lock(key_data.mutex()); ECKeyPointer ec_key(m_pkey); if (!ec_key) return WebCryptoKeyExportStatus::FAILED; const auto group = ec_key.getGroup(); @@ -617,7 +615,6 @@ WebCryptoKeyExportStatus ECKeyExportTraits::DoExport( bool ExportJWKEcKey(Environment* env, const KeyObjectData& key, Local target) { - Mutex::ScopedLock lock(key.mutex()); const auto& m_pkey = key.GetAsymmetricKey(); DCHECK(m_pkey.isA(KeyAlgorithm::EC)); @@ -762,7 +759,6 @@ KeyObjectData ImportJWKEcKey(Environment* env, Local jwk) { bool GetEcKeyDetail(Environment* env, const KeyObjectData& key, Local target) { - Mutex::ScopedLock lock(key.mutex()); const auto& m_pkey = key.GetAsymmetricKey(); DCHECK(m_pkey.isA(KeyAlgorithm::EC)); diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index fcd8f1d1aae..fbe124ef2a6 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -188,7 +188,6 @@ KeyObjectData ImportJWKSecretKey(Environment* env, Local jwk) { static bool ExportJWKRawKey(Environment* env, const KeyObjectData& key, Local target) { - Mutex::ScopedLock lock(key.mutex()); auto result = key.GetAsymmetricKey().exportRawJwk(key.GetKeyType() == kKeyTypePrivate); if (!result) { @@ -417,7 +416,6 @@ bool KeyObjectData::ToEncodedPublicKey( return ExportJWKInner( env, addRefWithType(KeyType::kKeyTypePublic), *out, false); } else if (config.format == EVPKeyPointer::PKFormatType::RAW_PUBLIC) { - Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); const auto* algorithm = pkey.getAlgorithm(); if (algorithm == &KeyAlgorithm::EC) { @@ -472,7 +470,6 @@ bool KeyObjectData::ToEncodedPrivateKey( return ExportJWKInner( env, addRefWithType(KeyType::kKeyTypePrivate), *out, false); } else if (config.format == EVPKeyPointer::PKFormatType::RAW_PRIVATE) { - Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); const auto* algorithm = pkey.getAlgorithm(); if (algorithm == &KeyAlgorithm::EC) { @@ -498,7 +495,6 @@ bool KeyObjectData::ToEncodedPrivateKey( return Buffer::Copy(env, raw_data.get(), raw_data.size()) .ToLocal(out); } else if (config.format == EVPKeyPointer::PKFormatType::RAW_SEED) { - Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); auto raw_data = pkey.rawSeed(); if (!raw_data) { @@ -1057,9 +1053,7 @@ KeyObjectData::KeyObjectData(ByteSource symmetric_key) data_(std::make_shared(std::move(symmetric_key))) {} KeyObjectData::KeyObjectData(KeyType type, EVPKeyPointer&& pkey) - : key_type_(type), - mutex_(std::make_shared()), - data_(std::make_shared(std::move(pkey))) {} + : key_type_(type), data_(std::make_shared(std::move(pkey))) {} void KeyObjectData::Data::MemoryInfo(MemoryTracker* tracker) const { if (asymmetric_key) { @@ -1076,11 +1070,6 @@ void KeyObjectData::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("data", data_); } -Mutex& KeyObjectData::mutex() const { - if (!mutex_) mutex_ = std::make_shared(); - return *mutex_.get(); -} - KeyObjectData KeyObjectData::CreateSecret(ByteSource key) { return KeyObjectData(std::move(key)); } @@ -1475,7 +1464,6 @@ void KeyObjectHandle::RawPublicKey( const KeyObjectData& data = key->Data(); CHECK_NE(data.GetKeyType(), kKeyTypeSecret); - Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); const bool is_raw_supported = pkey.supportsRawPublic(); @@ -1503,7 +1491,6 @@ void KeyObjectHandle::RawPrivateKey( const KeyObjectData& data = key->Data(); CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); - Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); const bool is_raw_supported = pkey.supportsRawPrivate(); @@ -1531,7 +1518,6 @@ void KeyObjectHandle::ExportECPublicRaw( const KeyObjectData& data = key->Data(); CHECK_NE(data.GetKeyType(), kKeyTypeSecret); - Mutex::ScopedLock lock(data.mutex()); const auto& m_pkey = data.GetAsymmetricKey(); if (!m_pkey.isA(KeyAlgorithm::EC)) { return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); @@ -1570,7 +1556,6 @@ void KeyObjectHandle::ExportECPrivateRaw( const KeyObjectData& data = key->Data(); CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); - Mutex::ScopedLock lock(data.mutex()); const auto& m_pkey = data.GetAsymmetricKey(); if (!m_pkey.isA(KeyAlgorithm::EC)) { return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); @@ -1593,7 +1578,6 @@ void KeyObjectHandle::ExportECPrivatePkcs8( ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); const KeyObjectData& data = key->Data(); CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); - Mutex::ScopedLock lock(data.mutex()); auto encoded = ncrypto::Ec::ExportPrivatePkcs8(data.GetAsymmetricKey()); if (!encoded) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, @@ -1612,7 +1596,6 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { const KeyObjectData& data = key->Data(); CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); - Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); auto raw_data = pkey.rawSeed(); @@ -1800,7 +1783,6 @@ std::unique_ptr NativeKeyObject::CloneForMessaging() WebCryptoKeyExportStatus PKEY_SPKI_Export(const KeyObjectData& key_data, ByteSource* out) { CHECK_EQ(key_data.GetKeyType(), kKeyTypePublic); - Mutex::ScopedLock lock(key_data.mutex()); auto bio = key_data.GetAsymmetricKey().derPublicKey(); if (!bio) return WebCryptoKeyExportStatus::FAILED; *out = ByteSource::FromBIO(bio); @@ -1810,12 +1792,23 @@ WebCryptoKeyExportStatus PKEY_SPKI_Export(const KeyObjectData& key_data, WebCryptoKeyExportStatus PKEY_PKCS8_Export(const KeyObjectData& key_data, ByteSource* out) { CHECK_EQ(key_data.GetKeyType(), kKeyTypePrivate); - Mutex::ScopedLock lock(key_data.mutex()); const auto& m_pkey = key_data.GetAsymmetricKey(); + EVP_PKEY* export_key = m_pkey.get(); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // OpenSSL's provider EC PKCS8 encoders temporarily change encoding flags. + // Use an independent key so concurrent exports do not change the source. + EVPKeyPointer key_copy; + if ((m_pkey.isA(KeyAlgorithm::EC) || m_pkey.isA(KeyAlgorithm::SM2)) && + EVP_PKEY_get0_provider(m_pkey.get()) != nullptr) { + key_copy.reset(EVP_PKEY_dup(m_pkey.get())); + if (!key_copy) return WebCryptoKeyExportStatus::FAILED; + export_key = key_copy.get(); + } +#endif auto bio = BIOPointer::NewMem(); CHECK(bio); - PKCS8Pointer p8inf(EVP_PKEY2PKCS8(m_pkey.get())); + PKCS8Pointer p8inf(EVP_PKEY2PKCS8(export_key)); if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio.get(), p8inf.get())) return WebCryptoKeyExportStatus::FAILED; diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 092e94d1be4..f563a38d616 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -54,8 +54,8 @@ class KeyObjectData final : public MemoryRetainer { KeyType GetKeyType() const; - // These functions allow unprotected access to the raw key material and should - // only be used to implement cryptographic operations requiring the key. + // The key material is immutable and can be used concurrently by operations + // with separate contexts. const ncrypto::EVPKeyPointer& GetAsymmetricKey() const; const char* GetSymmetricKey() const; size_t GetSymmetricKeySize() const; @@ -64,8 +64,6 @@ class KeyObjectData final : public MemoryRetainer { SET_MEMORY_INFO_NAME(KeyObjectData) SET_SELF_SIZE(KeyObjectData) - Mutex& mutex() const; - static v8::Maybe GetPublicKeyEncodingFromJs(const v8::FunctionCallbackInfo& args, unsigned int* offset, @@ -97,11 +95,11 @@ class KeyObjectData final : public MemoryRetainer { v8::Local* out); inline KeyObjectData addRef() const { - return KeyObjectData(key_type_, mutex_, data_); + return KeyObjectData(key_type_, data_); } inline KeyObjectData addRefWithType(KeyType type) const { - return KeyObjectData(type, mutex_, data_); + return KeyObjectData(type, data_); } private: @@ -115,7 +113,6 @@ class KeyObjectData final : public MemoryRetainer { const char* default_msg); KeyType key_type_; - mutable std::shared_ptr mutex_; struct Data final : public MemoryRetainer { const ByteSource symmetric_key; @@ -131,10 +128,8 @@ class KeyObjectData final : public MemoryRetainer { }; std::shared_ptr data_; - KeyObjectData(KeyType type, - std::shared_ptr mutex, - std::shared_ptr data) - : key_type_(type), mutex_(std::move(mutex)), data_(std::move(data)) {} + KeyObjectData(KeyType type, std::shared_ptr data) + : key_type_(type), data_(std::move(data)) {} }; class KeyObjectHandle : public BaseObject { diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index 57b9e460e41..c5448ddb0eb 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -340,7 +340,6 @@ WebCryptoCipherStatus RSACipherTraits::DoCipher(Environment* env, bool ExportJWKRsaKey(Environment* env, const KeyObjectData& key, Local target) { - Mutex::ScopedLock lock(key.mutex()); const auto& m_pkey = key.GetAsymmetricKey(); const ncrypto::Rsa rsa = m_pkey; @@ -568,7 +567,6 @@ KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { bool GetRsaKeyDetail(Environment* env, const KeyObjectData& key, Local target) { - Mutex::ScopedLock lock(key.mutex()); const auto& m_pkey = key.GetAsymmetricKey(); const auto rsa = ncrypto::Rsa::PublicOnly(m_pkey); diff --git a/test/parallel/test-crypto-key-reuse-concurrent.js b/test/parallel/test-crypto-key-reuse-concurrent.js index 6c16fb88f0d..e33e72a8363 100644 --- a/test/parallel/test-crypto-key-reuse-concurrent.js +++ b/test/parallel/test-crypto-key-reuse-concurrent.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals 'use strict'; const common = require('../common'); @@ -18,6 +19,17 @@ const { once } = require('events'); const { promisify } = require('util'); const { Worker, parentPort, workerData } = require('worker_threads'); const fixtures = require('../common/fixtures'); +const { internalBinding } = require('internal/test/binding'); +const { getKeyObjectHandle } = require('internal/crypto/keys'); +const { + ECKeyExportJob, + RSAKeyExportJob, + kCryptoJobWebCrypto, + kKeyVariantRSA_SSA_PKCS1_v1_5, + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, +} = internalBinding('crypto'); const { subtle } = globalThis.crypto; const signAsync = promisify(sign); const verifyAsync = promisify(verify); @@ -28,6 +40,14 @@ const sec1 = { type: 'sec1', format: 'der' }; const spki = { type: 'spki', format: 'der' }; const iterations = 16; +function exportLegacyKey(Job, format, key, expected, ...params) { + return new Job( + kCryptoJobWebCrypto, format, getKeyObjectHandle(key), ...params) + .run().then((encoded) => { + assert.deepStrictEqual(Buffer.from(encoded), Buffer.from(expected)); + }); +} + async function exercise({ keys, peer, expected }) { // Check generated signatures and exports with independent imports that do // not warm the shared key's provider caches before its first operation. @@ -37,6 +57,9 @@ async function exercise({ keys, peer, expected }) { const referencePublic = createPublicKey({ key: expected.public, ...spki }); const referencePem = referencePrivate.export(pkcs8Pem); const referenceSec1 = referencePrivate.export(sec1); + const referenceRsaPublic = createPublicKey({ + key: expected.rsa.public, ...spki, + }); for (let i = 0; i < iterations; i++) { const data = Buffer.from(`shared key operation ${i}`); @@ -76,6 +99,40 @@ async function exercise({ keys, peer, expected }) { ); } + // Public Web Crypto exports use direct bindings. Exercise the older + // export jobs explicitly while the same keys are used by other jobs. + pending.push( + exportLegacyKey(ECKeyExportJob, kWebCryptoKeyFormatRaw, + keys.publicKey, expected.rawPublic), + exportLegacyKey(ECKeyExportJob, kWebCryptoKeyFormatSPKI, + keys.publicKey, expected.public), + exportLegacyKey(ECKeyExportJob, kWebCryptoKeyFormatPKCS8, + keys.privateKey, expected.originalPrivate), + exportLegacyKey(RSAKeyExportJob, kWebCryptoKeyFormatSPKI, + keys.rsa.publicKey, expected.rsa.public, + kKeyVariantRSA_SSA_PKCS1_v1_5), + exportLegacyKey(RSAKeyExportJob, kWebCryptoKeyFormatPKCS8, + keys.rsa.privateKey, expected.rsa.private, + kKeyVariantRSA_SSA_PKCS1_v1_5), + assert.rejects(new ECKeyExportJob( + kCryptoJobWebCrypto, kWebCryptoKeyFormatPKCS8, + getKeyObjectHandle(keys.publicKey)).run(), (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual( + err.message, 'The operation failed for an operation-specific reason'); + assert.strictEqual(err.cause?.message, 'Invalid key type'); + return true; + }), + signAsync('sha256', data, keys.rsa.privateKey).then((signature) => { + assert.strictEqual( + verify('sha256', data, referenceRsaPublic, signature), true); + return verifyAsync('sha256', data, keys.rsa.publicKey, signature) + .then((valid) => { + assert.strictEqual(valid, true); + }); + }), + ); + // Export, compare, and query metadata while jobs use this same native key // in the threadpool and other Workers. Fresh wrappers also exercise the // native metadata getters on every iteration rather than their JS caches. @@ -106,7 +163,8 @@ async function exercise({ keys, peer, expected }) { format: 'raw-public', type: 'compressed', }), Buffer.from(expected.compressedPublic)); assert.deepStrictEqual( - publicKey.export(spki), Buffer.from(expected.public)); + publicKey.export(spki), + Buffer.from(expected.originalPublic ?? expected.public)); assert.deepStrictEqual( privateKey.export(pkcs8), Buffer.from(expected.originalPrivate)); assert.strictEqual(privateKey.export(pkcs8Pem), referencePem); @@ -161,8 +219,15 @@ if (workerData?.sharedKeyTest) { // exercising concurrent first use of the shared key. CryptoKey conversion // validates the key before its first concurrent sign/derive/export calls. const privateKey = createPrivateKey({ key: input, ...pkcs8 }); - const publicKey = createPublicKey(privateKey); - const keys = { privateKey, publicKey }; + const publicKey = expected.originalPublic === undefined ? + createPublicKey(privateKey) : + createPublicKey({ key: expected.originalPublic, ...spki }); + const rsaPrivate = createPrivateKey({ key: expected.rsa.private, ...pkcs8 }); + const keys = { + privateKey, + publicKey, + rsa: { privateKey: rsaPrivate, publicKey: createPublicKey(rsaPrivate) }, + }; if (webcrypto) { keys.ecdsaPrivate = privateKey.toCryptoKey({ name: 'ECDSA', namedCurve: 'P-256', @@ -198,6 +263,8 @@ if (workerData?.sharedKeyTest) { privateKey.export(pkcs8), Buffer.from(expected.originalPrivate)); assert.deepStrictEqual(privateKey.export(sec1), createPrivateKey({ key: input, ...pkcs8 }).export(sec1)); + assert.deepStrictEqual(publicKey.export(spki), + Buffer.from(expected.originalPublic ?? expected.public)); } (async () => { @@ -207,6 +274,7 @@ if (workerData?.sharedKeyTest) { namedCurve: 'prime256v1', }); const privateJwk = reference.export({ format: 'jwk' }); + const rsaPrivate = createPrivateKey(fixtures.readKey('rsa_private_2048.pem')); const expected = { private: reference.export(pkcs8), public: publicKey.export(spki), @@ -224,6 +292,10 @@ if (workerData?.sharedKeyTest) { dsaEncoding: 'ieee-p1363', }); }), + rsa: { + private: rsaPrivate.export(pkcs8), + public: createPublicKey(rsaPrivate).export(spki), + }, }; const cryptoPeer = peer.toCryptoKey({ name: 'ECDH', namedCurve: 'P-256', @@ -237,6 +309,14 @@ if (workerData?.sharedKeyTest) { const algorithmIdentifier = der(0x30, Buffer.from( '06072a8648ce3d020106082a8648ce3d030107', 'hex')); + const compressedPublic = der( + 0x30, algorithmIdentifier, + der(0x03, Buffer.from([0]), expected.compressedPublic)); + await run(expected.private, cryptoPeer, { + ...expected, + originalPrivate: expected.private, + originalPublic: compressedPublic, + }); const ecPrivateKey = der( 0x30, Buffer.from('020101', 'hex'), der(0x04, Buffer.from(privateJwk.d, 'base64url')));