Skip to content

Commit 800fb66

Browse files
Jake ChampionJakeChampion
authored andcommitted
feat: Implement subset of crypto.subtle.sign which can sign data with a JSONWebKey using RSASSA-PKCS1-v1_5
1 parent bfa84cc commit 800fb66

File tree

9 files changed

+646
-76
lines changed

9 files changed

+646
-76
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"__functional_base_03": "cpp",
9090
"memory_resource": "cpp",
9191
"numeric": "cpp",
92-
"c_at_e_world.h": "c",
92+
"fastly_world.h": "c"
9393
"__bits": "cpp",
9494
"__verbose_abort": "cpp",
9595
"any": "cpp",

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

Lines changed: 270 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include "openssl/rsa.h"
12
#include "openssl/sha.h"
23
#include <iostream>
34
#include <span>
@@ -10,6 +11,83 @@ namespace builtins {
1011

1112
namespace {
1213

14+
const EVP_MD *createDigestAlgorithm(JSContext *cx, JS::HandleObject key) {
15+
16+
JS::RootedObject alg(cx, CryptoKey::get_algorithm(key));
17+
18+
JS::RootedValue hash_val(cx);
19+
JS_GetProperty(cx, alg, "hash", &hash_val);
20+
JS::RootedObject hash(cx, &hash_val.toObject());
21+
JS::RootedValue name_val(cx);
22+
JS_GetProperty(cx, hash, "name", &name_val);
23+
size_t name_length;
24+
auto cc = encode(cx, name_val, &name_length);
25+
26+
std::string_view name(cc.get(), name_length);
27+
if (name == "SHA-1") {
28+
return EVP_sha1();
29+
} else if (name == "SHA-224") {
30+
return EVP_sha224();
31+
} else if (name == "SHA-256") {
32+
return EVP_sha256();
33+
} else if (name == "SHA-384") {
34+
return EVP_sha384();
35+
} else if (name == "SHA-512") {
36+
return EVP_sha512();
37+
} else {
38+
// TODO Rename error to NotSupportedError
39+
JS_ReportErrorLatin1(cx, "NotSupportedError");
40+
return nullptr;
41+
}
42+
}
43+
// This implements https://w3c.github.io/webcrypto/#sha-operations for all
44+
// the SHA algorithms that we support.
45+
std::optional<std::span<uint8_t>> rawDigest(JSContext *cx, std::span<uint8_t> data,
46+
const EVP_MD *algorithm, size_t buffer_size) {
47+
unsigned int size;
48+
auto buf = static_cast<unsigned char *>(JS_malloc(cx, buffer_size));
49+
if (!buf) {
50+
JS_ReportOutOfMemory(cx);
51+
return std::nullopt;
52+
}
53+
if (!EVP_Digest(data.data(), data.size(), buf, &size, algorithm, NULL)) {
54+
// 2. If performing the operation results in an error, then throw an OperationError.
55+
// TODO: Change to an OperationError DOMException
56+
JS_ReportErrorUTF8(cx, "SubtleCrypto.digest: failed to create digest");
57+
JS_free(cx, buf);
58+
return std::nullopt;
59+
}
60+
return std::span<uint8_t>(buf, size);
61+
};
62+
63+
// This implements https://w3c.github.io/webcrypto/#sha-operations for all
64+
// the SHA algorithms that we support.
65+
JSObject *digest(JSContext *cx, std::span<uint8_t> data, const EVP_MD *algorithm,
66+
size_t buffer_size) {
67+
unsigned int size;
68+
auto buf = static_cast<unsigned char *>(JS_malloc(cx, buffer_size));
69+
if (!buf) {
70+
JS_ReportOutOfMemory(cx);
71+
return nullptr;
72+
}
73+
if (!EVP_Digest(data.data(), data.size(), buf, &size, algorithm, NULL)) {
74+
// 2. If performing the operation results in an error, then throw an OperationError.
75+
// TODO: Change to an OperationError DOMException
76+
JS_ReportErrorUTF8(cx, "SubtleCrypto.digest: failed to create digest");
77+
JS_free(cx, buf);
78+
return nullptr;
79+
}
80+
// 3. Return a new ArrayBuffer containing result.
81+
JS::RootedObject array_buffer(cx);
82+
array_buffer.set(JS::NewArrayBufferWithContents(cx, size, buf));
83+
if (!array_buffer) {
84+
JS_free(cx, buf);
85+
JS_ReportOutOfMemory(cx);
86+
return nullptr;
87+
}
88+
return array_buffer;
89+
};
90+
1391
// https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1
1492
// 6.3.1. Parameters for RSA Public Keys
1593
std::unique_ptr<CryptoKeyRSAComponents> createRSAPublicKeyFromJWK(JSContext *cx, JsonWebKey *jwk) {
@@ -436,6 +514,196 @@ std::unique_ptr<CryptoAlgorithmDigest> CryptoAlgorithmDigest::normalize(JSContex
436514
}
437515
};
438516

517+
std::unique_ptr<CryptoAlgorithmSignVerify>
518+
CryptoAlgorithmSignVerify::normalize(JSContext *cx, JS::HandleValue value) {
519+
// Do steps 1 through 5.1 of https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm
520+
auto identifierResult = normalizeIdentifier(cx, value);
521+
if (identifierResult.isErr()) {
522+
// If we are here, this means either the identifier could not be coerced to a String or was not recognized
523+
// In both those scenarios an exception will have already been created, which is why we are not creating one here.
524+
return nullptr;
525+
}
526+
auto identifier = identifierResult.unwrap();
527+
JS::Rooted<JSObject *> params(cx);
528+
529+
// The value can either be a JS String or a JS Object with a 'name' property which is the algorithm identifier.
530+
// Other properties within the object will be the parameters for the algorithm to use.
531+
if (value.isString()) {
532+
auto obj = JS_NewPlainObject(cx);
533+
params.set(obj);
534+
if (!JS_SetProperty(cx, params, "name", value)) {
535+
return nullptr;
536+
}
537+
} else if (value.isObject()) {
538+
params.set(&value.toObject());
539+
}
540+
541+
// The table listed at https://w3c.github.io/webcrypto/#h-note-15 is what defines which algorithms support which operations
542+
// RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, HMAC, are the algorithms
543+
// which support the sign operation
544+
switch (identifier) {
545+
case CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5: {
546+
return std::make_unique<CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify>();
547+
break;
548+
}
549+
case CryptoAlgorithmIdentifier::HMAC:
550+
case CryptoAlgorithmIdentifier::ECDSA:
551+
case CryptoAlgorithmIdentifier::RSA_PSS: {
552+
MOZ_ASSERT(false);
553+
JS_ReportErrorASCII(cx, "Supplied algorithm is not yet supported");
554+
convertErrorToNotSupported(cx);
555+
return nullptr;
556+
}
557+
default: {
558+
return nullptr;
559+
}
560+
}
561+
};
562+
563+
JSObject *CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify::sign(JSContext *cx, JS::HandleObject key,
564+
std::span<uint8_t> data) {
565+
566+
// 1. If the [[type]] internal slot of key is not "private", then throw an InvalidAccessError.
567+
if (CryptoKey::type(key) != CryptoKeyType::Private) {
568+
// TODO: Change to an InvalidAccessError instance
569+
JS_ReportErrorLatin1(cx, "InvalidAccessError");
570+
return nullptr;
571+
}
572+
573+
MOZ_ASSERT(CryptoKey::is_instance(key));
574+
if (CryptoKey::type(key) != CryptoKeyType::Private) {
575+
// TODO Rename error to InvalidAccessError
576+
JS_ReportErrorLatin1(cx, "InvalidAccessError");
577+
return nullptr;
578+
}
579+
580+
const EVP_MD *algorithm = createDigestAlgorithm(cx, key);
581+
if (!algorithm) {
582+
// TODO Rename error to OperationError
583+
JS_ReportErrorLatin1(cx, "OperationError");
584+
return nullptr;
585+
}
586+
587+
auto digestOption = ::builtins::rawDigest(cx, data, algorithm, EVP_MD_size(algorithm));
588+
if (!digestOption.has_value()) {
589+
// TODO Rename error to OperationError
590+
JS_ReportErrorLatin1(cx, "OperationError");
591+
return nullptr;
592+
}
593+
auto digest = digestOption.value();
594+
595+
// 2. Perform the signature generation operation defined in Section 8.2 of [RFC3447] with the
596+
// key represented by the [[handle]] internal slot of key as the signer's private key and the
597+
// contents of message as M and using the hash function specified in the hash attribute of the
598+
// [[algorithm]] internal slot of key as the Hash option for the EMSA-PKCS1-v1_5 encoding
599+
// method.
600+
// 3. If performing the operation results in an error, then throw an OperationError.
601+
auto ctx = EVP_PKEY_CTX_new(CryptoKey::key(key), nullptr);
602+
if (!ctx) {
603+
// TODO Rename error to OperationError
604+
JS_ReportErrorLatin1(cx, "OperationError");
605+
return nullptr;
606+
}
607+
608+
if (EVP_PKEY_sign_init(ctx) <= 0) {
609+
// TODO Rename error to OperationError
610+
JS_ReportErrorLatin1(cx, "OperationError");
611+
return nullptr;
612+
}
613+
614+
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) {
615+
// TODO Rename error to OperationError
616+
JS_ReportErrorLatin1(cx, "OperationError");
617+
return nullptr;
618+
}
619+
620+
if (EVP_PKEY_CTX_set_signature_md(ctx, algorithm) <= 0) {
621+
// TODO Rename error to OperationError
622+
JS_ReportErrorLatin1(cx, "OperationError");
623+
return nullptr;
624+
}
625+
626+
size_t signature_length;
627+
if (EVP_PKEY_sign(ctx, nullptr, &signature_length, digest.data(), digest.size()) <= 0) {
628+
// TODO Rename error to OperationError
629+
JS_ReportErrorLatin1(cx, "OperationError");
630+
return nullptr;
631+
}
632+
633+
// 4. Let signature be the value S that results from performing the operation.
634+
uint8_t *signature = reinterpret_cast<uint8_t *>(calloc(signature_length, sizeof(uint8_t)));
635+
if (EVP_PKEY_sign(ctx, signature, &signature_length, digest.data(), digest.size()) <= 0) {
636+
// TODO Rename error to OperationError
637+
JS_ReportErrorLatin1(cx, "OperationError");
638+
return nullptr;
639+
}
640+
641+
// 5. Return a new ArrayBuffer associated with the relevant global object of this [HTML], and
642+
// containing the bytes of signature.
643+
JS::RootedObject buffer(cx, JS::NewArrayBufferWithContents(cx, signature_length, signature));
644+
if (!buffer) {
645+
// We can be here is the array buffer was too large -- if that was the case then a
646+
// JSMSG_BAD_ARRAY_LENGTH will have been created. No other failure scenarios in this path will
647+
// create a JS exception and so we need to create one.
648+
if (!JS_IsExceptionPending(cx)) {
649+
// TODO Rename error to InternalError
650+
JS_ReportErrorLatin1(cx, "InternalError");
651+
}
652+
JS_free(cx, signature);
653+
return nullptr;
654+
}
655+
return buffer;
656+
}
657+
658+
JS::Result<bool> CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify::verify(JSContext *cx, JS::HandleObject key,
659+
std::span<uint8_t> signature,
660+
std::span<uint8_t> data) {
661+
MOZ_ASSERT(CryptoKey::is_instance(key));
662+
663+
if (CryptoKey::type(key) != CryptoKeyType::Public) {
664+
// TODO Rename error to InvalidAccessError
665+
JS_ReportErrorLatin1(cx, "InvalidAccessError");
666+
return JS::Result<bool>(JS::Error());
667+
}
668+
const EVP_MD *algorithm = createDigestAlgorithm(cx, key);
669+
670+
auto digestOption = ::builtins::rawDigest(cx, data, algorithm, EVP_MD_size(algorithm));
671+
if (!digestOption.has_value()) {
672+
// TODO Rename error to OperationError
673+
JS_ReportErrorLatin1(cx, "OperationError");
674+
return JS::Result<bool>(JS::Error());
675+
}
676+
677+
auto digest = digestOption.value();
678+
679+
auto ctx = EVP_PKEY_CTX_new(CryptoKey::key(key), nullptr);
680+
if (!ctx) {
681+
// TODO Rename error to OperationError
682+
JS_ReportErrorLatin1(cx, "OperationError");
683+
return JS::Result<bool>(JS::Error());
684+
}
685+
686+
if (EVP_PKEY_verify_init(ctx) != 1) {
687+
// TODO Rename error to OperationError
688+
JS_ReportErrorLatin1(cx, "OperationError");
689+
return JS::Result<bool>(JS::Error());
690+
}
691+
692+
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) != 1) {
693+
// TODO Rename error to OperationError
694+
JS_ReportErrorLatin1(cx, "OperationError");
695+
return JS::Result<bool>(JS::Error());
696+
}
697+
698+
if (EVP_PKEY_CTX_set_signature_md(ctx, algorithm) != 1) {
699+
// TODO Rename error to OperationError
700+
JS_ReportErrorLatin1(cx, "OperationError");
701+
return JS::Result<bool>(JS::Error());
702+
}
703+
704+
return EVP_PKEY_verify(ctx, signature.data(), signature.size(), digest.data(), digest.size()) ==
705+
1;
706+
}
439707

