From f0b441230a936a79a5557762a18a1c4d1613eba2 Mon Sep 17 00:00:00 2001
From: Filip Skokan <panva.ip@gmail.com>
Date: Sun, 6 Oct 2024 20:09:02 +0200
Subject: [PATCH] crypto: add KeyObject.prototype.toCryptoKey
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PR-URL: https://github.com/nodejs/node/pull/55262
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
---
 doc/api/crypto.md                             |  19 ++
 lib/internal/crypto/aes.js                    |   7 +-
 lib/internal/crypto/cfrg.js                   |   7 +-
 lib/internal/crypto/ec.js                     |   7 +-
 lib/internal/crypto/keys.js                   | 159 +++++++++++++++
 lib/internal/crypto/mac.js                    |  20 +-
 lib/internal/crypto/rsa.js                    |   7 +-
 lib/internal/crypto/webcrypto.js              |  61 +-----
 .../test-crypto-key-objects-to-crypto-key.js  | 182 ++++++++++++++++++
 test/parallel/test-webcrypto-export-import.js |   5 +
 10 files changed, 415 insertions(+), 59 deletions(-)
 create mode 100644 test/parallel/test-crypto-key-objects-to-crypto-key.js

diff --git a/doc/api/crypto.md b/doc/api/crypto.md
index a774507171d431..e836e6ced8c226 100644
--- a/doc/api/crypto.md
+++ b/doc/api/crypto.md
@@ -2134,6 +2134,24 @@ added: v11.6.0
 For secret keys, this property represents the size of the key in bytes. This
 property is `undefined` for asymmetric keys.
 
+### `keyObject.toCryptoKey(algorithm, extractable, keyUsages)`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+<!--lint disable maximum-line-length remark-lint-->
+
+* `algorithm`: {AlgorithmIdentifier|RsaHashedImportParams|EcKeyImportParams|HmacImportParams}
+
+<!--lint enable maximum-line-length remark-lint-->
+
+* `extractable`: {boolean}
+* `keyUsages`: {string\[]} See [Key usages][].
+* Returns: {CryptoKey}
+
+Converts a `KeyObject` instance to a `CryptoKey`.
+
 ### `keyObject.type`
 
 <!-- YAML
