Skip to content

Commit 9ea97a8

Browse files
committed
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>
1 parent 3cd2d6e commit 9ea97a8

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
@@ -591,87 +591,100 @@ function exportKeyPkcs8(key) {
591591
}
592592

593593
function exportKeyRawPublic(key, format) {
594+
let exporter;
595+
let exportFormat = kWebCryptoKeyFormatRaw;
594596
switch (getCryptoKeyAlgorithm(key).name) {
595597
case 'ECDSA':
596598
// Fall through
597599
case 'ECDH':
598-
return require('internal/crypto/ec')
599-
.ecExportKey(key, kWebCryptoKeyFormatRaw);
600+
exporter = require('internal/crypto/ec').ecExportKey;
601+
break;
600602
case 'Ed25519':
601603
// Fall through
602604
case 'Ed448':
603605
// Fall through
604606
case 'X25519':
605607
// Fall through
606608
case 'X448':
607-
return require('internal/crypto/cfrg')
608-
.cfrgExportKey(key, kWebCryptoKeyFormatRaw);
609+
exporter = require('internal/crypto/cfrg').cfrgExportKey;
610+
break;
609611
case 'ML-DSA-44':
610612
// Fall through
611613
case 'ML-DSA-65':
612614
// Fall through
613-
case 'ML-DSA-87': {
615+
case 'ML-DSA-87':
614616
// ML-DSA keys don't recognize "raw"
615617
if (format !== 'raw-public') {
616618
return undefined;
617619
}
618-
return require('internal/crypto/ml_dsa')
619-
.mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
620-
}
620+
exporter = require('internal/crypto/ml_dsa').mlDsaExportKey;
621+
break;
621622
case 'ML-KEM-512':
622623
// Fall through
623624
case 'ML-KEM-768':
624625
// Fall through
625-
case 'ML-KEM-1024': {
626+
case 'ML-KEM-1024':
626627
// ML-KEM keys don't recognize "raw"
627628
if (format !== 'raw-public') {
628629
return undefined;
629630
}
630-
return require('internal/crypto/ml_kem')
631-
.mlKemExportKey(key, kWebCryptoKeyFormatRaw);
632-
}
631+
exporter = require('internal/crypto/ml_kem').mlKemExportKey;
632+
break;
633633
case 'MLKEM768-P256':
634634
// Fall through
635635
case 'MLKEM768-X25519':
636636
// Fall through
637-
case 'MLKEM1024-P384': {
637+
case 'MLKEM1024-P384':
638638
if (format !== 'raw-public') {
639639
return undefined;
640640
}
641-
return require('internal/crypto/kem_hybrids')
642-
.kemHybridExportKey(key, format);
643-
}
641+
exporter = require('internal/crypto/kem_hybrids').kemHybridExportKey;
642+
exportFormat = format;
643+
break;
644644
default:
645645
return undefined;
646646
}
647+
648+
if (getCryptoKeyType(key) !== 'public')
649+
throw lazyDOMException('Key must be a public key', 'InvalidAccessError');
650+
651+
return exporter(key, exportFormat);
647652
}
648653

649654
function exportKeyRawSeed(key) {
655+
let exporter;
656+
let exportFormat = kWebCryptoKeyFormatRaw;
650657
switch (getCryptoKeyAlgorithm(key).name) {
651658
case 'ML-DSA-44':
652659
// Fall through
653660
case 'ML-DSA-65':
654661
// Fall through
655662
case 'ML-DSA-87':
656-
return require('internal/crypto/ml_dsa')
657-
.mlDsaExportKey(key, kWebCryptoKeyFormatRaw);
663+
exporter = require('internal/crypto/ml_dsa').mlDsaExportKey;
664+
break;
658665
case 'ML-KEM-512':
659666
// Fall through
660667
case 'ML-KEM-768':
661668
// Fall through
662669
case 'ML-KEM-1024':
663-
return require('internal/crypto/ml_kem')
664-
.mlKemExportKey(key, kWebCryptoKeyFormatRaw);
670+
exporter = require('internal/crypto/ml_kem').mlKemExportKey;
671+
break;
665672
case 'MLKEM768-P256':
666673
// Fall through
667674
case 'MLKEM768-X25519':
668675
// Fall through
669676
case 'MLKEM1024-P384':
670-
return require('internal/crypto/kem_hybrids')
671-
.kemHybridExportKey(key, 'raw-seed');
677+
exporter = require('internal/crypto/kem_hybrids').kemHybridExportKey;
678+
exportFormat = 'raw-seed';
679+
break;
672680
default:
673681
return undefined;
674682
}
683+
684+
if (getCryptoKeyType(key) !== 'private')
685+
throw lazyDOMException('Key must be a private key', 'InvalidAccessError');
686+
687+
return exporter(key, exportFormat);
675688
}
676689

677690
function exportKeyRawSecret(key, format) {
@@ -835,21 +848,17 @@ function exportKeySync(format, key) {
835848
break;
836849
}
837850
case 'raw-public': {
838-
if (type === 'public') {
839-
result = exportKeyRawPublic(key, format);
840-
}
851+
result = exportKeyRawPublic(key, format);
841852
break;
842853
}
843854
case 'raw-seed': {
844-
if (type === 'private') {
845-
result = exportKeyRawSeed(key);
846-
}
855+
result = exportKeyRawSeed(key);
847856
break;
848857
}
849858
case 'raw': {
850859
if (type === 'secret') {
851860
result = exportKeyRawSecret(key, format);
852-
} else if (type === 'public') {
861+
} else {
853862
result = exportKeyRawPublic(key, format);
854863
}
855864
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
@@ -578,6 +578,16 @@ async function testNonByteLengthWrapUnwrap({
578578
name: 'InvalidAccessError',
579579
});
580580

581+
// Exporting a private key as 'raw' must also fail with InvalidAccessError.
582+
await assert.rejects(
583+
subtle.wrapKey('raw', ecKey.privateKey, wrapKey, {
584+
name: 'AES-GCM',
585+
iv: new Uint8Array(12),
586+
}), {
587+
message: 'Key must be a public key',
588+
name: 'InvalidAccessError',
589+
});
590+
581591
// --- unwrapKey validation tests ---
582592

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

0 commit comments

Comments
 (0)