Skip to content

Commit b71f46c

Browse files
committed
fix(subtle): tighten WebCrypto JWK import validation across all importers
Address review feedback on PQC JWK import/export PR (#996): - Centralize JWK structural validation in `validateJwkStructure`: - `ext`: existing - `use`: new — must match algorithm's expected use ('sig' or 'enc') when usages are non-empty (mirrors Node's `validateJwk`) - `key_ops`: new duplicate detection per Node's `validateKeyOps` - Reorder validation in all 8 importers (kmac, rsa, hmac, aes, ec, ed, mldsa, mlkem) so JWK structure (kty/use/key_ops/ext/alg) is checked before key-usage validation — matches Node and the WebCrypto spec. - Wrap every `handle.initJwk()` call site in try/catch so C++ errors surface as `DataError` DOMExceptions, not generic `Error`s. - Add type guard in `pqcIsPublicImport` so a non-object passed at `format === 'jwk'` is not silently treated as a public key. - Fix `lazyDOMException` to format the object-form domName correctly (was producing `[object Object]` in the message).
1 parent 33c19e6 commit b71f46c

5 files changed

Lines changed: 260 additions & 170 deletions

File tree

packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ JWK HybridKeyObjectHandle::exportJwk(const JWK& key, bool handleRsaPss) {
365365
}
366366

367367
#if OPENSSL_VERSION_NUMBER >= 0x30500000L
368-
// Export AKP keys (ML-DSA, ML-KEM) per draft-ietf-cose-dilithium and RFC 9269
368+
// Export AKP keys (ML-DSA, ML-KEM)
369369
{
370370
const char* typeName = EVP_PKEY_get0_type_name(pkey.get());
371371
if (typeName != nullptr) {

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

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
kNamedCurveAliases,
2828
lazyDOMException,
2929
normalizeHashName,
30+
validateJwkStructure,
3031
HashContext,
3132
KeyEncoding,
3233
KFormatType,
@@ -93,36 +94,27 @@ export function ecImportKey(
9394
throw lazyDOMException('Unrecognized namedCurve', 'NotSupportedError');
9495
}
9596

96-
// Handle JWK format
9797
if (format === 'jwk') {
9898
const jwk = keyData as JWK;
9999

100-
// Validate JWK
100+
if (!jwk || typeof jwk !== 'object') {
101+
throw lazyDOMException('Invalid keyData', 'DataError');
102+
}
101103
if (jwk.kty !== 'EC') {
102104
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
103105
}
104-
105106
if (jwk.crv !== namedCurve) {
106107
throw lazyDOMException(
107108
'JWK "crv" does not match the requested algorithm',
108109
'DataError',
109110
);
110111
}
112+
const expectedUse = name === 'ECDH' ? 'enc' : 'sig';
113+
validateJwkStructure(jwk, extractable, keyUsages, expectedUse);
111114

112-
// Check use parameter if present
113-
if (jwk.use !== undefined) {
114-
const expectedUse = name === 'ECDH' ? 'enc' : 'sig';
115-
if (jwk.use !== expectedUse) {
116-
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
117-
}
118-
}
119-
120-
// Check alg parameter if present
121115
if (jwk.alg !== undefined) {
122116
let expectedAlg: string | undefined;
123-
124117
if (name === 'ECDSA') {
125-
// Map namedCurve to expected ECDSA algorithm
126118
expectedAlg =
127119
namedCurve === 'P-256'
128120
? 'ES256'
@@ -132,56 +124,69 @@ export function ecImportKey(
132124
? 'ES512'
133125
: undefined;
134126
} else if (name === 'ECDH') {
135-
// ECDH uses ECDH-ES algorithm
136127
expectedAlg = 'ECDH-ES';
137128
}
138-
139-
if (expectedAlg && jwk.alg !== undefined && jwk.alg !== expectedAlg) {
129+
if (expectedAlg && jwk.alg !== expectedAlg) {
140130
throw lazyDOMException(
141131
'JWK "alg" does not match the requested algorithm',
142132
'DataError',
143133
);
144134
}
145135
}
146136

147-
// Import using C++ layer
137+
const isPublicJwk = jwk.d === undefined;
138+
const validUsagesJwk: KeyUsage[] =
139+
name === 'ECDSA'
140+
? isPublicJwk
141+
? ['verify']
142+
: ['sign']
143+
: isPublicJwk
144+
? []
145+
: ['deriveKey', 'deriveBits'];
146+
if (hasAnyNotIn(keyUsages, validUsagesJwk)) {
147+
throw lazyDOMException(
148+
`Unsupported key usage for a ${name} key`,
149+
'SyntaxError',
150+
);
151+
}
152+
148153
const handle =
149154
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
150-
const keyType = handle.initJwk(jwk, namedCurve as NamedCurve);
151-
155+
let keyType: number | undefined;
156+
try {
157+
keyType = handle.initJwk(jwk, namedCurve as NamedCurve);
158+
} catch (err) {
159+
throw lazyDOMException('Invalid keyData', {
160+
name: 'DataError',
161+
cause: err,
162+
});
163+
}
152164
if (keyType === undefined) {
153-
throw lazyDOMException('Invalid JWK', 'DataError');
165+
throw lazyDOMException('Invalid keyData', 'DataError');
154166
}
155167

156-
// Create the appropriate KeyObject based on type
157168
let keyObject: KeyObject;
158169
if (keyType === 1) {
159170
keyObject = new PublicKeyObject(handle);
160171
} else if (keyType === 2) {
161172
keyObject = new PrivateKeyObject(handle);
162173
} else {
163-
throw lazyDOMException(
164-
'Unexpected key type from JWK import',
165-
'DataError',
166-
);
174+
throw lazyDOMException('Invalid keyData', 'DataError');
167175
}
168176

169177
return new CryptoKey(keyObject, algorithm, keyUsages, extractable);
170178
}
171179

172-
// Handle binary formats (spki, pkcs8, raw)
173180
if (format !== 'spki' && format !== 'pkcs8' && format !== 'raw') {
174181
throw lazyDOMException(
175182
`Unsupported format: ${format}`,
176183
'NotSupportedError',
177184
);
178185
}
179186

180-
// Determine expected key type based on format
181187
const expectedKeyType =
182188
format === 'spki' || format === 'raw' ? 'public' : 'private';
183189

184-
// Validate usages for the key type
185190
const isPublicKey = expectedKeyType === 'public';
186191
let validUsages: KeyUsage[];
187192

0 commit comments

Comments
 (0)