Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions example/src/tests/subtle/import_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand Down
41 changes: 41 additions & 0 deletions packages/react-native-quick-crypto/src/subtle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>('KeyObjectHandle');
let keyType: KeyType | undefined;
Expand Down Expand Up @@ -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>('KeyObjectHandle');
Expand Down
Loading