Skip to content

Commit 8861722

Browse files
panvaaduh95
authored andcommitted
crypto: validate JWK usages before key_ops
Check requested usages against the JWK public or private key type before validating key_ops. This preserves the SyntaxError precedence specified for RSA, EC, CFRG, ML-DSA, and ML-KEM imports. Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65550 Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 37df4ac commit 8861722

10 files changed

Lines changed: 186 additions & 31 deletions

lib/internal/crypto/cfrg.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ function cfrgImportKey(
146146
break;
147147
}
148148
case 'jwk': {
149+
const isPublic = keyData.d === undefined;
150+
verifyAcceptableKeyUse(
151+
name,
152+
usagesSet,
153+
isPublic ? allowedUsages.public : allowedUsages.private);
154+
149155
const expectedUse = (name === 'X25519' || name === 'X448') ? 'enc' : 'sig';
150156
validateJwk(keyData, 'OKP', extractable, usagesSet, expectedUse);
151157

@@ -159,11 +165,6 @@ function cfrgImportKey(
159165
'JWK "alg" does not match the requested algorithm', 'DataError');
160166
}
161167

162-
const isPublic = keyData.d === undefined;
163-
verifyAcceptableKeyUse(
164-
name,
165-
usagesSet,
166-
isPublic ? allowedUsages.public : allowedUsages.private);
167168
handle = importJwkKey(isPublic, keyData);
168169
break;
169170
}

lib/internal/crypto/ec.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ function ecImportKey(
164164
break;
165165
}
166166
case 'jwk': {
167+
const isPublic = keyData.d === undefined;
168+
verifyAcceptableKeyUse(
169+
name,
170+
usagesSet,
171+
isPublic ? allowedUsages.public : allowedUsages.private);
172+
167173
const expectedUse = name === 'ECDH' ? 'enc' : 'sig';
168174
validateJwk(keyData, 'EC', extractable, usagesSet, expectedUse);
169175

@@ -185,11 +191,6 @@ function ecImportKey(
185191
'DataError');
186192
}
187193

188-
const isPublic = keyData.d === undefined;
189-
verifyAcceptableKeyUse(
190-
name,
191-
usagesSet,
192-
isPublic ? allowedUsages.public : allowedUsages.private);
193194
handle = importJwkKey(isPublic, keyData);
194195
break;
195196
}

lib/internal/crypto/ml_dsa.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,17 +158,18 @@ function mlDsaImportKey(
158158
break;
159159
}
160160
case 'jwk': {
161+
const isPublic = keyData.priv === undefined;
162+
verifyAcceptableKeyUse(
163+
name,
164+
usagesSet,
165+
isPublic ? kUsages.public : kUsages.private);
166+
161167
validateJwk(keyData, 'AKP', extractable, usagesSet, 'sig');
162168

163169
if (keyData.alg !== name)
164170
throw lazyDOMException(
165171
'JWK "alg" Parameter and algorithm name mismatch', 'DataError');
166172

167-
const isPublic = keyData.priv === undefined;
168-
verifyAcceptableKeyUse(
169-
name,
170-
usagesSet,
171-
isPublic ? kUsages.public : kUsages.private);
172173
handle = importJwkKey(isPublic, keyData);
173174
break;
174175
}

lib/internal/crypto/ml_kem.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,17 +169,18 @@ function mlKemImportKey(
169169
break;
170170
}
171171
case 'jwk': {
172+
const isPublic = keyData.priv === undefined;
173+
verifyAcceptableKeyUse(
174+
name,
175+
usagesSet,
176+
isPublic ? kUsages.public : kUsages.private);
177+
172178
validateJwk(keyData, 'AKP', extractable, usagesSet, 'enc');
173179

174180
if (keyData.alg !== name)
175181
throw lazyDOMException(
176182
'JWK "alg" Parameter and algorithm name mismatch', 'DataError');
177183

178-
const isPublic = keyData.priv === undefined;
179-
verifyAcceptableKeyUse(
180-
name,
181-
usagesSet,
182-
isPublic ? kUsages.public : kUsages.private);
183184
handle = importJwkKey(isPublic, keyData);
184185
break;
185186
}

