Skip to content

Commit 53cde9d

Browse files
koreahghgaduh95
authored andcommitted
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: #66217 Reviewed-By: Filip Skokan <panva.ip@gmail.com>
1 parent cd16dcb commit 53cde9d

6 files changed

Lines changed: 84 additions & 29 deletions

‎lib/internal/crypto/webcrypto.js‎

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -597,87 +597,100 @@ function exportKeyPkcs8(key) {
597597
}
598598

599599
function exportKeyRawPublic(key, format) {
600+
let exporter;
601+
let exportFormat = kWebCryptoKeyFormatRaw;
600602
switch (getCryptoKeyAlgorithm(key).name) {
601603
case 'ECDSA':
602604
// Fall through
603605
case 'ECDH':
604-
return require('internal/crypto/ec')
605-
.ecExportKey(key, kWebCryptoKeyFormatRaw);
606+
exporter = require('internal/crypto/ec').ecExportKey;
607+
break;
606608
case 'Ed25519':
607609
// Fall through
608610
case 'Ed448':
609611
// Fall through
610612
case 'X25519':
611613
// Fall through
612614
case 'X448':
613-
return require('internal/crypto/cfrg')
614-
.cfrgExportKey(key, kWebCryptoKeyFormatRaw);
615+
exporter = require('internal/crypto/cfrg').cfrgExportKey;
616+
break;
615617
case 'ML-DSA-44':
616618
// Fall through
617619
case 'ML-DSA-65':
618620
// Fall through
619-
case 'ML-DSA-87': {
621+
case 'ML-DSA-87':
620622
// ML-DSA keys don't recognize "raw"
621623
if (format !== 'raw-public') {
622624
return undefined;
623625
}
624-
return require('internal/crypto/ml_dsa')
625-
.mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
626-
}
626+
exporter = require('internal/crypto/ml_dsa').mlDsaExportKey;
627+
break;
627628
case 'ML-KEM-512':
628629
// Fall through
629630
case 'ML-KEM-768':
630631
// Fall through
631-
case 'ML-KEM-1024': {
632+
case 'ML-KEM-1024':
632633
// ML-KEM keys don't recognize "raw"
633634
if (format !== 'raw-public') {
634635
return undefined;
635636
}
636-
return require('internal/crypto/ml_kem')
637-
.mlKemExportKey(key, kWebCryptoKeyFormatRaw);
638-
}
637+
exporter = require('internal/crypto/ml_kem').mlKemExportKey;
638+
break;
639639
case 'MLKEM768-P256':
640640
// Fall through
641641
case 'MLKEM768-X25519':
642642
// Fall through
643-
case 'MLKEM1024-P384': {
643+
case 'MLKEM1024-P384':
644644
if (format !== 'raw-public') {
645645
return undefined;
646646
}
647-
return require('internal/crypto/kem_hybrids')
648-
.kemHybridExportKey(key, format);
649-
}
647+
exporter = require('internal/crypto/kem_hybrids').kemHybridExportKey;
648+
exportFormat = format;
649+
break;
650650
default:
651651
return undefined;
652652
}
653+
654+
if (getCryptoKeyType(key) !== 'public')
655+
throw lazyDOMException('Key must be a public key', 'InvalidAccessError');
656+
657+
return exporter(key, exportFormat);
653658
}
654659

655660
function exportKeyRawSeed(key) {
661+
let exporter;
662+
let exportFormat = kWebCryptoKeyFormatRaw;
656663
switch (getCryptoKeyAlgorithm(key).name) {
657664
case 'ML-DSA-44':
658665
// Fall through
659666
case 'ML-DSA-65':
660667
// Fall through
661668
case 'ML-DSA-87':
662-
return require('internal/crypto/ml_dsa')
663-
.mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
669+
exporter = require('internal/crypto/ml_dsa').mlDsaExportKey;
670+
break;
664671
case 'ML-KEM-512':
665672
// Fall through
666673
case 'ML-KEM-768':
667674
// Fall through
668675
case 'ML-KEM-1024':
669-
return require('internal/crypto/ml_kem')
670-
.mlKemExportKey(key, kWebCryptoKeyFormatRaw);
676+
exporter = require('internal/crypto/ml_kem').mlKemExportKey;
677+
break;
671678
case 'MLKEM768-P256':
672679
// Fall through
673680
case 'MLKEM768-X25519':
674681
// Fall through
675682
case 'MLKEM1024-P384':
676-
return require('internal/crypto/kem_hybrids')
677-
.kemHybridExportKey(key, 'raw-seed');
683+
exporter = require('internal/crypto/kem_hybrids').kemHybridExportKey;
684+
exportFormat = 'raw-seed';
685+
break;
678686
default:
679687
return undefined;
680688
}
689+
690+
if (getCryptoKeyType(key) !== 'private')
691+
throw lazyDOMException('Key must be a private key', 'InvalidAccessError');
692+
693+
return exporter(key, exportFormat);
681694
}
682695

