diff --git a/example/src/tests/subtle/import_export.ts b/example/src/tests/subtle/import_export.ts index dc648bd1..12c8fc4f 100644 --- a/example/src/tests/subtle/import_export.ts +++ b/example/src/tests/subtle/import_export.ts @@ -2898,6 +2898,178 @@ for (const { name: curveName, rawSize } of edCurves) { }); } +// --- JWK validation: per-algorithm alg/crv/use mismatch (gh#1001) --- + +const RSA_JWK_ALG_CASES: { + name: RSAKeyPairAlgorithm; + hash: HashAlgorithm; + expected: string; + wrong: string; + usages: KeyUsage[]; +}[] = [ + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + expected: 'RS256', + wrong: 'RS384', + usages: ['verify'], + }, + { + name: 'RSA-PSS', + hash: 'SHA-256', + expected: 'PS256', + wrong: 'PS384', + usages: ['verify'], + }, + { + name: 'RSA-OAEP', + hash: 'SHA-256', + expected: 'RSA-OAEP-256', + wrong: 'RSA-OAEP-384', + usages: ['encrypt'], + }, +]; + +for (const { name, hash, wrong, usages } of RSA_JWK_ALG_CASES) { + test(SUITE, `${name}/${hash} importKey rejects wrong jwk alg`, async () => { + const keyPair = (await subtle.generateKey( + { + name, + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash, + }, + true, + name === 'RSA-OAEP' ? ['encrypt', 'decrypt'] : ['sign', 'verify'], + )) as CryptoKeyPair; + const jwk = (await subtle.exportKey( + 'jwk', + keyPair.publicKey as CryptoKey, + )) as JWK; + await assertThrowsAsync( + async () => + await subtle.importKey( + 'jwk', + { ...jwk, alg: wrong }, + { name, hash }, + true, + usages, + ), + 'JWK "alg" does not match the requested algorithm', + ); + }); +} + +for (const { name } of edCurves) { + test(SUITE, `${name} importKey rejects wrong jwk crv`, async () => { + const keyPair = (await subtle.generateKey({ name }, true, [ + 'sign', + 'verify', + ])) as CryptoKeyPair; + const jwk = (await subtle.exportKey( + 'jwk', + keyPair.publicKey as CryptoKey, + )) as JWK; + const wrongCrv = name === 'Ed25519' ? 'Ed448' : 'Ed25519'; + await assertThrowsAsync( + async () => + await subtle.importKey( + 'jwk', + { ...jwk, crv: wrongCrv }, + { name }, + true, + ['verify'], + ), + 'JWK "crv" Parameter and algorithm name mismatch', + ); + }); + + test(SUITE, `${name} importKey rejects wrong jwk alg`, async () => { + const keyPair = (await subtle.generateKey({ name }, true, [ + 'sign', + 'verify', + ])) as CryptoKeyPair; + const jwk = (await subtle.exportKey( + 'jwk', + keyPair.publicKey as CryptoKey, + )) as JWK; + await assertThrowsAsync( + async () => + await subtle.importKey( + 'jwk', + { ...jwk, alg: 'RS256' }, + { name }, + true, + ['verify'], + ), + 'JWK "alg" does not match the requested algorithm', + ); + }); + + test(SUITE, `${name} importKey accepts jwk alg "EdDSA"`, async () => { + const keyPair = (await subtle.generateKey({ name }, true, [ + 'sign', + 'verify', + ])) as CryptoKeyPair; + const jwk = (await subtle.exportKey( + 'jwk', + keyPair.publicKey as CryptoKey, + )) as JWK; + const imported = await subtle.importKey( + 'jwk', + { ...jwk, alg: 'EdDSA' }, + { name }, + true, + ['verify'], + ); + expect(imported.algorithm.name).to.equal(name); + }); + + test(SUITE, `${name} importKey rejects wrong jwk kty`, async () => { + const keyPair = (await subtle.generateKey({ name }, true, [ + 'sign', + 'verify', + ])) as CryptoKeyPair; + const jwk = (await subtle.exportKey( + 'jwk', + keyPair.publicKey as CryptoKey, + )) as JWK; + await assertThrowsAsync( + async () => + await subtle.importKey('jwk', { ...jwk, kty: 'EC' }, { name }, true, [ + 'verify', + ]), + 'Invalid JWK "kty" Parameter', + ); + }); +} + +const xCurves = ['X25519', 'X448'] as const; +for (const name of xCurves) { + test(SUITE, `${name} importKey rejects wrong jwk crv`, async () => { + const keyPair = (await subtle.generateKey({ name }, true, [ + 'deriveKey', + 'deriveBits', + ])) as CryptoKeyPair; + const jwk = (await subtle.exportKey( + 'jwk', + keyPair.publicKey as CryptoKey, + )) as JWK; + const wrongCrv = name === 'X25519' ? 'X448' : 'X25519'; + await assertThrowsAsync( + async () => + await subtle.importKey( + 'jwk', + { ...jwk, crv: wrongCrv }, + { name }, + true, + [], + ), + 'JWK "crv" Parameter and algorithm name mismatch', + ); + }); +} + // AES-OCB JWK export/import roundtrip test(SUITE, 'AES-OCB export/import jwk', async () => { const key = await subtle.generateKey({ name: 'AES-OCB', length: 256 }, true, [ diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index 7e0eade3..2a4c08b7 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -957,6 +957,27 @@ function rsaImportKey( validateJwkStructure(jwk, extractable, keyUsages, expectedUse); checkUsages(); + if (jwk.alg !== undefined) { + let jwkContext: HashContext; + switch (name) { + case 'RSASSA-PKCS1-v1_5': + jwkContext = HashContext.JwkRsa; + break; + case 'RSA-PSS': + jwkContext = HashContext.JwkRsaPss; + break; + default: + jwkContext = HashContext.JwkRsaOaep; + } + const expectedAlg = normalizeHashName(algorithm.hash, jwkContext); + if (jwk.alg !== expectedAlg) { + throw lazyDOMException( + 'JWK "alg" does not match the requested algorithm', + 'DataError', + ); + } + } + const handle = NitroModules.createHybridObject('KeyObjectHandle'); let keyType: KeyType | undefined; @@ -1246,8 +1267,28 @@ function edImportKey( if (!jwkData || typeof jwkData !== 'object') { throw lazyDOMException('Invalid keyData', 'DataError'); } + if (jwkData.kty !== 'OKP') { + throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + } const expectedUse = isX ? 'enc' : 'sig'; validateJwkStructure(jwkData, extractable, keyUsages, expectedUse); + + if (jwkData.crv !== name) { + throw lazyDOMException( + 'JWK "crv" Parameter and algorithm name mismatch', + 'DataError', + ); + } + + if (!isX && jwkData.alg !== undefined) { + if (jwkData.alg !== name && jwkData.alg !== 'EdDSA') { + throw lazyDOMException( + 'JWK "alg" does not match the requested algorithm', + 'DataError', + ); + } + } + checkUsages(); const handle = NitroModules.createHybridObject('KeyObjectHandle');