From 1a4fc6f3b34ce77a6e98d327451688a6f6ad3fa3 Mon Sep 17 00:00:00 2001 From: koreahghg Date: Fri, 28 Aug 2026 14:34:01 +0900 Subject: [PATCH] crypto: fix public PKCS8 export error exportKeySpki() already rejects exporting a private key as 'spki' with an InvalidAccessError, per the Web Crypto export key algorithm steps for each of RSA, EC, CFRG, ML-DSA, and ML-KEM. exportKeyPkcs8() was missing the symmetric check: exporting a public key as 'pkcs8' fell through to the generic "Unable to export ... key using pkcs8 format" NotSupportedError instead of the spec-mandated InvalidAccessError. Add the same key-type check to exportKeyPkcs8(), mirroring exportKeySpki(), and drop the now-redundant type guard around its call site in exportKeySync(). This also fixes wrapKey(), which delegates to the same export path. Add test coverage for both subtle.exportKey('pkcs8', publicKey) and subtle.wrapKey('pkcs8', publicKey, ...). Signed-off-by: koreahghg --- lib/internal/crypto/webcrypto.js | 30 +++++++++++-------- .../test-webcrypto-export-import-ec.js | 6 ++++ test/parallel/test-webcrypto-wrap-unwrap.js | 11 +++++++ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 342b5ee05fe0..f8a1ef8d00e7 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -530,19 +530,20 @@ function exportKeySpki(key) { } function exportKeyPkcs8(key) { + let exporter; switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': - return require('internal/crypto/rsa') - .rsaExportKey(key, kWebCryptoKeyFormatPKCS8); + exporter = require('internal/crypto/rsa').rsaExportKey; + break; case 'ECDSA': // Fall through case 'ECDH': - return require('internal/crypto/ec') - .ecExportKey(key, kWebCryptoKeyFormatPKCS8); + exporter = require('internal/crypto/ec').ecExportKey; + break; case 'Ed25519': // Fall through case 'Ed448': @@ -550,25 +551,30 @@ function exportKeyPkcs8(key) { case 'X25519': // Fall through case 'X448': - return require('internal/crypto/cfrg') - .cfrgExportKey(key, kWebCryptoKeyFormatPKCS8); + exporter = require('internal/crypto/cfrg').cfrgExportKey; + break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - return require('internal/crypto/ml_dsa') - .mlDsaExportKey(key, kWebCryptoKeyFormatPKCS8); + 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, kWebCryptoKeyFormatPKCS8); + exporter = require('internal/crypto/ml_kem').mlKemExportKey; + break; default: return undefined; } + + if (getCryptoKeyType(key) !== 'private') + throw lazyDOMException('Key must be a private key', 'InvalidAccessError'); + + return exporter(key, kWebCryptoKeyFormatPKCS8); } function exportKeyRawPublic(key, format) { @@ -777,9 +783,7 @@ function exportKeySync(format, key) { break; } case 'pkcs8': { - if (type === 'private') { - result = exportKeyPkcs8(key); - } + result = exportKeyPkcs8(key); break; } case 'jwk': { diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index 978aad14ebd9..f4f36ceeff5a 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -120,6 +120,12 @@ async function testImportSpki({ name, publicUsages }, namedCurve, extractable) { assert.strictEqual( Buffer.from(spki).toString('hex'), keyData[namedCurve].spki.toString('hex')); + + await assert.rejects( + subtle.exportKey('pkcs8', key), { + message: 'Key must be a private key', + name: 'InvalidAccessError', + }); } else { await assert.rejects( subtle.exportKey('spki', key), { diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 64ff4c5f5342..5ffffe230ac6 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -567,6 +567,17 @@ async function testNonByteLengthWrapUnwrap({ name: 'InvalidAccessError', }); + // Symmetric case: exporting a public key as 'pkcs8' must also fail with + // InvalidAccessError, not the generic NotSupportedError. + await assert.rejects( + subtle.wrapKey('pkcs8', ecKey.publicKey, wrapKey, { + name: 'AES-GCM', + iv: new Uint8Array(12), + }), { + message: 'Key must be a private key', + name: 'InvalidAccessError', + }); + // --- unwrapKey validation tests --- const ciphertext = new Uint8Array(32); // Dummy ciphertext