Skip to content

Commit 8ccbe8e

Browse files
authored
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 aac5b68 commit 8ccbe8e

File tree

5 files changed

+100
-58
lines changed

5 files changed

+100
-58
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
@@ -191,6 +191,19 @@ function mlDsaImportKey(
191191
}
192192
case 'pkcs8': {
193193
verifyAcceptableMlDsaKeyUse(name, false, usagesSet);
194+
195+
const privOnlyLengths = {
196+
'__proto__': null,
197+
'ML-DSA-44': 2588,
198+
'ML-DSA-65': 4060,
199+
'ML-DSA-87': 4924,
200+
};
201+
if (keyData.byteLength === privOnlyLengths[name]) {
202+
throw lazyDOMException(
203+
'Importing an ML-DSA PKCS#8 key without a seed is not supported',
204+
'NotSupportedError');
205+
}
206+
194207
try {
195208
keyObject = createPrivateKey({
196209
key: keyData,

lib/internal/crypto/ml_kem.js

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

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

Lines changed: 35 additions & 29 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

@@ -199,42 +200,32 @@ async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) {
199200
}
200201

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

216+
async function testImportPkcs8MismatchedSeed({ name, privateUsages }, extractable) {
217+
const modified = Buffer.from(keyData[name].pkcs8);
218+
modified[30] ^= 0xff;
230219
await assert.rejects(
231220
subtle.importKey(
232221
'pkcs8',
233-
keyData[name].pkcs8_seed_only,
222+
modified,
234223
{ name },
235224
extractable,
236-
[/* empty usages */]),
237-
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
225+
privateUsages),
226+
{
227+
name: 'DataError',
228+
});
238229
}
239230

240231
async function testImportJwk({ name, publicUsages, privateUsages }, extractable) {
@@ -499,6 +490,7 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
499490
tests.push(testImportPkcs8(vector, extractable));
500491
tests.push(testImportPkcs8SeedOnly(vector, extractable));
501492
tests.push(testImportPkcs8PrivOnly(vector, extractable));
493+
tests.push(testImportPkcs8MismatchedSeed(vector, extractable));
502494
tests.push(testImportJwk(vector, extractable));
503495
tests.push(testImportRawSeed(vector, extractable));
504496
tests.push(testImportRawPublic(vector, extractable));
@@ -515,3 +507,17 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
515507
message: 'Unable to import ML-DSA-44 using raw format',
516508
});
517509
})().then(common.mustCall());
510+
511+
(async function() {
512+
for (const { name, privateUsages } of testVectors) {
513+
const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii');
514+
const keyObject = createPrivateKey(pem);
515+
const key = keyObject.toCryptoKey({ name }, true, privateUsages);
516+
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
517+
assert.strictEqual(err.name, 'OperationError');
518+
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
519+
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
520+
return true;
521+
});
522+
}
523+
})().then(common.mustCall());

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

Lines changed: 35 additions & 29 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

@@ -182,42 +183,32 @@ async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) {
182183
}
183184

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

199+
async function testImportPkcs8MismatchedSeed({ name, privateUsages }, extractable) {
200+
const modified = Buffer.from(keyData[name].pkcs8);
201+
modified[30] ^= 0xff;
213202
await assert.rejects(
214203
subtle.importKey(
215204
'pkcs8',
216-
keyData[name].pkcs8_seed_only,
205+
modified,
217206
{ name },
218207
extractable,
219-
[/* empty usages */]),
220-
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' });
208+
privateUsages),
209+
{
210+
name: 'DataError',
211+
});
221212
}
222213

223214
async function testImportRawPublic({ name, publicUsages }, extractable) {
@@ -302,6 +293,7 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
302293
tests.push(testImportPkcs8(vector, extractable));
303294
tests.push(testImportPkcs8SeedOnly(vector, extractable));
304295
tests.push(testImportPkcs8PrivOnly(vector, extractable));
296+
tests.push(testImportPkcs8MismatchedSeed(vector, extractable));
305297
tests.push(testImportRawSeed(vector, extractable));
306298
tests.push(testImportRawPublic(vector, extractable));
307299
}
@@ -317,3 +309,17 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
317309
message: 'Unable to import ML-KEM-512 using raw format',
318310
});
319311
})().then(common.mustCall());
312+
313+
(async function() {
314+
for (const { name, privateUsages } of testVectors) {
315+
const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii');
316+
const keyObject = createPrivateKey(pem);
317+
const key = keyObject.toCryptoKey({ name }, true, privateUsages);
318+
await assert.rejects(subtle.exportKey('pkcs8', key), (err) => {
319+
assert.strictEqual(err.name, 'OperationError');
320+
assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED');
321+
assert.strictEqual(err.cause.message, 'Failed to get raw seed');
322+
return true;
323+
});
324+
}
325+
})().then(common.mustCall());

0 commit comments

Comments
 (0)