Commit 1a2b5114273 for nodejs

commit 1a2b5114273503f87ea7ee6b4dbd1a4aad4a6185
Author: Yagiz Nizipli <yagiz@nizipli.com>
Date:   Thu Oct 29 13:24:18 2026 -0400

    test: cover crypto digest and HMAC caches

    Check that HMAC instances duplicated from the same cached context stay
    independent when their updates are interleaved, and that warmed digest
    and HMAC caches follow FIPS mode changes without breaking an HMAC that
    is already in progress.

    Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
    Assisted-by: Grok
    PR-URL: https://github.com/nodejs/node/pull/67109
    Reviewed-By: Filip Skokan <panva.ip@gmail.com>

diff --git a/test/parallel/test-crypto-digest-cache-fips.js b/test/parallel/test-crypto-digest-cache-fips.js
new file mode 100644
index 00000000000..aa316ea15a9
--- /dev/null
+++ b/test/parallel/test-crypto-digest-cache-fips.js
@@ -0,0 +1,103 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const { isBoringSSL } = require('../common/crypto');
+if (isBoringSSL)
+  common.skip('this test requires OpenSSL provider support');
+
+const { isMainThread } = require('worker_threads');
+if (!isMainThread)
+  common.skip('crypto.setFips() is not supported in workers');
+
+const assert = require('assert');
+const crypto = require('crypto');
+
+// createHmac(), createSign(), createVerify() and the synchronous one-shot
+// functions cache digests and HMAC contexts per Environment. Changing the FIPS
+// state must invalidate those caches without breaking objects that already
+// hold a context.
+
+const key = Buffer.from('Jefe');
+const data = 'what do ya want for nothing?';
+// RFC 4231, test case 2.
+const expectedSha256 =
+  '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843';
+
+const { privateKey, publicKey } =
+  crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
+
+const operations = {
+  createHmac: (hash) => crypto.createHmac(hash, key).update(data).digest('hex'),
+  createSign: (hash) =>
+    crypto.createSign(hash).update(data).sign(privateKey, 'hex'),
+  createVerify: (hash) => {
+    const signature = crypto.sign(hash, Buffer.from(data), privateKey);
+    return crypto.createVerify(hash).update(data).verify(publicKey, signature);
+  },
+  sign: (hash) => crypto.sign(hash, Buffer.from(data), privateKey).toString('hex'),
+  verify: (hash) => {
+    const signature = crypto.sign(hash, Buffer.from(data), privateKey);
+    return crypto.verify(hash, Buffer.from(data), publicKey, signature);
+  },
+  hkdfSync: (hash) =>
+    Buffer.from(crypto.hkdfSync(hash, key, 'salt', 'info', 32)).toString('hex'),
+  pbkdf2Sync: (hash) =>
+    crypto.pbkdf2Sync(key, 'salt', 1, 32, hash).toString('hex'),
+};
+
+function runAll(hash) {
+  const results = {};
+  for (const [name, operation] of Object.entries(operations))
+    results[name] = operation(hash);
+  return results;
+}
+
+const originalFips = crypto.getFips();
+try {
+  crypto.setFips(false);
+} catch (err) {
+  if (err.code !== 'ERR_CRYPTO_FIPS_FORCED') throw err;
+  common.skip('FIPS mode cannot be disabled');
+}
+
+try {
+  // Warm the caches with a digest that FIPS mode never allows.
+  const md5 = runAll('md5');
+  assert.strictEqual(md5.createVerify, true);
+  assert.strictEqual(md5.verify, true);
+  assert.strictEqual(runAll('sha256').createHmac, expectedSha256);
+
+  const live = crypto.createHmac('sha256', key).update(data.slice(0, 10));
+
+  crypto.setFips(true);
+
+  // New operations follow the current policy instead of reusing cached
+  // implementations.
+  for (const [name, operation] of Object.entries(operations))
+    assert.throws(() => operation('md5'), Error, name);
+
+  let fipsSha256;
+  try {
+    fipsSha256 = crypto.createHash('sha256').update(data).digest('hex');
+  } catch {
+    // No FIPS provider is available, so nothing can use SHA-256 either.
+  }
+  if (fipsSha256 === undefined) {
+    assert.throws(() => operations.createHmac('sha256'), Error);
+  } else {
+    assert.strictEqual(operations.createHmac('sha256'), expectedSha256);
+  }
+
+  // An HMAC that already has its context is not affected.
+  assert.strictEqual(live.update(data.slice(10)).digest('hex'),
+                     expectedSha256);
+
+  crypto.setFips(false);
+  assert.deepStrictEqual(runAll('md5'), md5);
+  assert.strictEqual(operations.createHmac('sha256'), expectedSha256);
+} finally {
+  crypto.setFips(originalFips);
+}
diff --git a/test/parallel/test-crypto-hmac-independent-contexts.js b/test/parallel/test-crypto-hmac-independent-contexts.js
new file mode 100644
index 00000000000..8263eaf868a
--- /dev/null
+++ b/test/parallel/test-crypto-hmac-independent-contexts.js
@@ -0,0 +1,74 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const crypto = require('crypto');
+
+// HMAC instances for the same digest start from one cached context. Live
+// instances with different keys must not affect each other.
+
+// Test cases 1, 2 and 6 from RFC 4231. Case 6 uses a key longer than the block
+// size, which is hashed first.
+const vectors = [
+  {
+    key: Buffer.alloc(20, 0x0b),
+    data: 'Hi There',
+    sha256: 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7',
+    sha512: '87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde' +
+            'daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854',
+  },
+  {
+    key: Buffer.from('Jefe'),
+    data: 'what do ya want for nothing?',
+    sha256: '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843',
+    sha512: '164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea250554' +
+            '9758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737',
+  },
+  {
+    key: Buffer.alloc(131, 0xaa),
+    data: 'Test Using Larger Than Block-Size Key - Hash Key First',
+    sha256: '60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54',
+    sha512: '80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f352' +
+            '6b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598',
+  },
+];
+
+function chunks(data, size) {
+  const result = [];
+  for (let i = 0; i < data.length; i += size)
+    result.push(data.slice(i, i + size));
+  return result;
+}
+
+for (const hash of ['sha256', 'sha512']) {
+  // Create every instance before updating any of them.
+  const hmacs = vectors.map(({ key }) => crypto.createHmac(hash, key));
+  const parts = vectors.map(({ data }) => chunks(data, 5));
+
+  // Interleave the updates across instances.
+  const rounds = Math.max(...parts.map((p) => p.length));
+  for (let round = 0; round < rounds; round++) {
+    for (let i = 0; i < hmacs.length; i++) {
+      if (round < parts[i].length) hmacs[i].update(parts[i][round]);
+    }
+  }
+
+  // Finish them in reverse order.
+  for (let i = hmacs.length - 1; i >= 0; i--)
+    assert.strictEqual(hmacs[i].digest('hex'), vectors[i][hash]);
+
+  // Creating and finishing another instance does not affect one that is in
+  // progress, and later instances start from a clean context.
+  const { key, data } = vectors[1];
+  const early = crypto.createHmac(hash, key).update(data.slice(0, 10));
+  const other = crypto.createHmac(hash, vectors[0].key);
+  assert.strictEqual(other.update(vectors[0].data).digest('hex'),
+                     vectors[0][hash]);
+  assert.strictEqual(early.update(data.slice(10)).digest('hex'),
+                     vectors[1][hash]);
+  assert.strictEqual(crypto.createHmac(hash, key).update(data).digest('hex'),
+                     vectors[1][hash]);
+}