Commit d57738e5a5d for nodejs

commit d57738e5a5d013e38da6a0b8559e830f80facb32
Author: 한국 <koreahghg@gmail.com>
Date:   Wed Sep 30 08:56:19 2026 +0900

    crypto: fix raw key export error for wrong key type

    Exporting a key in 'raw', 'raw-public' or 'raw-seed' format when the
    key type does not match (e.g. an ECDSA private key as 'raw', or an
    ML-KEM public key as 'raw-seed') fell through to the generic
    NotSupportedError. The Web Crypto and modern-algos export key steps
    require an InvalidAccessError in these cases.

    Mirror exportKeySpki() and exportKeyPkcs8(): select the exporter per
    algorithm first, then check the key type, and drop the type guards
    around the call sites in exportKeySync(). Formats an algorithm does
    not support (e.g. 'raw' for ML-DSA) still throw NotSupportedError.
    This also fixes wrapKey(), which uses the same export path.

    Assisted-by: a closed-source coding agent
    Signed-off-by: koreahghg <koreahghg@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66217
    Reviewed-By: Filip Skokan <panva.ip@gmail.com>

diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js
index 7a2ff063fa1..02318f93dc9 100644
--- a/lib/internal/crypto/webcrypto.js
+++ b/lib/internal/crypto/webcrypto.js
@@ -597,12 +597,14 @@ function exportKeyPkcs8(key) {
 }

 function exportKeyRawPublic(key, format) {
+  let exporter;
+  let exportFormat = kWebCryptoKeyFormatRaw;
   switch (getCryptoKeyAlgorithm(key).name) {
     case 'ECDSA':
       // Fall through
     case 'ECDH':
-      return require('internal/crypto/ec')
-        .ecExportKey(key, kWebCryptoKeyFormatRaw);
+      exporter = require('internal/crypto/ec').ecExportKey;
+      break;
     case 'Ed25519':
       // Fall through
     case 'Ed448':
@@ -610,74 +612,85 @@ function exportKeyRawPublic(key, format) {
     case 'X25519':
       // Fall through
     case 'X448':
-      return require('internal/crypto/cfrg')
-        .cfrgExportKey(key, kWebCryptoKeyFormatRaw);
+      exporter = require('internal/crypto/cfrg').cfrgExportKey;
+      break;
     case 'ML-DSA-44':
       // Fall through
     case 'ML-DSA-65':
       // Fall through
-    case 'ML-DSA-87': {
+    case 'ML-DSA-87':
       // ML-DSA keys don't recognize "raw"
       if (format !== 'raw-public') {
         return undefined;
       }
-      return require('internal/crypto/ml_dsa')
-        .mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
-    }
+      exporter = require('internal/crypto/ml_dsa').mlDsaExportKey;
+      break;
     case 'ML-KEM-512':
       // Fall through
     case 'ML-KEM-768':
       // Fall through
-    case 'ML-KEM-1024': {
+    case 'ML-KEM-1024':
       // ML-KEM keys don't recognize "raw"
       if (format !== 'raw-public') {
         return undefined;
       }
-      return require('internal/crypto/ml_kem')
-        .mlKemExportKey(key, kWebCryptoKeyFormatRaw);
-    }
+      exporter = require('internal/crypto/ml_kem').mlKemExportKey;
+      break;
     case 'MLKEM768-P256':
       // Fall through
     case 'MLKEM768-X25519':
       // Fall through
-    case 'MLKEM1024-P384': {
+    case 'MLKEM1024-P384':
       if (format !== 'raw-public') {
         return undefined;
       }
-      return require('internal/crypto/kem_hybrids')
-        .kemHybridExportKey(key, format);
-    }
+      exporter = require('internal/crypto/kem_hybrids').kemHybridExportKey;
+      exportFormat = format;
+      break;
     default:
       return undefined;
   }
+
+  if (getCryptoKeyType(key) !== 'public')
+    throw lazyDOMException('Key must be a public key', 'InvalidAccessError');
+
+  return exporter(key, exportFormat);
 }

 function exportKeyRawSeed(key) {
+  let exporter;
+  let exportFormat = kWebCryptoKeyFormatRaw;
   switch (getCryptoKeyAlgorithm(key).name) {
     case 'ML-DSA-44':
       // Fall through
     case 'ML-DSA-65':
       // Fall through
     case 'ML-DSA-87':
-      return require('internal/crypto/ml_dsa')
-        .mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
+      exporter = require('internal/crypto/ml_dsa').mlDsaExportKey;
+      break;
     case 'ML-KEM-512':
       // Fall through
     case 'ML-KEM-768':
       // Fall through
     case 'ML-KEM-1024':
-      return require('internal/crypto/ml_kem')
-        .mlKemExportKey(key, kWebCryptoKeyFormatRaw);
+      exporter = require('internal/crypto/ml_kem').mlKemExportKey;
+      break;
     case 'MLKEM768-P256':
       // Fall through
     case 'MLKEM768-X25519':
       // Fall through
     case 'MLKEM1024-P384':
-      return require('internal/crypto/kem_hybrids')
-        .kemHybridExportKey(key, 'raw-seed');
+      exporter = require('internal/crypto/kem_hybrids').kemHybridExportKey;
+      exportFormat = 'raw-seed';
+      break;
     default:
       return undefined;
   }
+
+  if (getCryptoKeyType(key) !== 'private')
+    throw lazyDOMException('Key must be a private key', 'InvalidAccessError');
+
+  return exporter(key, exportFormat);
 }

 function exportKeyRawSecret(key, format) {
@@ -847,21 +860,17 @@ function exportKeySync(format, key) {
       break;
     }
     case 'raw-public': {
-      if (type === 'public') {
-        result = exportKeyRawPublic(key, format);
-      }
+      result = exportKeyRawPublic(key, format);
       break;
     }
     case 'raw-seed': {
-      if (type === 'private') {
-        result = exportKeyRawSeed(key);
-      }
+      result = exportKeyRawSeed(key);
       break;
     }
     case 'raw': {
       if (type === 'secret') {
         result = exportKeyRawSecret(key, format);
-      } else if (type === 'public') {
+      } else {
         result = exportKeyRawPublic(key, format);
       }
       break;
diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js
index 4747a477804..b12a9e9dbc3 100644
--- a/test/parallel/test-webcrypto-export-import-cfrg.js
+++ b/test/parallel/test-webcrypto-export-import-cfrg.js
@@ -172,6 +172,14 @@ async function testImportPkcs8({ name, privateUsages }, extractable) {
     assert.strictEqual(
       Buffer.from(pkcs8).toString('hex'),
       keyData[name].pkcs8.toString('hex'));
+
+    for (const format of ['raw', 'raw-public']) {
+      await assert.rejects(
+        subtle.exportKey(format, key), {
+          message: 'Key must be a public key',
+          name: 'InvalidAccessError',
+        });
+    }
   } else {
     await assert.rejects(
       subtle.exportKey('pkcs8', key), {
diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js
index f4f36ceeff5..e48eda190a0 100644
--- a/test/parallel/test-webcrypto-export-import-ec.js
+++ b/test/parallel/test-webcrypto-export-import-ec.js
@@ -175,6 +175,14 @@ async function testImportPkcs8(
         message: 'Key must be a public key',
         name: 'InvalidAccessError',
       });
+
+    for (const format of ['raw', 'raw-public']) {
+      await assert.rejects(
+        subtle.exportKey(format, key), {
+          message: 'Key must be a public key',
+          name: 'InvalidAccessError',
+        });
+    }
   } else {
     await assert.rejects(
       subtle.exportKey('pkcs8', key), {
diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js
index d79b8138f8a..e078b6a6c97 100644
--- a/test/parallel/test-webcrypto-export-import-ml-dsa.js
+++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js
@@ -414,6 +414,11 @@ async function testImportRawPublic({ name, publicUsages }, extractable) {
       name: 'NotSupportedError',
       message: `Unable to export ${publicKey.algorithm.name} public key using raw format`,
     });
+
+    await assert.rejects(subtle.exportKey('raw-seed', publicKey), {
+      name: 'InvalidAccessError',
+      message: 'Key must be a private key',
+    });
   }

   await assert.rejects(
@@ -453,6 +458,11 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
   if (extractable) {
     const value = await subtle.exportKey('raw-seed', privateKey);
     assert.deepStrictEqual(Buffer.from(value), seed);
+
+    await assert.rejects(subtle.exportKey('raw-public', privateKey), {
+      name: 'InvalidAccessError',
+      message: 'Key must be a public key',
+    });
   }

   await assert.rejects(
diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js
index c6ae5e51c56..64eb43ad984 100644
--- a/test/parallel/test-webcrypto-export-import-ml-kem.js
+++ b/test/parallel/test-webcrypto-export-import-ml-kem.js
@@ -237,6 +237,11 @@ async function testImportRawPublic({ name, publicUsages }, extractable) {
       name: 'NotSupportedError',
       message: `Unable to export ${publicKey.algorithm.name} public key using raw format`,
     });
+
+    await assert.rejects(subtle.exportKey('raw-seed', publicKey), {
+      name: 'InvalidAccessError',
+      message: 'Key must be a private key',
+    });
   }

   await assert.rejects(
@@ -276,6 +281,11 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
   if (extractable) {
     const value = await subtle.exportKey('raw-seed', privateKey);
     assert.deepStrictEqual(Buffer.from(value), seed);
+
+    await assert.rejects(subtle.exportKey('raw-public', privateKey), {
+      name: 'InvalidAccessError',
+      message: 'Key must be a public key',
+    });
   }

   await assert.rejects(
diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js
index 1f8d45f02c2..84024462204 100644
--- a/test/parallel/test-webcrypto-wrap-unwrap.js
+++ b/test/parallel/test-webcrypto-wrap-unwrap.js
@@ -587,6 +587,16 @@ async function testNonByteLengthWrapUnwrap({
       name: 'InvalidAccessError',
     });

+  // Exporting a private key as 'raw' must also fail with InvalidAccessError.
+  await assert.rejects(
+    subtle.wrapKey('raw', ecKey.privateKey, wrapKey, {
+      name: 'AES-GCM',
+      iv: new Uint8Array(12),
+    }), {
+      message: 'Key must be a public key',
+      name: 'InvalidAccessError',
+    });
+
   // --- unwrapKey validation tests ---

   const ciphertext = new Uint8Array(32); // Dummy ciphertext