lib/internal/crypto/rsa.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ function rsaImportKey(
186186
break;
187187
}
188188
case 'jwk': {
189+
const isPublic = keyData.d === undefined;
190+
verifyAcceptableKeyUse(
191+
algorithm.name,
192+
usagesSet,
193+
isPublic ? allowedUsages.public : allowedUsages.private);
194+
189195
const expectedUse = algorithm.name === 'RSA-OAEP' ? 'enc' : 'sig';
190196
validateJwk(keyData, 'RSA', extractable, usagesSet, expectedUse);
191197

@@ -202,11 +208,6 @@ function rsaImportKey(
202208
'DataError');
203209
}
204210

205-
const isPublic = keyData.d === undefined;
206-
verifyAcceptableKeyUse(
207-
algorithm.name,
208-
usagesSet,
209-
isPublic ? allowedUsages.public : allowedUsages.private);
210211
handle = importJwkKey(isPublic, keyData);
211212
break;
212213
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,41 @@ async function testImportRaw({ name, publicUsages }) {
432432
await Promise.all(tests);
433433
})().then(common.mustCall());
434434

435+
// JWK key usage validation precedes `key_ops` validation.
436+
(async function() {
437+
for (const { name, publicUsages, privateUsages } of testVectors) {
438+
const jwk = keyData[name].jwk;
439+
const publicJwk = {
440+
kty: jwk.kty,
441+
crv: jwk.crv,
442+
x: jwk.x,
443+
};
444+
const isKeyAgreement = name.startsWith('X');
445+
const invalidUsage = isKeyAgreement ?
446+
privateUsages[0] : publicUsages[0];
447+
const invalidJwk = isKeyAgreement ? publicJwk : jwk;
448+
449+
await assert.rejects(
450+
subtle.importKey(
451+
'jwk',
452+
{ ...invalidJwk, key_ops: [invalidUsage, invalidUsage] },
453+
{ name },
454+
true,
455+
[invalidUsage]),
456+
{ name: 'SyntaxError', message: /Unsupported key usage/ });
457+
458+
const validUsage = privateUsages[0];
459+
await assert.rejects(
460+
subtle.importKey(
461+
'jwk',
462+
{ ...jwk, key_ops: [validUsage, validUsage] },
463+
{ name },
464+
true,
465+
[validUsage]),
466+
{ name: 'DataError', message: 'Duplicate key operation' });
467+
}
468+
})().then(common.mustCall());
469+
435470
{
436471
const rsaPublic = crypto.createPublicKey(
437472
fixtures.readKey('rsa_public_2048.pem'));

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,42 @@ async function testImportRaw({ name, publicUsages }, namedCurve) {
412412
await Promise.all(tests);
413413
})().then(common.mustCall());
414414

415+
// JWK key usage validation precedes `key_ops` validation.
416+
(async function() {
417+
const jwk = keyData['P-256'].jwk;
418+
const publicJwk = {
419+
kty: jwk.kty,
420+
crv: jwk.crv,
421+
x: jwk.x,
422+
y: jwk.y,
423+
};
424+
425+
for (const { name, publicUsages, privateUsages } of testVectors) {
426+
const invalidUsage = name === 'ECDH' ?
427+
privateUsages[0] : publicUsages[0];
428+
const invalidJwk = name === 'ECDH' ? publicJwk : jwk;
429+
430+
await assert.rejects(
431+
subtle.importKey(
432+
'jwk',
433+
{ ...invalidJwk, key_ops: [invalidUsage, invalidUsage] },
434+
{ name, namedCurve: 'P-256' },
435+
true,
436+
[invalidUsage]),
437+
{ name: 'SyntaxError', message: /Unsupported key usage/ });
438+
439+
const validUsage = privateUsages[0];
440+
await assert.rejects(
441+
subtle.importKey(
442+
'jwk',
443+
{ ...jwk, key_ops: [validUsage, validUsage] },
444+
{ name, namedCurve: 'P-256' },
445+
true,
446+
[validUsage]),
447+
{ name: 'DataError', message: 'Duplicate key operation' });
448+
}
449+
})().then(common.mustCall());
450+
415451

416452
// https://github.com/nodejs/node/issues/45859
417453
(async function() {

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,40 @@ async function testImportRawSeed({ name, privateUsages }, extractable) {
491491
});
492492
})().then(common.mustCall());
493493

