diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 49c1260708d3..97eac5c5a139 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -309,7 +309,10 @@ added: v22.5.0 --> Closes the database connection. An exception is thrown if the database is not -open. This method is a wrapper around [`sqlite3_close_v2()`][]. +open. An [`ERR_INVALID_STATE`][] error is thrown if the method is called while +a statement is executing, such as inside a user-defined function, an aggregate +function, or an authorizer callback. This method is a wrapper around +[`sqlite3_close_v2()`][]. ### `database.loadExtension(path[, entryPoint])` diff --git a/doc/api/synopsis.md b/doc/api/synopsis.md index 24bb35e08f8c..85b2b4cf7470 100644 --- a/doc/api/synopsis.md +++ b/doc/api/synopsis.md @@ -15,15 +15,8 @@ Please see the [Command-line options][] document for more information. An example of a [web server][] written with Node.js which responds with `'Hello, World!'`: -Commands in this document start with `$` or `>` to replicate how they would -appear in a user's terminal. Do not include the `$` and `>` characters. They are -there to show the start of each command. - -Lines that don't start with `$` or `>` character show the output of the previous -command. - First, make sure to have downloaded and installed Node.js. See -[Installing Node.js via package manager][] for further install information. +[Installing Node.js][] for further install information. Now, create an empty project folder called `projects`, then navigate into it. @@ -90,5 +83,5 @@ If the browser displays the string `Hello, World!`, that indicates the server is working. [Command-line options]: cli.md#options -[Installing Node.js via package manager]: https://nodejs.org/en/download/package-manager/ +[Installing Node.js]: https://nodejs.org/en/download [web server]: http.md diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index f6f82238b549..3297297abd59 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -20,6 +20,7 @@ const { normalizeHashName, numBitsToBytes, truncateToBitLength, + validateKmacKeyLength, } = require('internal/crypto/util'); const { @@ -60,6 +61,9 @@ function normalizeKeyLength(handle, algorithm) { length = algorithm.length; } + if (algorithm.name === 'KMAC128' || algorithm.name === 'KMAC256') + validateKmacKeyLength(length); + return { handle, length }; } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index e33960e898ae..39dea84a83b6 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -47,9 +47,12 @@ const { EVP_PKEY_ML_KEM_1024, kKeyVariantAES_OCB_128: hasAesOcbMode, Argon2Job, + getFipsCrypto, KmacJob, } = internalBinding('crypto'); +const isFips = getFipsCrypto() === 1; + const { getOptionValue } = require('internal/options'); const { @@ -423,6 +426,8 @@ const conditionalAlgorithms = { 'Ed448': !process.features.openssl_is_boringssl, 'KMAC128': !!KmacJob, 'KMAC256': !!KmacJob, + 'KT128': !isFips, + 'KT256': !isFips, 'ML-DSA-44': !!EVP_PKEY_ML_DSA_44, 'ML-DSA-65': !!EVP_PKEY_ML_DSA_65, 'ML-DSA-87': !!EVP_PKEY_ML_DSA_87, @@ -435,6 +440,8 @@ const conditionalAlgorithms = { 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, }; @@ -579,6 +586,11 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } } +function validateKmacKeyLength(length) { + if ((length < 32 || length % 8) && isFips) + throw lazyDOMException('Invalid key length', 'NotSupportedError'); +} + /** * Converts a bit length to the number of bytes needed to contain it. * Non-byte lengths are rounded up to the next byte. @@ -1097,6 +1109,7 @@ module.exports = { kNamedCurveAliases, kSupportedAlgorithms, + isFips, normalizeAlgorithm, normalizeHashName, hasAnyNotIn, @@ -1106,6 +1119,7 @@ module.exports = { jobPromiseThen, cleanupWebCryptoResult, prepareWebCryptoResult, + validateKmacKeyLength, validateMaxBufferLength, numBitsToBytes, truncateToBitLength, diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index cab4a4c7631d..5661921a624c 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -9,7 +9,6 @@ const { StringPrototypeSplit, StringPrototypeStartsWith, StringPrototypeToLowerCase, - TypedArrayPrototypeGetLength, } = primordials; const { @@ -27,8 +26,10 @@ const { validateMaxBufferLength, getBufferSourceByteLength, getBufferSourceBytes, + isFips, kNamedCurveAliases, numBitsToBytes, + validateKmacKeyLength, } = require('internal/crypto/util'); const { converters: webidl, @@ -252,23 +253,24 @@ function validateCShakeOutputLength(V) { } } -function bufferSourceEqualsAscii(V, string) { - if (getBufferSourceByteLength(V) !== string.length) return false; - - const bytes = getBufferSourceBytes(V); - const length = TypedArrayPrototypeGetLength(bytes); - for (let i = 0; i < length; i++) { - if (bytes[i] !== StringPrototypeCharCodeAt(string, i)) return false; - } - return true; -} +const kCShakeFunctionNames = ['KMAC', 'TupleHash', 'ParallelHash']; function validateCShakeFunctionName(V) { - if (getBufferSourceByteLength(V) === 0 || - bufferSourceEqualsAscii(V, 'KMAC') || - bufferSourceEqualsAscii(V, 'TupleHash') || - bufferSourceEqualsAscii(V, 'ParallelHash')) { - return; + const length = getBufferSourceByteLength(V); + if (length === 0) return; + + if (!isFips) { + const bytes = getBufferSourceBytes(V); + for (let i = 0; i < kCShakeFunctionNames.length; i++) { + const functionName = kCShakeFunctionNames[i]; + if (length !== functionName.length) continue; + + let j = 0; + for (; j < length; j++) { + if (bytes[j] !== StringPrototypeCharCodeAt(functionName, j)) break; + } + if (j === length) return; + } } throw lazyDOMException( @@ -276,6 +278,14 @@ function validateCShakeFunctionName(V) { 'NotSupportedError'); } +function validateCShakeCustomization(V) { + if (isFips && getBufferSourceByteLength(V) !== 0) + throw lazyDOMException( + 'Unsupported CShakeParams customization', + 'NotSupportedError'); + validateMaxBufferLength(V, 'CShakeParams.customization', 512); +} + converters.RsaPssParams = createDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, @@ -433,7 +443,7 @@ converters.CShakeParams = createDictionaryConverter( { key: 'customization', converter: converters.BufferSource, - validator: (V, opts) => validateMaxBufferLength(V, 'CShakeParams.customization', 512), + validator: validateCShakeCustomization, }, ], ]); @@ -719,6 +729,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) { key: 'length', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), + validator: validateKmacKeyLength, }, ], ]); @@ -732,6 +743,12 @@ converters.KmacParams = createDictionaryConverter( key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), + validator: (V) => { + if ((V === 0 || V % 8) && isFips) + throw lazyDOMException( + 'Invalid KmacParams outputLength', + 'NotSupportedError'); + }, required: true, }, { diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index a5a53e44d29a..548ed006e152 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -182,7 +182,7 @@ function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, testSkipPatterns, - testTagFilterExpressions, + testTagFilters, only, hasFiles, testFiles, @@ -224,8 +224,8 @@ function getRunArgs(path, { forceExit, if (testSkipPatterns != null) { ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(runArgs, `--test-skip-pattern=${pattern}`)); } - if (testTagFilterExpressions != null) { - ArrayPrototypeForEach(testTagFilterExpressions, (value) => ArrayPrototypePush(runArgs, `--experimental-test-tag-filter=${value}`)); + if (testTagFilters != null) { + ArrayPrototypeForEach(testTagFilters, (value) => ArrayPrototypePush(runArgs, `--experimental-test-tag-filter=${value}`)); } if (only === true) { ArrayPrototypePush(runArgs, '--test-only'); @@ -284,6 +284,14 @@ class FileTest extends Test { this.timeout = null; } + willBeFilteredByTags() { + // File wrappers have no tags of their own. Tag filtering applies to the + // tests inside the file, which run in a child process (or in-process + // import); filtering the wrapper would prevent the file from running at + // all. + return false; + } + #skipReporting() { return this.#reportedChildren > 0 && (!this.error || this.error.failureType === kSubtestsFailed); } @@ -864,7 +872,6 @@ function run(options = kEmptyObject) { }); } - let testTagFilterExpressions = null; if (testTagFilters != null) { if (!ArrayIsArray(testTagFilters)) { testTagFilters = [testTagFilters]; @@ -876,10 +883,8 @@ function run(options = kEmptyObject) { testTagFilters = ArrayPrototypeMap(testTagFilters, (value, i) => ( validateAndCanonicalizeTagFilter(value, `options.testTagFilters[${i}]`) )); - testTagFilterExpressions = testTagFilters; } } - testTagFilterExpressions ??= options.testTagFilterExpressions; validateOneOf(isolation, 'options.isolation', ['process', 'none']); validateBoolean(coverage, 'options.coverage'); @@ -982,7 +987,6 @@ function run(options = kEmptyObject) { testNamePatterns, testSkipPatterns, testTagFilters, - testTagFilterExpressions, hasFiles: files != null, globPatterns, only, diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 38ce54e4ea9b..a728378182df 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -656,7 +656,7 @@ class Test extends AsyncResource { } if (isFilteringByTags) { - this.filteredByTag = !evaluateTagFilters(config.testTagFilters, this.tagSet); + this.filteredByTag = this.willBeFilteredByTags(); if (!this.filteredByTag) { for (let t = this.parent; t !== null && t.filteredByTag; t = t.parent) { t.filteredByTag = false; @@ -894,6 +894,10 @@ class Test extends AsyncResource { return false; } + willBeFilteredByTags() { + return !evaluateTagFilters(this.config.testTagFilters, this.tagSet); + } + /** * Returns a name of the test prefixed by name of all its ancestors in ascending order, separated by a space * Ex."grandparent parent test" diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 3590d3ef79b4..982fc6ef7bfe 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -273,7 +273,6 @@ function parseCommandLine() { let testNamePatterns = mapPatternFlagToRegExArray('--test-name-pattern'); let testSkipPatterns = mapPatternFlagToRegExArray('--test-skip-pattern'); let testTagFilters = null; - let testTagFilterExpressions = null; if (isChildProcessV8) { kBuiltinReporters.set('v8-serializer', 'internal/test_runner/reporter/v8-serializer'); @@ -309,19 +308,14 @@ function parseCommandLine() { const tagFilterFlag = getOptionValue('--experimental-test-tag-filter'); if (tagFilterFlag?.length > 0) { emitExperimentalWarning('Test tags'); - testTagFilterExpressions = tagFilterFlag; - // Validate at parent startup so a malformed flag fails fast, - // independent of isolation mode. Under isolation='process' the - // validated strings go unused at the parent (children re-validate - // and apply the filter); the validation here only surfaces input - // errors early. - const validated = ArrayPrototypeMap( + // File wrappers are exempt from tag filtering, so holding the filters + // in the parent is safe under any isolation mode; under + // isolation='process' the canonical values are re-emitted to the + // child processes, which apply the filter themselves. + testTagFilters = ArrayPrototypeMap( tagFilterFlag, (value, i) => validateAndCanonicalizeTagFilter(value, `--experimental-test-tag-filter[${i}]`), ); - if (isolation === 'none') { - testTagFilters = validated; - } } if (isolation === 'none') { @@ -365,7 +359,6 @@ function parseCommandLine() { const tagFilterFlag = getOptionValue('--experimental-test-tag-filter'); if (tagFilterFlag?.length > 0) { emitExperimentalWarning('Test tags'); - testTagFilterExpressions = tagFilterFlag; testTagFilters = ArrayPrototypeMap( tagFilterFlag, (value, i) => validateAndCanonicalizeTagFilter(value, `--experimental-test-tag-filter[${i}]`), @@ -433,7 +426,6 @@ function parseCommandLine() { sourceMaps, testNamePatterns, testSkipPatterns, - testTagFilterExpressions, testTagFilters, timeout, updateSnapshots, diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index dd69428c17e5..2ed356120c48 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -827,6 +827,11 @@ Maybe CShakeTraits::AdditionalConfig( 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(); diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index e5b29370768d..7bdbece96277 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -151,6 +151,8 @@ 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; diff --git a/src/crypto/crypto_turboshake.cc b/src/crypto/crypto_turboshake.cc index e53e2910c6d3..371851e69a5f 100644 --- a/src/crypto/crypto_turboshake.cc +++ b/src/crypto/crypto_turboshake.cc @@ -428,6 +428,11 @@ Maybe TurboShakeTraits::AdditionalConfig( TurboShakeConfig* params) { Environment* env = Environment::GetCurrent(args); + if (IsFipsEnabled()) { + THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); + return Nothing(); + } + // args[offset + 0] = algorithm name (string) CHECK(args[offset]->IsString()); Utf8Value algorithm_name(env->isolate(), args[offset]); @@ -535,6 +540,11 @@ Maybe KangarooTwelveTraits::AdditionalConfig( KangarooTwelveConfig* params) { Environment* env = Environment::GetCurrent(args); + if (IsFipsEnabled()) { + THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); + return Nothing(); + } + // args[offset + 0] = algorithm name (string) CHECK(args[offset]->IsString()); Utf8Value algorithm_name(env->isolate(), args[offset]); diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 166fea15da59..133a5c7f7f1d 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -146,6 +146,11 @@ bool InitCryptoOnce(Isolate* isolate) { // be part of a larger mutex for global OpenSSL state. static Mutex fips_mutex; +bool IsFipsEnabled() { + Mutex::ScopedLock fips_lock(fips_mutex); + return ncrypto::isFipsEnabled(); +} + void InitCryptoOnce() { Mutex::ScopedLock lock(per_process::cli_options_mutex); Mutex::ScopedLock fips_lock(fips_mutex); @@ -223,8 +228,7 @@ void InitCryptoOnce() { void GetFipsCrypto(const FunctionCallbackInfo& args) { Mutex::ScopedLock lock(per_process::cli_options_mutex); - Mutex::ScopedLock fips_lock(fips_mutex); - args.GetReturnValue().Set(ncrypto::isFipsEnabled() ? 1 : 0); + args.GetReturnValue().Set(IsFipsEnabled() ? 1 : 0); } void SetFipsCrypto(const FunctionCallbackInfo& args) { diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index dd7e0842a29b..c74a6e7fd507 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -66,6 +66,7 @@ constexpr T NumBitsToBytes(T bits) { // what went wrong, or std::nullopt when there was nothing to do or the // options were applied successfully. std::optional ProcessFipsOptions(); +bool IsFipsEnabled(); bool InitCryptoOnce(v8::Isolate* isolate); void InitCryptoOnce(); diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 97384bdf449d..68554ed33b31 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -88,6 +88,17 @@ inline MaybeLocal Utf8StringMaybeOneByte(Isolate* isolate, } \ } while (0) +#define RESET_OR_THROW(isolate, db, stmt, ret) \ + CHECK_ERROR_OR_THROW((isolate), (db), sqlite3_reset((stmt)), SQLITE_OK, (ret)) + +// Surface deferred SQLite errors that sqlite3_reset() returns from the prior +// sqlite3_step(). Disables the safety-net reset guard via |needs_reset|. +#define RESET_AND_CHECK(isolate, db, stmt, needs_reset, ret) \ + do { \ + (needs_reset) = false; \ + RESET_OR_THROW((isolate), (db), (stmt), (ret)); \ + } while (0) + #define THROW_AND_RETURN_ON_BAD_STATE(env, condition, msg) \ do { \ if ((condition)) { \ @@ -3020,9 +3031,20 @@ MaybeLocal StatementExecutionHelper::Run(Environment* env, bool use_big_ints) { Isolate* isolate = env->isolate(); EscapableHandleScope scope(isolate); - sqlite3_step(stmt); - int r = sqlite3_reset(stmt); - CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal()); + bool needs_reset = true; + auto reset = OnScopeLeave([&]() { + if (needs_reset) sqlite3_reset(stmt); + }); + + int step_r = sqlite3_step(stmt); + // SQLITE_ROW is accepted here (and discarded) so that run() can still be + // used on RETURNING/SELECT statements, matching prior behavior of + // ignoring the step result entirely. + if (step_r != SQLITE_DONE && step_r != SQLITE_ROW) { + THROW_ERR_SQLITE_ERROR(isolate, db); + return MaybeLocal(); + } + RESET_AND_CHECK(isolate, db, stmt, needs_reset, MaybeLocal()); sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(db->Connection()); sqlite3_int64 changes = sqlite3_changes64(db->Connection()); @@ -3096,10 +3118,16 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, bool use_big_ints) { Isolate* isolate = env->isolate(); EscapableHandleScope scope(isolate); - auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt); }); + bool needs_reset = true; + auto reset = OnScopeLeave([&]() { + if (needs_reset) sqlite3_reset(stmt); + }); int r = sqlite3_step(stmt); - if (r == SQLITE_DONE) return scope.Escape(Undefined(isolate)); + if (r == SQLITE_DONE) { + RESET_AND_CHECK(isolate, db, stmt, needs_reset, MaybeLocal()); + return scope.Escape(Undefined(isolate)); + } if (r != SQLITE_ROW) { THROW_ERR_SQLITE_ERROR(isolate, db); return MaybeLocal(); @@ -3107,7 +3135,8 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, int num_cols = sqlite3_column_count(stmt); if (num_cols == 0) { - return Undefined(isolate); + RESET_AND_CHECK(isolate, db, stmt, needs_reset, MaybeLocal()); + return scope.Escape(Undefined(isolate)); } LocalVector row_values(isolate); @@ -3116,9 +3145,9 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, return MaybeLocal(); } + Local result; if (return_arrays) { - return scope.Escape( - Array::New(isolate, row_values.data(), row_values.size())); + result = Array::New(isolate, row_values.data(), row_values.size()); } else { LocalVector keys(isolate); keys.reserve(num_cols); @@ -3131,9 +3160,12 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, } DCHECK_EQ(keys.size(), row_values.size()); - return scope.Escape(Object::New( - isolate, Null(isolate), keys.data(), row_values.data(), num_cols)); + result = Object::New( + isolate, Null(isolate), keys.data(), row_values.data(), num_cols); } + + RESET_AND_CHECK(isolate, db, stmt, needs_reset, MaybeLocal()); + return scope.Escape(result); } void StatementSync::All(const FunctionCallbackInfo& args) { @@ -3150,8 +3182,10 @@ void StatementSync::All(const FunctionCallbackInfo& args) { return; } - auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); - + bool needs_reset = true; + auto reset = OnScopeLeave([&]() { + if (needs_reset) sqlite3_reset(stmt->statement_); + }); Local result; if (StatementExecutionHelper::All(env, stmt->db_.get(), @@ -3159,6 +3193,8 @@ void StatementSync::All(const FunctionCallbackInfo& args) { stmt->return_arrays_, stmt->use_big_ints_) .ToLocal(&result)) { + RESET_AND_CHECK( + isolate, stmt->db_.get(), stmt->statement_, needs_reset, void()); args.GetReturnValue().Set(result); } } @@ -3592,7 +3628,11 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { return; } - auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); + Isolate* isolate = env->isolate(); + bool needs_reset = true; + auto reset = OnScopeLeave([&]() { + if (needs_reset) sqlite3_reset(stmt->statement_); + }); Local result; if (StatementExecutionHelper::All(env, stmt->db_.get(), @@ -3600,6 +3640,8 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { stmt->return_arrays_, stmt->use_big_ints_) .ToLocal(&result)) { + RESET_AND_CHECK( + isolate, stmt->db_.get(), stmt->statement_, needs_reset, void()); args.GetReturnValue().Set(result); } } @@ -3833,8 +3875,11 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { if (r != SQLITE_ROW) { CHECK_ERROR_OR_THROW( env->isolate(), iter->stmt_->db_.get(), r, SQLITE_DONE, void()); - sqlite3_reset(iter->stmt_->statement_); iter->done_ = true; + RESET_OR_THROW(env->isolate(), + iter->stmt_->db_.get(), + iter->stmt_->statement_, + void()); MaybeLocal values[] = {Boolean::New(isolate, true), Null(isolate)}; Local result; if (NewDictionaryInstanceNullProto(env->context(), iter_template, values) @@ -3886,6 +3931,10 @@ void StatementSyncIterator::Return(const FunctionCallbackInfo& args) { env, iter->stmt_->IsFinalized(), "statement has been finalized"); Isolate* isolate = env->isolate(); + // Unlike Next(), the reset result is intentionally ignored here: Return() + // is invoked by the language during abrupt completion (e.g. a `throw` + // inside a `for...of` body), and throwing on a deferred SQLite error + // would discard the caller's already-pending exception. sqlite3_reset(iter->stmt_->statement_); iter->done_ = true; 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 0fd3de845306..9d42b7719722 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -14,6 +14,7 @@ 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) => { @@ -142,7 +143,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - if (allowZeroKey) { + if (allowZeroKey && !fips) { const zeroKey = createSecretKey(Buffer.alloc(0)) .toCryptoKey(algorithm, true, usages); assert.strictEqual(zeroKey.algorithm.length, 0); @@ -150,6 +151,16 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const explicitZeroKey = createSecretKey(Buffer.alloc(0)) .toCryptoKey({ ...algorithm, length: 0 }, true, usages); assert.strictEqual(explicitZeroKey.algorithm.length, 0); + } else if (allowZeroKey) { + for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) { + assert.throws(() => { + createSecretKey(Buffer.alloc(0)) + .toCryptoKey(zeroAlgorithm, true, usages); + }, { + name: 'NotSupportedError', + message: 'Invalid key length', + }); + } } else { assert.throws(() => { createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); @@ -164,12 +175,15 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { message: 'Usages cannot be empty when importing a secret key.' }); - assert.throws(() => { - key.toCryptoKey({ ...algorithm, length: 0 }, true, usages); - }, { - name: 'DataError', - message: invalidLengthMessage, - }); + assert.throws( + () => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages), + allowZeroKey && fips ? { + name: 'NotSupportedError', + message: 'Invalid key length', + } : { + name: 'DataError', + message: invalidLengthMessage, + }); } function hmacVectors() { diff --git a/test/parallel/test-runner-tags-events.mjs b/test/parallel/test-runner-tags-events.mjs index 2f275cf876f9..7d1b35f7349d 100644 --- a/test/parallel/test-runner-tags-events.mjs +++ b/test/parallel/test-runner-tags-events.mjs @@ -83,9 +83,6 @@ describe('tag-bearing event payloads', { concurrency: false }, () => { }); it('test:pass fires only for selected tagged tests when filtered', async () => { - // isolation='none' so the parent applies the filter directly. Under - // 'process', the FileTest wrapper (which has no tags) would itself be - // filtered out by the include filter - same wart as --test-name-pattern. const stream = run({ files: [fixture], testTagFilters: ['db'], isolation: 'none' }); stream.on('test:fail', common.mustNotCall()); // 3 db-tagged tests pass + the db suite itself. @@ -93,4 +90,15 @@ describe('tag-bearing event payloads', { concurrency: false }, () => { // eslint-disable-next-line no-unused-vars for await (const _ of stream); }); + + it('filtering under process isolation runs the file and filters inside it', async () => { + // The FileTest wrapper has no tags and must not be filtered out itself; + // the filter is re-emitted to the child process and applied there. + const stream = run({ files: [fixture], testTagFilters: ['db'], isolation: 'process' }); + stream.on('test:fail', common.mustNotCall()); + // 3 db-tagged tests pass + the db suite itself. + stream.on('test:pass', common.mustCall(4)); + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + }); }); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index cf0e4daa45ca..a55e19fd14f3 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -79,6 +79,28 @@ suite('StatementSync.prototype.get()', () => { message: /statement has been finalized/, }); }); + + test('surfaces a deferred SQLite error from reset() even though a row was already built', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec(` + PRAGMA foreign_keys = ON; + PRAGMA defer_foreign_keys = ON; + CREATE TABLE parent(id INTEGER PRIMARY KEY); + CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id)); + `); + // The FK check is deferred until the implicit transaction commits, which + // happens inside reset() here because RETURNING leaves the statement's + // VDBE running after the row is produced. + const stmt = db.prepare( + 'INSERT INTO child (parent_id) VALUES (999) RETURNING id' + ); + t.assert.throws(() => { + stmt.get(); + }, { + code: 'ERR_SQLITE_ERROR', + message: /FOREIGN KEY constraint failed/, + }); + }); }); suite('StatementSync.prototype.all()', () => { @@ -144,6 +166,25 @@ suite('StatementSync.prototype.all()', () => { message: /statement has been finalized/, }); }); + + test('surfaces a deferred SQLite error from reset() even though the array was already built', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec(` + PRAGMA foreign_keys = ON; + PRAGMA defer_foreign_keys = ON; + CREATE TABLE parent(id INTEGER PRIMARY KEY); + CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id)); + `); + const stmt = db.prepare( + 'INSERT INTO child (parent_id) VALUES (999) RETURNING id' + ); + t.assert.throws(() => { + stmt.all(); + }, { + code: 'ERR_SQLITE_ERROR', + message: /FOREIGN KEY constraint failed/, + }); + }); }); suite('StatementSync.prototype.iterate()', () => { @@ -322,6 +363,41 @@ suite('StatementSync.prototype.iterate()', () => { message: /statement has been finalized/, }); }); + + test('does not replay results after the iterator is naturally exhausted', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE test(key TEXT); + INSERT INTO test (key) VALUES ('key1'); + `); + const it = db.prepare('SELECT * FROM test').iterate(); + t.assert.deepStrictEqual(it.next(), { + __proto__: null, done: false, value: { __proto__: null, key: 'key1' }, + }); + t.assert.deepStrictEqual( + it.next(), { __proto__: null, done: true, value: null }); + // Calling next() again on an exhausted iterator must keep reporting + // done, not silently reset the statement and replay from row 1. + t.assert.deepStrictEqual( + it.next(), { __proto__: null, done: true, value: null }); + }); + + test('propagates a pending exception when the loop body throws mid-iteration', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE test(key TEXT); + INSERT INTO test (key) VALUES ('key1'); + INSERT INTO test (key) VALUES ('key2'); + `); + const stmt = db.prepare('SELECT * FROM test'); + const userError = new Error('boom'); + t.assert.throws(() => { + // eslint-disable-next-line no-unused-vars + for (const row of stmt.iterate()) { + throw userError; + } + }, (err) => err === userError); + }); }); suite('StatementSync.prototype.run()', () => { diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index 392e3e998afe..631951c3f767 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -284,7 +284,7 @@ const fips4 = hasFIPS(4); })().then(common.mustCall()); } -if (hasOpenSSL(3)) { +if (hasOpenSSL(3) && !hasFIPS()) { (async () => { const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; const usages = ['sign']; @@ -326,11 +326,7 @@ if (hasOpenSSL(3)) { name: 'KMAC128', outputLength: 256, }, derived, new Uint8Array()); - if (fips4) { - await assert.rejects(signature, { name: 'OperationError' }); - } else { - assert.strictEqual((await signature).byteLength, 32); - } + assert.strictEqual((await signature).byteLength, 32); } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-digest-turboshake-rfc.js b/test/parallel/test-webcrypto-digest-turboshake-rfc.js index 271fde76ab23..462204654121 100644 --- a/test/parallel/test-webcrypto-digest-turboshake-rfc.js +++ b/test/parallel/test-webcrypto-digest-turboshake-rfc.js @@ -5,6 +5,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasFIPS } = require('../common/crypto'); + +if (hasFIPS()) + common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode'); + const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-digest-turboshake.js b/test/parallel/test-webcrypto-digest-turboshake.js index a6f4b2d50f94..bd09362caa25 100644 --- a/test/parallel/test-webcrypto-digest-turboshake.js +++ b/test/parallel/test-webcrypto-digest-turboshake.js @@ -5,6 +5,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasFIPS } = require('../common/crypto'); + +if (hasFIPS()) + common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode'); + const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 447948212bc1..7c0ef7668c61 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -10,7 +10,7 @@ const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); const { hasOpenSSL, hasFIPS } = require('../common/crypto'); -const fips4 = hasFIPS(4); +const fips = hasFIPS(); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -291,6 +291,8 @@ if (getHashes().includes('shake128')) { message: 'Unsupported CShakeParams functionName', }); + if (fips) return; + await assert.rejects( subtle.digest( { @@ -398,35 +400,19 @@ if (getHashes().includes('shake128')) { 'ca6f88db415829', }, ]) { - const digest = subtle.digest(algorithm, data); - if (fips4) { - await assert.rejects( - digest, - (err) => err.name === 'OperationError' && - err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'); - } else { - assert.strictEqual( - Buffer.from(await digest).toString('hex'), - expected); - } + assert.strictEqual( + Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), + expected); } - const truncatedDigest = subtle.digest( + const truncated = Buffer.from(await subtle.digest( { ...nistCShakeSample1.algorithm, outputLength: 255 }, - nistCShakeSample1.data); - if (fips4) { - await assert.rejects( - truncatedDigest, - (err) => err.name === 'OperationError' && - err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'); - } else { - const truncated = Buffer.from(await truncatedDigest); - 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]); - } + 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]); })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 9f6b2125d8ed..30d1ea622fae 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -286,66 +286,68 @@ if (hasOpenSSL(3)) { { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); { - const importedZeroImplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - name, - true, - ['sign', 'verify']); - const importedZeroImplicitRaw = - await subtle.exportKey('raw-secret', importedZeroImplicit); - assert.strictEqual(importedZeroImplicit.algorithm.length, 0); - assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); + if (getFips() !== 1) { + const importedZeroImplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array(), + name, + true, + ['sign', 'verify']); + const importedZeroImplicitRaw = + await subtle.exportKey('raw-secret', importedZeroImplicit); + assert.strictEqual(importedZeroImplicit.algorithm.length, 0); + assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); - const importedZeroExplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - { name, length: 0 }, - true, - ['sign', 'verify']); - const importedZeroExplicitRaw = - await subtle.exportKey('raw-secret', importedZeroExplicit); - assert.strictEqual(importedZeroExplicit.algorithm.length, 0); - assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); - - await assert.rejects( - subtle.importKey( + const importedZeroExplicit = await subtle.importKey( 'raw-secret', - new Uint8Array([0xff]), + new Uint8Array(), { name, length: 0 }, true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); - - const generated = await subtle.generateKey( - { name, length: 9 }, - true, - ['sign', 'verify']); - const generatedRaw = await subtle.exportKey('raw-secret', generated); - assert.strictEqual(generated.algorithm.length, 9); - assert.strictEqual(generatedRaw.byteLength, 2); - assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + ['sign', 'verify']); + const importedZeroExplicitRaw = + await subtle.exportKey('raw-secret', importedZeroExplicit); + assert.strictEqual(importedZeroExplicit.algorithm.length, 0); + assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 0 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + + const generated = await subtle.generateKey( + { name, length: 9 }, + true, + ['sign', 'verify']); + const generatedRaw = await subtle.exportKey('raw-secret', generated); + assert.strictEqual(generated.algorithm.length, 9); + assert.strictEqual(generatedRaw.byteLength, 2); + assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); - const importedExplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff]), - { name, length: 9 }, - true, - ['sign', 'verify']); - const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); - assert.strictEqual(importedExplicit.algorithm.length, 9); - assert.deepStrictEqual( - new Uint8Array(importedExplicitRaw), - new Uint8Array([0xff, 0x80])); - - await assert.rejects( - subtle.importKey( + const importedExplicit = await subtle.importKey( 'raw-secret', - new Uint8Array([0xff]), + new Uint8Array([0xff, 0xff]), { name, length: 9 }, true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); + ['sign', 'verify']); + const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); + assert.strictEqual(importedExplicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(importedExplicitRaw), + new Uint8Array([0xff, 0x80])); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 9 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + } } } diff --git a/test/parallel/test-webcrypto-fips-exceptions.mjs b/test/parallel/test-webcrypto-fips-exceptions.mjs new file mode 100644 index 000000000000..ecc0f3c6989d --- /dev/null +++ b/test/parallel/test-webcrypto-fips-exceptions.mjs @@ -0,0 +1,198 @@ +// Flags: --expose-internals + +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { createRequire } from 'node:module'; +import { hasFIPS } from '../common/crypto.js'; + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!hasFIPS(3)) + common.skip('requires OpenSSL >= 3 in FIPS mode'); + +const require = createRequire(import.meta.url); +const { internalBinding } = require('internal/test/binding'); +const { getCryptoKeyHandle } = require('internal/crypto/keys'); +const { + CShakeJob, + KangarooTwelveJob, + KmacJob, + TurboShakeJob, + kCryptoJobWebCrypto, + kSignJobModeSign, +} = internalBinding('crypto'); +const { subtle } = globalThis.crypto; +const { SubtleCrypto } = globalThis; +const data = new Uint8Array(); + +async function assertFipsException(operation, algorithm, fn, message) { + assert.strictEqual(SubtleCrypto.supports(operation, algorithm), false); + await assert.rejects(fn(), { + name: 'NotSupportedError', + message, + }); +} + +for (const algorithm of [ + { name: 'turboshake128', outputLength: 128 }, + { name: 'TurboSHAKE256', outputLength: 256 }, + { name: 'KT128', outputLength: 128 }, + { name: 'KT256', outputLength: 256, customization: data }, +]) { + await assertFipsException( + 'digest', + algorithm, + () => subtle.digest(algorithm, data), + 'Unrecognized algorithm name'); +} + +for (const createJob of [ + () => new TurboShakeJob( + kCryptoJobWebCrypto, 'TurboSHAKE128', 0x1f, 16, data), + () => new KangarooTwelveJob( + kCryptoJobWebCrypto, 'KT128', undefined, 16, data), + () => new CShakeJob( + kCryptoJobWebCrypto, + 'cSHAKE128', + data, + Buffer.from('KMAC'), + undefined, + 128), +]) { + assert.throws(createJob, { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + message: 'Unsupported crypto operation', + }); +} + +const emptyCShake = { + name: 'cSHAKE128', + outputLength: 256, + customization: data, + functionName: data, +}; +assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true); + +for (const length of [1, 513]) { + const algorithm = { + name: 'cSHAKE128', + outputLength: 256, + customization: new Uint8Array(length), + }; + await assertFipsException( + 'digest', + algorithm, + () => subtle.digest(algorithm, data), + 'Unsupported CShakeParams customization'); +} + +const functionName = { + name: 'cSHAKE256', + outputLength: 256, + functionName: Buffer.from('KMAC'), +}; +await assertFipsException( + 'digest', + functionName, + () => subtle.digest(functionName, data), + 'Unsupported CShakeParams functionName'); + +const bothCShakeParams = { + ...functionName, + customization: new Uint8Array(1), +}; +await assertFipsException( + 'digest', + bothCShakeParams, + () => subtle.digest(bothCShakeParams, data), + 'Unsupported CShakeParams customization'); + +for (const length of [0, 24, 33]) { + const algorithm = { name: 'KMAC128', length }; + await assertFipsException( + 'generateKey', + algorithm, + () => subtle.generateKey(algorithm, false, ['sign', 'verify']), + 'Invalid key length'); + await assertFipsException( + 'importKey', + algorithm, + () => subtle.importKey( + 'raw-secret', + new Uint8Array(length === 24 ? 4 : Math.ceil(length / 8)), + algorithm, + false, + ['sign', 'verify']), + 'Invalid key length'); +} + +const minimumKmac = { name: 'KMAC128', length: 32 }; +assert.strictEqual( + SubtleCrypto.supports('generateKey', minimumKmac), true); +assert.strictEqual( + SubtleCrypto.supports('importKey', minimumKmac), true); +await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(5), + minimumKmac, + false, + ['sign', 'verify']), { + name: 'DataError', + message: 'Invalid key length', + }); + +for (const length of [0, 3]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(length), + 'KMAC128', + false, + ['sign', 'verify']), { + name: 'NotSupportedError', + message: 'Invalid key length', + }); +} +const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(4), + 'KMAC128', + false, + ['sign', 'verify']); +assert.strictEqual(key.algorithm.length, 32); + +await assert.rejects( + new KmacJob( + kCryptoJobWebCrypto, + kSignJobModeSign, + getCryptoKeyHandle(key), + 'KMAC128', + undefined, + 32, + 9, + data, + undefined).run(), + (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual(err.cause?.code, 'ERR_CRYPTO_OPERATION_FAILED'); + return true; + }); + +const minimumOutput = { name: 'KMAC128', outputLength: 8 }; +assert.strictEqual(SubtleCrypto.supports('sign', minimumOutput), true); +assert.strictEqual(SubtleCrypto.supports('verify', minimumOutput), true); +for (const outputLength of [0, 9]) { + const algorithm = { name: 'KMAC128', outputLength }; + await assertFipsException( + 'sign', + algorithm, + () => subtle.sign(algorithm, key, data), + 'Invalid KmacParams outputLength'); + await assertFipsException( + 'verify', + algorithm, + () => subtle.verify(algorithm, key, data, data), + 'Invalid KmacParams outputLength'); +} diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index c1125412892e..33716095751f 100644 --- a/test/parallel/test-webcrypto-keygen-kmac.js +++ b/test/parallel/test-webcrypto-keygen-kmac.js @@ -5,7 +5,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); @@ -13,38 +13,45 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { subtle } = globalThis.crypto; +const fips = hasFIPS(); const usages = ['sign', 'verify']; async function test(name, length) { - length ??= name === 'KMAC128' ? 128 : 256; - const key = await subtle.generateKey({ - name, - length, - }, true, usages); - - assert(key); - assert(isCryptoKey(key)); - - assert.strictEqual(key.type, 'secret'); - assert.strictEqual(key.toString(), '[object CryptoKey]'); - assert.strictEqual(key.extractable, true); - assert.deepStrictEqual(key.usages, usages); - assert.strictEqual(key.algorithm.name, name); - assert.strictEqual(key.algorithm.length, length); - assert.strictEqual(key.algorithm, key.algorithm); - assert.strictEqual(key.usages, key.usages); - - const raw = await subtle.exportKey('raw-secret', key); - assert.strictEqual(raw.byteLength, Math.ceil(length / 8)); + const expectedLength = length ?? (name === 'KMAC128' ? 128 : 256); + const algorithm = { name }; + if (length !== undefined) + algorithm.length = length; + + if (fips && length !== undefined && + (length < 32 || length % 8 !== 0)) return; + + const generatedKey = await subtle.generateKey(algorithm, true, usages); + + assert(generatedKey); + assert(isCryptoKey(generatedKey)); + + assert.strictEqual(generatedKey.type, 'secret'); + assert.strictEqual(generatedKey.toString(), '[object CryptoKey]'); + assert.strictEqual(generatedKey.extractable, true); + assert.deepStrictEqual(generatedKey.usages, usages); + assert.strictEqual(generatedKey.algorithm.name, name); + assert.strictEqual(generatedKey.algorithm.length, expectedLength); + assert.strictEqual(generatedKey.algorithm, generatedKey.algorithm); + assert.strictEqual(generatedKey.usages, generatedKey.usages); + + const raw = await subtle.exportKey('raw-secret', generatedKey); + assert.strictEqual(raw.byteLength, Math.ceil(expectedLength / 8)); } const kTests = [ ['KMAC128', 0], + ['KMAC128', 32], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], ['KMAC256', 0], + ['KMAC256', 32], ['KMAC256', 128], ['KMAC256', 256], ['KMAC256'], diff --git a/test/parallel/test-webcrypto-prototype-pollution.mjs b/test/parallel/test-webcrypto-prototype-pollution.mjs index 8ca65ba376f4..9f0b41ac10bb 100644 --- a/test/parallel/test-webcrypto-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-prototype-pollution.mjs @@ -142,16 +142,24 @@ if (supports('digest', 'cSHAKE128')) { outputLength: 256, customization: new Uint8Array([1, 2, 3]), }; - const expected = new Uint8Array(await subtle.digest(algorithm, data)); - const plain = new Uint8Array( - await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data)); - assert.notDeepStrictEqual(expected, plain); - await withPoisoned(poisonTypedArrayByteLength(0), - common.mustCall(async () => { - assert.deepStrictEqual( - new Uint8Array(await subtle.digest(algorithm, data)), - expected); - })); + if (getFips() === 1) { + await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() => + assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams customization', + }))); + } else { + const expected = new Uint8Array(await subtle.digest(algorithm, data)); + const plain = new Uint8Array( + await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data)); + assert.notDeepStrictEqual(expected, plain); + await withPoisoned(poisonTypedArrayByteLength(0), + common.mustCall(async () => { + assert.deepStrictEqual( + new Uint8Array(await subtle.digest(algorithm, data)), + expected); + })); + } } } @@ -291,17 +299,17 @@ await withPoisoned( // enforceRangeOptions(): [EnforceRange] uses IntegerPart, not round-half-even. { const key = await subtle.importKey( - 'raw-secret', new Uint8Array(4), 'PBKDF2', false, ['deriveBits']); + 'raw-secret', new Uint8Array(32), 'PBKDF2', false, ['deriveBits']); const pbkdf2 = (iterations) => subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations, - }, key, 8); + }, key, 112); - const expected = new Uint8Array(await pbkdf2(1)); + const expected = new Uint8Array(await pbkdf2(1000)); await withPoisoned(inherited('clamp', true), common.mustCall(async () => { - assert.deepStrictEqual(new Uint8Array(await pbkdf2(1.5)), expected); + assert.deepStrictEqual(new Uint8Array(await pbkdf2(1000.5)), expected); })); } diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index 160067b9b760..ac0b738bcd57 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -12,15 +12,24 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { subtle } = globalThis.crypto; +const fips = hasFIPS(); const fips4 = hasFIPS(4); const vectors = require('../fixtures/crypto/kmac')(); -function isFipsUnsupported(err) { +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 || @@ -206,9 +215,13 @@ 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), isFipsUnsupported)); - variations.push(assert.rejects(testSign(vector), isFipsUnsupported)); + variations.push(assert.rejects( + testVerify(vector), isFipsProviderUnsupported)); + variations.push(assert.rejects( + testSign(vector), isFipsProviderUnsupported)); } else { variations.push(testVerify(vector)); variations.push(testSign(vector)); @@ -227,24 +240,18 @@ async function testSign({ algorithm, ['sign', 'verify']); const algorithm = { name: 'KMAC128', - outputLength: fips4 ? 16 : 9, + outputLength: fips ? 16 : 9, customization: new Uint8Array(), }; const data = new Uint8Array([1, 2, 3]); - if (fips4) { - await assert.rejects( - subtle.sign({ ...algorithm, outputLength: 9 }, key, data), - isFipsUnsupported); - } - const signature = await subtle.sign(algorithm, key, data); assert.strictEqual(signature.byteLength, 2); - if (!fips4) + if (!fips) assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); assert(await subtle.verify(algorithm, key, signature, data)); - if (fips4) { + if (fips) { const signature128 = await subtle.sign({ ...algorithm, outputLength: 128, @@ -264,25 +271,23 @@ async function testSign({ algorithm, } const invalidSignature = new Uint8Array(signature); - if (fips4) + if (fips) invalidSignature[0] ^= 0b00000001; else invalidSignature[1] |= 0b00000001; assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); - const nonByteKey = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff, 0xff, 0xff]), - { name: 'KMAC128', length: 25 }, - false, - ['sign', 'verify']); - const nonByteKeySignature = subtle.sign({ - ...algorithm, - outputLength: 16, - }, nonByteKey, data); - if (fips4) { - await assert.rejects(nonByteKeySignature, isFipsUnsupported); - } else { + if (!fips) { + const nonByteKey = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff, 0xff, 0xff]), + { name: 'KMAC128', length: 25 }, + false, + ['sign', 'verify']); + const nonByteKeySignature = subtle.sign({ + ...algorithm, + outputLength: 16, + }, nonByteKey, data); const result = await nonByteKeySignature; assert.strictEqual(result.byteLength, 2); assert(await subtle.verify({ @@ -293,6 +298,8 @@ async function testSign({ algorithm, })().then(common.mustCall()); (async function() { + if (fips) return; + const data = new Uint8Array([1, 2, 3]); for (const name of ['KMAC128', 'KMAC256']) { @@ -311,13 +318,9 @@ async function testSign({ algorithm, const algorithm = { name, outputLength: 256 }; const signature = subtle.sign(algorithm, key, data); - if (fips4) { - await assert.rejects(signature, isFipsUnsupported); - } else { - const result = await signature; - assert.strictEqual(result.byteLength, 32); - assert(await subtle.verify(algorithm, key, result, data)); - } + const result = await signature; + assert.strictEqual(result.byteLength, 32); + assert(await subtle.verify(algorithm, key, result, data)); } } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 342eae0859e4..c2c089ed0ffa 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -485,7 +485,7 @@ async function testNonByteLengthWrapUnwrap({ implicitAlgorithm: hmacAlgorithm, }); - if (hasOpenSSL(3)) { + if (hasOpenSSL(3) && getFips() !== 1) { const kmacAlgorithm = { name: 'KMAC128' }; const kmacKey = await subtle.importKey( 'raw-secret', diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 8dfd37b33b34..ae73e17f5bb9 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -117,6 +117,12 @@ if (hasFIPS(3)) { ]); } +if (hasFIPS()) { + skip( + 'digest/kangarootwelve.tentative.https.any.js', + 'digest/turboshake.tentative.https.any.js'); +} + // OpenSSL 3.0 through 3.3 reject SHA-1 signature generation in FIPS mode. // OpenSSL 3.4 permits it for legacy use cases while marking the operation as // non-approved through a per-operation FIPS indicator. Node does not expose @@ -171,8 +177,10 @@ if (hasFIPS(4)) { ]); } -skipSubtests( - ['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]); +if (!hasFIPS()) { + skipSubtests( + ['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]); +} function assertNoOverlap(fileSkips, subtestSkips) { const subtestSkipFiles = new Set(Object.keys(subtestSkips)); diff --git a/tools/dep_updaters/update-nixpkgs-pin.sh b/tools/dep_updaters/update-nixpkgs-pin.sh index ba1f0e6efcc0..65c6ff2465f1 100755 --- a/tools/dep_updaters/update-nixpkgs-pin.sh +++ b/tools/dep_updaters/update-nixpkgs-pin.sh @@ -86,7 +86,7 @@ nix-instantiate -I "nixpkgs=$NIXPKGS_PIN_FILE" --eval --strict --json -E " }: { - # "default" OpenSSL release line, should be kept in sync with the bundled version: + # \"default\" OpenSSL release line, should be kept in sync with the bundled version: openssl = pkgs.\(.default); # Other OpenSSL variants we want to test for: