Skip to content

Commit 96ac02d

Browse files
Jake ChampionJakeChampion
authored andcommitted
feat: Add support for HMAC within SubtleCrypto implementation
This braings in support for the HMAC algorithm for signing and verification operations of the Web Crypto API.
1 parent 329b733 commit 96ac02d

File tree

10 files changed

+2012
-895
lines changed

10 files changed

+2012
-895
lines changed

integration-tests/js-compute/fixtures/crypto/bin/index.js

Lines changed: 247 additions & 93 deletions
Large diffs are not rendered by default.

integration-tests/js-compute/fixtures/crypto/tests.json

Lines changed: 1002 additions & 701 deletions
Large diffs are not rendered by default.

runtime/js-compute-runtime/builtins/crypto-algorithm.cpp

Lines changed: 501 additions & 60 deletions
Large diffs are not rendered by default.

runtime/js-compute-runtime/builtins/crypto-algorithm.h

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ class CryptoAlgorithmSignVerify : public CryptoAlgorithm {
6262
static std::unique_ptr<CryptoAlgorithmSignVerify> normalize(JSContext *cx, JS::HandleValue value);
6363
};
6464

65+
class CryptoAlgorithmHMAC_Sign_Verify final : public CryptoAlgorithmSignVerify {
66+
public:
67+
const char *name() const noexcept override { return "HMAC"; };
68+
CryptoAlgorithmHMAC_Sign_Verify(){};
69+
CryptoAlgorithmIdentifier identifier() final { return CryptoAlgorithmIdentifier::HMAC; };
70+
71+
JSObject *sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> data) override;
72+
JS::Result<bool> verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature,
73+
std::span<uint8_t> data) override;
74+
static JSObject *exportKey(JSContext *cx, CryptoKeyFormat format, JS::HandleObject key);
75+
JSObject *toObject(JSContext *cx);
76+
};
77+
6578
class CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify final : public CryptoAlgorithmSignVerify {
6679
public:
6780
const char *name() const noexcept override { return "RSASSA-PKCS1-v1_5"; };
@@ -73,7 +86,6 @@ class CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify final : public CryptoAlgorith
7386
JSObject *sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> data) override;
7487
JS::Result<bool> verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature,
7588
std::span<uint8_t> data) override;
76-
static JSObject *exportKey(JSContext *cx, CryptoKeyFormat format, JS::HandleObject key);
7789
JSObject *toObject(JSContext *cx);
7890
};
7991

@@ -102,6 +114,38 @@ class CryptoAlgorithmRSASSA_PKCS1_v1_5_Import final : public CryptoAlgorithmImpo
102114
JSObject *toObject(JSContext *cx);
103115
};
104116

