diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json
index 97cf0b570e..bf972beeea 100644
--- a/.vscode/c_cpp_properties.json
+++ b/.vscode/c_cpp_properties.json
@@ -7,7 +7,8 @@
"${workspaceFolder}/runtime/spidermonkey/release/include",
"/opt/wasi-sdk/share/wasi-sysroot/include/",
"${workspaceFolder}/runtime/js-compute-runtime",
- "${workspaceFolder}/runtime/js-compute-runtime/build/openssl-3.0.7/include"
+ "${workspaceFolder}/runtime/js-compute-runtime/build/openssl-3.0.7/include",
+ "${workspaceFolder}/runtime/js-compute-runtime/third_party/fmt/include"
],
"defines": [
"__wasi__"
@@ -26,4 +27,4 @@
}
],
"version": 4
-}
+}
\ No newline at end of file
diff --git a/integration-tests/js-compute/fixtures/crypto/bin/index.js b/integration-tests/js-compute/fixtures/crypto/bin/index.js
index e31d68415a..9a663080b7 100644
--- a/integration-tests/js-compute/fixtures/crypto/bin/index.js
+++ b/integration-tests/js-compute/fixtures/crypto/bin/index.js
@@ -1,30 +1,8 @@
///
/* eslint-env serviceworker, shared-node-browser, browser */
-import { env } from 'fastly:env';
-import { pass, fail, assert, assertThrows, assertRejects, assertResolves } from "../../../assertions.js";
-
-addEventListener("fetch", event => {
- event.respondWith(app(event));
-});
-/**
- * @param {FetchEvent} event
- * @returns {Response}
- */
-async function app(event) {
- try {
- const path = (new URL(event.request.url)).pathname;
- console.log(`path: ${path}`)
- console.log(`FASTLY_SERVICE_VERSION: ${env('FASTLY_SERVICE_VERSION')}`)
- if (routes.has(path)) {
- const routeHandler = routes.get(path);
- return await routeHandler();
- }
- return fail(`${path} endpoint does not exist`)
- } catch (error) {
- return fail(`The routeHandler threw an error: ${error.message}` + '\n' + error.stack)
- }
-}
+import { pass, assert, assertThrows, assertRejects, assertResolves } from "../../../assertions.js";
+import { routes } from "../../../test-harness.js";
// From https://www.rfc-editor.org/rfc/rfc7517#appendix-A.1
const publicRsaJsonWebKeyData = {
@@ -92,14 +70,8 @@ const rsaJsonWebKeyAlgorithm = {
const ecdsaJsonWebKeyAlgorithm = {
name: "ECDSA",
namedCurve: "P-256",
-}
-
-const routes = new Map();
-routes.set('/', () => {
- routes.delete('/');
- let test_routes = Array.from(routes.keys())
- return new Response(JSON.stringify(test_routes), { 'headers': { 'content-type': 'application/json' } });
-});
+ hash: {name: "SHA-256"},
+};
let error;
routes.set("/crypto", async () => {
@@ -1207,7 +1179,7 @@ routes.set("/crypto.subtle", async () => {
}
// correct-signature
{
- routes.set("/crypto.subtle.verify/correct-signature-jwk", async () => {
+ routes.set("/crypto.subtle.verify/correct-signature-jwk-rsa", async () => {
const pkey = await crypto.subtle.importKey('jwk', privateRsaJsonWebKeyData, rsaJsonWebKeyAlgorithm, privateRsaJsonWebKeyData.ext, privateRsaJsonWebKeyData.key_ops);
const key = await crypto.subtle.importKey('jwk', publicRsaJsonWebKeyData, rsaJsonWebKeyAlgorithm, publicRsaJsonWebKeyData.ext, publicRsaJsonWebKeyData.key_ops);
const enc = new TextEncoder();
@@ -1218,6 +1190,17 @@ routes.set("/crypto.subtle", async () => {
if (error) { return error; }
return pass('ok');
});
+ routes.set("/crypto.subtle.verify/correct-signature-jwk-ecdsa", async () => {
+ const pkey = await crypto.subtle.importKey('jwk', privateEcdsaJsonWebKeyData, ecdsaJsonWebKeyAlgorithm, privateEcdsaJsonWebKeyData.ext, privateEcdsaJsonWebKeyData.key_ops);
+ const key = await crypto.subtle.importKey('jwk', publicEcdsaJsonWebKeyData, ecdsaJsonWebKeyAlgorithm, publicEcdsaJsonWebKeyData.ext, publicEcdsaJsonWebKeyData.key_ops);
+ const enc = new TextEncoder();
+ const data = enc.encode('hello world');
+ const signature = await crypto.subtle.sign(ecdsaJsonWebKeyAlgorithm, pkey, data);
+ const result = await crypto.subtle.verify(ecdsaJsonWebKeyAlgorithm, key, signature, data);
+ error = assert(result, true, "result === true");
+ if (error) { return error; }
+ return pass('ok');
+ });
routes.set("/crypto.subtle.verify/correct-signature-hmac", async () => {
const results = {
'SHA-1': new Uint8Array([
diff --git a/integration-tests/js-compute/fixtures/crypto/tests.json b/integration-tests/js-compute/fixtures/crypto/tests.json
index a0447ac543..5f71009b9f 100644
--- a/integration-tests/js-compute/fixtures/crypto/tests.json
+++ b/integration-tests/js-compute/fixtures/crypto/tests.json
@@ -1195,13 +1195,26 @@
"body": "ok"
}
},
- "GET /crypto.subtle.verify/correct-signature-jwk": {
+ "GET /crypto.subtle.verify/correct-signature-jwk-rsa": {
"environments": [
"viceroy", "c@e"
],
"downstream_request": {
"method": "GET",
- "pathname": "/crypto.subtle.verify/correct-signature-jwk"
+ "pathname": "/crypto.subtle.verify/correct-signature-jwk-rsa"
+ },
+ "downstream_response": {
+ "status": 200,
+ "body": "ok"
+ }
+ },
+ "GET /crypto.subtle.verify/correct-signature-jwk-ecdsa": {
+ "environments": [
+ "viceroy", "c@e"
+ ],
+ "downstream_request": {
+ "method": "GET",
+ "pathname": "/crypto.subtle.verify/correct-signature-jwk-ecdsa"
},
"downstream_response": {
"status": 200,
@@ -1221,4 +1234,4 @@
"body": "ok"
}
}
-}
\ No newline at end of file
+}
diff --git a/runtime/js-compute-runtime/builtins/crypto-algorithm.cpp b/runtime/js-compute-runtime/builtins/crypto-algorithm.cpp
index d2dde444fb..47f7a3c57d 100644
--- a/runtime/js-compute-runtime/builtins/crypto-algorithm.cpp
+++ b/runtime/js-compute-runtime/builtins/crypto-algorithm.cpp
@@ -2,6 +2,7 @@
#include "openssl/sha.h"
#include
#include
+#include
#include
#include
#include
@@ -16,6 +17,49 @@
namespace builtins {
namespace {
+int numBitsToBytes(int x) { return (x / 8) + (7 + (x % 8)) / 8; }
+
+std::pair, size_t>
+convertToBytesExpand(JSContext *cx, const BIGNUM *bignum, size_t minimumBufferSize) {
+ int length = BN_num_bytes(bignum);
+
+ size_t bufferSize = std::max(length, minimumBufferSize);
+ mozilla::UniquePtr bytes{
+ static_cast(JS_malloc(cx, bufferSize))};
+
+ size_t paddingLength = bufferSize - length;
+ if (paddingLength > 0) {
+ uint8_t padding = BN_is_negative(bignum) ? 0xFF : 0x00;
+ std::fill_n(bytes.get(), paddingLength, padding);
+ }
+ BN_bn2bin(bignum, bytes.get() + paddingLength);
+ return std::pair, size_t>(std::move(bytes),
+ bufferSize);
+}
+
+const EVP_MD *createDigestAlgorithm(JSContext *cx, CryptoAlgorithmIdentifier hashIdentifier) {
+ switch (hashIdentifier) {
+ case CryptoAlgorithmIdentifier::MD5: {
+ return EVP_md5();
+ }
+ case CryptoAlgorithmIdentifier::SHA_1: {
+ return EVP_sha1();
+ }
+ case CryptoAlgorithmIdentifier::SHA_256: {
+ return EVP_sha256();
+ }
+ case CryptoAlgorithmIdentifier::SHA_384: {
+ return EVP_sha384();
+ }
+ case CryptoAlgorithmIdentifier::SHA_512: {
+ return EVP_sha512();
+ }
+ default: {
+ DOMException::raise(cx, "NotSupportedError", "NotSupportedError");
+ return nullptr;
+ }
+ }
+}
const EVP_MD *createDigestAlgorithm(JSContext *cx, JS::HandleObject key) {
@@ -366,6 +410,30 @@ JS::Result toNamedCurve(JSContext *cx, JS::HandleValue val
}
}
+JS::Result curveSize(JSContext *cx, JS::HandleObject key) {
+
+ JS::RootedObject alg(cx, CryptoKey::get_algorithm(key));
+
+ JS::RootedValue namedCurve_val(cx);
+ JS_GetProperty(cx, alg, "namedCurve", &namedCurve_val);
+ auto namedCurve_chars = core::encode(cx, namedCurve_val);
+ if (!namedCurve_chars) {
+ return JS::Result(JS::Error());
+ }
+
+ std::string_view namedCurve = namedCurve_chars;
+ if (namedCurve == "P-256") {
+ return 256;
+ } else if (namedCurve == "P-384") {
+ return 384;
+ } else if (namedCurve == "P-521") {
+ return 521;
+ }
+
+ MOZ_ASSERT_UNREACHABLE();
+ return 0;
+}
+
// This implements the first section of
// https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm which is shared
// across all the diffent algorithms, but importantly does not implement the parts to do with the
@@ -755,12 +823,163 @@ JSObject *CryptoAlgorithmHMAC_Sign_Verify::toObject(JSContext *cx) {
JSObject *CryptoAlgorithmECDSA_Sign_Verify::sign(JSContext *cx, JS::HandleObject key, std::span data) {
MOZ_ASSERT(CryptoKey::is_instance(key));
- return nullptr;
+ // 1. If the [[type]] internal slot of key is not "private", then throw an InvalidAccessError.
+ if (CryptoKey::type(key) != CryptoKeyType::Private) {
+ DOMException::raise(cx, "InvalidAccessError", "InvalidAccessError");
+ return nullptr;
+ }
+
+ // 2. Let hashAlgorithm be the hash member of normalizedAlgorithm.
+ const EVP_MD* algorithm = createDigestAlgorithm(cx, this->hashIdentifier);
+ if (!algorithm) {
+ DOMException::raise(cx, "SubtleCrypto.sign: failed to sign", "OperationError");
+ return nullptr;
+ }
+
+ // 3. Let M be the result of performing the digest operation specified by hashAlgorithm using message.
+ auto digestOption = ::builtins::rawDigest(cx, data, algorithm, EVP_MD_size(algorithm));
+ if (!digestOption.has_value()) {
+ DOMException::raise(cx, "OperationError", "OperationError");
+ return nullptr;
+ }
+
+ auto digest = digestOption.value();
+
+ // 4. Let d be the ECDSA private key associated with key.
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ const EC_KEY * ecKey = EVP_PKEY_get0_EC_KEY(CryptoKey::key(key));
+ #pragma clang diagnostic pop
+ if (!ecKey) {
+ DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError");
+ return nullptr;
+ }
+
+ // 5. Let params be the EC domain parameters associated with key.
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ auto sig = ECDSA_do_sign(digest.data(), digest.size(), const_cast(ecKey));
+ #pragma clang diagnostic pop
+ if (!sig) {
+ DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError");
+ return nullptr;
+ }
+
+ // 6. If the namedCurve attribute of the [[algorithm]] internal slot of key is "P-256", "P-384" or "P-521":
+ // Perform the ECDSA signing process, as specified in [RFC6090], Section 5.4, with M as the message, using params as the EC domain parameters, and with d as the private key.
+ // Let r and s be the pair of integers resulting from performing the ECDSA signing process.
+ // Let result be an empty byte sequence.
+ // Let n be the smallest integer such that n * 8 is greater than the logarithm to base 2 of the order of the base point of the elliptic curve identified by params.
+ // Convert r to an octet string of length n and append this sequence of bytes to result.
+ // Convert s to an octet string of length n and append this sequence of bytes to result.
+ // Otherwise, the namedCurve attribute of the [[algorithm]] internal slot of key is a value specified in an applicable specification:
+ // Perform the ECDSA signature steps specified in that specification, passing in M, params and d and resulting in result.
+ const BIGNUM* r;
+ const BIGNUM* s;
+ ECDSA_SIG_get0(sig, &r, &s);
+ auto keySize = curveSize(cx, key);
+ if (keySize.isErr()) {
+ return nullptr;
+ }
+
+ size_t keySizeInBytes = numBitsToBytes(keySize.unwrap());
+
+ auto rBytesAndSize = convertToBytesExpand(cx, r, keySizeInBytes);
+ auto *rBytes = rBytesAndSize.first.get();
+ auto rBytesSize = rBytesAndSize.second;
+
+ auto sBytesAndSize = convertToBytesExpand(cx, s, keySizeInBytes);
+ auto *sBytes = sBytesAndSize.first.get();
+ auto sBytesSize = sBytesAndSize.second;
+
+ auto resultSize = rBytesSize + sBytesSize;
+ mozilla::UniquePtr result{
+ static_cast(JS_malloc(cx, resultSize))};
+
+ std::memcpy(result.get(), rBytes, rBytesSize);
+ std::memcpy(result.get() + rBytesSize, sBytes, sBytesSize);
+
+ // 7. Return the result of creating an ArrayBuffer containing result.
+ JS::RootedObject buffer(cx, JS::NewArrayBufferWithContents(cx, resultSize, result.get()));
+ if (!buffer) {
+ // We can be here is the array buffer was too large -- if that was the case then a
+ // JSMSG_BAD_ARRAY_LENGTH will have been created. No other failure scenarios in this path will
+ // create a JS exception and so we need to create one.
+ if (!JS_IsExceptionPending(cx)) {
+ // TODO Rename error to InternalError
+ JS_ReportErrorLatin1(cx, "InternalError");
+ }
+ return nullptr;
+ }
+
+ // `signature` is now owned by `buffer`
+ static_cast(result.release());
+
+ return buffer;
};
JS::Result CryptoAlgorithmECDSA_Sign_Verify::verify(JSContext *cx, JS::HandleObject key, std::span signature, std::span data) {
MOZ_ASSERT(CryptoKey::is_instance(key));
- DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError");
- return JS::Result(JS::Error());
+ // 1. If the [[type]] internal slot of key is not "public", then throw an InvalidAccessError.
+ if (CryptoKey::type(key) != CryptoKeyType::Public) {
+ DOMException::raise(cx, "InvalidAccessError", "InvalidAccessError");
+ return JS::Result(JS::Error());
+ }
+
+ // 2. Let hashAlgorithm be the hash member of normalizedAlgorithm.
+ const EVP_MD* algorithm = createDigestAlgorithm(cx, this->hashIdentifier);
+ if (!algorithm) {
+ DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError");
+ return JS::Result(JS::Error());
+ }
+
+ // 3. Let M be the result of performing the digest operation specified by hashAlgorithm using message.
+ auto digestOption = ::builtins::rawDigest(cx, data, algorithm, EVP_MD_size(algorithm));
+ if (!digestOption.has_value()) {
+ DOMException::raise(cx, "OperationError", "OperationError");
+ return JS::Result(JS::Error());
+ }
+
+ auto digest = digestOption.value();
+
+ // 4. Let Q be the ECDSA public key associated with key.
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ const EC_KEY * ecKey = EVP_PKEY_get0_EC_KEY(CryptoKey::key(key));
+ #pragma clang diagnostic pop
+ if (!ecKey) {
+ DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError");
+ return JS::Result(JS::Error());
+ }
+
+ // 5. Let params be the EC domain parameters associated with key.
+ // 6. If the namedCurve attribute of the [[algorithm]] internal slot of key is "P-256", "P-384" or "P-521":
+ // Perform the ECDSA verifying process, as specified in RFC6090, Section 5.3, with M as the received message, signature as the received signature and using params as the EC domain parameters, and Q as the public key.
+ // Otherwise, the namedCurve attribute of the [[algorithm]] internal slot of key is a value specified in an applicable specification:
+ // Perform the ECDSA verification steps specified in that specification passing in M, signature, params and Q and resulting in an indication of whether or not the purported signature is valid.
+ auto keySize = curveSize(cx, key);
+ if (keySize.isErr()) {
+ return JS::Result(JS::Error());
+ }
+
+ size_t keySizeInBytes = numBitsToBytes(keySize.unwrap());
+
+ auto sig = ECDSA_SIG_new();
+ auto r = BN_bin2bn(signature.data(), keySizeInBytes, nullptr);
+ auto s = BN_bin2bn(signature.data() + keySizeInBytes, keySizeInBytes, nullptr);
+
+ if (!ECDSA_SIG_set0(sig, r, s)) {
+ DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError");
+ return JS::Result(JS::Error());
+ }
+
+ // 7. Let result be a boolean with the value true if the signature is valid and the value false otherwise.
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ bool result = ECDSA_do_verify(digest.data(), digest.size(), sig, const_cast(ecKey)) == 1;
+ #pragma clang diagnostic pop
+
+ // 8. Return result.
+ return result;
};
JSObject *CryptoAlgorithmECDSA_Sign_Verify::toObject(JSContext *cx) {
return nullptr;
diff --git a/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp b/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp
index 5b17751893..68041e6425 100644
--- a/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp
+++ b/runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp
@@ -593,8 +593,10 @@ bool fastly_compute_at_edge_http_resp_header_names_get(
cursor = (uint32_t)next_cursor;
}
cabi_free(buf);
- strs = static_cast(cabi_realloc(
- strs, str_max * sizeof(fastly_world_string_t), 1, str_cnt * sizeof(fastly_world_string_t)));
+ if (str_cnt != 0) {
+ strs = static_cast(cabi_realloc(
+ strs, str_max * sizeof(fastly_world_string_t), 1, str_cnt * sizeof(fastly_world_string_t)));
+ }
ret->ptr = strs;
ret->len = str_cnt;
return true;