Repository: nodejs/node
Pull request: [v24.x backport] raw key formats support in KeyObject APIs

Backports #62240 (partially) and #62480 to v24.x

The omitted portion of #62240 is the removal and cleanup of `KeyExportJob` use in WebCryptoAPI.

Changed files:
filename: deps/ncrypto/ncrypto.h
patch: @@ -855,6 +855,9 @@ class EVPKeyPointer final {
     DER,
     PEM,
     JWK,
+    RAW_PUBLIC,
+    RAW_PRIVATE,
+    RAW_SEED,
   };
 
   enum class PKParseError { NOT_RECOGNIZED, NEED_PASSPHRASE, FAILED };
@@ -864,6 +867,7 @@ class EVPKeyPointer final {
     bool output_key_object = false;
     PKFormatType format = PKFormatType::DER;
     PKEncodingType type = PKEncodingType::PKCS8;
+    int ec_point_form = POINT_CONVERSION_UNCOMPRESSED;
     AsymmetricKeyEncodingConfig() = default;
     AsymmetricKeyEncodingConfig(bool output_key_object,
                                 PKFormatType format,

filename: lib/internal/crypto/keys.js
patch: @@ -21,6 +21,9 @@ const {
   kKeyFormatPEM,
   kKeyFormatDER,
   kKeyFormatJWK,
+  kKeyFormatRawPublic,
+  kKeyFormatRawPrivate,
+  kKeyFormatRawSeed,
   kKeyEncodingPKCS1,
   kKeyEncodingPKCS8,
   kKeyEncodingSPKI,
@@ -419,6 +422,12 @@ function parseKeyFormat(formatStr, defaultFormat, optionName) {
     return kKeyFormatDER;
   else if (formatStr === 'jwk')
     return kKeyFormatJWK;
+  else if (formatStr === 'raw-public')
+    return kKeyFormatRawPublic;
+  else if (formatStr === 'raw-private')
+    return kKeyFormatRawPrivate;
+  else if (formatStr === 'raw-seed')
+    return kKeyFormatRawSeed;
   throw new ERR_INVALID_ARG_VALUE(optionName, formatStr);
 }
 
@@ -459,6 +468,33 @@ function parseKeyFormatAndType(enc, keyType, isPublic, objName) {
                                 isInput ? kKeyFormatPEM : undefined,
                                 option('format', objName));
 
+  if (format === kKeyFormatRawPublic) {
+    if (isPublic === false) {
+      throw new ERR_INVALID_ARG_VALUE(option('format', objName), 'raw-public');
+    }
+    let type;
+    if (typeStr === undefined || typeStr === 'uncompressed') {
+      type = POINT_CONVERSION_UNCOMPRESSED;
+    } else if (typeStr === 'compressed') {
+      type = POINT_CONVERSION_COMPRESSED;
+    } else {
+      throw new ERR_INVALID_ARG_VALUE(option('type', objName), typeStr);
+    }
+    return { format, type };
+  }
+
+  if (format === kKeyFormatRawPrivate || format === kKeyFormatRawSeed) {
+    if (isPublic === true) {
+      throw new ERR_INVALID_ARG_VALUE(
+        option('format', objName),
+        format === kKeyFormatRawPrivate ? 'raw-private' : 'raw-seed');
+    }
+    if (typeStr !== undefined) {
+      throw new ERR_INVALID_ARG_VALUE(option('type', objName), typeStr);
+    }
+    return { format };
+  }
+
   const isRequired = (!isInput ||
                       format === kKeyFormatDER) &&
                       format !== kKeyFormatJWK;
@@ -490,6 +526,14 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) {
   if (isPublic !== true) {
     ({ cipher, passphrase, encoding } = enc);
 
+    if (format === kKeyFormatRawPrivate || format === kKeyFormatRawSeed) {
+      if (cipher != null || passphrase !== undefined) {
+        throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
+          'raw format', 'does not support encryption');
+      }
+      return { format, type };
+    }
+
     if (!isInput) {
       if (cipher != null) {
         if (typeof cipher !== 'string')

filename: src/crypto/crypto_keys.cc
patch: @@ -67,7 +67,16 @@ Maybe<EVPKeyPointer::AsymmetricKeyEncodingConfig> GetKeyFormatAndTypeFromJs(
     config.format = static_cast<EVPKeyPointer::PKFormatType>(
         args[*offset].As<Int32>()->Value());
 
-    if (args[*offset + 1]->IsInt32()) {
+    if (config.format == EVPKeyPointer::PKFormatType::RAW_PUBLIC ||
+        config.format == EVPKeyPointer::PKFormatType::RAW_PRIVATE ||
+        config.format == EVPKeyPointer::PKFormatType::RAW_SEED) {
+      // Raw formats use the type slot for ec_point_form (int) or null.
+      if (args[*offset + 1]->IsInt32()) {
+        config.ec_point_form = args[*offset + 1].As<Int32>()->Value();
+      } else {
+        CHECK(args[*offset + 1]->IsNullOrUndefined());
+      }
+    } else if (args[*offset + 1]->IsInt32()) {
       config.type = static_cast<EVPKeyPointer::PKEncodingType>(
           args[*offset + 1].As<Int32>()->Value());
     } else {
@@ -325,6 +334,54 @@ bool KeyObjectData::ToEncodedPublicKey(
     *out = Object::New(env->isolate());
     return ExportJWKInner(
         env, addRefWithType(KeyType::kKeyTypePublic), *out, false);
+  } else if (config.format == EVPKeyPointer::PKFormatType::RAW_PUBLIC) {
+    Mutex::ScopedLock lock(mutex());
+    const auto& pkey = GetAsymmetricKey();
+    if (pkey.id() == EVP_PKEY_EC) {
+      const EC_KEY* ec_key = pkey;
+      CHECK_NOT_NULL(ec_key);
+      auto form = static_cast<point_conversion_form_t>(config.ec_point_form);
+      const auto group = ECKeyPointer::GetGroup(ec_key);
+      const auto point = ECKeyPointer::GetPublicKey(ec_key);
+      return ECPointToBuffer(env, group, point, form).ToLocal(out);
+    }
+    switch (pkey.id()) {
+      case EVP_PKEY_ED25519:
+      case EVP_PKEY_ED448:
+      case EVP_PKEY_X25519:
+      case EVP_PKEY_X448:
+#if OPENSSL_WITH_PQC
+      case EVP_PKEY_ML_DSA_44:
+      case EVP_PKEY_ML_DSA_65:
+      case EVP_PKEY_ML_DSA_87:
+      case EVP_PKEY_ML_KEM_512:
+      case EVP_PKEY_ML_KEM_768:
+      case EVP_PKEY_ML_KEM_1024:
+      case EVP_PKEY_SLH_DSA_SHA2_128F:
+      case EVP_PKEY_SLH_DSA_SHA2_128S:
+      case EVP_PKEY_SLH_DSA_SHA2_192F:
+      case EVP_PKEY_SLH_DSA_SHA2_192S:
+      case EVP_PKEY_SLH_DSA_SHA2_256F:
+      case EVP_PKEY_SLH_DSA_SHA2_256S:
+      case EVP_PKEY_SLH_DSA_SHAKE_128F:
+      case EVP_PKEY_SLH_DSA_SHAKE_128S:
+      case EVP_PKEY_SLH_DSA_SHAKE_192F:
+      case EVP_PKEY_SLH_DSA_SHAKE_192S:
+      case EVP_PKEY_SLH_DSA_SHAKE_256F:
+      case EVP_PKEY_SLH_DSA_SHAKE_256S:
+#endif
+        break;
+      default:
+        THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
+        return false;
+    }
+    auto raw_data = pkey.rawPublicKey();
+    if (!raw_data) {
+      THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw public key");
+      return false;
+    }
+    return Buffer::Copy(env, raw_data.get<const char>(), raw_data.size())
+        .ToLocal(out);
   }
 
   return WritePublicKey(env, GetAsymmetricKey(), config).ToLocal(out);
@@ -343,6 +400,86 @@ bool KeyObjectData::ToEncodedPrivateKey(
     *out = Object::New(env->isolate());
     return ExportJWKInner(
         env, addRefWithType(KeyType::kKeyTypePrivate), *out, false);
+  } else if (config.format == EVPKeyPointer::PKFormatType::RAW_PRIVATE) {
+    Mutex::ScopedLock lock(mutex());
+    const auto& pkey = GetAsymmetricKey();
+    if (pkey.id() == EVP_PKEY_EC) {
+      const EC_KEY* ec_key = pkey;
+      CHECK_NOT_NULL(ec_key);
+      const BIGNUM* private_key = ECKeyPointer::GetPrivateKey(ec_key);
+      CHECK_NOT_NULL(private_key);
+      const auto group = ECKeyPointer::GetGroup(ec_key);
+      auto order = BignumPointer::New();
+      CHECK(order);
+      CHECK(EC_GROUP_get_order(group, order.get(), nullptr));
+      auto buf = BignumPointer::EncodePadded(private_key, order.byteLength());
+      if (!buf) {
+        THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+                                          "Failed to export EC private key");
+        return false;
+      }
+      return Buffer::Copy(env, buf.get<const char>(), buf.size()).ToLocal(out);
+    }
+    switch (pkey.id()) {
+      case EVP_PKEY_ED25519:
+      case EVP_PKEY_ED448:
+      case EVP_PKEY_X25519:
+      case EVP_PKEY_X448:
+#if OPENSSL_WITH_PQC
+      case EVP_PKEY_SLH_DSA_SHA2_128F:
+      case EVP_PKEY_SLH_DSA_SHA2_128S:
+      case EVP_PKEY_SLH_DSA_SHA2_192F:
+      case EVP_PKEY_SLH_DSA_SHA2_192S:
+      case EVP_PKEY_SLH_DSA_SHA2_256F:
+      case EVP_PKEY_SLH_DSA_SHA2_256S:
+      case EVP_PKEY_SLH_DSA_SHAKE_128F:
+      case EVP_PKEY_SLH_DSA_SHAKE_128S:
+      case EVP_PKEY_SLH_DSA_SHAKE_192F:
+      case EVP_PKEY_SLH_DSA_SHAKE_192S:
+      case EVP_PKEY_SLH_DSA_SHAKE_256F:
+      case EVP_PKEY_SLH_DSA_SHAKE_256S:
+#endif
+        break;
+      default:
+        THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
+        return false;
+    }
+    auto raw_data = pkey.rawPrivateKey();
+    if (!raw_data) {
+      THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw private key");
+      return false;
+    }
+    return Buffer::Copy(env, raw_data.get<const char>(), raw_data.size())
+        .ToLocal(out);
+  } else if (config.format == EVPKeyPointer::PKFormatType::RAW_SEED) {
+    Mutex::ScopedLock lock(mutex());
+    const auto& pkey = GetAsymmetricKey();
+    switch (pkey.id()) {
+#if OPENSSL_WITH_PQC
+      case EVP_PKEY_ML_DSA_44:
+      case EVP_PKEY_ML_DSA_65:
+      case EVP_PKEY_ML_DSA_87:
+      case EVP_PKEY_ML_KEM_512:
+      case EVP_PKEY_ML_KEM_768:
+      case EVP_PKEY_ML_KEM_1024:
+        break;
+#endif
+      default:
+        THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
+        return false;
+    }
+#if OPENSSL_WITH_PQC
+    auto raw_data = pkey.rawSeed();
+    if (!raw_data) {
+      THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed");
+      return false;
+    }
+    return Buffer::Copy(env, raw_data.get<const char>(), raw_data.size())
+        .ToLocal(out);
+#else
+    THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env);
+    return false;
+#endif
   }
 
   return WritePrivateKey(env, GetAsymmetricKey(), config).ToLocal(out);
@@ -363,6 +500,14 @@ KeyObjectData::GetPrivateKeyEncodingFromJs(
   if (config.output_key_object) {
     if (context != kKeyContextInput)
       (*offset)++;
+  } else if (config.format == EVPKeyPointer::PKFormatType::RAW_PRIVATE ||
+             config.format == EVPKeyPointer::PKFormatType::RAW_SEED) {
+    // Raw formats don't support encryption. Still consume the arg offsets.
+    if (context != kKeyContextInput) {
+      CHECK(args[*offset]->IsNullOrUndefined());
+      (*offset)++;
+    }
+    CHECK(args[*offset]->IsNullOrUndefined());
   } else {
     bool needs_passphrase = false;
     if (context != kKeyContextInput) {
@@ -1581,6 +1726,12 @@ void Initialize(Environment* env, Local<Object> target) {
       static_cast<int>(EVPKeyPointer::PKFormatType::PEM);
   constexpr int kKeyFormatJWK =
       static_cast<int>(EVPKeyPointer::PKFormatType::JWK);
+  constexpr int kKeyFormatRawPublic =
+      static_cast<int>(EVPKeyPointer::PKFormatType::RAW_PUBLIC);
+  constexpr int kKeyFormatRawPrivate =
+      static_cast<int>(EVPKeyPointer::PKFormatType::RAW_PRIVATE);
+  constexpr int kKeyFormatRawSeed =
+      static_cast<int>(EVPKeyPointer::PKFormatType::RAW_SEED);
 
   constexpr auto kSigEncDER = DSASigEnc::DER;
   constexpr auto kSigEncP1363 = DSASigEnc::P1363;
@@ -1620,6 +1771,9 @@ void Initialize(Environment* env, Local<Object> target) {
   NODE_DEFINE_CONSTANT(target, kKeyFormatDER);
   NODE_DEFINE_CONSTANT(target, kKeyFormatPEM);
   NODE_DEFINE_CONSTANT(target, kKeyFormatJWK);
+  NODE_DEFINE_CONSTANT(target, kKeyFormatRawPublic);
+  NODE_DEFINE_CONSTANT(target, kKeyFormatRawPrivate);
+  NODE_DEFINE_CONSTANT(target, kKeyFormatRawSeed);
   NODE_DEFINE_CONSTANT(target, kKeyTypeSecret);
   NODE_DEFINE_CONSTANT(target, kKeyTypePublic);
   NODE_DEFINE_CONSTANT(target, kKeyTypePrivate);

filename: test/parallel/test-crypto-keygen-raw.js
patch: @@ -0,0 +1,285 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const {
+  generateKeyPair,
+  generateKeyPairSync,
+  createPublicKey,
+  createPrivateKey,
+} = require('crypto');
+const { hasOpenSSL } = require('../common/crypto');
+
+// Test generateKeyPairSync with raw encoding for EdDSA/ECDH key types.
+{
+  const types = ['ed25519', 'x25519'];
+  if (!process.features.openssl_is_boringssl) {
+    types.push('ed448', 'x448');
+  }
+  for (const type of types) {
+    const { publicKey, privateKey } = generateKeyPairSync(type, {
+      publicKeyEncoding: { format: 'raw-public' },
+      privateKeyEncoding: { format: 'raw-private' },
+    });
+
+    assert(Buffer.isBuffer(publicKey));
+    assert(Buffer.isBuffer(privateKey));
+
+    // Roundtrip: import from raw, re-export, and compare.
+    const importedPub = createPublicKey({
+      key: publicKey,
+      format: 'raw-public',
+      asymmetricKeyType: type,
+    });
+    const importedPriv = createPrivateKey({
+      key: privateKey,
+      format: 'raw-private',
+      asymmetricKeyType: type,
+    });
+
+    assert.deepStrictEqual(importedPub.export({ format: 'raw-public' }),
+                           publicKey);
+    assert.deepStrictEqual(importedPriv.export({ format: 'raw-private' }),
+                           privateKey);
+  }
+}
+
+// Test async generateKeyPair with raw encoding for EdDSA/ECDH key types.
+{
+  const types = ['ed25519', 'x25519'];
+  if (!process.features.openssl_is_boringssl) {
+    types.push('ed448', 'x448');
+  }
+  for (const type of types) {
+    generateKeyPair(type, {
+      publicKeyEncoding: { format: 'raw-public' },
+      privateKeyEncoding: { format: 'raw-private' },
+    }, common.mustSucceed((publicKey, privateKey) => {
+      assert(Buffer.isBuffer(publicKey));
+      assert(Buffer.isBuffer(privateKey));
+    }));
+  }
+}
+
+// Test generateKeyPairSync with raw encoding for EC keys.
+{
+  for (const namedCurve of ['P-256', 'P-384', 'P-521']) {
+    const { publicKey, privateKey } = generateKeyPairSync('ec', {
+      namedCurve,
+      publicKeyEncoding: { format: 'raw-public' },
+      privateKeyEncoding: { format: 'raw-private' },
+    });
+
+    assert(Buffer.isBuffer(publicKey));
+    assert(Buffer.isBuffer(privateKey));
+
+    // Roundtrip with EC.
+    const importedPub = createPublicKey({
+      key: publicKey,
+      format: 'raw-public',
+      asymmetricKeyType: 'ec',
+      namedCurve,
+    });
+    const importedPriv = createPrivateKey({
+      key: privateKey,
+      format: 'raw-private',
+      asymmetricKeyType: 'ec',
+      namedCurve,
+    });
+
+    assert.deepStrictEqual(importedPub.export({ format: 'raw-public' }),
+                           publicKey);
+    assert.deepStrictEqual(importedPriv.export({ format: 'raw-private' }),
+                           privateKey);
+  }
+}
+
+// Test EC raw-public with compressed and uncompressed point formats.
+{
+  const { publicKey: uncompressed } = generateKeyPairSync('ec', {
+    namedCurve: 'P-256',
+    publicKeyEncoding: { format: 'raw-public', type: 'uncompressed' },
+    privateKeyEncoding: { format: 'raw-private' },
+  });
+  const { publicKey: compressed } = generateKeyPairSync('ec', {
+    namedCurve: 'P-256',
+    publicKeyEncoding: { format: 'raw-public', type: 'compressed' },
+    privateKeyEncoding: { format: 'raw-private' },
+  });
+
+  // Uncompressed P-256 public key is 65 bytes, compressed is 33 bytes.
+  assert.strictEqual(uncompressed.length, 65);
+  assert.strictEqual(compressed.length, 33);
+}
+
+// Test mixed: one side raw, other side pem.
+{
+  const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
+    publicKeyEncoding: { format: 'raw-public' },
+    privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
+  });
+
+  assert(Buffer.isBuffer(publicKey));
+  assert.strictEqual(typeof privateKey, 'string');
+  assert(privateKey.startsWith('-----BEGIN PRIVATE KEY-----'));
+}
+
+{
+  const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
+    publicKeyEncoding: { format: 'pem', type: 'spki' },
+    privateKeyEncoding: { format: 'raw-private' },
+  });
+
+  assert.strictEqual(typeof publicKey, 'string');
+  assert(publicKey.startsWith('-----BEGIN PUBLIC KEY-----'));
+  assert(Buffer.isBuffer(privateKey));
+}
+
+// Test mixed: one side raw, other side KeyObject (no encoding).
+{
+  const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
+    publicKeyEncoding: { format: 'raw-public' },
+  });
+
+  assert(Buffer.isBuffer(publicKey));
+  assert.strictEqual(privateKey.type, 'private');
+}
+
+// Test error: raw with RSA.
+{
+  assert.throws(() => generateKeyPairSync('rsa', {
+    modulusLength: 2048,
+    publicKeyEncoding: { format: 'raw-public' },
+    privateKeyEncoding: { format: 'raw-private' },
+  }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
+}
+
+// Test error: raw with DSA.
+{
+  assert.throws(() => generateKeyPairSync('dsa', {
+    modulusLength: 2048,
+    publicKeyEncoding: { format: 'raw-public' },
+    privateKeyEncoding: { format: 'raw-private' },
+  }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
+}
+
+// Test error: raw-private in publicKeyEncoding.
+{
+  assert.throws(() => generateKeyPairSync('ed25519', {
+    publicKeyEncoding: { format: 'raw-private' },
+    privateKeyEncoding: { format: 'raw-private' },
+  }), { code: 'ERR_INVALID_ARG_VALUE' });
+}
+
+// Test error: raw-seed in publicKeyEncoding.
+{
+  assert.throws(() => generateKeyPairSync('ed25519', {
+    publicKeyEncoding: { format: 'raw-seed' },
+    privateKeyEncoding: { format: 'raw-private' },
+  }), { code: 'ERR_INVALID_ARG_VALUE' });
+}
+
+// Test error: raw-public in privateKeyEncoding.
+{
+  assert.throws(() => generateKeyPairSync('ed25519', {
+    publicKeyEncoding: { format: 'raw-public' },
+    privateKeyEncoding: { format: 'raw-public' },
+  }), { code: 'ERR_INVALID_ARG_VALUE' });
+}
+
+// Test error: passphrase with raw private key encoding.
+{
+  assert.throws(() => generateKeyPairSync('ed25519', {
+    publicKeyEncoding: { format: 'raw-public' },
+    privateKeyEncoding: {
+      format: 'raw-private',
+      cipher: 'aes-256-cbc',
+      passphrase: 'secret',
+    },
+  }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
+}
+
+// PQC key types
+if (hasOpenSSL(3, 5)) {
+  // Test raw encoding for ML-DSA key types (raw-public + raw-seed only).
+  {
+    for (const type of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) {
+      const { publicKey, privateKey } = generateKeyPairSync(type, {
+        publicKeyEncoding: { format: 'raw-public' },
+        privateKeyEncoding: { format: 'raw-seed' },
+      });
+
+      assert(Buffer.isBuffer(publicKey));
+      assert(Buffer.isBuffer(privateKey));
+      // raw-seed output should be 32 bytes for ML-DSA.
+      assert.strictEqual(privateKey.length, 32);
+    }
+  }
+
+  // Test error: raw-private with ML-DSA (not supported).
+  {
+    assert.throws(() => generateKeyPairSync('ml-dsa-44', {
+      publicKeyEncoding: { format: 'raw-public' },
+      privateKeyEncoding: { format: 'raw-private' },
+    }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
+  }
+
+  // Test raw encoding for ML-KEM key types (raw-public + raw-seed only).
+  {
+    for (const type of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) {
+      const { publicKey, privateKey } = generateKeyPairSync(type, {
+        publicKeyEncoding: { format: 'raw-public' },
+        privateKeyEncoding: { format: 'raw-seed' },
+      });
+
+      assert(Buffer.isBuffer(publicKey));
+      assert(Buffer.isBuffer(privateKey));
+      // raw-seed output should be 64 bytes for ML-KEM.
+      assert.strictEqual(privateKey.length, 64);
+    }
+  }
+
+  // Test error: raw-private with ML-KEM (not supported).
+  {
+    assert.throws(() => generateKeyPairSync('ml-kem-512', {
+      publicKeyEncoding: { format: 'raw-public' },
+      privateKeyEncoding: { format: 'raw-private' },
+    }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
+  }
+
+  // Test raw encoding for SLH-DSA key types.
+  {
+    for (const type of ['slh-dsa-sha2-128f', 'slh-dsa-shake-128f']) {
+      const { publicKey, privateKey } = generateKeyPairSync(type, {
+        publicKeyEncoding: { format: 'raw-public' },
+        privateKeyEncoding: { format: 'raw-private' },
+      });
+
+      assert(Buffer.isBuffer(publicKey));
+      assert(Buffer.isBuffer(privateKey));
+    }
+  }
+
+  // Test error: raw-seed with SLH-DSA (not supported).
+  {
+    assert.throws(() => generateKeyPairSync('slh-dsa-sha2-128f', {
+      publicKeyEncoding: { format: 'raw-public' },
+      privateKeyEncoding: { format: 'raw-seed' },
+    }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' });
+  }
+
+  // Test async generateKeyPair with raw encoding for PQC types.
+  {
+    generateKeyPair('ml-dsa-44', {
+      publicKeyEncoding: { format: 'raw-public' },
+      privateKeyEncoding: { format: 'raw-seed' },
+    }, common.mustSucceed((publicKey, privateKey) => {
+      assert(Buffer.isBuffer(publicKey));
+      assert(Buffer.isBuffer(privateKey));
+      assert.strictEqual(privateKey.length, 32);
+    }));
+  }
+}