Skip to content

Commit f5725ca

Browse files
panvaaduh95
authored andcommitted
crypto: reject ML-KEM/ML-DSA PKCS#8 import without seed in SubtleCrypto
Reject importing ML-KEM and ML-DSA PKCS#8 private keys that do not include a seed, throwing NotSupportedError. Also add tests for importing PKCS#8 keys with a mismatched expanded key. Refs: https://redirect.github.com/WICG/webcrypto-modern-algos/pull/34 PR-URL: #62218 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Mattias Buelens <mattias@buelens.com>
1 parent e254f65 commit f5725ca

File tree

5 files changed

+100
-56
lines changed

5 files changed

+100
-56
lines changed

doc/api/webcrypto.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,10 @@ The {CryptoKey} (secret key) generating algorithms supported include:
12601260
<!-- YAML
12611261
added: v15.0.0
12621262
changes:
1263+
- version: REPLACEME
1264+
pr-url: https://github.com/nodejs/node/pull/62218
1265+
description: Importing ML-DSA and ML-KEM PKCS#8 keys
1266+
without a seed is no longer supported.
12631267
- version: v24.8.0
12641268
pr-url: https://github.com/nodejs/node/pull/59647
12651269
description: KMAC algorithms are now supported.

lib/internal/crypto/ml_dsa.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ function mlDsaImportKey(
192192
}
193193
case 'pkcs8': {
194194
verifyAcceptableMlDsaKeyUse(name, false, usagesSet);
195+
196+
const privOnlyLengths = {
197+
'__proto__': null,
198+
'ML-DSA-44': 2588,
199+
'ML-DSA-65': 4060,
200+
'ML-DSA-87': 4924,
201+
};
202+
if (keyData.byteLength === privOnlyLengths[name]) {
203+
throw lazyDOMException(
204+
'Importing an ML-DSA PKCS#8 key without a seed is not supported',
205+
'NotSupportedError');
206+
}
207+
195208
try {
196209
keyObject = createPrivateKey({
197210
key: keyData,

lib/internal/crypto/ml_kem.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,19 @@ function mlKemImportKey(
182182
}
183183
case 'pkcs8': {
184184
verifyAcceptableMlKemKeyUse(name, false, usagesSet);
185+
186+
const privOnlyLengths = {
187+
'__proto__': null,
188+
'ML-KEM-512': 1660,
189+
'ML-KEM-768': 2428,
190+
'ML-KEM-1024': 3196,
191+
};
192+
if (keyData.byteLength === privOnlyLengths[name]) {
193+
throw lazyDOMException(
194+
'Importing an ML-KEM PKCS#8 key without a seed is not supported',
195+
'NotSupportedError');
196+
}
197+
185198
try {
186199
keyObject = createPrivateKey({
187200
key: keyData,

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

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ if (!hasOpenSSL(3, 5))
1212

1313
const assert = require('assert');
1414
const { subtle } = globalThis.crypto;
15+
const { createPrivateKey } = require('crypto');
1516

1617
const fixtures = require('../common/fixtures');
1718

@@ -196,41 +197,32 @@ async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) {
196197
}
197198

198199
async function testImportPkcs8PrivOnly({ name, privateUsages }, extractable) {
199-
const key = await subtle.importKey(
200-
'pkcs8',
201-
keyData[name].pkcs8_priv_only,
202-
{ name },
203-
extractable,
204-
privateUsages);
205-
assert.strictEqual(key.type, 'private');
206-
assert.strictEqual(key.extractable, extractable);
207-
assert.deepStrictEqual(key.usages, privateUsages);
208-
assert.deepStrictEqual(key.algorithm.name, name);
209-
assert.strictEqual(key.algorithm, key.algorithm);
210-
assert.strictEqual(key.usages, key.usages);
211-
212-
if (extractable) {
213-
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
214-
assert.strictEqual(err.name, 'OperationError');
215-
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
216-
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
217-
return true;
200+
await assert.rejects(
201+
subtle.importKey(
202+
'pkcs8',
203+
keyData[name].pkcs8_priv_only,
204+
{ name },
205+
extractable,
206+
privateUsages),
207+
{
208+
name: 'NotSupportedError',
209+
message: 'Importing an ML-DSA PKCS#8 key without a seed is not supported',
218210
});
219-
} else {
220-
await assert.rejects(
221-
subtle.exportKey('pkcs8', key), {
222-
message: /key is not extractable/
223-
});
224-
}
211+
}
225212

213+
async function testImportPkcs8MismatchedSeed({ name, privateUsages }, extractable) {
214+
const modified = Buffer.from(keyData[name].pkcs8);
215+
modified[30] ^= 0xff;
226216
await assert.rejects(
227217
subtle.importKey(
228218
'pkcs8',
229-
keyData[name].pkcs8_seed_only,
219+
modified,
230220
{ name },
231221
extractable,
232-
[/* empty usages */]),
233-
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
222+
privateUsages),
223+
{
224+
name: 'DataError',
225+
});
234226
}
235227

236228
async function testImportJwk({ name, publicUsages, privateUsages }, extractable) {
@@ -493,6 +485,7 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
493485
tests.push(testImportPkcs8(vector, extractable));
494486
tests.push(testImportPkcs8SeedOnly(vector, extractable));
495487
tests.push(testImportPkcs8PrivOnly(vector, extractable));
488+
tests.push(testImportPkcs8MismatchedSeed(vector, extractable));
496489
tests.push(testImportJwk(vector, extractable));
497490
tests.push(testImportRawSeed(vector, extractable));
498491
tests.push(testImportRawPublic(vector, extractable));
@@ -509,3 +502,17 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
509502
message: 'Unable to import ML-DSA-44 using raw format',
510503
});
511504
})().then(common.mustCall());
505+
506+
(async function() {
507+
for (const { name, privateUsages } of testVectors) {
508+
const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii');
509+
const keyObject = createPrivateKey(pem);
510+
const key = keyObject.toCryptoKey({ name }, true, privateUsages);
511+
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
512+
assert.strictEqual(err.name, 'OperationError');
513+
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
514+
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
515+
return true;
516+
});
517+
}
518+
})().then(common.mustCall());

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

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ if (!hasOpenSSL(3, 5))
1212

1313
const assert = require('assert');
1414
const { subtle } = globalThis.crypto;
15+
const { createPrivateKey } = require('crypto');
1516

1617
const fixtures = require('../common/fixtures');
1718

@@ -179,41 +180,32 @@ async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) {
179180
}
180181

