Skip to content

Commit 3076769

Browse files
authored
fix: validate JWK alg/crv/kty per algorithm in subtle.importKey (#1021)
1 parent 28be337 commit 3076769

2 files changed

Lines changed: 213 additions & 0 deletions

File tree

example/src/tests/subtle/import_export.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2898,6 +2898,178 @@ for (const { name: curveName, rawSize } of edCurves) {
28982898
});
28992899
}
29002900

2901+
// --- JWK validation: per-algorithm alg/crv/use mismatch (gh#1001) ---
2902+
2903+
const RSA_JWK_ALG_CASES: {
2904+
name: RSAKeyPairAlgorithm;
2905+
hash: HashAlgorithm;
2906+
expected: string;
2907+
wrong: string;
2908+
usages: KeyUsage[];
2909+
}[] = [
2910+
{
2911+
name: 'RSASSA-PKCS1-v1_5',
2912+
hash: 'SHA-256',
2913+
expected: 'RS256',
2914+
wrong: 'RS384',
2915+
usages: ['verify'],
2916+
},
2917+
{
2918+
name: 'RSA-PSS',
2919+
hash: 'SHA-256',
2920+
expected: 'PS256',
2921+
wrong: 'PS384',
2922+
usages: ['verify'],
2923+
},
2924+
{
2925+
name: 'RSA-OAEP',
2926+
hash: 'SHA-256',
2927+
expected: 'RSA-OAEP-256',
2928+
wrong: 'RSA-OAEP-384',
2929+
usages: ['encrypt'],
2930+
},
2931+
];
2932+
2933+
for (const { name, hash, wrong, usages } of RSA_JWK_ALG_CASES) {
2934+
test(SUITE, `${name}/${hash} importKey rejects wrong jwk alg`, async () => {
2935+
const keyPair = (await subtle.generateKey(
2936+
{
2937+
name,
2938+
modulusLength: 2048,
2939+
publicExponent: new Uint8Array([1, 0, 1]),
2940+
hash,
2941+
},
2942+
true,
2943+
name === 'RSA-OAEP' ? ['encrypt', 'decrypt'] : ['sign', 'verify'],
2944+
)) as CryptoKeyPair;
2945+
const jwk = (await subtle.exportKey(
2946+
'jwk',
2947+
keyPair.publicKey as CryptoKey,
2948+
)) as JWK;
2949+
await assertThrowsAsync(
2950+
async () =>
2951+
await subtle.importKey(
2952+
'jwk',
2953+
{ ...jwk, alg: wrong },
2954+
{ name, hash },
2955+
true,
2956+
usages,
2957+
),
2958+
'JWK "alg" does not match the requested algorithm',
2959+
);
2960+
});
2961+
}
2962+
2963+
for (const { name } of edCurves) {
2964+
test(SUITE, `${name} importKey rejects wrong jwk crv`, async () => {
2965+
const keyPair = (await subtle.generateKey({ name }, true, [
2966+
'sign',
2967+
'verify',
2968+
])) as CryptoKeyPair;
2969+
const jwk = (await subtle.exportKey(
2970+
'jwk',
2971+
keyPair.publicKey as CryptoKey,
2972+
)) as JWK;
2973+
const wrongCrv = name === 'Ed25519' ? 'Ed448' : 'Ed25519';
2974+
await assertThrowsAsync(
2975+
async () =>
2976+
await subtle.importKey(
2977+
'jwk',
2978+
{ ...jwk, crv: wrongCrv },
2979+
{ name },
2980+
true,
2981+
['verify'],
2982+
),
2983+
'JWK "crv" Parameter and algorithm name mismatch',
2984+
);
2985+
});
2986+
2987+
test(SUITE, `${name} importKey rejects wrong jwk alg`, async () => {
2988+
const keyPair = (await subtle.generateKey({ name }, true, [
2989+
'sign',
2990+
'verify',
2991+
])) as CryptoKeyPair;
2992+
const jwk = (await subtle.exportKey(
2993+
'jwk',
2994+
keyPair.publicKey as CryptoKey,
2995+
)) as JWK;
2996+
await assertThrowsAsync(
2997+
async () =>
2998+
await subtle.importKey(
2999+
'jwk',
3000+
{ ...jwk, alg: 'RS256' },
3001+
{ name },
3002+
true,
3003+
['verify'],
3004+
),
3005+
'JWK "alg" does not match the requested algorithm',
3006+
);
3007+
});
3008+
3009+
test(SUITE, `${name} importKey accepts jwk alg "EdDSA"`, async () => {
3010+
const keyPair = (await subtle.generateKey({ name }, true, [
3011+
'sign',
3012+
'verify',
3013+
])) as CryptoKeyPair;
3014+
const jwk = (await subtle.exportKey(
3015+
'jwk',
3016+
keyPair.publicKey as CryptoKey,
3017+
)) as JWK;
3018+
const imported = await subtle.importKey(
3019+
'jwk',
3020+
{ ...jwk, alg: 'EdDSA' },
3021+
{ name },
3022+
true,
3023+
['verify'],
3024+
);
3025+
expect(imported.algorithm.name).to.equal(name);
3026+
});
3027+
3028+
test(SUITE, `${name} importKey rejects wrong jwk kty`, async () => {
3029+
const keyPair = (await subtle.generateKey({ name }, true, [
3030+
'sign',
3031+
'verify',
3032+
])) as CryptoKeyPair;
3033+
const jwk = (await subtle.exportKey(
3034+
'jwk',
3035+
keyPair.publicKey as CryptoKey,
3036+
)) as JWK;
3037+
await assertThrowsAsync(
3038+
async () =>
3039+
await subtle.importKey('jwk', { ...jwk, kty: 'EC' }, { name }, true, [
3040+
'verify',
3041+
]),
3042+
'Invalid JWK "kty" Parameter',
3043+
);
3044+
});
3045+
}
3046+
3047+
const xCurves = ['X25519', 'X448'] as const;
3048+
for (const name of xCurves) {
3049+
test(SUITE, `${name} importKey rejects wrong jwk crv`, async () => {
3050+
const keyPair = (await subtle.generateKey({ name }, true, [
3051+
'deriveKey',
3052+
'deriveBits',
3053+
])) as CryptoKeyPair;
3054+
const jwk = (await subtle.exportKey(
3055+
'jwk',
3056+
keyPair.publicKey as CryptoKey,
3057+
)) as JWK;
3058+
const wrongCrv = name === 'X25519' ? 'X448' : 'X25519';
3059+
await assertThrowsAsync(
3060+
async () =>
3061+
await subtle.importKey(
3062+
'jwk',
3063+
{ ...jwk, crv: wrongCrv },
3064+
{ name },
3065+
true,
3066+
[],
3067+
),
3068+
'JWK "crv" Parameter and algorithm name mismatch',
3069+
);
3070+
});
3071+
}
3072+
29013073
// AES-OCB JWK export/import roundtrip
29023074
test(SUITE, 'AES-OCB export/import jwk', async () => {
29033075
const key = await subtle.generateKey({ name: 'AES-OCB', length: 256 }, true, [

packages/react-native-quick-crypto/src/subtle.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,27 @@ function rsaImportKey(
957957
validateJwkStructure(jwk, extractable, keyUsages, expectedUse);
958958
checkUsages();
959959

960+
if (jwk.alg !== undefined) {
961+
let jwkContext: HashContext;
962+
switch (name) {
963+
case 'RSASSA-PKCS1-v1_5':
964+
jwkContext = HashContext.JwkRsa;
965+
break;
966+
case 'RSA-PSS':
967+
jwkContext = HashContext.JwkRsaPss;
968+
break;
969+
default:
970+
jwkContext = HashContext.JwkRsaOaep;
971+
}
972+
const expectedAlg = normalizeHashName(algorithm.hash, jwkContext);
973+
if (jwk.alg !== expectedAlg) {
974+
throw lazyDOMException(
975+
'JWK "alg" does not match the requested algorithm',
976+
'DataError',
977+
);
978+
}
979+
}
980+
960981
const handle =
961982
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
962983
let keyType: KeyType | undefined;
@@ -1246,8 +1267,28 @@ function edImportKey(
12461267
if (!jwkData || typeof jwkData !== 'object') {
12471268
throw lazyDOMException('Invalid keyData', 'DataError');
12481269
}
1270+
if (jwkData.kty !== 'OKP') {
1271+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
1272+
}
12491273
const expectedUse = isX ? 'enc' : 'sig';
12501274
validateJwkStructure(jwkData, extractable, keyUsages, expectedUse);
1275+
1276+
if (jwkData.crv !== name) {
1277+
throw lazyDOMException(
1278+
'JWK "crv" Parameter and algorithm name mismatch',
1279+
'DataError',
1280+
);
1281+
}
1282+
1283+
if (!isX && jwkData.alg !== undefined) {
1284+
if (jwkData.alg !== name && jwkData.alg !== 'EdDSA') {
1285+
throw lazyDOMException(
1286+
'JWK "alg" does not match the requested algorithm',
1287+
'DataError',
1288+
);
1289+
}
1290+
}
1291+
12511292
checkUsages();
12521293
const handle =
12531294
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');

0 commit comments

Comments
 (0)