440708
std::unique_ptr<CryptoAlgorithmImportKey>
441709
CryptoAlgorithmImportKey::normalize(JSContext *cx, JS::HandleValue value) {
@@ -460,7 +728,7 @@ CryptoAlgorithmImportKey::normalize(JSContext *cx, JS::HandleValue value) {
460728
}
461729

462730
// The table listed at https://w3c.github.io/webcrypto/#h-note-15 is what defines which algorithms support which operations
463-
// RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP, ECDSA, ECDH, AES-CTR, AES-CBC, AES-GCM, AES-KW, HMAC, HKDF, PBKDF2 are the algorithms
731+
// RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP, ECDSA, ECDH, AES-CTR, AES-CBC, AES-GCM, AES-KW, HMAC, HKDF, PBKDF2 are the algorithms
464732
// which support the importKey operation
465733
switch (identifier) {
466734
case CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5: {
@@ -524,7 +792,7 @@ JSObject *CryptoAlgorithmRSASSA_PKCS1_v1_5_Import::importKey(JSContext *cx, Cryp
524792

525793

526794
// 2.2 If the d field of jwk is present and usages contains an entry which
527-
// is not "sign", or, if the d field of jwk is not present and usages
795+
// is not "sign", or, if the d field of jwk is not present and usages
528796
// contains an entry which is not "verify" then throw a SyntaxError.
529797
bool isUsagesAllowed = false;
530798
// public key
@@ -729,35 +997,6 @@ JSObject *CryptoAlgorithmRSASSA_PKCS1_v1_5_Import::toObject(JSContext *cx) {
729997
return algorithm;
730998
}
731999

732-
namespace {
733-
// This implements https://w3c.github.io/webcrypto/#sha-operations for all
734-
// the SHA algorithms that we support.
735-
JSObject *digest(JSContext *cx, std::span<uint8_t> data, const EVP_MD * algorithm, size_t buffer_size) {
736-
unsigned int size;
737-
auto buf = static_cast<unsigned char *>(JS_malloc(cx, buffer_size));
738-
if (!buf) {
739-
JS_ReportOutOfMemory(cx);
740-
return nullptr;
741-
}
742-
if (!EVP_Digest(data.data(), data.size(), buf, &size, algorithm, NULL)) {
743-
// 2. If performing the operation results in an error, then throw an OperationError.
744-
// TODO: Change to an OperationError DOMException
745-
JS_ReportErrorUTF8(cx, "SubtleCrypto.digest: failed to create digest");
746-
JS_free(cx, buf);
747-
return nullptr;
748-
}
749-
// 3. Return a new ArrayBuffer containing result.
750-
JS::RootedObject array_buffer(cx);
751-
array_buffer.set(JS::NewArrayBufferWithContents(cx, size, buf));
752-
if (!array_buffer) {
753-
JS_free(cx, buf);
754-
JS_ReportOutOfMemory(cx);
755-
return nullptr;
756-
}
757-
return array_buffer;
758-
};
759-
}
760-
7611000
JSObject *CryptoAlgorithmSHA1::digest(JSContext *cx, std::span<uint8_t> data) {
7621001
return ::builtins::digest(cx, data, EVP_sha1(), SHA_DIGEST_LENGTH);
7631002
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ class CryptoAlgorithmImportKey : public CryptoAlgorithm {
5050
static std::unique_ptr<CryptoAlgorithmImportKey> normalize(JSContext *cx, JS::HandleValue value);
5151
};
5252

53+
class CryptoAlgorithmSignVerify : public CryptoAlgorithm {
54+
public:
55+
virtual JSObject *sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> data) = 0;
56+
virtual JS::Result<bool> verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature,
57+
std::span<uint8_t> data) = 0;
58+
static std::unique_ptr<CryptoAlgorithmSignVerify> normalize(JSContext *cx, JS::HandleValue value);
59+
};
60+
61+
class CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify final : public CryptoAlgorithmSignVerify {
62+
public:
63+
const char *name() const noexcept override { return "RSASSA-PKCS1-v1_5"; };
64+
CryptoAlgorithmRSASSA_PKCS1_v1_5_Sign_Verify(){};
65+
CryptoAlgorithmIdentifier identifier() final {
66+
return CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5;
67+
};
68+
69+
JSObject *sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> data) override;
70+
JS::Result<bool> verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature,
71+
std::span<uint8_t> data) override;
72+
static JSObject *exportKey(JSContext *cx, CryptoKeyFormat format, JS::HandleObject key);
73+
JSObject *toObject(JSContext *cx);
74+
};
75+
5376
class CryptoAlgorithmRSASSA_PKCS1_v1_5_Import final : public CryptoAlgorithmImportKey {
5477
public:
5578
// The hash member describes the hash algorithm to use.

0 commit comments

Comments
 (0)