From 673cdef7d9ea5cb41b26b2a48e710fab03e116b6 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Wed, 12 Aug 2026 00:30:37 +0900 Subject: [PATCH 1/6] sqlite: check sqlite3_step() and sqlite3_reset() results Signed-off-by: semimikoh PR-URL: https://github.com/nodejs/node/pull/63319 Fixes: https://github.com/nodejs/node/issues/63311 Reviewed-By: Trivikram Kamat --- src/node_sqlite.cc | 77 +++++++++++++++++---- test/parallel/test-sqlite-statement-sync.js | 76 ++++++++++++++++++++ 2 files changed, 139 insertions(+), 14 deletions(-) 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-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()', () => { From 6a447d7cd33035920aeba734458fb36cfd9a8a37 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 11 Aug 2026 19:06:25 +0200 Subject: [PATCH 2/6] tools: fix quote escaping in `update-nixpkgs-pin.sh` Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/65166 Reviewed-By: Filip Skokan Reviewed-By: Chemi Atlow Reviewed-By: Colin Ihrig --- tools/dep_updaters/update-nixpkgs-pin.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 7d712ae426a8f34adf9797f8d04a65b551fa0032 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:40:25 -0700 Subject: [PATCH 3/6] doc: document close() error when in a sqlite callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/65090 Refs: https://github.com/nodejs/node/pull/64743 Reviewed-By: René Reviewed-By: Edy Silva --- doc/api/sqlite.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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])` From ed9f464b31ec0d81399d70b2fad804608d191f68 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 11 Aug 2026 21:34:34 +0200 Subject: [PATCH 4/6] crypto: disable non-FIPS WebCrypto paths in FIPS mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hide TurboSHAKE and KangarooTwelve when FIPS is enabled. Reject cSHAKE and KMAC parameters that require implementations outside the OpenSSL provider, while keeping provider-backed paths available. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65172 Reviewed-By: Matteo Collina Reviewed-By: Yagiz Nizipli Reviewed-By: Tobias Nießen --- lib/internal/crypto/mac.js | 4 + lib/internal/crypto/util.js | 14 ++ lib/internal/crypto/webidl.js | 51 +++-- src/crypto/crypto_hash.cc | 5 + src/crypto/crypto_kmac.cc | 2 + src/crypto/crypto_turboshake.cc | 10 + src/crypto/crypto_util.cc | 8 +- src/crypto/crypto_util.h | 1 + .../test-crypto-key-objects-to-crypto-key.js | 28 ++- test/parallel/test-webcrypto-derivekey.js | 8 +- .../test-webcrypto-digest-turboshake-rfc.js | 5 + .../test-webcrypto-digest-turboshake.js | 5 + test/parallel/test-webcrypto-digest.js | 42 ++-- test/parallel/test-webcrypto-export-import.js | 106 +++++----- .../test-webcrypto-fips-exceptions.mjs | 198 ++++++++++++++++++ test/parallel/test-webcrypto-keygen-kmac.js | 49 +++-- .../test-webcrypto-prototype-pollution.mjs | 36 ++-- .../test-webcrypto-sign-verify-kmac.js | 69 +++--- test/parallel/test-webcrypto-wrap-unwrap.js | 2 +- test/wpt/status/WebCryptoAPI.cjs | 12 +- 20 files changed, 472 insertions(+), 183 deletions(-) create mode 100644 test/parallel/test-webcrypto-fips-exceptions.mjs 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/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/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-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)); From d1f895fb76443c8bb7d279b1d282ff98fc5e3204 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:34:44 +0200 Subject: [PATCH 5/6] doc: update synopsis Signed-off-by: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/65171 Reviewed-By: Mike McCready <66998419+MikeMcC399@users.noreply.github.com> Reviewed-By: Antoine du Hamel Reviewed-By: Richard Lau Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca --- doc/api/synopsis.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) 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 From 2749388e44ac789b68e027d434cf7d9a59d62c26 Mon Sep 17 00:00:00 2001 From: Chemi Atlow Date: Tue, 11 Aug 2026 22:34:54 +0300 Subject: [PATCH 6/6] test_runner: do not tag-filter test file wrappers Under run({ testTagFilters, isolation: 'process' }) the parent process's FileTest wrappers have empty tag sets, so any include filter filtered out the wrappers themselves and no test file was ever spawned. The same applied to the single re-spawned child in watch mode with isolation 'none'. Exempt file wrappers from tag filtering: the filter is re-emitted to the child process and applied there, matching isolation 'none' results. This also removes the testTagFilterExpressions bookkeeping and the isolation-conditional assignment of testTagFilters, both of which existed only to keep the parent process from filtering its own file wrappers. The parent now always holds the canonical filter values and re-emits them to child processes. Refs: https://github.com/nodejs/node/pull/63221 Signed-off-by: atlowChemi PR-URL: https://github.com/nodejs/node/pull/65170 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Moshe Atlow --- lib/internal/test_runner/runner.js | 18 +++++++++++------- lib/internal/test_runner/test.js | 6 +++++- lib/internal/test_runner/utils.js | 18 +++++------------- test/parallel/test-runner-tags-events.mjs | 14 +++++++++++--- 4 files changed, 32 insertions(+), 24 deletions(-) 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/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); + }); });