Skip to content

Commit

Permalink
feat: add support for ECDSA keys to be used with SubtleCrypto.prototy…
Browse files Browse the repository at this point in the history
…pe.sign and SubtleCrypto.prototype.verify (#667)

In the last release we added support for importing an ECDSA key with SubtleCrypto.prototype.importKey. This patch adds the ability to use these keys to sign data and to verify data was signed with the same underlying key.

With this patch, Fastly Compute customers are now able to validate signatures provided by Fastly Fanout requests

Co-authored-by: Trevor Elliott <telliott@fastly.com>
  • Loading branch information
JakeChampion and elliottt committed Sep 21, 2023
1 parent a44e101 commit 51bb170
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 43 deletions.
5 changes: 3 additions & 2 deletions .vscode/c_cpp_properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -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__"
Expand All @@ -26,4 +27,4 @@
}
],
"version": 4
}
}
49 changes: 16 additions & 33 deletions integration-tests/js-compute/fixtures/crypto/bin/index.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
/// <reference path="../../../../../types/index.d.ts" />
/* 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 = {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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();
Expand All @@ -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([
Expand Down
19 changes: 16 additions & 3 deletions integration-tests/js-compute/fixtures/crypto/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1221,4 +1234,4 @@
"body": "ok"
}
}
}
}
225 changes: 222 additions & 3 deletions runtime/js-compute-runtime/builtins/crypto-algorithm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "openssl/sha.h"
#include <fmt/format.h>
#include <iostream>
#include <openssl/ecdsa.h>
#include <optional>
#include <span>
#include <vector>
Expand All @@ -16,6 +17,49 @@
namespace builtins {

namespace {
int numBitsToBytes(int x) { return (x / 8) + (7 + (x % 8)) / 8; }

std::pair<mozilla::UniquePtr<uint8_t[], JS::FreePolicy>, size_t>
convertToBytesExpand(JSContext *cx, const BIGNUM *bignum, size_t minimumBufferSize) {
int length = BN_num_bytes(bignum);

size_t bufferSize = std::max<size_t>(length, minimumBufferSize);
mozilla::UniquePtr<uint8_t[], JS::FreePolicy> bytes{
static_cast<uint8_t *>(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<mozilla::UniquePtr<uint8_t[], JS::FreePolicy>, 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) {

Expand Down Expand Up @@ -366,6 +410,30 @@ JS::Result<builtins::NamedCurve> toNamedCurve(JSContext *cx, JS::HandleValue val
}
}

JS::Result<size_t> 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<size_t>(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
Expand Down Expand Up @@ -755,12 +823,163 @@ JSObject *CryptoAlgorithmHMAC_Sign_Verify::toObject(JSContext *cx) {
JSObject *CryptoAlgorithmECDSA_Sign_Verify::sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> 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<EC_KEY*>(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<uint8_t[], JS::FreePolicy> result{
static_cast<uint8_t *>(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<void>(result.release());

return buffer;
};
JS::Result<bool> CryptoAlgorithmECDSA_Sign_Verify::verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature, std::span<uint8_t> data) {
MOZ_ASSERT(CryptoKey::is_instance(key));
DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError");
return JS::Result<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<EC_KEY*>(ecKey)) == 1;
#pragma clang diagnostic pop

// 8. Return result.
return result;
};
JSObject *CryptoAlgorithmECDSA_Sign_Verify::toObject(JSContext *cx) {
return nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<fastly_world_string_t *>(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<fastly_world_string_t *>(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;
Expand Down

0 comments on commit 51bb170

Please sign in to comment.