Skip to content

Commit

Permalink
feat: Implement subset of crypto.subtle.verify which can verify a sig…
Browse files Browse the repository at this point in the history
…nature with a JSONWebKey using RSASSA-PKCS1-v1_5
  • Loading branch information
JakeChampion committed Apr 15, 2023
1 parent 52bc9d0 commit 077adfd
Show file tree
Hide file tree
Showing 7 changed files with 929 additions and 149 deletions.
8 changes: 7 additions & 1 deletion c-dependencies/js-compute-runtime/builtins/crypto-key.cpp
Expand Up @@ -618,7 +618,7 @@ EVP_PKEY *CryptoKey::key(JSObject *self) {

JS::Result<bool> CryptoKey::is_algorithm(JSContext *cx, JS::HandleObject self,
CryptoAlgorithmIdentifier algorithm) {
MOZ_ASSERT(is_instance(self));
MOZ_ASSERT(CryptoKey::is_instance(self));
JS::RootedObject self_algorithm(cx, JS::GetReservedSlot(self, Slots::Algorithm).toObjectOrNull());
MOZ_ASSERT(self_algorithm != nullptr);
JS::Rooted<JS::Value> name_val(cx);
Expand Down Expand Up @@ -649,4 +649,10 @@ bool CryptoKey::canSign(JS::HandleObject self) {
return usage.canSign();
}

bool CryptoKey::canVerify(JS::HandleObject self) {
MOZ_ASSERT(is_instance(self));
auto usage = CryptoKeyUsages(JS::GetReservedSlot(self, Slots::Usages).toInt32());
return usage.canVerify();
}

} // namespace builtins
1 change: 1 addition & 0 deletions c-dependencies/js-compute-runtime/builtins/crypto-key.h
Expand Up @@ -118,6 +118,7 @@ class CryptoKey : public BuiltinImpl<CryptoKey> {
static JSObject *get_algorithm(JS::HandleObject self);
static EVP_PKEY *key(JSObject *self);
static bool canSign(JS::HandleObject self);
static bool canVerify(JS::HandleObject self);
static JS::Result<bool> is_algorithm(JSContext *cx, JS::HandleObject self,
CryptoAlgorithmIdentifier algorithm);
};
Expand Down
110 changes: 106 additions & 4 deletions c-dependencies/js-compute-runtime/builtins/subtle-crypto.cpp
Expand Up @@ -264,10 +264,112 @@ bool SubtleCrypto::sign(JSContext *cx, unsigned argc, JS::Value *vp) {
return true;
}

const JSFunctionSpec SubtleCrypto::methods[] = {JS_FN("digest", digest, 2, JSPROP_ENUMERATE),
JS_FN("importKey", importKey, 5, JSPROP_ENUMERATE),
JS_FN("sign", sign, 3, JSPROP_ENUMERATE),
JS_FS_END};
// Promise<any> verify(AlgorithmIdentifier algorithm,
// CryptoKey key,
// BufferSource signature,
// BufferSource data);
// https://w3c.github.io/webcrypto/#SubtleCrypto-method-verify
bool SubtleCrypto::verify(JSContext *cx, unsigned argc, JS::Value *vp) {
MOZ_ASSERT(cx);
MOZ_ASSERT(vp);
JS::CallArgs args = CallArgsFromVp(argc, vp);

if (!args.requireAtLeast(cx, "SubtleCrypto.verify", 4)) {
return ReturnPromiseRejectedWithPendingError(cx, args);
}
if (!check_receiver(cx, args.thisv(), "SubtleCrypto.verify")) {
return ReturnPromiseRejectedWithPendingError(cx, args);
}

// 1. Let algorithm and key be the algorithm and key parameters passed to the verify() method,
// respectively.
auto algorithm = args.get(0);
JS::RootedObject key(cx);
{
auto key_arg = args.get(1);
if (!key_arg.isObject()) {
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
return ReturnPromiseRejectedWithPendingError(cx, args);
}
key.set(&key_arg.toObject());

if (!CryptoKey::is_instance(key)) {
JS_ReportErrorASCII(
cx, "SubtleCrypto.verify: key (argument 2) does not implement interface CryptoKey");
return ReturnPromiseRejectedWithPendingError(cx, args);
}
}

// 2. Let signature be the result of getting a copy of the bytes held by the signature
// parameter passed to the verify() method.
std::optional<std::span<uint8_t>> signature =
value_to_buffer(cx, args.get(2), "SubtleCrypto.verify: signature (argument 3)");
if (!signature) {
return ReturnPromiseRejectedWithPendingError(cx, args);
}

// 3. Let data be the result of getting a copy of the bytes held by the data parameter passed
// to the verify() method.
std::optional<std::span<uint8_t>> data =
value_to_buffer(cx, args.get(3), "SubtleCrypto.verify: data (argument 4)");
if (!data) {
return ReturnPromiseRejectedWithPendingError(cx, args);
}

// 4. Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to
// algorithm and op set to "verify".
// 5. If an error occurred, return a Promise rejected with normalizedAlgorithm.
auto normalizedAlgorithm = CryptoAlgorithmSignVerify::normalize(cx, algorithm);
if (!normalizedAlgorithm) {
// TODO Rename error to NotSupportedError
return ReturnPromiseRejectedWithPendingError(cx, args);
}

// 6. Let promise be a new Promise.
JS::RootedObject promise(cx, JS::NewPromiseObject(cx, nullptr));
if (!promise) {
return ReturnPromiseRejectedWithPendingError(cx, args);
}

// 7. Return promise and perform the remaining steps in parallel.
args.rval().setObject(*promise);

// 8. If the following steps or referenced procedures say to throw an error, reject promise
// with the returned error and then terminate the algorithm.
// 9. If the name member of normalizedAlgorithm is not equal to the name attribute of the
// [[algorithm]] internal slot of key then throw an InvalidAccessError.
auto identifier = normalizedAlgorithm->identifier();
auto match_result = CryptoKey::is_algorithm(cx, key, identifier);
if (match_result.isErr() || match_result.unwrap() == false) {
// TODO: Change to an InvalidAccessError instance
JS_ReportErrorUTF8(cx, "CryptoKey doesn't match AlgorithmIdentifier");
return RejectPromiseWithPendingError(cx, promise);
}
// 10. If the [[usages]] internal slot of key does not contain an entry that is "verify", then
// throw an InvalidAccessError.
if (!CryptoKey::canVerify(key)) {
// TODO: Change to an InvalidAccessError instance
JS_ReportErrorUTF8(cx, "CryptoKey doesn't support verification");
return RejectPromiseWithPendingError(cx, promise);
}
// 11. Let result be the result of performing the verify operation specified by
// normalizedAlgorithm using key, algorithm and signature and with data as message.

auto matchResult = normalizedAlgorithm->verify(cx, key, signature.value(), data.value());
if (matchResult.isErr()) {
return RejectPromiseWithPendingError(cx, promise);
}
// 12. Resolve promise with result.
JS::RootedValue result(cx);
result.setBoolean(matchResult.unwrap());
JS::ResolvePromise(cx, promise, result);

return true;
}
const JSFunctionSpec SubtleCrypto::methods[] = {
JS_FN("digest", digest, 2, JSPROP_ENUMERATE),
JS_FN("importKey", importKey, 5, JSPROP_ENUMERATE), JS_FN("sign", sign, 3, JSPROP_ENUMERATE),
JS_FN("verify", verify, 4, JSPROP_ENUMERATE), JS_FS_END};

const JSPropertySpec SubtleCrypto::properties[] = {
JS_STRING_SYM_PS(toStringTag, "SubtleCrypto", JSPROP_READONLY), JS_PS_END};
Expand Down
1 change: 1 addition & 0 deletions c-dependencies/js-compute-runtime/builtins/subtle-crypto.h
Expand Up @@ -32,6 +32,7 @@ class SubtleCrypto : public BuiltinImpl<SubtleCrypto> {
static bool digest(JSContext *cx, unsigned argc, JS::Value *vp);
static bool importKey(JSContext *cx, unsigned argc, JS::Value *vp);
static bool sign(JSContext *cx, unsigned argc, JS::Value *vp);
static bool verify(JSContext *cx, unsigned argc, JS::Value *vp);

static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
static bool init_class(JSContext *cx, JS::HandleObject global);
Expand Down
138 changes: 138 additions & 0 deletions integration-tests/js-compute/fixtures/crypto/bin/index.js
Expand Up @@ -708,4 +708,142 @@ routes.set("/crypto.subtle", async () => {
return pass();
});
}
}

