Skip to content

Commit 077adfd

Browse files
Jake ChampionJakeChampion
authored andcommitted
feat: Implement subset of crypto.subtle.verify which can verify a signature with a JSONWebKey using RSASSA-PKCS1-v1_5
1 parent 52bc9d0 commit 077adfd

File tree

7 files changed

+929
-149
lines changed

7 files changed

+929
-149
lines changed

c-dependencies/js-compute-runtime/builtins/crypto-key.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ EVP_PKEY *CryptoKey::key(JSObject *self) {
618618

619619
JS::Result<bool> CryptoKey::is_algorithm(JSContext *cx, JS::HandleObject self,
620620
CryptoAlgorithmIdentifier algorithm) {
621-
MOZ_ASSERT(is_instance(self));
621+
MOZ_ASSERT(CryptoKey::is_instance(self));
622622
JS::RootedObject self_algorithm(cx, JS::GetReservedSlot(self, Slots::Algorithm).toObjectOrNull());
623623
MOZ_ASSERT(self_algorithm != nullptr);
624624
JS::Rooted<JS::Value> name_val(cx);
@@ -649,4 +649,10 @@ bool CryptoKey::canSign(JS::HandleObject self) {
649649
return usage.canSign();
650650
}
651651

652+
bool CryptoKey::canVerify(JS::HandleObject self) {
653+
MOZ_ASSERT(is_instance(self));
654+
auto usage = CryptoKeyUsages(JS::GetReservedSlot(self, Slots::Usages).toInt32());
655+
return usage.canVerify();
656+
}
657+
652658
} // namespace builtins

c-dependencies/js-compute-runtime/builtins/crypto-key.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class CryptoKey : public BuiltinImpl<CryptoKey> {
118118
static JSObject *get_algorithm(JS::HandleObject self);
119119
static EVP_PKEY *key(JSObject *self);
120120
static bool canSign(JS::HandleObject self);
121+
static bool canVerify(JS::HandleObject self);
121122
static JS::Result<bool> is_algorithm(JSContext *cx, JS::HandleObject self,
122123
CryptoAlgorithmIdentifier algorithm);
123124
};

c-dependencies/js-compute-runtime/builtins/subtle-crypto.cpp

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,112 @@ bool SubtleCrypto::sign(JSContext *cx, unsigned argc, JS::Value *vp) {
264264
return true;
265265
}
266266

267-
const JSFunctionSpec SubtleCrypto::methods[] = {JS_FN("digest", digest, 2, JSPROP_ENUMERATE),
268-
JS_FN("importKey", importKey, 5, JSPROP_ENUMERATE),
269-
JS_FN("sign", sign, 3, JSPROP_ENUMERATE),
270-
JS_FS_END};
267+
// Promise<any> verify(AlgorithmIdentifier algorithm,
268+
// CryptoKey key,
269+
// BufferSource signature,
270+
// BufferSource data);
271+
// https://w3c.github.io/webcrypto/#SubtleCrypto-method-verify
272+
bool SubtleCrypto::verify(JSContext *cx, unsigned argc, JS::Value *vp) {
273+
MOZ_ASSERT(cx);
274+
MOZ_ASSERT(vp);
275+
JS::CallArgs args = CallArgsFromVp(argc, vp);
276+
277+
if (!args.requireAtLeast(cx, "SubtleCrypto.verify", 4)) {
278+
return ReturnPromiseRejectedWithPendingError(cx, args);
279+
}
280+
if (!check_receiver(cx, args.thisv(), "SubtleCrypto.verify")) {
281+
return ReturnPromiseRejectedWithPendingError(cx, args);
282+
}
283+
284+
// 1. Let algorithm and key be the algorithm and key parameters passed to the verify() method,
285+
// respectively.
286+
auto algorithm = args.get(0);
287+
JS::RootedObject key(cx);
288+
{
289+
auto key_arg = args.get(1);
290+
if (!key_arg.isObject()) {
291+
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
292+
return ReturnPromiseRejectedWithPendingError(cx, args);
293+
}
294+
key.set(&key_arg.toObject());
295+
296+
if (!CryptoKey::is_instance(key)) {
297+
JS_ReportErrorASCII(
298+
cx, "SubtleCrypto.verify: key (argument 2) does not implement interface CryptoKey");
299+
return ReturnPromiseRejectedWithPendingError(cx, args);
300+
}
301+
}
302+
303+
// 2. Let signature be the result of getting a copy of the bytes held by the signature
304+
// parameter passed to the verify() method.
305+
std::optional<std::span<uint8_t>> signature =
306+
value_to_buffer(cx, args.get(2), "SubtleCrypto.verify: signature (argument 3)");
307+
if (!signature) {
308+
return ReturnPromiseRejectedWithPendingError(cx, args);
309+
}
310+
311+
// 3. Let data be the result of getting a copy of the bytes held by the data parameter passed
312+
// to the verify() method.
313+
std::optional<std::span<uint8_t>> data =
314+
value_to_buffer(cx, args.get(3), "SubtleCrypto.verify: data (argument 4)");
315+
if (!data) {
316+
return ReturnPromiseRejectedWithPendingError(cx, args);
317+
}
318+
319+
// 4. Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to
320+
// algorithm and op set to "verify".
321+
// 5. If an error occurred, return a Promise rejected with normalizedAlgorithm.
322+
auto normalizedAlgorithm = CryptoAlgorithmSignVerify::normalize(cx, algorithm);
323+
if (!normalizedAlgorithm) {
324+
// TODO Rename error to NotSupportedError
325+
return ReturnPromiseRejectedWithPendingError(cx, args);
326+
}
327+
328+
// 6. Let promise be a new Promise.
329+
JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
330+
if (!promise) {
331+
return ReturnPromiseRejectedWithPendingError(cx, args);
332+
}
333+
334+
// 7. Return promise and perform the remaining steps in parallel.
335+
args.rval().setObject(*promise);
336+
337+
// 8. If the following steps or referenced procedures say to throw an error, reject promise
338+
// with the returned error and then terminate the algorithm.
339+
// 9. If the name member of normalizedAlgorithm is not equal to the name attribute of the
340+
// [[algorithm]] internal slot of key then throw an InvalidAccessError.
341+
auto identifier = normalizedAlgorithm->identifier();
342+
auto match_result = CryptoKey::is_algorithm(cx, key, identifier);
343+
if (match_result.isErr() || match_result.unwrap() == false) {
344+
// TODO: Change to an InvalidAccessError instance
345+
JS_ReportErrorUTF8(cx, "CryptoKey doesn't match AlgorithmIdentifier");
346+
return RejectPromiseWithPendingError(cx, promise);
347+
}
348+
// 10. If the [[usages]] internal slot of key does not contain an entry that is "verify", then
349+
// throw an InvalidAccessError.
350+
if (!CryptoKey::canVerify(key)) {
351+
// TODO: Change to an InvalidAccessError instance
352+
JS_ReportErrorUTF8(cx, "CryptoKey doesn't support verification");
353+
return RejectPromiseWithPendingError(cx, promise);
354+
}
355+
// 11. Let result be the result of performing the verify operation specified by
356+
// normalizedAlgorithm using key, algorithm and signature and with data as message.
357+
358+
auto matchResult = normalizedAlgorithm->verify(cx, key, signature.value(), data.value());
359+
if (matchResult.isErr()) {
360+
return RejectPromiseWithPendingError(cx, promise);
361+
}
362+
// 12. Resolve promise with result.
363+
JS::RootedValue result(cx);
364+
result.setBoolean(matchResult.unwrap());
365+
JS::ResolvePromise(cx, promise, result);
366+
367+
return true;
368+
}
369+
const JSFunctionSpec SubtleCrypto::methods[] = {
370+
JS_FN("digest", digest, 2, JSPROP_ENUMERATE),
371+
JS_FN("importKey", importKey, 5, JSPROP_ENUMERATE), JS_FN("sign", sign, 3, JSPROP_ENUMERATE),
372+
JS_FN("verify", verify, 4, JSPROP_ENUMERATE), JS_FS_END};
271373

272374
const JSPropertySpec SubtleCrypto::properties[] = {
273375
JS_STRING_SYM_PS(toStringTag, "SubtleCrypto", JSPROP_READONLY), JS_PS_END};

c-dependencies/js-compute-runtime/builtins/subtle-crypto.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class SubtleCrypto : public BuiltinImpl<SubtleCrypto> {
3232
static bool digest(JSContext *cx, unsigned argc, JS::Value *vp);
3333
static bool importKey(JSContext *cx, unsigned argc, JS::Value *vp);
3434
static bool sign(JSContext *cx, unsigned argc, JS::Value *vp);
35+
static bool verify(JSContext *cx, unsigned argc, JS::Value *vp);
3536

3637
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
3738
static bool init_class(JSContext *cx, JS::HandleObject global);

integration-tests/js-compute/fixtures/crypto/bin/index.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,4 +708,142 @@ routes.set("/crypto.subtle", async () => {
708708
return pass();
709709
});
710710
}
711+
}
712+
713+
// verify
714+
{
715+
routes.set("/crypto.subtle.verify", async () => {
716+
error = assert(typeof crypto.subtle.verify, 'function', `typeof crypto.subtle.verify`)
717+
if (error) { return error; }
718+
error = assert(crypto.subtle.verify, SubtleCrypto.prototype.verify, `crypto.subtle.verify === SubtleCrypto.prototype.verify`)
719+
if (error) { return error; }
720+
return pass();
721+
});
722+
routes.set("/crypto.subtle.verify/length", async () => {
723+
error = assert(crypto.subtle.verify.length, 4, `crypto.subtle.verify.length === 4`)
724+
if (error) { return error; }
725+
return pass();
726+
});
727+
routes.set("/crypto.subtle.verify/called-as-constructor", async () => {
728+
error = assertThrows(() => {
729+
new crypto.subtle.verify
730+
}, TypeError, "crypto.subtle.verify is not a constructor")
731+
if (error) { return error; }
732+
return pass();
733+
});
734+
routes.set("/crypto.subtle.verify/called-with-wrong-this", async () => {
735+
error = await assertRejects(async () => {
736+
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
737+
await crypto.subtle.verify.call(undefined, jsonWebKeyAlgorithm, key, new Uint8Array, new Uint8Array)
738+
}, TypeError, "Method SubtleCrypto.verify called on receiver that's not an instance of SubtleCrypto")
739+
if (error) { return error; }
740+
return pass();
741+
});
742+
routes.set("/crypto.subtle.verify/called-with-no-arguments", async () => {
743+
error = await assertRejects(async () => {
744+
await crypto.subtle.verify()
745+
}, TypeError, "SubtleCrypto.verify: At least 4 arguments required, but only 0 passed")
746+
if (error) { return error; }
747+
return pass();
748+
});
749+
// first-parameter
750+
{
751+
routes.set("/crypto.subtle.verify/first-parameter-calls-7.1.17-ToString", async () => {
752+
const sentinel = Symbol("sentinel");
753+
const test = async () => {
754+
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
755+
await crypto.subtle.verify({
756+
name: {
757+
toString() {
758+
throw sentinel;
759+
}
760+
}
761+
}, key, new Uint8Array, new Uint8Array);
762+
}
763+
let error = await assertRejects(test)
764+
if (error) { return error; }
765+
try {
766+
await test()
767+
} catch (thrownError) {
768+
let error = assert(thrownError, sentinel, 'thrownError === sentinel')
769+
if (error) { return error; }
770+
}
771+
return pass();
772+
});
773+
routes.set("/crypto.subtle.verify/first-parameter-non-existant-algorithm", async () => {
774+
let error = await assertRejects(async () => {
775+
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
776+
await crypto.subtle.verify('jake', key, new Uint8Array, new Uint8Array)
777+
}, Error, "Algorithm: Unrecognized name")
778+
if (error) { return error; }
779+
return pass();
780+
});
781+
}
782+
// second-parameter
783+
{
784+
routes.set("/crypto.subtle.verify/second-parameter-invalid-format", async () => {
785+
let error = await assertRejects(async () => {
786+
await crypto.subtle.verify(jsonWebKeyAlgorithm, "jake", new Uint8Array, new Uint8Array)
787+
}, Error, "parameter 2 is not of type 'CryptoKey'")
788+
if (error) { return error; }
789+
return pass();
790+
});
791+
routes.set("/crypto.subtle.verify/second-parameter-invalid-usages", async () => {
792+
let error = await assertRejects(async () => {
793+
const key = await crypto.subtle.importKey('jwk', privateJsonWebKeyData, jsonWebKeyAlgorithm, privateJsonWebKeyData.ext, privateJsonWebKeyData.key_ops);
794+
await crypto.subtle.verify(jsonWebKeyAlgorithm, key, new Uint8Array(), new Uint8Array());
795+
}, Error, "CryptoKey doesn't support verification")
796+
if (error) { return error; }
797+
return pass();
798+
});
799+
}
800+
// third-parameter
801+
{
802+
routes.set("/crypto.subtle.verify/third-parameter-invalid-format", async () => {
803+
let error = await assertRejects(async () => {
804+
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
805+
await crypto.subtle.verify(jsonWebKeyAlgorithm, key, undefined, new Uint8Array());
806+
}, Error, "SubtleCrypto.verify: signature (argument 3) must be of type ArrayBuffer or ArrayBufferView but got \"\"")
807+
if (error) { return error; }
808+
return pass();
809+
});
810+
}
811+
// fourth-parameter
812+
{
813+
routes.set("/crypto.subtle.verify/fourth-parameter-invalid-format", async () => {
814+
let error = await assertRejects(async () => {
815+
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
816+
await crypto.subtle.verify(jsonWebKeyAlgorithm, key, new Uint8Array(), undefined);
817+
}, Error, "SubtleCrypto.verify: data (argument 4) must be of type ArrayBuffer or ArrayBufferView but got \"\"")
818+
if (error) { return error; }
819+
return pass();
820+
});
821+
}
822+
// incorrect-signature
823+
{
824+
routes.set("/crypto.subtle.verify/incorrect-signature", async () => {
825+
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
826+
const signature = new Uint8Array;
827+
const enc = new TextEncoder();
828+
const data = enc.encode('hello world');
829+
const result = await crypto.subtle.verify(jsonWebKeyAlgorithm, key, signature, data);
830+
error = assert(result, false, "result === false");
831+
if (error) { return error; }
832+
return pass();
833+
});
834+
}
835+
// correct-signature
836+
{
837+
routes.set("/crypto.subtle.verify/incorrect-signature", async () => {
838+
const pkey = await crypto.subtle.importKey('jwk', privateJsonWebKeyData, jsonWebKeyAlgorithm, privateJsonWebKeyData.ext, privateJsonWebKeyData.key_ops);
839+
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
840+
const enc = new TextEncoder();
841+
const data = enc.encode('hello world');
842+
const signature = await crypto.subtle.sign(jsonWebKeyAlgorithm, pkey, data);
843+
const result = await crypto.subtle.verify(jsonWebKeyAlgorithm, key, signature, data);
844+
error = assert(result, true, "result === true");
845+
if (error) { return error; }
846+
return pass();
847+
});
848+
}
711849
}

0 commit comments

Comments
 (0)