diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml index e70079530482..1c9eeeff7f25 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 df894b236cc9..afc04b5d4628 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 581b3f84883c..1672fd6f445e 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 }} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index c48d168531e5..e005fd957fd0 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 f3e8cd10e5df..9a501a551b46 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/doc/api/webcrypto.md b/doc/api/webcrypto.md index 458e5fedaabc..bc4a43dec984 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/aes.js b/lib/internal/crypto/aes.js index 3280ddff8459..dd95748dcab4 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 bf9eaedd85a6..844f9dc94a3e 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 026fa925a95a..fe37f5c9f2b6 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 45071cded217..32bbcfbf0b74 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 16ee3fda7b59..03b62faa082e 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 7f277addd872..bb857be74c84 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 62bec9660607..617fafe091d5 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,8 @@ const { const { getStringOption, jobPromise, - jobPromiseThen, normalizeHashName, - numBitsToBytes, - truncateToBitLength, + validateAlgorithm, validateMaxBufferLength, kHandle, getCachedHashId, @@ -42,7 +38,6 @@ const { } = require('internal/crypto/keys'); const { - lazyDOMException, normalizeEncoding, encodingsMap, isPendingDeprecation, @@ -259,6 +254,7 @@ Hmac.prototype._transform = Hash.prototype._transform; function asyncDigest(algorithm, data) { validateMaxBufferLength(data, 'data'); + validateAlgorithm(algorithm, 'digest'); switch (algorithm.name) { case 'SHA-1': @@ -284,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/hkdf.js b/lib/internal/crypto/hkdf.js index 8673f9b46817..bf8a296bc4ba 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/keygen.js b/lib/internal/crypto/keygen.js index 3f127a472ec1..223a09a7623b 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/mac.js b/lib/internal/crypto/mac.js index 3297297abd59..ed2be2b7fdba 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, @@ -188,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/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 3a566b369d88..621a377d47d1 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/random.js b/lib/internal/crypto/random.js index c1ab718b16f7..3c78bd680224 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 { @@ -30,6 +35,7 @@ const { CheckPrimeJob, kCryptoJobAsync, kCryptoJobSync, + randomFillSync: randomFillSyncImpl, secureBuffer, } = internalBinding('crypto'); @@ -59,6 +65,8 @@ const { const { isArrayBufferView, isAnyArrayBuffer, + isDataView, + isSharedArrayBuffer, isTypedArray, isFloat16Array, isFloat32Array, @@ -78,6 +86,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,29 +171,21 @@ 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) 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; } @@ -168,26 +197,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 +367,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/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index d8e589f9e03e..8fa3ecdff1b8 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 4f9fb7201618..3633bce9cb18 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 !== 0) 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'); @@ -776,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. @@ -813,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); @@ -1229,13 +1276,16 @@ module.exports = { toBuf, kNamedCurveAliases, - kSupportedAlgorithms, + get kSupportedAlgorithms() { + return getSupportedAlgorithms().algorithms; + }, isFips, normalizeAlgorithm, normalizeHashName, hasAnyNotIn, validateByteSource, validateKeyOps, + validateAlgorithm, jobPromise, jobPromiseThen, cleanupWebCryptoResult, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 754452f3b6e6..3d3b2cf7e94e 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -24,7 +24,6 @@ const { } = primordials; const { - CShakeJob, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, @@ -41,6 +40,7 @@ const { codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -70,8 +70,8 @@ const { normalizeHashName, numBitsToBytes, prepareWebCryptoResult, + validateAlgorithm, validateMaxBufferLength, - getOptionalByteLength, } = require('internal/crypto/util'); const { @@ -318,7 +318,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 +332,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); } @@ -1775,7 +1779,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'"; @@ -1878,6 +1881,7 @@ class SubtleCrypto { normalizeAlgorithm(algorithm, 'get shared key length'); sharedKeyLength = getSharedKeyLength(normalizedAlgorithm); normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey'); + validateAlgorithm(normalizedAdditionalAlgorithm, 'importKey'); } catch { return false; } @@ -1906,6 +1910,7 @@ function check(op, alg, length) { let normalizedAlgorithm; try { normalizedAlgorithm = normalizeAlgorithm(alg, op); + validateAlgorithm(normalizedAlgorithm, op); } catch { if (op === 'wrapKey') { return check('encrypt', alg); @@ -1919,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 645dd413f4cc..64fb0a8a226c 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -2,21 +2,22 @@ const { ArrayPrototypeIncludes, + ArrayPrototypePush, + ArrayPrototypeToSorted, MathPow, NumberParseInt, ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeToLowerCase, + TypedArrayPrototypeIncludes, } = primordials; const { lazyDOMException, kEmptyObject, } = require('internal/util'); -const { - isUint32, -} = require('internal/validators'); const { getCryptoKeyAlgorithm, getCryptoKeyType, @@ -27,9 +28,9 @@ const { validateMaxBufferLength, getBufferSourceByteLength, getBufferSourceBytes, + getHashes, isFips, kNamedCurveAliases, - numBitsToBytes, validateKmacKeyLength, } = require('internal/crypto/util'); const { @@ -42,9 +43,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( @@ -137,6 +135,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) => { @@ -168,6 +201,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}`, @@ -198,13 +232,13 @@ const dictRsaKeyGenParams = [ }, ]; -converters.RsaKeyGenParams = createDictionaryConverter( +converters.RsaKeyGenParams = createAlgorithmDictionaryConverter( 'RsaKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, ]); -converters.RsaHashedKeyGenParams = createDictionaryConverter( +converters.RsaHashedKeyGenParams = createAlgorithmDictionaryConverter( 'RsaHashedKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, @@ -217,7 +251,7 @@ converters.RsaHashedKeyGenParams = createDictionaryConverter( ], ]); -converters.RsaHashedImportParams = createDictionaryConverter( +converters.RsaHashedImportParams = createAlgorithmDictionaryConverter( 'RsaHashedImportParams', [ dictAlgorithm, [ @@ -231,7 +265,7 @@ converters.RsaHashedImportParams = createDictionaryConverter( converters.NamedCurve = converters.DOMString; -converters.EcKeyImportParams = createDictionaryConverter( +converters.EcKeyImportParams = createAlgorithmDictionaryConverter( 'EcKeyImportParams', [ dictAlgorithm, [ @@ -244,7 +278,7 @@ converters.EcKeyImportParams = createDictionaryConverter( ], ]); -converters.EcKeyGenParams = createDictionaryConverter( +converters.EcKeyGenParams = createAlgorithmDictionaryConverter( 'EcKeyGenParams', [ dictAlgorithm, [ @@ -257,7 +291,7 @@ converters.EcKeyGenParams = createDictionaryConverter( ], ]); -converters.AesKeyGenParams = createDictionaryConverter( +converters.AesKeyGenParams = createAlgorithmDictionaryConverter( 'AesKeyGenParams', [ dictAlgorithm, [ @@ -281,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]; @@ -313,15 +347,20 @@ 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 = createDictionaryConverter( +converters.RsaPssParams = createAlgorithmDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, [ @@ -334,7 +373,7 @@ converters.RsaPssParams = createDictionaryConverter( ], ]); -converters.RsaOaepParams = createDictionaryConverter( +converters.RsaOaepParams = createAlgorithmDictionaryConverter( 'RsaOaepParams', [ dictAlgorithm, [ @@ -345,7 +384,7 @@ converters.RsaOaepParams = createDictionaryConverter( ], ]); -converters.EcdsaParams = createDictionaryConverter( +converters.EcdsaParams = createAlgorithmDictionaryConverter( 'EcdsaParams', [ dictAlgorithm, [ @@ -370,7 +409,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, [ @@ -433,7 +472,7 @@ converters.JsonWebKey = createDictionaryConverter( simpleDomStringKey('priv'), ]); -converters.HkdfParams = createDictionaryConverter( +converters.HkdfParams = createAlgorithmDictionaryConverter( 'HkdfParams', [ dictAlgorithm, [ @@ -456,7 +495,7 @@ converters.HkdfParams = createDictionaryConverter( ], ]); -converters.CShakeParams = createDictionaryConverter( +converters.CShakeParams = createAlgorithmDictionaryConverter( 'CShakeParams', [ dictAlgorithm, [ @@ -480,7 +519,7 @@ converters.CShakeParams = createDictionaryConverter( ], ]); -converters.Pbkdf2Params = createDictionaryConverter( +converters.Pbkdf2Params = createAlgorithmDictionaryConverter( 'Pbkdf2Params', [ dictAlgorithm, [ @@ -503,7 +542,7 @@ converters.Pbkdf2Params = createDictionaryConverter( ], ]); -converters.AesDerivedKeyParams = createDictionaryConverter( +converters.AesDerivedKeyParams = createAlgorithmDictionaryConverter( 'AesDerivedKeyParams', [ dictAlgorithm, [ @@ -517,7 +556,7 @@ converters.AesDerivedKeyParams = createDictionaryConverter( ], ]); -converters.AesCbcParams = createDictionaryConverter( +converters.AesCbcParams = createAlgorithmDictionaryConverter( 'AesCbcParams', [ dictAlgorithm, [ @@ -530,7 +569,7 @@ converters.AesCbcParams = createDictionaryConverter( ], ]); -converters.AeadParams = createDictionaryConverter( +converters.AeadParams = createAlgorithmDictionaryConverter( 'AeadParams', [ dictAlgorithm, [ @@ -594,7 +633,7 @@ converters.AeadParams = createDictionaryConverter( ], ]); -converters.AesCtrParams = createDictionaryConverter( +converters.AesCtrParams = createAlgorithmDictionaryConverter( 'AesCtrParams', [ dictAlgorithm, [ @@ -622,7 +661,7 @@ converters.AesCtrParams = createDictionaryConverter( converters.CryptoKey = createInterfaceConverter( 'CryptoKey', isCryptoKey); -converters.EcdhKeyDeriveParams = createDictionaryConverter( +converters.EcdhKeyDeriveParams = createAlgorithmDictionaryConverter( 'EcdhKeyDeriveParams', [ dictAlgorithm, [ @@ -644,7 +683,7 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter( ], ]); -converters.ContextParams = createDictionaryConverter( +converters.ContextParams = createAlgorithmDictionaryConverter( 'ContextParams', [ dictAlgorithm, [ @@ -678,7 +717,7 @@ converters.ContextParams = createDictionaryConverter( ], ]); -converters.Argon2Params = createDictionaryConverter( +converters.Argon2Params = createAlgorithmDictionaryConverter( 'Argon2Params', [ dictAlgorithm, [ @@ -755,7 +794,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, [ @@ -769,7 +808,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) { ]); } -converters.KmacParams = createDictionaryConverter( +converters.KmacParams = createAlgorithmDictionaryConverter( 'KmacParams', [ dictAlgorithm, [ @@ -778,7 +817,7 @@ converters.KmacParams = createDictionaryConverter( 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'); @@ -792,7 +831,7 @@ converters.KmacParams = createDictionaryConverter( ], ]); -converters.KangarooTwelveParams = createDictionaryConverter( +converters.KangarooTwelveParams = createAlgorithmDictionaryConverter( 'KangarooTwelveParams', [ dictAlgorithm, [ @@ -814,7 +853,7 @@ converters.KangarooTwelveParams = createDictionaryConverter( ], ]); -converters.TurboShakeParams = createDictionaryConverter( +converters.TurboShakeParams = createAlgorithmDictionaryConverter( 'TurboShakeParams', [ dictAlgorithm, [ @@ -844,6 +883,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/node.gyp b/node.gyp index c6ae477ce8f0..f829825ceca4 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 e4988647a557..fbda94ae4b38 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_ec.cc b/src/crypto/crypto_ec.cc index 1056f58d0428..e1461a6079fc 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_hash.cc b/src/crypto/crypto_hash.cc index 2cf971347b9e..65735042ec4d 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 2bab4c24bd5c..ad6c439afaa3 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_kem.cc b/src/crypto/crypto_kem.cc index 85d0c65247bd..10c7d96ebd36 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/src/crypto/crypto_keygen.cc b/src/crypto/crypto_keygen.cc index 7e273eb57a59..1da910690b13 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 448c419fae38..fbe124ef2a6e 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, @@ -181,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) { @@ -410,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) { @@ -465,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) { @@ -491,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) { @@ -1050,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) { @@ -1069,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)); } @@ -1468,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(); @@ -1496,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(); @@ -1524,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); @@ -1563,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); @@ -1586,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, @@ -1605,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(); @@ -1793,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); @@ -1803,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; @@ -2233,7 +2233,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 +2255,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_keys.h b/src/crypto/crypto_keys.h index 092e94d1be40..f563a38d6162 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_kmac.cc b/src/crypto/crypto_kmac.cc index 292dedf6ccc8..1e29a247454c 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 65617fd76232..951b67da8874 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/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc deleted file mode 100644 index 17d6855ada7b..000000000000 --- 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 23cdcdc05123..000000000000 --- 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_random.cc b/src/crypto/crypto_random.cc index de4c69c6cfc1..d7cba36895a9 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/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index 57b9e460e41d..c5448ddb0eb2 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/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index 7a6798c78165..1b3fdcd0002f 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 e15e5958808c..92d35b844f99 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/common/wpt/rejections.js b/test/common/wpt/rejections.js new file mode 100644 index 000000000000..f0cda7a4c603 --- /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 4927a3a7ec7f..c3a915662fae 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/fixtures/crypto/kmac.js b/test/fixtures/crypto/kmac.js index ed265bb9c9f3..cc1870af2bb0 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-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index db1f6327396e..2e2014bb5f14 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 67fad15f7f39..8d58397783ed 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -1,63 +1,69 @@ 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 cshake128 = crypto.getHashes().includes('cshake128'); +const cshake256 = crypto.getHashes().includes('cshake256'); 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') }], + [cshake128, { 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) }], + [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, { 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, { 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'], - [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 +90,9 @@ export const vectors = { [false, 'KMAC128'], [false, 'KMAC256'], [kmac, { name: 'KMAC128', outputLength: 256 }], - [kmac, { name: 'KMAC128', outputLength: 255 }], + [false, { name: 'KMAC128', outputLength: 255 }], [kmac, { name: 'KMAC256', outputLength: 256 }], - [kmac, { name: 'KMAC256', outputLength: 255 }], + [false, { name: 'KMAC256', outputLength: 255 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], @@ -95,7 +101,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 +113,14 @@ 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 }], + [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'], @@ -119,7 +129,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 +141,14 @@ 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 }], + [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'], @@ -143,7 +157,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 +172,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 +200,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 +229,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 +256,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 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], @@ -274,14 +288,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 }], + [false, '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 fa3f1d365213..52dd1c95995a 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 fe256dcfb168..699b72bb11b4 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-common-wpt-allowed-rejections.js b/test/parallel/test-common-wpt-allowed-rejections.js new file mode 100644 index 000000000000..2441fd292427 --- /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/parallel/test-crypto-argon2.js b/test/parallel/test-crypto-argon2.js index 98367bab5471..127c66019f8c 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-kem-shared-key.js b/test/parallel/test-crypto-kem-shared-key.js new file mode 100644 index 000000000000..da28d5b41347 --- /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()); +} 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 a8fe94f1dd43..6024029da1b2 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', } : { @@ -250,8 +241,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 +297,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 f4af79e31b5e..c2cf0dc6cdc7 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-reuse-concurrent.js b/test/parallel/test-crypto-key-reuse-concurrent.js index 6c16fb88f0d4..e33e72a83630 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'))); diff --git a/test/parallel/test-crypto-key-store.js b/test/parallel/test-crypto-key-store.js index e777440f2c0d..d5636f350dc2 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 84ecccd55eee..bd097f04ecaf 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 ff17d03e7523..a9faf39d0ab3 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 0006e5e15355..9c6b85a1fc2a 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-pbkdf2.js b/test/parallel/test-crypto-pbkdf2.js index 7cd1206f4f02..5083396f7337 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-provider-cache.js b/test/parallel/test-crypto-provider-cache.js index 4767d47dc52a..bbfee76b615d 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-crypto-randomfill-properties.js b/test/parallel/test-crypto-randomfill-properties.js new file mode 100644 index 000000000000..07066b5d462f --- /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-crypto-randomfillsync-trace-sync-io.js b/test/parallel/test-crypto-randomfillsync-trace-sync-io.js new file mode 100644 index 000000000000..8f9461f19f38 --- /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/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index 0f033e05d6d2..e4a4a071558a 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-aead-decrypt-detached-buffer.js b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js index 8ac5125b276c..53db81496b2a 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-aes-kw-short-input.js b/test/parallel/test-webcrypto-aes-kw-short-input.js index 09fe0268ab9b..e11e19360de9 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-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js index 5f5f1761a4c7..c050ad5c26bf 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-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index 56feda2c7bdc..f8d4dbd7a832 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-argon2.js b/test/parallel/test-webcrypto-derivebits-argon2.js index 129b716c5919..a7fbed77f258 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-derivebits-cfrg.js b/test/parallel/test-webcrypto-derivebits-cfrg.js index d2fbb2998ae1..faacfb6922a9 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 fafd6a7afa58..4fb372fd3a8b 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 b81669f95de7..ee4de1c26a29 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 133292cbefff..130e2ad4cc8e 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 325c4472d802..675b740d939d 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()); } @@ -365,8 +355,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-digest.js b/test/parallel/test-webcrypto-digest.js index 47a56a912d69..89bd86bd9284 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-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js index 14cc0127188a..bdf1a1d66667 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 ae8cd7fcfdb0..8c851682432a 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 { @@ -211,8 +171,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 70594c36507e..f892755e13cc 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-export-import.js b/test/parallel/test-webcrypto-export-import.js index 385ae8c65e9d..63fdde05f1ef 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 d9c26a1674d6..ebbec86e688b 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 new file mode 100644 index 000000000000..4eff258b72b9 --- /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, getHashes, 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]), + }), getHashes().includes('cshake128')); + 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-getrandomvalues-properties.js b/test/parallel/test-webcrypto-getrandomvalues-properties.js new file mode 100644 index 000000000000..c9aca5c9276f --- /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); 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 000000000000..ce3201210eb6 --- /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 33716095751f..999e70df6a60 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-keygen.js b/test/parallel/test-webcrypto-keygen.js index 951ec07484be..30744bcdbe53 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-normalization-boundary.js b/test/parallel/test-webcrypto-normalization-boundary.js new file mode 100644 index 000000000000..20102c32e6a5 --- /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-prototype-pollution.mjs b/test/parallel/test-webcrypto-prototype-pollution.mjs index bb8a8e20d3c7..45b5dfc8389d 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-raw-format-aliases.js b/test/parallel/test-webcrypto-raw-format-aliases.js index bce0c353904d..49a09713f497 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-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index ac0b738bcd57..5829ab3685c0 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-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 8ba0257b1772..c64793390dd8 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/parallel/test-webcrypto-supports-fips.js b/test/parallel/test-webcrypto-supports-fips.js new file mode 100644 index 000000000000..e08551c65f35 --- /dev/null +++ b/test/parallel/test-webcrypto-supports-fips.js @@ -0,0 +1,137 @@ +// 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 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) { + // 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 salt = new Uint8Array(16); + 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 15874102a22b..7b98cf13c0db 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-webidl.js b/test/parallel/test-webcrypto-webidl.js index d91099aed2bf..372d2c0b51b0 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', }); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index ed0bba774fab..840244622042 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(); @@ -486,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/test/wpt/status/FileAPI/blob.cjs b/test/wpt/status/FileAPI/blob.cjs index bf40df1b8425..e7d2cdfe23d5 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', }, diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 56de2929fd1f..ff58519e2b3e 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', @@ -188,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( [ diff --git a/test/wpt/status/web-locks.json b/test/wpt/status/web-locks.json index 97a0a7900d64..f7774a38fa76 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": [ diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 67a67d90feda..22ee303f5991 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; @@ -832,7 +819,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 +974,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; @@ -1015,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;