// verify
{
routes.set("/crypto.subtle.verify", async () => {
error = assert(typeof crypto.subtle.verify, 'function', `typeof crypto.subtle.verify`)
if (error) { return error; }
error = assert(crypto.subtle.verify, SubtleCrypto.prototype.verify, `crypto.subtle.verify === SubtleCrypto.prototype.verify`)
if (error) { return error; }
return pass();
});
routes.set("/crypto.subtle.verify/length", async () => {
error = assert(crypto.subtle.verify.length, 4, `crypto.subtle.verify.length === 4`)
if (error) { return error; }
return pass();
});
routes.set("/crypto.subtle.verify/called-as-constructor", async () => {
error = assertThrows(() => {
new crypto.subtle.verify
}, TypeError, "crypto.subtle.verify is not a constructor")
if (error) { return error; }
return pass();
});
routes.set("/crypto.subtle.verify/called-with-wrong-this", async () => {
error = await assertRejects(async () => {
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
await crypto.subtle.verify.call(undefined, jsonWebKeyAlgorithm, key, new Uint8Array, new Uint8Array)
}, TypeError, "Method SubtleCrypto.verify called on receiver that's not an instance of SubtleCrypto")
if (error) { return error; }
return pass();
});
routes.set("/crypto.subtle.verify/called-with-no-arguments", async () => {
error = await assertRejects(async () => {
await crypto.subtle.verify()
}, TypeError, "SubtleCrypto.verify: At least 4 arguments required, but only 0 passed")
if (error) { return error; }
return pass();
});
// first-parameter
{
routes.set("/crypto.subtle.verify/first-parameter-calls-7.1.17-ToString", async () => {
const sentinel = Symbol("sentinel");
const test = async () => {
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
await crypto.subtle.verify({
name: {
toString() {
throw sentinel;
}
}
}, key, new Uint8Array, new Uint8Array);
}
let error = await assertRejects(test)
if (error) { return error; }
try {
await test()
} catch (thrownError) {
let error = assert(thrownError, sentinel, 'thrownError === sentinel')
if (error) { return error; }
}
return pass();
});
routes.set("/crypto.subtle.verify/first-parameter-non-existant-algorithm", async () => {
let error = await assertRejects(async () => {
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
await crypto.subtle.verify('jake', key, new Uint8Array, new Uint8Array)
}, Error, "Algorithm: Unrecognized name")
if (error) { return error; }
return pass();
});
}
// second-parameter
{
routes.set("/crypto.subtle.verify/second-parameter-invalid-format", async () => {
let error = await assertRejects(async () => {
await crypto.subtle.verify(jsonWebKeyAlgorithm, "jake", new Uint8Array, new Uint8Array)
}, Error, "parameter 2 is not of type 'CryptoKey'")
if (error) { return error; }
return pass();
});
routes.set("/crypto.subtle.verify/second-parameter-invalid-usages", async () => {
let error = await assertRejects(async () => {
const key = await crypto.subtle.importKey('jwk', privateJsonWebKeyData, jsonWebKeyAlgorithm, privateJsonWebKeyData.ext, privateJsonWebKeyData.key_ops);
await crypto.subtle.verify(jsonWebKeyAlgorithm, key, new Uint8Array(), new Uint8Array());
}, Error, "CryptoKey doesn't support verification")
if (error) { return error; }
return pass();
});
}
// third-parameter
{
routes.set("/crypto.subtle.verify/third-parameter-invalid-format", async () => {
let error = await assertRejects(async () => {
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
await crypto.subtle.verify(jsonWebKeyAlgorithm, key, undefined, new Uint8Array());
}, Error, "SubtleCrypto.verify: signature (argument 3) must be of type ArrayBuffer or ArrayBufferView but got \"\"")
if (error) { return error; }
return pass();
});
}
// fourth-parameter
{
routes.set("/crypto.subtle.verify/fourth-parameter-invalid-format", async () => {
let error = await assertRejects(async () => {
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
await crypto.subtle.verify(jsonWebKeyAlgorithm, key, new Uint8Array(), undefined);
}, Error, "SubtleCrypto.verify: data (argument 4) must be of type ArrayBuffer or ArrayBufferView but got \"\"")
if (error) { return error; }
return pass();
});
}
// incorrect-signature
{
routes.set("/crypto.subtle.verify/incorrect-signature", async () => {
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
const signature = new Uint8Array;
const enc = new TextEncoder();
const data = enc.encode('hello world');
const result = await crypto.subtle.verify(jsonWebKeyAlgorithm, key, signature, data);
error = assert(result, false, "result === false");
if (error) { return error; }
return pass();
});
}
// correct-signature
{
routes.set("/crypto.subtle.verify/incorrect-signature", async () => {
const pkey = await crypto.subtle.importKey('jwk', privateJsonWebKeyData, jsonWebKeyAlgorithm, privateJsonWebKeyData.ext, privateJsonWebKeyData.key_ops);
const key = await crypto.subtle.importKey('jwk', publicJsonWebKeyData, jsonWebKeyAlgorithm, publicJsonWebKeyData.ext, publicJsonWebKeyData.key_ops);
const enc = new TextEncoder();
const data = enc.encode('hello world');
const signature = await crypto.subtle.sign(jsonWebKeyAlgorithm, pkey, data);
const result = await crypto.subtle.verify(jsonWebKeyAlgorithm, key, signature, data);
error = assert(result, true, "result === true");
if (error) { return error; }
return pass();
});
}
}

0 comments on commit 077adfd

Please sign in to comment.