@@ -6084,6 +6102,7 @@ See the [list of SSL OP Flags][] for details.
 [FIPS provider from OpenSSL 3]: https://www.openssl.org/docs/man3.0/man7/crypto.html#FIPS-provider
 [HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed
 [JWK]: https://tools.ietf.org/html/rfc7517
+[Key usages]: webcrypto.md#cryptokeyusages
 [NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf
 [NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
 [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js
index 762e8ead4ac46f..1d5d0bdb2380d8 100644
--- a/lib/internal/crypto/aes.js
+++ b/lib/internal/crypto/aes.js
@@ -245,7 +245,7 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) {
     extractable);
 }
 
-async function aesImportKey(
+function aesImportKey(
   algorithm,
   format,
   keyData,
@@ -266,6 +266,11 @@ async function aesImportKey(
   let keyObject;
   let length;
   switch (format) {
+    case 'KeyObject': {
+      validateKeyLength(keyData.symmetricKeySize * 8);
+      keyObject = keyData;
+      break;
+    }
     case 'raw': {
       validateKeyLength(keyData.byteLength * 8);
       keyObject = createSecretKey(keyData);
diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js
index 9c5e59ebb3bf86..7bea4ed3544f87 100644
--- a/lib/internal/crypto/cfrg.js
+++ b/lib/internal/crypto/cfrg.js
@@ -197,7 +197,7 @@ function cfrgExportKey(key, format) {
     key[kKeyObject][kHandle]));
 }
 
-async function cfrgImportKey(
+function cfrgImportKey(
   format,
   keyData,
   algorithm,
@@ -208,6 +208,11 @@ async function cfrgImportKey(
   let keyObject;
   const usagesSet = new SafeSet(keyUsages);
   switch (format) {
+    case 'KeyObject': {
+      verifyAcceptableCfrgKeyUse(name, keyData.type === 'public', usagesSet);
+      keyObject = keyData;
+      break;
+    }
     case 'spki': {
       verifyAcceptableCfrgKeyUse(name, true, usagesSet);
       try {
diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js
index e155a5b6610044..3cd5d12cdb2bc2 100644
--- a/lib/internal/crypto/ec.js
+++ b/lib/internal/crypto/ec.js
@@ -149,7 +149,7 @@ function ecExportKey(key, format) {
     key[kKeyObject][kHandle]));
 }
 
-async function ecImportKey(
+function ecImportKey(
   format,
   keyData,
   algorithm,
@@ -167,6 +167,11 @@ async function ecImportKey(
   let keyObject;
   const usagesSet = new SafeSet(keyUsages);
   switch (format) {
+    case 'KeyObject': {
+      verifyAcceptableEcKeyUse(name, keyData.type === 'public', usagesSet);
+      keyObject = keyData;
+      break;
+    }
     case 'spki': {
       verifyAcceptableEcKeyUse(name, true, usagesSet);
       try {
diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js
index e7d2275ee1a9fa..4738fbbbf208d1 100644
--- a/lib/internal/crypto/keys.js
+++ b/lib/internal/crypto/keys.js
@@ -6,6 +6,7 @@ const {
   ObjectDefineProperties,
   ObjectDefineProperty,
   ObjectSetPrototypeOf,
+  SafeSet,
   Symbol,
   SymbolToStringTag,
   Uint8Array,
@@ -49,6 +50,8 @@ const {
   kKeyObject,
   getArrayBufferOrView,
   bigIntArrayToUnsignedBigInt,
+  normalizeAlgorithm,
+  hasAnyNotIn,
 } = require('internal/crypto/util');
 
 const {
@@ -65,6 +68,7 @@ const {
 const {
   customInspectSymbol: kInspect,
   kEnumerableProperty,
+  lazyDOMException,
 } = require('internal/util');
 
 const { inspect } = require('internal/util/inspect');
@@ -148,6 +152,8 @@ const {
     },
   });
 
+  let webidl;
+
   class SecretKeyObject extends KeyObject {
     constructor(handle) {
       super('secret', handle);
@@ -168,6 +174,51 @@ const {
       }
       return this[kHandle].export();
     }
+
+    toCryptoKey(algorithm, extractable, keyUsages) {
+      webidl ??= require('internal/crypto/webidl');
+      algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey');
+      extractable = webidl.converters.boolean(extractable);
+      keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages);
+
+      let result;
+      switch (algorithm.name) {
+        case 'HMAC':
+          result = require('internal/crypto/mac')
+            .hmacImportKey('KeyObject', this, algorithm, extractable, keyUsages);
+          break;
+        case 'AES-CTR':
+          // Fall through
+        case 'AES-CBC':
+          // Fall through
+        case 'AES-GCM':
+          // Fall through
+        case 'AES-KW':
+          result = require('internal/crypto/aes')
+            .aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages);
+          break;
+        case 'HKDF':
+          // Fall through
+        case 'PBKDF2':
+          result = importGenericSecretKey(
+            algorithm,
+            'KeyObject',
+            this,
+            extractable,
+            keyUsages);
+          break;
+        default:
+          throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
+      }
+
+      if (result.usages.length === 0) {
+        throw lazyDOMException(
+          `Usages cannot be empty when importing a ${result.type} key.`,
+          'SyntaxError');
+      }
+
+      return result;
+    }
   }
 
   const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');
@@ -209,6 +260,51 @@ const {
           return {};
       }
     }
+
+    toCryptoKey(algorithm, extractable, keyUsages) {
+      webidl ??= require('internal/crypto/webidl');
+      algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey');
+      extractable = webidl.converters.boolean(extractable);
+      keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages);
+
+      let result;
+      switch (algorithm.name) {
+        case 'RSASSA-PKCS1-v1_5':
+          // Fall through
+        case 'RSA-PSS':
+          // Fall through
+        case 'RSA-OAEP':
+          result = require('internal/crypto/rsa')
+            .rsaImportKey('KeyObject', this, algorithm, extractable, keyUsages);
+          break;
+        case 'ECDSA':
+          // Fall through
+        case 'ECDH':
+          result = require('internal/crypto/ec')
+            .ecImportKey('KeyObject', this, algorithm, extractable, keyUsages);
+          break;
+        case 'Ed25519':
+          // Fall through
+        case 'Ed448':
+          // Fall through
+        case 'X25519':
+          // Fall through
+        case 'X448':
+          result = require('internal/crypto/cfrg')
+            .cfrgImportKey('KeyObject', this, algorithm, extractable, keyUsages);
+          break;
+        default:
+          throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
+      }
+
+      if (result.type === 'private' && result.usages.length === 0) {
+        throw lazyDOMException(
+          `Usages cannot be empty when importing a ${result.type} key.`,
+          'SyntaxError');
+      }
+
+      return result;
+    }
   }
 
   class PublicKeyObject extends AsymmetricKeyObject {
@@ -801,6 +897,68 @@ function isCryptoKey(obj) {
   return obj != null && obj[kKeyObject] !== undefined;
 }
 
+function importGenericSecretKey(
+  { name, length },
+  format,
+  keyData,
+  extractable,
+  keyUsages) {
+  const usagesSet = new SafeSet(keyUsages);
+  if (extractable)
+    throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
+
+  if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
+    throw lazyDOMException(
+      `Unsupported key usage for a ${name} key`,
+      'SyntaxError');
+  }
+
+  switch (format) {
+    case 'KeyObject': {
+      if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
+        throw lazyDOMException(
+          `Unsupported key usage for a ${name} key`,
+          'SyntaxError');
+      }
+
+      const checkLength = keyData.symmetricKeySize * 8;
+
+      // The Web Crypto spec allows for key lengths that are not multiples of
+      // 8. We don't. Our check here is stricter than that defined by the spec
+      // in that we require that algorithm.length match keyData.length * 8 if
+      // algorithm.length is specified.
+      if (length !== undefined && length !== checkLength) {
+        throw lazyDOMException('Invalid key length', 'DataError');
+      }
+      return new InternalCryptoKey(keyData, { name }, keyUsages, false);
+    }
+    case 'raw': {
+      if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
+        throw lazyDOMException(
+          `Unsupported key usage for a ${name} key`,
+          'SyntaxError');
+      }
+
+      const checkLength = keyData.byteLength * 8;
+
+      // The Web Crypto spec allows for key lengths that are not multiples of
+      // 8. We don't. Our check here is stricter than that defined by the spec
+      // in that we require that algorithm.length match keyData.length * 8 if
+      // algorithm.length is specified.
+      if (length !== undefined && length !== checkLength) {
+        throw lazyDOMException('Invalid key length', 'DataError');
+      }
+
+      const keyObject = createSecretKey(keyData);
+      return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
+    }
+  }
+
+  throw lazyDOMException(
+    `Unable to import ${name} key with format ${format}`,
+    'NotSupportedError');
+}
+
 module.exports = {
   // Public API.
   createSecretKey,
@@ -822,4 +980,5 @@ module.exports = {
   PrivateKeyObject,
   isKeyObject,
   isCryptoKey,
+  importGenericSecretKey,
 };
diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js
index 112861523c605f..356ce5a8d79a31 100644
--- a/lib/internal/crypto/mac.js
+++ b/lib/internal/crypto/mac.js
@@ -82,7 +82,7 @@ function getAlgorithmName(hash) {
   }
 }
 
-async function hmacImportKey(
+function hmacImportKey(
   format,
   keyData,
   algorithm,
@@ -96,6 +96,24 @@ async function hmacImportKey(
   }
   let keyObject;
   switch (format) {
+    case 'KeyObject': {
+      const checkLength = keyData.symmetricKeySize * 8;
+
+      if (checkLength === 0 || algorithm.length === 0)
+        throw lazyDOMException('Zero-length key is not supported', 'DataError');
+
+      // The Web Crypto spec allows for key lengths that are not multiples of
+      // 8. We don't. Our check here is stricter than that defined by the spec
+      // in that we require that algorithm.length match keyData.length * 8 if
+      // algorithm.length is specified.
+      if (algorithm.length !== undefined &&
+          algorithm.length !== checkLength) {
+        throw lazyDOMException('Invalid key length', 'DataError');
+      }
+
+      keyObject = keyData;
+      break;
+    }
     case 'raw': {
       const checkLength = keyData.byteLength * 8;
 
diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js
index fd223d3cb632ab..99c12ebbe3b83f 100644
--- a/lib/internal/crypto/rsa.js
+++ b/lib/internal/crypto/rsa.js
@@ -200,7 +200,7 @@ function rsaExportKey(key, format) {
     kRsaVariants[key.algorithm.name]));
 }
 
-async function rsaImportKey(
+function rsaImportKey(
   format,
   keyData,
   algorithm,
@@ -209,6 +209,11 @@ async function rsaImportKey(
   const usagesSet = new SafeSet(keyUsages);
   let keyObject;
   switch (format) {
+    case 'KeyObject': {
+      verifyAcceptableRsaKeyUse(algorithm.name, keyData.type === 'public', usagesSet);
+      keyObject = keyData;
+      break;
+    }
     case 'spki': {
       verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet);
       try {
diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js
index 8cd27717532344..4758c2c1c7133a 100644
--- a/lib/internal/crypto/webcrypto.js
+++ b/lib/internal/crypto/webcrypto.js
@@ -8,7 +8,6 @@ const {
   ObjectDefineProperty,
   ReflectApply,
   ReflectConstruct,
-  SafeSet,
   StringPrototypeRepeat,
   SymbolToStringTag,
 } = primordials;
@@ -36,8 +35,7 @@ const {
 
 const {
   CryptoKey,
-  InternalCryptoKey,
-  createSecretKey,
+  importGenericSecretKey,
 } = require('internal/crypto/keys');
 
 const {
@@ -46,7 +44,6 @@ const {
 
 const {
   getBlockSize,
-  hasAnyNotIn,
   normalizeAlgorithm,
   normalizeHashName,
   validateMaxBufferLength,
@@ -526,50 +523,6 @@ async function exportKey(format, key) {
     'Export format is unsupported', 'NotSupportedError');
 }
 
-async function importGenericSecretKey(
-  { name, length },
-  format,
-  keyData,
-  extractable,
-  keyUsages) {
-  const usagesSet = new SafeSet(keyUsages);
-  if (extractable)
-    throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
-
-  if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
-    throw lazyDOMException(
-      `Unsupported key usage for a ${name} key`,
-      'SyntaxError');
-  }
-
-  switch (format) {
-    case 'raw': {
-      if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
-        throw lazyDOMException(
-          `Unsupported key usage for a ${name} key`,
-          'SyntaxError');
-      }
-
-      const checkLength = keyData.byteLength * 8;
-
-      // The Web Crypto spec allows for key lengths that are not multiples of
-      // 8. We don't. Our check here is stricter than that defined by the spec
-      // in that we require that algorithm.length match keyData.length * 8 if
-      // algorithm.length is specified.
-      if (length !== undefined && length !== checkLength) {
-        throw lazyDOMException('Invalid key length', 'DataError');
-      }
-
-      const keyObject = createSecretKey(keyData);
-      return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
-    }
-  }
-
-  throw lazyDOMException(
-    `Unable to import ${name} key with format ${format}`,
-    'NotSupportedError');
-}
-
 async function importKey(
   format,
   keyData,
@@ -611,13 +564,13 @@ async function importKey(
     case 'RSA-PSS':
       // Fall through
     case 'RSA-OAEP':
-      result = await require('internal/crypto/rsa')
+      result = require('internal/crypto/rsa')
         .rsaImportKey(format, keyData, algorithm, extractable, keyUsages);
       break;
     case 'ECDSA':
       // Fall through
     case 'ECDH':
-      result = await require('internal/crypto/ec')
+      result = require('internal/crypto/ec')
         .ecImportKey(format, keyData, algorithm, extractable, keyUsages);
       break;
     case 'Ed25519':
@@ -627,11 +580,11 @@ async function importKey(
     case 'X25519':
       // Fall through
     case 'X448':
-      result = await require('internal/crypto/cfrg')
+      result = require('internal/crypto/cfrg')
         .cfrgImportKey(format, keyData, algorithm, extractable, keyUsages);
       break;
     case 'HMAC':
-      result = await require('internal/crypto/mac')
+      result = require('internal/crypto/mac')
         .hmacImportKey(format, keyData, algorithm, extractable, keyUsages);
       break;
     case 'AES-CTR':
@@ -641,13 +594,13 @@ async function importKey(
     case 'AES-GCM':
       // Fall through
     case 'AES-KW':
-      result = await require('internal/crypto/aes')
+      result = require('internal/crypto/aes')
         .aesImportKey(algorithm, format, keyData, extractable, keyUsages);
       break;
     case 'HKDF':
       // Fall through
     case 'PBKDF2':
-      result = await importGenericSecretKey(
+      result = importGenericSecretKey(
         algorithm,
         format,
         keyData,
diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js
new file mode 100644
index 00000000000000..1656f37a3c58b5
--- /dev/null
+++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js
@@ -0,0 +1,182 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const {
+  createSecretKey,
+  KeyObject,
+  randomBytes,
+  generateKeyPairSync,
+} = require('crypto');
+
+function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) {
+  assert.strictEqual(cryptoKey instanceof CryptoKey, true);
+  assert.strictEqual(cryptoKey.type, keyObject.type);
+  assert.strictEqual(cryptoKey.algorithm.name, algorithm);
+  assert.strictEqual(cryptoKey.extractable, extractable);
+  assert.deepStrictEqual(cryptoKey.usages, usages);
+  assert.strictEqual(keyObject.equals(KeyObject.from(cryptoKey)), true);
+}
+
+{
+  for (const length of [128, 192, 256]) {
+    const aes = createSecretKey(randomBytes(length >> 3));
+    for (const algorithm of ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']) {
+      const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt'];
+      for (const extractable of [true, false]) {
+        const cryptoKey = aes.toCryptoKey(algorithm, extractable, usages);
+        assertCryptoKey(cryptoKey, aes, algorithm, extractable, usages);
+        assert.strictEqual(cryptoKey.algorithm.length, length);
+      }
+    }
+  }
+}
+
+{
+  const pbkdf2 = createSecretKey(randomBytes(16));
+  const algorithm = 'PBKDF2';
+  const usages = ['deriveBits'];
+  assert.throws(() => pbkdf2.toCryptoKey(algorithm, true, usages), {
+    name: 'SyntaxError',
+    message: 'PBKDF2 keys are not extractable'
+  });
+  assert.throws(() => pbkdf2.toCryptoKey(algorithm, false, ['wrapKey']), {
+    name: 'SyntaxError',
+    message: 'Unsupported key usage for a PBKDF2 key'
+  });
+  const cryptoKey = pbkdf2.toCryptoKey(algorithm, false, usages);
+  assertCryptoKey(cryptoKey, pbkdf2, algorithm, false, usages);
+  assert.strictEqual(cryptoKey.algorithm.length, undefined);
+}
+
+{
+  for (const length of [128, 192, 256]) {
+    const hmac = createSecretKey(randomBytes(length >> 3));
+    const algorithm = 'HMAC';
+    const usages = ['sign', 'verify'];
+
+    assert.throws(() => {
+      createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm, hash: 'SHA-256' }, true, usages);
+    }, {
+      name: 'DataError',
+      message: 'Zero-length key is not supported',
+    });
+
+    assert.throws(() => {
+      hmac.toCryptoKey({
+        name: algorithm,
+        hash: 'SHA-256',
+      }, true, []);
+    }, {
+      name: 'SyntaxError',
+      message: 'Usages cannot be empty when importing a secret key.'
+    });
+
+    for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
+      for (const extractable of [true, false]) {
+        assert.throws(() => {
+          hmac.toCryptoKey({ name: algorithm, hash: 'SHA-256', length: 0 }, true, usages);
+        }, {
+          name: 'DataError',
+          message: 'Zero-length key is not supported',
+        });
+        const cryptoKey = hmac.toCryptoKey({ name: algorithm, hash }, extractable, usages);
+        assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages);
+        assert.strictEqual(cryptoKey.algorithm.length, length);
+      }
+    }
+  }
+}
+
+{
+  for (const algorithm of ['Ed25519', 'Ed448', 'X25519', 'X448']) {
+    const { publicKey, privateKey } = generateKeyPairSync(algorithm.toLowerCase());
+    assert.throws(() => {
+      publicKey.toCryptoKey(algorithm === 'Ed25519' ? 'X25519' : 'Ed25519', true, []);
+    }, {
+      name: 'DataError',
+      message: 'Invalid key type'
+    });
+    for (const key of [publicKey, privateKey]) {
+      let usages;
+      if (algorithm.startsWith('E')) {
+        usages = key.type === 'public' ? ['verify'] : ['sign'];
+      } else {
+        usages = key.type === 'public' ? [] : ['deriveBits'];
+      }
+      for (const extractable of [true, false]) {
+        const cryptoKey = key.toCryptoKey(algorithm, extractable, usages);
+        assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
+      }
+    }
+  }
+}
+
+{
+  const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
+  for (const key of [publicKey, privateKey]) {
+    for (const algorithm of ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP']) {
+      let usages;
+      if (algorithm === 'RSA-OAEP') {
+        usages = key.type === 'public' ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];
+      } else {
+        usages = key.type === 'public' ? ['verify'] : ['sign'];
+      }
+      for (const extractable of [true, false]) {
+        for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
+          const cryptoKey = key.toCryptoKey({
+            name: algorithm,
+            hash
+          }, extractable, usages);
+          assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
+          assert.strictEqual(cryptoKey.algorithm.hash.name, hash);
+        }
+      }
+    }
+  }
+}
+
+{
+  for (const namedCurve of ['P-256', 'P-384', 'P-521']) {
+    const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve });
+    assert.throws(() => {
+      privateKey.toCryptoKey({
+        name: 'ECDH',
+        namedCurve,
+      }, true, []);
+    }, {
+      name: 'SyntaxError',
+      message: 'Usages cannot be empty when importing a private key.'
+    });
+    assert.throws(() => {
+      publicKey.toCryptoKey({
+        name: 'ECDH',
+        namedCurve: namedCurve === 'P-256' ? 'P-384' : 'P-256'
+      }, true, []);
+    }, {
+      name: 'DataError',
+      message: 'Named curve mismatch'
+    });
+    for (const key of [publicKey, privateKey]) {
+      for (const algorithm of ['ECDH', 'ECDSA']) {
+        let usages;
+        if (algorithm === 'ECDH') {
+          usages = key.type === 'public' ? [] : ['deriveBits'];
+        } else {
+          usages = key.type === 'public' ? ['verify'] : ['sign'];
+        }
+        for (const extractable of [true, false]) {
+          const cryptoKey = key.toCryptoKey({
+            name: algorithm,
+            namedCurve
+          }, extractable, usages);
+          assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
+          assert.strictEqual(cryptoKey.algorithm.namedCurve, namedCurve);
+        }
+      }
+    }
+  }
+}
diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js
index e7d45dbc5efeea..21a8d16edbea0e 100644
--- a/test/parallel/test-webcrypto-export-import.js
+++ b/test/parallel/test-webcrypto-export-import.js
@@ -21,6 +21,11 @@ const { subtle } = globalThis.crypto;
       subtle.importKey('not valid', keyData, {}, false, ['wrapKey']), {
         code: 'ERR_INVALID_ARG_VALUE'
       });
+    await assert.rejects(
+      subtle.importKey('KeyObject', keyData, {}, false, ['wrapKey']), {
+        message: /'KeyObject' is not a valid enum value of type KeyFormat/,
+        code: 'ERR_INVALID_ARG_VALUE'
+      });
     await assert.rejects(
       subtle.importKey('raw', 1, {}, false, ['deriveBits']), {
         code: 'ERR_INVALID_ARG_TYPE'