181182
async function testImportPkcs8PrivOnly({ name, privateUsages }, extractable) {
182-
const key = await subtle.importKey(
183-
'pkcs8',
184-
keyData[name].pkcs8_priv_only,
185-
{ name },
186-
extractable,
187-
privateUsages);
188-
assert.strictEqual(key.type, 'private');
189-
assert.strictEqual(key.extractable, extractable);
190-
assert.deepStrictEqual(key.usages, privateUsages);
191-
assert.deepStrictEqual(key.algorithm.name, name);
192-
assert.strictEqual(key.algorithm, key.algorithm);
193-
assert.strictEqual(key.usages, key.usages);
194-
195-
if (extractable) {
196-
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
197-
assert.strictEqual(err.name, 'OperationError');
198-
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
199-
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
200-
return true;
183+
await assert.rejects(
184+
subtle.importKey(
185+
'pkcs8',
186+
keyData[name].pkcs8_priv_only,
187+
{ name },
188+
extractable,
189+
privateUsages),
190+
{
191+
name: 'NotSupportedError',
192+
message: 'Importing an ML-KEM PKCS#8 key without a seed is not supported',
201193
});
202-
} else {
203-
await assert.rejects(
204-
subtle.exportKey('pkcs8', key), {
205-
message: /key is not extractable/
206-
});
207-
}
194+
}
208195

196+
async function testImportPkcs8MismatchedSeed({ name, privateUsages }, extractable) {
197+
const modified = Buffer.from(keyData[name].pkcs8);
198+
modified[30] ^= 0xff;
209199
await assert.rejects(
210200
subtle.importKey(
211201
'pkcs8',
212-
keyData[name].pkcs8_seed_only,
202+
modified,
213203
{ name },
214204
extractable,
215-
[/* empty usages */]),
216-
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
205+
privateUsages),
206+
{
207+
name: 'DataError',
208+
});
217209
}
218210

219211
async function testImportRawPublic({ name, publicUsages }, extractable) {
@@ -298,6 +290,7 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
298290
tests.push(testImportPkcs8(vector, extractable));
299291
tests.push(testImportPkcs8SeedOnly(vector, extractable));
300292
tests.push(testImportPkcs8PrivOnly(vector, extractable));
293+
tests.push(testImportPkcs8MismatchedSeed(vector, extractable));
301294
tests.push(testImportRawSeed(vector, extractable));
302295
tests.push(testImportRawPublic(vector, extractable));
303296
}
@@ -313,3 +306,17 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
313306
message: 'Unable to import ML-KEM-512 using raw format',
314307
});
315308
})().then(common.mustCall());
309+
310+
(async function() {
311+
for (const { name, privateUsages } of testVectors) {
312+
const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii');
313+
const keyObject = createPrivateKey(pem);
314+
const key = keyObject.toCryptoKey({ name }, true, privateUsages);
315+
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
316+
assert.strictEqual(err.name, 'OperationError');
317+
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
318+
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
319+
return true;
320+
});
321+
}
322+
})().then(common.mustCall());

0 commit comments

Comments
 (0)