Skip to content

Commit

Permalink
feat: Add support for HMAC within SubtleCrypto implementation
Browse files Browse the repository at this point in the history
This braings in support for the HMAC algorithm for signing and verification operations of the Web Crypto API.
  • Loading branch information
JakeChampion committed Jul 5, 2023
1 parent 329b733 commit 96ac02d
Show file tree
Hide file tree
Showing 10 changed files with 2,012 additions and 895 deletions.
340 changes: 247 additions & 93 deletions integration-tests/js-compute/fixtures/crypto/bin/index.js

Large diffs are not rendered by default.

1,703 changes: 1,002 additions & 701 deletions integration-tests/js-compute/fixtures/crypto/tests.json

Large diffs are not rendered by default.

561 changes: 501 additions & 60 deletions runtime/js-compute-runtime/builtins/crypto-algorithm.cpp

Large diffs are not rendered by default.

46 changes: 45 additions & 1 deletion runtime/js-compute-runtime/builtins/crypto-algorithm.h
Expand Up @@ -62,6 +62,19 @@ class CryptoAlgorithmSignVerify : public CryptoAlgorithm {
static std::unique_ptr<CryptoAlgorithmSignVerify> normalize(JSContext *cx, JS::HandleValue value);
};

class CryptoAlgorithmHMAC_Sign_Verify final : public CryptoAlgorithmSignVerify {
public:
const char *name() const noexcept override { return "HMAC"; };
CryptoAlgorithmHMAC_Sign_Verify(){};
CryptoAlgorithmIdentifier identifier() final { return CryptoAlgorithmIdentifier::HMAC; };

JSObject *sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> data) override;
JS::Result<bool> verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature,
std::span<uint8_t> data) override;
static JSObject *exportKey(JSContext *cx, CryptoKeyFormat format, JS::HandleObject key);
JSObject *toObject(JSContext *cx);
};

class CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify final : public CryptoAlgorithmSignVerify {
public:
const char *name() const noexcept override { return "RSASSA-PKCS1-v1_5"; };
Expand All @@ -73,7 +86,6 @@ class CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify final : public CryptoAlgorith
JSObject *sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> data) override;
JS::Result<bool> verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature,
std::span<uint8_t> data) override;
static JSObject *exportKey(JSContext *cx, CryptoKeyFormat format, JS::HandleObject key);
JSObject *toObject(JSContext *cx);
};

Expand Down Expand Up @@ -102,6 +114,38 @@ class CryptoAlgorithmRSASSA_PKCS1_v1_5_Import final : public CryptoAlgorithmImpo
JSObject *toObject(JSContext *cx);
};

class CryptoAlgorithmHMAC_Import final : public CryptoAlgorithmImportKey {
public:
// The hash member describes the hash algorithm to use.
// Valid values are SHA_256, SHA_384, SHA_512.
CryptoAlgorithmIdentifier hashIdentifier;
// A Number representing the length in bits of the key.
// If this is omitted the length of the key is equal to the length of the digest generated by the
// hash algorithm defined in hashIdentifier.
std::optional<size_t> length;

const char *name() const noexcept override { return "HMAC"; };

CryptoAlgorithmHMAC_Import(CryptoAlgorithmIdentifier hashIdentifier)
: hashIdentifier{hashIdentifier} {};

CryptoAlgorithmHMAC_Import(CryptoAlgorithmIdentifier hashIdentifier, size_t length)
: hashIdentifier{hashIdentifier}, length{length} {};

// https://w3c.github.io/webcrypto/#hmac-importparams
// 29.3 HmacImportParams dictionary
static std::unique_ptr<CryptoAlgorithmHMAC_Import> fromParameters(JSContext *cx,
JS::HandleObject parameters);

CryptoAlgorithmIdentifier identifier() final { return CryptoAlgorithmIdentifier::HMAC; };

JSObject *importKey(JSContext *cx, CryptoKeyFormat format, JS::HandleValue, bool extractable,
CryptoKeyUsages usages) override;
JSObject *importKey(JSContext *cx, CryptoKeyFormat format, KeyData, bool extractable,
CryptoKeyUsages usages) override;
JSObject *toObject(JSContext *cx);
};