117+
class CryptoAlgorithmHMAC_Import final : public CryptoAlgorithmImportKey {
118+
public:
119+
// The hash member describes the hash algorithm to use.
120+
// Valid values are SHA_256, SHA_384, SHA_512.
121+
CryptoAlgorithmIdentifier hashIdentifier;
122+
// A Number representing the length in bits of the key.
123+
// If this is omitted the length of the key is equal to the length of the digest generated by the
124+
// hash algorithm defined in hashIdentifier.
125+
std::optional<size_t> length;
126+
127+
const char *name() const noexcept override { return "HMAC"; };
128+
129+
CryptoAlgorithmHMAC_Import(CryptoAlgorithmIdentifier hashIdentifier)
130+
: hashIdentifier{hashIdentifier} {};
131+
132+
CryptoAlgorithmHMAC_Import(CryptoAlgorithmIdentifier hashIdentifier, size_t length)
133+
: hashIdentifier{hashIdentifier}, length{length} {};
134+
135+
// https://w3c.github.io/webcrypto/#hmac-importparams
136+
// 29.3 HmacImportParams dictionary
137+
static std::unique_ptr<CryptoAlgorithmHMAC_Import> fromParameters(JSContext *cx,
138+
JS::HandleObject parameters);
139+
140+
CryptoAlgorithmIdentifier identifier() final { return CryptoAlgorithmIdentifier::HMAC; };
141+
142+
JSObject *importKey(JSContext *cx, CryptoKeyFormat format, JS::HandleValue, bool extractable,
143+
CryptoKeyUsages usages) override;
144+
JSObject *importKey(JSContext *cx, CryptoKeyFormat format, KeyData, bool extractable,
145+
CryptoKeyUsages usages) override;
146+
JSObject *toObject(JSContext *cx);
147+
};
148+
105149
class CryptoAlgorithmDigest : public CryptoAlgorithm {
106150
public:
107151
virtual JSObject *digest(JSContext *cx, std::span<uint8_t>) = 0;

runtime/js-compute-runtime/builtins/crypto-key.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "crypto-algorithm.h"
33
#include "js-compute-builtins.h"
44
#include "openssl/rsa.h"
5+
#include <iostream>
56
#include <utility>
67

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

351+
JSObject *CryptoKey::createHMAC(JSContext *cx, CryptoAlgorithmHMAC_Import *algorithm,
352+
std::unique_ptr<std::span<uint8_t>> data, unsigned long length,
353+
bool extractable, CryptoKeyUsages usages) {
354+
MOZ_ASSERT(cx);
355+
MOZ_ASSERT(algorithm);
356+
JS::RootedObject instance(
357+
cx, JS_NewObjectWithGivenProto(cx, &CryptoKey::class_, CryptoKey::proto_obj));
358+
if (!instance) {
359+
return nullptr;
360+
}
361+
362+
JS::RootedObject alg(cx, algorithm->toObject(cx));
363+
if (!alg) {
364+
return nullptr;
365+
}
366+
367+
JS::SetReservedSlot(instance, Slots::Algorithm, JS::ObjectValue(*alg));
368+
JS::SetReservedSlot(instance, Slots::Type,
369+
JS::Int32Value(static_cast<uint8_t>(CryptoKeyType::Secret)));
370+
JS::SetReservedSlot(instance, Slots::Extractable, JS::BooleanValue(extractable));
371+
JS::SetReservedSlot(instance, Slots::Usages, JS::Int32Value(usages.toInt()));
372+
JS::SetReservedSlot(instance, Slots::KeyDataLength, JS::Int32Value(data->size()));
373+
JS::SetReservedSlot(instance, Slots::KeyData, JS::PrivateValue(data.release()->data()));
374+
return instance;
375+
}
376+
350377
JSObject *CryptoKey::createRSA(JSContext *cx, CryptoAlgorithmRSASSA_PKCS1_v1_5_Import *algorithm,
351378
std::unique_ptr<CryptoKeyRSAComponents> keyData, bool extractable,
352379
CryptoKeyUsages usages) {
@@ -551,6 +578,13 @@ EVP_PKEY *CryptoKey::key(JSObject *self) {
551578
return static_cast<EVP_PKEY *>(JS::GetReservedSlot(self, Slots::Key).toPrivate());
552579
}
553580

581+
std::span<uint8_t> CryptoKey::hmacKeyData(JSObject *self) {
582+
MOZ_ASSERT(is_instance(self));
583+
return std::span<uint8_t>(
584+
static_cast<uint8_t *>(JS::GetReservedSlot(self, Slots::KeyData).toPrivate()),
585+
JS::GetReservedSlot(self, Slots::KeyDataLength).toInt32());
586+
}
587+
554588
JS::Result<bool> CryptoKey::is_algorithm(JSContext *cx, JS::HandleObject self,
555589
CryptoAlgorithmIdentifier algorithm) {
556590
MOZ_ASSERT(CryptoKey::is_instance(self));

runtime/js-compute-runtime/builtins/crypto-key.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
#define JS_COMPUTE_RUNTIME_CRYPTO_KEY_H
33

44
#include "builtin.h"
5-
// #include "crypto-algorithm.h"
5+
66
#include "crypto-key-rsa-components.h"
77
#include "openssl/evp.h"
88

99
namespace builtins {
1010
enum class CryptoAlgorithmIdentifier : uint8_t;
1111
class CryptoAlgorithmRSASSA_PKCS1_v1_5_Import;
12+
class CryptoAlgorithmHMAC_Import;
1213
enum class CryptoKeyType : uint8_t { Public, Private, Secret };
1314

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

120+
static JSObject *createHMAC(JSContext *cx, CryptoAlgorithmHMAC_Import *algorithm,
121+
std::unique_ptr<std::span<uint8_t>> data, unsigned long length,
122+
bool extractable, CryptoKeyUsages usages);
116123
static JSObject *createRSA(JSContext *cx, CryptoAlgorithmRSASSA_PKCS1_v1_5_Import *algorithm,
117124
std::unique_ptr<CryptoKeyRSAComponents> keyData, bool extractable,
118125
CryptoKeyUsages usages);
119126
static CryptoKeyType type(JSObject *self);
120127
static JSObject *get_algorithm(JS::HandleObject self);
121128
static EVP_PKEY *key(JSObject *self);
129+
static std::span<uint8_t> hmacKeyData(JSObject *self);
122130
static bool canSign(JS::HandleObject self);
123131
static bool canVerify(JS::HandleObject self);
124132
static JS::Result<bool> is_algorithm(JSContext *cx, JS::HandleObject self,

runtime/js-compute-runtime/builtins/subtle-crypto.cpp

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
#include "js-compute-builtins.h"
44

55
namespace builtins {
6+
7+
namespace {
8+
void convertErrorToInvalidAccessError(JSContext *cx) {
9+
MOZ_ASSERT(JS_IsExceptionPending(cx));
10+
JS::RootedValue exn(cx);
11+
if (!JS_GetPendingException(cx, &exn)) {
12+
return;
13+
}
14+
MOZ_ASSERT(exn.isObject());
15+
JS::RootedObject error(cx, &exn.toObject());
16+
JS::RootedValue name(cx, JS::StringValue(JS_NewStringCopyZ(cx, "InvalidAccessError")));
17+
JS_SetProperty(cx, error, "name", name);
18+
JS::RootedValue code(cx, JS::NumberValue(15));
19+
JS_SetProperty(cx, error, "code", code);
20+
}
21+
} // namespace
622
// digest(algorithm, data)
723
// https://w3c.github.io/webcrypto/#SubtleCrypto-method-digest
824
bool SubtleCrypto::digest(JSContext *cx, unsigned argc, JS::Value *vp) {
@@ -180,33 +196,27 @@ bool SubtleCrypto::sign(JSContext *cx, unsigned argc, JS::Value *vp) {
180196
// 1. Let algorithm and key be the algorithm and key parameters passed to the sign() method,
181197
// respectively.
182198
auto algorithm = args.get(0);
183-
JS::RootedObject key(cx);
184-
{
185-
auto key_arg = args.get(1);
186-
if (!key_arg.isObject()) {
187-
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
188-
return ReturnPromiseRejectedWithPendingError(cx, args);
189-
}
190-
key.set(&key_arg.toObject());
191-
if (!CryptoKey::is_instance(key)) {
192-
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
193-
return ReturnPromiseRejectedWithPendingError(cx, args);
194-
}
199+
auto key_arg = args.get(1);
200+
if (!key_arg.isObject()) {
201+
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
202+
return ReturnPromiseRejectedWithPendingError(cx, args);
203+
}
204+
JS::RootedObject key(cx, &key_arg.toObject());
205+
if (!CryptoKey::is_instance(key)) {
206+
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
207+
return ReturnPromiseRejectedWithPendingError(cx, args);
195208
}
196209

197210
// 2. Let data be the result of getting a copy of the bytes held by the data parameter passed to
198211
// the sign() method.
199-
std::span<uint8_t> data;
200-
{
201-
std::optional<std::span<uint8_t>> dataOptional =
202-
value_to_buffer(cx, args.get(2), "SubtleCrypto.sign: data");
203-
if (!dataOptional.has_value()) {
204-
// value_to_buffer would have already created a JS exception so we don't need to create one
205-
// ourselves.
206-
return ReturnPromiseRejectedWithPendingError(cx, args);
207-
}
208-
data = dataOptional.value();
212+
std::optional<std::span<uint8_t>> dataOptional =
213+
value_to_buffer(cx, args.get(2), "SubtleCrypto.sign: data");
214+
if (!dataOptional.has_value()) {
215+
// value_to_buffer would have already created a JS exception so we don't need to create one
216+
// ourselves.
217+
return ReturnPromiseRejectedWithPendingError(cx, args);
209218
}
219+
std::span<uint8_t> data = dataOptional.value();
210220

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

240251
if (match_result.unwrap() == false) {
241-
// TODO: Change to an InvalidAccessError instance
242252
JS_ReportErrorUTF8(cx, "CryptoKey doesn't match AlgorithmIdentifier");
253+
convertErrorToInvalidAccessError(cx);
243254
return RejectPromiseWithPendingError(cx, promise);
244255
}
245256

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

@@ -284,20 +295,17 @@ bool SubtleCrypto::verify(JSContext *cx, unsigned argc, JS::Value *vp) {
284295
// 1. Let algorithm and key be the algorithm and key parameters passed to the verify() method,
285296
// respectively.
286297
auto algorithm = args.get(0);
287-
JS::RootedObject key(cx);
288-
{
289-
auto key_arg = args.get(1);
290-
if (!key_arg.isObject()) {
291-
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
292-
return ReturnPromiseRejectedWithPendingError(cx, args);
293-
}
294-
key.set(&key_arg.toObject());
298+
auto key_arg = args.get(1);
299+
if (!key_arg.isObject()) {
300+
JS_ReportErrorLatin1(cx, "parameter 2 is not of type 'CryptoKey'");
301+
return ReturnPromiseRejectedWithPendingError(cx, args);
302+
}
303+
JS::RootedObject key(cx, &key_arg.toObject());
295304

296-
if (!CryptoKey::is_instance(key)) {
297-
JS_ReportErrorASCII(
298-
cx, "SubtleCrypto.verify: key (argument 2) does not implement interface CryptoKey");
299-
return ReturnPromiseRejectedWithPendingError(cx, args);
300-
}
305+
if (!CryptoKey::is_instance(key)) {
306+
JS_ReportErrorASCII(
307+
cx, "SubtleCrypto.verify: key (argument 2) does not implement interface CryptoKey");
308+
return ReturnPromiseRejectedWithPendingError(cx, args);
301309
}
302310

303311
// 2. Let signature be the result of getting a copy of the bytes held by the signature
@@ -341,15 +349,15 @@ bool SubtleCrypto::verify(JSContext *cx, unsigned argc, JS::Value *vp) {
341349
auto identifier = normalizedAlgorithm->identifier();
342350
auto match_result = CryptoKey::is_algorithm(cx, key, identifier);
343351
if (match_result.isErr() || match_result.unwrap() == false) {
344-
// TODO: Change to an InvalidAccessError instance
345352
JS_ReportErrorUTF8(cx, "CryptoKey doesn't match AlgorithmIdentifier");
353+
convertErrorToInvalidAccessError(cx);
346354
return RejectPromiseWithPendingError(cx, promise);
347355
}
348356
// 10. If the [[usages]] internal slot of key does not contain an entry that is "verify", then
349357
// throw an InvalidAccessError.
350358
if (!CryptoKey::canVerify(key)) {
351-
// TODO: Change to an InvalidAccessError instance
352359
JS_ReportErrorUTF8(cx, "CryptoKey doesn't support verification");
360+
convertErrorToInvalidAccessError(cx);
353361
return RejectPromiseWithPendingError(cx, promise);
354362
}
355363
// 11. Let result be the result of performing the verify operation specified by

0 commit comments

Comments
 (0)