683696
function exportKeyRawSecret(key, format) {
@@ -847,21 +860,17 @@ function exportKeySync(format, key) {
847860
break;
848861
}
849862
case 'raw-public': {
850-
if (type === 'public') {
851-
result = exportKeyRawPublic(key, format);
852-
}
863+
result = exportKeyRawPublic(key, format);
853864
break;
854865
}
855866
case 'raw-seed': {
856-
if (type === 'private') {
857-
result = exportKeyRawSeed(key);
858-
}
867+
result = exportKeyRawSeed(key);
859868
break;
860869
}
861870
case 'raw': {
862871
if (type === 'secret') {
863872
result = exportKeyRawSecret(key, format);
864-
} else if (type === 'public') {
873+
} else {
865874
result = exportKeyRawPublic(key, format);
866875
}
867876
break;

‎test/parallel/test-webcrypto-export-import-cfrg.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,14 @@ async function testImportPkcs8({ name, privateUsages }, extractable) {
172172
assert.strictEqual(
173173
Buffer.from(pkcs8).toString('hex'),
174174
keyData[name].pkcs8.toString('hex'));
175+
176+
for (const format of ['raw', 'raw-public']) {
177+
await assert.rejects(
178+
subtle.exportKey(format, key), {
179+
message: 'Key must be a public key',
180+
name: 'InvalidAccessError',
181+
});
182+
}
175183
} else {
176184
await assert.rejects(
177185
subtle.exportKey('pkcs8', key), {

‎test/parallel/test-webcrypto-export-import-ec.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ async function testImportPkcs8(
175175
message: 'Key must be a public key',
176176
name: 'InvalidAccessError',
177177
});
178+
179+
for (const format of ['raw', 'raw-public']) {
180+
await assert.rejects(
181+
subtle.exportKey(format, key), {
182+
message: 'Key must be a public key',
183+
name: 'InvalidAccessError',
184+
});
185+
}
178186
} else {
179187
await assert.rejects(
180188
subtle.exportKey('pkcs8', key), {

‎test/parallel/test-webcrypto-export-import-ml-dsa.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,11 @@ async function testImportRawPublic({ name, publicUsages }, extractable) {
414414
name: 'NotSupportedError',
415415
message: `Unable to export ${publicKey.algorithm.name} public key using raw format`,
416416
});
417+
418+
await assert.rejects(subtle.exportKey('raw-seed', publicKey), {
419+
name: 'InvalidAccessError',
420+
message: 'Key must be a private key',
421+
});
417422
}
418423

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

458468
await assert.rejects(

‎test/parallel/test-webcrypto-export-import-ml-kem.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ async function testImportRawPublic({ name, publicUsages }, extractable) {
237237
name: 'NotSupportedError',
238238
message: `Unable to export ${publicKey.algorithm.name} public key using raw format`,
239239
});
240+
241+
await assert.rejects(subtle.exportKey('raw-seed', publicKey), {
242+
name: 'InvalidAccessError',
243+
message: 'Key must be a private key',
244+
});
240245
}
241246

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

281291
await assert.rejects(

‎test/parallel/test-webcrypto-wrap-unwrap.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,16 @@ async function testNonByteLengthWrapUnwrap({
587587
name: 'InvalidAccessError',
588588
});
589589

590+
// Exporting a private key as 'raw' must also fail with InvalidAccessError.
591+
await assert.rejects(
592+
subtle.wrapKey('raw', ecKey.privateKey, wrapKey, {
593+
name: 'AES-GCM',
594+
iv: new Uint8Array(12),
595+
}), {
596+
message: 'Key must be a public key',
597+
name: 'InvalidAccessError',
598+
});
599+
590600
// --- unwrapKey validation tests ---
591601

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

0 commit comments

Comments
 (0)