class CryptoAlgorithmDigest : public CryptoAlgorithm {
public:
virtual JSObject *digest(JSContext *cx, std::span<uint8_t>) = 0;
Expand Down
34 changes: 34 additions & 0 deletions runtime/js-compute-runtime/builtins/crypto-key.cpp
Expand Up @@ -2,6 +2,7 @@
#include "crypto-algorithm.h"
#include "js-compute-builtins.h"
#include "openssl/rsa.h"
#include <iostream>
#include <utility>

namespace builtins {
Expand Down Expand Up @@ -347,6 +348,32 @@ BIGNUM *convertToBigNumber(std::string_view bytes) {
int getBigNumberLength(BIGNUM *a) { return BN_num_bytes(a) * 8; }
} // namespace

JSObject *CryptoKey::createHMAC(JSContext *cx, CryptoAlgorithmHMAC_Import *algorithm,
std::unique_ptr<std::span<uint8_t>> data, unsigned long length,
bool extractable, CryptoKeyUsages usages) {
MOZ_ASSERT(cx);
MOZ_ASSERT(algorithm);
JS::RootedObject instance(
cx, JS_NewObjectWithGivenProto(cx, &CryptoKey::class_, CryptoKey::proto_obj));
if (!instance) {
return nullptr;
}

JS::RootedObject alg(cx, algorithm->toObject(cx));
if (!alg) {
return nullptr;
}

JS::SetReservedSlot(instance, Slots::Algorithm, JS::ObjectValue(*alg));
JS::SetReservedSlot(instance, Slots::Type,
JS::Int32Value(static_cast<uint8_t>(CryptoKeyType::Secret)));
JS::SetReservedSlot(instance, Slots::Extractable, JS::BooleanValue(extractable));
JS::SetReservedSlot(instance, Slots::Usages, JS::Int32Value(usages.toInt()));
JS::SetReservedSlot(instance, Slots::KeyDataLength, JS::Int32Value(data->size()));
JS::SetReservedSlot(instance, Slots::KeyData, JS::PrivateValue(data.release()->data()));
return instance;
}

JSObject *CryptoKey::createRSA(JSContext *cx, CryptoAlgorithmRSASSA_PKCS1_v1_5_Import *algorithm,
std::unique_ptr<CryptoKeyRSAComponents> keyData, bool extractable,
CryptoKeyUsages usages) {
Expand Down Expand Up @@ -551,6 +578,13 @@ EVP_PKEY *CryptoKey::key(JSObject *self) {
return static_cast<EVP_PKEY *>(JS::GetReservedSlot(self, Slots::Key).toPrivate());
}

std::span<uint8_t> CryptoKey::hmacKeyData(JSObject *self) {
MOZ_ASSERT(is_instance(self));
return std::span<uint8_t>(
static_cast<uint8_t *>(JS::GetReservedSlot(self, Slots::KeyData).toPrivate()),
JS::GetReservedSlot(self, Slots::KeyDataLength).toInt32());
}

JS::Result<bool> CryptoKey::is_algorithm(JSContext *cx, JS::HandleObject self,
CryptoAlgorithmIdentifier algorithm) {
MOZ_ASSERT(CryptoKey::is_instance(self));
Expand Down
10 changes: 9 additions & 1 deletion runtime/js-compute-runtime/builtins/crypto-key.h
Expand Up @@ -2,13 +2,14 @@
#define JS_COMPUTE_RUNTIME_CRYPTO_KEY_H

#include "builtin.h"
// #include "crypto-algorithm.h"

#include "crypto-key-rsa-components.h"
#include "openssl/evp.h"

namespace builtins {
enum class CryptoAlgorithmIdentifier : uint8_t;
class CryptoAlgorithmRSASSA_PKCS1_v1_5_Import;
class CryptoAlgorithmHMAC_Import;
enum class CryptoKeyType : uint8_t { Public, Private, Secret };

enum class CryptoKeyFormat : uint8_t { Raw, Spki, Pkcs8, Jwk };
Expand Down Expand Up @@ -53,6 +54,7 @@ class CryptoKeyUsages {
bool canOnlyDecrypt() { return this->mask == decrypt_flag; };
bool canOnlySign() { return this->mask == sign_flag; };
bool canOnlyVerify() { return this->mask == verify_flag; };
bool canOnlySignOrVerify() { return this->mask & (sign_flag | verify_flag); };
bool canOnlyDeriveKey() { return this->mask == derive_key_flag; };
bool canOnlyDeriveBits() { return this->mask == derive_bits_flag; };
bool canOnlyWrapKey() { return this->mask == wrap_key_flag; };
Expand Down Expand Up @@ -104,6 +106,8 @@ class CryptoKey : public BuiltinImpl<CryptoKey> {
// It will either be an `EVP_PKEY *` or an `uint8_t *`.
// `uint8_t *` is used only for HMAC keys, `EVP_PKEY *` is used for all the other key types.
Key,
KeyData,
KeyDataLength,
Count
};
static const JSFunctionSpec static_methods[];
Expand All @@ -113,12 +117,16 @@ class CryptoKey : public BuiltinImpl<CryptoKey> {
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
static bool init_class(JSContext *cx, JS::HandleObject global);

static JSObject *createHMAC(JSContext *cx, CryptoAlgorithmHMAC_Import *algorithm,
std::unique_ptr<std::span<uint8_t>> data, unsigned long length,
bool extractable, CryptoKeyUsages usages);
static JSObject *createRSA(JSContext *cx, CryptoAlgorithmRSASSA_PKCS1_v1_5_Import *algorithm,
std::unique_ptr<CryptoKeyRSAComponents> keyData, bool extractable,
CryptoKeyUsages usages);
static CryptoKeyType type(JSObject *self);
static JSObject *get_algorithm(JS::HandleObject self);
static EVP_PKEY *key(JSObject *self);
static std::span<uint8_t> hmacKeyData(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,
Expand Down
86 changes: 47 additions & 39 deletions runtime/js-compute-runtime/builtins/subtle-crypto.cpp
Expand Up @@ -3,6 +3,22 @@
#include "js-compute-builtins.h"

namespace builtins {

namespace {
void convertErrorToInvalidAccessError(JSContext *cx) {
MOZ_ASSERT(JS_IsExceptionPending(cx));
JS::RootedValue exn(cx);
if (!JS_GetPendingException(cx, &exn)) {
return;
}
MOZ_ASSERT(exn.isObject());
JS::RootedObject error(cx, &exn.toObject());
JS::RootedValue name(cx, JS::StringValue(JS_NewStringCopyZ(cx, "InvalidAccessError")));
JS_SetProperty(cx, error, "name", name);
JS::RootedValue code(cx, JS::NumberValue(15));
JS_SetProperty(cx, error, "code", code);
}
} // namespace
// digest(algorithm, data)
// https://w3c.github.io/webcrypto/#SubtleCrypto-method-digest
bool SubtleCrypto::digest(JSContext *cx, unsigned argc, JS::Value *vp) {
Expand Down Expand Up @@ -180,33 +196,27 @@ bool SubtleCrypto::sign(JSContext *cx, unsigned argc, JS::Value *vp) {
// 1. Let algorithm and key be the algorithm and key parameters passed to the sign() 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_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
return ReturnPromiseRejectedWithPendingError(cx, args);
}
auto key_arg = args.get(1);
if (!key_arg.isObject()) {
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
return ReturnPromiseRejectedWithPendingError(cx, args);
}
JS::RootedObject key(cx, &key_arg.toObject());
if (!CryptoKey::is_instance(key)) {
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
return ReturnPromiseRejectedWithPendingError(cx, args);
}

// 2. Let data be the result of getting a copy of the bytes held by the data parameter passed to
// the sign() method.
std::span<uint8_t> data;
{
std::optional<std::span<uint8_t>> dataOptional =
value_to_buffer(cx, args.get(2), "SubtleCrypto.sign: data");
if (!dataOptional.has_value()) {
// value_to_buffer would have already created a JS exception so we don't need to create one
// ourselves.
return ReturnPromiseRejectedWithPendingError(cx, args);
}
data = dataOptional.value();
std::optional<std::span<uint8_t>> dataOptional =
value_to_buffer(cx, args.get(2), "SubtleCrypto.sign: data");
if (!dataOptional.has_value()) {
// value_to_buffer would have already created a JS exception so we don't need to create one
// ourselves.
return ReturnPromiseRejectedWithPendingError(cx, args);
}
std::span<uint8_t> data = dataOptional.value();

// 3. Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to algorithm
// and op set to "sign".
Expand Down Expand Up @@ -234,20 +244,21 @@ bool SubtleCrypto::sign(JSContext *cx, unsigned argc, JS::Value *vp) {
auto match_result = CryptoKey::is_algorithm(cx, key, identifier);
if (match_result.isErr()) {
JS_ReportErrorUTF8(cx, "CryptoKey doesn't match AlgorithmIdentifier");
convertErrorToInvalidAccessError(cx);
return RejectPromiseWithPendingError(cx, promise);
}

if (match_result.unwrap() == false) {
// TODO: Change to an InvalidAccessError instance
JS_ReportErrorUTF8(cx, "CryptoKey doesn't match AlgorithmIdentifier");
convertErrorToInvalidAccessError(cx);
return RejectPromiseWithPendingError(cx, promise);
}

// 9. If the [[usages]] internal slot of key does not contain an entry that is "sign", then throw
// an InvalidAccessError.
if (!CryptoKey::canSign(key)) {
// TODO: Change to an InvalidAccessError instance
JS_ReportErrorLatin1(cx, "CryptoKey doesn't support signing");
convertErrorToInvalidAccessError(cx);
return RejectPromiseWithPendingError(cx, promise);
}

Expand Down Expand Up @@ -284,20 +295,17 @@ bool SubtleCrypto::verify(JSContext *cx, unsigned argc, JS::Value *vp) {
// 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());
auto key_arg = args.get(1);
if (!key_arg.isObject()) {
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
return ReturnPromiseRejectedWithPendingError(cx, args);
}
JS::RootedObject key(cx, &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);
}
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
Expand Down Expand Up @@ -341,15 +349,15 @@ bool SubtleCrypto::verify(JSContext *cx, unsigned argc, JS::Value *vp) {
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");
convertErrorToInvalidAccessError(cx);
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");
convertErrorToInvalidAccessError(cx);
return RejectPromiseWithPendingError(cx, promise);
}
// 11. Let result be the result of performing the verify operation specified by
Expand Down

0 comments on commit 96ac02d

Please sign in to comment.