From 99cbdfb004e459b114525713661e7f969b4eb754 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Sat, 1 Mar 2025 03:01:39 -0800 Subject: [PATCH] `node:crypto`: move `Sign` and `Verify` to c++ (#17692) Co-authored-by: Jarred Sumner --- cmake/targets/BuildBun.cmake | 2 + patches/ncrypto.patch | 919 ++++++ src/bun.js/bindings/ErrorCode.cpp | 155 + src/bun.js/bindings/ErrorCode.h | 10 + src/bun.js/bindings/ErrorCode.ts | 4 + src/bun.js/bindings/JSBuffer.cpp | 5 +- src/bun.js/bindings/JSBuffer.h | 2 + src/bun.js/bindings/JSX509Certificate.cpp | 2 +- src/bun.js/bindings/JSX509Certificate.h | 2 +- .../bindings/JSX509CertificatePrototype.cpp | 2 +- src/bun.js/bindings/KeyObject.cpp | 17 +- src/bun.js/bindings/KeyObject.h | 2 +- src/bun.js/bindings/NodeValidator.cpp | 101 +- src/bun.js/bindings/NodeValidator.h | 7 + src/bun.js/bindings/ZigGlobalObject.cpp | 15 + src/bun.js/bindings/ZigGlobalObject.h | 2 + src/bun.js/bindings/ncrypto.cpp | 2782 +++++++++++++---- src/bun.js/bindings/ncrypto.h | 665 +++- src/bun.js/bindings/node/crypto/JSSign.cpp | 916 ++++++ src/bun.js/bindings/node/crypto/JSSign.h | 85 + src/bun.js/bindings/node/crypto/JSVerify.cpp | 1400 +++++++++ src/bun.js/bindings/node/crypto/JSVerify.h | 85 + .../bindings/{ => node/crypto}/NodeCrypto.cpp | 70 +- .../bindings/{ => node/crypto}/NodeCrypto.h | 0 src/bun.js/bindings/node/crypto/util.cpp | 355 +++ src/bun.js/bindings/node/crypto/util.h | 34 + .../bindings/webcore/DOMClientIsoSubspaces.h | 3 +- src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 2 + src/js/internal/validators.ts | 22 +- src/js/node/crypto.ts | 462 +-- test/bun.lock | 2 +- .../js/node/crypto/crypto.key-objects.test.ts | 6 +- test/js/node/crypto/fixtures/sign.fixture.ts | 2 + test/js/node/test/common/crypto.js | 56 +- .../parallel/test-crypto-async-sign-verify.js | 147 + .../test/parallel/test-crypto-sign-verify.js | 834 +++++ 36 files changed, 8036 insertions(+), 1139 deletions(-) create mode 100644 patches/ncrypto.patch create mode 100644 src/bun.js/bindings/node/crypto/JSSign.cpp create mode 100644 src/bun.js/bindings/node/crypto/JSSign.h create mode 100644 src/bun.js/bindings/node/crypto/JSVerify.cpp create mode 100644 src/bun.js/bindings/node/crypto/JSVerify.h rename src/bun.js/bindings/{ => node/crypto}/NodeCrypto.cpp (88%) rename src/bun.js/bindings/{ => node/crypto}/NodeCrypto.h (100%) create mode 100644 src/bun.js/bindings/node/crypto/util.cpp create mode 100644 src/bun.js/bindings/node/crypto/util.h create mode 100644 test/js/node/test/parallel/test-crypto-async-sign-verify.js create mode 100644 test/js/node/test/parallel/test-crypto-sign-verify.js diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index a221068103..3740e869a7 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -622,6 +622,7 @@ file(GLOB BUN_CXX_SOURCES ${CONFIGURE_DEPENDS} ${CWD}/src/bun.js/bindings/sqlite/*.cpp ${CWD}/src/bun.js/bindings/webcrypto/*.cpp ${CWD}/src/bun.js/bindings/webcrypto/*/*.cpp + ${CWD}/src/bun.js/bindings/node/crypto/*.cpp ${CWD}/src/bun.js/bindings/v8/*.cpp ${CWD}/src/bun.js/bindings/v8/shim/*.cpp ${CWD}/src/bake/*.cpp @@ -759,6 +760,7 @@ target_include_directories(${bun} PRIVATE ${CWD}/src/bun.js/bindings ${CWD}/src/bun.js/bindings/webcore ${CWD}/src/bun.js/bindings/webcrypto + ${CWD}/src/bun.js/bindings/node/crypto ${CWD}/src/bun.js/bindings/sqlite ${CWD}/src/bun.js/bindings/v8 ${CWD}/src/bun.js/modules diff --git a/patches/ncrypto.patch b/patches/ncrypto.patch new file mode 100644 index 0000000000..59b81b4e9e --- /dev/null +++ b/patches/ncrypto.patch @@ -0,0 +1,919 @@ +diff --git a/include/ncrypto.h b/include/ncrypto.h +index be9e0ca..f8000de 100644 +--- a/include/ncrypto.h ++++ b/include/ncrypto.h +@@ -1,5 +1,15 @@ + #pragma once + ++#include "root.h" ++ ++#ifdef ASSERT_ENABLED ++#define NCRYPTO_DEVELOPMENT_CHECKS 1 ++#endif ++ ++#include ++#include ++#include ++ + #include + #include + #include +@@ -61,30 +71,11 @@ namespace ncrypto { + + #if NCRYPTO_DEVELOPMENT_CHECKS + #define NCRYPTO_STR(x) #x +-#define NCRYPTO_REQUIRE(EXPR) \ +- { \ +- if (!(EXPR) { abort(); }) } +- +-#define NCRYPTO_FAIL(MESSAGE) \ +- do { \ +- std::cerr << "FAIL: " << (MESSAGE) << std::endl; \ +- abort(); \ +- } while (0); +-#define NCRYPTO_ASSERT_EQUAL(LHS, RHS, MESSAGE) \ +- do { \ +- if (LHS != RHS) { \ +- std::cerr << "Mismatch: '" << LHS << "' - '" << RHS << "'" << std::endl; \ +- NCRYPTO_FAIL(MESSAGE); \ +- } \ +- } while (0); +-#define NCRYPTO_ASSERT_TRUE(COND) \ +- do { \ +- if (!(COND)) { \ +- std::cerr << "Assert at line " << __LINE__ << " of file " << __FILE__ \ +- << std::endl; \ +- NCRYPTO_FAIL(NCRYPTO_STR(COND)); \ +- } \ +- } while (0); ++#define NCRYPTO_REQUIRE(EXPR) ASSERT_WITH_MESSAGE(EXPR, "Assertion failed") ++#define NCRYPTO_FAIL(MESSAGE) ASSERT_WITH_MESSAGE(false, MESSAGE) ++#define NCRYPTO_ASSERT_EQUAL(LHS, RHS, MESSAGE) \ ++ ASSERT_WITH_MESSAGE(LHS == RHS, MESSAGE) ++#define NCRYPTO_ASSERT_TRUE(COND) ASSERT_WITH_MESSAGE(COND, NCRYPTO_STR(COND)) + #else + #define NCRYPTO_FAIL(MESSAGE) + #define NCRYPTO_ASSERT_EQUAL(LHS, RHS, MESSAGE) +@@ -131,9 +122,9 @@ class CryptoErrorList final { + void capture(); + + // Add an error message to the end of the stack. +- void add(std::string message); ++ void add(WTF::String message); + +- inline const std::string& peek_back() const { return errors_.back(); } ++ inline const WTF::String& peek_back() const { return errors_.back(); } + inline size_t size() const { return errors_.size(); } + inline bool empty() const { return errors_.empty(); } + +@@ -142,11 +133,11 @@ class CryptoErrorList final { + inline auto rbegin() const noexcept { return errors_.rbegin(); } + inline auto rend() const noexcept { return errors_.rend(); } + +- std::optional pop_back(); +- std::optional pop_front(); ++ std::optional pop_back(); ++ std::optional pop_front(); + + private: +- std::list errors_; ++ std::list errors_; + }; + + // Forcibly clears the error stack on destruction. This stops stale errors +@@ -277,12 +268,12 @@ class Cipher final { + int getIvLength() const; + int getKeyLength() const; + int getBlockSize() const; +- std::string_view getModeLabel() const; +- std::string_view getName() const; ++ WTF::ASCIILiteral getModeLabel() const; ++ WTF::String getName() const; + + bool isSupportedAuthenticatedMode() const; + +- static const Cipher FromName(std::string_view name); ++ static const Cipher FromName(WTF::StringView name); + static const Cipher FromNid(int nid); + static const Cipher FromCtx(const CipherCtxPointer& ctx); + +@@ -336,6 +327,8 @@ class Dsa final { + }; + + class BignumPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(BignumPointer); ++ + public: + BignumPointer() = default; + explicit BignumPointer(BIGNUM* bignum); +@@ -429,8 +422,8 @@ class Rsa final { + const BIGNUM* qi; + }; + struct PssParams { +- std::string_view digest = "sha1"; +- std::optional mgf1_digest = "sha1"; ++ WTF::StringView digest = "sha1"_s; ++ std::optional mgf1_digest = "sha1"_s; + int64_t salt_length = 20; + }; + +@@ -465,7 +458,7 @@ class Ec final { + const EC_GROUP* getGroup() const; + int getCurve() const; + uint32_t getDegree() const; +- std::string getCurveName() const; ++ WTF::String getCurveName() const; + const EC_POINT* getPublicKey() const; + const BIGNUM* getPrivateKey() const; + +@@ -535,13 +528,15 @@ class DataPointer final { + }; + + class BIOPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(BIOPointer); ++ + public: + static BIOPointer NewMem(); + static BIOPointer NewSecMem(); + static BIOPointer New(const BIO_METHOD* method); + static BIOPointer New(const void* data, size_t len); + static BIOPointer New(const BIGNUM* bn); +- static BIOPointer NewFile(std::string_view filename, std::string_view mode); ++ static BIOPointer NewFile(WTF::StringView filename, WTF::StringView mode); + static BIOPointer NewFp(FILE* fd, int flags); + + template +@@ -575,7 +570,7 @@ class BIOPointer final { + + bool resetBio() const; + +- static int Write(BIOPointer* bio, std::string_view message); ++ static int Write(BIOPointer* bio, WTF::StringView message); + + template + static void Printf(BIOPointer* bio, const char* format, Args... args) { +@@ -588,6 +583,8 @@ class BIOPointer final { + }; + + class CipherCtxPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(CipherCtxPointer); ++ + public: + static CipherCtxPointer New(); + +@@ -630,6 +627,8 @@ class CipherCtxPointer final { + }; + + class EVPKeyCtxPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(EVPKeyCtxPointer); ++ + public: + EVPKeyCtxPointer(); + explicit EVPKeyCtxPointer(EVP_PKEY_CTX* ctx); +@@ -697,6 +696,8 @@ class EVPKeyCtxPointer final { + }; + + class EVPKeyPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(EVPKeyPointer); ++ + public: + static EVPKeyPointer New(); + static EVPKeyPointer NewRawPublic(int id, +@@ -821,6 +822,8 @@ class EVPKeyPointer final { + }; + + class DHPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(DHPointer); ++ + public: + enum class FindGroupOption { + NONE, +@@ -833,9 +836,9 @@ class DHPointer final { + static BignumPointer GetStandardGenerator(); + + static BignumPointer FindGroup( +- const std::string_view name, ++ const WTF::StringView name, + FindGroupOption option = FindGroupOption::NONE); +- static DHPointer FromGroup(const std::string_view name, ++ static DHPointer FromGroup(const WTF::StringView name, + FindGroupOption option = FindGroupOption::NONE); + + static DHPointer New(BignumPointer&& p, BignumPointer&& g); +@@ -910,6 +913,8 @@ struct StackOfX509Deleter { + using StackOfX509 = std::unique_ptr; + + class SSLCtxPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(SSLCtxPointer); ++ + public: + SSLCtxPointer() = default; + explicit SSLCtxPointer(SSL_CTX* ctx); +@@ -943,6 +948,8 @@ class SSLCtxPointer final { + }; + + class SSLPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(SSLPointer); ++ + public: + SSLPointer() = default; + explicit SSLPointer(SSL* ssl); +@@ -961,31 +968,33 @@ class SSLPointer final { + bool setSession(const SSLSessionPointer& session); + bool setSniContext(const SSLCtxPointer& ctx) const; + +- const std::string_view getClientHelloAlpn() const; +- const std::string_view getClientHelloServerName() const; ++ const WTF::StringView getClientHelloAlpn() const; ++ const WTF::StringView getClientHelloServerName() const; + +- std::optional getServerName() const; ++ std::optional getServerName() const; + X509View getCertificate() const; + EVPKeyPointer getPeerTempKey() const; + const SSL_CIPHER* getCipher() const; + bool isServer() const; + +- std::optional getCipherName() const; +- std::optional getCipherStandardName() const; +- std::optional getCipherVersion() const; ++ std::optional getCipherName() const; ++ std::optional getCipherStandardName() const; ++ std::optional getCipherVersion() const; + + std::optional verifyPeerCertificate() const; + +- void getCiphers(std::function cb) const; ++ void getCiphers(WTF::Function&& cb) const; + + static SSLPointer New(const SSLCtxPointer& ctx); +- static std::optional GetServerName(const SSL* ssl); ++ static std::optional GetServerName(const SSL* ssl); + + private: + DeleteFnPtr ssl_; + }; + + class X509Name final { ++ WTF_MAKE_TZONE_ALLOCATED(X509Name); ++ + public: + X509Name(); + explicit X509Name(const X509_NAME* name); +@@ -1007,7 +1016,7 @@ class X509Name final { + operator bool() const; + bool operator==(const Iterator& other) const; + bool operator!=(const Iterator& other) const; +- std::pair operator*() const; ++ std::pair operator*() const; + + private: + const X509Name& name_; +@@ -1062,7 +1071,7 @@ class X509View final { + bool checkPrivateKey(const EVPKeyPointer& pkey) const; + bool checkPublicKey(const EVPKeyPointer& pkey) const; + +- std::optional getFingerprint(const EVP_MD* method) const; ++ std::optional getFingerprint(const EVP_MD* method) const; + + X509Pointer clone() const; + +@@ -1072,16 +1081,16 @@ class X509View final { + INVALID_NAME, + OPERATION_FAILED, + }; +- CheckMatch checkHost(const std::string_view host, int flags, ++ CheckMatch checkHost(const std::span host, int flags, + DataPointer* peerName = nullptr) const; +- CheckMatch checkEmail(const std::string_view email, int flags) const; +- CheckMatch checkIp(const std::string_view ip, int flags) const; ++ CheckMatch checkEmail(const std::span email, int flags) const; ++ CheckMatch checkIp(const char* ip, int flags) const; + +- using UsageCallback = std::function; ++ using UsageCallback = WTF::Function)>; + bool enumUsages(UsageCallback callback) const; + + template +- using KeyCallback = std::function; ++ using KeyCallback = WTF::Function; + bool ifRsa(KeyCallback callback) const; + bool ifEc(KeyCallback callback) const; + +@@ -1090,6 +1099,8 @@ class X509View final { + }; + + class X509Pointer final { ++ WTF_MAKE_TZONE_ALLOCATED(X509Pointer); ++ + public: + static Result Parse(Buffer buffer); + static X509Pointer IssuerFrom(const SSLPointer& ssl, const X509View& view); +@@ -1114,14 +1125,16 @@ class X509Pointer final { + X509View view() const; + operator X509View() const { return view(); } + +- static std::string_view ErrorCode(int32_t err); +- static std::optional ErrorReason(int32_t err); ++ static WTF::ASCIILiteral ErrorCode(int32_t err); ++ static std::optional ErrorReason(int32_t err); + + private: + DeleteFnPtr cert_; + }; + + class ECDSASigPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(ECDSASigPointer); ++ + public: + explicit ECDSASigPointer(); + explicit ECDSASigPointer(ECDSA_SIG* sig); +@@ -1154,6 +1167,8 @@ class ECDSASigPointer final { + }; + + class ECGroupPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(ECGroupPointer); ++ + public: + explicit ECGroupPointer(); + explicit ECGroupPointer(EC_GROUP* group); +@@ -1176,6 +1191,8 @@ class ECGroupPointer final { + }; + + class ECPointPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(ECPointPointer); ++ + public: + ECPointPointer(); + explicit ECPointPointer(EC_POINT* point); +@@ -1202,6 +1219,8 @@ class ECPointPointer final { + }; + + class ECKeyPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(ECKeyPointer); ++ + public: + ECKeyPointer(); + explicit ECKeyPointer(EC_KEY* key); +@@ -1242,6 +1261,8 @@ class ECKeyPointer final { + }; + + class EVPMDCtxPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(EVPMDCtxPointer); ++ + public: + EVPMDCtxPointer(); + explicit EVPMDCtxPointer(EVP_MD_CTX* ctx); +@@ -1286,6 +1307,8 @@ class EVPMDCtxPointer final { + }; + + class HMACCtxPointer final { ++ WTF_MAKE_TZONE_ALLOCATED(HMACCtxPointer); ++ + public: + HMACCtxPointer(); + explicit HMACCtxPointer(HMAC_CTX* ctx); +@@ -1331,7 +1354,7 @@ class EnginePointer final { + + bool setAsDefault(uint32_t flags, CryptoErrorList* errors = nullptr); + bool init(bool finish_on_exit = false); +- EVPKeyPointer loadPrivateKey(const std::string_view key_name); ++ EVPKeyPointer loadPrivateKey(const WTF::StringView key_name); + + // Release ownership of the ENGINE* pointer. + ENGINE* release(); +@@ -1339,7 +1362,7 @@ class EnginePointer final { + // Retrieve an OpenSSL Engine instance by name. If the name does not + // identify a valid named engine, the returned EnginePointer will be + // empty. +- static EnginePointer getEngineByName(const std::string_view name, ++ static EnginePointer getEngineByName(const WTF::StringView name, + CryptoErrorList* errors = nullptr); + + // Call once when initializing OpenSSL at startup for the process. +@@ -1396,8 +1419,8 @@ DataPointer ExportChallenge(const Buffer& buf); + // ============================================================================ + // KDF + +-const EVP_MD* getDigestByName(const std::string_view name); +-const EVP_CIPHER* getCipherByName(const std::string_view name); ++const EVP_MD* getDigestByName(const WTF::StringView name); ++const EVP_CIPHER* getCipherByName(const WTF::StringView name); + + // Verify that the specified HKDF output length is valid for the given digest. + // The maximum length for HKDF output for a given digest is 255 times the +diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp +index 2e411ce..2315eb5 100644 +--- a/src/ncrypto.cpp ++++ b/src/ncrypto.cpp +@@ -1,3 +1,8 @@ ++#include "root.h" ++#include "wtf/text/ASCIILiteral.h" ++#include "wtf/text/StringImpl.h" ++#include "wtf/text/WTFString.h" ++ + #include "ncrypto.h" + + #include +@@ -75,22 +80,22 @@ void CryptoErrorList::capture() { + while (const auto err = ERR_get_error()) { + char buf[256]; + ERR_error_string_n(err, buf, sizeof(buf)); +- errors_.emplace_front(buf); ++ errors_.emplace_front(WTF::String::fromUTF8(buf)); + } + } + +-void CryptoErrorList::add(std::string error) { errors_.push_back(error); } ++void CryptoErrorList::add(WTF::String error) { errors_.push_back(error); } + +-std::optional CryptoErrorList::pop_back() { ++std::optional CryptoErrorList::pop_back() { + if (errors_.empty()) return std::nullopt; +- std::string error = errors_.back(); ++ WTF::String error = errors_.back(); + errors_.pop_back(); + return error; + } + +-std::optional CryptoErrorList::pop_front() { ++std::optional CryptoErrorList::pop_front() { + if (errors_.empty()) return std::nullopt; +- std::string error = errors_.front(); ++ WTF::String error = errors_.front(); + errors_.pop_front(); + return error; + } +@@ -1104,7 +1109,8 @@ bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const { + return X509_verify(const_cast(cert_), pkey.get()) == 1; + } + +-X509View::CheckMatch X509View::checkHost(const std::string_view host, int flags, ++X509View::CheckMatch X509View::checkHost(const std::span host, ++ int flags, + DataPointer* peerName) const { + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return CheckMatch::NO_MATCH; +@@ -1127,7 +1133,7 @@ X509View::CheckMatch X509View::checkHost(const std::string_view host, int flags, + } + } + +-X509View::CheckMatch X509View::checkEmail(const std::string_view email, ++X509View::CheckMatch X509View::checkEmail(const std::span email, + int flags) const { + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return CheckMatch::NO_MATCH; +@@ -1144,11 +1150,10 @@ X509View::CheckMatch X509View::checkEmail(const std::string_view email, + } + } + +-X509View::CheckMatch X509View::checkIp(const std::string_view ip, +- int flags) const { ++X509View::CheckMatch X509View::checkIp(const char* ip, int flags) const { + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return CheckMatch::NO_MATCH; +- switch (X509_check_ip_asc(const_cast(cert_), ip.data(), flags)) { ++ switch (X509_check_ip_asc(const_cast(cert_), ip, flags)) { + case 0: + return CheckMatch::NO_MATCH; + case 1: +@@ -1172,7 +1177,7 @@ X509View X509View::From(const SSLCtxPointer& ctx) { + return X509View(SSL_CTX_get0_certificate(ctx.get())); + } + +-std::optional X509View::getFingerprint( ++std::optional X509View::getFingerprint( + const EVP_MD* method) const { + unsigned int md_size; + unsigned char md[EVP_MAX_MD_SIZE]; +@@ -1180,7 +1185,9 @@ std::optional X509View::getFingerprint( + + if (X509_digest(get(), method, md, &md_size)) { + if (md_size == 0) return std::nullopt; +- std::string fingerprint((md_size * 3) - 1, 0); ++ std::span fingerprint; ++ WTF::String fingerprintStr = ++ WTF::String::createUninitialized((md_size * 3) - 1, fingerprint); + for (unsigned int i = 0; i < md_size; i++) { + auto idx = 3 * i; + fingerprint[idx] = hex[(md[i] & 0xf0) >> 4]; +@@ -1189,7 +1196,7 @@ std::optional X509View::getFingerprint( + fingerprint[idx + 2] = ':'; + } + +- return fingerprint; ++ return fingerprintStr; + } + + return std::nullopt; +@@ -1299,10 +1306,10 @@ X509Pointer X509Pointer::PeerFrom(const SSLPointer& ssl) { + // When adding or removing errors below, please also update the list in the API + // documentation. See the "OpenSSL Error Codes" section of doc/api/errors.md + // Also *please* update the respective section in doc/api/tls.md as well +-std::string_view X509Pointer::ErrorCode(int32_t err) { // NOLINT(runtime/int) ++WTF::ASCIILiteral X509Pointer::ErrorCode(int32_t err) { // NOLINT(runtime/int) + #define CASE(CODE) \ + case X509_V_ERR_##CODE: \ +- return #CODE; ++ return #CODE##_s; + switch (err) { + CASE(UNABLE_TO_GET_ISSUER_CERT) + CASE(UNABLE_TO_GET_CRL) +@@ -1334,12 +1341,24 @@ std::string_view X509Pointer::ErrorCode(int32_t err) { // NOLINT(runtime/int) + CASE(HOSTNAME_MISMATCH) + } + #undef CASE +- return "UNSPECIFIED"; ++ return "UNSPECIFIED"_s; + } + +-std::optional X509Pointer::ErrorReason(int32_t err) { ++std::optional X509Pointer::ErrorReason(int32_t err) { + if (err == X509_V_OK) return std::nullopt; +- return X509_verify_cert_error_string(err); ++ ++ // TODO(dylan-conway): delete this switch? ++ switch (err) { ++#define V(name, msg) \ ++ case X509_V_ERR_##name: \ ++ return msg##_s; ++ V(HOSTNAME_MISMATCH, "Hostname does not match certificate") ++ V(EMAIL_MISMATCH, "Email address does not match certificate") ++ V(IP_ADDRESS_MISMATCH, "IP address does not match certificate") ++#undef V ++ } ++ return WTF::ASCIILiteral::fromLiteralUnsafe( ++ X509_verify_cert_error_string(err)); + } + + // ============================================================================ +@@ -1385,9 +1404,10 @@ BIOPointer BIOPointer::New(const void* data, size_t len) { + return BIOPointer(BIO_new_mem_buf(data, len)); + } + +-BIOPointer BIOPointer::NewFile(std::string_view filename, +- std::string_view mode) { +- return BIOPointer(BIO_new_file(filename.data(), mode.data())); ++BIOPointer BIOPointer::NewFile(WTF::StringView filename, WTF::StringView mode) { ++ auto filenameUtf8 = filename.utf8(); ++ auto modeUtf8 = mode.utf8(); ++ return BIOPointer(BIO_new_file(filenameUtf8.data(), modeUtf8.data())); + } + + BIOPointer BIOPointer::NewFp(FILE* fd, int close_flag) { +@@ -1400,20 +1420,18 @@ BIOPointer BIOPointer::New(const BIGNUM* bn) { + return res; + } + +-int BIOPointer::Write(BIOPointer* bio, std::string_view message) { +- if (bio == nullptr || !*bio) return 0; +- return BIO_write(bio->get(), message.data(), message.size()); ++int BIOPointer::Write(BIOPointer* bio, WTF::StringView message) { ++ auto messageUtf8 = message.utf8(); ++ return Write(bio, messageUtf8.span()); + } + + // ============================================================================ + // DHPointer + + namespace { +-bool EqualNoCase(const std::string_view a, const std::string_view b) { +- if (a.size() != b.size()) return false; +- return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char a, char b) { +- return std::tolower(a) == std::tolower(b); +- }); ++bool EqualNoCase(const WTF::StringView a, const WTF::StringView b) { ++ if (a.length() != b.length()) return false; ++ return a.startsWithIgnoringASCIICase(b); + } + } // namespace + +@@ -1433,23 +1451,23 @@ void DHPointer::reset(DH* dh) { dh_.reset(dh); } + + DH* DHPointer::release() { return dh_.release(); } + +-BignumPointer DHPointer::FindGroup(const std::string_view name, ++BignumPointer DHPointer::FindGroup(const WTF::StringView name, + FindGroupOption option) { + #define V(n, p) \ + if (EqualNoCase(name, n)) return BignumPointer(p(nullptr)); + if (option != FindGroupOption::NO_SMALL_PRIMES) { + #ifndef OPENSSL_IS_BORINGSSL + // Boringssl does not support the 768 and 1024 small primes +- V("modp1", BN_get_rfc2409_prime_768); +- V("modp2", BN_get_rfc2409_prime_1024); ++ V("modp1"_s, BN_get_rfc2409_prime_768); ++ V("modp2"_s, BN_get_rfc2409_prime_1024); + #endif +- V("modp5", BN_get_rfc3526_prime_1536); ++ V("modp5"_s, BN_get_rfc3526_prime_1536); + } +- V("modp14", BN_get_rfc3526_prime_2048); +- V("modp15", BN_get_rfc3526_prime_3072); +- V("modp16", BN_get_rfc3526_prime_4096); +- V("modp17", BN_get_rfc3526_prime_6144); +- V("modp18", BN_get_rfc3526_prime_8192); ++ V("modp14"_s, BN_get_rfc3526_prime_2048); ++ V("modp15"_s, BN_get_rfc3526_prime_3072); ++ V("modp16"_s, BN_get_rfc3526_prime_4096); ++ V("modp17"_s, BN_get_rfc3526_prime_6144); ++ V("modp18"_s, BN_get_rfc3526_prime_8192); + #undef V + return {}; + } +@@ -1461,7 +1479,7 @@ BignumPointer DHPointer::GetStandardGenerator() { + return bn; + } + +-DHPointer DHPointer::FromGroup(const std::string_view name, ++DHPointer DHPointer::FromGroup(const WTF::StringView name, + FindGroupOption option) { + auto group = FindGroup(name, option); + if (!group) return {}; // Unable to find the named group. +@@ -1469,7 +1487,7 @@ DHPointer DHPointer::FromGroup(const std::string_view name, + auto generator = GetStandardGenerator(); + if (!generator) return {}; // Unable to create the generator. + +- return New(std::move(group), std::move(generator)); ++ return New(WTFMove(group), WTFMove(generator)); + } + + DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) { +@@ -1663,17 +1681,24 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, + // ============================================================================ + // KDF + +-const EVP_MD* getDigestByName(const std::string_view name) { ++const EVP_MD* getDigestByName(const WTF::StringView name) { + // Historically, "dss1" and "DSS1" were DSA aliases for SHA-1 + // exposed through the public API. +- if (name == "dss1" || name == "DSS1") [[unlikely]] { ++ if (name == "dss1"_s || name == "DSS1"_s) [[unlikely]] { + return EVP_sha1(); + } +- return EVP_get_digestbyname(name.data()); ++ ++ // if (name == "ripemd160WithRSA"_s || name == "RSA-RIPEMD160"_s) { ++ // return EVP_ripemd160(); ++ // } ++ ++ auto nameUtf8 = name.utf8(); ++ return EVP_get_digestbyname(nameUtf8.data()); + } + +-const EVP_CIPHER* getCipherByName(const std::string_view name) { +- return EVP_get_cipherbyname(name.data()); ++const EVP_CIPHER* getCipherByName(const WTF::StringView name) { ++ auto nameUtf8 = name.utf8(); ++ return EVP_get_cipherbyname(nameUtf8.data()); + } + + bool checkHkdfLength(const EVP_MD* md, size_t length) { +@@ -2499,7 +2524,7 @@ SSLPointer SSLPointer::New(const SSLCtxPointer& ctx) { + } + + void SSLPointer::getCiphers( +- std::function cb) const { ++ WTF::Function&& cb) const { + if (!ssl_) return; + STACK_OF(SSL_CIPHER)* ciphers = SSL_get_ciphers(get()); + +@@ -2507,16 +2532,16 @@ void SSLPointer::getCiphers( + // document them, but since there are only 5, easier to just add them manually + // and not have to explain their absence in the API docs. They are lower-cased + // because the docs say they will be. +- static constexpr const char* TLS13_CIPHERS[] = { +- "tls_aes_256_gcm_sha384", "tls_chacha20_poly1305_sha256", +- "tls_aes_128_gcm_sha256", "tls_aes_128_ccm_8_sha256", +- "tls_aes_128_ccm_sha256"}; ++ static constexpr WTF::ASCIILiteral TLS13_CIPHERS[] = { ++ "tls_aes_256_gcm_sha384"_s, "tls_chacha20_poly1305_sha256"_s, ++ "tls_aes_128_gcm_sha256"_s, "tls_aes_128_ccm_8_sha256"_s, ++ "tls_aes_128_ccm_sha256"_s}; + + const int n = sk_SSL_CIPHER_num(ciphers); + + for (int i = 0; i < n; ++i) { + const SSL_CIPHER* cipher = sk_SSL_CIPHER_value(ciphers, i); +- cb(SSL_CIPHER_get_name(cipher)); ++ cb(WTF::ASCIILiteral::fromLiteralUnsafe(SSL_CIPHER_get_name(cipher))); + } + + for (unsigned i = 0; i < 5; ++i) { +@@ -2562,7 +2587,7 @@ std::optional SSLPointer::verifyPeerCertificate() const { + return std::nullopt; + } + +-const std::string_view SSLPointer::getClientHelloAlpn() const { ++const WTF::StringView SSLPointer::getClientHelloAlpn() const { + if (ssl_ == nullptr) return {}; + #ifndef OPENSSL_IS_BORINGSSL + const unsigned char* buf; +@@ -2585,7 +2610,7 @@ const std::string_view SSLPointer::getClientHelloAlpn() const { + #endif + } + +-const std::string_view SSLPointer::getClientHelloServerName() const { ++const WTF::StringView SSLPointer::getClientHelloServerName() const { + if (ssl_ == nullptr) return {}; + #ifndef OPENSSL_IS_BORINGSSL + const unsigned char* buf; +@@ -2613,15 +2638,14 @@ const std::string_view SSLPointer::getClientHelloServerName() const { + #endif + } + +-std::optional SSLPointer::GetServerName( +- const SSL* ssl) { ++std::optional SSLPointer::GetServerName(const SSL* ssl) { + if (ssl == nullptr) return std::nullopt; + auto res = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + if (res == nullptr) return std::nullopt; +- return res; ++ return WTF::String::fromUTF8(res); + } + +-std::optional SSLPointer::getServerName() const { ++std::optional SSLPointer::getServerName() const { + if (!ssl_) return std::nullopt; + return GetServerName(get()); + } +@@ -2650,22 +2674,28 @@ EVPKeyPointer SSLPointer::getPeerTempKey() const { + return EVPKeyPointer(raw_key); + } + +-std::optional SSLPointer::getCipherName() const { ++std::optional SSLPointer::getCipherName() const { + auto cipher = getCipher(); + if (cipher == nullptr) return std::nullopt; +- return SSL_CIPHER_get_name(cipher); ++ const char* name = SSL_CIPHER_get_name(cipher); ++ if (!name) return std::nullopt; ++ return WTF::StringView::fromLatin1(name); + } + +-std::optional SSLPointer::getCipherStandardName() const { ++std::optional SSLPointer::getCipherStandardName() const { + auto cipher = getCipher(); + if (cipher == nullptr) return std::nullopt; +- return SSL_CIPHER_standard_name(cipher); ++ const char* name = SSL_CIPHER_standard_name(cipher); ++ if (!name) return std::nullopt; ++ return WTF::StringView::fromLatin1(name); + } + +-std::optional SSLPointer::getCipherVersion() const { ++std::optional SSLPointer::getCipherVersion() const { + auto cipher = getCipher(); + if (cipher == nullptr) return std::nullopt; +- return SSL_CIPHER_get_version(cipher); ++ auto version = SSL_CIPHER_get_version(cipher); ++ if (!version) return std::nullopt; ++ return WTF::StringView::fromLatin1(version); + } + + SSLCtxPointer::SSLCtxPointer(SSL_CTX* ctx) : ctx_(ctx) {} +@@ -2713,8 +2743,9 @@ bool SSLCtxPointer::setGroups(const char* groups) { + + // ============================================================================ + +-const Cipher Cipher::FromName(std::string_view name) { +- return Cipher(EVP_get_cipherbyname(name.data())); ++const Cipher Cipher::FromName(WTF::StringView name) { ++ auto nameUtf8 = name.utf8(); ++ return Cipher(EVP_get_cipherbyname(nameUtf8.data())); + } + + const Cipher Cipher::FromNid(int nid) { +@@ -2750,40 +2781,40 @@ int Cipher::getNid() const { + return EVP_CIPHER_nid(cipher_); + } + +-std::string_view Cipher::getModeLabel() const { ++WTF::ASCIILiteral Cipher::getModeLabel() const { + if (!cipher_) return {}; + switch (getMode()) { + case EVP_CIPH_CCM_MODE: +- return "ccm"; ++ return "ccm"_s; + case EVP_CIPH_CFB_MODE: +- return "cfb"; ++ return "cfb"_s; + case EVP_CIPH_CBC_MODE: +- return "cbc"; ++ return "cbc"_s; + case EVP_CIPH_CTR_MODE: +- return "ctr"; ++ return "ctr"_s; + case EVP_CIPH_ECB_MODE: +- return "ecb"; ++ return "ecb"_s; + case EVP_CIPH_GCM_MODE: +- return "gcm"; ++ return "gcm"_s; + case EVP_CIPH_OCB_MODE: +- return "ocb"; ++ return "ocb"_s; + case EVP_CIPH_OFB_MODE: +- return "ofb"; ++ return "ofb"_s; + case EVP_CIPH_WRAP_MODE: +- return "wrap"; ++ return "wrap"_s; + case EVP_CIPH_XTS_MODE: +- return "xts"; ++ return "xts"_s; + case EVP_CIPH_STREAM_CIPHER: +- return "stream"; ++ return "stream"_s; + } +- return "{unknown}"; ++ return "{unknown}"_s; + } + +-std::string_view Cipher::getName() const { ++WTF::String Cipher::getName() const { + if (!cipher_) return {}; + // OBJ_nid2sn(EVP_CIPHER_nid(cipher)) is used here instead of + // EVP_CIPHER_name(cipher) for compatibility with BoringSSL. +- return OBJ_nid2sn(getNid()); ++ return WTF::String::fromUTF8(OBJ_nid2sn(getNid())); + } + + bool Cipher::isSupportedAuthenticatedMode() const { +@@ -3497,15 +3528,15 @@ const std::optional Rsa::getPssParams() const { + const RSA_PSS_PARAMS* params = RSA_get0_pss_params(rsa_); + if (params == nullptr) return std::nullopt; + Rsa::PssParams ret{ +- .digest = OBJ_nid2ln(NID_sha1), +- .mgf1_digest = OBJ_nid2ln(NID_sha1), ++ .digest = WTF::StringView::fromLatin1(OBJ_nid2ln(NID_sha1)), ++ .mgf1_digest = WTF::StringView::fromLatin1(OBJ_nid2ln(NID_sha1)), + .salt_length = 20, + }; + + if (params->hashAlgorithm != nullptr) { + const ASN1_OBJECT* hash_obj; + X509_ALGOR_get0(&hash_obj, nullptr, nullptr, params->hashAlgorithm); +- ret.digest = OBJ_nid2ln(OBJ_obj2nid(hash_obj)); ++ ret.digest = WTF::StringView::fromLatin1(OBJ_nid2ln(OBJ_obj2nid(hash_obj))); + } + + if (params->maskGenAlgorithm != nullptr) { +@@ -3515,7 +3546,8 @@ const std::optional Rsa::getPssParams() const { + if (mgf_nid == NID_mgf1) { + const ASN1_OBJECT* mgf1_hash_obj; + X509_ALGOR_get0(&mgf1_hash_obj, nullptr, nullptr, params->maskHash); +- ret.mgf1_digest = OBJ_nid2ln(OBJ_obj2nid(mgf1_hash_obj)); ++ ret.mgf1_digest = ++ WTF::StringView::fromLatin1(OBJ_nid2ln(OBJ_obj2nid(mgf1_hash_obj))); + } + } + +@@ -3627,8 +3659,8 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } + + uint32_t Ec::getDegree() const { return EC_GROUP_get_degree(getGroup()); } + +-std::string Ec::getCurveName() const { +- return std::string(OBJ_nid2sn(getCurve())); ++WTF::String Ec::getCurveName() const { ++ return WTF::String::fromUTF8(OBJ_nid2sn(getCurve())); + } + + const EC_POINT* Ec::getPublicKey() const { return EC_KEY_get0_public_key(ec_); } +@@ -3891,7 +3923,7 @@ bool X509Name::Iterator::operator!=(const Iterator& other) const { + return loc_ != other.loc_; + } + +-std::pair X509Name::Iterator::operator*() const { ++std::pair X509Name::Iterator::operator*() const { + if (loc_ == name_.total_) return {{}, {}}; + + X509_NAME_ENTRY* entry = X509_NAME_get_entry(name_, loc_); +@@ -3906,21 +3938,22 @@ std::pair X509Name::Iterator::operator*() const { + } + + int nid = OBJ_obj2nid(name); +- std::string name_str; ++ WTF::String name_str; + if (nid != NID_undef) { +- name_str = std::string(OBJ_nid2sn(nid)); ++ name_str = WTF::String::fromUTF8(OBJ_nid2sn(nid)); + } else { + char buf[80]; + OBJ_obj2txt(buf, sizeof(buf), name, 0); +- name_str = std::string(buf); ++ name_str = WTF::String::fromUTF8(buf); + } + + unsigned char* value_str; + int value_str_size = ASN1_STRING_to_UTF8(&value_str, value); + + return { +- std::move(name_str), +- std::string(reinterpret_cast(value_str), value_str_size)}; ++ name_str, ++ WTF::String::fromUTF8(std::span(value_str, value_str_size)), ++ }; + } + + // ============================================================================ diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 3232289e0b..68414ff34e 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -637,6 +637,23 @@ JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalO return {}; } +// When you want INVALID_ARG_TYPE to say "The argument must be an instance of X. Received Y." instead of "The argument must be of type X. Received Y." +JSC::EncodedJSValue INVALID_ARG_INSTANCE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value) +{ + auto& vm = JSC::getVM(globalObject); + WTF::StringBuilder builder; + builder.append("The \""_s); + builder.append(arg_name); + builder.append("\" argument must be an instance of "_s); + builder.append(expected_type); + builder.append(". Received "_s); + determineSpecificType(vm, globalObject, builder, val_actual_value); + RETURN_IF_EXCEPTION(throwScope, {}); + + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, builder.toString())); + return {}; +} + JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, double lower, double upper, JSC::JSValue actual) { WTF::StringBuilder builder; @@ -789,6 +806,93 @@ JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobal return {}; } +// for validateOneOf +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, WTF::ASCIILiteral reason, JSC::JSArray* oneOf) +{ + WTF::StringBuilder builder; + builder.append("The argument '"_s); + JSValueToStringSafe(globalObject, builder, name); + RETURN_IF_EXCEPTION(throwScope, {}); + + builder.append("' "_s); + builder.append(reason); + unsigned length = oneOf->length(); + for (size_t i = 0; i < length; i++) { + JSValue index = oneOf->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(throwScope, {}); + if (index.isString()) { + JSString* str = index.toString(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + builder.append('\''); + builder.append(str->view(globalObject)); + builder.append('\''); + } else { + JSValueToStringSafe(globalObject, builder, index); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + if (i < length - 1) { + builder.append(", "_s); + } + } + builder.append(". Received "_s); + JSValueToStringSafe(globalObject, builder, value, true); + RETURN_IF_EXCEPTION(throwScope, {}); + + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, builder.toString())); + return {}; +} + +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, WTF::ASCIILiteral reason, JSC::JSValue value, const WTF::Vector& oneOf) +{ + WTF::StringBuilder builder; + builder.append("The "_s); + if (WTF::StringView(name).contains('.')) { + builder.append("property '"_s); + } else { + builder.append("argument '"_s); + } + builder.append(name); + builder.append("' "_s); + builder.append(reason); + + bool first = true; + for (ASCIILiteral oneOfStr : oneOf) { + if (!first) { + builder.append(", "_s); + } + first = false; + builder.append('`'); + builder.append(oneOfStr); + builder.append('`'); + } + + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, builder.toString())); + return {}; +} + +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& name, JSC::JSValue value, const WTF::String& reason) +{ + WTF::StringBuilder builder; + + builder.append("The "_s); + if (name.contains('.')) { + builder.append("property '"_s); + } else { + builder.append("argument '"_s); + } + builder.append(name); + builder.append("' "_s); + builder.append(reason); + builder.append(". Received "_s); + + JSValueToStringSafe(globalObject, builder, value); + RETURN_IF_EXCEPTION(throwScope, {}); + + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_VALUE, builder.toString())); + return {}; +} + JSC::EncodedJSValue INVALID_URL_SCHEME(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& expectedScheme) { auto message = makeString("The URL must be of scheme "_s, expectedScheme); @@ -911,6 +1015,57 @@ JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope& throwScope, JS return {}; } +JSC::EncodedJSValue CRYPTO_INVALID_JWK(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject) +{ + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INVALID_JWK, "Invalid JWK data"_s)); + return {}; +} + +JSC::EncodedJSValue CRYPTO_SIGN_KEY_REQUIRED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject) +{ + auto message = "No key provided to sign"_s; + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_SIGN_KEY_REQUIRED, message)); + return {}; +} + +JSC::EncodedJSValue CRYPTO_INVALID_KEY_OBJECT_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSValue received, WTF::ASCIILiteral expected) +{ + WTF::StringBuilder builder; + builder.append("Invalid key object type "_s); + JSValueToStringSafe(globalObject, builder, received); + RETURN_IF_EXCEPTION(throwScope, {}); + + builder.append(". Expected "_s); + builder.append(expected); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, builder.toString())); + return {}; +} + +JSC::EncodedJSValue CRYPTO_INCOMPATIBLE_KEY_OPTIONS(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& receivedKeyEncoding, const WTF::String& expectedOperation) +{ + WTF::StringBuilder builder; + builder.append("The selected key encoding "_s); + builder.append(receivedKeyEncoding); + builder.append(" does not support "_s); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, builder.toString())); + return {}; +} + +JSC::EncodedJSValue CRYPTO_INVALID_DIGEST(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& digest) +{ + WTF::StringBuilder builder; + builder.append("Invalid digest: "_s); + builder.append(digest); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INVALID_DIGEST, builder.toString())); + return {}; +} + +JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message) +{ + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_MISSING_PASSPHRASE, message)); + return {}; +} + } // namespace ERR static JSC::JSValue ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue arg0, JSValue arg1, JSValue arg2) diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 533aa01bde..619a476648 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -67,14 +67,18 @@ namespace ERR { JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value); JSC::EncodedJSValue INVALID_ARG_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value); +JSC::EncodedJSValue INVALID_ARG_INSTANCE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, const WTF::String& expected_type, JSC::JSValue val_actual_value); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name, double lower, double upper, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name, double lower, double upper, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, double bound_num, Bound bound, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue arg_name_val, const WTF::String& msg, JSC::JSValue actual); JSC::EncodedJSValue OUT_OF_RANGE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& arg_name_val, const WTF::String& msg, JSC::JSValue actual); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, WTF::ASCIILiteral reason, JSC::JSArray* oneOf); +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, WTF::ASCIILiteral reason, JSC::JSValue value, const WTF::Vector& oneOf); JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); +JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding); JSC::EncodedJSValue INVALID_STATE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& statemsg); JSC::EncodedJSValue STRING_TOO_LONG(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); @@ -86,6 +90,12 @@ JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* JSC::EncodedJSValue ASSERTION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, ASCIILiteral msg); JSC::EncodedJSValue CRYPTO_INVALID_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope&, JSC::JSGlobalObject*, const WTF::String&); +JSC::EncodedJSValue CRYPTO_INVALID_JWK(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); +JSC::EncodedJSValue CRYPTO_SIGN_KEY_REQUIRED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); +JSC::EncodedJSValue CRYPTO_INVALID_KEY_OBJECT_TYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSValue received, WTF::ASCIILiteral expected); +JSC::EncodedJSValue CRYPTO_INCOMPATIBLE_KEY_OPTIONS(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& received, const WTF::String& expected); +JSC::EncodedJSValue CRYPTO_INVALID_DIGEST(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& digest); +JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message); // URL diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index bc10108830..6b38d195c8 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -30,6 +30,10 @@ const errors: ErrorCodeMapping = [ ["ERR_CRYPTO_JWK_UNSUPPORTED_CURVE", Error], ["ERR_CRYPTO_OPERATION_FAILED", Error], ["ERR_CRYPTO_SCRYPT_INVALID_PARAMETER", Error], + ["ERR_CRYPTO_SIGN_KEY_REQUIRED", Error], + ["ERR_CRYPTO_INVALID_JWK", TypeError], + ["ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", Error], + ["ERR_MISSING_PASSPHRASE", TypeError], ["ERR_DLOPEN_FAILED", Error], ["ERR_ENCODING_INVALID_ENCODED_DATA", TypeError], ["ERR_ILLEGAL_CONSTRUCTOR", TypeError], diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index 745caba24e..2cd3ce690a 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -98,7 +98,6 @@ static JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_byteLength); static JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_compare); static JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_concat); static JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_copyBytesFrom); -static JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_from); static JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_isBuffer); static JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_isEncoding); @@ -500,7 +499,7 @@ static JSC::EncodedJSValue constructBufferEmpty(JSGlobalObject* lexicalGlobalObj return JSBuffer__bufferFromLength(lexicalGlobalObject, 0); } -static JSC::EncodedJSValue constructFromEncoding(JSGlobalObject* lexicalGlobalObject, JSString* str, WebCore::BufferEncodingType encoding) +JSC::EncodedJSValue constructFromEncoding(JSGlobalObject* lexicalGlobalObject, JSString* str, WebCore::BufferEncodingType encoding) { auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); @@ -1715,7 +1714,7 @@ static JSC::EncodedJSValue jsBufferPrototypeFunction_swap64Body(JSC::JSGlobalObj return JSC::JSValue::encode(castedThis); } -static JSC::EncodedJSValue jsBufferToString(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSArrayBufferView* castedThis, size_t offset, size_t length, WebCore::BufferEncodingType encoding) +JSC::EncodedJSValue jsBufferToString(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSArrayBufferView* castedThis, size_t offset, size_t length, WebCore::BufferEncodingType encoding) { auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/bun.js/bindings/JSBuffer.h b/src/bun.js/bindings/JSBuffer.h index 14c10000a8..b583e79816 100644 --- a/src/bun.js/bindings/JSBuffer.h +++ b/src/bun.js/bindings/JSBuffer.h @@ -64,5 +64,7 @@ JSC_DECLARE_HOST_FUNCTION(constructSlowBuffer); JSC::JSObject* createBufferPrototype(JSC::VM&, JSC::JSGlobalObject*); JSC::Structure* createBufferStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); JSC::JSObject* createBufferConstructor(JSC::VM&, JSC::JSGlobalObject*, JSC::JSObject* bufferPrototype); +JSC::EncodedJSValue jsBufferToString(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSArrayBufferView* castedThis, size_t offset, size_t length, WebCore::BufferEncodingType encoding); +JSC::EncodedJSValue constructFromEncoding(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSString* str, WebCore::BufferEncodingType encoding); } diff --git a/src/bun.js/bindings/JSX509Certificate.cpp b/src/bun.js/bindings/JSX509Certificate.cpp index 9b84b3dc4a..43aa3948c7 100644 --- a/src/bun.js/bindings/JSX509Certificate.cpp +++ b/src/bun.js/bindings/JSX509Certificate.cpp @@ -635,7 +635,7 @@ bool JSX509Certificate::checkEmail(JSGlobalObject* globalObject, std::span ip) +bool JSX509Certificate::checkIP(JSGlobalObject* globalObject, const char* ip) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/bun.js/bindings/JSX509Certificate.h b/src/bun.js/bindings/JSX509Certificate.h index 0a473af7f6..9fd998cd8f 100644 --- a/src/bun.js/bindings/JSX509Certificate.h +++ b/src/bun.js/bindings/JSX509Certificate.h @@ -66,7 +66,7 @@ public: // Certificate validation methods bool checkHost(JSGlobalObject*, std::span, uint32_t flags); bool checkEmail(JSGlobalObject*, std::span, uint32_t flags); - bool checkIP(JSGlobalObject*, std::span); + bool checkIP(JSGlobalObject*, const char*); bool checkIssued(JSGlobalObject*, JSX509Certificate* issuer); bool checkPrivateKey(JSGlobalObject*, EVP_PKEY* pkey); bool verify(JSGlobalObject*, EVP_PKEY* pkey); diff --git a/src/bun.js/bindings/JSX509CertificatePrototype.cpp b/src/bun.js/bindings/JSX509CertificatePrototype.cpp index caa127f364..9e6b4a1fab 100644 --- a/src/bun.js/bindings/JSX509CertificatePrototype.cpp +++ b/src/bun.js/bindings/JSX509CertificatePrototype.cpp @@ -335,7 +335,7 @@ JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncCheckIP, (JSGlobalObject * gl // uint32_t flags = getFlags(vm, globalObject, scope, callFrame->argument(1)); // RETURN_IF_EXCEPTION(scope, {}); - if (!thisObject->checkIP(globalObject, ip.spanIncludingNullTerminator())) + if (!thisObject->checkIP(globalObject, ip.data())) return JSValue::encode(jsUndefined()); return JSValue::encode(ipString); } diff --git a/src/bun.js/bindings/KeyObject.cpp b/src/bun.js/bindings/KeyObject.cpp index 6a9b816f3d..eff2053941 100644 --- a/src/bun.js/bindings/KeyObject.cpp +++ b/src/bun.js/bindings/KeyObject.cpp @@ -102,7 +102,7 @@ static bool KeyObject__IsASN1Sequence(const unsigned char* data, size_t size, if (data[1] & 0x80) { // Long form. size_t n_bytes = data[1] & ~0x80; - if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) + if (n_bytes > size || n_bytes > sizeof(size_t)) return false; size_t length = 0; for (size_t i = 0; i < n_bytes; i++) @@ -1319,7 +1319,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__createSecretKey, (JSC::JSGlobalObject * lexi return {}; } -ExceptionOr> KeyObject__GetBuffer(JSValue bufferArg) +ExceptionOr> KeyObject__GetBuffer(JSValue bufferArg) { if (!bufferArg.isCell()) { return Exception { OperationError }; @@ -1343,13 +1343,10 @@ ExceptionOr> KeyObject__GetBuffer(JSValue bufferArg) case BigInt64ArrayType: case BigUint64ArrayType: { JSC::JSArrayBufferView* view = jsCast(bufferArgCell); - - void* data = view->vector(); - size_t byteLength = view->length(); - if (UNLIKELY(!data)) { + if (view->isDetached()) { break; } - return Vector(std::span { (uint8_t*)data, byteLength }); + return view->span(); } case ArrayBufferType: { auto* jsBuffer = jsDynamicCast(bufferArgCell); @@ -1357,12 +1354,10 @@ ExceptionOr> KeyObject__GetBuffer(JSValue bufferArg) break; } auto* buffer = jsBuffer->impl(); - void* data = buffer->data(); - size_t byteLength = buffer->byteLength(); - if (UNLIKELY(!data)) { + if (buffer->isDetached()) { break; } - return Vector(std::span { (uint8_t*)data, byteLength }); + return buffer->span(); } default: { break; diff --git a/src/bun.js/bindings/KeyObject.h b/src/bun.js/bindings/KeyObject.h index 9c4bd99731..c34eec66df 100644 --- a/src/bun.js/bindings/KeyObject.h +++ b/src/bun.js/bindings/KeyObject.h @@ -7,7 +7,7 @@ namespace WebCore { -ExceptionOr> KeyObject__GetBuffer(JSC::JSValue bufferArg); +ExceptionOr> KeyObject__GetBuffer(JSC::JSValue bufferArg); JSC::JSValue createKeyObjectBinding(Zig::GlobalObject* globalObject); } // namespace WebCore diff --git a/src/bun.js/bindings/NodeValidator.cpp b/src/bun.js/bindings/NodeValidator.cpp index 088a03a3b5..23228f32ef 100644 --- a/src/bun.js/bindings/NodeValidator.cpp +++ b/src/bun.js/bindings/NodeValidator.cpp @@ -213,6 +213,15 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_checkRangesOrGetDefault, (JSC::JSGlobalObjec return JSValue::encode(number); } +JSC::EncodedJSValue V::validateFunction(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name) +{ + if (JSC::getCallData(value).type == JSC::CallData::Type::None) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "function"_s, value); + } + + return JSValue::encode(jsUndefined()); +} + JSC_DEFINE_HOST_FUNCTION(jsFunction_validateFunction, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto& vm = JSC::getVM(globalObject); @@ -379,6 +388,19 @@ JSC::EncodedJSValue V::validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject return JSValue::encode(jsUndefined()); } +JSC::EncodedJSValue V::validateArrayBufferView(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, + ASCIILiteral name) +{ + if (value.isCell()) { + auto type = value.asCell()->type(); + if (type >= Int8ArrayType && type <= DataViewType) { + return JSValue::encode(jsUndefined()); + } + } + + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Buffer, TypedArray, or DataView"_s, value); +} + JSC_DEFINE_HOST_FUNCTION(jsFunction_validateInt32, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto& vm = JSC::getVM(globalObject); @@ -546,6 +568,84 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateBuffer, (JSC::JSGlobalObject * globa return JSValue::encode(jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsFunction_validateOneOf, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue value = callFrame->argument(0); + + JSValue arrayValue = callFrame->argument(2); + + if (JSArray* array = jsDynamicCast(arrayValue)) { + unsigned length = array->length(); + for (size_t i = 0; i < length; i++) { + JSValue element = array->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + if (JSC::sameValue(globalObject, value, element)) { + return JSValue::encode(jsUndefined()); + } + } + + JSValue name = callFrame->argument(1); + + return Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, name, value, "must be one of: "_s, array); + } + + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "values"_s, "Array"_s, arrayValue); +} + +JSC::EncodedJSValue V::validateOneOf(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, ASCIILiteral name, JSValue value, const WTF::Vector& oneOf) +{ + if (!value.isString()) { + return Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, name, "must be one of: "_s, value, oneOf); + } + + JSC::JSString* valueStr = value.toString(globalObject); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + WTF::StringView valueView = valueStr->view(globalObject); + + for (ASCIILiteral oneOfStr : oneOf) { + + if (valueView == oneOfStr) { + return JSValue::encode(jsUndefined()); + } + } + + return Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, name, "must be one of: "_s, value, oneOf); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunction_validateObject, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto value = callFrame->argument(0); + + if (value.isNull() || JSC::isArray(globalObject, value)) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, callFrame->argument(1), "object"_s, value); + } + + if (!value.isObject() || value.asCell()->type() != FinalObjectType) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, callFrame->argument(1), "object"_s, value); + } + + return JSValue::encode(jsUndefined()); +} + +JSC::EncodedJSValue V::validateObject(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name) +{ + if (value.isNull() || JSC::isArray(globalObject, value)) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "object"_s, value); + } + + if (!value.isObject() || value.asCell()->type() != FinalObjectType) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "object"_s, value); + } + + return JSValue::encode(jsUndefined()); +} + // // @@ -556,5 +656,4 @@ template JSC::EncodedJSValue V::validateInteger(JSC::ThrowScope& scope template JSC::EncodedJSValue V::validateInteger(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max, size_t* out); template JSC::EncodedJSValue V::validateInteger(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max, ssize_t* out); template JSC::EncodedJSValue V::validateInteger(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSC::JSValue value, ASCIILiteral name, JSC::JSValue min, JSC::JSValue max, uint32_t* out); - } diff --git a/src/bun.js/bindings/NodeValidator.h b/src/bun.js/bindings/NodeValidator.h index 26593243d6..d60ed19b2f 100644 --- a/src/bun.js/bindings/NodeValidator.h +++ b/src/bun.js/bindings/NodeValidator.h @@ -2,6 +2,7 @@ #include "ZigGlobalObject.h" #include "ErrorCode.h" +#include "BufferEncodingType.h" #include "JavaScriptCore/JSCJSValue.h" namespace Bun { @@ -23,6 +24,8 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateEncoding, (JSC::JSGlobalObject * glo JSC_DEFINE_HOST_FUNCTION(jsFunction_validatePlainFunction, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)); JSC_DEFINE_HOST_FUNCTION(jsFunction_validateUndefined, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)); JSC_DEFINE_HOST_FUNCTION(jsFunction_validateBuffer, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)); +JSC_DEFINE_HOST_FUNCTION(jsFunction_validateOneOf, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)); +JSC_DEFINE_HOST_FUNCTION(jsFunction_validateObject, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)); namespace V { @@ -36,8 +39,12 @@ JSC::EncodedJSValue validateString(JSC::ThrowScope& scope, JSC::JSGlobalObject* JSC::EncodedJSValue validateString(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name); JSC::EncodedJSValue validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, JSValue name, JSValue minLength); JSC::EncodedJSValue validateArray(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue minLength); +JSC::EncodedJSValue validateArrayBufferView(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name); JSC::EncodedJSValue validateUint32(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, JSValue name, JSValue positive); JSC::EncodedJSValue validateUint32(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name, JSValue positive); +JSC::EncodedJSValue validateFunction(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name); +JSC::EncodedJSValue validateOneOf(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, ASCIILiteral name, JSValue value, const WTF::Vector& oneOf); +JSC::EncodedJSValue validateObject(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue value, ASCIILiteral name); } diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 4da4477966..257a876d39 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -160,6 +160,8 @@ #include "JSPerformanceResourceTiming.h" #include "JSPerformanceTiming.h" #include "JSX509Certificate.h" +#include "JSSign.h" +#include "JSVerify.h" #include "JSS3File.h" #include "S3Error.h" #include "ProcessBindingBuffer.h" @@ -2814,6 +2816,17 @@ void GlobalObject::finishCreation(VM& vm) m_JSX509CertificateClassStructure.initLater([](LazyClassStructure::Initializer& init) { setupX509CertificateClassStructure(init); }); + + m_JSSignClassStructure.initLater( + [](LazyClassStructure::Initializer& init) { + setupJSSignClassStructure(init); + }); + + m_JSVerifyClassStructure.initLater( + [](LazyClassStructure::Initializer& init) { + setupJSVerifyClassStructure(init); + }); + m_lazyStackCustomGetterSetter.initLater( [](const Initializer& init) { init.set(CustomGetterSetter::create(init.vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter)); @@ -4000,6 +4013,8 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSBunRequestStructure.visit(visitor); thisObject->m_JSBunRequestParamsPrototype.visit(visitor); thisObject->m_JSX509CertificateClassStructure.visit(visitor); + thisObject->m_JSSignClassStructure.visit(visitor); + thisObject->m_JSVerifyClassStructure.visit(visitor); thisObject->m_statValues.visit(visitor); thisObject->m_bigintStatValues.visit(visitor); thisObject->m_statFsValues.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 843342a1c0..983840900e 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -532,6 +532,8 @@ public: LazyClassStructure m_JSBufferClassStructure; LazyClassStructure m_NodeVMScriptClassStructure; LazyClassStructure m_JSX509CertificateClassStructure; + LazyClassStructure m_JSSignClassStructure; + LazyClassStructure m_JSVerifyClassStructure; /** * WARNING: You must update visitChildrenImpl() if you add a new field. diff --git a/src/bun.js/bindings/ncrypto.cpp b/src/bun.js/bindings/ncrypto.cpp index 20d78bb385..2a36671bd7 100644 --- a/src/bun.js/bindings/ncrypto.cpp +++ b/src/bun.js/bindings/ncrypto.cpp @@ -1,8 +1,11 @@ #include "root.h" #include "wtf/text/ASCIILiteral.h" #include "wtf/text/StringImpl.h" +#include "wtf/text/WTFString.h" #include "ncrypto.h" + +#include #include #include #include @@ -10,24 +13,35 @@ #include #include #include + +#ifndef NCRYPTO_NO_KDF_H +#include +#else +#include +#endif + #include #include #if OPENSSL_VERSION_MAJOR >= 3 #include #endif -#ifdef OPENSSL_IS_BORINGSSL -#include "dh-primes.h" -#endif // OPENSSL_IS_BORINGSSL + +// EVP_PKEY_CTX_set_dsa_paramgen_q_bits was added in OpenSSL 1.1.1e. +#if OPENSSL_VERSION_NUMBER < 0x1010105fL +#define EVP_PKEY_CTX_set_dsa_paramgen_q_bits(ctx, qbits) \ + EVP_PKEY_CTX_ctrl((ctx), EVP_PKEY_DSA, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DSA_PARAMGEN_Q_BITS, (qbits), nullptr) +#endif + +// ============================================================================ namespace ncrypto { - namespace { -static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; +using BignumCtxPointer = DeleteFnPtr; +using BignumGenCallbackPointer = DeleteFnPtr; +using NetscapeSPKIPointer = DeleteFnPtr; -bool EqualNoCase(WTF::StringView a, ASCIILiteral b) -{ - return WTF::equalIgnoringASCIICase(a, b); -} +static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; } // namespace // ============================================================================ @@ -44,10 +58,7 @@ ClearErrorOnReturn::~ClearErrorOnReturn() ERR_clear_error(); } -int ClearErrorOnReturn::peekError() -{ - return ERR_peek_error(); -} +int ClearErrorOnReturn::peekError() { return ERR_peek_error(); } MarkPopErrorOnReturn::MarkPopErrorOnReturn(CryptoErrorList* errors) : errors_(errors) @@ -61,10 +72,7 @@ MarkPopErrorOnReturn::~MarkPopErrorOnReturn() ERR_pop_to_mark(); } -int MarkPopErrorOnReturn::peekError() -{ - return ERR_peek_error(); -} +int MarkPopErrorOnReturn::peekError() { return ERR_peek_error(); } CryptoErrorList::CryptoErrorList(CryptoErrorList::Option option) { @@ -73,39 +81,49 @@ CryptoErrorList::CryptoErrorList(CryptoErrorList::Option option) void CryptoErrorList::capture() { - unsigned long err; - while ((err = ERR_get_error()) != 0) { + errors_.clear(); + while (const auto err = ERR_get_error()) { char buf[256]; ERR_error_string_n(err, buf, sizeof(buf)); - add(WTF::String::fromUTF8(buf)); + errors_.emplace_front(WTF::String::fromUTF8(buf)); } } -void CryptoErrorList::add(WTF::String message) -{ - errors_.push_back(WTFMove(message)); -} +void CryptoErrorList::add(WTF::String error) { errors_.push_back(error); } std::optional CryptoErrorList::pop_back() { if (errors_.empty()) return std::nullopt; - WTF::String message = WTFMove(errors_.back()); + WTF::String error = errors_.back(); errors_.pop_back(); - return message; + return error; } std::optional CryptoErrorList::pop_front() { if (errors_.empty()) return std::nullopt; - WTF::String message = WTFMove(errors_.front()); + WTF::String error = errors_.front(); errors_.pop_front(); - return message; + return error; } // ============================================================================ DataPointer DataPointer::Alloc(size_t len) { +#ifdef OPENSSL_IS_BORINGSSL + // Boringssl does not implement OPENSSL_zalloc + auto ptr = OPENSSL_malloc(len); + if (ptr == nullptr) return {}; + memset(ptr, 0, len); + return DataPointer(ptr, len); +#else return DataPointer(OPENSSL_zalloc(len), len); +#endif +} + +DataPointer DataPointer::Copy(const Buffer& buffer) +{ + return DataPointer(OPENSSL_memdup(buffer.data, buffer.len), buffer.len); } DataPointer::DataPointer(void* data, size_t length) @@ -135,9 +153,12 @@ DataPointer& DataPointer::operator=(DataPointer&& other) noexcept return *new (this) DataPointer(std::move(other)); } -DataPointer::~DataPointer() +DataPointer::~DataPointer() { reset(); } + +void DataPointer::zero() { - reset(); + if (!data_) return; + OPENSSL_cleanse(data_, len_); } void DataPointer::reset(void* data, size_t length) @@ -165,13 +186,23 @@ Buffer DataPointer::release() return buf; } +DataPointer DataPointer::resize(size_t len) +{ + size_t actual_len = std::min(len_, len); + auto buf = release(); + if (actual_len == len_) return DataPointer(buf); + buf.data = OPENSSL_realloc(buf.data, actual_len); + buf.len = actual_len; + return DataPointer(buf); +} + // ============================================================================ bool isFipsEnabled() { -#ifdef OPENSSL_FIPS - return FIPS_mode() == 1; +#if OPENSSL_VERSION_MAJOR >= 3 + return EVP_default_properties_is_fips_enabled(nullptr) == 1; #else - return false; + return FIPS_mode() == 1; #endif } @@ -179,20 +210,32 @@ bool setFipsEnabled(bool enable, CryptoErrorList* errors) { if (isFipsEnabled() == enable) return true; ClearErrorOnReturn clearErrorOnReturn(errors); -#ifdef OPENSSL_FIPS - return FIPS_mode_set(enable ? 1 : 0) == 1; +#if OPENSSL_VERSION_MAJOR >= 3 + return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1; #else - return false; + return FIPS_mode_set(enable ? 1 : 0) == 1; #endif } bool testFipsEnabled() { -#ifdef OPENSSL_FIPS - return FIPS_selftest() == 1; +#if OPENSSL_VERSION_MAJOR >= 3 + OSSL_PROVIDER* fips_provider = nullptr; + if (OSSL_PROVIDER_available(nullptr, "fips")) { + fips_provider = OSSL_PROVIDER_load(nullptr, "fips"); + } + const auto enabled = fips_provider == nullptr ? 0 + : OSSL_PROVIDER_self_test(fips_provider) ? 1 + : 0; #else - return false; +#ifdef OPENSSL_FIPS + const auto enabled = FIPS_selftest() ? 1 : 0; +#else // OPENSSL_FIPS + const auto enabled = 0; +#endif // OPENSSL_FIPS #endif + + return enabled; } // ============================================================================ @@ -212,14 +255,16 @@ BignumPointer::BignumPointer(BignumPointer&& other) noexcept { } -BignumPointer BignumPointer::New() -{ - return BignumPointer(BN_new()); -} +BignumPointer BignumPointer::New() { return BignumPointer(BN_new()); } BignumPointer BignumPointer::NewSecure() { +#ifdef OPENSSL_IS_BORINGSSL + // Boringssl does not implement BN_secure_new. + return New(); +#else return BignumPointer(BN_secure_new()); +#endif } BignumPointer& BignumPointer::operator=(BignumPointer&& other) noexcept @@ -229,32 +274,29 @@ BignumPointer& BignumPointer::operator=(BignumPointer&& other) noexcept return *new (this) BignumPointer(std::move(other)); } -BignumPointer::~BignumPointer() -{ - reset(); -} +BignumPointer::~BignumPointer() { reset(); } -void BignumPointer::reset(BIGNUM* bn) -{ - bn_.reset(bn); -} +void BignumPointer::reset(BIGNUM* bn) { bn_.reset(bn); } void BignumPointer::reset(const unsigned char* data, size_t len) { reset(BN_bin2bn(data, len, nullptr)); } -BIGNUM* BignumPointer::release() -{ - return bn_.release(); -} +BIGNUM* BignumPointer::release() { return bn_.release(); } size_t BignumPointer::byteLength() const { - if (bn_ == nullptr) return 0; + if (!bn_) return 0; return BN_num_bytes(bn_.get()); } +size_t BignumPointer::bitLength() const +{ + if (!bn_) return 0; + return BN_num_bits(bn_.get()); +} + DataPointer BignumPointer::encode() const { return EncodePadded(bn_.get(), byteLength()); @@ -307,8 +349,7 @@ DataPointer BignumPointer::EncodePadded(const BIGNUM* bn, size_t s) BN_bn2binpad(bn, reinterpret_cast(buf.get()), size); return buf; } -size_t BignumPointer::EncodePaddedInto(const BIGNUM* bn, - unsigned char* out, +size_t BignumPointer::EncodePaddedInto(const BIGNUM* bn, unsigned char* out, size_t size) { if (bn == nullptr) return 0; @@ -339,30 +380,15 @@ DataPointer BignumPointer::toHex() const return DataPointer(hex, strlen(hex)); } -int BignumPointer::GetBitCount(const BIGNUM* bn) -{ - return BN_num_bits(bn); -} +int BignumPointer::GetBitCount(const BIGNUM* bn) { return BN_num_bits(bn); } -int BignumPointer::GetByteCount(const BIGNUM* bn) -{ - return BN_num_bytes(bn); -} +int BignumPointer::GetByteCount(const BIGNUM* bn) { return BN_num_bytes(bn); } -bool BignumPointer::isZero() const -{ - return bn_ && BN_is_zero(bn_.get()); -} +bool BignumPointer::isZero() const { return bn_ && BN_is_zero(bn_.get()); } -bool BignumPointer::isOne() const -{ - return bn_ && BN_is_one(bn_.get()); -} +bool BignumPointer::isOne() const { return bn_ && BN_is_one(bn_.get()); } -const BIGNUM* BignumPointer::One() -{ - return BN_value_one(); -} +const BIGNUM* BignumPointer::One() { return BN_value_one(); } BignumPointer BignumPointer::clone() { @@ -384,7 +410,11 @@ int BignumPointer::isPrime(int nchecks, // TODO(@jasnell): This could be refactored to allow inlining. // Not too important right now tho. [](int a, int b, BN_GENCB* ctx) mutable -> int { - PrimeCheckCallback& ptr = *static_cast(BN_GENCB_get_arg(ctx)); + PrimeCheckCallback& ptr = *static_cast(ctx->arg); + // Newer versions of openssl and boringssl define the BN_GENCB_get_arg + // API which is what is supposed to be used here. Older versions, + // however, omit that API. + // *static_cast(BN_GENCB_get_arg(ctx)); return ptr(a, b) ? 1 : 0; }, &innerCb); @@ -407,7 +437,7 @@ bool BignumPointer::generate(const PrimeConfig& params, { // BN_generate_prime_ex() calls RAND_bytes_ex() internally. // Make sure the CSPRNG is properly seeded. - (void)CSPRNG(nullptr, 0); + std::ignore = CSPRNG(nullptr, 0); BignumGenCallbackPointer cb(nullptr); if (innerCb != nullptr) { cb = BignumGenCallbackPointer(BN_GENCB_new()); @@ -416,17 +446,17 @@ bool BignumPointer::generate(const PrimeConfig& params, BN_GENCB_set( cb.get(), [](int a, int b, BN_GENCB* ctx) mutable -> int { - PrimeCheckCallback& ptr = *static_cast(BN_GENCB_get_arg(ctx)); + PrimeCheckCallback& ptr = *static_cast(ctx->arg); + // Newer versions of openssl and boringssl define the BN_GENCB_get_arg + // API which is what is supposed to be used here. Older versions, + // however, omit that API. + // *static_cast(BN_GENCB_get_arg(ctx)); return ptr(a, b) ? 1 : 0; }, &innerCb); } - if (BN_generate_prime_ex(get(), - params.bits, - params.safe ? 1 : 0, - params.add.get(), - params.rem.get(), - cb.get()) + if (BN_generate_prime_ex(get(), params.bits, params.safe ? 1 : 0, + params.add.get(), params.rem.get(), cb.get()) == 0) { return false; } @@ -493,10 +523,7 @@ bool CSPRNG(void* buffer, size_t length) return false; } -int NoPasswordCallback(char* buf, int size, int rwflag, void* u) -{ - return 0; -} +int NoPasswordCallback(char* buf, int size, int rwflag, void* u) { return 0; } int PasswordCallback(char* buf, int size, int rwflag, void* u) { @@ -523,6 +550,7 @@ constexpr int days_from_epoch(int y, unsigned m, unsigned d) return era * 146097 + static_cast(doe) - 719468; } +#ifndef OPENSSL_IS_BORINGSSL // tm must be in UTC // using time_t causes problems on 32-bit systems and windows x64. int64_t PortableTimeGM(struct tm* t) @@ -541,12 +569,15 @@ int64_t PortableTimeGM(struct tm* t) return 60 * (60 * (24LL * static_cast(days_since_epoch) + t->tm_hour) + t->tm_min) + t->tm_sec; } +#endif // ============================================================================ // SPKAC -bool VerifySpkac(const char* input, size_t length) +namespace { +bool VerifySpkacImpl(const char* input, size_t length) { + ClearErrorOnReturn clearErrorOnReturn; #ifdef OPENSSL_IS_BORINGSSL // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters, // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not. @@ -564,10 +595,12 @@ bool VerifySpkac(const char* input, size_t length) return pkey ? NETSCAPE_SPKI_verify(spki.get(), pkey.get()) > 0 : false; } -BIOPointer ExportPublicKey(const char* input, size_t length) +BIOPointer ExportPublicKeyImpl(const char* input, size_t length) { - BIOPointer bio(BIO_new(BIO_s_mem())); - if (!bio) return {}; + ClearErrorOnReturn clearErrorOnReturn; + auto bio = BIOPointer::NewMem(); + if (!bio) [[unlikely]] + return {}; #ifdef OPENSSL_IS_BORINGSSL // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters, @@ -576,18 +609,22 @@ BIOPointer ExportPublicKey(const char* input, size_t length) length = std::string_view(input, length).find_last_not_of(" \n\r\t") + 1; #endif NetscapeSPKIPointer spki(NETSCAPE_SPKI_b64_decode(input, length)); - if (!spki) return {}; + if (!spki) [[unlikely]] { + return {}; + } EVPKeyPointer pkey(NETSCAPE_SPKI_get_pubkey(spki.get())); - if (!pkey) return {}; - if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) return {}; + if (!pkey || PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) [[unlikely]] { + return {}; + } return bio; } -Buffer ExportChallenge(const char* input, size_t length) +DataPointer ExportChallengeImpl(const char* input, size_t length) { + ClearErrorOnReturn clearErrorOnReturn; #ifdef OPENSSL_IS_BORINGSSL // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters, // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not. @@ -600,14 +637,52 @@ Buffer ExportChallenge(const char* input, size_t length) unsigned char* buf = nullptr; int buf_size = ASN1_STRING_to_UTF8(&buf, sp->spkac->challenge); if (buf_size >= 0) { - return { + return DataPointer({ .data = reinterpret_cast(buf), .len = static_cast(buf_size), - }; + }); } return {}; } +} // namespace + +bool VerifySpkac(const Buffer& input) +{ + return VerifySpkacImpl(input.data, input.len); +} + +BIOPointer ExportPublicKey(const Buffer& input) +{ + return ExportPublicKeyImpl(input.data, input.len); +} + +DataPointer ExportChallenge(const Buffer& input) +{ + return ExportChallengeImpl(input.data, input.len); +} + +bool VerifySpkac(const char* input, size_t length) +{ + return VerifySpkacImpl(input, length); +} + +BIOPointer ExportPublicKey(const char* input, size_t length) +{ + return ExportPublicKeyImpl(input, length); +} + +Buffer ExportChallenge(const char* input, size_t length) +{ + if (auto dp = ExportChallengeImpl(input, length)) { + auto released = dp.release(); + return Buffer { + .data = static_cast(released.data), + .len = released.len, + }; + } + return {}; +} // ============================================================================ namespace { @@ -656,9 +731,7 @@ bool IsSafeAltName(const char* name, size_t length, AltNameOption option) return true; } -void PrintAltName(const BIOPointer& out, - const char* name, - size_t length, +void PrintAltName(const BIOPointer& out, const char* name, size_t length, AltNameOption option = AltNameOption::NONE, const char* safe_prefix = nullptr) { @@ -742,19 +815,16 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) BIO_printf(out.get(), "DirName:"); BIOPointer tmp(BIO_new(BIO_s_mem())); NCRYPTO_ASSERT_TRUE(tmp); - if (X509_NAME_print_ex( - tmp.get(), gen->d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) + if (X509_NAME_print_ex(tmp.get(), gen->d.dirn, 0, + kX509NameFlagsRFC2253WithinUtf8JSON) < 0) { return false; } char* oline = nullptr; long n_bytes = BIO_get_mem_data(tmp.get(), &oline); // NOLINT(runtime/int) NCRYPTO_ASSERT_TRUE(n_bytes >= 0); - PrintAltName(out, - oline, - static_cast(n_bytes), - ncrypto::AltNameOption::UTF8, - nullptr); + PrintAltName(out, oline, static_cast(n_bytes), + ncrypto::AltNameOption::UTF8, nullptr); } else if (gen->type == GEN_IPADD) { BIO_printf(out.get(), "IP Address:"); const ASN1_OCTET_STRING* ip = gen->d.ip; @@ -816,18 +886,12 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) BIO_printf(out.get(), "othername:"); if (unicode) { auto name = gen->d.otherName->value->value.utf8string; - PrintAltName(out, - reinterpret_cast(name->data), - name->length, - AltNameOption::UTF8, - prefix); + PrintAltName(out, reinterpret_cast(name->data), + name->length, AltNameOption::UTF8, prefix); } else { auto name = gen->d.otherName->value->value.ia5string; - PrintAltName(out, - reinterpret_cast(name->data), - name->length, - AltNameOption::NONE, - prefix); + PrintAltName(out, reinterpret_cast(name->data), + name->length, AltNameOption::NONE, prefix); } } } else if (gen->type == GEN_X400) { @@ -848,15 +912,15 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) bool SafeX509SubjectAltNamePrint(const BIOPointer& out, X509_EXTENSION* ext) { - [[maybe_unused]] auto ret = OBJ_obj2nid(X509_EXTENSION_get_object(ext)); - NCRYPTO_ASSERT_EQUAL(ret, NID_subject_alt_name, "unexpected extension type"); + auto ret = OBJ_obj2nid(X509_EXTENSION_get_object(ext)); + if (ret != NID_subject_alt_name) return false; GENERAL_NAMES* names = static_cast(X509V3_EXT_d2i(ext)); if (names == nullptr) return false; bool ok = true; - for (int i = 0; i < sk_GENERAL_NAME_num(names); i++) { + for (OPENSSL_SIZE_T i = 0; i < sk_GENERAL_NAME_num(names); i++) { GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i); if (i != 0) BIO_write(out.get(), ", ", 2); @@ -873,14 +937,14 @@ bool SafeX509SubjectAltNamePrint(const BIOPointer& out, X509_EXTENSION* ext) bool SafeX509InfoAccessPrint(const BIOPointer& out, X509_EXTENSION* ext) { auto ret = OBJ_obj2nid(X509_EXTENSION_get_object(ext)); - NCRYPTO_ASSERT_EQUAL(ret, NID_info_access, "unexpected extension type"); + if (ret != NID_info_access) return false; AUTHORITY_INFO_ACCESS* descs = static_cast(X509V3_EXT_d2i(ext)); if (descs == nullptr) return false; bool ok = true; - for (int i = 0; i < sk_ACCESS_DESCRIPTION_num(descs); i++) { + for (OPENSSL_SIZE_T i = 0; i < sk_ACCESS_DESCRIPTION_num(descs); i++) { ACCESS_DESCRIPTION* desc = sk_ACCESS_DESCRIPTION_value(descs, i); if (i != 0) BIO_write(out.get(), "\n", 1); @@ -921,25 +985,13 @@ X509Pointer& X509Pointer::operator=(X509Pointer&& other) noexcept return *new (this) X509Pointer(std::move(other)); } -X509Pointer::~X509Pointer() -{ - reset(); -} +X509Pointer::~X509Pointer() { reset(); } -void X509Pointer::reset(X509* x509) -{ - cert_.reset(x509); -} +void X509Pointer::reset(X509* x509) { cert_.reset(x509); } -X509* X509Pointer::release() -{ - return cert_.release(); -} +X509* X509Pointer::release() { return cert_.release(); } -X509View X509Pointer::view() const -{ - return X509View(cert_.get()); -} +X509View X509Pointer::view() const { return X509View(cert_.get()); } BIOPointer X509View::toPEM() const { @@ -961,15 +1013,27 @@ BIOPointer X509View::toDER() const return bio; } +const X509Name X509View::getSubjectName() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + return X509Name(X509_get_subject_name(cert_)); +} + +const X509Name X509View::getIssuerName() const +{ + ClearErrorOnReturn clearErrorOnReturn; + if (cert_ == nullptr) return {}; + return X509Name(X509_get_issuer_name(cert_)); +} + BIOPointer X509View::getSubject() const { ClearErrorOnReturn clearErrorOnReturn; if (cert_ == nullptr) return {}; BIOPointer bio(BIO_new(BIO_s_mem())); if (!bio) return {}; - if (X509_NAME_print_ex(bio.get(), - X509_get_subject_name(cert_), - 0, + if (X509_NAME_print_ex(bio.get(), X509_get_subject_name(cert_), 0, kX509NameFlagsMultiline) <= 0) { return {}; @@ -996,8 +1060,8 @@ BIOPointer X509View::getIssuer() const if (cert_ == nullptr) return {}; BIOPointer bio(BIO_new(BIO_s_mem())); if (!bio) return {}; - if (X509_NAME_print_ex( - bio.get(), X509_get_issuer_name(cert_), 0, kX509NameFlagsMultiline) + if (X509_NAME_print_ex(bio.get(), X509_get_issuer_name(cert_), 0, + kX509NameFlagsMultiline) <= 0) { return {}; } @@ -1040,22 +1104,44 @@ BIOPointer X509View::getValidTo() const int64_t X509View::getValidToTime() const { - const ASN1_TIME* time = X509_get0_notAfter(cert_); - if (!time) return 0; - if (!ASN1_TIME_check(time)) return 0; - int day, sec; - if (!ASN1_TIME_diff(&day, &sec, nullptr, time)) return 0; - return (day * 24 * 60 * 60) + sec; +#ifdef OPENSSL_IS_BORINGSSL +#ifndef NCRYPTO_NO_ASN1_TIME + // Boringssl does not implement ASN1_TIME_to_tm in a public way, + // and only recently added ASN1_TIME_to_posix. Some boringssl + // users on older version may still need to patch around this + // or use a different implementation. + int64_t tp; + ASN1_TIME_to_posix(X509_get0_notAfter(cert_), &tp); + return tp; +#else + // Older versions of Boringssl do not implement the ASN1_TIME_to_* + // version functions. For now, neither shall we. + return 0LL; +#endif // NCRYPTO_NO_ASN1_TIME +#else // OPENSSL_IS_BORINGSSL + struct tm tp; + ASN1_TIME_to_tm(X509_get0_notAfter(cert_), &tp); + return PortableTimeGM(&tp); +#endif } int64_t X509View::getValidFromTime() const { - const ASN1_TIME* time = X509_get0_notBefore(cert_); - if (!time) return 0; - if (!ASN1_TIME_check(time)) return 0; - int day, sec; - if (!ASN1_TIME_diff(&day, &sec, nullptr, time)) return 0; - return (day * 24 * 60 * 60) + sec; +#ifdef OPENSSL_IS_BORINGSSL +#ifndef NCRYPTO_NO_ASN1_TIME + int64_t tp; + ASN1_TIME_to_posix(X509_get0_notBefore(cert_), &tp); + return tp; +#else + // Older versions of Boringssl do not implement the ASN1_TIME_to_* + // version functions. For now, neither shall we. + return 0LL; +#endif // NCRYPTO_NO_ASN1_TIME +#else + struct tm tp; + ASN1_TIME_to_tm(X509_get0_notBefore(cert_), &tp); + return PortableTimeGM(&tp); +#endif } DataPointer X509View::getSerialNumber() const @@ -1117,15 +1203,15 @@ bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const return X509_verify(const_cast(cert_), pkey.get()) == 1; } -X509View::CheckMatch X509View::checkHost(std::span host, +X509View::CheckMatch X509View::checkHost(const std::span host, int flags, DataPointer* peerName) const { ClearErrorOnReturn clearErrorOnReturn; if (cert_ == nullptr) return CheckMatch::NO_MATCH; char* peername; - switch (X509_check_host( - const_cast(cert_), host.data(), host.size(), flags, &peername)) { + switch (X509_check_host(const_cast(cert_), host.data(), host.size(), + flags, &peername)) { case 0: return CheckMatch::NO_MATCH; case 1: { @@ -1142,12 +1228,13 @@ X509View::CheckMatch X509View::checkHost(std::span host, } } -X509View::CheckMatch X509View::checkEmail(std::span email, int flags) const +X509View::CheckMatch X509View::checkEmail(const std::span email, + int flags) const { ClearErrorOnReturn clearErrorOnReturn; if (cert_ == nullptr) return CheckMatch::NO_MATCH; - switch (X509_check_email( - const_cast(cert_), email.data(), email.size(), flags)) { + switch (X509_check_email(const_cast(cert_), email.data(), email.size(), + flags)) { case 0: return CheckMatch::NO_MATCH; case 1: @@ -1159,12 +1246,11 @@ X509View::CheckMatch X509View::checkEmail(std::span email, int flags } } -X509View::CheckMatch X509View::checkIp(std::span ip, int flags) const +X509View::CheckMatch X509View::checkIp(const char* ip, int flags) const { ClearErrorOnReturn clearErrorOnReturn; if (cert_ == nullptr) return CheckMatch::NO_MATCH; - switch (X509_check_ip_asc( - const_cast(cert_), ip.data(), flags)) { + switch (X509_check_ip_asc(const_cast(cert_), ip, flags)) { case 0: return CheckMatch::NO_MATCH; case 1: @@ -1190,26 +1276,29 @@ X509View X509View::From(const SSLCtxPointer& ctx) return X509View(SSL_CTX_get0_certificate(ctx.get())); } -std::optional X509View::getFingerprint(const EVP_MD* method) const +std::optional X509View::getFingerprint( + const EVP_MD* method) const { - ClearErrorOnReturn clearErrorOnReturn; - if (cert_ == nullptr || method == nullptr) return std::nullopt; - - unsigned int len = 0; - unsigned char fingerprint[EVP_MAX_MD_SIZE]; - - if (X509_digest(cert_, method, fingerprint, &len) != 1) { - return std::nullopt; - } - - WTF::StringBuilder builder; + unsigned int md_size; + unsigned char md[EVP_MAX_MD_SIZE]; static constexpr char hex[] = "0123456789ABCDEF"; - for (unsigned int i = 0; i < len; i++) { - if (i > 0) builder.append(':'); - builder.append(hex[(fingerprint[i] & 0xf0) >> 4]); - builder.append(hex[fingerprint[i] & 0x0f]); + + if (X509_digest(get(), method, md, &md_size)) { + if (md_size == 0) return std::nullopt; + std::span fingerprint; + WTF::String fingerprintStr = WTF::String::createUninitialized((md_size * 3) - 1, fingerprint); + for (unsigned int i = 0; i < md_size; i++) { + auto idx = 3 * i; + fingerprint[idx] = hex[(md[i] & 0xf0) >> 4]; + fingerprint[idx + 1] = hex[(md[i] & 0x0f)]; + if (i == md_size - 1) break; + fingerprint[idx + 2] = ':'; + } + + return fingerprintStr; } - return builder.toString(); + + return std::nullopt; } X509Pointer X509View::clone() const @@ -1237,6 +1326,63 @@ Result X509Pointer::Parse( return Result(ERR_get_error()); } +bool X509View::enumUsages(UsageCallback callback) const +{ + if (cert_ == nullptr) return false; + StackOfASN1 eku(static_cast( + X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr))); + if (!eku) return false; + const int count = sk_ASN1_OBJECT_num(eku.get()); + char buf[256] {}; + + for (int i = 0; i < count; i++) { + if (OBJ_obj2txt(buf, sizeof(buf), sk_ASN1_OBJECT_value(eku.get(), i), 1) >= 0) { + callback(buf); + } + } + return true; +} + +bool X509View::ifRsa(KeyCallback callback) const +{ + if (cert_ == nullptr) return true; + // The const_cast is a bit unfortunate. The X509_get_pubkey API accepts + // a const X509* in newer versions of openssl and boringssl but a non-const + // X509* in older versions. By removing the const if it exists we can + // support both. + EVPKeyPointer pkey(X509_get_pubkey(const_cast(cert_))); + if (!pkey) [[unlikely]] + return true; + auto id = pkey.id(); + if (id == EVP_PKEY_RSA || id == EVP_PKEY_RSA2 || id == EVP_PKEY_RSA_PSS) { + Rsa rsa = pkey; + if (!rsa) [[unlikely]] + return true; + return callback(rsa); + } + return true; +} + +bool X509View::ifEc(KeyCallback callback) const +{ + if (cert_ == nullptr) return true; + // The const_cast is a bit unfortunate. The X509_get_pubkey API accepts + // a const X509* in newer versions of openssl and boringssl but a non-const + // X509* in older versions. By removing the const if it exists we can + // support both. + EVPKeyPointer pkey(X509_get_pubkey(const_cast(cert_))); + if (!pkey) [[unlikely]] + return true; + auto id = pkey.id(); + if (id == EVP_PKEY_EC) { + Ec ec = pkey; + if (!ec) [[unlikely]] + return true; + return callback(ec); + } + return true; +} + X509Pointer X509Pointer::IssuerFrom(const SSLPointer& ssl, const X509View& view) { @@ -1264,49 +1410,50 @@ X509Pointer X509Pointer::PeerFrom(const SSLPointer& ssl) // When adding or removing errors below, please also update the list in the API // documentation. See the "OpenSSL Error Codes" section of doc/api/errors.md // Also *please* update the respective section in doc/api/tls.md as well -ASCIILiteral X509Pointer::ErrorCode(int32_t err) -{ +WTF::ASCIILiteral X509Pointer::ErrorCode(int32_t err) +{ // NOLINT(runtime/int) +#define CASE(CODE) \ + case X509_V_ERR_##CODE: \ + return #CODE##_s; switch (err) { -#define V(name, msg) \ - case X509_V_ERR_##name: \ - return msg##_s; - V(UNABLE_TO_GET_ISSUER_CERT, "unable to get issuer certificate") - V(UNABLE_TO_GET_CRL, "unable to get certificate CRL") - V(UNABLE_TO_DECRYPT_CERT_SIGNATURE, "unable to decrypt certificate's signature") - V(UNABLE_TO_DECRYPT_CRL_SIGNATURE, "unable to decrypt CRL's signature") - V(UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY, "unable to decode issuer public key") - V(CERT_SIGNATURE_FAILURE, "certificate signature failure") - V(CRL_SIGNATURE_FAILURE, "CRL signature failure") - V(CERT_NOT_YET_VALID, "certificate is not yet valid") - V(CERT_HAS_EXPIRED, "certificate has expired") - V(CRL_NOT_YET_VALID, "CRL is not yet valid") - V(CRL_HAS_EXPIRED, "CRL has expired") - V(ERROR_IN_CERT_NOT_BEFORE_FIELD, "format error in certificate's notBefore field") - V(ERROR_IN_CERT_NOT_AFTER_FIELD, "format error in certificate's notAfter field") - V(ERROR_IN_CRL_LAST_UPDATE_FIELD, "format error in CRL's lastUpdate field") - V(ERROR_IN_CRL_NEXT_UPDATE_FIELD, "format error in CRL's nextUpdate field") - V(OUT_OF_MEM, "out of memory") - V(DEPTH_ZERO_SELF_SIGNED_CERT, "self signed certificate") - V(SELF_SIGNED_CERT_IN_CHAIN, "self signed certificate in certificate chain") - V(UNABLE_TO_GET_ISSUER_CERT_LOCALLY, "unable to get local issuer certificate") - V(UNABLE_TO_VERIFY_LEAF_SIGNATURE, "unable to verify the first certificate") - V(CERT_CHAIN_TOO_LONG, "certificate chain too long") - V(CERT_REVOKED, "certificate revoked") - V(INVALID_CA, "invalid CA certificate") - V(PATH_LENGTH_EXCEEDED, "path length constraint exceeded") - V(INVALID_PURPOSE, "unsupported certificate purpose") - V(CERT_UNTRUSTED, "certificate not trusted") - V(CERT_REJECTED, "certificate rejected") - V(HOSTNAME_MISMATCH, "Hostname mismatch") - V(EMAIL_MISMATCH, "Email address mismatch") - V(IP_ADDRESS_MISMATCH, "IP address mismatch") -#undef V + CASE(UNABLE_TO_GET_ISSUER_CERT) + CASE(UNABLE_TO_GET_CRL) + CASE(UNABLE_TO_DECRYPT_CERT_SIGNATURE) + CASE(UNABLE_TO_DECRYPT_CRL_SIGNATURE) + CASE(UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY) + CASE(CERT_SIGNATURE_FAILURE) + CASE(CRL_SIGNATURE_FAILURE) + CASE(CERT_NOT_YET_VALID) + CASE(CERT_HAS_EXPIRED) + CASE(CRL_NOT_YET_VALID) + CASE(CRL_HAS_EXPIRED) + CASE(ERROR_IN_CERT_NOT_BEFORE_FIELD) + CASE(ERROR_IN_CERT_NOT_AFTER_FIELD) + CASE(ERROR_IN_CRL_LAST_UPDATE_FIELD) + CASE(ERROR_IN_CRL_NEXT_UPDATE_FIELD) + CASE(OUT_OF_MEM) + CASE(DEPTH_ZERO_SELF_SIGNED_CERT) + CASE(SELF_SIGNED_CERT_IN_CHAIN) + CASE(UNABLE_TO_GET_ISSUER_CERT_LOCALLY) + CASE(UNABLE_TO_VERIFY_LEAF_SIGNATURE) + CASE(CERT_CHAIN_TOO_LONG) + CASE(CERT_REVOKED) + CASE(INVALID_CA) + CASE(PATH_LENGTH_EXCEEDED) + CASE(INVALID_PURPOSE) + CASE(CERT_UNTRUSTED) + CASE(CERT_REJECTED) + CASE(HOSTNAME_MISMATCH) } - return ""_s; +#undef CASE + return "UNSPECIFIED"_s; } -std::optional X509Pointer::ErrorReason(int32_t err) +std::optional X509Pointer::ErrorReason(int32_t err) { + if (err == X509_V_OK) return std::nullopt; + + // TODO(dylan-conway): delete this switch? switch (err) { #define V(name, msg) \ case X509_V_ERR_##name: \ @@ -1316,7 +1463,8 @@ std::optional X509Pointer::ErrorReason(int32_t err) V(IP_ADDRESS_MISMATCH, "IP address does not match certificate") #undef V } - return std::nullopt; + return WTF::ASCIILiteral::fromLiteralUnsafe( + X509_verify_cert_error_string(err)); } // ============================================================================ @@ -1339,20 +1487,11 @@ BIOPointer& BIOPointer::operator=(BIOPointer&& other) noexcept return *new (this) BIOPointer(std::move(other)); } -BIOPointer::~BIOPointer() -{ - reset(); -} +BIOPointer::~BIOPointer() { reset(); } -void BIOPointer::reset(BIO* bio) -{ - bio_.reset(bio); -} +void BIOPointer::reset(BIO* bio) { bio_.reset(bio); } -BIO* BIOPointer::release() -{ - return bio_.release(); -} +BIO* BIOPointer::release() { return bio_.release(); } bool BIOPointer::resetBio() const { @@ -1360,17 +1499,15 @@ bool BIOPointer::resetBio() const return BIO_reset(bio_.get()) == 1; } -BIOPointer BIOPointer::NewMem() -{ - return BIOPointer(BIO_new(BIO_s_mem())); -} +BIOPointer BIOPointer::NewMem() { return BIOPointer(BIO_new(BIO_s_mem())); } BIOPointer BIOPointer::NewSecMem() { -#ifndef OPENSSL_IS_BORINGSSL - return BIOPointer(BIO_new(BIO_s_secmem())); -#else +#ifdef OPENSSL_IS_BORINGSSL + // Boringssl does not implement the BIO_s_secmem API. return BIOPointer(BIO_new(BIO_s_mem())); +#else + return BIOPointer(BIO_new(BIO_s_secmem())); #endif } @@ -1384,12 +1521,11 @@ BIOPointer BIOPointer::New(const void* data, size_t len) return BIOPointer(BIO_new_mem_buf(data, len)); } -BIOPointer BIOPointer::NewFile(WTF::StringView filename, - WTF::StringView mode) +BIOPointer BIOPointer::NewFile(WTF::StringView filename, WTF::StringView mode) { - auto utf8 = filename.utf8(); + auto filenameUtf8 = filename.utf8(); auto modeUtf8 = mode.utf8(); - return BIOPointer(BIO_new_file(utf8.data(), modeUtf8.data())); + return BIOPointer(BIO_new_file(filenameUtf8.data(), modeUtf8.data())); } BIOPointer BIOPointer::NewFp(FILE* fd, int close_flag) @@ -1404,21 +1540,23 @@ BIOPointer BIOPointer::New(const BIGNUM* bn) return res; } -int BIOPointer::Write(BIOPointer* bio, std::span message) -{ - if (bio == nullptr || !*bio) return 0; - return BIO_write(bio->get(), message.data(), message.size()); -} - int BIOPointer::Write(BIOPointer* bio, WTF::StringView message) { - auto utf8 = message.utf8(); - return Write(bio, utf8.span()); + auto messageUtf8 = message.utf8(); + return Write(bio, messageUtf8.span()); } // ============================================================================ // DHPointer +namespace { +bool EqualNoCase(const WTF::StringView a, const WTF::StringView b) +{ + if (a.length() != b.length()) return false; + return a.startsWithIgnoringASCIICase(b); +} +} // namespace + DHPointer::DHPointer(DH* dh) : dh_(dh) { @@ -1436,28 +1574,20 @@ DHPointer& DHPointer::operator=(DHPointer&& other) noexcept return *new (this) DHPointer(std::move(other)); } -DHPointer::~DHPointer() -{ - reset(); -} +DHPointer::~DHPointer() { reset(); } -void DHPointer::reset(DH* dh) -{ - dh_.reset(dh); -} +void DHPointer::reset(DH* dh) { dh_.reset(dh); } -DH* DHPointer::release() -{ - return dh_.release(); -} +DH* DHPointer::release() { return dh_.release(); } -BignumPointer DHPointer::FindGroup(WTF::StringView name, +BignumPointer DHPointer::FindGroup(const WTF::StringView name, FindGroupOption option) { #define V(n, p) \ if (EqualNoCase(name, n)) return BignumPointer(p(nullptr)); if (option != FindGroupOption::NO_SMALL_PRIMES) { #ifndef OPENSSL_IS_BORINGSSL + // Boringssl does not support the 768 and 1024 small primes V("modp1"_s, BN_get_rfc2409_prime_768); V("modp2"_s, BN_get_rfc2409_prime_1024); #endif @@ -1480,7 +1610,7 @@ BignumPointer DHPointer::GetStandardGenerator() return bn; } -DHPointer DHPointer::FromGroup(WTF::StringView name, +DHPointer DHPointer::FromGroup(const WTF::StringView name, FindGroupOption option) { auto group = FindGroup(name, option); @@ -1489,7 +1619,7 @@ DHPointer DHPointer::FromGroup(WTF::StringView name, auto generator = GetStandardGenerator(); if (!generator) return {}; // Unable to create the generator. - return New(std::move(group), std::move(generator)); + return New(WTFMove(group), WTFMove(generator)); } DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) @@ -1502,8 +1632,11 @@ DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) if (DH_set0_pqg(dh.get(), p.get(), nullptr, g.get()) != 1) return {}; // If the call above is successful, the DH object takes ownership of the - // BIGNUMs, so we must release them here. + // BIGNUMs, so we must release them here. Unfortunately coverity does not + // know that so we need to tell it not to complain. + // coverity[resource_leak] p.release(); + // coverity[resource_leak] g.release(); return dh; @@ -1535,14 +1668,18 @@ DHPointer::CheckPublicKeyResult DHPointer::checkPublicKey( const BignumPointer& pub_key) { ClearErrorOnReturn clearErrorOnReturn; - if (!pub_key || !dh_) return DHPointer::CheckPublicKeyResult::CHECK_FAILED; - int codes = 0; - if (DH_check_pub_key(dh_.get(), pub_key.get(), &codes) != 1) + if (!pub_key || !dh_) { return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + } + int codes = 0; + if (DH_check_pub_key(dh_.get(), pub_key.get(), &codes) != 1) { + return DHPointer::CheckPublicKeyResult::CHECK_FAILED; + } #ifndef OPENSSL_IS_BORINGSSL + // Boringssl does not define DH_CHECK_PUBKEY_TOO_SMALL or TOO_LARGE if (codes & DH_CHECK_PUBKEY_TOO_SMALL) { return DHPointer::CheckPublicKeyResult::TOO_SMALL; - } else if (codes & DH_CHECK_PUBKEY_TOO_SMALL) { + } else if (codes & DH_CHECK_PUBKEY_TOO_LARGE) { return DHPointer::CheckPublicKeyResult::TOO_LARGE; } #endif @@ -1598,7 +1735,10 @@ DataPointer DHPointer::generateKeys() const size_t DHPointer::size() const { if (!dh_) return 0; - return DH_size(dh_.get()); + int ret = DH_size(dh_.get()); + // DH_size can return a -1 on error but we just want to return a 0 + // in that case so we don't wrap around when returning the size_t. + return ret >= 0 ? static_cast(ret) : 0; } DataPointer DHPointer::computeSecret(const BignumPointer& peer) const @@ -1628,6 +1768,10 @@ bool DHPointer::setPublicKey(BignumPointer&& key) { if (!dh_) return false; if (DH_set0_key(dh_.get(), key.get(), nullptr) == 1) { + // If DH_set0_key returns successfully, then dh_ takes ownership of the + // BIGNUM, so we must release it here. Unfortunately coverity does not + // know that so we need to tell it not to complain. + // coverity[resource_leak] key.release(); return true; } @@ -1638,6 +1782,10 @@ bool DHPointer::setPrivateKey(BignumPointer&& key) { if (!dh_) return false; if (DH_set0_key(dh_.get(), nullptr, key.get()) == 1) { + // If DH_set0_key returns successfully, then dh_ takes ownership of the + // BIGNUM, so we must release it here. Unfortunately coverity does not + // know that so we need to tell it not to complain. + // coverity[resource_leak] key.release(); return true; } @@ -1650,7 +1798,7 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, size_t out_size; if (!ourKey || !theirKey) return {}; - EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(ourKey.get(), nullptr)); + auto ctx = EVPKeyCtxPointer::New(ourKey); if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || EVP_PKEY_derive_set_peer(ctx.get(), theirKey.get()) <= 0 || EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) { return {}; } @@ -1658,8 +1806,8 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, if (out_size == 0) return {}; auto out = DataPointer::Alloc(out_size); - if (EVP_PKEY_derive( - ctx.get(), reinterpret_cast(out.get()), &out_size) + if (EVP_PKEY_derive(ctx.get(), reinterpret_cast(out.get()), + &out_size) <= 0) { return {}; } @@ -1677,9 +1825,26 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, // ============================================================================ // KDF -const EVP_MD* getDigestByName(const std::string_view name) +const EVP_MD* getDigestByName(const WTF::StringView name) { - return EVP_get_digestbyname(name.data()); + // Historically, "dss1" and "DSS1" were DSA aliases for SHA-1 + // exposed through the public API. + if (name == "dss1"_s || name == "DSS1"_s) [[unlikely]] { + return EVP_sha1(); + } + + // if (name == "ripemd160WithRSA"_s || name == "RSA-RIPEMD160"_s) { + // return EVP_ripemd160(); + // } + + auto nameUtf8 = name.utf8(); + return EVP_get_digestbyname(nameUtf8.data()); +} + +const EVP_CIPHER* getCipherByName(const WTF::StringView name) +{ + auto nameUtf8 = name.utf8(); + return EVP_get_cipherbyname(nameUtf8.data()); } bool checkHkdfLength(const EVP_MD* md, size_t length) @@ -1693,21 +1858,15 @@ bool checkHkdfLength(const EVP_MD* md, size_t length) return true; } -DataPointer hkdf(const EVP_MD* md, - const Buffer& key, +bool hkdfInfo(const EVP_MD* md, const Buffer& key, const Buffer& info, - const Buffer& salt, - size_t length) + const Buffer& salt, size_t length, + Buffer* out) { ClearErrorOnReturn clearErrorOnReturn; if (!checkHkdfLength(md, length) || info.len > INT_MAX || salt.len > INT_MAX) { - return {}; - } - - EVPKeyCtxPointer ctx = EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr)); - if (!ctx || !EVP_PKEY_derive_init(ctx.get()) || !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) || !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) { - return {}; + return false; } std::string_view actual_salt; @@ -1718,39 +1877,49 @@ DataPointer hkdf(const EVP_MD* md, actual_salt = { default_salt, static_cast(EVP_MD_size(md)) }; } +#ifndef NCRYPTO_NO_KDF_H + auto ctx = EVPKeyCtxPointer::NewFromID(EVP_PKEY_HKDF); + if (!ctx || !EVP_PKEY_derive_init(ctx.get()) || !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) || !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) { + return false; + } + // We do not use EVP_PKEY_HKDF_MODE_EXTRACT_AND_EXPAND because and instead // implement the extraction step ourselves because EVP_PKEY_derive does not // handle zero-length keys, which are required for Web Crypto. // TODO(jasnell): Once OpenSSL 1.1.1 support is dropped completely, and once - // BoringSSL is confirmed to support it, wen can hopefully drop this and use + // BoringSSL is confirmed to support it, we can hopefully drop this and use // EVP_KDF directly which does support zero length keys. unsigned char pseudorandom_key[EVP_MAX_MD_SIZE]; unsigned pseudorandom_key_len = sizeof(pseudorandom_key); - if (HMAC(md, - actual_salt.data(), - actual_salt.size(), - key.data, - key.len, - pseudorandom_key, - &pseudorandom_key_len) + if (HMAC(md, actual_salt.data(), actual_salt.size(), key.data, key.len, + pseudorandom_key, &pseudorandom_key_len) == nullptr) { - return {}; + return false; } if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) || !EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), pseudorandom_key, pseudorandom_key_len)) { - return {}; + return false; } + if (out == nullptr || out->len != length) return false; + + return EVP_PKEY_derive(ctx.get(), out->data, &length) > 0; +#else + return HKDF(out->data, length, md, key.data, key.len, salt.data, salt.len, + info.data, info.len); +#endif +} + +DataPointer hkdf(const EVP_MD* md, const Buffer& key, + const Buffer& info, + const Buffer& salt, size_t length) +{ auto buf = DataPointer::Alloc(length); if (!buf) return {}; + Buffer out = buf; - if (EVP_PKEY_derive( - ctx.get(), static_cast(buf.get()), &length) - <= 0) { - return {}; - } - - return buf; + return hkdfInfo(md, key, info, salt, length, &out) ? std::move(buf) + : DataPointer(); } bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem) @@ -1758,43 +1927,66 @@ bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem) return EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem, nullptr, 0) == 1; } -DataPointer scrypt(const Buffer& pass, - const Buffer& salt, - uint64_t N, - uint64_t r, - uint64_t p, - uint64_t maxmem, - size_t length) +bool scryptInto(const Buffer& pass, + const Buffer& salt, uint64_t N, uint64_t r, + uint64_t p, uint64_t maxmem, size_t length, + Buffer* out) { ClearErrorOnReturn clearErrorOnReturn; - if (pass.len > INT_MAX || salt.len > INT_MAX) { - return {}; + if (pass.len > INT_MAX || salt.len > INT_MAX || out == nullptr) { + return false; } - auto dp = DataPointer::Alloc(length); - if (dp && EVP_PBE_scrypt(pass.data, pass.len, salt.data, salt.len, N, r, p, maxmem, reinterpret_cast(dp.get()), length)) { - return dp; + if (auto dp = DataPointer::Alloc(length)) { + return EVP_PBE_scrypt(pass.data, pass.len, salt.data, salt.len, N, r, p, + maxmem, out->data, length); + } + + return false; +} + +DataPointer scrypt(const Buffer& pass, + const Buffer& salt, uint64_t N, + uint64_t r, uint64_t p, uint64_t maxmem, size_t length) +{ + if (auto dp = DataPointer::Alloc(length)) { + Buffer buf = dp; + if (scryptInto(pass, salt, N, r, p, maxmem, length, &buf)) { + return dp; + } } return {}; } -DataPointer pbkdf2(const EVP_MD* md, - const Buffer& pass, - const Buffer& salt, - uint32_t iterations, - size_t length) +bool pbkdf2Into(const EVP_MD* md, const Buffer& pass, + const Buffer& salt, uint32_t iterations, + size_t length, Buffer* out) { ClearErrorOnReturn clearErrorOnReturn; - if (pass.len > INT_MAX || salt.len > INT_MAX || length > INT_MAX) { - return {}; + if (pass.len > INT_MAX || salt.len > INT_MAX || length > INT_MAX || out == nullptr) { + return false; } - auto dp = DataPointer::Alloc(length); - if (dp && PKCS5_PBKDF2_HMAC(pass.data, pass.len, salt.data, salt.len, iterations, md, length, reinterpret_cast(dp.get()))) { - return dp; + if (PKCS5_PBKDF2_HMAC(pass.data, pass.len, salt.data, salt.len, iterations, + md, length, out->data)) { + return true; + } + + return false; +} + +DataPointer pbkdf2(const EVP_MD* md, const Buffer& pass, + const Buffer& salt, uint32_t iterations, + size_t length) +{ + if (auto dp = DataPointer::Alloc(length)) { + Buffer buf = dp; + if (pbkdf2Into(md, pass, salt, iterations, length, &buf)) { + return dp; + } } return {}; @@ -1804,8 +1996,8 @@ DataPointer pbkdf2(const EVP_MD* md, EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig( const PrivateKeyEncodingConfig& other) - : PrivateKeyEncodingConfig( - other.output_key_object, other.format, other.type) + : PrivateKeyEncodingConfig(other.output_key_object, other.format, + other.type) { cipher = other.cipher; if (other.passphrase.has_value()) { @@ -1833,10 +2025,7 @@ EVPKeyPointer::PrivateKeyEncodingConfig::operator=( return *new (this) PrivateKeyEncodingConfig(other); } -EVPKeyPointer EVPKeyPointer::New() -{ - return EVPKeyPointer(EVP_PKEY_new()); -} +EVPKeyPointer EVPKeyPointer::New() { return EVPKeyPointer(EVP_PKEY_new()); } EVPKeyPointer EVPKeyPointer::NewRawPublic( int id, const Buffer& data) @@ -1854,6 +2043,34 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len)); } +EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) +{ +#ifndef NCRYPTO_NO_EVP_DH + if (!dh) return {}; + auto key = New(); + if (!key) return {}; + if (EVP_PKEY_assign_DH(key.get(), dh.get())) { + dh.release(); + } + return key; +#else + // Older versions of openssl/boringssl do not implement the EVP_PKEY_*_DH + // APIs + return {}; +#endif +} + +EVPKeyPointer EVPKeyPointer::NewRSA(RSAPointer&& rsa) +{ + if (!rsa) return {}; + auto key = New(); + if (!key) return {}; + if (EVP_PKEY_assign_RSA(key.get(), rsa.get())) { + rsa.release(); + } + return key; +} + EVPKeyPointer::EVPKeyPointer(EVP_PKEY* pkey) : pkey_(pkey) { @@ -1864,6 +2081,13 @@ EVPKeyPointer::EVPKeyPointer(EVPKeyPointer&& other) noexcept { } +EVPKeyPointer EVPKeyPointer::clone() const +{ + if (!pkey_) return {}; + if (!EVP_PKEY_up_ref(pkey_.get())) return {}; + return EVPKeyPointer(pkey_.get()); +} + EVPKeyPointer& EVPKeyPointer::operator=(EVPKeyPointer&& other) noexcept { if (this == &other) return *this; @@ -1871,20 +2095,11 @@ EVPKeyPointer& EVPKeyPointer::operator=(EVPKeyPointer&& other) noexcept return *new (this) EVPKeyPointer(std::move(other)); } -EVPKeyPointer::~EVPKeyPointer() -{ - reset(); -} +EVPKeyPointer::~EVPKeyPointer() { reset(); } -void EVPKeyPointer::reset(EVP_PKEY* pkey) -{ - pkey_.reset(pkey); -} +void EVPKeyPointer::reset(EVP_PKEY* pkey) { pkey_.reset(pkey); } -EVP_PKEY* EVPKeyPointer::release() -{ - return pkey_.release(); -} +EVP_PKEY* EVPKeyPointer::release() { return pkey_.release(); } int EVPKeyPointer::id(const EVP_PKEY* key) { @@ -1898,15 +2113,9 @@ int EVPKeyPointer::base_id(const EVP_PKEY* key) return EVP_PKEY_base_id(key); } -int EVPKeyPointer::id() const -{ - return id(get()); -} +int EVPKeyPointer::id() const { return id(get()); } -int EVPKeyPointer::base_id() const -{ - return base_id(get()); -} +int EVPKeyPointer::base_id() const { return base_id(get()); } int EVPKeyPointer::bits() const { @@ -1923,7 +2132,7 @@ size_t EVPKeyPointer::size() const EVPKeyCtxPointer EVPKeyPointer::newCtx() const { if (!pkey_) return {}; - return EVPKeyCtxPointer(EVP_PKEY_CTX_new(get(), nullptr)); + return EVPKeyCtxPointer::New(*this); } size_t EVPKeyPointer::rawPublicKeySize() const @@ -2007,8 +2216,8 @@ EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp, // This skips surrounding data and decodes PEM to DER. { MarkPopErrorOnReturn mark_pop_error_on_return; - if (PEM_bytes_read_bio( - &der_data, &der_len, nullptr, name, bp.get(), nullptr, nullptr) + if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name, bp.get(), + nullptr, nullptr) != 1) return EVPKeyPointer::ParseKeyResult( EVPKeyPointer::PKParseError::NOT_RECOGNIZED); @@ -2023,10 +2232,8 @@ EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp, return EVPKeyPointer::ParseKeyResult(std::move(pkey)); } -constexpr bool IsASN1Sequence(const unsigned char* data, - size_t size, - size_t* data_offset, - size_t* data_size) +constexpr bool IsASN1Sequence(const unsigned char* data, size_t size, + size_t* data_offset, size_t* data_size) { if (size < 2 || data[0] != 0x30) return false; @@ -2085,8 +2292,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM( // Try parsing as SubjectPublicKeyInfo (SPKI) first. if (auto ret = TryParsePublicKeyInner( - bp, - "PUBLIC KEY", + bp, "PUBLIC KEY", [](const unsigned char** p, long l) { // NOLINT(runtime/int) return d2i_PUBKEY(nullptr, p, l); })) { @@ -2095,8 +2301,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM( // Maybe it is PKCS#1. if (auto ret = TryParsePublicKeyInner( - bp, - "RSA PUBLIC KEY", + bp, "RSA PUBLIC KEY", [](const unsigned char** p, long l) { // NOLINT(runtime/int) return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); })) { @@ -2105,8 +2310,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM( // X.509 fallback. if (auto ret = TryParsePublicKeyInner( - bp, - "CERTIFICATE", + bp, "CERTIFICATE", [](const unsigned char** p, long l) { // NOLINT(runtime/int) X509Pointer x509(d2i_X509(nullptr, p, l)); return x509 ? X509_get_pubkey(x509.get()) : nullptr; @@ -2179,6 +2383,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ && !had_passphrase) { return ParseKeyResult(PKParseError::NEED_PASSPHRASE); } + return ParseKeyResult(PKParseError::FAILED, err); } if (!pkey) return ParseKeyResult(PKParseError::FAILED); @@ -2192,9 +2397,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( if (config.format == PKFormatType::PEM) { auto key = PEM_read_bio_PrivateKey( - bio.get(), - nullptr, - PasswordCallback, + bio.get(), nullptr, PasswordCallback, config.passphrase.has_value() ? &passphrase : nullptr); return keyOrError(EVPKeyPointer(key), config.passphrase.has_value()); } @@ -2211,9 +2414,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( case PKEncodingType::PKCS8: { if (IsEncryptedPrivateKeyInfo(buffer)) { auto key = d2i_PKCS8PrivateKey_bio( - bio.get(), - nullptr, - PasswordCallback, + bio.get(), nullptr, PasswordCallback, config.passphrase.has_value() ? &passphrase : nullptr); return keyOrError(EVPKeyPointer(key), config.passphrase.has_value()); } @@ -2255,21 +2456,14 @@ Result EVPKeyPointer::writePrivateKey( // PKCS1 is only permitted for RSA keys. if (id() != EVP_PKEY_RSA) return Result(false); -#if OPENSSL_VERSION_MAJOR >= 3 - const RSA* rsa = EVP_PKEY_get0_RSA(get()); -#else - RSA* rsa = EVP_PKEY_get0_RSA(get()); -#endif + OSSL3_CONST RSA* rsa = EVP_PKEY_get0_RSA(get()); + switch (config.format) { case PKFormatType::PEM: { err = PEM_write_bio_RSAPrivateKey( - bio.get(), - rsa, - config.cipher, + bio.get(), rsa, config.cipher, reinterpret_cast(passphrase.data), - passphrase.len, - nullptr, - nullptr) + passphrase.len, nullptr, nullptr) != 1; break; } @@ -2289,24 +2483,16 @@ Result EVPKeyPointer::writePrivateKey( switch (config.format) { case PKFormatType::PEM: { // Encode PKCS#8 as PEM. - err = PEM_write_bio_PKCS8PrivateKey(bio.get(), - get(), - config.cipher, - passphrase.data, - passphrase.len, - nullptr, - nullptr) + err = PEM_write_bio_PKCS8PrivateKey(bio.get(), get(), config.cipher, + passphrase.data, passphrase.len, + nullptr, nullptr) != 1; break; } case PKFormatType::DER: { - err = i2d_PKCS8PrivateKey_bio(bio.get(), - get(), - config.cipher, - passphrase.data, - passphrase.len, - nullptr, - nullptr) + err = i2d_PKCS8PrivateKey_bio(bio.get(), get(), config.cipher, + passphrase.data, passphrase.len, + nullptr, nullptr) != 1; break; } @@ -2321,21 +2507,14 @@ Result EVPKeyPointer::writePrivateKey( // SEC1 is only permitted for EC keys if (id() != EVP_PKEY_EC) return Result(false); -#if OPENSSL_VERSION_MAJOR >= 3 - const EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get()); -#else - EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get()); -#endif + OSSL3_CONST EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get()); + switch (config.format) { case PKFormatType::PEM: { err = PEM_write_bio_ECPrivateKey( - bio.get(), - ec, - config.cipher, + bio.get(), ec, config.cipher, reinterpret_cast(passphrase.data), - passphrase.len, - nullptr, - nullptr) + passphrase.len, nullptr, nullptr) != 1; break; } @@ -2415,6 +2594,109 @@ Result EVPKeyPointer::writePublicKey( return bio; } +bool EVPKeyPointer::isRsaVariant() const +{ + if (!pkey_) return false; + int type = id(); + return type == EVP_PKEY_RSA || type == EVP_PKEY_RSA2 || type == EVP_PKEY_RSA_PSS; +} + +bool EVPKeyPointer::isOneShotVariant() const +{ + if (!pkey_) return false; + int type = id(); + return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448; +} + +bool EVPKeyPointer::isSigVariant() const +{ + if (!pkey_) return false; + int type = id(); + return type == EVP_PKEY_EC || type == EVP_PKEY_DSA; +} + +int EVPKeyPointer::getDefaultSignPadding() const +{ + return id() == EVP_PKEY_RSA_PSS ? RSA_PKCS1_PSS_PADDING : RSA_PKCS1_PADDING; +} + +std::optional EVPKeyPointer::getBytesOfRS() const +{ + if (!pkey_) return std::nullopt; + int bits, id = base_id(); + + if (id == EVP_PKEY_DSA) { + const DSA* dsa_key = EVP_PKEY_get0_DSA(get()); + // Both r and s are computed mod q, so their width is limited by that of q. + bits = BignumPointer::GetBitCount(DSA_get0_q(dsa_key)); + } else if (id == EVP_PKEY_EC) { + bits = EC_GROUP_order_bits(ECKeyPointer::GetGroup(*this)); + } else { + return std::nullopt; + } + + return (bits + 7) / 8; +} + +EVPKeyPointer::operator Rsa() const +{ + int type = id(); + if (type != EVP_PKEY_RSA && type != EVP_PKEY_RSA_PSS) return {}; + + // TODO(tniessen): Remove the "else" branch once we drop support for OpenSSL + // versions older than 1.1.1e via FIPS / dynamic linking. + OSSL3_CONST RSA* rsa; + if (OPENSSL_VERSION_NUMBER >= 0x1010105fL) { + rsa = EVP_PKEY_get0_RSA(get()); + } else { + rsa = static_cast(EVP_PKEY_get0(get())); + } + if (rsa == nullptr) return {}; + return Rsa(rsa); +} + +EVPKeyPointer::operator Dsa() const +{ + int type = id(); + if (type != EVP_PKEY_DSA) return {}; + + OSSL3_CONST DSA* dsa = EVP_PKEY_get0_DSA(get()); + if (dsa == nullptr) return {}; + return Dsa(dsa); +} + +EVPKeyPointer::operator Ec() const +{ + int type = id(); + if (type != EVP_PKEY_EC) return {}; + + OSSL3_CONST EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get()); + if (ec == nullptr) return {}; + return Ec(ec); +} + +bool EVPKeyPointer::validateDsaParameters() const +{ + if (!pkey_) return false; + /* Validate DSA2 parameters from FIPS 186-4 */ +#if OPENSSL_VERSION_MAJOR >= 3 + if (EVP_default_properties_is_fips_enabled(nullptr) && EVP_PKEY_DSA == id()) { +#else + if (FIPS_mode() && EVP_PKEY_DSA == id()) { +#endif + const DSA* dsa = EVP_PKEY_get0_DSA(pkey_.get()); + const BIGNUM* p; + const BIGNUM* q; + DSA_get0_pqg(dsa, &p, &q, nullptr); + int L = BignumPointer::GetBitCount(p); + int N = BignumPointer::GetBitCount(q); + + return (L == 1024 && N == 160) || (L == 2048 && N == 224) || (L == 2048 && N == 256) || (L == 3072 && N == 256); + } + + return true; +} + // ============================================================================ SSLPointer::SSLPointer(SSL* ssl) @@ -2434,20 +2716,11 @@ SSLPointer& SSLPointer::operator=(SSLPointer&& other) noexcept return *new (this) SSLPointer(std::move(other)); } -SSLPointer::~SSLPointer() -{ - reset(); -} +SSLPointer::~SSLPointer() { reset(); } -void SSLPointer::reset(SSL* ssl) -{ - ssl_.reset(ssl); -} +void SSLPointer::reset(SSL* ssl) { ssl_.reset(ssl); } -SSL* SSLPointer::release() -{ - return ssl_.release(); -} +SSL* SSLPointer::release() { return ssl_.release(); } SSLPointer SSLPointer::New(const SSLCtxPointer& ctx) { @@ -2455,7 +2728,8 @@ SSLPointer SSLPointer::New(const SSLCtxPointer& ctx) return SSLPointer(SSL_new(ctx.get())); } -void SSLPointer::getCiphers(WTF::Function&& cb) const +void SSLPointer::getCiphers( + WTF::Function&& cb) const { if (!ssl_) return; STACK_OF(SSL_CIPHER)* ciphers = SSL_get_ciphers(get()); @@ -2464,11 +2738,9 @@ void SSLPointer::getCiphers(WTF::Function&& cb) const // document them, but since there are only 5, easier to just add them manually // and not have to explain their absence in the API docs. They are lower-cased // because the docs say they will be. - static constexpr ASCIILiteral TLS13_CIPHERS[] = { - "tls_aes_256_gcm_sha384"_s, - "tls_chacha20_poly1305_sha256"_s, - "tls_aes_128_gcm_sha256"_s, - "tls_aes_128_ccm_8_sha256"_s, + static constexpr WTF::ASCIILiteral TLS13_CIPHERS[] = { + "tls_aes_256_gcm_sha384"_s, "tls_chacha20_poly1305_sha256"_s, + "tls_aes_128_gcm_sha256"_s, "tls_aes_128_ccm_8_sha256"_s, "tls_aes_128_ccm_sha256"_s }; @@ -2476,14 +2748,11 @@ void SSLPointer::getCiphers(WTF::Function&& cb) const for (int i = 0; i < n; ++i) { const SSL_CIPHER* cipher = sk_SSL_CIPHER_value(ciphers, i); - const char* name = SSL_CIPHER_get_name(cipher); - WTF::String str = WTF::String::fromUTF8(name); - cb(str); + cb(WTF::ASCIILiteral::fromLiteralUnsafe(SSL_CIPHER_get_name(cipher))); } for (unsigned i = 0; i < 5; ++i) { - WTF::String str = WTF::String(TLS13_CIPHERS[i]); - cb(str); + cb(TLS13_CIPHERS[i]); } } @@ -2526,23 +2795,59 @@ std::optional SSLPointer::verifyPeerCertificate() const return std::nullopt; } -WTF::StringView SSLPointer::getClientHelloAlpn() const +const WTF::StringView SSLPointer::getClientHelloAlpn() const { if (ssl_ == nullptr) return {}; - // BoringSSL doesn't have SSL_client_hello_get0_ext - // We'll need to use the early callback mechanism instead +#ifndef OPENSSL_IS_BORINGSSL + const unsigned char* buf; + size_t len; + size_t rem; + + if (!SSL_client_hello_get0_ext( + get(), TLSEXT_TYPE_application_layer_protocol_negotiation, &buf, + &rem) + || rem < 2) { + return {}; + } + + len = (buf[0] << 8) | buf[1]; + if (len + 2 != rem) return {}; + return reinterpret_cast(buf + 3); +#else + // Boringssl doesn't have a public API for this. return {}; +#endif } -WTF::StringView SSLPointer::getClientHelloServerName() const +const WTF::StringView SSLPointer::getClientHelloServerName() const { if (ssl_ == nullptr) return {}; - // BoringSSL doesn't have SSL_client_hello_get0_ext - // We'll need to use the early callback mechanism instead +#ifndef OPENSSL_IS_BORINGSSL + const unsigned char* buf; + size_t len; + size_t rem; + + if (!SSL_client_hello_get0_ext(get(), TLSEXT_TYPE_server_name, &buf, &rem) || rem <= 2) { + return {}; + } + + len = (*buf << 8) | *(buf + 1); + if (len + 2 != rem) return {}; + rem = len; + + if (rem == 0 || *(buf + 2) != TLSEXT_NAMETYPE_host_name) return {}; + rem--; + if (rem <= 2) return {}; + len = (*(buf + 3) << 8) | *(buf + 4); + if (len + 2 > rem) return {}; + return reinterpret_cast(buf + 5); +#else + // Boringssl doesn't have a public API for this. return {}; +#endif } -std::optional SSLPointer::GetServerName(const SSL* ssl) +std::optional SSLPointer::GetServerName(const SSL* ssl) { if (ssl == nullptr) return std::nullopt; auto res = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); @@ -2550,7 +2855,7 @@ std::optional SSLPointer::GetServerName(const SSL* ssl) return WTF::String::fromUTF8(res); } -std::optional SSLPointer::getServerName() const +std::optional SSLPointer::getServerName() const { if (!ssl_) return std::nullopt; return GetServerName(get()); @@ -2569,10 +2874,7 @@ const SSL_CIPHER* SSLPointer::getCipher() const return SSL_get_current_cipher(get()); } -bool SSLPointer::isServer() const -{ - return SSL_is_server(get()) != 0; -} +bool SSLPointer::isServer() const { return SSL_is_server(get()) != 0; } EVPKeyPointer SSLPointer::getPeerTempKey() const { @@ -2586,6 +2888,33 @@ EVPKeyPointer SSLPointer::getPeerTempKey() const return EVPKeyPointer(raw_key); } +std::optional SSLPointer::getCipherName() const +{ + auto cipher = getCipher(); + if (cipher == nullptr) return std::nullopt; + const char* name = SSL_CIPHER_get_name(cipher); + if (!name) return std::nullopt; + return WTF::StringView::fromLatin1(name); +} + +std::optional SSLPointer::getCipherStandardName() const +{ + auto cipher = getCipher(); + if (cipher == nullptr) return std::nullopt; + const char* name = SSL_CIPHER_standard_name(cipher); + if (!name) return std::nullopt; + return WTF::StringView::fromLatin1(name); +} + +std::optional SSLPointer::getCipherVersion() const +{ + auto cipher = getCipher(); + if (cipher == nullptr) return std::nullopt; + auto version = SSL_CIPHER_get_version(cipher); + if (!version) return std::nullopt; + return WTF::StringView::fromLatin1(version); +} + SSLCtxPointer::SSLCtxPointer(SSL_CTX* ctx) : ctx_(ctx) { @@ -2603,25 +2932,16 @@ SSLCtxPointer& SSLCtxPointer::operator=(SSLCtxPointer&& other) noexcept return *new (this) SSLCtxPointer(std::move(other)); } -SSLCtxPointer::~SSLCtxPointer() -{ - reset(); -} +SSLCtxPointer::~SSLCtxPointer() { reset(); } -void SSLCtxPointer::reset(SSL_CTX* ctx) -{ - ctx_.reset(ctx); -} +void SSLCtxPointer::reset(SSL_CTX* ctx) { ctx_.reset(ctx); } void SSLCtxPointer::reset(const SSL_METHOD* method) { ctx_.reset(SSL_CTX_new(method)); } -SSL_CTX* SSLCtxPointer::release() -{ - return ctx_.release(); -} +SSL_CTX* SSLCtxPointer::release() { return ctx_.release(); } SSLCtxPointer SSLCtxPointer::NewServer() { @@ -2640,14 +2960,21 @@ SSLCtxPointer SSLCtxPointer::New(const SSL_METHOD* method) bool SSLCtxPointer::setGroups(const char* groups) { +#ifndef NCRYPTO_NO_SSL_GROUP_LIST return SSL_CTX_set1_groups_list(get(), groups) == 1; +#else + // Older versions of openssl/boringssl do not implement the + // SSL_CTX_set1_groups_list API + return false; +#endif } // ============================================================================ -const Cipher Cipher::FromName(const char* name) +const Cipher Cipher::FromName(WTF::StringView name) { - return Cipher(EVP_get_cipherbyname(name)); + auto nameUtf8 = name.utf8(); + return Cipher(EVP_get_cipherbyname(nameUtf8.data())); } const Cipher Cipher::FromNid(int nid) @@ -2690,42 +3017,42 @@ int Cipher::getNid() const return EVP_CIPHER_nid(cipher_); } -std::string_view Cipher::getModeLabel() const +WTF::ASCIILiteral Cipher::getModeLabel() const { if (!cipher_) return {}; switch (getMode()) { case EVP_CIPH_CCM_MODE: - return "ccm"; + return "ccm"_s; case EVP_CIPH_CFB_MODE: - return "cfb"; + return "cfb"_s; case EVP_CIPH_CBC_MODE: - return "cbc"; + return "cbc"_s; case EVP_CIPH_CTR_MODE: - return "ctr"; + return "ctr"_s; case EVP_CIPH_ECB_MODE: - return "ecb"; + return "ecb"_s; case EVP_CIPH_GCM_MODE: - return "gcm"; + return "gcm"_s; case EVP_CIPH_OCB_MODE: - return "ocb"; + return "ocb"_s; case EVP_CIPH_OFB_MODE: - return "ofb"; + return "ofb"_s; case EVP_CIPH_WRAP_MODE: - return "wrap"; + return "wrap"_s; case EVP_CIPH_XTS_MODE: - return "xts"; + return "xts"_s; case EVP_CIPH_STREAM_CIPHER: - return "stream"; + return "stream"_s; } - return "{unknown}"; + return "{unknown}"_s; } -std::string_view Cipher::getName() const +WTF::String Cipher::getName() const { if (!cipher_) return {}; // OBJ_nid2sn(EVP_CIPHER_nid(cipher)) is used here instead of // EVP_CIPHER_name(cipher) for compatibility with BoringSSL. - return OBJ_nid2sn(getNid()); + return WTF::String::fromUTF8(OBJ_nid2sn(getNid())); } bool Cipher::isSupportedAuthenticatedMode() const @@ -2772,20 +3099,11 @@ CipherCtxPointer& CipherCtxPointer::operator=( return *new (this) CipherCtxPointer(std::move(other)); } -CipherCtxPointer::~CipherCtxPointer() -{ - reset(); -} +CipherCtxPointer::~CipherCtxPointer() { reset(); } -void CipherCtxPointer::reset(EVP_CIPHER_CTX* ctx) -{ - ctx_.reset(ctx); -} +void CipherCtxPointer::reset(EVP_CIPHER_CTX* ctx) { ctx_.reset(ctx); } -EVP_CIPHER_CTX* CipherCtxPointer::release() -{ - return ctx_.release(); -} +EVP_CIPHER_CTX* CipherCtxPointer::release() { return ctx_.release(); } void CipherCtxPointer::setFlags(int flags) { @@ -2802,22 +3120,22 @@ bool CipherCtxPointer::setKeyLength(size_t length) bool CipherCtxPointer::setIvLength(size_t length) { if (!ctx_) return false; - return EVP_CIPHER_CTX_ctrl( - ctx_.get(), EVP_CTRL_AEAD_SET_IVLEN, length, nullptr); + return EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_SET_IVLEN, length, + nullptr); } bool CipherCtxPointer::setAeadTag(const Buffer& tag) { if (!ctx_) return false; - return EVP_CIPHER_CTX_ctrl( - ctx_.get(), EVP_CTRL_AEAD_SET_TAG, tag.len, const_cast(tag.data)); + return EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_SET_TAG, tag.len, + const_cast(tag.data)); } bool CipherCtxPointer::setAeadTagLength(size_t length) { if (!ctx_) return false; - return EVP_CIPHER_CTX_ctrl( - ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, nullptr); + return EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, + nullptr); } bool CipherCtxPointer::setPadding(bool padding) @@ -2844,21 +3162,17 @@ int CipherCtxPointer::getNid() const return EVP_CIPHER_CTX_nid(ctx_.get()); } -bool CipherCtxPointer::init(const Cipher& cipher, - bool encrypt, - const unsigned char* key, - const unsigned char* iv) +bool CipherCtxPointer::init(const Cipher& cipher, bool encrypt, + const unsigned char* key, const unsigned char* iv) { if (!ctx_) return false; - return EVP_CipherInit_ex( - ctx_.get(), cipher, nullptr, key, iv, encrypt ? 1 : 0) + return EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, key, iv, + encrypt ? 1 : 0) == 1; } bool CipherCtxPointer::update(const Buffer& in, - unsigned char* out, - int* out_len, - bool finalize) + unsigned char* out, int* out_len, bool finalize) { if (!ctx_) return false; if (!finalize) { @@ -2903,10 +3217,7 @@ ECDSASigPointer& ECDSASigPointer::operator=(ECDSASigPointer&& other) noexcept return *this; } -ECDSASigPointer::~ECDSASigPointer() -{ - reset(); -} +ECDSASigPointer::~ECDSASigPointer() { reset(); } void ECDSASigPointer::reset(ECDSA_SIG* sig) { @@ -2974,20 +3285,11 @@ ECGroupPointer& ECGroupPointer::operator=(ECGroupPointer&& other) noexcept return *this; } -ECGroupPointer::~ECGroupPointer() -{ - reset(); -} +ECGroupPointer::~ECGroupPointer() { reset(); } -void ECGroupPointer::reset(EC_GROUP* group) -{ - group_.reset(); -} +void ECGroupPointer::reset(EC_GROUP* group) { group_.reset(); } -EC_GROUP* ECGroupPointer::release() -{ - return group_.release(); -} +EC_GROUP* ECGroupPointer::release() { return group_.release(); } ECGroupPointer ECGroupPointer::NewByCurveName(int nid) { @@ -3017,20 +3319,11 @@ ECPointPointer& ECPointPointer::operator=(ECPointPointer&& other) noexcept return *this; } -ECPointPointer::~ECPointPointer() -{ - reset(); -} +ECPointPointer::~ECPointPointer() { reset(); } -void ECPointPointer::reset(EC_POINT* point) -{ - point_.reset(point); -} +void ECPointPointer::reset(EC_POINT* point) { point_.reset(point); } -EC_POINT* ECPointPointer::release() -{ - return point_.release(); -} +EC_POINT* ECPointPointer::release() { return point_.release(); } ECPointPointer ECPointPointer::New(const EC_GROUP* group) { @@ -3041,8 +3334,8 @@ bool ECPointPointer::setFromBuffer(const Buffer& buffer, const EC_GROUP* group) { if (!point_) return false; - return EC_POINT_oct2point( - group, point_.get(), buffer.data, buffer.len, nullptr); + return EC_POINT_oct2point(group, point_.get(), buffer.data, buffer.len, + nullptr); } bool ECPointPointer::mul(const EC_GROUP* group, const BIGNUM* priv_key) @@ -3074,20 +3367,11 @@ ECKeyPointer& ECKeyPointer::operator=(ECKeyPointer&& other) noexcept return *this; } -ECKeyPointer::~ECKeyPointer() -{ - reset(); -} +ECKeyPointer::~ECKeyPointer() { reset(); } -void ECKeyPointer::reset(EC_KEY* key) -{ - key_.reset(key); -} +void ECKeyPointer::reset(EC_KEY* key) { key_.reset(key); } -EC_KEY* ECKeyPointer::release() -{ - return key_.release(); -} +EC_KEY* ECKeyPointer::release() { return key_.release(); } ECKeyPointer ECKeyPointer::clone() const { @@ -3111,8 +3395,8 @@ bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x, const BignumPointer& y) { if (!key_) return false; - return EC_KEY_set_public_key_affine_coordinates( - key_.get(), x.get(), y.get()) + return EC_KEY_set_public_key_affine_coordinates(key_.get(), x.get(), + y.get()) == 1; } @@ -3185,4 +3469,1410 @@ ECKeyPointer ECKeyPointer::New(const EC_GROUP* group) return ptr; } +// ============================================================================ + +EVPKeyCtxPointer::EVPKeyCtxPointer() + : ctx_(nullptr) +{ +} + +EVPKeyCtxPointer::EVPKeyCtxPointer(EVP_PKEY_CTX* ctx) + : ctx_(ctx) +{ +} + +EVPKeyCtxPointer::EVPKeyCtxPointer(EVPKeyCtxPointer&& other) noexcept + : ctx_(other.release()) +{ +} + +EVPKeyCtxPointer& EVPKeyCtxPointer::operator=( + EVPKeyCtxPointer&& other) noexcept +{ + ctx_.reset(other.release()); + return *this; +} + +EVPKeyCtxPointer::~EVPKeyCtxPointer() { reset(); } + +void EVPKeyCtxPointer::reset(EVP_PKEY_CTX* ctx) { ctx_.reset(ctx); } + +EVP_PKEY_CTX* EVPKeyCtxPointer::release() { return ctx_.release(); } + +EVPKeyCtxPointer EVPKeyCtxPointer::New(const EVPKeyPointer& key) +{ + if (!key) return {}; + return EVPKeyCtxPointer(EVP_PKEY_CTX_new(key.get(), nullptr)); +} + +EVPKeyCtxPointer EVPKeyCtxPointer::NewFromID(int id) +{ + return EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(id, nullptr)); +} + +bool EVPKeyCtxPointer::initForDerive(const EVPKeyPointer& peer) +{ + if (!ctx_) return false; + if (EVP_PKEY_derive_init(ctx_.get()) != 1) return false; + return EVP_PKEY_derive_set_peer(ctx_.get(), peer.get()) == 1; +} + +bool EVPKeyCtxPointer::initForKeygen() +{ + if (!ctx_) return false; + return EVP_PKEY_keygen_init(ctx_.get()) == 1; +} + +bool EVPKeyCtxPointer::initForParamgen() +{ + if (!ctx_) return false; + return EVP_PKEY_paramgen_init(ctx_.get()) == 1; +} + +int EVPKeyCtxPointer::initForVerify() +{ + if (!ctx_) return 0; + return EVP_PKEY_verify_init(ctx_.get()); +} + +int EVPKeyCtxPointer::initForSign() +{ + if (!ctx_) return 0; + return EVP_PKEY_sign_init(ctx_.get()); +} + +bool EVPKeyCtxPointer::setDhParameters(int prime_size, uint32_t generator) +{ +#ifndef OPENSSL_IS_BORINGSSL + if (!ctx_) return false; + return EVP_PKEY_CTX_set_dh_paramgen_prime_len(ctx_.get(), prime_size) == 1 && EVP_PKEY_CTX_set_dh_paramgen_generator(ctx_.get(), generator) == 1; +#else + // TODO(jasnell): Boringssl appears not to support this operation. + // Is there an alternative approach that Boringssl does support? + return false; +#endif +} + +bool EVPKeyCtxPointer::setDsaParameters(uint32_t bits, + std::optional q_bits) +{ +#ifndef NCRYPTO_NO_DSA_KEYGEN + if (!ctx_) return false; + if (EVP_PKEY_CTX_set_dsa_paramgen_bits(ctx_.get(), bits) != 1) { + return false; + } + if (q_bits.has_value() && EVP_PKEY_CTX_set_dsa_paramgen_q_bits(ctx_.get(), q_bits.value()) != 1) { + return false; + } + return true; +#else + // Older versions of openssl/boringssl do not implement the DSA keygen. + return false; +#endif +} + +bool EVPKeyCtxPointer::setEcParameters(int curve, int encoding) +{ + if (!ctx_) return false; + return EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx_.get(), curve) == 1 && EVP_PKEY_CTX_set_ec_param_enc(ctx_.get(), encoding) == 1; +} + +bool EVPKeyCtxPointer::setRsaOaepMd(const EVP_MD* md) +{ + if (md == nullptr || !ctx_) return false; + return EVP_PKEY_CTX_set_rsa_oaep_md(ctx_.get(), md) > 0; +} + +bool EVPKeyCtxPointer::setRsaMgf1Md(const EVP_MD* md) +{ + if (md == nullptr || !ctx_) return false; + return EVP_PKEY_CTX_set_rsa_mgf1_md(ctx_.get(), md) > 0; +} + +bool EVPKeyCtxPointer::setRsaPadding(int padding) +{ + return setRsaPadding(ctx_.get(), padding, std::nullopt); +} + +bool EVPKeyCtxPointer::setRsaPadding(EVP_PKEY_CTX* ctx, int padding, + std::optional salt_len) +{ + if (ctx == nullptr) return false; + if (EVP_PKEY_CTX_set_rsa_padding(ctx, padding) <= 0) { + return false; + } + if (padding == RSA_PKCS1_PSS_PADDING && salt_len.has_value()) { + return EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, salt_len.value()) > 0; + } + return true; +} + +bool EVPKeyCtxPointer::setRsaKeygenBits(int bits) +{ + if (!ctx_) return false; + return EVP_PKEY_CTX_set_rsa_keygen_bits(ctx_.get(), bits) == 1; +} + +bool EVPKeyCtxPointer::setRsaKeygenPubExp(BignumPointer&& e) +{ + if (!ctx_) return false; + if (EVP_PKEY_CTX_set_rsa_keygen_pubexp(ctx_.get(), e.get()) == 1) { + // The ctx_ takes ownership of e on success. + e.release(); + return true; + } + return false; +} + +bool EVPKeyCtxPointer::setRsaPssKeygenMd(const EVP_MD* md) +{ + if (md == nullptr || !ctx_) return false; + return EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx_.get(), md) > 0; +} + +bool EVPKeyCtxPointer::setRsaPssKeygenMgf1Md(const EVP_MD* md) +{ + if (md == nullptr || !ctx_) return false; + return EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md(ctx_.get(), md) > 0; +} + +bool EVPKeyCtxPointer::setRsaPssSaltlen(int salt_len) +{ + if (!ctx_) return false; + return EVP_PKEY_CTX_set_rsa_pss_keygen_saltlen(ctx_.get(), salt_len) > 0; +} + +bool EVPKeyCtxPointer::setRsaImplicitRejection() +{ +#ifndef OPENSSL_IS_BORINGSSL + if (!ctx_) return false; + return EVP_PKEY_CTX_ctrl_str(ctx_.get(), "rsa_pkcs1_implicit_rejection", + "1") + > 0; + // From the doc -2 means that the option is not supported. + // The default for the option is enabled and if it has been + // specifically disabled we want to respect that so we will + // not throw an error if the option is supported regardless + // of how it is set. The call to set the value + // will not affect what is used since a different context is + // used in the call if the option is supported +#else + // TODO(jasnell): Boringssl appears not to support this operation. + // Is there an alternative approach that Boringssl does support? + return true; +#endif +} + +bool EVPKeyCtxPointer::setRsaOaepLabel(DataPointer&& data) +{ + if (!ctx_) return false; + if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx_.get(), + static_cast(data.get()), + data.size()) + > 0) { + // The ctx_ takes ownership of data on success. + data.release(); + return true; + } + return false; +} + +bool EVPKeyCtxPointer::setSignatureMd(const EVPMDCtxPointer& md) +{ + if (!ctx_) return false; + return EVP_PKEY_CTX_set_signature_md(ctx_.get(), EVP_MD_CTX_md(md.get())) == 1; +} + +bool EVPKeyCtxPointer::initForEncrypt() +{ + if (!ctx_) return false; + return EVP_PKEY_encrypt_init(ctx_.get()) == 1; +} + +bool EVPKeyCtxPointer::initForDecrypt() +{ + if (!ctx_) return false; + return EVP_PKEY_decrypt_init(ctx_.get()) == 1; +} + +DataPointer EVPKeyCtxPointer::derive() const +{ + if (!ctx_) return {}; + size_t len = 0; + if (EVP_PKEY_derive(ctx_.get(), nullptr, &len) != 1) return {}; + auto data = DataPointer::Alloc(len); + if (!data) return {}; + if (EVP_PKEY_derive(ctx_.get(), static_cast(data.get()), + &len) + != 1) { + return {}; + } + return data; +} + +EVPKeyPointer EVPKeyCtxPointer::paramgen() const +{ + if (!ctx_) return {}; + EVP_PKEY* key = nullptr; + if (EVP_PKEY_paramgen(ctx_.get(), &key) != 1) return {}; + return EVPKeyPointer(key); +} + +bool EVPKeyCtxPointer::publicCheck() const +{ + if (!ctx_) return false; +#ifndef OPENSSL_IS_BORINGSSL + return EVP_PKEY_public_check(ctx_.get()) == 1; +#if OPENSSL_VERSION_MAJOR >= 3 + return EVP_PKEY_public_check_quick(ctx_.get()) == 1; +#else + return EVP_PKEY_public_check(ctx_.get()) == 1; +#endif +#else // OPENSSL_IS_BORINGSSL + // Boringssl appears not to support this operation. + // TODO(jasnell): Is there an alternative approach that Boringssl does + // support? + return true; +#endif +} + +bool EVPKeyCtxPointer::privateCheck() const +{ + if (!ctx_) return false; +#ifndef OPENSSL_IS_BORINGSSL + return EVP_PKEY_check(ctx_.get()) == 1; +#else + // Boringssl appears not to support this operation. + // TODO(jasnell): Is there an alternative approach that Boringssl does + // support? + return true; +#endif +} + +bool EVPKeyCtxPointer::verify(const Buffer& sig, + const Buffer& data) +{ + if (!ctx_) return false; + return EVP_PKEY_verify(ctx_.get(), sig.data, sig.len, data.data, data.len) == 1; +} + +DataPointer EVPKeyCtxPointer::sign(const Buffer& data) +{ + if (!ctx_) return {}; + size_t len = 0; + if (EVP_PKEY_sign(ctx_.get(), nullptr, &len, data.data, data.len) != 1) { + return {}; + } + auto buf = DataPointer::Alloc(len); + if (!buf) return {}; + if (EVP_PKEY_sign(ctx_.get(), static_cast(buf.get()), &len, + data.data, data.len) + != 1) { + return {}; + } + return buf.resize(len); +} + +bool EVPKeyCtxPointer::signInto(const Buffer& data, + Buffer* sig) +{ + if (!ctx_) return false; + size_t len = sig->len; + if (EVP_PKEY_sign(ctx_.get(), sig->data, &len, data.data, data.len) != 1) { + return false; + } + sig->len = len; + return true; +} + +// ============================================================================ + +namespace { + +using EVP_PKEY_cipher_init_t = int(EVP_PKEY_CTX* ctx); +using EVP_PKEY_cipher_t = int(EVP_PKEY_CTX* ctx, unsigned char* out, + size_t* outlen, const unsigned char* in, + size_t inlen); + +template +DataPointer RSA_Cipher(const EVPKeyPointer& key, + const Rsa::CipherParams& params, + const Buffer in) +{ + if (!key) return {}; + EVPKeyCtxPointer ctx = key.newCtx(); + + if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) || (params.digest != nullptr && (!ctx.setRsaOaepMd(params.digest) || !ctx.setRsaMgf1Md(params.digest)))) { + return {}; + } + + if (params.label.len != 0 && params.label.data != nullptr && !ctx.setRsaOaepLabel(DataPointer::Copy(params.label))) { + return {}; + } + + size_t out_len = 0; + if (cipher(ctx.get(), nullptr, &out_len, + reinterpret_cast(in.data), in.len) + <= 0) { + return {}; + } + + auto buf = DataPointer::Alloc(out_len); + if (!buf) return {}; + + if (cipher(ctx.get(), static_cast(buf.get()), &out_len, + static_cast(in.data), in.len) + <= 0) { + return {}; + } + + return buf.resize(out_len); +} + +template +DataPointer CipherImpl(const EVPKeyPointer& key, + const Rsa::CipherParams& params, + const Buffer in) +{ + if (!key) return {}; + EVPKeyCtxPointer ctx = key.newCtx(); + if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) || (params.digest != nullptr && !ctx.setRsaOaepMd(params.digest))) { + return {}; + } + + if (params.label.len != 0 && params.label.data != nullptr && !ctx.setRsaOaepLabel(DataPointer::Copy(params.label))) { + return {}; + } + + size_t out_len = 0; + if (cipher(ctx.get(), nullptr, &out_len, + static_cast(in.data), in.len) + <= 0) { + return {}; + } + + auto buf = DataPointer::Alloc(out_len); + if (!buf) return {}; + + if (cipher(ctx.get(), static_cast(buf.get()), &out_len, + static_cast(in.data), in.len) + <= 0) { + return {}; + } + + return buf.resize(out_len); +} +} // namespace + +Rsa::Rsa() + : rsa_(nullptr) +{ +} + +Rsa::Rsa(OSSL3_CONST RSA* ptr) + : rsa_(ptr) +{ +} + +const Rsa::PublicKey Rsa::getPublicKey() const +{ + if (rsa_ == nullptr) return {}; + PublicKey key; + RSA_get0_key(rsa_, &key.n, &key.e, &key.d); + return key; +} + +const Rsa::PrivateKey Rsa::getPrivateKey() const +{ + if (rsa_ == nullptr) return {}; + PrivateKey key; + RSA_get0_factors(rsa_, &key.p, &key.q); + RSA_get0_crt_params(rsa_, &key.dp, &key.dq, &key.qi); + return key; +} + +const std::optional Rsa::getPssParams() const +{ + if (rsa_ == nullptr) return std::nullopt; + const RSA_PSS_PARAMS* params = RSA_get0_pss_params(rsa_); + if (params == nullptr) return std::nullopt; + Rsa::PssParams ret { + .digest = WTF::StringView::fromLatin1(OBJ_nid2ln(NID_sha1)), + .mgf1_digest = WTF::StringView::fromLatin1(OBJ_nid2ln(NID_sha1)), + .salt_length = 20, + }; + + if (params->hashAlgorithm != nullptr) { + const ASN1_OBJECT* hash_obj; + X509_ALGOR_get0(&hash_obj, nullptr, nullptr, params->hashAlgorithm); + ret.digest = WTF::StringView::fromLatin1(OBJ_nid2ln(OBJ_obj2nid(hash_obj))); + } + + if (params->maskGenAlgorithm != nullptr) { + const ASN1_OBJECT* mgf_obj; + X509_ALGOR_get0(&mgf_obj, nullptr, nullptr, params->maskGenAlgorithm); + int mgf_nid = OBJ_obj2nid(mgf_obj); + if (mgf_nid == NID_mgf1) { + const ASN1_OBJECT* mgf1_hash_obj; + X509_ALGOR_get0(&mgf1_hash_obj, nullptr, nullptr, params->maskHash); + ret.mgf1_digest = WTF::StringView::fromLatin1(OBJ_nid2ln(OBJ_obj2nid(mgf1_hash_obj))); + } + } + + if (params->saltLength != nullptr) { + // Older versions of openssl/boringssl do not implement + // ASN1_INTEGER_get_int64, which the salt length here technically + // is. Let's walk it through uint64_t with a conversion. + uint64_t temp; + if (ASN1_INTEGER_get_uint64(&temp, params->saltLength) != 1) { + return std::nullopt; + } + ret.salt_length = static_cast(temp); + } + return ret; +} + +bool Rsa::setPublicKey(BignumPointer&& n, BignumPointer&& e) +{ + if (!n || !e) return false; + if (RSA_set0_key(const_cast(rsa_), n.get(), e.get(), nullptr) == 1) { + n.release(); + e.release(); + return true; + } + return false; +} + +bool Rsa::setPrivateKey(BignumPointer&& d, BignumPointer&& q, BignumPointer&& p, + BignumPointer&& dp, BignumPointer&& dq, + BignumPointer&& qi) +{ + if (!RSA_set0_key(const_cast(rsa_), nullptr, nullptr, d.get())) { + return false; + } + d.release(); + + if (!RSA_set0_factors(const_cast(rsa_), p.get(), q.get())) { + return false; + } + p.release(); + q.release(); + + if (!RSA_set0_crt_params(const_cast(rsa_), dp.get(), dq.get(), + qi.get())) { + return false; + } + dp.release(); + dq.release(); + qi.release(); + return true; +} + +DataPointer Rsa::encrypt(const EVPKeyPointer& key, + const Rsa::CipherParams& params, + const Buffer in) +{ + if (!key) return {}; + return RSA_Cipher(key, params, in); +} + +DataPointer Rsa::decrypt(const EVPKeyPointer& key, + const Rsa::CipherParams& params, + const Buffer in) +{ + if (!key) return {}; + return RSA_Cipher(key, params, in); +} + +DataPointer Cipher::encrypt(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in) +{ + // public operation + return CipherImpl(key, params, in); +} + +DataPointer Cipher::decrypt(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in) +{ + // private operation + return CipherImpl(key, params, in); +} + +DataPointer Cipher::sign(const EVPKeyPointer& key, const CipherParams& params, + const Buffer in) +{ + // private operation + return CipherImpl(key, params, in); +} + +DataPointer Cipher::recover(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in) +{ + // public operation + return CipherImpl( + key, params, in); +} + +// ============================================================================ + +Ec::Ec() + : ec_(nullptr) +{ +} + +Ec::Ec(OSSL3_CONST EC_KEY* key) + : ec_(key) + , x_(BignumPointer::New()) + , y_(BignumPointer::New()) +{ + if (ec_ != nullptr) { + MarkPopErrorOnReturn mark_pop_error_on_return; + EC_POINT_get_affine_coordinates(getGroup(), getPublicKey(), x_.get(), + y_.get(), nullptr); + } +} + +const EC_GROUP* Ec::getGroup() const { return ECKeyPointer::GetGroup(ec_); } + +int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } + +uint32_t Ec::getDegree() const { return EC_GROUP_get_degree(getGroup()); } + +WTF::String Ec::getCurveName() const +{ + return WTF::String::fromUTF8(OBJ_nid2sn(getCurve())); +} + +const EC_POINT* Ec::getPublicKey() const { return EC_KEY_get0_public_key(ec_); } + +const BIGNUM* Ec::getPrivateKey() const { return EC_KEY_get0_private_key(ec_); } + +// ============================================================================ + +EVPMDCtxPointer::EVPMDCtxPointer() + : ctx_(nullptr) +{ +} + +EVPMDCtxPointer::EVPMDCtxPointer(EVP_MD_CTX* ctx) + : ctx_(ctx) +{ +} + +EVPMDCtxPointer::EVPMDCtxPointer(EVPMDCtxPointer&& other) noexcept + : ctx_(other.release()) +{ +} + +EVPMDCtxPointer& EVPMDCtxPointer::operator=(EVPMDCtxPointer&& other) noexcept +{ + ctx_.reset(other.release()); + return *this; +} + +EVPMDCtxPointer::~EVPMDCtxPointer() { reset(); } + +void EVPMDCtxPointer::reset(EVP_MD_CTX* ctx) { ctx_.reset(ctx); } + +EVP_MD_CTX* EVPMDCtxPointer::release() { return ctx_.release(); } + +bool EVPMDCtxPointer::digestInit(const EVP_MD* digest) +{ + if (!ctx_) return false; + return EVP_DigestInit_ex(ctx_.get(), digest, nullptr) > 0; +} + +bool EVPMDCtxPointer::digestUpdate(const Buffer& in) +{ + if (!ctx_) return false; + return EVP_DigestUpdate(ctx_.get(), in.data, in.len) > 0; +} + +DataPointer EVPMDCtxPointer::digestFinal(size_t length) +{ + if (!ctx_) return {}; + + auto buf = DataPointer::Alloc(length); + if (!buf) return {}; + + Buffer buffer = buf; + + if (!digestFinalInto(&buffer)) [[unlikely]] { + return {}; + } + + return buf; +} + +bool EVPMDCtxPointer::digestFinalInto(Buffer* buf) +{ + if (!ctx_) return false; + + auto ptr = static_cast(buf->data); + + int ret = (buf->len == getExpectedSize()) + ? EVP_DigestFinal_ex(ctx_.get(), ptr, nullptr) + : EVP_DigestFinalXOF(ctx_.get(), ptr, buf->len); + + if (ret != 1) [[unlikely]] + return false; + + return true; +} + +size_t EVPMDCtxPointer::getExpectedSize() +{ + if (!ctx_) return 0; + return EVP_MD_CTX_size(ctx_.get()); +} + +size_t EVPMDCtxPointer::getDigestSize() const +{ + return EVP_MD_size(getDigest()); +} + +const EVP_MD* EVPMDCtxPointer::getDigest() const +{ + if (!ctx_) return nullptr; + return EVP_MD_CTX_md(ctx_.get()); +} + +bool EVPMDCtxPointer::hasXofFlag() const +{ + if (!ctx_) return false; + return (EVP_MD_flags(getDigest()) & EVP_MD_FLAG_XOF) == EVP_MD_FLAG_XOF; +} + +bool EVPMDCtxPointer::copyTo(const EVPMDCtxPointer& other) const +{ + if (!ctx_ || !other) return {}; + if (EVP_MD_CTX_copy(other.get(), ctx_.get()) != 1) return false; + return true; +} + +std::optional EVPMDCtxPointer::signInit(const EVPKeyPointer& key, + const EVP_MD* digest) +{ + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestSignInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + return ctx; +} + +std::optional EVPMDCtxPointer::verifyInit( + const EVPKeyPointer& key, const EVP_MD* digest) +{ + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestVerifyInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + return ctx; +} + +DataPointer EVPMDCtxPointer::signOneShot( + const Buffer& buf) const +{ + if (!ctx_) return {}; + size_t len; + if (!EVP_DigestSign(ctx_.get(), nullptr, &len, buf.data, buf.len)) { + return {}; + } + auto data = DataPointer::Alloc(len); + if (!data) [[unlikely]] + return {}; + + if (!EVP_DigestSign(ctx_.get(), static_cast(data.get()), &len, + buf.data, buf.len)) { + return {}; + } + return data; +} + +DataPointer EVPMDCtxPointer::sign( + const Buffer& buf) const +{ + if (!ctx_) [[unlikely]] + return {}; + size_t len; + if (!EVP_DigestSignUpdate(ctx_.get(), buf.data, buf.len) || !EVP_DigestSignFinal(ctx_.get(), nullptr, &len)) { + return {}; + } + auto data = DataPointer::Alloc(len); + if (!data) [[unlikely]] + return {}; + if (!EVP_DigestSignFinal(ctx_.get(), static_cast(data.get()), + &len)) { + return {}; + } + return data.resize(len); +} + +bool EVPMDCtxPointer::verify(const Buffer& buf, + const Buffer& sig) const +{ + if (!ctx_) return false; + int ret = EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len); + return ret == 1; +} + +EVPMDCtxPointer EVPMDCtxPointer::New() +{ + return EVPMDCtxPointer(EVP_MD_CTX_new()); +} + +// ============================================================================ + +bool extractP1363(const Buffer& buf, unsigned char* dest, + size_t n) +{ + auto asn1_sig = ECDSASigPointer::Parse(buf); + if (!asn1_sig) return false; + + return BignumPointer::EncodePaddedInto(asn1_sig.r(), dest, n) > 0 && BignumPointer::EncodePaddedInto(asn1_sig.s(), dest + n, n) > 0; +} + +// ============================================================================ + +HMACCtxPointer::HMACCtxPointer() + : ctx_(nullptr) +{ +} + +HMACCtxPointer::HMACCtxPointer(HMAC_CTX* ctx) + : ctx_(ctx) +{ +} + +HMACCtxPointer::HMACCtxPointer(HMACCtxPointer&& other) noexcept + : ctx_(other.release()) +{ +} + +HMACCtxPointer& HMACCtxPointer::operator=(HMACCtxPointer&& other) noexcept +{ + ctx_.reset(other.release()); + return *this; +} + +HMACCtxPointer::~HMACCtxPointer() { reset(); } + +void HMACCtxPointer::reset(HMAC_CTX* ctx) { ctx_.reset(ctx); } + +HMAC_CTX* HMACCtxPointer::release() { return ctx_.release(); } + +bool HMACCtxPointer::init(const Buffer& buf, const EVP_MD* md) +{ + if (!ctx_) return false; + return HMAC_Init_ex(ctx_.get(), buf.data, buf.len, md, nullptr) == 1; +} + +bool HMACCtxPointer::update(const Buffer& buf) +{ + if (!ctx_) return false; + return HMAC_Update(ctx_.get(), static_cast(buf.data), + buf.len) + == 1; +} + +DataPointer HMACCtxPointer::digest() +{ + auto data = DataPointer::Alloc(EVP_MAX_MD_SIZE); + if (!data) return {}; + Buffer buf = data; + if (!digestInto(&buf)) return {}; + return data.resize(buf.len); +} + +bool HMACCtxPointer::digestInto(Buffer* buf) +{ + if (!ctx_) return false; + + unsigned int len = buf->len; + if (!HMAC_Final(ctx_.get(), static_cast(buf->data), &len)) { + return false; + } + buf->len = len; + return true; +} + +HMACCtxPointer HMACCtxPointer::New() { return HMACCtxPointer(HMAC_CTX_new()); } + +DataPointer hashDigest(const Buffer& buf, + const EVP_MD* md) +{ + if (md == nullptr) return {}; + size_t md_len = EVP_MD_size(md); + unsigned int result_size; + auto data = DataPointer::Alloc(md_len); + if (!data) return {}; + + if (!EVP_Digest(buf.data, buf.len, + reinterpret_cast(data.get()), &result_size, + md, nullptr)) { + return {}; + } + + return data.resize(result_size); +} + +// ============================================================================ + +X509Name::X509Name() + : name_(nullptr) + , total_(0) +{ +} + +X509Name::X509Name(const X509_NAME* name) + : name_(name) + , total_(X509_NAME_entry_count(name)) +{ +} + +X509Name::Iterator::Iterator(const X509Name& name, int pos) + : name_(name) + , loc_(pos) +{ +} + +X509Name::Iterator& X509Name::Iterator::operator++() +{ + ++loc_; + return *this; +} + +X509Name::Iterator::operator bool() const { return loc_ < name_.total_; } + +bool X509Name::Iterator::operator==(const Iterator& other) const +{ + return loc_ == other.loc_; +} + +bool X509Name::Iterator::operator!=(const Iterator& other) const +{ + return loc_ != other.loc_; +} + +std::pair X509Name::Iterator::operator*() const +{ + if (loc_ == name_.total_) return { {}, {} }; + + X509_NAME_ENTRY* entry = X509_NAME_get_entry(name_, loc_); + if (entry == nullptr) [[unlikely]] + return { {}, {} }; + + ASN1_OBJECT* name = X509_NAME_ENTRY_get_object(entry); + ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry); + + if (name == nullptr || value == nullptr) [[unlikely]] { + return { {}, {} }; + } + + int nid = OBJ_obj2nid(name); + WTF::String name_str; + if (nid != NID_undef) { + name_str = WTF::String::fromUTF8(OBJ_nid2sn(nid)); + } else { + char buf[80]; + OBJ_obj2txt(buf, sizeof(buf), name, 0); + name_str = WTF::String::fromUTF8(buf); + } + + unsigned char* value_str; + int value_str_size = ASN1_STRING_to_UTF8(&value_str, value); + + return { + name_str, + WTF::String::fromUTF8(std::span(value_str, value_str_size)), + }; +} + +// ============================================================================ + +Dsa::Dsa() + : dsa_(nullptr) +{ +} + +Dsa::Dsa(OSSL3_CONST DSA* dsa) + : dsa_(dsa) +{ +} + +const BIGNUM* Dsa::getP() const +{ + if (dsa_ == nullptr) return nullptr; + const BIGNUM* p; + DSA_get0_pqg(dsa_, &p, nullptr, nullptr); + return p; +} + +const BIGNUM* Dsa::getQ() const +{ + if (dsa_ == nullptr) return nullptr; + const BIGNUM* q; + DSA_get0_pqg(dsa_, nullptr, &q, nullptr); + return q; +} + +size_t Dsa::getModulusLength() const +{ + if (dsa_ == nullptr) return 0; + return BignumPointer::GetBitCount(getP()); +} + +size_t Dsa::getDivisorLength() const +{ + if (dsa_ == nullptr) return 0; + return BignumPointer::GetBitCount(getQ()); +} } // namespace ncrypto + +// =========================================================================== +#ifdef NCRYPTO_BSSL_NEEDS_DH_PRIMES +// While newer versions of BoringSSL have these primes, older versions do not, +// in particular older versions that conform to fips. We conditionally add +// them here only if the NCRYPTO_BSSL_NEEDS_DH_PRIMES define is set. Their +// implementations are defined here to prevent duplicating the symbols. +extern "C" int bn_set_words(BIGNUM* bn, const BN_ULONG* words, size_t num); + +// Backporting primes that may not be supported in earlier boringssl versions. +// Intentionally keeping the existing C-style formatting. + +#define OPENSSL_ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +#if defined(OPENSSL_64_BIT) +#define TOBN(hi, lo) ((BN_ULONG)(hi) << 32 | (lo)) +#elif defined(OPENSSL_32_BIT) +#define TOBN(hi, lo) (lo), (hi) +#else +#error "Must define either OPENSSL_32_BIT or OPENSSL_64_BIT" +#endif + +static BIGNUM* get_params(BIGNUM* ret, const BN_ULONG* words, + size_t num_words) +{ + BIGNUM* alloc = nullptr; + if (ret == nullptr) { + alloc = BN_new(); + if (alloc == nullptr) { + return nullptr; + } + ret = alloc; + } + + if (!bn_set_words(ret, words, num_words)) { + BN_free(alloc); + return nullptr; + } + + return ret; +} + +BIGNUM* BN_get_rfc3526_prime_2048(BIGNUM* ret) +{ + static const BN_ULONG kWords[] = { + TOBN(0xffffffff, 0xffffffff), + TOBN(0x15728e5a, 0x8aacaa68), + TOBN(0x15d22618, 0x98fa0510), + TOBN(0x3995497c, 0xea956ae5), + TOBN(0xde2bcbf6, 0x95581718), + TOBN(0xb5c55df0, 0x6f4c52c9), + TOBN(0x9b2783a2, 0xec07a28f), + TOBN(0xe39e772c, 0x180e8603), + TOBN(0x32905e46, 0x2e36ce3b), + TOBN(0xf1746c08, 0xca18217c), + TOBN(0x670c354e, 0x4abc9804), + TOBN(0x9ed52907, 0x7096966d), + TOBN(0x1c62f356, 0x208552bb), + TOBN(0x83655d23, 0xdca3ad96), + TOBN(0x69163fa8, 0xfd24cf5f), + TOBN(0x98da4836, 0x1c55d39a), + TOBN(0xc2007cb8, 0xa163bf05), + TOBN(0x49286651, 0xece45b3d), + TOBN(0xae9f2411, 0x7c4b1fe6), + TOBN(0xee386bfb, 0x5a899fa5), + TOBN(0x0bff5cb6, 0xf406b7ed), + TOBN(0xf44c42e9, 0xa637ed6b), + TOBN(0xe485b576, 0x625e7ec6), + TOBN(0x4fe1356d, 0x6d51c245), + TOBN(0x302b0a6d, 0xf25f1437), + TOBN(0xef9519b3, 0xcd3a431b), + TOBN(0x514a0879, 0x8e3404dd), + TOBN(0x020bbea6, 0x3b139b22), + TOBN(0x29024e08, 0x8a67cc74), + TOBN(0xc4c6628b, 0x80dc1cd1), + TOBN(0xc90fdaa2, 0x2168c234), + TOBN(0xffffffff, 0xffffffff), + }; + return get_params(ret, kWords, OPENSSL_ARRAY_SIZE(kWords)); +} + +BIGNUM* BN_get_rfc3526_prime_3072(BIGNUM* ret) +{ + static const BN_ULONG kWords[] = { + TOBN(0xffffffff, 0xffffffff), + TOBN(0x4b82d120, 0xa93ad2ca), + TOBN(0x43db5bfc, 0xe0fd108e), + TOBN(0x08e24fa0, 0x74e5ab31), + TOBN(0x770988c0, 0xbad946e2), + TOBN(0xbbe11757, 0x7a615d6c), + TOBN(0x521f2b18, 0x177b200c), + TOBN(0xd8760273, 0x3ec86a64), + TOBN(0xf12ffa06, 0xd98a0864), + TOBN(0xcee3d226, 0x1ad2ee6b), + TOBN(0x1e8c94e0, 0x4a25619d), + TOBN(0xabf5ae8c, 0xdb0933d7), + TOBN(0xb3970f85, 0xa6e1e4c7), + TOBN(0x8aea7157, 0x5d060c7d), + TOBN(0xecfb8504, 0x58dbef0a), + TOBN(0xa85521ab, 0xdf1cba64), + TOBN(0xad33170d, 0x04507a33), + TOBN(0x15728e5a, 0x8aaac42d), + TOBN(0x15d22618, 0x98fa0510), + TOBN(0x3995497c, 0xea956ae5), + TOBN(0xde2bcbf6, 0x95581718), + TOBN(0xb5c55df0, 0x6f4c52c9), + TOBN(0x9b2783a2, 0xec07a28f), + TOBN(0xe39e772c, 0x180e8603), + TOBN(0x32905e46, 0x2e36ce3b), + TOBN(0xf1746c08, 0xca18217c), + TOBN(0x670c354e, 0x4abc9804), + TOBN(0x9ed52907, 0x7096966d), + TOBN(0x1c62f356, 0x208552bb), + TOBN(0x83655d23, 0xdca3ad96), + TOBN(0x69163fa8, 0xfd24cf5f), + TOBN(0x98da4836, 0x1c55d39a), + TOBN(0xc2007cb8, 0xa163bf05), + TOBN(0x49286651, 0xece45b3d), + TOBN(0xae9f2411, 0x7c4b1fe6), + TOBN(0xee386bfb, 0x5a899fa5), + TOBN(0x0bff5cb6, 0xf406b7ed), + TOBN(0xf44c42e9, 0xa637ed6b), + TOBN(0xe485b576, 0x625e7ec6), + TOBN(0x4fe1356d, 0x6d51c245), + TOBN(0x302b0a6d, 0xf25f1437), + TOBN(0xef9519b3, 0xcd3a431b), + TOBN(0x514a0879, 0x8e3404dd), + TOBN(0x020bbea6, 0x3b139b22), + TOBN(0x29024e08, 0x8a67cc74), + TOBN(0xc4c6628b, 0x80dc1cd1), + TOBN(0xc90fdaa2, 0x2168c234), + TOBN(0xffffffff, 0xffffffff), + }; + return get_params(ret, kWords, OPENSSL_ARRAY_SIZE(kWords)); +} + +BIGNUM* BN_get_rfc3526_prime_4096(BIGNUM* ret) +{ + static const BN_ULONG kWords[] = { + TOBN(0xffffffff, 0xffffffff), + TOBN(0x4df435c9, 0x34063199), + TOBN(0x86ffb7dc, 0x90a6c08f), + TOBN(0x93b4ea98, 0x8d8fddc1), + TOBN(0xd0069127, 0xd5b05aa9), + TOBN(0xb81bdd76, 0x2170481c), + TOBN(0x1f612970, 0xcee2d7af), + TOBN(0x233ba186, 0x515be7ed), + TOBN(0x99b2964f, 0xa090c3a2), + TOBN(0x287c5947, 0x4e6bc05d), + TOBN(0x2e8efc14, 0x1fbecaa6), + TOBN(0xdbbbc2db, 0x04de8ef9), + TOBN(0x2583e9ca, 0x2ad44ce8), + TOBN(0x1a946834, 0xb6150bda), + TOBN(0x99c32718, 0x6af4e23c), + TOBN(0x88719a10, 0xbdba5b26), + TOBN(0x1a723c12, 0xa787e6d7), + TOBN(0x4b82d120, 0xa9210801), + TOBN(0x43db5bfc, 0xe0fd108e), + TOBN(0x08e24fa0, 0x74e5ab31), + TOBN(0x770988c0, 0xbad946e2), + TOBN(0xbbe11757, 0x7a615d6c), + TOBN(0x521f2b18, 0x177b200c), + TOBN(0xd8760273, 0x3ec86a64), + TOBN(0xf12ffa06, 0xd98a0864), + TOBN(0xcee3d226, 0x1ad2ee6b), + TOBN(0x1e8c94e0, 0x4a25619d), + TOBN(0xabf5ae8c, 0xdb0933d7), + TOBN(0xb3970f85, 0xa6e1e4c7), + TOBN(0x8aea7157, 0x5d060c7d), + TOBN(0xecfb8504, 0x58dbef0a), + TOBN(0xa85521ab, 0xdf1cba64), + TOBN(0xad33170d, 0x04507a33), + TOBN(0x15728e5a, 0x8aaac42d), + TOBN(0x15d22618, 0x98fa0510), + TOBN(0x3995497c, 0xea956ae5), + TOBN(0xde2bcbf6, 0x95581718), + TOBN(0xb5c55df0, 0x6f4c52c9), + TOBN(0x9b2783a2, 0xec07a28f), + TOBN(0xe39e772c, 0x180e8603), + TOBN(0x32905e46, 0x2e36ce3b), + TOBN(0xf1746c08, 0xca18217c), + TOBN(0x670c354e, 0x4abc9804), + TOBN(0x9ed52907, 0x7096966d), + TOBN(0x1c62f356, 0x208552bb), + TOBN(0x83655d23, 0xdca3ad96), + TOBN(0x69163fa8, 0xfd24cf5f), + TOBN(0x98da4836, 0x1c55d39a), + TOBN(0xc2007cb8, 0xa163bf05), + TOBN(0x49286651, 0xece45b3d), + TOBN(0xae9f2411, 0x7c4b1fe6), + TOBN(0xee386bfb, 0x5a899fa5), + TOBN(0x0bff5cb6, 0xf406b7ed), + TOBN(0xf44c42e9, 0xa637ed6b), + TOBN(0xe485b576, 0x625e7ec6), + TOBN(0x4fe1356d, 0x6d51c245), + TOBN(0x302b0a6d, 0xf25f1437), + TOBN(0xef9519b3, 0xcd3a431b), + TOBN(0x514a0879, 0x8e3404dd), + TOBN(0x020bbea6, 0x3b139b22), + TOBN(0x29024e08, 0x8a67cc74), + TOBN(0xc4c6628b, 0x80dc1cd1), + TOBN(0xc90fdaa2, 0x2168c234), + TOBN(0xffffffff, 0xffffffff), + }; + return get_params(ret, kWords, OPENSSL_ARRAY_SIZE(kWords)); +} + +BIGNUM* BN_get_rfc3526_prime_6144(BIGNUM* ret) +{ + static const BN_ULONG kWords[] = { + TOBN(0xffffffff, 0xffffffff), + TOBN(0xe694f91e, 0x6dcc4024), + TOBN(0x12bf2d5b, 0x0b7474d6), + TOBN(0x043e8f66, 0x3f4860ee), + TOBN(0x387fe8d7, 0x6e3c0468), + TOBN(0xda56c9ec, 0x2ef29632), + TOBN(0xeb19ccb1, 0xa313d55c), + TOBN(0xf550aa3d, 0x8a1fbff0), + TOBN(0x06a1d58b, 0xb7c5da76), + TOBN(0xa79715ee, 0xf29be328), + TOBN(0x14cc5ed2, 0x0f8037e0), + TOBN(0xcc8f6d7e, 0xbf48e1d8), + TOBN(0x4bd407b2, 0x2b4154aa), + TOBN(0x0f1d45b7, 0xff585ac5), + TOBN(0x23a97a7e, 0x36cc88be), + TOBN(0x59e7c97f, 0xbec7e8f3), + TOBN(0xb5a84031, 0x900b1c9e), + TOBN(0xd55e702f, 0x46980c82), + TOBN(0xf482d7ce, 0x6e74fef6), + TOBN(0xf032ea15, 0xd1721d03), + TOBN(0x5983ca01, 0xc64b92ec), + TOBN(0x6fb8f401, 0x378cd2bf), + TOBN(0x33205151, 0x2bd7af42), + TOBN(0xdb7f1447, 0xe6cc254b), + TOBN(0x44ce6cba, 0xced4bb1b), + TOBN(0xda3edbeb, 0xcf9b14ed), + TOBN(0x179727b0, 0x865a8918), + TOBN(0xb06a53ed, 0x9027d831), + TOBN(0xe5db382f, 0x413001ae), + TOBN(0xf8ff9406, 0xad9e530e), + TOBN(0xc9751e76, 0x3dba37bd), + TOBN(0xc1d4dcb2, 0x602646de), + TOBN(0x36c3fab4, 0xd27c7026), + TOBN(0x4df435c9, 0x34028492), + TOBN(0x86ffb7dc, 0x90a6c08f), + TOBN(0x93b4ea98, 0x8d8fddc1), + TOBN(0xd0069127, 0xd5b05aa9), + TOBN(0xb81bdd76, 0x2170481c), + TOBN(0x1f612970, 0xcee2d7af), + TOBN(0x233ba186, 0x515be7ed), + TOBN(0x99b2964f, 0xa090c3a2), + TOBN(0x287c5947, 0x4e6bc05d), + TOBN(0x2e8efc14, 0x1fbecaa6), + TOBN(0xdbbbc2db, 0x04de8ef9), + TOBN(0x2583e9ca, 0x2ad44ce8), + TOBN(0x1a946834, 0xb6150bda), + TOBN(0x99c32718, 0x6af4e23c), + TOBN(0x88719a10, 0xbdba5b26), + TOBN(0x1a723c12, 0xa787e6d7), + TOBN(0x4b82d120, 0xa9210801), + TOBN(0x43db5bfc, 0xe0fd108e), + TOBN(0x08e24fa0, 0x74e5ab31), + TOBN(0x770988c0, 0xbad946e2), + TOBN(0xbbe11757, 0x7a615d6c), + TOBN(0x521f2b18, 0x177b200c), + TOBN(0xd8760273, 0x3ec86a64), + TOBN(0xf12ffa06, 0xd98a0864), + TOBN(0xcee3d226, 0x1ad2ee6b), + TOBN(0x1e8c94e0, 0x4a25619d), + TOBN(0xabf5ae8c, 0xdb0933d7), + TOBN(0xb3970f85, 0xa6e1e4c7), + TOBN(0x8aea7157, 0x5d060c7d), + TOBN(0xecfb8504, 0x58dbef0a), + TOBN(0xa85521ab, 0xdf1cba64), + TOBN(0xad33170d, 0x04507a33), + TOBN(0x15728e5a, 0x8aaac42d), + TOBN(0x15d22618, 0x98fa0510), + TOBN(0x3995497c, 0xea956ae5), + TOBN(0xde2bcbf6, 0x95581718), + TOBN(0xb5c55df0, 0x6f4c52c9), + TOBN(0x9b2783a2, 0xec07a28f), + TOBN(0xe39e772c, 0x180e8603), + TOBN(0x32905e46, 0x2e36ce3b), + TOBN(0xf1746c08, 0xca18217c), + TOBN(0x670c354e, 0x4abc9804), + TOBN(0x9ed52907, 0x7096966d), + TOBN(0x1c62f356, 0x208552bb), + TOBN(0x83655d23, 0xdca3ad96), + TOBN(0x69163fa8, 0xfd24cf5f), + TOBN(0x98da4836, 0x1c55d39a), + TOBN(0xc2007cb8, 0xa163bf05), + TOBN(0x49286651, 0xece45b3d), + TOBN(0xae9f2411, 0x7c4b1fe6), + TOBN(0xee386bfb, 0x5a899fa5), + TOBN(0x0bff5cb6, 0xf406b7ed), + TOBN(0xf44c42e9, 0xa637ed6b), + TOBN(0xe485b576, 0x625e7ec6), + TOBN(0x4fe1356d, 0x6d51c245), + TOBN(0x302b0a6d, 0xf25f1437), + TOBN(0xef9519b3, 0xcd3a431b), + TOBN(0x514a0879, 0x8e3404dd), + TOBN(0x020bbea6, 0x3b139b22), + TOBN(0x29024e08, 0x8a67cc74), + TOBN(0xc4c6628b, 0x80dc1cd1), + TOBN(0xc90fdaa2, 0x2168c234), + TOBN(0xffffffff, 0xffffffff), + }; + return get_params(ret, kWords, OPENSSL_ARRAY_SIZE(kWords)); +} + +BIGNUM* BN_get_rfc3526_prime_8192(BIGNUM* ret) +{ + static const BN_ULONG kWords[] = { + TOBN(0xffffffff, 0xffffffff), + TOBN(0x60c980dd, 0x98edd3df), + TOBN(0xc81f56e8, 0x80b96e71), + TOBN(0x9e3050e2, 0x765694df), + TOBN(0x9558e447, 0x5677e9aa), + TOBN(0xc9190da6, 0xfc026e47), + TOBN(0x889a002e, 0xd5ee382b), + TOBN(0x4009438b, 0x481c6cd7), + TOBN(0x359046f4, 0xeb879f92), + TOBN(0xfaf36bc3, 0x1ecfa268), + TOBN(0xb1d510bd, 0x7ee74d73), + TOBN(0xf9ab4819, 0x5ded7ea1), + TOBN(0x64f31cc5, 0x0846851d), + TOBN(0x4597e899, 0xa0255dc1), + TOBN(0xdf310ee0, 0x74ab6a36), + TOBN(0x6d2a13f8, 0x3f44f82d), + TOBN(0x062b3cf5, 0xb3a278a6), + TOBN(0x79683303, 0xed5bdd3a), + TOBN(0xfa9d4b7f, 0xa2c087e8), + TOBN(0x4bcbc886, 0x2f8385dd), + TOBN(0x3473fc64, 0x6cea306b), + TOBN(0x13eb57a8, 0x1a23f0c7), + TOBN(0x22222e04, 0xa4037c07), + TOBN(0xe3fdb8be, 0xfc848ad9), + TOBN(0x238f16cb, 0xe39d652d), + TOBN(0x3423b474, 0x2bf1c978), + TOBN(0x3aab639c, 0x5ae4f568), + TOBN(0x2576f693, 0x6ba42466), + TOBN(0x741fa7bf, 0x8afc47ed), + TOBN(0x3bc832b6, 0x8d9dd300), + TOBN(0xd8bec4d0, 0x73b931ba), + TOBN(0x38777cb6, 0xa932df8c), + TOBN(0x74a3926f, 0x12fee5e4), + TOBN(0xe694f91e, 0x6dbe1159), + TOBN(0x12bf2d5b, 0x0b7474d6), + TOBN(0x043e8f66, 0x3f4860ee), + TOBN(0x387fe8d7, 0x6e3c0468), + TOBN(0xda56c9ec, 0x2ef29632), + TOBN(0xeb19ccb1, 0xa313d55c), + TOBN(0xf550aa3d, 0x8a1fbff0), + TOBN(0x06a1d58b, 0xb7c5da76), + TOBN(0xa79715ee, 0xf29be328), + TOBN(0x14cc5ed2, 0x0f8037e0), + TOBN(0xcc8f6d7e, 0xbf48e1d8), + TOBN(0x4bd407b2, 0x2b4154aa), + TOBN(0x0f1d45b7, 0xff585ac5), + TOBN(0x23a97a7e, 0x36cc88be), + TOBN(0x59e7c97f, 0xbec7e8f3), + TOBN(0xb5a84031, 0x900b1c9e), + TOBN(0xd55e702f, 0x46980c82), + TOBN(0xf482d7ce, 0x6e74fef6), + TOBN(0xf032ea15, 0xd1721d03), + TOBN(0x5983ca01, 0xc64b92ec), + TOBN(0x6fb8f401, 0x378cd2bf), + TOBN(0x33205151, 0x2bd7af42), + TOBN(0xdb7f1447, 0xe6cc254b), + TOBN(0x44ce6cba, 0xced4bb1b), + TOBN(0xda3edbeb, 0xcf9b14ed), + TOBN(0x179727b0, 0x865a8918), + TOBN(0xb06a53ed, 0x9027d831), + TOBN(0xe5db382f, 0x413001ae), + TOBN(0xf8ff9406, 0xad9e530e), + TOBN(0xc9751e76, 0x3dba37bd), + TOBN(0xc1d4dcb2, 0x602646de), + TOBN(0x36c3fab4, 0xd27c7026), + TOBN(0x4df435c9, 0x34028492), + TOBN(0x86ffb7dc, 0x90a6c08f), + TOBN(0x93b4ea98, 0x8d8fddc1), + TOBN(0xd0069127, 0xd5b05aa9), + TOBN(0xb81bdd76, 0x2170481c), + TOBN(0x1f612970, 0xcee2d7af), + TOBN(0x233ba186, 0x515be7ed), + TOBN(0x99b2964f, 0xa090c3a2), + TOBN(0x287c5947, 0x4e6bc05d), + TOBN(0x2e8efc14, 0x1fbecaa6), + TOBN(0xdbbbc2db, 0x04de8ef9), + TOBN(0x2583e9ca, 0x2ad44ce8), + TOBN(0x1a946834, 0xb6150bda), + TOBN(0x99c32718, 0x6af4e23c), + TOBN(0x88719a10, 0xbdba5b26), + TOBN(0x1a723c12, 0xa787e6d7), + TOBN(0x4b82d120, 0xa9210801), + TOBN(0x43db5bfc, 0xe0fd108e), + TOBN(0x08e24fa0, 0x74e5ab31), + TOBN(0x770988c0, 0xbad946e2), + TOBN(0xbbe11757, 0x7a615d6c), + TOBN(0x521f2b18, 0x177b200c), + TOBN(0xd8760273, 0x3ec86a64), + TOBN(0xf12ffa06, 0xd98a0864), + TOBN(0xcee3d226, 0x1ad2ee6b), + TOBN(0x1e8c94e0, 0x4a25619d), + TOBN(0xabf5ae8c, 0xdb0933d7), + TOBN(0xb3970f85, 0xa6e1e4c7), + TOBN(0x8aea7157, 0x5d060c7d), + TOBN(0xecfb8504, 0x58dbef0a), + TOBN(0xa85521ab, 0xdf1cba64), + TOBN(0xad33170d, 0x04507a33), + TOBN(0x15728e5a, 0x8aaac42d), + TOBN(0x15d22618, 0x98fa0510), + TOBN(0x3995497c, 0xea956ae5), + TOBN(0xde2bcbf6, 0x95581718), + TOBN(0xb5c55df0, 0x6f4c52c9), + TOBN(0x9b2783a2, 0xec07a28f), + TOBN(0xe39e772c, 0x180e8603), + TOBN(0x32905e46, 0x2e36ce3b), + TOBN(0xf1746c08, 0xca18217c), + TOBN(0x670c354e, 0x4abc9804), + TOBN(0x9ed52907, 0x7096966d), + TOBN(0x1c62f356, 0x208552bb), + TOBN(0x83655d23, 0xdca3ad96), + TOBN(0x69163fa8, 0xfd24cf5f), + TOBN(0x98da4836, 0x1c55d39a), + TOBN(0xc2007cb8, 0xa163bf05), + TOBN(0x49286651, 0xece45b3d), + TOBN(0xae9f2411, 0x7c4b1fe6), + TOBN(0xee386bfb, 0x5a899fa5), + TOBN(0x0bff5cb6, 0xf406b7ed), + TOBN(0xf44c42e9, 0xa637ed6b), + TOBN(0xe485b576, 0x625e7ec6), + TOBN(0x4fe1356d, 0x6d51c245), + TOBN(0x302b0a6d, 0xf25f1437), + TOBN(0xef9519b3, 0xcd3a431b), + TOBN(0x514a0879, 0x8e3404dd), + TOBN(0x020bbea6, 0x3b139b22), + TOBN(0x29024e08, 0x8a67cc74), + TOBN(0xc4c6628b, 0x80dc1cd1), + TOBN(0xc90fdaa2, 0x2168c234), + TOBN(0xffffffff, 0xffffffff), + }; + return get_params(ret, kWords, OPENSSL_ARRAY_SIZE(kWords)); +} +#endif diff --git a/src/bun.js/bindings/ncrypto.h b/src/bun.js/bindings/ncrypto.h index d580e9af30..2d519338a9 100644 --- a/src/bun.js/bindings/ncrypto.h +++ b/src/bun.js/bindings/ncrypto.h @@ -18,11 +18,10 @@ #include #include #include -#include #include #include #include -#include + #include #include #include @@ -51,6 +50,20 @@ #define NCRYPTO_MUST_USE_RESULT #endif +#ifdef OPENSSL_IS_BORINGSSL +// Boringssl has opted to use size_t for some size related +// APIs while Openssl is still using ints +using OPENSSL_SIZE_T = size_t; +#else +using OPENSSL_SIZE_T = int; +#endif + +#ifdef OPENSSL_IS_BORINGSSL +#ifdef NCRYPTO_BSSL_NEEDS_DH_PRIMES +#include "dh-primes.h" +#endif // NCRYPTO_BSSL_NEEDS_DH_PRIMES +#endif // OPENSSL_IS_BORINGSSL + namespace ncrypto { // ============================================================================ @@ -60,7 +73,8 @@ namespace ncrypto { #define NCRYPTO_STR(x) #x #define NCRYPTO_REQUIRE(EXPR) ASSERT_WITH_MESSAGE(EXPR, "Assertion failed") #define NCRYPTO_FAIL(MESSAGE) ASSERT_WITH_MESSAGE(false, MESSAGE) -#define NCRYPTO_ASSERT_EQUAL(LHS, RHS, MESSAGE) ASSERT_WITH_MESSAGE(LHS == RHS, MESSAGE) +#define NCRYPTO_ASSERT_EQUAL(LHS, RHS, MESSAGE) \ + ASSERT_WITH_MESSAGE(LHS == RHS, MESSAGE) #define NCRYPTO_ASSERT_TRUE(COND) ASSERT_WITH_MESSAGE(COND, NCRYPTO_STR(COND)) #else #define NCRYPTO_FAIL(MESSAGE) @@ -198,25 +212,29 @@ struct FunctionDeleter { template using DeleteFnPtr = typename FunctionDeleter::Pointer; -using BignumCtxPointer = DeleteFnPtr; -using BignumGenCallbackPointer = DeleteFnPtr; -using DSAPointer = DeleteFnPtr; -using DSASigPointer = DeleteFnPtr; -// using ECDSASigPointer = DeleteFnPtr; -// using ECPointer = DeleteFnPtr; -// using ECGroupPointer = DeleteFnPtr; -// using ECKeyPointer = DeleteFnPtr; -// using ECPointPointer = DeleteFnPtr; -using EVPKeyCtxPointer = DeleteFnPtr; -using EVPMDCtxPointer = DeleteFnPtr; -using HMACCtxPointer = DeleteFnPtr; -using NetscapeSPKIPointer = DeleteFnPtr; using PKCS8Pointer = DeleteFnPtr; using RSAPointer = DeleteFnPtr; using SSLSessionPointer = DeleteFnPtr; +class BIOPointer; +class BignumPointer; class CipherCtxPointer; +class DataPointer; +class DHPointer; class ECKeyPointer; +class EVPKeyPointer; +class EVPMDCtxPointer; +class SSLCtxPointer; +class SSLPointer; +class X509View; +class X509Pointer; +class ECDSASigPointer; +class ECGroupPointer; +class ECPointPointer; +class ECKeyPointer; +class Dsa; +class Rsa; +class Ec; struct StackOfXASN1Deleter { void operator()(STACK_OF(ASN1_OBJECT) * p) const @@ -233,6 +251,9 @@ struct Buffer { size_t len = 0; }; +DataPointer hashDigest(const Buffer& data, + const EVP_MD* md); + class Cipher final { public: Cipher() = default; @@ -258,24 +279,222 @@ public: int getIvLength() const; int getKeyLength() const; int getBlockSize() const; - std::string_view getModeLabel() const; - std::string_view getName() const; + WTF::ASCIILiteral getModeLabel() const; + WTF::String getName() const; bool isSupportedAuthenticatedMode() const; - static const Cipher FromName(const char* name); + static const Cipher FromName(WTF::StringView name); static const Cipher FromNid(int nid); static const Cipher FromCtx(const CipherCtxPointer& ctx); + struct CipherParams { + int padding; + const EVP_MD* digest; + const Buffer label; + }; + + static DataPointer encrypt(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in); + static DataPointer decrypt(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in); + + static DataPointer sign(const EVPKeyPointer& key, const CipherParams& params, + const Buffer in); + + static DataPointer recover(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in); + + static constexpr bool IsValidGCMTagLength(unsigned int tag_len) + { + return tag_len == 4 || tag_len == 8 || (tag_len >= 12 && tag_len <= 16); + } + private: const EVP_CIPHER* cipher_ = nullptr; }; +// ============================================================================ +// DSA + +class Dsa final { +public: + Dsa(); + Dsa(OSSL3_CONST DSA* dsa); + NCRYPTO_DISALLOW_COPY_AND_MOVE(Dsa) + + inline operator bool() const { return dsa_ != nullptr; } + inline operator OSSL3_CONST DSA*() const { return dsa_; } + + const BIGNUM* getP() const; + const BIGNUM* getQ() const; + size_t getModulusLength() const; + size_t getDivisorLength() const; + +private: + OSSL3_CONST DSA* dsa_; +}; + +class BignumPointer final { + WTF_MAKE_TZONE_ALLOCATED(BignumPointer); + +public: + BignumPointer() = default; + explicit BignumPointer(BIGNUM* bignum); + explicit BignumPointer(const unsigned char* data, size_t len); + BignumPointer(BignumPointer&& other) noexcept; + BignumPointer& operator=(BignumPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(BignumPointer) + ~BignumPointer(); + + int operator<=>(const BignumPointer& other) const noexcept; + int operator<=>(const BIGNUM* other) const noexcept; + inline operator bool() const { return bn_ != nullptr; } + inline BIGNUM* get() const noexcept { return bn_.get(); } + void reset(BIGNUM* bn = nullptr); + void reset(const unsigned char* data, size_t len); + BIGNUM* release(); + + bool isZero() const; + bool isOne() const; + + bool setWord(unsigned long w); // NOLINT(runtime/int) + unsigned long getWord() const; // NOLINT(runtime/int) + + size_t byteLength() const; + size_t bitLength() const; + + DataPointer toHex() const; + DataPointer encode() const; + DataPointer encodePadded(size_t size) const; + size_t encodeInto(unsigned char* out) const; + size_t encodePaddedInto(unsigned char* out, size_t size) const; + + using PrimeCheckCallback = std::function; + int isPrime(int checks, + PrimeCheckCallback cb = defaultPrimeCheckCallback) const; + struct PrimeConfig { + int bits; + bool safe = false; + const BignumPointer& add; + const BignumPointer& rem; + }; + + static BignumPointer NewPrime( + const PrimeConfig& params, + PrimeCheckCallback cb = defaultPrimeCheckCallback); + + bool generate(const PrimeConfig& params, + PrimeCheckCallback cb = defaultPrimeCheckCallback) const; + + static BignumPointer New(); + static BignumPointer NewSecure(); + static BignumPointer NewSub(const BignumPointer& a, const BignumPointer& b); + static BignumPointer NewLShift(size_t length); + + static DataPointer Encode(const BIGNUM* bn); + static DataPointer EncodePadded(const BIGNUM* bn, size_t size); + static size_t EncodePaddedInto(const BIGNUM* bn, unsigned char* out, + size_t size); + static int GetBitCount(const BIGNUM* bn); + static int GetByteCount(const BIGNUM* bn); + static unsigned long GetWord(const BIGNUM* bn); // NOLINT(runtime/int) + static const BIGNUM* One(); + + BignumPointer clone(); + +private: + DeleteFnPtr bn_; + + static bool defaultPrimeCheckCallback(int, int) { return 1; } +}; + +class Rsa final { +public: + Rsa(); + Rsa(OSSL3_CONST RSA* rsa); + NCRYPTO_DISALLOW_COPY_AND_MOVE(Rsa) + + inline operator bool() const { return rsa_ != nullptr; } + inline operator OSSL3_CONST RSA*() const { return rsa_; } + + struct PublicKey { + const BIGNUM* n; + const BIGNUM* e; + const BIGNUM* d; + }; + struct PrivateKey { + const BIGNUM* p; + const BIGNUM* q; + const BIGNUM* dp; + const BIGNUM* dq; + const BIGNUM* qi; + }; + struct PssParams { + WTF::StringView digest = "sha1"_s; + std::optional mgf1_digest = "sha1"_s; + int64_t salt_length = 20; + }; + + const PublicKey getPublicKey() const; + const PrivateKey getPrivateKey() const; + const std::optional getPssParams() const; + + bool setPublicKey(BignumPointer&& n, BignumPointer&& e); + bool setPrivateKey(BignumPointer&& d, BignumPointer&& q, BignumPointer&& p, + BignumPointer&& dp, BignumPointer&& dq, + BignumPointer&& qi); + + using CipherParams = Cipher::CipherParams; + + static DataPointer encrypt(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in); + static DataPointer decrypt(const EVPKeyPointer& key, + const CipherParams& params, + const Buffer in); + +private: + OSSL3_CONST RSA* rsa_; +}; + +class Ec final { +public: + Ec(); + Ec(OSSL3_CONST EC_KEY* key); + NCRYPTO_DISALLOW_COPY_AND_MOVE(Ec) + + const EC_GROUP* getGroup() const; + int getCurve() const; + uint32_t getDegree() const; + WTF::String getCurveName() const; + const EC_POINT* getPublicKey() const; + const BIGNUM* getPrivateKey() const; + + inline operator bool() const { return ec_ != nullptr; } + inline operator OSSL3_CONST EC_KEY*() const { return ec_; } + + inline const BignumPointer& getX() const { return x_; } + inline const BignumPointer& getY() const { return y_; } + inline const BignumPointer& getD() const { return d_; } + +private: + OSSL3_CONST EC_KEY* ec_ = nullptr; + // Affine coordinates for the EC_KEY. + BignumPointer x_; + BignumPointer y_; + BignumPointer d_; +}; + // A managed pointer to a buffer of data. When destroyed the underlying // buffer will be freed. class DataPointer final { public: static DataPointer Alloc(size_t len); + static DataPointer Copy(const Buffer& buffer); DataPointer() = default; explicit DataPointer(void* data, size_t len); @@ -287,11 +506,22 @@ public: inline bool operator==(std::nullptr_t) noexcept { return data_ == nullptr; } inline operator bool() const { return data_ != nullptr; } - inline void* get() const noexcept { return data_; } + + template + inline T* get() const noexcept + { + return static_cast(data_); + } + inline size_t size() const noexcept { return len_; } void reset(void* data = nullptr, size_t len = 0); void reset(const Buffer& buffer); + // Sets the underlying data buffer to all zeros. + void zero(); + + DataPointer resize(size_t len); + // Releases ownership of the underlying data buffer. It is the caller's // responsibility to ensure the buffer is appropriately freed. Buffer release(); @@ -360,7 +590,6 @@ public: bool resetBio() const; static int Write(BIOPointer* bio, WTF::StringView message); - static int Write(BIOPointer* bio, std::span message); template static void Printf(BIOPointer* bio, const char* format, Args... args) @@ -373,78 +602,6 @@ private: mutable DeleteFnPtr bio_; }; -class BignumPointer final { -public: - BignumPointer() = default; - explicit BignumPointer(BIGNUM* bignum); - explicit BignumPointer(const unsigned char* data, size_t len); - BignumPointer(BignumPointer&& other) noexcept; - BignumPointer& operator=(BignumPointer&& other) noexcept; - NCRYPTO_DISALLOW_COPY(BignumPointer) - ~BignumPointer(); - - int operator<=>(const BignumPointer& other) const noexcept; - int operator<=>(const BIGNUM* other) const noexcept; - inline operator bool() const { return bn_ != nullptr; } - inline BIGNUM* get() const noexcept { return bn_.get(); } - void reset(BIGNUM* bn = nullptr); - void reset(const unsigned char* data, size_t len); - BIGNUM* release(); - - bool isZero() const; - bool isOne() const; - - bool setWord(unsigned long w); // NOLINT(runtime/int) - unsigned long getWord() const; // NOLINT(runtime/int) - - size_t byteLength() const; - - DataPointer toHex() const; - DataPointer encode() const; - DataPointer encodePadded(size_t size) const; - size_t encodeInto(unsigned char* out) const; - size_t encodePaddedInto(unsigned char* out, size_t size) const; - - using PrimeCheckCallback = std::function; - int isPrime(int checks, - PrimeCheckCallback cb = defaultPrimeCheckCallback) const; - struct PrimeConfig { - int bits; - bool safe = false; - const BignumPointer& add; - const BignumPointer& rem; - }; - - static BignumPointer NewPrime( - const PrimeConfig& params, - PrimeCheckCallback cb = defaultPrimeCheckCallback); - - bool generate(const PrimeConfig& params, - PrimeCheckCallback cb = defaultPrimeCheckCallback) const; - - static BignumPointer New(); - static BignumPointer NewSecure(); - static BignumPointer NewSub(const BignumPointer& a, const BignumPointer& b); - static BignumPointer NewLShift(size_t length); - - static DataPointer Encode(const BIGNUM* bn); - static DataPointer EncodePadded(const BIGNUM* bn, size_t size); - static size_t EncodePaddedInto(const BIGNUM* bn, - unsigned char* out, - size_t size); - static int GetBitCount(const BIGNUM* bn); - static int GetByteCount(const BIGNUM* bn); - static unsigned long GetWord(const BIGNUM* bn); // NOLINT(runtime/int) - static const BIGNUM* One(); - - BignumPointer clone(); - -private: - DeleteFnPtr bn_; - - static bool defaultPrimeCheckCallback(int, int) { return 1; } -}; - class CipherCtxPointer final { WTF_MAKE_TZONE_ALLOCATED(CipherCtxPointer); @@ -474,8 +631,7 @@ public: bool setAeadTag(const Buffer& tag); bool setAeadTagLength(size_t length); bool setPadding(bool padding); - bool init(const Cipher& cipher, - bool encrypt, + bool init(const Cipher& cipher, bool encrypt, const unsigned char* key = nullptr, const unsigned char* iv = nullptr); @@ -483,16 +639,84 @@ public: int getMode() const; int getNid() const; - bool update(const Buffer& in, - unsigned char* out, - int* out_len, - bool finalize = false); + bool update(const Buffer& in, unsigned char* out, + int* out_len, bool finalize = false); bool getAeadTag(size_t len, unsigned char* out); private: DeleteFnPtr ctx_; }; +class EVPKeyCtxPointer final { + WTF_MAKE_TZONE_ALLOCATED(EVPKeyCtxPointer); + +public: + EVPKeyCtxPointer(); + explicit EVPKeyCtxPointer(EVP_PKEY_CTX* ctx); + EVPKeyCtxPointer(EVPKeyCtxPointer&& other) noexcept; + EVPKeyCtxPointer& operator=(EVPKeyCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(EVPKeyCtxPointer) + ~EVPKeyCtxPointer(); + + inline bool operator==(std::nullptr_t) const noexcept + { + return ctx_ == nullptr; + } + inline operator bool() const { return ctx_ != nullptr; } + inline EVP_PKEY_CTX* get() const { return ctx_.get(); } + void reset(EVP_PKEY_CTX* ctx = nullptr); + EVP_PKEY_CTX* release(); + + bool initForDerive(const EVPKeyPointer& peer); + DataPointer derive() const; + + bool initForParamgen(); + bool setDhParameters(int prime_size, uint32_t generator); + bool setDsaParameters(uint32_t bits, std::optional q_bits); + bool setEcParameters(int curve, int encoding); + + bool setRsaOaepMd(const EVP_MD* md); + bool setRsaMgf1Md(const EVP_MD* md); + bool setRsaPadding(int padding); + bool setRsaKeygenPubExp(BignumPointer&& e); + bool setRsaKeygenBits(int bits); + bool setRsaPssKeygenMd(const EVP_MD* md); + bool setRsaPssKeygenMgf1Md(const EVP_MD* md); + bool setRsaPssSaltlen(int salt_len); + bool setRsaImplicitRejection(); + bool setRsaOaepLabel(DataPointer&& data); + + bool setSignatureMd(const EVPMDCtxPointer& md); + + bool publicCheck() const; + bool privateCheck() const; + + bool verify(const Buffer& sig, + const Buffer& data); + DataPointer sign(const Buffer& data); + bool signInto(const Buffer& data, + Buffer* sig); + + static constexpr int kDefaultRsaExponent = 0x10001; + + static bool setRsaPadding(EVP_PKEY_CTX* ctx, int padding, + std::optional salt_len = std::nullopt); + + EVPKeyPointer paramgen() const; + + bool initForEncrypt(); + bool initForDecrypt(); + bool initForKeygen(); + int initForVerify(); + int initForSign(); + + static EVPKeyCtxPointer New(const EVPKeyPointer& key); + static EVPKeyCtxPointer NewFromID(int id); + +private: + DeleteFnPtr ctx_; +}; + class EVPKeyPointer final { WTF_MAKE_TZONE_ALLOCATED(EVPKeyPointer); @@ -502,6 +726,8 @@ public: const Buffer& data); static EVPKeyPointer NewRawPrivate(int id, const Buffer& data); + static EVPKeyPointer NewDH(DHPointer&& dh); + static EVPKeyPointer NewRSA(RSAPointer&& rsa); enum class PKEncodingType { // RSAPublicKey / RSAPrivateKey according to PKCS#1. @@ -530,8 +756,7 @@ public: PKFormatType format = PKFormatType::DER; PKEncodingType type = PKEncodingType::PKCS8; AsymmetricKeyEncodingConfig() = default; - AsymmetricKeyEncodingConfig(bool output_key_object, - PKFormatType format, + AsymmetricKeyEncodingConfig(bool output_key_object, PKFormatType format, PKEncodingType type); AsymmetricKeyEncodingConfig(const AsymmetricKeyEncodingConfig&) = default; AsymmetricKeyEncodingConfig& operator=(const AsymmetricKeyEncodingConfig&) = default; @@ -542,8 +767,7 @@ public: const EVP_CIPHER* cipher = nullptr; std::optional passphrase = std::nullopt; PrivateKeyEncodingConfig() = default; - PrivateKeyEncodingConfig(bool output_key_object, - PKFormatType format, + PrivateKeyEncodingConfig(bool output_key_object, PKFormatType format, PKEncodingType type) : AsymmetricKeyEncodingConfig(output_key_object, format, type) { @@ -606,6 +830,19 @@ public: static bool IsRSAPrivateKey(const Buffer& buffer); + std::optional getBytesOfRS() const; + int getDefaultSignPadding() const; + operator Rsa() const; + operator Dsa() const; + operator Ec() const; + + bool isRsaVariant() const; + bool isOneShotVariant() const; + bool isSigVariant() const; + bool validateDsaParameters() const; + + EVPKeyPointer clone() const; + private: DeleteFnPtr pkey_; }; @@ -625,9 +862,9 @@ public: static BignumPointer GetStandardGenerator(); static BignumPointer FindGroup( - WTF::StringView name, + const WTF::StringView name, FindGroupOption option = FindGroupOption::NONE); - static DHPointer FromGroup(WTF::StringView name, + static DHPointer FromGroup(const WTF::StringView name, FindGroupOption option = FindGroupOption::NONE); static DHPointer New(BignumPointer&& p, BignumPointer&& g); @@ -654,6 +891,7 @@ public: NOT_SUITABLE_GENERATOR = DH_NOT_SUITABLE_GENERATOR, Q_NOT_PRIME = DH_CHECK_Q_NOT_PRIME, #ifndef OPENSSL_IS_BORINGSSL + // Boringssl does not define the DH_CHECK_INVALID_[Q or J]_VALUE INVALID_Q = DH_CHECK_INVALID_Q_VALUE, INVALID_J = DH_CHECK_INVALID_J_VALUE, #endif @@ -664,10 +902,13 @@ public: enum class CheckPublicKeyResult { NONE, #ifndef OPENSSL_IS_BORINGSSL + // Boringssl does not define DH_R_CHECK_PUBKEY_TOO_SMALL or TOO_LARGE TOO_SMALL = DH_R_CHECK_PUBKEY_TOO_SMALL, TOO_LARGE = DH_R_CHECK_PUBKEY_TOO_LARGE, -#endif + INVALID = DH_R_CHECK_PUBKEY_INVALID, +#else INVALID = DH_R_INVALID_PUBKEY, +#endif CHECK_FAILED = 512, }; // Check to see if the given public key is suitable for this DH instance. @@ -697,9 +938,6 @@ struct StackOfX509Deleter { }; using StackOfX509 = std::unique_ptr; -class X509Pointer; -class X509View; - class SSLCtxPointer final { WTF_MAKE_TZONE_ALLOCATED(SSLCtxPointer); @@ -758,26 +996,69 @@ public: bool setSession(const SSLSessionPointer& session); bool setSniContext(const SSLCtxPointer& ctx) const; - WTF::StringView getClientHelloAlpn() const; - WTF::StringView getClientHelloServerName() const; + const WTF::StringView getClientHelloAlpn() const; + const WTF::StringView getClientHelloServerName() const; - std::optional getServerName() const; + std::optional getServerName() const; X509View getCertificate() const; EVPKeyPointer getPeerTempKey() const; const SSL_CIPHER* getCipher() const; bool isServer() const; + std::optional getCipherName() const; + std::optional getCipherStandardName() const; + std::optional getCipherVersion() const; + std::optional verifyPeerCertificate() const; - void getCiphers(WTF::Function&& cb) const; + void getCiphers(WTF::Function&& cb) const; static SSLPointer New(const SSLCtxPointer& ctx); - static std::optional GetServerName(const SSL* ssl); + static std::optional GetServerName(const SSL* ssl); private: DeleteFnPtr ssl_; }; +class X509Name final { + WTF_MAKE_TZONE_ALLOCATED(X509Name); + +public: + X509Name(); + explicit X509Name(const X509_NAME* name); + NCRYPTO_DISALLOW_COPY_AND_MOVE(X509Name) + + inline operator const X509_NAME*() const { return name_; } + inline operator bool() const { return name_ != nullptr; } + inline const X509_NAME* get() const { return name_; } + inline size_t size() const { return total_; } + + class Iterator final { + public: + Iterator(const X509Name& name, int pos); + Iterator(const Iterator& other) = default; + Iterator(Iterator&& other) = default; + Iterator& operator=(const Iterator& other) = delete; + Iterator& operator=(Iterator&& other) = delete; + Iterator& operator++(); + operator bool() const; + bool operator==(const Iterator& other) const; + bool operator!=(const Iterator& other) const; + std::pair operator*() const; + + private: + const X509Name& name_; + int loc_; + }; + + inline Iterator begin() const { return Iterator(*this, 0); } + inline Iterator end() const { return Iterator(*this, total_); } + +private: + const X509_NAME* name_; + int total_; +}; + class X509View final { public: static X509View From(const SSLPointer& ssl); @@ -802,6 +1083,8 @@ public: BIOPointer toPEM() const; BIOPointer toDER() const; + const X509Name getSubjectName() const; + const X509Name getIssuerName() const; BIOPointer getSubject() const; BIOPointer getSubjectAltName() const; BIOPointer getIssuer() const; @@ -829,11 +1112,18 @@ public: INVALID_NAME, OPERATION_FAILED, }; - CheckMatch checkHost(std::span host, - int flags, + CheckMatch checkHost(const std::span host, int flags, DataPointer* peerName = nullptr) const; - CheckMatch checkEmail(std::span email, int flags) const; - CheckMatch checkIp(std::span ip, int flags) const; + CheckMatch checkEmail(const std::span email, int flags) const; + CheckMatch checkIp(const char* ip, int flags) const; + + using UsageCallback = WTF::Function)>; + bool enumUsages(UsageCallback callback) const; + + template + using KeyCallback = WTF::Function; + bool ifRsa(KeyCallback callback) const; + bool ifEc(KeyCallback callback) const; private: const X509* cert_ = nullptr; @@ -866,8 +1156,8 @@ public: X509View view() const; operator X509View() const { return view(); } - static ASCIILiteral ErrorCode(int32_t err); - static std::optional ErrorReason(int32_t err); + static WTF::ASCIILiteral ErrorCode(int32_t err); + static std::optional ErrorReason(int32_t err); private: DeleteFnPtr cert_; @@ -1001,6 +1291,81 @@ private: DeleteFnPtr key_; }; +class EVPMDCtxPointer final { + WTF_MAKE_TZONE_ALLOCATED(EVPMDCtxPointer); + +public: + EVPMDCtxPointer(); + explicit EVPMDCtxPointer(EVP_MD_CTX* ctx); + EVPMDCtxPointer(EVPMDCtxPointer&& other) noexcept; + EVPMDCtxPointer& operator=(EVPMDCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(EVPMDCtxPointer) + ~EVPMDCtxPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return ctx_ == nullptr; } + inline operator bool() const { return ctx_ != nullptr; } + inline EVP_MD_CTX* get() const { return ctx_.get(); } + inline operator EVP_MD_CTX*() const { return ctx_.get(); } + void reset(EVP_MD_CTX* ctx = nullptr); + EVP_MD_CTX* release(); + + bool digestInit(const EVP_MD* digest); + bool digestUpdate(const Buffer& in); + DataPointer digestFinal(size_t length); + bool digestFinalInto(Buffer* buf); + size_t getExpectedSize(); + + std::optional signInit(const EVPKeyPointer& key, + const EVP_MD* digest); + std::optional verifyInit(const EVPKeyPointer& key, + const EVP_MD* digest); + + DataPointer signOneShot(const Buffer& buf) const; + DataPointer sign(const Buffer& buf) const; + bool verify(const Buffer& buf, + const Buffer& sig) const; + + const EVP_MD* getDigest() const; + size_t getDigestSize() const; + bool hasXofFlag() const; + + bool copyTo(const EVPMDCtxPointer& other) const; + + static EVPMDCtxPointer New(); + +private: + DeleteFnPtr ctx_; +}; + +class HMACCtxPointer final { + WTF_MAKE_TZONE_ALLOCATED(HMACCtxPointer); + +public: + HMACCtxPointer(); + explicit HMACCtxPointer(HMAC_CTX* ctx); + HMACCtxPointer(HMACCtxPointer&& other) noexcept; + HMACCtxPointer& operator=(HMACCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(HMACCtxPointer) + ~HMACCtxPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return ctx_ == nullptr; } + inline operator bool() const { return ctx_ != nullptr; } + inline HMAC_CTX* get() const { return ctx_.get(); } + inline operator HMAC_CTX*() const { return ctx_.get(); } + void reset(HMAC_CTX* ctx = nullptr); + HMAC_CTX* release(); + + bool init(const Buffer& buf, const EVP_MD* md); + bool update(const Buffer& buf); + DataPointer digest(); + bool digestInto(Buffer* buf); + + static HMACCtxPointer New(); + +private: + DeleteFnPtr ctx_; +}; + #ifndef OPENSSL_NO_ENGINE class EnginePointer final { public: @@ -1020,7 +1385,7 @@ public: bool setAsDefault(uint32_t flags, CryptoErrorList* errors = nullptr); bool init(bool finish_on_exit = false); - EVPKeyPointer loadPrivateKey(WTF::StringView key_name); + EVPKeyPointer loadPrivateKey(const WTF::StringView key_name); // Release ownership of the ENGINE* pointer. ENGINE* release(); @@ -1028,7 +1393,7 @@ public: // Retrieve an OpenSSL Engine instance by name. If the name does not // identify a valid named engine, the returned EnginePointer will be // empty. - static EnginePointer getEngineByName(WTF::StringView name, + static EnginePointer getEngineByName(const WTF::StringView name, CryptoErrorList* errors = nullptr); // Call once when initializing OpenSSL at startup for the process. @@ -1068,42 +1433,60 @@ bool SafeX509InfoAccessPrint(const BIOPointer& out, X509_EXTENSION* ext); // ============================================================================ // SPKAC -bool VerifySpkac(const char* input, size_t length); -BIOPointer ExportPublicKey(const char* input, size_t length); +[[deprecated("Use the version that takes a Buffer")]] bool VerifySpkac( + const char* input, size_t length); + +[[deprecated("Use the version that takes a Buffer")]] BIOPointer +ExportPublicKey(const char* input, size_t length); // The caller takes ownership of the returned Buffer -Buffer ExportChallenge(const char* input, size_t length); +[[deprecated("Use the version that takes a Buffer")]] Buffer +ExportChallenge(const char* input, size_t length); + +bool VerifySpkac(const Buffer& buf); +BIOPointer ExportPublicKey(const Buffer& buf); +DataPointer ExportChallenge(const Buffer& buf); // ============================================================================ // KDF -const EVP_MD* getDigestByName(const std::string_view name); +const EVP_MD* getDigestByName(const WTF::StringView name); +const EVP_CIPHER* getCipherByName(const WTF::StringView name); // Verify that the specified HKDF output length is valid for the given digest. // The maximum length for HKDF output for a given digest is 255 times the // hash size for the given digest algorithm. bool checkHkdfLength(const EVP_MD* md, size_t length); -DataPointer hkdf(const EVP_MD* md, - const Buffer& key, +bool extractP1363(const Buffer& buf, unsigned char* dest, + size_t n); + +bool hkdfInfo(const EVP_MD* md, const Buffer& key, const Buffer& info, - const Buffer& salt, - size_t length); + const Buffer& salt, size_t length, + Buffer* out); + +DataPointer hkdf(const EVP_MD* md, const Buffer& key, + const Buffer& info, + const Buffer& salt, size_t length); bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem); -DataPointer scrypt(const Buffer& pass, - const Buffer& salt, - uint64_t N, - uint64_t r, - uint64_t p, - uint64_t maxmem, - size_t length); +bool scryptInto(const Buffer& pass, + const Buffer& salt, uint64_t N, uint64_t r, + uint64_t p, uint64_t maxmem, size_t length, + Buffer* out); -DataPointer pbkdf2(const EVP_MD* md, - const Buffer& pass, - const Buffer& salt, - uint32_t iterations, +DataPointer scrypt(const Buffer& pass, + const Buffer& salt, uint64_t N, + uint64_t r, uint64_t p, uint64_t maxmem, size_t length); + +bool pbkdf2Into(const EVP_MD* md, const Buffer& pass, + const Buffer& salt, uint32_t iterations, + size_t length, Buffer* out); + +DataPointer pbkdf2(const EVP_MD* md, const Buffer& pass, + const Buffer& salt, uint32_t iterations, size_t length); // ============================================================================ diff --git a/src/bun.js/bindings/node/crypto/JSSign.cpp b/src/bun.js/bindings/node/crypto/JSSign.cpp new file mode 100644 index 0000000000..66cc90bd9f --- /dev/null +++ b/src/bun.js/bindings/node/crypto/JSSign.cpp @@ -0,0 +1,916 @@ +#include "JSSign.h" +#include "JavaScriptCore/JSArrayBufferView.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/JSType.h" +#include "ZigGlobalObject.h" +#include "ErrorCode.h" +#include "JSDOMExceptionHandling.h" +#include +#include +#include "JSBufferEncodingType.h" +#include "KeyObject.h" +#include "JSCryptoKey.h" +#include "AsymmetricKeyValue.h" +#include "NodeValidator.h" +#include "JSBuffer.h" +#include "util.h" +#include "BunString.h" +#include "JSVerify.h" +#include "CryptoAlgorithmRegistry.h" +#include "CryptoKeyRSA.h" + +namespace Bun { + +using namespace JSC; + +// Forward declarations for prototype functions +JSC_DECLARE_HOST_FUNCTION(jsSignProtoFuncInit); +JSC_DECLARE_HOST_FUNCTION(jsSignProtoFuncUpdate); +JSC_DECLARE_HOST_FUNCTION(jsSignProtoFuncSign); + +// Constructor functions +JSC_DECLARE_HOST_FUNCTION(callSign); +JSC_DECLARE_HOST_FUNCTION(constructSign); + +// Property table for Sign prototype +static const JSC::HashTableValue JSSignPrototypeTableValues[] = { + { "init"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, + { JSC::HashTableValue::NativeFunctionType, jsSignProtoFuncInit, 1 } }, + { "update"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, + { JSC::HashTableValue::NativeFunctionType, jsSignProtoFuncUpdate, 2 } }, + { "sign"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, + { JSC::HashTableValue::NativeFunctionType, jsSignProtoFuncSign, 2 } }, +}; + +// JSSign implementation +const JSC::ClassInfo JSSign::s_info = { "Sign"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSign) }; + +JSSign::JSSign(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) +{ +} + +void JSSign::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); +} + +JSSign* JSSign::create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject) +{ + JSSign* sign = new (NotNull, JSC::allocateCell(vm)) JSSign(vm, structure); + sign->finishCreation(vm, globalObject); + return sign; +} + +JSC::Structure* JSSign::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); +} + +template +JSC::GCClient::IsoSubspace* JSSign::subspaceFor(JSC::VM& vm) +{ + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSSign.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSSign = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSSign.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSSign = std::forward(space); }); +} + +// JSSignPrototype implementation +const JSC::ClassInfo JSSignPrototype::s_info = { "Sign"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSignPrototype) }; + +JSSignPrototype::JSSignPrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) +{ +} + +void JSSignPrototype::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSSign::info(), JSSignPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSSignPrototype* JSSignPrototype::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) +{ + JSSignPrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSSignPrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; +} + +JSC::Structure* JSSignPrototype::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; +} + +// JSSignConstructor implementation +const JSC::ClassInfo JSSignConstructor::s_info = { "Sign"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSignConstructor) }; + +JSSignConstructor::JSSignConstructor(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure, callSign, constructSign) +{ +} + +void JSSignConstructor::finishCreation(JSC::VM& vm, JSC::JSObject* prototype) +{ + Base::finishCreation(vm, 0, "Sign"_s); + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); +} + +JSSignConstructor* JSSignConstructor::create(JSC::VM& vm, JSC::Structure* structure, JSC::JSObject* prototype) +{ + JSSignConstructor* constructor = new (NotNull, JSC::allocateCell(vm)) JSSignConstructor(vm, structure); + constructor->finishCreation(vm, prototype); + return constructor; +} + +JSC::Structure* JSSignConstructor::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); +} + +// Prototype function implementations +JSC_DEFINE_HOST_FUNCTION(jsSignProtoFuncInit, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Get the JSSign object from thisValue and verify it's valid + JSSign* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "Sign"_s, "init"_s); + return {}; + } + + // Check that we have at least 1 argument (the digest name) + if (callFrame->argumentCount() < 1) { + throwVMError(globalObject, scope, "Sign.prototype.init requires at least 1 argument"_s); + return {}; + } + + // Verify the first argument is a string and extract it + JSC::JSValue digestArg = callFrame->argument(0); + if (!digestArg.isString()) { + throwTypeError(globalObject, scope, "First argument must be a string specifying the hash function"_s); + return {}; + } + + // Convert the digest name to a string_view + WTF::String digestName = digestArg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + // Get the EVP_MD* for the digest using ncrypto helper + auto* digest = ncrypto::getDigestByName(digestName); + if (!digest) { + return Bun::ERR::CRYPTO_INVALID_DIGEST(scope, globalObject, digestName); + } + + // Create a new EVPMDCtxPointer using ncrypto's wrapper + auto mdCtx = ncrypto::EVPMDCtxPointer::New(); + if (!mdCtx) { + throwTypeError(globalObject, scope, "Failed to create message digest context"_s); + return {}; + } + + // Initialize the digest context with proper error handling + if (!mdCtx.digestInit(digest)) { + throwTypeError(globalObject, scope, "Failed to initialize message digest"_s); + return {}; + } + + // Store the initialized context in the JSSign object + thisObject->m_mdCtx = WTFMove(mdCtx); + + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +void updateWithBufferView(JSGlobalObject* globalObject, JSSign* sign, JSC::JSArrayBufferView* bufferView) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + + if (bufferView->isDetached()) { + throwTypeError(globalObject, scope, "Buffer is detached"_s); + return; + } + + size_t byteLength = bufferView->byteLength(); + if (byteLength > INT_MAX) { + throwRangeError(globalObject, scope, "data is too long"_s); + return; + } + + auto buffer = ncrypto::Buffer { + .data = bufferView->vector(), + .len = byteLength, + }; + + if (!sign->m_mdCtx.digestUpdate(buffer)) { + throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to update digest"); + return; + } +} + +JSC_DEFINE_HOST_FUNCTION(jsSignProtoFuncUpdate, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Get the JSSign object from thisValue and verify it's valid + JSSign* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "Sign"_s, "update"_s); + return JSValue::encode({}); + } + + // Check that we have at least 1 argument (the data) + if (callFrame->argumentCount() < 1) { + throwVMError(globalObject, scope, "Sign.prototype.update requires at least 1 argument"_s); + return JSValue::encode({}); + } + + // Get the data argument + JSC::JSValue data = callFrame->argument(0); + + // if it's a string, using encoding for decode. if it's a buffer, just use the buffer + if (data.isString()) { + JSString* dataString = data.toString(globalObject); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + JSValue encodingValue = callFrame->argument(1); + auto encoding = parseEnumeration(*globalObject, encodingValue).value_or(BufferEncodingType::utf8); + RETURN_IF_EXCEPTION(scope, {}); + + if (encoding == BufferEncodingType::hex && dataString->length() % 2 != 0) { + return Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, "encoding"_s, encodingValue, makeString("is invalid for data of length "_s, dataString->length())); + } + + JSValue buf = JSValue::decode(constructFromEncoding(globalObject, dataString, encoding)); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + auto* view = jsDynamicCast(buf); + + updateWithBufferView(globalObject, thisObject, view); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + return JSValue::encode(callFrame->thisValue()); + } + + if (!data.isCell() || !JSC::isTypedArrayTypeIncludingDataView(data.asCell()->type())) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "data"_s, "string or an instance of Buffer, TypedArray, or DataView"_s, data); + } + + // Handle ArrayBufferView input + if (auto* view = JSC::jsDynamicCast(data)) { + + updateWithBufferView(globalObject, thisObject, view); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + return JSValue::encode(callFrame->thisValue()); + } + + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "data"_s, "string or an instance of Buffer, TypedArray, or DataView"_s, data); +} + +JSUint8Array* signWithKey(JSC::JSGlobalObject* lexicalGlobalObject, JSSign* thisObject, const ncrypto::EVPKeyPointer& pkey, NodeCryptoKeys::DSASigEnc dsa_sig_enc, int padding, std::optional salt_len) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Check if the context is initialized + if (!thisObject->m_mdCtx) { + throwTypeError(lexicalGlobalObject, scope, "Sign.prototype.sign cannot be called before Sign.prototype.init"_s); + return nullptr; + } + + // Move mdCtx out of JSSign object + ncrypto::EVPMDCtxPointer mdCtx = WTFMove(thisObject->m_mdCtx); + + // Validate DSA parameters + if (!pkey.validateDsaParameters()) { + throwTypeError(lexicalGlobalObject, scope, "Invalid DSA parameters"_s); + return nullptr; + } + + // Get the final digest + auto data = mdCtx.digestFinal(mdCtx.getExpectedSize()); + if (!data) { + throwTypeError(lexicalGlobalObject, scope, "Failed to finalize digest"_s); + return nullptr; + } + + // Create signing context + auto pkctx = pkey.newCtx(); + if (!pkctx || pkctx.initForSign() <= 0) { + throwCryptoError(lexicalGlobalObject, scope, ERR_peek_error(), "Failed to initialize signing context"_s); + return nullptr; + } + + // Set RSA padding mode and salt length if applicable + if (pkey.isRsaVariant()) { + if (!ncrypto::EVPKeyCtxPointer::setRsaPadding(pkctx.get(), padding, salt_len)) { + throwCryptoError(lexicalGlobalObject, scope, ERR_peek_error(), "Failed to set RSA padding"_s); + return nullptr; + } + } + + // Set signature MD from the digest context + if (!pkctx.setSignatureMd(mdCtx)) { + throwCryptoError(lexicalGlobalObject, scope, ERR_peek_error(), "Failed to set signature message digest"_s); + return nullptr; + } + + // Create buffer for signature + auto sigBuffer = JSC::ArrayBuffer::tryCreate(pkey.size(), 1); + if (!sigBuffer) { + throwTypeError(lexicalGlobalObject, scope, "Failed to allocate signature buffer"_s); + return nullptr; + } + + // Perform signing operation + ncrypto::Buffer sigBuf { + .data = static_cast(sigBuffer->data()), + .len = pkey.size() + }; + + if (!pkctx.signInto(data, &sigBuf)) { + throwTypeError(lexicalGlobalObject, scope, "Failed to create signature"_s); + return nullptr; + } + + // Convert to P1363 format if requested for EC keys + if (dsa_sig_enc == NodeCryptoKeys::DSASigEnc::P1363 && pkey.isSigVariant()) { + auto p1363Size = pkey.getBytesOfRS().value_or(0) * 2; + if (p1363Size > 0) { + auto p1363Buffer = JSC::ArrayBuffer::tryCreate(p1363Size, 1); + if (!p1363Buffer) { + throwTypeError(lexicalGlobalObject, scope, "Failed to allocate P1363 buffer"_s); + return nullptr; + } + + ncrypto::Buffer derSig { + .data = static_cast(sigBuffer->data()), + .len = sigBuf.len + }; + + if (!ncrypto::extractP1363(derSig, static_cast(p1363Buffer->data()), p1363Size / 2)) { + throwTypeError(lexicalGlobalObject, scope, "Failed to convert signature format"_s); + return nullptr; + } + + sigBuffer = p1363Buffer; + } + } + + // Create and return JSUint8Array + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + return JSC::JSUint8Array::create(lexicalGlobalObject, globalObject->JSBufferSubclassStructure(), WTFMove(sigBuffer), 0, sigBuf.len); +} + +std::optional preparePrivateKey(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue maybeKey, std::optional algorithmIdentifier) +{ + ncrypto::ClearErrorOnReturn clearError; + + VM& vm = lexicalGlobalObject->vm(); + + if (!maybeKey.isCell()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, maybeKey); + return std::nullopt; + } + + auto optionsCell = maybeKey.asCell(); + auto optionsType = optionsCell->type(); + + if (optionsCell->inherits()) { + auto* cryptoKey = jsCast(optionsCell); + + // convert it to a key object, then to EVPKeyPointer + auto& key = cryptoKey->wrapped(); + + if (algorithmIdentifier) { + switch (key.keyClass()) { + case CryptoKeyClass::RSA: { + const auto& rsa = downcast(key); + CryptoAlgorithmIdentifier restrictHash; + bool isRestricted = rsa.isRestrictedToHash(restrictHash); + if (isRestricted && algorithmIdentifier.value() != restrictHash) { + JSC::throwTypeError(lexicalGlobalObject, scope, "digest not allowed"_s); + return std::nullopt; + } + } + default: + break; + } + } + + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(keyValue.key); + return keyPtr; + } + + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } else if (maybeKey.isObject()) { + JSObject* optionsObj = optionsCell->getObject(); + const auto& names = WebCore::builtinNames(vm); + + if (auto val = optionsObj->getIfPropertyExists(lexicalGlobalObject, names.bunNativePtrPrivateName())) { + if (val.isCell() && val.inherits()) { + auto* cryptoKey = jsCast(val.asCell()); + + auto& key = cryptoKey->wrapped(); + + if (algorithmIdentifier) { + switch (key.keyClass()) { + case CryptoKeyClass::RSA: { + const auto& rsa = downcast(key); + CryptoAlgorithmIdentifier restrictHash; + bool isRestricted = rsa.isRestrictedToHash(restrictHash); + if (isRestricted && algorithmIdentifier.value() != restrictHash) { + JSC::throwTypeError(lexicalGlobalObject, scope, "digest not allowed"_s); + return std::nullopt; + } + } + default: + break; + } + } + + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(keyValue.key); + return keyPtr; + } + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } + } else if (optionsType >= Int8ArrayType && optionsType <= DataViewType) { + auto dataBuf = KeyObject__GetBuffer(maybeKey); + if (dataBuf.hasException()) { + return std::nullopt; + } + + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig config; + config.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + + auto buffer = dataBuf.releaseReturnValue(); + ncrypto::Buffer ncryptoBuf { + .data = buffer.data(), + .len = buffer.size(), + }; + + auto res = ncrypto::EVPKeyPointer::TryParsePrivateKey(config, ncryptoBuf); + if (res) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(res.value)); + return keyPtr; + } + + if (res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + Bun::ERR::MISSING_PASSPHRASE(scope, lexicalGlobalObject, "Passphrase required for encrypted key"_s); + return std::nullopt; + } + + throwCryptoError(lexicalGlobalObject, scope, res.openssl_error.value_or(0), "Failed to read private key"_s); + return std::nullopt; + } + + JSValue key = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "key"_s)); + RETURN_IF_EXCEPTION(scope, {}); + JSValue encodingValue = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "encoding"_s)); + RETURN_IF_EXCEPTION(scope, {}); + JSValue passphrase = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "passphrase"_s)); + RETURN_IF_EXCEPTION(scope, {}); + JSValue formatValue = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "format"_s)); + RETURN_IF_EXCEPTION(scope, {}); + WTF::StringView formatStr = WTF::nullStringView(); + if (formatValue.isString()) { + auto str = formatValue.toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + formatStr = str->view(lexicalGlobalObject); + } + + if (!key.isCell()) { + if (formatStr == "jwk"_s) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.key"_s, "object"_s, key); + } else { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, key); + } + return std::nullopt; + } + + String encodingString = encodingValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + + auto keyCell = key.asCell(); + auto keyCellType = keyCell->type(); + if (keyCell->inherits()) { + auto* cryptoKey = jsCast(keyCell); + auto& key = cryptoKey->wrapped(); + + if (algorithmIdentifier) { + switch (key.keyClass()) { + case CryptoKeyClass::RSA: { + const auto& rsa = downcast(key); + CryptoAlgorithmIdentifier restrictHash; + bool isRestricted = rsa.isRestrictedToHash(restrictHash); + if (isRestricted && algorithmIdentifier.value() != restrictHash) { + JSC::throwTypeError(lexicalGlobalObject, scope, "digest not allowed"_s); + return std::nullopt; + } + } + default: + break; + } + } + + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(keyValue.key); + return keyPtr; + } + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } else if (key.isObject()) { + JSObject* keyObj = key.getObject(); + if (auto keyVal = keyObj->getIfPropertyExists(lexicalGlobalObject, names.bunNativePtrPrivateName())) { + if (keyVal.isCell() && keyVal.inherits()) { + auto* cryptoKey = jsCast(keyVal.asCell()); + + auto& key = cryptoKey->wrapped(); + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(WTFMove(keyValue.key)); + return keyPtr; + } + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } + } else if (keyCellType >= Int8ArrayType && keyCellType <= DataViewType) { + auto dataBuf = KeyObject__GetBuffer(key); + if (dataBuf.hasException()) { + return std::nullopt; + } + + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig config; + config.format = parseKeyFormat(lexicalGlobalObject, formatValue, "options.format"_s, ncrypto::EVPKeyPointer::PKFormatType::PEM); + + config.passphrase = passphraseFromBufferSource(lexicalGlobalObject, scope, passphrase); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Get the type value from options + JSValue typeValue = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "type"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Parse key type for private key + auto keyType = parseKeyType(lexicalGlobalObject, typeValue, config.format == ncrypto::EVPKeyPointer::PKFormatType::DER, WTF::nullStringView(), false, "options.type"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + config.type = keyType.value_or(ncrypto::EVPKeyPointer::PKEncodingType::PKCS1); + + auto buffer = dataBuf.releaseReturnValue(); + ncrypto::Buffer ncryptoBuf { + .data = buffer.data(), + .len = buffer.size(), + }; + + auto res = ncrypto::EVPKeyPointer::TryParsePrivateKey(config, ncryptoBuf); + if (!res) { + if (res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + Bun::ERR::MISSING_PASSPHRASE(scope, lexicalGlobalObject, "Passphrase required for encrypted key"_s); + return std::nullopt; + } + + throwCryptoError(lexicalGlobalObject, scope, res.openssl_error.value_or(0), "Failed to read private key"_s); + return std::nullopt; + } + + ncrypto::EVPKeyPointer keyPtr(WTFMove(res.value)); + return keyPtr; + } else if (formatStr == "jwk"_s) { + bool isPublic = false; + return getKeyObjectHandleFromJwk(lexicalGlobalObject, scope, key, isPublic); + } + } else if (formatStr == "jwk"_s) { + bool isPublic = false; + return getKeyObjectHandleFromJwk(lexicalGlobalObject, scope, key, isPublic); + } else if (key.isString()) { + WTF::String keyStr = key.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + return keyFromString(lexicalGlobalObject, scope, keyStr, passphrase); + } + + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, key); + return std::nullopt; + } else if (maybeKey.isString()) { + WTF::String keyStr = maybeKey.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + return keyFromString(lexicalGlobalObject, scope, keyStr, jsUndefined()); + } + + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, maybeKey); + return std::nullopt; +} + +JSC_DEFINE_HOST_FUNCTION(jsSignProtoFuncSign, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + ncrypto::ClearErrorOnReturn clearError; + + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Get the JSSign object from thisValue and verify it's valid + JSSign* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*lexicalGlobalObject, scope, "Sign"_s, "sign"_s); + return {}; + } + + // This function receives two arguments: options and encoding + JSValue options = callFrame->argument(0); + + bool optionsBool = options.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + // https://github.com/nodejs/node/blob/1b2d2f7e682268228b1352cba7389db01614812a/lib/internal/crypto/sig.js#L116 + if (!optionsBool) { + return Bun::ERR::CRYPTO_SIGN_KEY_REQUIRED(scope, lexicalGlobalObject); + } + + if (!options.isCell()) { + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, options); + } + + JSValue outputEncodingValue = callFrame->argument(1); + auto outputEncoding = parseEnumeration(*lexicalGlobalObject, outputEncodingValue).value_or(BufferEncodingType::buffer); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + // Get RSA padding mode and salt length if applicable + int32_t padding = getPadding(lexicalGlobalObject, options, {}); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + std::optional saltLen = getSaltLength(lexicalGlobalObject, options); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + // Get DSA signature encoding format + NodeCryptoKeys::DSASigEnc dsaSigEnc = getDSASigEnc(lexicalGlobalObject, options); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + // Get key argument + std::optional maybeKeyPtr = preparePrivateKey(lexicalGlobalObject, scope, options, std::nullopt); + ASSERT(!!scope.exception() == !maybeKeyPtr.has_value()); + if (!maybeKeyPtr) { + return {}; + } + ncrypto::EVPKeyPointer keyPtr = WTFMove(maybeKeyPtr.value()); + + // Use the signWithKey function to perform the signing operation + JSUint8Array* signature = signWithKey(lexicalGlobalObject, thisObject, keyPtr, dsaSigEnc, padding, saltLen); + if (!signature) { + return {}; + } + + // If output encoding is not buffer, convert the signature to the requested encoding + if (outputEncoding != BufferEncodingType::buffer) { + EncodedJSValue encodedSignature = jsBufferToString(vm, lexicalGlobalObject, signature, 0, signature->byteLength(), outputEncoding); + RETURN_IF_EXCEPTION(scope, {}); + return encodedSignature; + } + + return JSValue::encode(signature); +} + +// Constructor function implementations +JSC_DEFINE_HOST_FUNCTION(callSign, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + throwTypeError(globalObject, scope, "Sign constructor cannot be called as a function"_s); + return JSValue::encode({}); +} + +JSC_DEFINE_HOST_FUNCTION(constructSign, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSC::Structure* structure = zigGlobalObject->m_JSSignClassStructure.get(zigGlobalObject); + + JSC::JSValue newTarget = callFrame->newTarget(); + if (UNLIKELY(zigGlobalObject->m_JSSignClassStructure.constructor(zigGlobalObject) != newTarget)) { + if (!newTarget) { + throwTypeError(globalObject, scope, "Class constructor Sign cannot be invoked without 'new'"_s); + return {}; + } + + auto* functionGlobalObject = defaultGlobalObject(getFunctionRealm(globalObject, newTarget.getObject())); + RETURN_IF_EXCEPTION(scope, {}); + structure = JSC::InternalFunction::createSubclassStructure(globalObject, newTarget.getObject(), functionGlobalObject->m_JSSignClassStructure.get(functionGlobalObject)); + RETURN_IF_EXCEPTION(scope, {}); + } + + return JSC::JSValue::encode(JSSign::create(vm, structure, globalObject)); +} + +JSC_DEFINE_HOST_FUNCTION(jsSignOneShot, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ncrypto::ClearErrorOnReturn clearError; + + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto argCount = callFrame->argumentCount(); + + // Validate algorithm if provided + JSValue algorithmValue = callFrame->argument(0); + std::optional hash = std::nullopt; + const EVP_MD* digest = nullptr; + if (!algorithmValue.isUndefinedOrNull()) { + Bun::V::validateString(scope, globalObject, algorithmValue, "algorithm"_s); + RETURN_IF_EXCEPTION(scope, {}); + + WTF::String algorithmName = algorithmValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + hash = WebCore::CryptoAlgorithmRegistry::singleton().identifier(algorithmName); + + digest = ncrypto::getDigestByName(algorithmName); + if (!digest) { + return Bun::ERR::CRYPTO_INVALID_DIGEST(scope, globalObject, algorithmName); + } + } + + // Get data argument + JSValue dataValue = callFrame->argument(1); + JSC::JSArrayBufferView* dataView = getArrayBufferOrView(globalObject, scope, dataValue, "data"_s, jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + if (!dataView) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "data must be a Buffer, TypedArray, or DataView"_s); + return {}; + } + + // Get key argument + JSValue keyValue = callFrame->argument(2); + + std::optional saltLen = getSaltLength(globalObject, keyValue); + RETURN_IF_EXCEPTION(scope, {}); + + // Get DSA signature encoding format + NodeCryptoKeys::DSASigEnc dsaSigEnc = getDSASigEnc(globalObject, keyValue); + RETURN_IF_EXCEPTION(scope, {}); + + // Prepare the private key + std::optional maybeKeyPtr = preparePrivateKey(globalObject, scope, keyValue, hash); + ASSERT(!!scope.exception() == !maybeKeyPtr.has_value()); + if (!maybeKeyPtr) { + return {}; + } + ncrypto::EVPKeyPointer keyPtr = WTFMove(maybeKeyPtr.value()); + + // Get callback if provided + JSValue callbackValue; + bool hasCallback = false; + if (argCount > 3) { + callbackValue = callFrame->argument(3); + if (!callbackValue.isUndefined()) { + Bun::V::validateFunction(scope, globalObject, callbackValue, "callback"_s); + RETURN_IF_EXCEPTION(scope, {}); + hasCallback = true; + } + } + + // Get RSA padding mode and salt length if applicable + int32_t padding = getPadding(globalObject, keyValue, keyPtr); + RETURN_IF_EXCEPTION(scope, {}); + + // Create data buffer + ncrypto::Buffer dataBuf { + .data = reinterpret_cast(dataView->vector()), + .len = dataView->byteLength() + }; + + // Create a new EVP_MD_CTX for signing + auto mdCtx = ncrypto::EVPMDCtxPointer::New(); + if (!mdCtx) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to create message digest context"_s); + return {}; + } + + // Initialize the context for signing with the key and digest + auto ctx = mdCtx.signInit(keyPtr, digest); + if (!ctx.has_value()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to initialize signing context"_s); + return {}; + } + + // Apply RSA options if needed + if (keyPtr.isRsaVariant()) { + if (!ncrypto::EVPKeyCtxPointer::setRsaPadding(ctx.value(), padding, saltLen)) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to set RSA padding"_s); + return {}; + } + } + + RefPtr sigBuffer = nullptr; + if (keyPtr.isOneShotVariant()) { + auto data = mdCtx.signOneShot(dataBuf); + if (!data) { + throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to create signature"_s); + return {}; + } + + sigBuffer = JSC::ArrayBuffer::tryCreate(data.size(), 1); + if (!sigBuffer) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to allocate signature buffer"_s); + return {}; + } + + memcpy(sigBuffer->data(), data.get(), data.size()); + + } else { + auto signatureData = mdCtx.sign(dataBuf); + if (!signatureData) { + throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to create signature"_s); + return {}; + } + + // Convert to P1363 format if requested for EC keys + if (dsaSigEnc == NodeCryptoKeys::DSASigEnc::P1363 && keyPtr.isSigVariant() && keyPtr.getBytesOfRS().value_or(0) * 2 > 0) { + auto p1363Size = keyPtr.getBytesOfRS().value_or(0) * 2; + sigBuffer = JSC::ArrayBuffer::tryCreate(p1363Size, 1); + if (!sigBuffer) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to allocate P1363 buffer"_s); + return {}; + } + + ncrypto::Buffer derSig { + .data = reinterpret_cast(signatureData.get()), + .len = signatureData.size() + }; + + if (!ncrypto::extractP1363(derSig, static_cast(sigBuffer->data()), p1363Size / 2)) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to convert signature format"_s); + return {}; + } + } else { + sigBuffer = JSC::ArrayBuffer::tryCreate(signatureData.size(), 1); + if (!sigBuffer) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to allocate signature buffer"_s); + return {}; + } + + memcpy(sigBuffer->data(), signatureData.get(), signatureData.size()); + } + } + ASSERT(sigBuffer); + + // Create JSUint8Array from the signature buffer + auto* globalObj = defaultGlobalObject(globalObject); + auto* signature = JSC::JSUint8Array::create(globalObject, globalObj->JSBufferSubclassStructure(), WTFMove(sigBuffer), 0, sigBuffer->byteLength()); + + // If we have a callback, call it with the signature + if (hasCallback) { + JSC::MarkedArgumentBuffer args; + args.append(jsNull()); + args.append(signature); + ASSERT(!args.hasOverflowed()); + + NakedPtr returnedException = nullptr; + JSC::profiledCall(globalObject, JSC::ProfilingReason::API, callbackValue, JSC::getCallData(callbackValue), JSC::jsUndefined(), args, returnedException); + RETURN_IF_EXCEPTION(scope, {}); + if (returnedException) { + scope.throwException(globalObject, returnedException.get()); + } + + return JSValue::encode(jsUndefined()); + } + + // Otherwise, return the signature directly + return JSValue::encode(signature); +} + +void setupJSSignClassStructure(LazyClassStructure::Initializer& init) +{ + auto* prototypeStructure = JSSignPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()); + auto* prototype = JSSignPrototype::create(init.vm, init.global, prototypeStructure); + + auto* constructorStructure = JSSignConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()); + auto* constructor = JSSignConstructor::create(init.vm, constructorStructure, prototype); + + auto* structure = JSSign::createStructure(init.vm, init.global, prototype); + init.setPrototype(prototype); + init.setStructure(structure); + init.setConstructor(constructor); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/node/crypto/JSSign.h b/src/bun.js/bindings/node/crypto/JSSign.h new file mode 100644 index 0000000000..bfe4e08411 --- /dev/null +++ b/src/bun.js/bindings/node/crypto/JSSign.h @@ -0,0 +1,85 @@ +#pragma once + +#include "root.h" +#include "JSBuffer.h" +#include "helpers.h" +#include "ncrypto.h" +#include "util.h" +#include +#include + +namespace Bun { + +// JSC_DECLARE_HOST_FUNCTION(jsSignOneShot); + +class JSSign; +class JSSignPrototype; +class JSSignConstructor; + +class JSSign final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSSign* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); + + DECLARE_INFO; + + ncrypto::EVPMDCtxPointer m_mdCtx; + +private: + JSSign(JSC::VM& vm, JSC::Structure* structure); + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject); +}; + +class JSSignPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSSignPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + + DECLARE_INFO; + +private: + JSSignPrototype(JSC::VM& vm, JSC::Structure* structure); + void finishCreation(JSC::VM& vm); +}; + +class JSSignConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSSignConstructor* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSObject* prototype); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.internalFunctionSpace(); + } + + DECLARE_INFO; + +private: + JSSignConstructor(JSC::VM& vm, JSC::Structure* structure); + void finishCreation(JSC::VM& vm, JSC::JSObject* prototype); +}; + +JSC_DECLARE_HOST_FUNCTION(jsSignOneShot); + +void setupJSSignClassStructure(JSC::LazyClassStructure::Initializer& init); + +} // namespace Bun diff --git a/src/bun.js/bindings/node/crypto/JSVerify.cpp b/src/bun.js/bindings/node/crypto/JSVerify.cpp new file mode 100644 index 0000000000..d359848fb2 --- /dev/null +++ b/src/bun.js/bindings/node/crypto/JSVerify.cpp @@ -0,0 +1,1400 @@ +#include "JSVerify.h" +#include "JavaScriptCore/JSArrayBufferView.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/JSType.h" +#include "SubtleCrypto.h" +#include "ZigGlobalObject.h" +#include "ErrorCode.h" +#include "JSDOMExceptionHandling.h" +#include +#include +#include "JSBufferEncodingType.h" +#include "KeyObject.h" +#include "JSCryptoKey.h" +#include "AsymmetricKeyValue.h" +#include "NodeValidator.h" +#include "JSBuffer.h" +#include "util.h" +#include "BunString.h" +#include +#include +#include "ncrypto.h" +#include "JSSign.h" +#include "JsonWebKey.h" +#include "CryptoKeyEC.h" +#include "CryptoKeyRSA.h" +#include "wtf/text/Base64.h" + +// Forward declarations for functions defined in other files +namespace Bun { + +using namespace JSC; + +// Forward declarations for prototype functions +JSC_DECLARE_HOST_FUNCTION(jsVerifyProtoFuncInit); +JSC_DECLARE_HOST_FUNCTION(jsVerifyProtoFuncUpdate); +JSC_DECLARE_HOST_FUNCTION(jsVerifyProtoFuncVerify); +JSC_DECLARE_HOST_FUNCTION(jsVerifyOneShot); + +// Constructor functions +JSC_DECLARE_HOST_FUNCTION(callVerify); +JSC_DECLARE_HOST_FUNCTION(constructVerify); + +// Property table for Verify prototype +static const JSC::HashTableValue JSVerifyPrototypeTableValues[] = { + { "init"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, + { JSC::HashTableValue::NativeFunctionType, jsVerifyProtoFuncInit, 1 } }, + { "update"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, + { JSC::HashTableValue::NativeFunctionType, jsVerifyProtoFuncUpdate, 2 } }, + { "verify"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, + { JSC::HashTableValue::NativeFunctionType, jsVerifyProtoFuncVerify, 3 } }, +}; + +// JSVerify implementation +const JSC::ClassInfo JSVerify::s_info = { "Verify"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSVerify) }; + +JSVerify::JSVerify(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) +{ +} + +void JSVerify::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); +} + +JSVerify* JSVerify::create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject) +{ + JSVerify* verify = new (NotNull, JSC::allocateCell(vm)) JSVerify(vm, structure); + verify->finishCreation(vm, globalObject); + return verify; +} + +JSC::Structure* JSVerify::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); +} + +template +JSC::GCClient::IsoSubspace* JSVerify::subspaceFor(JSC::VM& vm) +{ + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSVerify.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSVerify = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSVerify.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSVerify = std::forward(space); }); +} + +// JSVerifyPrototype implementation +const JSC::ClassInfo JSVerifyPrototype::s_info = { "Verify"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSVerifyPrototype) }; + +JSVerifyPrototype::JSVerifyPrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) +{ +} + +void JSVerifyPrototype::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSVerify::info(), JSVerifyPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSVerifyPrototype* JSVerifyPrototype::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) +{ + JSVerifyPrototype* prototype = new (NotNull, JSC::allocateCell(vm)) JSVerifyPrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; +} + +JSC::Structure* JSVerifyPrototype::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); +} + +// JSVerifyConstructor implementation +const JSC::ClassInfo JSVerifyConstructor::s_info = { "Verify"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSVerifyConstructor) }; + +JSVerifyConstructor::JSVerifyConstructor(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure, callVerify, constructVerify) +{ +} + +void JSVerifyConstructor::finishCreation(JSC::VM& vm, JSC::JSObject* prototype) +{ + Base::finishCreation(vm, 1, "Verify"_s); + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); +} + +JSVerifyConstructor* JSVerifyConstructor::create(JSC::VM& vm, JSC::Structure* structure, JSC::JSObject* prototype) +{ + JSVerifyConstructor* constructor = new (NotNull, JSC::allocateCell(vm)) JSVerifyConstructor(vm, structure); + constructor->finishCreation(vm, prototype); + return constructor; +} + +JSC::Structure* JSVerifyConstructor::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) +{ + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); +} + +// Function stubs for implementation later +JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncInit, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Get the JSVerify object from thisValue and verify it's valid + JSVerify* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "Verify"_s, "init"_s); + return {}; + } + + // Check that we have at least 1 argument (the digest name) + if (callFrame->argumentCount() < 1) { + throwVMError(globalObject, scope, "Verify.prototype.init requires at least 1 argument"_s); + return {}; + } + + // Verify the first argument is a string and extract it + JSC::JSValue digestArg = callFrame->argument(0); + if (!digestArg.isString()) { + throwTypeError(globalObject, scope, "First argument must be a string specifying the hash function"_s); + return {}; + } + + // Convert the digest name to a string_view + WTF::String digestName = digestArg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + // Get the EVP_MD* for the digest using ncrypto helper + auto* digest = ncrypto::getDigestByName(digestName); + if (!digest) { + return Bun::ERR::CRYPTO_INVALID_DIGEST(scope, globalObject, digestName); + } + + // Create a new EVPMDCtxPointer using ncrypto's wrapper + auto mdCtx = ncrypto::EVPMDCtxPointer::New(); + if (!mdCtx) { + throwTypeError(globalObject, scope, "Failed to create message digest context"_s); + return {}; + } + + // Initialize the digest context with proper error handling + if (!mdCtx.digestInit(digest)) { + throwTypeError(globalObject, scope, "Failed to initialize message digest"_s); + return {}; + } + + // Store the initialized context in the JSVerify object + thisObject->m_mdCtx = WTFMove(mdCtx); + + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncUpdate, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Get the JSVerify object from thisValue and verify it's valid + JSVerify* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "Verify"_s, "update"_s); + return JSValue::encode({}); + } + + // Check that we have at least 1 argument (the data) + if (callFrame->argumentCount() < 1) { + throwVMError(globalObject, scope, "Verify.prototype.update requires at least 1 argument"_s); + return JSValue::encode({}); + } + + // Get the data argument + JSC::JSValue data = callFrame->argument(0); + + // if it's a string, using encoding for decode. if it's a buffer, just use the buffer + if (data.isString()) { + JSString* dataString = data.toString(globalObject); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + JSValue encodingValue = callFrame->argument(1); + auto encoding = parseEnumeration(*globalObject, encodingValue).value_or(BufferEncodingType::utf8); + RETURN_IF_EXCEPTION(scope, {}); + + if (encoding == BufferEncodingType::hex && dataString->length() % 2 != 0) { + return Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, "encoding"_s, encodingValue, makeString("is invalid for data of length "_s, dataString->length())); + } + + JSValue buf = JSValue::decode(constructFromEncoding(globalObject, dataString, encoding)); + RETURN_IF_EXCEPTION(scope, JSValue::encode({})); + + auto* view = jsDynamicCast(buf); + + // Update the digest context with the buffer data + if (view->isDetached()) { + throwTypeError(globalObject, scope, "Buffer is detached"_s); + return JSValue::encode({}); + } + + size_t byteLength = view->byteLength(); + if (byteLength > INT_MAX) { + throwRangeError(globalObject, scope, "data is too long"_s); + return JSValue::encode({}); + } + + auto buffer = ncrypto::Buffer { + .data = view->vector(), + .len = byteLength, + }; + + if (!thisObject->m_mdCtx.digestUpdate(buffer)) { + throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to update digest"); + return JSValue::encode({}); + } + + return JSValue::encode(callFrame->thisValue()); + } + + if (!data.isCell() || !JSC::isTypedArrayTypeIncludingDataView(data.asCell()->type())) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "data"_s, "string or an instance of Buffer, TypedArray, or DataView"_s, data); + } + + // Handle ArrayBufferView input + if (auto* view = JSC::jsDynamicCast(data)) { + if (view->isDetached()) { + throwTypeError(globalObject, scope, "Buffer is detached"_s); + return JSValue::encode({}); + } + + size_t byteLength = view->byteLength(); + if (byteLength > INT_MAX) { + throwRangeError(globalObject, scope, "data is too long"_s); + return JSValue::encode({}); + } + + auto buffer = ncrypto::Buffer { + .data = view->vector(), + .len = byteLength, + }; + + if (!thisObject->m_mdCtx.digestUpdate(buffer)) { + throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to update digest"); + return JSValue::encode({}); + } + + return JSValue::encode(callFrame->thisValue()); + } + + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "data"_s, "string or an instance of Buffer, TypedArray, or DataView"_s, data); +} + +std::optional preparePublicOrPrivateKey(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue maybeKey); +bool convertP1363ToDER(const ncrypto::Buffer& p1363Sig, + const ncrypto::EVPKeyPointer& pkey, + WTF::Vector& derBuffer); + +JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncVerify, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + ncrypto::ClearErrorOnReturn clearErrorOnReturn; + + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Get the JSVerify object from thisValue and verify it's valid + JSVerify* thisObject = jsDynamicCast(callFrame->thisValue()); + if (UNLIKELY(!thisObject)) { + Bun::throwThisTypeError(*globalObject, scope, "Verify"_s, "verify"_s); + return JSValue::encode(jsBoolean(false)); + } + + // Check if the context is initialized + if (!thisObject->m_mdCtx) { + throwTypeError(globalObject, scope, "Verify.prototype.verify cannot be called before Verify.prototype.init"_s); + return JSValue::encode(jsBoolean(false)); + } + + // This function receives two arguments: options and signature + JSValue options = callFrame->argument(0); + JSValue signatureValue = callFrame->argument(1); + JSValue sigEncodingValue = callFrame->argument(2); + + JSC::JSArrayBufferView* signatureBuffer = getArrayBufferOrView(globalObject, scope, signatureValue, "signature"_s, sigEncodingValue); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsBoolean(false))); + + // Prepare the public or private key from options + std::optional maybeKeyPtr = preparePublicOrPrivateKey(globalObject, scope, options); + ASSERT(!!scope.exception() == !maybeKeyPtr.has_value()); + if (!maybeKeyPtr) { + return JSValue::encode({}); + } + ncrypto::EVPKeyPointer keyPtr = WTFMove(maybeKeyPtr.value()); + + // Get RSA padding mode and salt length if applicable + int32_t padding = getPadding(globalObject, options, keyPtr); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsBoolean(false))); + + std::optional saltLen = getSaltLength(globalObject, options); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsBoolean(false))); + + // Get DSA signature encoding format + NodeCryptoKeys::DSASigEnc dsaSigEnc = getDSASigEnc(globalObject, options); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsBoolean(false))); + + // Get the signature buffer + + // Move mdCtx out of JSVerify object to finalize it + ncrypto::EVPMDCtxPointer mdCtx = WTFMove(thisObject->m_mdCtx); + + // Validate DSA parameters + if (!keyPtr.validateDsaParameters()) { + throwTypeError(globalObject, scope, "Invalid DSA parameters"_s); + return JSValue::encode(jsBoolean(false)); + } + + // Get the final digest + auto data = mdCtx.digestFinal(mdCtx.getExpectedSize()); + if (!data) { + throwTypeError(globalObject, scope, "Failed to finalize digest"_s); + return JSValue::encode(jsBoolean(false)); + } + + // Create verification context + auto pkctx = keyPtr.newCtx(); + if (!pkctx || pkctx.initForVerify() <= 0) { + throwCryptoError(globalObject, scope, ERR_peek_error(), "Failed to initialize verification context"_s); + return JSValue::encode(jsBoolean(false)); + } + + // Set RSA padding mode and salt length if applicable + if (keyPtr.isRsaVariant()) { + if (!ncrypto::EVPKeyCtxPointer::setRsaPadding(pkctx.get(), padding, saltLen)) { + throwCryptoError(globalObject, scope, ERR_peek_error(), "Failed to set RSA padding"_s); + return JSValue::encode(jsBoolean(false)); + } + } + + // Set signature MD from the digest context + if (!pkctx.setSignatureMd(mdCtx)) { + throwCryptoError(globalObject, scope, ERR_peek_error(), "Failed to set signature message digest"_s); + return JSValue::encode(jsBoolean(false)); + } + + // Handle P1363 format conversion for EC keys if needed + ncrypto::Buffer sigBuf { + .data = static_cast(signatureBuffer->vector()), + .len = signatureBuffer->byteLength(), + }; + + if (dsaSigEnc == NodeCryptoKeys::DSASigEnc::P1363 && keyPtr.isSigVariant()) { + WTF::Vector derBuffer; + + if (convertP1363ToDER(sigBuf, keyPtr, derBuffer)) { + // Conversion succeeded, perform verification with the converted signature + ncrypto::Buffer derSigBuf { + .data = derBuffer.data(), + .len = derBuffer.size(), + }; + + bool result = pkctx.verify(derSigBuf, data); + return JSValue::encode(jsBoolean(result)); + } + // If conversion failed, fall through to use the original signature + } + + // Perform verification with the original signature + bool result = pkctx.verify(sigBuf, data); + return JSValue::encode(jsBoolean(result)); +} + +JSC_DEFINE_HOST_FUNCTION(jsVerifyOneShot, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ncrypto::ClearErrorOnReturn clearError; + + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto argCount = callFrame->argumentCount(); + + // Validate algorithm if provided + JSValue algorithmValue = callFrame->argument(0); + const EVP_MD* digest = nullptr; + if (!algorithmValue.isUndefinedOrNull()) { + Bun::V::validateString(scope, globalObject, algorithmValue, "algorithm"_s); + RETURN_IF_EXCEPTION(scope, {}); + + WTF::String algorithmName = algorithmValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + digest = ncrypto::getDigestByName(algorithmName); + if (!digest) { + return Bun::ERR::CRYPTO_INVALID_DIGEST(scope, globalObject, algorithmName); + } + } + + // Get data argument + JSValue dataValue = callFrame->argument(1); + JSC::JSArrayBufferView* dataView = getArrayBufferOrView(globalObject, scope, dataValue, "data"_s, jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + if (!dataView) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "data"_s, "Buffer, TypedArray, or DataView"_s, dataValue); + } + + // Get signature argument + JSValue signatureValue = callFrame->argument(3); + JSC::JSArrayBufferView* signatureView = getArrayBufferOrView(globalObject, scope, signatureValue, "signature"_s, jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + if (!signatureView) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "signature"_s, "Buffer, TypedArray, or DataView"_s, signatureValue); + } + + // Get key argument + JSValue keyValue = callFrame->argument(2); + + // Prepare the public or private key + std::optional maybeKeyPtr = preparePublicOrPrivateKey(globalObject, scope, keyValue); + ASSERT(!!scope.exception() == !maybeKeyPtr.has_value()); + if (!maybeKeyPtr) { + return {}; + } + ncrypto::EVPKeyPointer keyPtr = WTFMove(maybeKeyPtr.value()); + + // Get callback if provided + JSValue callbackValue; + bool hasCallback = false; + if (argCount > 4) { + callbackValue = callFrame->argument(4); + if (!callbackValue.isUndefined()) { + Bun::V::validateFunction(scope, globalObject, callbackValue, "callback"_s); + RETURN_IF_EXCEPTION(scope, {}); + hasCallback = true; + } + } + + // Get RSA padding mode and salt length if applicable + int32_t padding = getPadding(globalObject, keyValue, keyPtr); + RETURN_IF_EXCEPTION(scope, {}); + + std::optional saltLen = getSaltLength(globalObject, keyValue); + RETURN_IF_EXCEPTION(scope, {}); + + // Get DSA signature encoding format + NodeCryptoKeys::DSASigEnc dsaSigEnc = getDSASigEnc(globalObject, keyValue); + RETURN_IF_EXCEPTION(scope, {}); + + // Create data buffer + ncrypto::Buffer dataBuf { + .data = static_cast(dataView->vector()), + .len = dataView->byteLength() + }; + + // Create signature buffer + ncrypto::Buffer sigBuf { + .data = static_cast(signatureView->vector()), + .len = signatureView->byteLength() + }; + + // Create a new EVP_MD_CTX for verification + auto mdCtx = ncrypto::EVPMDCtxPointer::New(); + if (!mdCtx) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to create message digest context"_s); + return {}; + } + + // Initialize the context for verification with the key and digest + auto ctx = mdCtx.verifyInit(keyPtr, digest); + if (!ctx.has_value()) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to initialize verification context"_s); + return {}; + } + + // Apply RSA options if needed + if (keyPtr.isRsaVariant()) { + if (!ncrypto::EVPKeyCtxPointer::setRsaPadding(ctx.value(), padding, saltLen)) { + Bun::throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to set RSA padding"_s); + return {}; + } + } + + // Handle P1363 format conversion if needed + bool result = false; + if (dsaSigEnc == NodeCryptoKeys::DSASigEnc::P1363 && keyPtr.isSigVariant()) { + WTF::Vector derBuffer; + + if (convertP1363ToDER(sigBuf, keyPtr, derBuffer)) { + // Conversion succeeded, create a new buffer with the converted signature + ncrypto::Buffer derSigBuf { + .data = derBuffer.data(), + .len = derBuffer.size(), + }; + + // Perform verification with the converted signature + result = mdCtx.verify(dataBuf, derSigBuf); + } else { + // If conversion failed, try with the original signature + result = mdCtx.verify(dataBuf, sigBuf); + } + } else { + // Perform verification with the original signature + result = mdCtx.verify(dataBuf, sigBuf); + } + + // If we have a callback, call it with the result + if (hasCallback) { + JSC::MarkedArgumentBuffer args; + args.append(jsNull()); + args.append(jsBoolean(result)); + ASSERT(!args.hasOverflowed()); + + NakedPtr returnedException = nullptr; + JSC::CallData callData = JSC::getCallData(callbackValue); + JSC::profiledCall(globalObject, JSC::ProfilingReason::API, callbackValue, callData, JSC::jsUndefined(), args, returnedException); + RETURN_IF_EXCEPTION(scope, {}); + if (returnedException) { + scope.throwException(globalObject, returnedException.get()); + } + + return JSValue::encode(jsUndefined()); + } + + // Otherwise, return the result directly + return JSValue::encode(jsBoolean(result)); +} + +JSC_DEFINE_HOST_FUNCTION(callVerify, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + return JSValue::encode(JSVerify::create(vm, defaultGlobalObject(globalObject)->m_JSVerifyClassStructure.get(defaultGlobalObject(globalObject)), globalObject)); +} + +JSC_DEFINE_HOST_FUNCTION(constructVerify, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + return JSValue::encode(JSVerify::create(vm, defaultGlobalObject(globalObject)->m_JSVerifyClassStructure.get(defaultGlobalObject(globalObject)), globalObject)); +} + +void setupJSVerifyClassStructure(LazyClassStructure::Initializer& init) +{ + auto* prototypeStructure = JSVerifyPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()); + auto* prototype = JSVerifyPrototype::create(init.vm, init.global, prototypeStructure); + + auto* constructorStructure = JSVerifyConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()); + auto* constructor = JSVerifyConstructor::create(init.vm, constructorStructure, prototype); + + auto* structure = JSVerify::createStructure(init.vm, init.global, prototype); + + init.setPrototype(prototype); + init.setStructure(structure); + init.setConstructor(constructor); +} + +std::optional keyFromPublicString(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, const WTF::StringView& keyView) +{ + ncrypto::EVPKeyPointer::PublicKeyEncodingConfig publicConfig; + publicConfig.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + + UTF8View keyUtf8(keyView); + auto keySpan = keyUtf8.span(); + + ncrypto::Buffer ncryptoBuf { + .data = reinterpret_cast(keySpan.data()), + .len = keySpan.size(), + }; + + auto publicRes = ncrypto::EVPKeyPointer::TryParsePublicKey(publicConfig, ncryptoBuf); + if (publicRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(publicRes.value)); + return keyPtr; + } + + if (publicRes.error.value() == ncrypto::EVPKeyPointer::PKParseError::NOT_RECOGNIZED) { + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig privateConfig; + privateConfig.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + auto privateRes = ncrypto::EVPKeyPointer::TryParsePrivateKey(privateConfig, ncryptoBuf); + if (privateRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(privateRes.value)); + return keyPtr; + } + } + + throwCryptoError(lexicalGlobalObject, scope, publicRes.openssl_error.value_or(0), "Failed to read public key"_s); + return std::nullopt; +} + +std::optional preparePublicOrPrivateKey(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue maybeKey) +{ + VM& vm = lexicalGlobalObject->vm(); + + bool optionsBool = maybeKey.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Check if the key is provided + if (!optionsBool) { + Bun::ERR::CRYPTO_SIGN_KEY_REQUIRED(scope, lexicalGlobalObject); + return std::nullopt; + } + + if (!maybeKey.isCell()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, maybeKey); + return std::nullopt; + } + + auto optionsCell = maybeKey.asCell(); + auto optionsType = optionsCell->type(); + + // Handle CryptoKey directly + if (optionsCell->inherits()) { + auto* cryptoKey = jsCast(optionsCell); + + // Convert it to a key object, then to EVPKeyPointer + auto& key = cryptoKey->wrapped(); + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(keyValue.key); + return keyPtr; + } + + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } else if (maybeKey.isObject()) { + JSObject* optionsObj = optionsCell->getObject(); + const auto& names = WebCore::builtinNames(vm); + + // Check for native pointer (CryptoKey) + if (auto val = optionsObj->getIfPropertyExists(lexicalGlobalObject, names.bunNativePtrPrivateName())) { + if (val.isCell() && val.inherits()) { + auto* cryptoKey = jsCast(val.asCell()); + + auto& key = cryptoKey->wrapped(); + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(keyValue.key); + return keyPtr; + } + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } + } else if (optionsType >= Int8ArrayType && optionsType <= DataViewType) { + // Handle buffer input directly + auto dataBuf = KeyObject__GetBuffer(maybeKey); + if (dataBuf.hasException()) { + return std::nullopt; + } + + auto buffer = dataBuf.releaseReturnValue(); + ncrypto::Buffer ncryptoBuf { + .data = buffer.data(), + .len = buffer.size(), + }; + + // Try as public key first with default PEM format + ncrypto::EVPKeyPointer::PublicKeyEncodingConfig pubConfig; + pubConfig.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + + auto pubRes = ncrypto::EVPKeyPointer::TryParsePublicKey(pubConfig, ncryptoBuf); + if (pubRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(pubRes.value)); + return keyPtr; + } + + // If public key parsing fails, try as a private key + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig privConfig; + privConfig.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + + auto privRes = ncrypto::EVPKeyPointer::TryParsePrivateKey(privConfig, ncryptoBuf); + if (privRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(privRes.value)); + return keyPtr; + } + + if (privRes.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + Bun::ERR::MISSING_PASSPHRASE(scope, lexicalGlobalObject, "Passphrase required for encrypted key"_s); + return std::nullopt; + } + + throwCryptoError(lexicalGlobalObject, scope, privRes.openssl_error.value_or(0), "Failed to read key"_s); + return std::nullopt; + } + + // Handle options object with key property + JSValue key = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "key"_s)); + RETURN_IF_EXCEPTION(scope, {}); + JSValue formatValue = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "format"_s)); + RETURN_IF_EXCEPTION(scope, {}); + JSValue typeValue = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "type"_s)); + RETURN_IF_EXCEPTION(scope, {}); + JSValue passphrase = optionsObj->get(lexicalGlobalObject, Identifier::fromString(vm, "passphrase"_s)); + RETURN_IF_EXCEPTION(scope, {}); + + WTF::StringView formatStr = WTF::nullStringView(); + if (formatValue.isString()) { + auto str = formatValue.toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + formatStr = str->view(lexicalGlobalObject); + } + + if (!key.isCell()) { + if (formatStr == "jwk"_s) { + // Use our implementation of JWK key handling + bool isPublic = true; + return getKeyObjectHandleFromJwk(lexicalGlobalObject, scope, key, isPublic); + } else { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, key); + } + return std::nullopt; + } + + auto keyCell = key.asCell(); + auto keyCellType = keyCell->type(); + + // Handle CryptoKey in key property + if (keyCell->inherits()) { + auto* cryptoKey = jsCast(keyCell); + auto& key = cryptoKey->wrapped(); + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(keyValue.key); + return keyPtr; + } + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } else if (key.isObject()) { + JSObject* keyObj = key.getObject(); + if (auto keyVal = keyObj->getIfPropertyExists(lexicalGlobalObject, names.bunNativePtrPrivateName())) { + if (keyVal.isCell() && keyVal.inherits()) { + auto* cryptoKey = jsCast(keyVal.asCell()); + + auto& key = cryptoKey->wrapped(); + AsymmetricKeyValue keyValue(key); + if (keyValue.key) { + EVP_PKEY_up_ref(keyValue.key); + ncrypto::EVPKeyPointer keyPtr(WTFMove(keyValue.key)); + return keyPtr; + } + throwCryptoOperationFailed(lexicalGlobalObject, scope); + return std::nullopt; + } + } else if (keyCellType >= Int8ArrayType && keyCellType <= DataViewType) { + // Handle buffer in key property + auto dataBuf = KeyObject__GetBuffer(key); + if (dataBuf.hasException()) { + return std::nullopt; + } + + auto buffer = dataBuf.releaseReturnValue(); + ncrypto::Buffer ncryptoBuf { + .data = buffer.data(), + .len = buffer.size(), + }; + + // Parse format and type from options + auto format = parseKeyFormat(lexicalGlobalObject, formatValue, "options.format"_s, ncrypto::EVPKeyPointer::PKFormatType::PEM); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // If format is JWK, use our JWK implementation + if (format == ncrypto::EVPKeyPointer::PKFormatType::JWK) { + bool isPublic = true; + return getKeyObjectHandleFromJwk(lexicalGlobalObject, scope, key, isPublic); + } + + // Try as public key first + ncrypto::EVPKeyPointer::PublicKeyEncodingConfig pubConfig; + pubConfig.format = format; + + // Parse type for public key + auto pubType = parseKeyType(lexicalGlobalObject, typeValue, format == ncrypto::EVPKeyPointer::PKFormatType::DER, WTF::nullStringView(), std::nullopt, "options.type"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (pubType.has_value()) { + pubConfig.type = pubType.value(); + } + + auto pubRes = ncrypto::EVPKeyPointer::TryParsePublicKey(pubConfig, ncryptoBuf); + if (pubRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(pubRes.value)); + return keyPtr; + } + + // If public key parsing fails, try as a private key + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig privConfig; + privConfig.format = format; + + // Parse type for private key + auto privType = parseKeyType(lexicalGlobalObject, typeValue, format == ncrypto::EVPKeyPointer::PKFormatType::DER, WTF::nullStringView(), false, "options.type"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (privType.has_value()) { + privConfig.type = privType.value(); + } + + privConfig.passphrase = passphraseFromBufferSource(lexicalGlobalObject, scope, passphrase); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + auto privRes = ncrypto::EVPKeyPointer::TryParsePrivateKey(privConfig, ncryptoBuf); + if (privRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(privRes.value)); + return keyPtr; + } + + if (privRes.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + Bun::ERR::MISSING_PASSPHRASE(scope, lexicalGlobalObject, "Passphrase required for encrypted key"_s); + return std::nullopt; + } + + throwCryptoError(lexicalGlobalObject, scope, privRes.openssl_error.value_or(0), "Failed to read key"_s); + return std::nullopt; + } else if (formatStr == "jwk"_s) { + // Use our implementation of JWK key handling + bool isPublic = true; + return getKeyObjectHandleFromJwk(lexicalGlobalObject, scope, key, isPublic); + } + } else if (key.isString()) { + // Handle string key + WTF::String keyStr = key.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Parse format and type from options + auto format = parseKeyFormat(lexicalGlobalObject, formatValue, "options.format"_s, ncrypto::EVPKeyPointer::PKFormatType::PEM); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // If format is JWK, use our JWK implementation + if (format == ncrypto::EVPKeyPointer::PKFormatType::JWK) { + bool isPublic = true; + return getKeyObjectHandleFromJwk(lexicalGlobalObject, scope, key, isPublic); + } + + // Try as public key first with specified format and type + UTF8View keyUtf8(keyStr); + auto keySpan = keyUtf8.span(); + + ncrypto::Buffer ncryptoBuf { + .data = reinterpret_cast(keySpan.data()), + .len = keySpan.size(), + }; + + // Try as public key first + ncrypto::EVPKeyPointer::PublicKeyEncodingConfig pubConfig; + pubConfig.format = format; + + // Parse type for public key + auto pubType = parseKeyType(lexicalGlobalObject, typeValue, format == ncrypto::EVPKeyPointer::PKFormatType::DER, WTF::nullStringView(), std::nullopt, "options.type"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (pubType.has_value()) { + pubConfig.type = pubType.value(); + } + + auto pubRes = ncrypto::EVPKeyPointer::TryParsePublicKey(pubConfig, ncryptoBuf); + if (pubRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(pubRes.value)); + return keyPtr; + } + + // If public key parsing fails, try as a private key + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig privConfig; + privConfig.format = format; + + // Parse type for private key + auto privType = parseKeyType(lexicalGlobalObject, typeValue, format == ncrypto::EVPKeyPointer::PKFormatType::DER, WTF::nullStringView(), false, "options.type"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (privType.has_value()) { + privConfig.type = privType.value(); + } + + privConfig.passphrase = passphraseFromBufferSource(lexicalGlobalObject, scope, passphrase); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + auto privRes = ncrypto::EVPKeyPointer::TryParsePrivateKey(privConfig, ncryptoBuf); + if (privRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(privRes.value)); + return keyPtr; + } + + if (privRes.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + Bun::ERR::MISSING_PASSPHRASE(scope, lexicalGlobalObject, "Passphrase required for encrypted key"_s); + return std::nullopt; + } + + throwCryptoError(lexicalGlobalObject, scope, privRes.openssl_error.value_or(0), "Failed to read key"_s); + return std::nullopt; + } + + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, key); + return std::nullopt; + } else if (maybeKey.isString()) { + // Handle string key directly + WTF::String keyStr = maybeKey.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Try as public key first with default PEM format + UTF8View keyUtf8(keyStr); + auto keySpan = keyUtf8.span(); + + ncrypto::Buffer ncryptoBuf { + .data = reinterpret_cast(keySpan.data()), + .len = keySpan.size(), + }; + + // Try as public key first + ncrypto::EVPKeyPointer::PublicKeyEncodingConfig pubConfig; + pubConfig.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + + auto pubRes = ncrypto::EVPKeyPointer::TryParsePublicKey(pubConfig, ncryptoBuf); + if (pubRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(pubRes.value)); + return keyPtr; + } + + // If public key parsing fails, try as a private key + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig privConfig; + privConfig.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + + auto privRes = ncrypto::EVPKeyPointer::TryParsePrivateKey(privConfig, ncryptoBuf); + if (privRes) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(privRes.value)); + return keyPtr; + } + + if (privRes.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + Bun::ERR::MISSING_PASSPHRASE(scope, lexicalGlobalObject, "Passphrase required for encrypted key"_s); + return std::nullopt; + } + + throwCryptoError(lexicalGlobalObject, scope, privRes.openssl_error.value_or(0), "Failed to read key"_s); + return std::nullopt; + } + + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, maybeKey); + return std::nullopt; +} + +// Implements the getKeyObjectHandleFromJwk function similar to Node.js implementation +std::optional getKeyObjectHandleFromJwk(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue key, bool isPublic) +{ + auto& vm = lexicalGlobalObject->vm(); + + // Validate that key is an object + Bun::V::validateObject(scope, lexicalGlobalObject, key, "key.key"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + JSObject* keyObj = key.getObject(); + + // Get and validate key.kty + JSValue ktyValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "kty"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!ktyValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.kty"_s, "string"_s, ktyValue); + return std::nullopt; + } + + WTF::String kty = ktyValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Validate kty is one of the supported types + const WTF::Vector validKeyTypes = { "RSA"_s, "EC"_s, "OKP"_s }; + bool isValidType = false; + for (const auto& validType : validKeyTypes) { + if (kty == validType) { + isValidType = true; + break; + } + } + + if (!isValidType) { + Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "key.kty"_s, "must be one of: "_s, ktyValue, validKeyTypes); + return std::nullopt; + } + + // Handle OKP keys + if (kty == "OKP"_s) { + // Get and validate key.crv + JSValue crvValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "crv"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!crvValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.crv"_s, "string"_s, crvValue); + return std::nullopt; + } + + WTF::String crv = crvValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Validate crv is one of the supported curves + const WTF::Vector validCurves = { "Ed25519"_s, "Ed448"_s, "X25519"_s, "X448"_s }; + bool validCurve = false; + for (const auto& validCurveType : validCurves) { + if (crv == validCurveType) { + validCurve = true; + break; + } + } + + if (!validCurve) { + Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "key.crv"_s, "must be one of: "_s, crvValue, validCurves); + return std::nullopt; + } + + // Get and validate key.x + JSValue xValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "x"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!xValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.x"_s, "string"_s, xValue); + return std::nullopt; + } + + WTF::String xStr = xValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // For private keys, validate key.d + WTF::String dStr; + if (!isPublic) { + JSValue dValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "d"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!dValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.d"_s, "string"_s, dValue); + return std::nullopt; + } + + dStr = dValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + } + + // Convert base64 strings to binary data + Vector keyData; + if (isPublic) { + auto xData = WTF::base64Decode(xStr); + // auto xData = WTF::base64Decode(xStr); + if (!xData) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } + keyData = WTFMove(*xData); + } else { + auto dData = WTF::base64Decode(dStr); + if (!dData) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } + keyData = WTFMove(*dData); + } + + // Validate key length based on curve + if ((crv == "Ed25519"_s || crv == "X25519"_s) && keyData.size() != 32) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } else if (crv == "Ed448"_s && keyData.size() != 57) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } else if (crv == "X448"_s && keyData.size() != 56) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } + + // Create the key + int nid = 0; + if (crv == "Ed25519"_s) { + nid = EVP_PKEY_ED25519; + } else if (crv == "Ed448"_s) { + nid = EVP_PKEY_ED448; + } else if (crv == "X25519"_s) { + nid = EVP_PKEY_X25519; + } else if (crv == "X448"_s) { + nid = EVP_PKEY_X448; + } + + ncrypto::Buffer buffer { + .data = keyData.data(), + .len = keyData.size(), + }; + + if (isPublic) { + return ncrypto::EVPKeyPointer::NewRawPublic(nid, buffer); + } else { + return ncrypto::EVPKeyPointer::NewRawPrivate(nid, buffer); + } + } + // Handle EC keys + else if (kty == "EC"_s) { + // Get and validate key.crv + JSValue crvValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "crv"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!crvValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.crv"_s, "string"_s, crvValue); + return std::nullopt; + } + + WTF::String crv = crvValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + // Validate crv is one of the supported curves + const WTF::Vector validCurves = { "P-256"_s, "secp256k1"_s, "P-384"_s, "P-521"_s }; + bool validCurve = false; + for (const auto& validCurveType : validCurves) { + if (crv == validCurveType) { + validCurve = true; + break; + } + } + + if (!validCurve) { + Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "key.crv"_s, "must be one of:"_s, crvValue, validCurves); + return std::nullopt; + } + + // Get and validate key.x and key.y + JSValue xValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "x"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!xValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.x"_s, "string"_s, xValue); + return std::nullopt; + } + + JSValue yValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "y"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!yValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.y"_s, "string"_s, yValue); + return std::nullopt; + } + + // For private keys, validate key.d + if (!isPublic) { + JSValue dValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "d"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!dValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.d"_s, "string"_s, dValue); + return std::nullopt; + } + } + + // Convert to WebCrypto JsonWebKey and use existing implementation + auto jwk = WebCore::JsonWebKey(); + jwk.kty = kty; + jwk.crv = crv; + jwk.x = xValue.toWTFString(lexicalGlobalObject); + jwk.y = yValue.toWTFString(lexicalGlobalObject); + + if (!isPublic) { + jwk.d = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "d"_s)).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + } + + // Use the WebCrypto implementation to import the key + RefPtr result; + if (isPublic) { + result = WebCore::CryptoKeyEC::importJwk(WebCore::CryptoAlgorithmIdentifier::ECDSA, crv, WTFMove(jwk), true, WebCore::CryptoKeyUsageVerify); + } else { + result = WebCore::CryptoKeyEC::importJwk(WebCore::CryptoAlgorithmIdentifier::ECDSA, crv, WTFMove(jwk), true, WebCore::CryptoKeyUsageSign); + } + + if (!result) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } + + // Convert CryptoKeyEC to EVPKeyPointer + AsymmetricKeyValue keyValue(*result); + if (!keyValue.key) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } + + EVP_PKEY_up_ref(keyValue.key); + return ncrypto::EVPKeyPointer(keyValue.key); + } + // Handle RSA keys + else if (kty == "RSA"_s) { + // Get and validate key.n and key.e + JSValue nValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "n"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!nValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.n"_s, "string"_s, nValue); + return std::nullopt; + } + + JSValue eValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "e"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!eValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.e"_s, "string"_s, eValue); + return std::nullopt; + } + + // For private keys, validate additional parameters + if (!isPublic) { + JSValue dValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "d"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!dValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.d"_s, "string"_s, dValue); + return std::nullopt; + } + + JSValue pValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "p"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!pValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.p"_s, "string"_s, pValue); + return std::nullopt; + } + + JSValue qValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "q"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!qValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.q"_s, "string"_s, qValue); + return std::nullopt; + } + + JSValue dpValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "dp"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!dpValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.dp"_s, "string"_s, dpValue); + return std::nullopt; + } + + JSValue dqValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "dq"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!dqValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.dq"_s, "string"_s, dqValue); + return std::nullopt; + } + + JSValue qiValue = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "qi"_s)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!qiValue.isString()) { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key.qi"_s, "string"_s, qiValue); + return std::nullopt; + } + } + + // Convert to WebCrypto JsonWebKey and use existing implementation + auto jwk = WebCore::JsonWebKey(); + jwk.kty = kty; + jwk.n = nValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + jwk.e = eValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (!isPublic) { + jwk.d = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "d"_s)).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + jwk.p = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "p"_s)).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + jwk.q = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "q"_s)).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + jwk.dp = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "dp"_s)).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + jwk.dq = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "dq"_s)).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + jwk.qi = keyObj->get(lexicalGlobalObject, Identifier::fromString(vm, "qi"_s)).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + } + + // Use the WebCrypto implementation to import the key + RefPtr result; + if (isPublic) { + result = WebCore::CryptoKeyRSA::importJwk(WebCore::CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5, std::nullopt, WTFMove(jwk), true, WebCore::CryptoKeyUsageVerify); + } else { + result = WebCore::CryptoKeyRSA::importJwk(WebCore::CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5, std::nullopt, WTFMove(jwk), true, WebCore::CryptoKeyUsageSign); + } + + if (!result) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } + + // Convert CryptoKeyRSA to EVPKeyPointer + AsymmetricKeyValue keyValue(*result); + if (!keyValue.key) { + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; + } + + EVP_PKEY_up_ref(keyValue.key); + return ncrypto::EVPKeyPointer(keyValue.key); + } + + // Should never reach here due to earlier validation + Bun::ERR::CRYPTO_INVALID_JWK(scope, lexicalGlobalObject); + return std::nullopt; +} + +bool convertP1363ToDER(const ncrypto::Buffer& p1363Sig, + const ncrypto::EVPKeyPointer& pkey, + WTF::Vector& derBuffer) +{ + // Get the size of r and s components from the key + auto bytesOfRS = pkey.getBytesOfRS(); + if (!bytesOfRS) { + // If we can't get the bytes of RS, this is not a signature variant + // that we can convert. Return false to indicate that the original + // signature should be used. + return false; + } + + size_t bytesOfRSValue = bytesOfRS.value(); + + // Check if the signature size is valid (should be 2 * bytesOfRS) + if (p1363Sig.len != 2 * bytesOfRSValue) { + // If the signature size doesn't match what we expect, return false + // to indicate that the original signature should be used. + return false; + } + + // Create BignumPointers for r and s components + ncrypto::BignumPointer r(p1363Sig.data, bytesOfRSValue); + if (!r) { + return false; + } + + ncrypto::BignumPointer s(p1363Sig.data + bytesOfRSValue, bytesOfRSValue); + if (!s) { + return false; + } + + // Create a new ECDSA_SIG structure and set r and s components + auto asn1_sig = ncrypto::ECDSASigPointer::New(); + if (!asn1_sig) { + return false; + } + + if (!asn1_sig.setParams(std::move(r), std::move(s))) { + return false; + } + + // Encode the signature in DER format + auto buf = asn1_sig.encode(); + if (buf.len < 0) { + return false; + } + + if (!derBuffer.tryAppend(std::span { buf.data, buf.len })) { + return false; + } + + return true; +} + +} // namespace Bun diff --git a/src/bun.js/bindings/node/crypto/JSVerify.h b/src/bun.js/bindings/node/crypto/JSVerify.h new file mode 100644 index 0000000000..9ef45293c4 --- /dev/null +++ b/src/bun.js/bindings/node/crypto/JSVerify.h @@ -0,0 +1,85 @@ +#pragma once + +#include "root.h" +#include "JSBuffer.h" +#include "helpers.h" +#include "ncrypto.h" +#include +#include + +namespace Bun { + +class JSVerify; +class JSVerifyPrototype; +class JSVerifyConstructor; + +// Function to handle JWK format keys +std::optional getKeyObjectHandleFromJwk(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue key, bool isPublic); + +class JSVerify final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSVerify* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); + + DECLARE_INFO; + + ncrypto::EVPMDCtxPointer m_mdCtx; + +private: + JSVerify(JSC::VM& vm, JSC::Structure* structure); + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject); +}; + +class JSVerifyPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSVerifyPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + + DECLARE_INFO; + +private: + JSVerifyPrototype(JSC::VM& vm, JSC::Structure* structure); + void finishCreation(JSC::VM& vm); +}; + +class JSVerifyConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSVerifyConstructor* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSObject* prototype); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.internalFunctionSpace(); + } + + DECLARE_INFO; + +private: + JSVerifyConstructor(JSC::VM& vm, JSC::Structure* structure); + void finishCreation(JSC::VM& vm, JSC::JSObject* prototype); +}; + +JSC_DECLARE_HOST_FUNCTION(jsVerifyOneShot); + +void setupJSVerifyClassStructure(JSC::LazyClassStructure::Initializer& init); + +} // namespace Bun diff --git a/src/bun.js/bindings/NodeCrypto.cpp b/src/bun.js/bindings/node/crypto/NodeCrypto.cpp similarity index 88% rename from src/bun.js/bindings/NodeCrypto.cpp rename to src/bun.js/bindings/node/crypto/NodeCrypto.cpp index be7ec8b08f..3e05218d03 100644 --- a/src/bun.js/bindings/NodeCrypto.cpp +++ b/src/bun.js/bindings/node/crypto/NodeCrypto.cpp @@ -40,6 +40,8 @@ #include "ncrypto.h" #include "AsymmetricKeyValue.h" #include "NodeValidator.h" +#include "JSSign.h" +#include "JSVerify.h" using namespace JSC; using namespace Bun; @@ -114,7 +116,9 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHConvertKey, (JSC::JSGlobalObject * lexicalGlobalO if (keyBuffer.hasException()) return JSValue::encode(jsUndefined()); - if (keyBuffer.returnValue().isEmpty()) + auto buffer = keyBuffer.releaseReturnValue(); + + if (buffer.size() == 0) return JSValue::encode(JSC::jsEmptyString(vm)); auto curveName = callFrame->argument(1).toWTFString(lexicalGlobalObject); @@ -133,8 +137,8 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHConvertKey, (JSC::JSGlobalObject * lexicalGlobalO if (!point) return throwVMError(lexicalGlobalObject, scope, "Failed to create EC_POINT"_s); - const unsigned char* key_data = keyBuffer.returnValue().data(); - size_t key_length = keyBuffer.returnValue().size(); + const unsigned char* key_data = buffer.data(); + size_t key_length = buffer.size(); if (!EC_POINT_oct2point(group, point, key_data, key_length, nullptr)) return throwVMError(lexicalGlobalObject, scope, "Failed to convert Buffer to EC_POINT"_s); @@ -236,12 +240,17 @@ JSC_DEFINE_HOST_FUNCTION(jsCertVerifySpkac, (JSC::JSGlobalObject * lexicalGlobal return JSValue::encode(jsUndefined()); } - auto buffer = input.returnValue(); + auto buffer = input.releaseReturnValue(); if (buffer.size() > std::numeric_limits().max()) { return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "spkac"_s, 0, std::numeric_limits().max(), jsNumber(buffer.size())); } - bool result = ncrypto::VerifySpkac(reinterpret_cast(buffer.data()), buffer.size()); + ncrypto::Buffer buf { + .data = reinterpret_cast(buffer.data()), + .len = buffer.size() + }; + + bool result = ncrypto::VerifySpkac(buf); return JSValue::encode(JSC::jsBoolean(result)); } @@ -255,12 +264,17 @@ JSC_DEFINE_HOST_FUNCTION(jsCertExportPublicKey, (JSC::JSGlobalObject * lexicalGl return JSValue::encode(jsEmptyString(vm)); } - auto buffer = input.returnValue(); + auto buffer = input.releaseReturnValue(); if (buffer.size() > std::numeric_limits().max()) { return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "spkac"_s, 0, std::numeric_limits().max(), jsNumber(buffer.size())); } - auto bio = ncrypto::ExportPublicKey(reinterpret_cast(buffer.data()), buffer.size()); + ncrypto::Buffer buf { + .data = reinterpret_cast(buffer.data()), + .len = buffer.size() + }; + + auto bio = ncrypto::ExportPublicKey(buf); if (!bio) { return JSValue::encode(jsEmptyString(vm)); } @@ -284,22 +298,27 @@ JSC_DEFINE_HOST_FUNCTION(jsCertExportChallenge, (JSC::JSGlobalObject * lexicalGl return JSValue::encode(jsEmptyString(vm)); } - auto buffer = input.returnValue(); + auto buffer = input.releaseReturnValue(); if (buffer.size() > std::numeric_limits().max()) { return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "spkac"_s, 0, std::numeric_limits().max(), jsNumber(buffer.size())); } - auto cert = ncrypto::ExportChallenge(reinterpret_cast(buffer.data()), buffer.size()); - if (!cert.data || cert.len == 0) { + ncrypto::Buffer buf { + .data = reinterpret_cast(buffer.data()), + .len = buffer.size() + }; + + auto cert = ncrypto::ExportChallenge(buf); + if (!cert || cert.size() == 0) { return JSValue::encode(jsEmptyString(vm)); } - auto result = JSC::ArrayBuffer::tryCreate({ reinterpret_cast(cert.data), cert.len }); + auto result = JSC::ArrayBuffer::tryCreate({ reinterpret_cast(cert.get()), cert.size() }); if (!result) { return JSValue::encode(jsEmptyString(vm)); } - auto* bufferResult = JSC::JSUint8Array::create(lexicalGlobalObject, reinterpret_cast(lexicalGlobalObject)->JSBufferSubclassStructure(), WTFMove(result), 0, cert.len); + auto* bufferResult = JSC::JSUint8Array::create(lexicalGlobalObject, reinterpret_cast(lexicalGlobalObject)->JSBufferSubclassStructure(), WTFMove(result), 0, cert.size()); return JSValue::encode(bufferResult); } @@ -323,9 +342,9 @@ JSC_DEFINE_HOST_FUNCTION(jsGetCipherInfo, (JSC::JSGlobalObject * lexicalGlobalOb // Get cipher from name or nid ncrypto::Cipher cipher; if (callFrame->argument(1).isString()) { - auto cipherName = callFrame->argument(1).toWTFString(lexicalGlobalObject); + JSString* cipherName = callFrame->argument(1).toString(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - cipher = ncrypto::Cipher::FromName(cipherName.utf8().data()); + cipher = ncrypto::Cipher::FromName(cipherName->view(lexicalGlobalObject)); } else if (callFrame->argument(1).isInt32()) { int nid = callFrame->argument(1).asInt32(); cipher = ncrypto::Cipher::FromNid(nid); @@ -376,17 +395,17 @@ JSC_DEFINE_HOST_FUNCTION(jsGetCipherInfo, (JSC::JSGlobalObject * lexicalGlobalOb } // Set mode if available - auto mode_label = cipher.getModeLabel(); - if (!mode_label.empty()) { + WTF::ASCIILiteral mode_label = cipher.getModeLabel(); + if (!mode_label.isEmpty()) { info->putDirect(vm, PropertyName(Identifier::fromString(vm, "mode"_s)), - jsString(vm, String::fromUTF8({ mode_label.data(), mode_label.length() }))); + jsString(vm, String::fromUTF8(mode_label))); RETURN_IF_EXCEPTION(scope, {}); } // Set name - auto name = cipher.getName(); + WTF::String name = cipher.getName(); info->putDirect(vm, vm.propertyNames->name, - jsString(vm, String::fromUTF8({ name.data(), name.length() }))); + jsString(vm, name)); RETURN_IF_EXCEPTION(scope, {}); // Set nid @@ -427,7 +446,7 @@ JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject) JSFunction::create(vm, globalObject, 3, "ecdhConvertKey"_s, jsECDHConvertKey, ImplementationVisibility::Public, NoIntrinsic), 0); obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "certVerifySpkac"_s)), - JSFunction::create(vm, globalObject, 1, "verifySpkac"_s, jsCertVerifySpkac, ImplementationVisibility::Public, NoIntrinsic), 1); + JSFunction::create(vm, globalObject, 1, "verifySpkac"_s, jsCertVerifySpkac, ImplementationVisibility::Public, NoIntrinsic), 0); obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "certExportPublicKey"_s)), JSFunction::create(vm, globalObject, 1, "certExportPublicKey"_s, jsCertExportPublicKey, ImplementationVisibility::Public, NoIntrinsic), 1); obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "certExportChallenge"_s)), @@ -438,7 +457,16 @@ JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject) obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getCiphers"_s)), JSFunction::create(vm, globalObject, 0, "getCiphers"_s, jsGetCiphers, ImplementationVisibility::Public, NoIntrinsic), 0); obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "_getCipherInfo"_s)), - JSFunction::create(vm, globalObject, 1, "_getCipherInfo"_s, jsGetCipherInfo, ImplementationVisibility::Public, NoIntrinsic), 4); + JSFunction::create(vm, globalObject, 1, "_getCipherInfo"_s, jsGetCipherInfo, ImplementationVisibility::Public, NoIntrinsic), 0); + + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "Sign"_s)), + globalObject->m_JSSignClassStructure.constructor(globalObject)); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "sign"_s)), + JSFunction::create(vm, globalObject, 4, "sign"_s, jsSignOneShot, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "Verify"_s)), + globalObject->m_JSVerifyClassStructure.constructor(globalObject)); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "verify"_s)), + JSFunction::create(vm, globalObject, 4, "verify"_s, jsVerifyOneShot, ImplementationVisibility::Public, NoIntrinsic), 0); return obj; } diff --git a/src/bun.js/bindings/NodeCrypto.h b/src/bun.js/bindings/node/crypto/NodeCrypto.h similarity index 100% rename from src/bun.js/bindings/NodeCrypto.h rename to src/bun.js/bindings/node/crypto/NodeCrypto.h diff --git a/src/bun.js/bindings/node/crypto/util.cpp b/src/bun.js/bindings/node/crypto/util.cpp new file mode 100644 index 0000000000..1b350603ea --- /dev/null +++ b/src/bun.js/bindings/node/crypto/util.cpp @@ -0,0 +1,355 @@ +#include "util.h" +#include +#include +#include "ErrorCode.h" +#include "ncrypto.h" +#include "BunString.h" +#include "JSBuffer.h" +#include "JSDOMConvertEnumeration.h" + +namespace Bun { + +using namespace JSC; + +std::optional keyFromString(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, const WTF::StringView& keyView, JSValue passphraseValue) +{ + ncrypto::EVPKeyPointer::PrivateKeyEncodingConfig config; + config.format = ncrypto::EVPKeyPointer::PKFormatType::PEM; + + config.passphrase = passphraseFromBufferSource(lexicalGlobalObject, scope, passphraseValue); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + UTF8View keyUtf8(keyView); + + auto keySpan = keyUtf8.span(); + + ncrypto::Buffer ncryptoBuf { + .data = reinterpret_cast(keySpan.data()), + .len = keySpan.size(), + }; + auto res = ncrypto::EVPKeyPointer::TryParsePrivateKey(config, ncryptoBuf); + if (res) { + ncrypto::EVPKeyPointer keyPtr(WTFMove(res.value)); + return keyPtr; + } + + if (res.error.value() == ncrypto::EVPKeyPointer::PKParseError::NEED_PASSPHRASE) { + Bun::ERR::MISSING_PASSPHRASE(scope, lexicalGlobalObject, "Passphrase required for encrypted key"_s); + return std::nullopt; + } + + throwCryptoError(lexicalGlobalObject, scope, res.openssl_error.value_or(0), "Failed to read private key"_s); + return std::nullopt; +} + +ncrypto::EVPKeyPointer::PKFormatType parseKeyFormat(JSC::JSGlobalObject* globalObject, JSValue formatValue, WTF::ASCIILiteral optionName, std::optional defaultFormat) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + + if (formatValue.isUndefined() && defaultFormat) { + return defaultFormat.value(); + } + + if (!formatValue.isString()) { + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, optionName, formatValue); + return {}; + } + + WTF::String formatStr = formatValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + if (formatStr == "pem"_s) { + return ncrypto::EVPKeyPointer::PKFormatType::PEM; + } + + if (formatStr == "der"_s) { + return ncrypto::EVPKeyPointer::PKFormatType::DER; + } + + if (formatStr == "jwk"_s) { + return ncrypto::EVPKeyPointer::PKFormatType::JWK; + } + + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, optionName, formatValue); + return {}; +} + +std::optional parseKeyType(JSC::JSGlobalObject* globalObject, JSValue typeValue, bool required, WTF::StringView keyType, std::optional isPublic, WTF::ASCIILiteral optionName) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + + if (typeValue.isUndefined() && !required) { + return std::nullopt; + } + + if (!typeValue.isString()) { + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, optionName, typeValue); + return std::nullopt; + } + + WTF::String typeStr = typeValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (typeStr == "pkcs1"_s) { + if (keyType && keyType != "rsa"_s) { + Bun::ERR::CRYPTO_INCOMPATIBLE_KEY_OPTIONS(scope, globalObject, "pkcs1"_s, "can only be used for RSA keys"_s); + return std::nullopt; + } + return ncrypto::EVPKeyPointer::PKEncodingType::PKCS1; + } else if (typeStr == "spki"_s && isPublic != false) { + return ncrypto::EVPKeyPointer::PKEncodingType::SPKI; + } else if (typeStr == "pkcs8"_s && isPublic != true) { + return ncrypto::EVPKeyPointer::PKEncodingType::PKCS8; + } else if (typeStr == "sec1"_s && isPublic != true) { + if (keyType && keyType != "ec"_s) { + Bun::ERR::CRYPTO_INCOMPATIBLE_KEY_OPTIONS(scope, globalObject, "sec1"_s, "can only be used for EC keys"_s); + return std::nullopt; + } + return ncrypto::EVPKeyPointer::PKEncodingType::SEC1; + } + + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, optionName, typeValue); + return std::nullopt; +} + +std::optional passphraseFromBufferSource(JSC::JSGlobalObject* globalObject, ThrowScope& scope, JSValue input) +{ + if (input.isUndefinedOrNull()) { + return std::nullopt; + } + + if (input.isString()) { + WTF::String passphraseStr = input.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + UTF8View utf8(passphraseStr); + + auto span = utf8.span(); + if (auto ptr = ncrypto::DataPointer::Alloc(span.size())) { + memcpy(ptr.get(), span.data(), span.size()); + return WTFMove(ptr); + } + + throwOutOfMemoryError(globalObject, scope); + return std::nullopt; + } + + if (auto* array = jsDynamicCast(input)) { + if (array->isDetached()) { + throwTypeError(globalObject, scope, "passphrase must not be detached"_s); + return std::nullopt; + } + + auto length = array->byteLength(); + if (auto ptr = ncrypto::DataPointer::Alloc(length)) { + memcpy(ptr.get(), array->vector(), length); + return WTFMove(ptr); + } + + throwOutOfMemoryError(globalObject, scope); + return std::nullopt; + } + + throwTypeError(globalObject, scope, "passphrase must be a Buffer or string"_s); + return std::nullopt; +} + +// Throws a crypto error with optional OpenSSL error details +void throwCryptoError(JSC::JSGlobalObject* globalObject, ThrowScope& scope, unsigned long err, const char* message) +{ + JSC::VM& vm = globalObject->vm(); + + // Format OpenSSL error message if err is provided + char message_buffer[128] = { 0 }; + if (err != 0 || message == nullptr) { + ERR_error_string_n(err, message_buffer, sizeof(message_buffer)); + message = message_buffer; + } + + WTF::String errorMessage = WTF::String::fromUTF8(message); + RETURN_IF_EXCEPTION(scope, void()); + + // Create error object with the message + JSC::JSObject* errorObject = createTypeError(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + + PutPropertySlot messageSlot(errorObject, false); + errorObject->put(errorObject, globalObject, Identifier::fromString(vm, "message"_s), jsString(vm, errorMessage), messageSlot); + RETURN_IF_EXCEPTION(scope, void()); + + ncrypto::CryptoErrorList errorStack; + errorStack.capture(); + + // If there's an OpenSSL error code, decorate the error object with additional info + if (err != 0) { + // Get library, function and reason strings from OpenSSL + const char* lib = ERR_lib_error_string(err); + const char* func = ERR_func_error_string(err); + const char* reason = ERR_reason_error_string(err); + + // Add library info if available + if (lib) { + WTF::String libString = WTF::String::fromUTF8(lib); + PutPropertySlot slot(errorObject, false); + errorObject->put(errorObject, globalObject, Identifier::fromString(vm, "library"_s), jsString(vm, libString), slot); + RETURN_IF_EXCEPTION(scope, void()); + } + + // Add function info if available + if (func) { + WTF::String funcString = WTF::String::fromUTF8(func); + PutPropertySlot slot(errorObject, false); + + errorObject->put(errorObject, globalObject, Identifier::fromString(vm, "function"_s), jsString(vm, funcString), slot); + RETURN_IF_EXCEPTION(scope, void()); + } + + // Add reason info if available + if (reason) { + WTF::String reasonString = WTF::String::fromUTF8(reason); + PutPropertySlot reasonSlot(errorObject, false); + + errorObject->put(errorObject, globalObject, Identifier::fromString(vm, "reason"_s), jsString(vm, reasonString), reasonSlot); + RETURN_IF_EXCEPTION(scope, void()); + + // Convert reason to error code (e.g. "this error" -> "ERR_OSSL_THIS_ERROR") + String upperReason = reasonString.convertToASCIIUppercase(); + String code = makeString("ERR_OSSL_"_s, upperReason); + + PutPropertySlot codeSlot(errorObject, false); + errorObject->put(errorObject, globalObject, Identifier::fromString(vm, "code"_s), jsString(vm, code), codeSlot); + RETURN_IF_EXCEPTION(scope, void()); + } + } + + // If there are multiple errors, add them to the error stack + if (errorStack.size() > 0) { + PutPropertySlot stackSlot(errorObject, false); + auto arr = JSC::constructEmptyArray(globalObject, nullptr, errorStack.size()); + RETURN_IF_EXCEPTION(scope, void()); + for (int32_t i = 0; i < errorStack.size(); i++) { + WTF::String error = errorStack.pop_back().value(); + arr->putDirectIndex(globalObject, i, jsString(vm, error)); + } + errorObject->put(errorObject, globalObject, Identifier::fromString(vm, "opensslErrorStack"_s), arr, stackSlot); + RETURN_IF_EXCEPTION(scope, void()); + } + + // Throw the decorated error + throwException(globalObject, scope, errorObject); +} + +std::optional getIntOption(JSC::JSGlobalObject* globalObject, JSValue options, WTF::ASCIILiteral name) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::JSValue value = options.get(globalObject, JSC::Identifier::fromString(vm, name)); + RETURN_IF_EXCEPTION(scope, std::nullopt); + + if (value.isUndefined()) + return std::nullopt; + + if (!value.isInt32()) { + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, makeString("options."_s, name), value); + return std::nullopt; + } + + return value.asInt32(); +} + +int32_t getPadding(JSC::JSGlobalObject* globalObject, JSValue options, const ncrypto::EVPKeyPointer& pkey) +{ + auto padding = getIntOption(globalObject, options, "padding"_s); + return padding.value_or(pkey.getDefaultSignPadding()); +} + +std::optional getSaltLength(JSC::JSGlobalObject* globalObject, JSValue options) +{ + return getIntOption(globalObject, options, "saltLength"_s); +} + +NodeCryptoKeys::DSASigEnc getDSASigEnc(JSC::JSGlobalObject* globalObject, JSValue options) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + if (!options.isObject() || options.asCell()->type() != JSC::JSType::FinalObjectType) { + return NodeCryptoKeys::DSASigEnc::DER; + } + + JSValue dsaEncoding = options.get(globalObject, Identifier::fromString(globalObject->vm(), "dsaEncoding"_s)); + RETURN_IF_EXCEPTION(scope, {}); + + if (dsaEncoding.isUndefined()) { + return NodeCryptoKeys::DSASigEnc::DER; + } + + if (!dsaEncoding.isString()) { + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, "options.dsaEncoding"_s, dsaEncoding); + return {}; + } + + WTF::String dsaEncodingStr = dsaEncoding.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + if (dsaEncodingStr == "der"_s) { + return NodeCryptoKeys::DSASigEnc::DER; + } + + if (dsaEncodingStr == "ieee-p1363"_s) { + return NodeCryptoKeys::DSASigEnc::P1363; + } + + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, "options.dsaEncoding"_s, dsaEncoding); + return {}; +} + +JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, ThrowScope& scope, JSValue value, ASCIILiteral argName, JSValue encodingValue) +{ + if (value.isString()) { + JSString* dataString = value.toString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + auto encoding = parseEnumeration(*globalObject, encodingValue).value_or(WebCore::BufferEncodingType::utf8); + RETURN_IF_EXCEPTION(scope, {}); + + if (encoding == WebCore::BufferEncodingType::hex && dataString->length() % 2 != 0) { + Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, "encoding"_s, encodingValue, makeString("is invalid for data of length "_s, dataString->length())); + return {}; + } + + JSValue buf = JSValue::decode(WebCore::constructFromEncoding(globalObject, dataString, encoding)); + RETURN_IF_EXCEPTION(scope, {}); + + auto* view = jsDynamicCast(buf); + if (!view) { + Bun::ERR::INVALID_ARG_INSTANCE(scope, globalObject, argName, "Buffer, TypedArray, or DataView"_s, value); + return {}; + } + + if (view->isDetached()) { + throwTypeError(globalObject, scope, "Buffer is detached"_s); + return {}; + } + + return view; + } + + if (!value.isCell() || !JSC::isTypedArrayTypeIncludingDataView(value.asCell()->type())) { + Bun::ERR::INVALID_ARG_INSTANCE(scope, globalObject, argName, "Buffer, TypedArray, or DataView"_s, value); + return {}; + } + + auto* view = JSC::jsDynamicCast(value); + if (!view) { + Bun::ERR::INVALID_ARG_INSTANCE(scope, globalObject, argName, "Buffer, TypedArray, or DataView"_s, value); + return {}; + } + + if (view->isDetached()) { + throwTypeError(globalObject, scope, "Buffer is detached"_s); + return {}; + } + + return view; +} +} diff --git a/src/bun.js/bindings/node/crypto/util.h b/src/bun.js/bindings/node/crypto/util.h new file mode 100644 index 0000000000..b588a47d3e --- /dev/null +++ b/src/bun.js/bindings/node/crypto/util.h @@ -0,0 +1,34 @@ +#pragma once + +#include "root.h" +#include "ncrypto.h" +#include +#include + +namespace Bun { + +using namespace JSC; + +namespace NodeCryptoKeys { +enum class DSASigEnc { + DER, + P1363, + Invalid, +}; + +} + +// void CheckThrow(JSC::JSGlobalObject* globalObject, SignBase::Error error); +std::optional keyFromString(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, const WTF::StringView& keyView, JSValue passphraseValue); +ncrypto::EVPKeyPointer::PKFormatType parseKeyFormat(JSC::JSGlobalObject* globalObject, JSValue formatValue, WTF::ASCIILiteral optionName, std::optional defaultFormat = std::nullopt); +std::optional parseKeyType(JSC::JSGlobalObject* globalObject, JSValue typeValue, bool required, WTF::StringView keyType, std::optional isPublic, WTF::ASCIILiteral optionName); +std::optional passphraseFromBufferSource(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSValue input); +void throwCryptoError(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, unsigned long err, const char* message = nullptr); +void throwCryptoOperationFailed(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope); +std::optional getIntOption(JSC::JSGlobalObject* globalObject, JSValue options, WTF::ASCIILiteral name); +int32_t getPadding(JSC::JSGlobalObject* globalObject, JSValue options, const ncrypto::EVPKeyPointer& pkey); +std::optional getSaltLength(JSC::JSGlobalObject* globalObject, JSValue options); +NodeCryptoKeys::DSASigEnc getDSASigEnc(JSC::JSGlobalObject* globalObject, JSValue options); +JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, ThrowScope& scope, JSValue value, ASCIILiteral argName, JSValue encodingValue); + +} diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 436bbb7d1b..da42ff975c 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -920,7 +920,8 @@ public: std::unique_ptr m_clientSubspaceForEventListener; std::unique_ptr m_clientSubspaceForEventTarget; std::unique_ptr m_clientSubspaceForEventEmitter; - + std::unique_ptr m_clientSubspaceForJSSign; + std::unique_ptr m_clientSubspaceForJSVerify; std::unique_ptr m_clientSubspaceForServerRouteList; std::unique_ptr m_clientSubspaceForBunRequest; }; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index ffc284dc9f..8a0bb923ec 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -923,6 +923,8 @@ public: // std::unique_ptr m_subspaceForDOMFormData; // std::unique_ptr m_subspaceForDOMFormDataIterator; std::unique_ptr m_subspaceForDOMURL; + std::unique_ptr m_subspaceForJSSign; + std::unique_ptr m_subspaceForJSVerify; std::unique_ptr m_subspaceForServerRouteList; std::unique_ptr m_subspaceForBunRequest; }; diff --git a/src/js/internal/validators.ts b/src/js/internal/validators.ts index e06492b28e..2adcc50032 100644 --- a/src/js/internal/validators.ts +++ b/src/js/internal/validators.ts @@ -67,26 +67,10 @@ function validateLinkHeaderValue(hints) { ); } hideFromStack(validateLinkHeaderValue); -// TODO: do it in NodeValidator.cpp -function validateObject(value: unknown, name: string): asserts value is object { - if (typeof value !== "object" || value === null) throw $ERR_INVALID_ARG_TYPE(name, "object", value); -} -hideFromStack(validateObject); - -function validateOneOf(value, name, oneOf) { - if (!ArrayPrototypeIncludes.$call(oneOf, value)) { - const allowed = ArrayPrototypeJoin.$call( - ArrayPrototypeMap.$call(oneOf, v => (typeof v === "string" ? `'${v}'` : String(v))), - ", ", - ); - const reason = "must be one of: " + allowed; - throw $ERR_INVALID_ARG_VALUE(name, value, reason); - } -} -hideFromStack(validateOneOf); export default { - validateObject: validateObject, + /** (value, name) */ + validateObject: $newCppFunction("NodeValidator.cpp", "jsFunction_validateObject", 2), validateLinkHeaderValue: validateLinkHeaderValue, checkIsHttpToken: checkIsHttpToken, /** @@ -137,5 +121,5 @@ export default { /** `(buffer, name = 'buffer')` */ validateBuffer: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0), /** `(value, name, oneOf)` */ - validateOneOf, + validateOneOf: $newCppFunction("NodeValidator.cpp", "jsFunction_validateOneOf", 0), }; diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 8c790af5be..02ff363fd6 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -6,6 +6,7 @@ const BufferModule = require("node:buffer"); const StringDecoder = require("node:string_decoder").StringDecoder; const StringPrototypeToLowerCase = String.prototype.toLowerCase; const { CryptoHasher } = Bun; + const { symmetricKeySize, asymmetricKeyDetails, @@ -17,8 +18,6 @@ const { createPrivateKey, generateKeySync, generateKeyPairSync, - sign: nativeSign, - verify: nativeVerify, publicEncrypt, privateDecrypt, privateEncrypt, @@ -35,6 +34,10 @@ const { certExportChallenge, getCiphers, _getCipherInfo, + Sign: _Sign, + sign, + Verify: _Verify, + verify, } = $cpp("NodeCrypto.cpp", "createNodeCryptoBinding"); const { POINT_CONVERSION_COMPRESSED, POINT_CONVERSION_HYBRID, POINT_CONVERSION_UNCOMPRESSED } = @@ -48,6 +51,8 @@ const { const { validateObject, validateString, validateInt32 } = require("internal/validators"); +const kHandle = Symbol("kHandle"); + function verifySpkac(spkac, encoding) { return certVerifySpkac(getArrayBufferOrView(spkac, "spkac", encoding)); } @@ -165,22 +170,7 @@ function exportIfKeyObject(key) { } return key; } -function getKeyFrom(key, type) { - if (key instanceof KeyObject) { - key = key.export(); - } else if (key instanceof CryptoKey) { - key = KeyObject.from(key).export(); - } else if (!Buffer.isBuffer(key) && typeof key === "object") { - if ((typeof key.format === "string" || typeof key.passphrase === "string") && typeof key.key !== "undefined") { - key = type === "public" ? _createPublicKey(key).export() : _createPrivateKey(key).export(); - } - } else if (typeof key === "string" && type === "public") { - // make public key from non encrypted private PEM - key.indexOf("PRIVATE KEY-----") !== -1 && (key = _createPublicKey(key).export()); - } - return key; -} -function getArrayBufferOrView(buffer, name, encoding) { +function getArrayBufferOrView(buffer, name, encoding?) { if (buffer instanceof KeyObject) { if (buffer.type !== "secret") { const error = new TypeError( @@ -11220,274 +11210,6 @@ var require_curves2 = __commonJS({ }, }); -// node_modules/browserify-sign/browser/sign.js -var require_sign = __commonJS({ - "node_modules/browserify-sign/browser/sign.js"(exports, module) { - var Buffer2 = require_safe_buffer().Buffer, - createHmac = require_browser3(), - crt = require_browserify_rsa(), - EC = require_elliptic().ec, - BN = require_bn3(), - parseKeys = require_parse_asn1(), - curves = require_curves2(); - function sign(hash, key, hashType, signType, tag) { - var priv = parseKeys(getKeyFrom(key, "private")); - if (priv.curve) { - if (signType !== "ecdsa" && signType !== "ecdsa/rsa") throw new Error("wrong private key type"); - return ecSign(hash, priv); - } else if (priv.type === "dsa") { - if (signType !== "dsa") throw new Error("wrong private key type"); - return dsaSign(hash, priv, hashType); - } else if (signType !== "rsa" && signType !== "ecdsa/rsa") throw new Error("wrong private key type"); - hash = Buffer2.concat([tag, hash]); - for (var len = priv.modulus.byteLength(), pad = [0, 1]; hash.length + pad.length + 1 < len; ) pad.push(255); - pad.push(0); - for (var i = -1; ++i < hash.length; ) pad.push(hash[i]); - var out = crt(pad, priv); - return out; - } - function ecSign(hash, priv) { - var curveId = curves[priv.curve.join(".")]; - if (!curveId) throw new Error("unknown curve " + priv.curve.join(".")); - var curve = new EC(curveId), - key = curve.keyFromPrivate(priv.privateKey), - out = key.sign(hash); - return Buffer2.from(out.toDER()); - } - function dsaSign(hash, priv, algo) { - for ( - var x = priv.params.priv_key, - p = priv.params.p, - q = priv.params.q, - g = priv.params.g, - r = new BN(0), - k, - H = bits2int(hash, q).mod(q), - s = !1, - kv = getKey(x, q, hash, algo); - s === !1; - - ) - (k = makeKey(q, kv, algo)), - (r = makeR(g, k, p, q)), - (s = k - .invm(q) - .imul(H.add(x.mul(r))) - .mod(q)), - s.cmpn(0) === 0 && ((s = !1), (r = new BN(0))); - return toDER(r, s); - } - function toDER(r, s) { - (r = r.toArray()), (s = s.toArray()), r[0] & 128 && (r = [0].concat(r)), s[0] & 128 && (s = [0].concat(s)); - var total = r.length + s.length + 4, - res = [48, total, 2, r.length]; - return (res = res.concat(r, [2, s.length], s)), Buffer2.from(res); - } - function getKey(x, q, hash, algo) { - if (((x = Buffer2.from(x.toArray())), x.length < q.byteLength())) { - var zeros = Buffer2.alloc(q.byteLength() - x.length); - x = Buffer2.concat([zeros, x]); - } - var hlen = hash.length, - hbits = bits2octets(hash, q), - v = Buffer2.alloc(hlen); - v.fill(1); - var k = Buffer2.alloc(hlen); - return ( - (k = createHmac(algo, k) - .update(v) - .update(Buffer2.from([0])) - .update(x) - .update(hbits) - .digest()), - (v = createHmac(algo, k).update(v).digest()), - (k = createHmac(algo, k) - .update(v) - .update(Buffer2.from([1])) - .update(x) - .update(hbits) - .digest()), - (v = createHmac(algo, k).update(v).digest()), - { k, v } - ); - } - function bits2int(obits, q) { - var bits = new BN(obits), - shift = (obits.length << 3) - q.bitLength(); - return shift > 0 && bits.ishrn(shift), bits; - } - function bits2octets(bits, q) { - (bits = bits2int(bits, q)), (bits = bits.mod(q)); - var out = Buffer2.from(bits.toArray()); - if (out.length < q.byteLength()) { - var zeros = Buffer2.alloc(q.byteLength() - out.length); - out = Buffer2.concat([zeros, out]); - } - return out; - } - function makeKey(q, kv, algo) { - var t, k; - do { - for (t = Buffer2.alloc(0); t.length * 8 < q.bitLength(); ) - (kv.v = createHmac(algo, kv.k).update(kv.v).digest()), (t = Buffer2.concat([t, kv.v])); - (k = bits2int(t, q)), - (kv.k = createHmac(algo, kv.k) - .update(kv.v) - .update(Buffer2.from([0])) - .digest()), - (kv.v = createHmac(algo, kv.k).update(kv.v).digest()); - } while (k.cmp(q) !== -1); - return k; - } - function makeR(g, k, p, q) { - return g.toRed(BN.mont(p)).redPow(k).fromRed().mod(q); - } - module.exports = sign; - module.exports.getKey = getKey; - module.exports.makeKey = makeKey; - }, -}); - -// node_modules/browserify-sign/browser/verify.js -var require_verify = __commonJS({ - "node_modules/browserify-sign/browser/verify.js"(exports, module) { - var Buffer2 = require_safe_buffer().Buffer, - BN = require_bn3(), - EC = require_elliptic().ec, - parseKeys = require_parse_asn1(), - curves = require_curves2(); - function verify(sig, hash, key, signType, tag) { - var pub = parseKeys(getKeyFrom(key, "public")); - if (pub.type === "ec") { - if (signType !== "ecdsa" && signType !== "ecdsa/rsa") throw new Error("wrong public key type"); - return ecVerify(sig, hash, pub); - } else if (pub.type === "dsa") { - if (signType !== "dsa") throw new Error("wrong public key type"); - return dsaVerify(sig, hash, pub); - } else if (signType !== "rsa" && signType !== "ecdsa/rsa") throw new Error("wrong public key type"); - hash = Buffer2.concat([tag, hash]); - for (var len = pub.modulus.byteLength(), pad = [1], padNum = 0; hash.length + pad.length + 2 < len; ) - pad.push(255), padNum++; - pad.push(0); - for (var i = -1; ++i < hash.length; ) pad.push(hash[i]); - pad = Buffer2.from(pad); - var red = BN.mont(pub.modulus); - (sig = new BN(sig).toRed(red)), - (sig = sig.redPow(new BN(pub.publicExponent))), - (sig = Buffer2.from(sig.fromRed().toArray())); - var out = padNum < 8 ? 1 : 0; - for (len = Math.min(sig.length, pad.length), sig.length !== pad.length && (out = 1), i = -1; ++i < len; ) - out |= sig[i] ^ pad[i]; - return out === 0; - } - function ecVerify(sig, hash, pub) { - var curveId = curves[pub.data.algorithm.curve.join(".")]; - if (!curveId) throw new Error("unknown curve " + pub.data.algorithm.curve.join(".")); - var curve = new EC(curveId), - pubkey = pub.data.subjectPrivateKey.data; - return curve.verify(hash, sig, pubkey); - } - function dsaVerify(sig, hash, pub) { - var p = pub.data.p, - q = pub.data.q, - g = pub.data.g, - y = pub.data.pub_key, - unpacked = parseKeys.signature.decode(sig, "der"), - s = unpacked.s, - r = unpacked.r; - checkValue(s, q), checkValue(r, q); - var montp = BN.mont(p), - w = s.invm(q), - v = g - .toRed(montp) - .redPow(new BN(hash).mul(w).mod(q)) - .fromRed() - .mul(y.toRed(montp).redPow(r.mul(w).mod(q)).fromRed()) - .mod(p) - .mod(q); - return v.cmp(r) === 0; - } - function checkValue(b, q) { - if (b.cmpn(0) <= 0) throw new Error("invalid sig"); - if (b.cmp(q) >= q) throw new Error("invalid sig"); - } - module.exports = verify; - }, -}); - -// node_modules/browserify-sign/browser/index.js -var require_browser8 = __commonJS({ - "node_modules/browserify-sign/browser/index.js"(exports, module) { - var Buffer2 = require_safe_buffer().Buffer; - var createHash = require_browser2(); - var inherits = require_inherits_browser(); - var sign = require_sign(); - var verify = require_verify(); - var algorithms = require_algorithms(); - Object.keys(algorithms).forEach(function (key) { - (algorithms[key].id = Buffer2.from(algorithms[key].id, "hex")), (algorithms[key.toLowerCase()] = algorithms[key]); - }); - function Sign(algorithm) { - if (typeof algorithm === "string") { - algorithm = algorithm.toLowerCase(); - } - StreamModule.Writable.$call(this); - var data = algorithms[algorithm]; - if (!data) throw new Error("Unknown message digest"); - (this._hashType = data.hash), - (this._hash = createHash(data.hash)), - (this._tag = data.id), - (this._signType = data.sign); - } - inherits(Sign, StreamModule.Writable); - Sign.prototype._write = function (data, _, done) { - this._hash.update(data), done(); - }; - Sign.prototype.update = function (data, enc) { - return typeof data == "string" && (data = Buffer2.from(data, enc)), this._hash.update(data), this; - }; - Sign.prototype.sign = function (key, enc) { - this.end(); - var hash = this._hash.digest(), - sig = sign(hash, key, this._hashType, this._signType, this._tag); - return enc ? sig.toString(enc) : sig; - }; - function Verify(algorithm) { - StreamModule.Writable.$call(this); - if (typeof algorithm === "string") { - algorithm = algorithm.toLowerCase(); - } - var data = algorithms[algorithm]; - if (!data) throw new Error("Unknown message digest"); - (this._hash = createHash(data.hash)), (this._tag = data.id), (this._signType = data.sign); - } - inherits(Verify, StreamModule.Writable); - Verify.prototype._write = function (data, _, done) { - this._hash.update(data), done(); - }; - Verify.prototype.update = function (data, enc) { - return typeof data == "string" && (data = Buffer2.from(data, enc)), this._hash.update(data), this; - }; - Verify.prototype.verify = function (key, sig, enc) { - typeof sig == "string" && (sig = Buffer2.from(sig, enc)), this.end(); - var hash = this._hash.digest(); - return verify(sig, hash, key, this._signType, this._tag); - }; - function createSign(algorithm) { - return new Sign(algorithm); - } - function createVerify(algorithm) { - return new Verify(algorithm); - } - module.exports = { - Sign: createSign, - Verify: createVerify, - createSign, - createVerify, - }; - }, -}); - // node_modules/create-ecdh/node_modules/bn.js/lib/bn.js var require_bn6 = require_bn; @@ -11693,11 +11415,6 @@ var require_crypto_browserify2 = __commonJS({ exports.createDiffieHellman = dh.createDiffieHellman; exports.DiffieHellman = dh.DiffieHellman; exports.diffieHellman = dh.diffieHellman; - var sign = require_browser8(); - exports.createSign = sign.createSign; - exports.Sign = sign.Sign; - exports.createVerify = sign.createVerify; - exports.Verify = sign.Verify; const ecdh = require_browser9(); exports.ECDH = ecdh.ECDH; exports.createECDH = ecdh.createECDH; @@ -11705,16 +11422,6 @@ var require_crypto_browserify2 = __commonJS({ var rf = require_browser11(); exports.randomFill = rf.randomFill; exports.randomFillSync = rf.randomFillSync; - exports.createCredentials = function () { - throw new Error( - [ - "sorry, createCredentials is not implemented yet", - "we accept pull requests", - "https://github.com/crypto-browserify/crypto-browserify", - ].join(` -`), - ); - }; exports.constants = $processBindingConstants.crypto; }, }); @@ -12030,92 +11737,6 @@ crypto_exports.createPublicKey = _createPublicKey; crypto_exports.KeyObject = KeyObject; var webcrypto = crypto; var _subtle = webcrypto.subtle; -const _createSign = crypto_exports.createSign; - -crypto_exports.sign = function (algorithm, data, key, callback) { - // TODO: move this to native - var dsaEncoding, padding, saltLength; - // key must be a KeyObject - if (!(key instanceof KeyObject)) { - if ($isObject(key) && key.key) { - padding = key.padding; - saltLength = key.saltLength; - dsaEncoding = key.dsaEncoding; - } - if (key.key instanceof KeyObject) { - key = key.key; - } else { - key = _createPrivateKey(key); - } - } - if (typeof callback === "function") { - try { - let result; - if (key.asymmetricKeyType === "rsa") { - // RSA-PSS is supported by native but other RSA algorithms are not - result = _createSign(algorithm || "sha256") - .update(data) - .sign(key); - } else { - result = nativeSign(key.$bunNativePtr, data, algorithm, dsaEncoding, padding, saltLength); - } - callback(null, result); - } catch (err) { - callback(err); - } - } else { - if (key.asymmetricKeyType === "rsa") { - return _createSign(algorithm || "sha256") - .update(data) - .sign(key); - } else { - return nativeSign(key.$bunNativePtr, data, algorithm, dsaEncoding, padding, saltLength); - } - } -}; -const _createVerify = crypto_exports.createVerify; - -crypto_exports.verify = function (algorithm, data, key, signature, callback) { - // TODO: move this to native - var dsaEncoding, padding, saltLength; - // key must be a KeyObject - if (!(key instanceof KeyObject)) { - if ($isObject(key) && key.key) { - padding = key.padding; - saltLength = key.saltLength; - dsaEncoding = key.dsaEncoding; - } - if (key.key instanceof KeyObject && key.key.type === "public") { - key = key.key; - } else { - key = _createPublicKey(key); - } - } - if (typeof callback === "function") { - try { - let result; - if (key.asymmetricKeyType === "rsa") { - // RSA-PSS is supported by native but other RSA algorithms are not - result = _createVerify(algorithm || "sha256") - .update(data) - .verify(key, signature); - } else { - result = nativeVerify(key.$bunNativePtr, data, signature, algorithm, dsaEncoding, padding, saltLength); - } - callback(null, result); - } catch (err) { - callback(err); - } - } else { - if (key.asymmetricKeyType === "rsa") { - return _createVerify(algorithm || "sha256") - .update(data) - .verify(key, signature); - } else { - return nativeVerify(key.$bunNativePtr, data, signature, algorithm, dsaEncoding, padding, saltLength); - } - } -}; // We are not allowed to call createPublicKey/createPrivateKey when we're already working with a // KeyObject/CryptoKey of the same type (public/private). @@ -12197,5 +11818,72 @@ crypto_exports.webcrypto = webcrypto; crypto_exports.subtle = _subtle; crypto_exports.X509Certificate = X509Certificate; crypto_exports.Certificate = Certificate; + +function Sign(algorithm, options): void { + if (!(this instanceof Sign)) { + return new Sign(algorithm, options); + } + + validateString(algorithm, "algorithm"); + this[kHandle] = new _Sign(); + this[kHandle].init(algorithm); + + StreamModule.Writable.$apply(this, [options]); +} + +$toClass(Sign, "Sign", StreamModule.Writable); + +Sign.prototype._write = function _write(chunk, encoding, callback) { + this.update(chunk, encoding); + callback(); +}; + +Sign.prototype.update = function update(data, encoding) { + return this[kHandle].update(data, encoding); +}; + +Sign.prototype.sign = function sign(options, encoding) { + return this[kHandle].sign(options, encoding); +}; + +crypto_exports.Sign = Sign; +crypto_exports.sign = sign; + +function createSign(algorithm, options?) { + return new Sign(algorithm, options); +} + +crypto_exports.createSign = createSign; + +function Verify(algorithm, options): void { + if (!(this instanceof Verify)) { + return new Verify(algorithm, options); + } + + validateString(algorithm, "algorithm"); + this[kHandle] = new _Verify(); + this[kHandle].init(algorithm); + + StreamModule.Writable.$apply(this, [options]); +} + +$toClass(Verify, "Verify", StreamModule.Writable); + +Verify.prototype._write = Sign.prototype._write; +Verify.prototype.update = Sign.prototype.update; + +Verify.prototype.verify = function verify(options, signature, sigEncoding) { + return this[kHandle].verify(options, signature, sigEncoding); +}; + +crypto_exports.Verify = Verify; +crypto_exports.verify = verify; + +function createVerify(algorithm, options?) { + return new Verify(algorithm, options); +} + +crypto_exports.createVerify = createVerify; + export default crypto_exports; /*! safe-buffer. MIT License. Feross Aboukhadijeh */ diff --git a/test/bun.lock b/test/bun.lock index cdf45ffb59..4662f45647 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -52,7 +52,7 @@ "nodemailer": "6.9.3", "pg": "8.11.1", "pg-connection-string": "2.6.1", - "pg-gateway": "^0.3.0-beta.4", + "pg-gateway": "0.3.0-beta.4", "pino": "9.4.0", "pino-pretty": "11.2.2", "postgres": "3.3.5", diff --git a/test/js/node/crypto/crypto.key-objects.test.ts b/test/js/node/crypto/crypto.key-objects.test.ts index 5c6cc1f902..c26f6c2134 100644 --- a/test/js/node/crypto/crypto.key-objects.test.ts +++ b/test/js/node/crypto/crypto.key-objects.test.ts @@ -1716,19 +1716,19 @@ test("ECDSA should work", async () => { expect(() => { //@ts-ignore sign("sha256", Buffer.from("foo"), { key: privateKey, dsaEncoding: "kjshdakjshd" }); - }).toThrow(/invalid dsaEncoding/); + }).toThrow(/The property 'options.dsaEncoding' is invalid. Received 'kjshdakjshd'/); expect(() => { const signature = sign("sha256", Buffer.from("foo"), privateKey); //@ts-ignore verify("sha256", Buffer.from("foo"), { key: publicKey, dsaEncoding: "ieee-p136" }, signature); - }).toThrow(/invalid dsaEncoding/); + }).toThrow(/The property 'options.dsaEncoding' is invalid. Received 'ieee-p136'/); expect(() => { //@ts-ignore const signature = sign("sha256", Buffer.from("foo"), { key: privateKey, dsaEncoding: "ieee-p136" }); verify("sha256", Buffer.from("foo"), { key: publicKey, dsaEncoding: "der" }, signature); - }).toThrow(/invalid dsaEncoding/); + }).toThrow(/The property 'options.dsaEncoding' is invalid. Received 'ieee-p136'/); }); function randomProp() { diff --git a/test/js/node/crypto/fixtures/sign.fixture.ts b/test/js/node/crypto/fixtures/sign.fixture.ts index a54f986113..285a1be37d 100644 --- a/test/js/node/crypto/fixtures/sign.fixture.ts +++ b/test/js/node/crypto/fixtures/sign.fixture.ts @@ -131,6 +131,7 @@ export const hashesFixture = [ "fixtures/rsa_public.pem", "sMwPPWMz4TcQbyImUs7GHwlop5Gx34s9kBmaHGoygU+dp7sdWq5+dF/iKKCJcaDWzv07IpWl/ksNRDjT6iUTjiDO+mHiMoKMYA5eVrwy+aO4PqXHkJYkRcefs9JgkXjd0xmalxPRRjGcdTYSpwlr6RQvOdcHXxVkuvaV9qurnWbOey8Uyl0MFSjAMdc9wWMUdWW5uxVMosFXb+ISpGwmyh3Ti6fYL92WYmOEyTlJSuxNA2R+yj+VmmzWxs6roonHvszR0ab2+YDxc2MxoWxzVIVeP4fiXuSa/nZx9hT9BB1EEaJxLOtSRBhQXHaY8CafZ3VoNfj2YgjbOX1wgXntQw==", ], + /* Using ncrypto and boringssl means we lose support for these algorithms [ "ripemd160WithRSA", "fixtures/rsa_private.pem", @@ -143,6 +144,7 @@ export const hashesFixture = [ "fixtures/rsa_public.pem", "Yu8F0migkeBsXPX8whGaFfL48wOnC3SNQ0m6gC9T/GjdLXzzmIj29CF/9MV51uSZ/Ih1dnox/dieW+1nLBK+sK1i185i+DlPevcZST+XWMF1CtPr36GwBJBD4+TgBzQHqgI5BUtm7pjBV14Iykolek9gwX/LUO0Ft0k4Pk8HAtJ8zLCqNbGEngtbmasRUSVfcuZTrjm194AR7eLERvoo4c8/3pdoyUCoh49cwbEgtIzTCR04xv00yF6PTnAg71XkgCDjwENCrMeTNxf9vQ6PC177C3QyWA3ARFSIjh8rhpS77hqFCTmoHPKMepbC6iAHtxSSA3PZXQ9zWQsDLGp2Pg==", ], + */ [ "md5WithRSAEncryption", "fixtures/rsa_private.pem", diff --git a/test/js/node/test/common/crypto.js b/test/js/node/test/common/crypto.js index ba47285df4..e859723e69 100644 --- a/test/js/node/test/common/crypto.js +++ b/test/js/node/test/common/crypto.js @@ -1,8 +1,9 @@ 'use strict'; const common = require('../common'); -if (!common.hasCrypto) +if (!common.hasCrypto) { common.skip('missing crypto'); +} const assert = require('assert'); const crypto = require('crypto'); @@ -115,6 +116,27 @@ const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY'); const sec1Exp = getRegExpForPEM('EC PRIVATE KEY'); const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); +// Synthesize OPENSSL_VERSION_NUMBER format with the layout 0xMNN00PPSL +const opensslVersionNumber = (major = 0, minor = 0, patch = 0) => { + assert(major >= 0 && major <= 0xf); + assert(minor >= 0 && minor <= 0xff); + assert(patch >= 0 && patch <= 0xff); + return (major << 28) | (minor << 20) | (patch << 4); +}; + +let OPENSSL_VERSION_NUMBER; +const hasOpenSSL = (major = 0, minor = 0, patch = 0) => { + if (!common.hasCrypto) return false; + if (OPENSSL_VERSION_NUMBER === undefined) { + const regexp = /(?\d+)\.(?\d+)\.(?

\d+)/; + const { m, n, p } = process.versions.openssl.match(regexp).groups; + OPENSSL_VERSION_NUMBER = opensslVersionNumber(m, n, p); + } + return OPENSSL_VERSION_NUMBER >= opensslVersionNumber(major, minor, patch); +}; + +let opensslCli = null; + module.exports = { modp2buf, testDH, @@ -129,4 +151,36 @@ module.exports = { pkcs8EncExp, // used once sec1Exp, sec1EncExp, + hasOpenSSL, + get hasOpenSSL3() { + return hasOpenSSL(3); + }, + // opensslCli defined lazily to reduce overhead of spawnSync + get opensslCli() { + if (opensslCli !== null) return opensslCli; + + opensslCli = Bun.which('openssl'); + + return opensslCli; + + // if (process.config.variables.node_shared_openssl) { + // // Use external command + // opensslCli = 'openssl'; + // } else { + // const path = require('path'); + // // Use command built from sources included in Node.js repository + // opensslCli = path.join(path.dirname(process.execPath), 'openssl-cli'); + // } + + // if (exports.isWindows) opensslCli += '.exe'; + + // const { spawnSync } = require('child_process'); + + // const opensslCmd = spawnSync(opensslCli, ['version']); + // if (opensslCmd.status !== 0 || opensslCmd.error !== undefined) { + // // OpenSSL command cannot be executed + // opensslCli = false; + // } + // return opensslCli; + }, }; diff --git a/test/js/node/test/parallel/test-crypto-async-sign-verify.js b/test/js/node/test/parallel/test-crypto-async-sign-verify.js new file mode 100644 index 0000000000..d40725fe3c --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-async-sign-verify.js @@ -0,0 +1,147 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const util = require('util'); +const crypto = require('crypto'); +const fixtures = require('../common/fixtures'); + +function test( + publicFixture, + privateFixture, + algorithm, + deterministic, + options +) { + let publicPem = fixtures.readKey(publicFixture); + let privatePem = fixtures.readKey(privateFixture); + let privateKey = crypto.createPrivateKey(privatePem); + let publicKey = crypto.createPublicKey(publicPem); + const privateDer = { + key: privateKey.export({ format: 'der', type: 'pkcs8' }), + format: 'der', + type: 'pkcs8', + ...options + }; + const publicDer = { + key: publicKey.export({ format: 'der', type: 'spki' }), + format: 'der', + type: 'spki', + ...options + }; + + if (options) { + publicPem = { ...options, key: publicPem }; + privatePem = { ...options, key: privatePem }; + privateKey = { ...options, key: privateKey }; + publicKey = { ...options, key: publicKey }; + } + + const data = Buffer.from('Hello world'); + const expected = crypto.sign(algorithm, data, privateKey); + + for (const key of [privatePem, privateKey, privateDer]) { + crypto.sign(algorithm, data, key, common.mustSucceed((actual) => { + if (deterministic) { + assert.deepStrictEqual(actual, expected); + } + + assert.strictEqual( + crypto.verify(algorithm, data, key, actual), true); + })); + } + + const verifyInputs = [ + publicPem, publicKey, publicDer, privatePem, privateKey, privateDer]; + for (const key of verifyInputs) { + crypto.verify(algorithm, data, key, expected, common.mustSucceed( + (verified) => assert.strictEqual(verified, true))); + + crypto.verify(algorithm, data, key, Buffer.from(''), common.mustSucceed( + (verified) => assert.strictEqual(verified, false))); + } +} + +// RSA w/ default padding +test('rsa_public.pem', 'rsa_private.pem', 'sha256', true); +test('rsa_public.pem', 'rsa_private.pem', 'sha256', true, + { padding: crypto.constants.RSA_PKCS1_PADDING }); + +// RSA w/ PSS_PADDING and default saltLength +test('rsa_public.pem', 'rsa_private.pem', 'sha256', false, + { padding: crypto.constants.RSA_PKCS1_PSS_PADDING }); +test('rsa_public.pem', 'rsa_private.pem', 'sha256', false, + { + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN + }); + +// RSA w/ PSS_PADDING and PSS_SALTLEN_DIGEST +test('rsa_public.pem', 'rsa_private.pem', 'sha256', false, + { + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST + }); + +// ED25519 +test('ed25519_public.pem', 'ed25519_private.pem', undefined, true); +// ED448 +// skip tests from electron +// https://github.com/electron/electron/blob/add374ef6a3e576fa4f73dbf68199540668a75cf/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L56 +if (!common.openSSLIsBoringSSL) { +test('ed448_public.pem', 'ed448_private.pem', undefined, true); + +// ECDSA w/ der signature encoding +test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', + false); +test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', + false, { dsaEncoding: 'der' }); + +// ECDSA w/ ieee-p1363 signature encoding +test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', false, + { dsaEncoding: 'ieee-p1363' }); + +// DSA w/ der signature encoding +test('dsa_public.pem', 'dsa_private.pem', 'sha256', + false); +test('dsa_public.pem', 'dsa_private.pem', 'sha256', + false, { dsaEncoding: 'der' }); + +// DSA w/ ieee-p1363 signature encoding +test('dsa_public.pem', 'dsa_private.pem', 'sha256', false, + { dsaEncoding: 'ieee-p1363' }); +} + +// Test Parallel Execution w/ KeyObject is threadsafe in openssl3 +{ + const publicKey = { + key: crypto.createPublicKey( + fixtures.readKey('ec_p256_public.pem')), + dsaEncoding: 'ieee-p1363', + }; + const privateKey = { + key: crypto.createPrivateKey( + fixtures.readKey('ec_p256_private.pem')), + dsaEncoding: 'ieee-p1363', + }; + + const sign = util.promisify(crypto.sign); + const verify = util.promisify(crypto.verify); + + const data = Buffer.from('hello world'); + + Promise.all([ + sign('sha256', data, privateKey), + sign('sha256', data, privateKey), + sign('sha256', data, privateKey), + ]).then(([signature]) => { + return Promise.all([ + verify('sha256', data, publicKey, signature), + verify('sha256', data, publicKey, signature), + verify('sha256', data, publicKey, signature), + ]).then(common.mustCall()); + }) + .catch(common.mustNotCall()); +} diff --git a/test/js/node/test/parallel/test-crypto-sign-verify.js b/test/js/node/test/parallel/test-crypto-sign-verify.js new file mode 100644 index 0000000000..843812806d --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-sign-verify.js @@ -0,0 +1,834 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fs = require('fs'); +const exec = require('child_process').exec; +const crypto = require('crypto'); +const fixtures = require('../common/fixtures'); +const { + hasOpenSSL3, + opensslCli, +} = require('../common/crypto'); + +// Test certificates +const certPem = fixtures.readKey('rsa_cert.crt'); +const keyPem = fixtures.readKey('rsa_private.pem'); +const keySize = 2048; + +{ + const Sign = crypto.Sign; + const instance = Sign('SHA256'); + assert(instance instanceof Sign, 'Sign is expected to return a new ' + + 'instance when called without `new`'); +} + +{ + const Verify = crypto.Verify; + const instance = Verify('SHA256'); + assert(instance instanceof Verify, 'Verify is expected to return a new ' + + 'instance when called without `new`'); +} + +// Test handling of exceptional conditions +{ + const library = { + configurable: true, + set() { + throw new Error('bye, bye, library'); + } + }; + Object.defineProperty(Object.prototype, 'library', library); + + assert.throws(() => { + crypto.createSign('sha1').sign( + `-----BEGIN RSA PRIVATE KEY----- + AAAAAAAAAAAA + -----END RSA PRIVATE KEY-----`); + }, { message: 'bye, bye, library' }); + + delete Object.prototype.library; + + const errorStack = { + configurable: true, + set() { + throw new Error('bye, bye, error stack'); + } + }; + Object.defineProperty(Object.prototype, 'opensslErrorStack', errorStack); + + assert.throws(() => { + crypto.createSign('SHA1') + .update('Test123') + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING + }); + }, { message: hasOpenSSL3 ? + 'error:1C8000A5:Provider routines::illegal or unsupported padding mode' : + 'error:0600006d:public key routines:OPENSSL_internal:ILLEGAL_OR_UNSUPPORTED_PADDING_MODE' }); + + delete Object.prototype.opensslErrorStack; +} + +assert.throws( + () => crypto.createVerify('SHA256').verify({ + key: certPem, + padding: null, + }, ''), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: "The property 'options.padding' is invalid. Received null", + }); + +assert.throws( + () => crypto.createVerify('SHA256').verify({ + key: certPem, + saltLength: null, + }, ''), + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: "The property 'options.saltLength' is invalid. Received null", + }); + +// Test signing and verifying +{ + const s1 = crypto.createSign('SHA1') + .update('Test123') + .sign(keyPem, 'base64'); + let s1stream = crypto.createSign('SHA1'); + s1stream.end('Test123'); + s1stream = s1stream.sign(keyPem, 'base64'); + assert.strictEqual(s1, s1stream, `${s1} should equal ${s1stream}`); + + const verified = crypto.createVerify('SHA1') + .update('Test') + .update('123') + .verify(certPem, s1, 'base64'); + assert.strictEqual(verified, true); +} + +{ + const s2 = crypto.createSign('SHA256') + .update('Test123') + .sign(keyPem, 'latin1'); + let s2stream = crypto.createSign('SHA256'); + s2stream.end('Test123'); + s2stream = s2stream.sign(keyPem, 'latin1'); + assert.strictEqual(s2, s2stream, `${s2} should equal ${s2stream}`); + + let verified = crypto.createVerify('SHA256') + .update('Test') + .update('123') + .verify(certPem, s2, 'latin1'); + assert.strictEqual(verified, true); + + const verStream = crypto.createVerify('SHA256'); + verStream.write('Tes'); + verStream.write('t12'); + verStream.end('3'); + verified = verStream.verify(certPem, s2, 'latin1'); + assert.strictEqual(verified, true); +} + +{ + const s3 = crypto.createSign('SHA1') + .update('Test123') + .sign(keyPem, 'buffer'); + let verified = crypto.createVerify('SHA1') + .update('Test') + .update('123') + .verify(certPem, s3); + assert.strictEqual(verified, true); + + const verStream = crypto.createVerify('SHA1'); + verStream.write('Tes'); + verStream.write('t12'); + verStream.end('3'); + verified = verStream.verify(certPem, s3); + assert.strictEqual(verified, true); +} + +// Special tests for RSA_PKCS1_PSS_PADDING +{ + function testPSS(algo, hLen) { + // Maximum permissible salt length + const max = keySize / 8 - hLen - 2; + + function getEffectiveSaltLength(saltLength) { + switch (saltLength) { + case crypto.constants.RSA_PSS_SALTLEN_DIGEST: + return hLen; + case crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN: + return max; + default: + return saltLength; + } + } + + const signSaltLengths = [ + crypto.constants.RSA_PSS_SALTLEN_DIGEST, + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_DIGEST), + crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN, + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN), + 0, 16, 32, 64, 128, + ]; + + const verifySaltLengths = [ + crypto.constants.RSA_PSS_SALTLEN_DIGEST, + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_DIGEST), + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN), + 0, 16, 32, 64, 128, + ]; + const errMessage = /^Error:.*data too large for key size$/; + + const data = Buffer.from('Test123'); + + signSaltLengths.forEach((signSaltLength) => { + if (signSaltLength > max) { + // If the salt length is too big, an Error should be thrown + assert.throws(() => { + crypto.createSign(algo) + .update(data) + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + }); + }, errMessage); + assert.throws(() => { + crypto.sign(algo, data, { + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + }); + }, errMessage); + } else { + // Otherwise, a valid signature should be generated + const s4 = crypto.createSign(algo) + .update(data) + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + }); + const s4_2 = crypto.sign(algo, data, { + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + }); + + [s4, s4_2].forEach((sig) => { + let verified; + verifySaltLengths.forEach((verifySaltLength) => { + // Verification should succeed if and only if the salt length is + // correct + verified = crypto.createVerify(algo) + .update(data) + .verify({ + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: verifySaltLength + }, sig); + assert.strictEqual(verified, crypto.verify(algo, data, { + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: verifySaltLength + }, sig)); + const saltLengthCorrect = getEffectiveSaltLength(signSaltLength) === + getEffectiveSaltLength(verifySaltLength); + assert.strictEqual(verified, saltLengthCorrect); + }); + + // Verification using RSA_PSS_SALTLEN_AUTO should always work + verified = crypto.createVerify(algo) + .update(data) + .verify({ + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO + }, sig); + assert.strictEqual(verified, true); + assert.strictEqual(verified, crypto.verify(algo, data, { + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO + }, sig)); + + // Verifying an incorrect message should never work + const wrongData = Buffer.from('Test1234'); + verified = crypto.createVerify(algo) + .update(wrongData) + .verify({ + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO + }, sig); + assert.strictEqual(verified, false); + assert.strictEqual(verified, crypto.verify(algo, wrongData, { + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO + }, sig)); + }); + } + }); + } + + testPSS('SHA1', 20); + testPSS('SHA256', 32); +} + +// Test vectors for RSA_PKCS1_PSS_PADDING provided by the RSA Laboratories: +// https://www.emc.com/emc-plus/rsa-labs/standards-initiatives/pkcs-rsa-cryptography-standard.htm +{ + // We only test verification as we cannot specify explicit salts when signing + function testVerify(cert, vector) { + const verified = crypto.createVerify('SHA1') + .update(Buffer.from(vector.message, 'hex')) + .verify({ + key: cert, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: vector.salt.length / 2 + }, vector.signature, 'hex'); + assert.strictEqual(verified, true); + } + + const examples = JSON.parse(fixtures.readSync('pss-vectors.json', 'utf8')); + + for (const key in examples) { + const example = examples[key]; + const publicKey = example.publicKey.join('\n'); + example.tests.forEach((test) => testVerify(publicKey, test)); + } +} + +// Test exceptions for invalid `padding` and `saltLength` values +{ + [null, NaN, 'boom', {}, [], true, false] + .forEach((invalidValue) => { + assert.throws(() => { + crypto.createSign('SHA256') + .update('Test123') + .sign({ + key: keyPem, + padding: invalidValue + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError' + }); + + assert.throws(() => { + crypto.createSign('SHA256') + .update('Test123') + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: invalidValue + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError' + }); + }); + + if (!common.openSSLIsBoringSSL) { + assert.throws(() => { + crypto.createSign('SHA1') + .update('Test123') + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING + }); + }, hasOpenSSL3 ? { + code: 'ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE', + message: /illegal or unsupported padding mode/, + } : { + code: 'ERR_OSSL_RSA_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE', + message: /illegal or unsupported padding mode/, + opensslErrorStack: [ + 'error:06089093:digital envelope routines:EVP_PKEY_CTX_ctrl:' + + 'command not supported', + ], + }); + } +} + +// Test throws exception when key options is null +{ + assert.throws(() => { + crypto.createSign('SHA1').update('Test123').sign(null, 'base64'); + }, { + code: 'ERR_CRYPTO_SIGN_KEY_REQUIRED', + name: 'Error' + }); +} + +{ + const sign = crypto.createSign('SHA1'); + const verify = crypto.createVerify('SHA1'); + + [1, [], {}, undefined, null, true, Infinity].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "algorithm" argument must be of type string.' + + `${common.invalidArgTypeHelper(input)}` + }; + assert.throws(() => crypto.createSign(input), errObj); + assert.throws(() => crypto.createVerify(input), errObj); + + errObj.message = 'The "data" argument must be of type string or an ' + + 'instance of Buffer, TypedArray, or DataView.' + + common.invalidArgTypeHelper(input); + assert.throws(() => sign.update(input), errObj); + assert.throws(() => verify.update(input), errObj); + assert.throws(() => sign._write(input, 'utf8', () => {}), errObj); + assert.throws(() => verify._write(input, 'utf8', () => {}), errObj); + }); + + [ + Uint8Array, Uint16Array, Uint32Array, Float32Array, Float64Array, + ].forEach((clazz) => { + // These should all just work + sign.update(new clazz()); + verify.update(new clazz()); + }); + + [1, {}, [], Infinity].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }; + assert.throws(() => sign.sign(input), errObj); + assert.throws(() => verify.verify(input), errObj); + assert.throws(() => verify.verify('test', input), errObj); + }); +} + +{ + assert.throws( + () => crypto.createSign('sha8'), + /Invalid digest/); + assert.throws( + () => crypto.sign('sha8', Buffer.alloc(1), keyPem), + /Invalid digest/); +} + +[ + { private: fixtures.readKey('ed25519_private.pem', 'ascii'), + public: fixtures.readKey('ed25519_public.pem', 'ascii'), + algo: null, + sigLen: 64 }, + // Skipped by electron + // https://github.com/electron/electron/blob/add374ef6a3e576fa4f73dbf68199540668a75cf/patches/node/fix_crypto_tests_to_run_with_bssl.patch#L407 + /* + { private: fixtures.readKey('ed448_private.pem', 'ascii'), + public: fixtures.readKey('ed448_public.pem', 'ascii'), + algo: null, + sigLen: 114 }, + */ + { private: fixtures.readKey('rsa_private_2048.pem', 'ascii'), + public: fixtures.readKey('rsa_public_2048.pem', 'ascii'), + algo: 'sha1', + sigLen: 256 }, +].forEach((pair) => { + const algo = pair.algo; + + { + const data = Buffer.from('Hello world'); + const sig = crypto.sign(algo, data, pair.private); + assert.strictEqual(sig.length, pair.sigLen); + + assert.strictEqual(crypto.verify(algo, data, pair.private, sig), + true); + assert.strictEqual(crypto.verify(algo, data, pair.public, sig), + true); + } + + { + const data = Buffer.from('Hello world'); + const privKeyObj = crypto.createPrivateKey(pair.private); + const pubKeyObj = crypto.createPublicKey(pair.public); + + const sig = crypto.sign(algo, data, privKeyObj); + assert.strictEqual(sig.length, pair.sigLen); + + assert.strictEqual(crypto.verify(algo, data, privKeyObj, sig), true); + assert.strictEqual(crypto.verify(algo, data, pubKeyObj, sig), true); + } + + { + const data = Buffer.from('Hello world'); + const otherData = Buffer.from('Goodbye world'); + const otherSig = crypto.sign(algo, otherData, pair.private); + assert.strictEqual(crypto.verify(algo, data, pair.private, otherSig), + false); + } + + [ + Uint8Array, Uint16Array, Uint32Array, Float32Array, Float64Array, + ].forEach((clazz) => { + const data = new clazz(); + const sig = crypto.sign(algo, data, pair.private); + assert.strictEqual(crypto.verify(algo, data, pair.private, sig), + true); + }); +}); + +[1, {}, [], true, Infinity].forEach((input) => { + const data = Buffer.alloc(1); + const sig = Buffer.alloc(1); + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }; + + assert.throws(() => crypto.sign(null, input, 'asdf'), errObj); + assert.throws(() => crypto.verify(null, input, 'asdf', sig), errObj); + + assert.throws(() => crypto.sign(null, data, input), errObj); + assert.throws(() => crypto.verify(null, data, input, sig), errObj); + + errObj.message = 'The "signature" argument must be an instance of ' + + 'Buffer, TypedArray, or DataView.' + + common.invalidArgTypeHelper(input); + assert.throws(() => crypto.verify(null, data, 'test', input), errObj); +}); + +{ + const data = Buffer.from('Hello world'); + const keys = [['ec-key.pem', 64]]; + + // Unabled to sign or verify with DSA keys + // https://boringssl.googlesource.com/boringssl/+/HEAD/PORTING.md#dsa-s + if (!common.openSSLIsBoringSSL) { + keys.push(['dsa_private_1025.pem', 40]); + } + + for (const [file, length] of keys) { + const privKey = fixtures.readKey(file); + [ + crypto.createSign('sha1').update(data).sign(privKey), + crypto.sign('sha1', data, privKey), + crypto.sign('sha1', data, { key: privKey, dsaEncoding: 'der' }), + ].forEach((sig) => { + // Signature length variability due to DER encoding + assert(sig.length >= length + 4 && sig.length <= length + 8); + + assert.strictEqual( + crypto.createVerify('sha1').update(data).verify(privKey, sig), + true + ); + assert.strictEqual(crypto.verify('sha1', data, privKey, sig), true); + }); + + // Test (EC)DSA signature conversion. + const opts = { key: privKey, dsaEncoding: 'ieee-p1363' }; + let sig = crypto.sign('sha1', data, opts); + // Unlike DER signatures, IEEE P1363 signatures have a predictable length. + assert.strictEqual(sig.length, length); + assert.strictEqual(crypto.verify('sha1', data, opts, sig), true); + assert.strictEqual(crypto.createVerify('sha1') + .update(data) + .verify(opts, sig), true); + + // Test invalid signature lengths. + for (const i of [-2, -1, 1, 2, 4, 8]) { + sig = crypto.randomBytes(length + i); + let result; + try { + result = crypto.verify('sha1', data, opts, sig); + } catch (err) { + assert.match(err.message, /asn1 encoding/); + assert.strictEqual(err.library, 'asn1 encoding routines'); + continue; + } + assert.strictEqual(result, false); + } + } + + // Test verifying externally signed messages. + const extSig = Buffer.from('494c18ab5c8a62a72aea5041966902bcfa229821af2bf65' + + '0b5b4870d1fe6aebeaed9460c62210693b5b0a300033823' + + '33d9529c8abd8c5948940af944828be16c', 'hex'); + for (const ok of [true, false]) { + assert.strictEqual( + crypto.verify('sha256', data, { + key: fixtures.readKey('ec-key.pem'), + dsaEncoding: 'ieee-p1363' + }, extSig), + ok + ); + + assert.strictEqual( + crypto.createVerify('sha256').update(data).verify({ + key: fixtures.readKey('ec-key.pem'), + dsaEncoding: 'ieee-p1363' + }, extSig), + ok + ); + + extSig[Math.floor(Math.random() * extSig.length)] ^= 1; + } + + // Non-(EC)DSA keys should ignore the option. + const sig = crypto.sign('sha1', data, { + key: keyPem, + dsaEncoding: 'ieee-p1363' + }); + assert.strictEqual(crypto.verify('sha1', data, certPem, sig), true); + assert.strictEqual( + crypto.verify('sha1', data, { + key: certPem, + dsaEncoding: 'ieee-p1363' + }, sig), + true + ); + assert.strictEqual( + crypto.verify('sha1', data, { + key: certPem, + dsaEncoding: 'der' + }, sig), + true + ); + + for (const dsaEncoding of ['foo', null, {}, 5, true, NaN]) { + assert.throws(() => { + crypto.sign('sha1', data, { + key: certPem, + dsaEncoding + }); + }, { + code: 'ERR_INVALID_ARG_VALUE' + }); + } +} + +{ + // Test RSA-PSS. + if (!common.openSSLIsBoringSSL) { + // This key pair does not restrict the message digest algorithm or salt + // length. + const publicPem = fixtures.readKey('rsa_pss_public_2048.pem'); + const privatePem = fixtures.readKey('rsa_pss_private_2048.pem'); + + const publicKey = crypto.createPublicKey(publicPem); + const privateKey = crypto.createPrivateKey(privatePem); + + for (const key of [privatePem, privateKey]) { + // Any algorithm should work. + for (const algo of ['sha1', 'sha256']) { + // Any salt length should work. + for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { + const signature = crypto.sign(algo, 'foo', { key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = crypto.verify( + algo, + 'foo', + { key: pkey, saltLength }, + signature + ); + + assert.ok(okay); + } + } + } + } + } + + if (!common.openSSLIsBoringSSL) { + // This key pair enforces sha256 as the message digest and the MGF1 + // message digest and a salt length of at least 16 bytes. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha256_sha256_16.pem'); + + const publicKey = crypto.createPublicKey(publicPem); + const privateKey = crypto.createPrivateKey(privatePem); + + for (const key of [privatePem, privateKey]) { + // Signing with anything other than sha256 should fail. + assert.throws(() => { + crypto.sign('sha1', 'foo', key); + }, /digest not allowed/); + + // Signing with salt lengths less than 16 bytes should fail. + for (const saltLength of [8, 10, 12]) { + assert.throws(() => { + crypto.sign('sha256', 'foo', { key, saltLength }); + }, /pss saltlen too small/); + } + + // Signing with sha256 and appropriate salt lengths should work. + for (const saltLength of [undefined, 16, 18, 20]) { + const signature = crypto.sign('sha256', 'foo', { key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = crypto.verify( + 'sha256', + 'foo', + { key: pkey, saltLength }, + signature + ); + + assert.ok(okay); + } + } + } + } + + if (!common.openSSLIsBoringSSL) { + // This key enforces sha512 as the message digest and sha256 as the MGF1 + // message digest. + const publicPem = + fixtures.readKey('rsa_pss_public_2048_sha512_sha256_20.pem'); + const privatePem = + fixtures.readKey('rsa_pss_private_2048_sha512_sha256_20.pem'); + + const publicKey = crypto.createPublicKey(publicPem); + const privateKey = crypto.createPrivateKey(privatePem); + + // Node.js usually uses the same hash function for the message and for MGF1. + // However, when a different MGF1 message digest algorithm has been + // specified as part of the key, it should automatically switch to that. + // This behavior is required by sections 3.1 and 3.3 of RFC4055. + for (const key of [privatePem, privateKey]) { + // sha256 matches the MGF1 hash function and should be used internally, + // but it should not be permitted as the main message digest algorithm. + for (const algo of ['sha1', 'sha256']) { + assert.throws(() => { + crypto.sign(algo, 'foo', key); + }, /digest not allowed/); + } + + // sha512 should produce a valid signature. + const signature = crypto.sign('sha512', 'foo', key); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = crypto.verify('sha512', 'foo', pkey, signature); + + assert.ok(okay); + } + } + } +} + +// The sign function should not swallow OpenSSL errors. +// Regression test for https://github.com/nodejs/node/issues/40794. +{ + assert.throws(() => { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + crypto.sign('sha512', 'message', privateKey); + }, { + code: common.openSSLIsBoringSSL ? 'ERR_OSSL_DIGEST_TOO_BIG_FOR_RSA_KEY' : 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY', + message: common.openSSLIsBoringSSL ? /DIGEST_TOO_BIG_FOR_RSA_KEY/ : /digest too big for rsa key/ + }); +} + +{ + // This should not cause a crash: https://github.com/nodejs/node/issues/44471 + for (const key of ['', 'foo', null, undefined, true, Boolean]) { + assert.throws(() => { + crypto.verify('sha256', 'foo', { key, format: 'jwk' }, Buffer.alloc(0)); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + assert.throws(() => { + crypto.createVerify('sha256').verify({ key, format: 'jwk' }, Buffer.alloc(0)); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + assert.throws(() => { + crypto.sign('sha256', 'foo', { key, format: 'jwk' }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + assert.throws(() => { + crypto.createSign('sha256').sign({ key, format: 'jwk' }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + } +} + +{ + // Ed25519 and Ed448 must use the one-shot methods + const keys = [{ privateKey: fixtures.readKey('ed25519_private.pem', 'ascii'), + publicKey: fixtures.readKey('ed25519_public.pem', 'ascii') }]; + + if (!common.openSSLIsBoringSSL) { + keys.push({ privateKey: fixtures.readKey('ed448_private.pem', 'ascii'), + publicKey: fixtures.readKey('ed448_public.pem', 'ascii') }); + } + + for (const { publicKey, privateKey } of keys) { + assert.throws(() => { + crypto.createSign('SHA256').update('Test123').sign(privateKey); + }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_COMMAND_NOT_SUPPORTED' : 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: common.openSSLIsBoringSSL ? /public key.*COMMAND_NOT_SUPPORTED/ : 'Unsupported crypto operation' }); + assert.throws(() => { + crypto.createVerify('SHA256').update('Test123').verify(privateKey, 'sig'); + }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_COMMAND_NOT_SUPPORTED' : 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: common.openSSLIsBoringSSL ? /public key.*COMMAND_NOT_SUPPORTED/ : 'Unsupported crypto operation' }); + assert.throws(() => { + crypto.createVerify('SHA256').update('Test123').verify(publicKey, 'sig'); + }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_COMMAND_NOT_SUPPORTED' : 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: common.openSSLIsBoringSSL ? /public key.*COMMAND_NOT_SUPPORTED/ : 'Unsupported crypto operation' }); + } +} + +{ + // Dh, x25519 and x448 should not be used for signing/verifying + // https://github.com/nodejs/node/issues/53742 + const algos = ['x25519']; + if (!common.openSSLIsBoringSSL) { + algos.push('dh', 'x448'); + } + for (const algo of algos) { + const privateKey = fixtures.readKey(`${algo}_private.pem`, 'ascii'); + const publicKey = fixtures.readKey(`${algo}_public.pem`, 'ascii'); + assert.throws(() => { + crypto.createSign('SHA256').update('Test123').sign(privateKey); + }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' : 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE', message: common.openSSLIsBoringSSL ? /public key routines.*OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE/ : /operation not supported for this keytype/ }); + assert.throws(() => { + crypto.createVerify('SHA256').update('Test123').verify(privateKey, 'sig'); + }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' : 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE', message: common.openSSLIsBoringSSL ? /public key routines.*OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE/ : /operation not supported for this keytype/ }); + assert.throws(() => { + crypto.createVerify('SHA256').update('Test123').verify(publicKey, 'sig'); + }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' : 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE', message: common.openSSLIsBoringSSL ? /public key routines.*OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE/ : /operation not supported for this keytype/ }); + } +} + +// RSA-PSS Sign test by verifying with 'openssl dgst -verify' +// Note: this particular test *must* be the last in this file as it will exit +// early if no openssl binary is found +{ + if (!opensslCli) { + common.skip('node compiled without OpenSSL CLI.'); + } + + const pubfile = fixtures.path('keys', 'rsa_public_2048.pem'); + const privkey = fixtures.readKey('rsa_private_2048.pem'); + + const msg = 'Test123'; + const s5 = crypto.createSign('SHA256') + .update(msg) + .sign({ + key: privkey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING + }); + + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + + const sigfile = tmpdir.resolve('s5.sig'); + fs.writeFileSync(sigfile, s5); + const msgfile = tmpdir.resolve('s5.msg'); + fs.writeFileSync(msgfile, msg); + + exec(...common.escapePOSIXShell`"${ + opensslCli}" dgst -sha256 -verify "${pubfile}" -signature "${ + sigfile}" -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-2 "${msgfile + }"`, common.mustCall((err, stdout, stderr) => { + assert(stdout.includes('Verified OK')); + })); +}