GeezerVison is showing github.com — crypto: reject ML-KEM/ML-DSA PKCS#8 import without seed in SubtleCrypto · nodejs/node@f5725ca
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/webcrypto.md +4Lines changed: 4 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1260,6 +1260,10 @@ The {CryptoKey} (secret key) generating algorithms supported include: | 1260 | 1260 | <!-- YAML | 1261 | 1261 | added: v15.0.0 | 1262 | 1262 | 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. | 1263 | 1267 | - version: v24.8.0 | 1264 | 1268 | pr-url: https://github.com/nodejs/node/pull/59647 | 1265 | 1269 | description: KMAC algorithms are now supported. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/crypto/ml_dsa.js +13Lines changed: 13 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -192,6 +192,19 @@ function mlDsaImportKey( | 192 | 192 | } | 193 | 193 | case 'pkcs8': { | 194 | 194 | 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 | + | 195 | 208 | try { | 196 | 209 | keyObject = createPrivateKey({ | 197 | 210 | key: keyData, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/crypto/ml_kem.js +13Lines changed: 13 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -182,6 +182,19 @@ function mlKemImportKey( | 182 | 182 | } | 183 | 183 | case 'pkcs8': { | 184 | 184 | 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 | + | 185 | 198 | try { | 186 | 199 | keyObject = createPrivateKey({ | 187 | 200 | key: keyData, | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-webcrypto-export-import-ml-dsa.js +35-28Lines changed: 35 additions & 28 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -12,6 +12,7 @@ if (!hasOpenSSL(3, 5)) | 12 | 12 |
| 13 | 13 | const assert = require('assert'); | 14 | 14 | const { subtle } = globalThis.crypto; | | 15 | +const { createPrivateKey } = require('crypto'); | 15 | 16 |
| 16 | 17 | const fixtures = require('../common/fixtures'); | 17 | 18 |
| @@ -196,41 +197,32 @@ async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) { | 196 | 197 | } | 197 | 198 |
| 198 | 199 | 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', | 218 | 210 | }); | 219 | | - } else { | 220 | | - await assert.rejects( | 221 | | - subtle.exportKey('pkcs8', key), { | 222 | | - message: /key is not extractable/ | 223 | | - }); | 224 | | - } | | 211 | +} | 225 | 212 |
| | 213 | +async function testImportPkcs8MismatchedSeed({ name, privateUsages }, extractable) { | | 214 | + const modified = Buffer.from(keyData[name].pkcs8); | | 215 | + modified[30] ^= 0xff; | 226 | 216 | await assert.rejects( | 227 | 217 | subtle.importKey( | 228 | 218 | 'pkcs8', | 229 | | - keyData[name].pkcs8_seed_only, | | 219 | + modified, | 230 | 220 | { name }, | 231 | 221 | extractable, | 232 | | - [/* empty usages */]), | 233 | | - { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' }); | | 222 | + privateUsages), | | 223 | + { | | 224 | + name: 'DataError', | | 225 | + }); | 234 | 226 | } | 235 | 227 |
| 236 | 228 | async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { | @@ -493,6 +485,7 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { | 493 | 485 | tests.push(testImportPkcs8(vector, extractable)); | 494 | 486 | tests.push(testImportPkcs8SeedOnly(vector, extractable)); | 495 | 487 | tests.push(testImportPkcs8PrivOnly(vector, extractable)); | | 488 | + tests.push(testImportPkcs8MismatchedSeed(vector, extractable)); | 496 | 489 | tests.push(testImportJwk(vector, extractable)); | 497 | 490 | tests.push(testImportRawSeed(vector, extractable)); | 498 | 491 | tests.push(testImportRawPublic(vector, extractable)); | @@ -509,3 +502,17 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { | 509 | 502 | message: 'Unable to import ML-DSA-44 using raw format', | 510 | 503 | }); | 511 | 504 | })().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()); |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-webcrypto-export-import-ml-kem.js +35-28Lines changed: 35 additions & 28 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -12,6 +12,7 @@ if (!hasOpenSSL(3, 5)) | 12 | 12 |
| 13 | 13 | const assert = require('assert'); | 14 | 14 | const { subtle } = globalThis.crypto; | | 15 | +const { createPrivateKey } = require('crypto'); | 15 | 16 |
| 16 | 17 | const fixtures = require('../common/fixtures'); | 17 | 18 |
| @@ -179,41 +180,32 @@ async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) { | 179 | 180 | } | 180 | 181 |
| 181 | 182 | 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', | 201 | 193 | }); | 202 | | - } else { | 203 | | - await assert.rejects( | 204 | | - subtle.exportKey('pkcs8', key), { | 205 | | - message: /key is not extractable/ | 206 | | - }); | 207 | | - } | | 194 | +} | 208 | 195 |
| | 196 | +async function testImportPkcs8MismatchedSeed({ name, privateUsages }, extractable) { | | 197 | + const modified = Buffer.from(keyData[name].pkcs8); | | 198 | + modified[30] ^= 0xff; | 209 | 199 | await assert.rejects( | 210 | 200 | subtle.importKey( | 211 | 201 | 'pkcs8', | 212 | | - keyData[name].pkcs8_seed_only, | | 202 | + modified, | 213 | 203 | { name }, | 214 | 204 | extractable, | 215 | | - [/* empty usages */]), | 216 | | - { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' }); | | 205 | + privateUsages), | | 206 | + { | | 207 | + name: 'DataError', | | 208 | + }); | 217 | 209 | } | 218 | 210 |
| 219 | 211 | async function testImportRawPublic({ name, publicUsages }, extractable) { | @@ -298,6 +290,7 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { | 298 | 290 | tests.push(testImportPkcs8(vector, extractable)); | 299 | 291 | tests.push(testImportPkcs8SeedOnly(vector, extractable)); | 300 | 292 | tests.push(testImportPkcs8PrivOnly(vector, extractable)); | | 293 | + tests.push(testImportPkcs8MismatchedSeed(vector, extractable)); | 301 | 294 | tests.push(testImportRawSeed(vector, extractable)); | 302 | 295 | tests.push(testImportRawPublic(vector, extractable)); | 303 | 296 | } | @@ -313,3 +306,17 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { | 313 | 306 | message: 'Unable to import ML-KEM-512 using raw format', | 314 | 307 | }); | 315 | 308 | })().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()); |
|