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;