494+
// JWK key usage validation precedes `key_ops` validation.
495+
(async function() {
496+
const privateJwk = keyData['ML-DSA-65'].jwk;
497+
const publicJwk = { ...privateJwk, priv: undefined };
498+
499+
for (const [jwk, usage] of [
500+
[privateJwk, 'verify'],
501+
[publicJwk, 'sign'],
502+
]) {
503+
await assert.rejects(
504+
subtle.importKey(
505+
'jwk',
506+
{ ...jwk, key_ops: [usage, usage] },
507+
'ML-DSA-65',
508+
true,
509+
[usage]),
510+
{ name: 'SyntaxError', message: /Unsupported key usage/ });
511+
}
512+
513+
for (const [jwk, usage] of [
514+
[privateJwk, 'sign'],
515+
[publicJwk, 'verify'],
516+
]) {
517+
await assert.rejects(
518+
subtle.importKey(
519+
'jwk',
520+
{ ...jwk, key_ops: [usage, usage] },
521+
'ML-DSA-65',
522+
true,
523+
[usage]),
524+
{ name: 'DataError', message: /Duplicate key operation/ });
525+
}
526+
})().then(common.mustCall());
527+
494528
if (!process.features.openssl_is_boringssl) {
495529
(async function() {
496530
for (const { name, privateUsages } of testVectors) {

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -496,13 +496,29 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable)
496496
});
497497
})().then(common.mustCall());
498498

499-
// Regression test: JWK `key_ops` validation must recognize ML-KEM operations
500-
// (encapsulateKey, encapsulateBits, decapsulateKey, decapsulateBits) so that
501-
// duplicate entries are rejected
499+
// JWK key usage validation precedes `key_ops` validation.
502500
(async function() {
503-
for (const op of ['encapsulateKey', 'encapsulateBits',
504-
'decapsulateKey', 'decapsulateBits']) {
505-
const jwk = { ...keyData['ML-KEM-768'].jwk, key_ops: [op, op] };
501+
const privateJwk = keyData['ML-KEM-768'].jwk;
502+
const encapsulationOps = ['encapsulateKey', 'encapsulateBits'];
503+
const decapsulationOps = ['decapsulateKey', 'decapsulateBits'];
504+
505+
for (const op of encapsulationOps) {
506+
const jwk = { ...privateJwk, key_ops: [op, op] };
507+
await assert.rejects(
508+
subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]),
509+
{ name: 'SyntaxError', message: /Unsupported key usage/ });
510+
}
511+
512+
// Duplicate entries are still rejected when the requested usages are valid.
513+
for (const op of encapsulationOps) {
514+
const jwk = { ...privateJwk, priv: undefined, key_ops: [op, op] };
515+
await assert.rejects(
516+
subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]),
517+
{ name: 'DataError', message: /Duplicate key operation/ });
518+
}
519+
520+
for (const op of decapsulationOps) {
521+
const jwk = { ...privateJwk, key_ops: [op, op] };
506522
await assert.rejects(
507523
subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]),
508524
{ name: 'DataError', message: /Duplicate key operation/ });

test/parallel/test-webcrypto-export-import-rsa.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,35 @@ const testVectors = [
651651
await Promise.all(variations);
652652
})().then(common.mustCall());
653653

654+
// Type-specific JWK usage validation precedes `key_ops` validation.
655+
(async function() {
656+
const privateJwk = keyData[1024].jwk;
657+
658+
for (const { name, publicUsages, privateUsages } of testVectors) {
659+
const algorithm = { name, hash: 'SHA-256' };
660+
const invalidUsage = publicUsages[0];
661+
const validUsage = privateUsages[0];
662+
663+
await assert.rejects(
664+
subtle.importKey(
665+
'jwk',
666+
{ ...privateJwk, key_ops: [invalidUsage, invalidUsage] },
667+
algorithm,
668+
true,
669+
[invalidUsage]),
670+
{ name: 'SyntaxError', message: /Unsupported key usage/ });
671+
672+
await assert.rejects(
673+
subtle.importKey(
674+
'jwk',
675+
{ ...privateJwk, key_ops: [validUsage, validUsage] },
676+
algorithm,
677+
true,
678+
[validUsage]),
679+
{ name: 'DataError', message: 'Duplicate key operation' });
680+
}
681+
})().then(common.mustCall());
682+
654683
{
655684
const ecPublic = crypto.createPublicKey(
656685
fixtures.readKey('ec_p256_public.pem'));

0 commit comments

Comments
 (0)