diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 1c73bb55af..52f14c4908 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -897,7 +897,7 @@ pub const Listener = struct { socket.setTimeout(120); } - pub fn addServerName(this: *Listener, global: *JSC.JSGlobalObject, hostname: JSValue, tls: JSValue) bun.JSError!JSValue { + pub fn addServerName(this: *Listener, global: *JSC.JSGlobalObject, hostname: JSValue, context: JSValue) bun.JSError!JSValue { if (!this.ssl) { return global.throwInvalidArguments("addServerName requires SSL support", .{}); } @@ -915,7 +915,7 @@ pub const Listener = struct { return global.throwInvalidArguments("hostname pattern cannot be empty", .{}); } - if (try JSC.API.ServerConfig.SSLConfig.fromJS(JSC.VirtualMachine.get(), global, tls)) |ssl_config| { + if (try JSC.API.ServerConfig.SSLConfig.fromJS(JSC.VirtualMachine.get(), global, context)) |ssl_config| { // to keep nodejs compatibility, we allow to replace the server name this.socket_context.?.removeServerName(true, server_name); this.socket_context.?.addServerName(true, server_name, ssl_config.asUSockets()); @@ -2846,7 +2846,7 @@ fn NewSocket(comptime ssl: bool) type { } const request_cert = request_cert_js.toBoolean(); - const reject_unauthorized = request_cert_js.toBoolean(); + const reject_unauthorized = reject_unauthorized_js.toBoolean(); var verify_mode: c_int = BoringSSL.SSL_VERIFY_NONE; if (this.handlers.is_server) { if (request_cert) { diff --git a/src/bun.js/bindings/NodeTLS.cpp b/src/bun.js/bindings/NodeTLS.cpp index ac500c7dc0..f03dff75bf 100644 --- a/src/bun.js/bindings/NodeTLS.cpp +++ b/src/bun.js/bindings/NodeTLS.cpp @@ -136,11 +136,9 @@ NodeTLSSecureContext::~NodeTLSSecureContext() = default; void NodeTLSSecureContext::setCACert(const ncrypto::BIOPointer& bio) { - if (!bio) { - return; - } + ASSERT(bio); - while (ncrypto::X509Pointer x509 = ncrypto::X509Pointer(PEM_read_bio_X509_AUX(bio.get(), nullptr, ncrypto::NoPasswordCallback, nullptr))) { + while (ncrypto::X509Pointer x509 { PEM_read_bio_X509_AUX(bio.get(), nullptr, ncrypto::NoPasswordCallback, nullptr) }) { RELEASE_ASSERT(X509_STORE_add_cert(getCertStore(), x509.get()) == 1); RELEASE_ASSERT(SSL_CTX_add_client_CA(context(), x509.get()) == 1); } @@ -156,13 +154,17 @@ void NodeTLSSecureContext::setRootCerts() bool NodeTLSSecureContext::applySNI(SSL* ssl) { - // TODO(@heimskr): ERR_clear_error()? - X509* x509 = SSL_get_certificate(ssl); + SSL_CTX* ctx = context(); + + X509* x509 = [ctx] { + ncrypto::ClearErrorOnReturn clearErrorOnReturn; + return SSL_CTX_get0_certificate(ctx); + }(); + if (!x509) { return false; } - SSL_CTX* ctx = context(); EVP_PKEY* pkey = SSL_CTX_get0_privatekey(ctx); STACK_OF(X509) * chain; @@ -235,7 +237,7 @@ int NodeTLSSecureContext::ticketCompatibilityCallback(SSL* ssl, unsigned char* n return 1; } -// https://github.com/190n/node/blob/5812a61a68d50c65127beb68dd4dfb0242e3c5c9/src/crypto/crypto_context.cc#L112 +// https://github.com/nodejs/node/blob/5812a61a68d50c65127beb68dd4dfb0242e3c5c9/src/crypto/crypto_context.cc#L112 static int useCertificateChain(SSL_CTX* ctx, ncrypto::X509Pointer&& x, STACK_OF(X509) * extra_certs, ncrypto::X509Pointer* cert, ncrypto::X509Pointer* issuer_) { RELEASE_ASSERT(!*issuer_); @@ -285,7 +287,7 @@ static int useCertificateChain(SSL_CTX* ctx, ncrypto::X509Pointer&& x, STACK_OF( return ret; } -// https://github.com/190n/node/blob/5812a61a68d50c65127beb68dd4dfb0242e3c5c9/src/crypto/crypto_context.cc#L183 +// https://github.com/nodejs/node/blob/5812a61a68d50c65127beb68dd4dfb0242e3c5c9/src/crypto/crypto_context.cc#L183 static int useCertificateChain(SSL_CTX* ctx, ncrypto::BIOPointer&& in, ncrypto::X509Pointer* cert, ncrypto::X509Pointer* issuer) { ERR_clear_error(); @@ -366,7 +368,7 @@ bool NodeTLSSecureContext::addCert(JSGlobalObject* globalObject, ThrowScope& sco } if (useCertificateChain(context(), std::move(bio), &m_cert, &m_issuer) == 0) { - return throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to set certificate"); + throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to set certificate"); return false; } @@ -380,14 +382,23 @@ JSC_DEFINE_HOST_FUNCTION(secureContextInit, (JSGlobalObject * globalObject, Call auto scope = DECLARE_THROW_SCOPE(vm); ArgList args(callFrame); - JSValue secureProtocolValue = args.at(0); + JSValue optionsValue = args.at(0); JSValue minVersionValue = args.at(1); JSValue maxVersionValue = args.at(2); + if (!optionsValue.isObject()) { + return throwArgumentTypeError(*globalObject, scope, 0, "options"_s, "SecureContext"_s, "init"_s, "object"_s); + } + int minVersion = minVersionValue.toInt32(globalObject); int maxVersion = maxVersionValue.toInt32(globalObject); const SSL_METHOD* method = TLS_method(); + JSObject* options = JSC::asObject(optionsValue); + + JSValue secureProtocolValue = options->get(globalObject, Identifier::fromString(vm, "secureProtocol"_s)); + RETURN_IF_EXCEPTION(scope, {}); + if (secureProtocolValue.isString()) { String secureProtocol = secureProtocolValue.toWTFString(globalObject); @@ -461,6 +472,24 @@ JSC_DEFINE_HOST_FUNCTION(secureContextInit, (JSGlobalObject * globalObject, Call } } + auto getTriState = [&](ASCIILiteral name) -> WTF::TriState { + JSValue value = options->get(globalObject, Identifier::fromString(vm, name)); + RETURN_IF_EXCEPTION(scope, WTF::TriState::Indeterminate); + + if (value.isBoolean()) { + return triState(value.asBoolean()); + } + + if (!value.isUndefined()) { + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, makeString("options."_s, name), "boolean"_s, value); + } + + return WTF::TriState::Indeterminate; + }; + + WTF::TriState requestCert = getTriState("requestCert"); + RETURN_IF_EXCEPTION(scope, {}); + thisObject->context(SSL_CTX_new(method)); SSL_CTX* context = thisObject->context(); @@ -472,6 +501,18 @@ JSC_DEFINE_HOST_FUNCTION(secureContextInit, (JSGlobalObject * globalObject, Call SSL_CTX_set_options(context, SSL_OP_NO_SSLv2); SSL_CTX_set_options(context, SSL_OP_NO_SSLv3); + if (requestCert != TriState::True) { + SSL_CTX_set_verify(context, SSL_VERIFY_NONE, nullptr); + } else { + WTF::TriState rejectUnauthorized = getTriState("rejectUnauthorized"); + RETURN_IF_EXCEPTION(scope, {}); + if (rejectUnauthorized == WTF::TriState::True) { + SSL_CTX_set_verify(context, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + } else { + SSL_CTX_set_verify(context, SSL_VERIFY_PEER, nullptr); + } + } + #if OPENSSL_VERSION_MAJOR >= 3 // TODO(@heimskr): OPENSSL_VERSION_MAJOR doesn't appear to be defined anywhere. SSL_CTX_set_options(context, SSL_OP_ALLOW_CLIENT_RENEGOTIATION); @@ -554,11 +595,11 @@ JSC_DEFINE_HOST_FUNCTION(secureContextAddCACert, (JSGlobalObject * globalObject, int written = ncrypto::BIOPointer::Write(&bio, cert.span()); if (written < 0 || static_cast(written) != cert.length()) { - return JSC::encodedJSUndefined(); + return JSValue::encode(jsBoolean(false)); } thisObject->setCACert(bio); - return JSC::encodedJSUndefined(); + return JSValue::encode(jsBoolean(true)); } JSC_DEFINE_HOST_FUNCTION(secureContextSetECDHCurve, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -632,7 +673,7 @@ JSC_DEFINE_HOST_FUNCTION(secureContextSetKey, (JSGlobalObject * globalObject, Ca return throwCryptoError(globalObject, scope, ERR_get_error(), "SSL_CTX_use_PrivateKey"); } - return JSC::encodedJSUndefined(); + return JSValue::encode(jsBoolean(true)); } static const HashTableValue NodeTLSSecureContextPrototypeTableValues[] = { diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 31317bc4d3..250bdeac11 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -361,10 +361,10 @@ const ServerHandlers: SocketHandler = { socket[kServerSocket] = self._handle; const options = self[bunSocketServerOptions]; const { pauseOnConnect, connectionListener, [kSocketClass]: SClass, requestCert, rejectUnauthorized } = options; - const _socket = new SClass(options) as NetSocket | TLSSocket; + const _socket = new SClass({ ...options, isServer: true }) as NetSocket | TLSSocket; _socket.isServer = true; - _socket._requestCert = requestCert; - _socket._rejectUnauthorized = rejectUnauthorized; + _socket._requestCert = requestCert ?? _socket._requestCert; + _socket._rejectUnauthorized = rejectUnauthorized ?? _socket._rejectUnauthorized; _socket[kAttach](this.localPort, socket); @@ -2597,6 +2597,7 @@ function initSocketHandle(self) { // Handle creation may be deferred to bind() or connect() time. if (self._handle) { self._handle[owner_symbol] = self; + self._configureHandle?.(); } } diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index 4d56675378..6d83790560 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -222,9 +222,9 @@ var InternalSecureContext = class SecureContext { secureOptions; constructor(options) { - const { honorCipherOrder, minVersion, maxVersion, secureProtocol } = options; + const { honorCipherOrder, minVersion, maxVersion } = options; - this.context = new NodeTLSSecureContext(secureProtocol, minVersion, maxVersion); + this.context = new NodeTLSSecureContext(options); if (options) { let { cert } = options; @@ -285,7 +285,7 @@ var InternalSecureContext = class SecureContext { } this.context.init( - secureProtocol, + options, toV("minimum", minVersion, DEFAULT_MIN_VERSION), toV("maximum", maxVersion, DEFAULT_MAX_VERSION), ); @@ -504,7 +504,6 @@ function toBuf(val, encoding?: BufferEncoding | "buffer") { function addCACerts(context, certs, name) { ArrayPrototypeForEach.$call(certs, cert => { validateKeyOrCertOption(name, cert); - context.addCACert(cert); }); } @@ -526,7 +525,6 @@ function setKey(context, key, passphrase, name) { if (passphrase !== undefined && passphrase !== null) { validateString(passphrase, `${name}.passphrase`); } - context.setKey(key, passphrase); } function processCiphers(ciphers, name) { @@ -608,7 +606,7 @@ function TLSSocket(socket?, options?) { } if (typeof options === "object") { - const { ALPNProtocols, SNICallback: sni } = options; + const { ALPNProtocols, SNICallback: sni, rejectUnauthorized, requestCert } = options; if (ALPNProtocols) { convertALPNProtocols(ALPNProtocols, this); } @@ -618,12 +616,21 @@ function TLSSocket(socket?, options?) { this._SNICallback = sni; } + if (typeof rejectUnauthorized !== "undefined") { + this._rejectUnauthorized = rejectUnauthorized; + } + + if (typeof requestCert !== "undefined") { + this._requestCert = requestCert; + } + if (isNetSocketOrDuplex) { this._handle = socket; // keep compatibility with http2-wrapper or other places that try to grab JSStreamSocket in node.js, with here is just the TLSSocket this._handle._parentWrap = this; } } + this[ksecureContext] = options.secureContext || createSecureContext(options); this.authorized = false; this.secureConnecting = true; @@ -633,9 +640,12 @@ function TLSSocket(socket?, options?) { this[ksession] = options.session || null; this.once("connect", socket => { - if (socket?.isServer) { - this._handle.certCallback = loadSNI.bind(this); - this._handle.enableCertCallback(); + if (socket) { + socket._handle?.setVerifyMode(!!socket._requestCert || !socket.isServer, !!socket._rejectUnauthorized); + if (socket.isServer) { + socket._handle.certCallback = loadSNI.bind(socket); + socket._handle.enableCertCallback(); + } } }); } @@ -646,6 +656,12 @@ TLSSocket.prototype._start = function _start() { this.connect(); }; +TLSSocket.prototype._configureHandle = function _configureHandle() { + if (typeof this._rejectUnauthorized !== "undefined") { + this._handle.setVerifyMode(!!this._requestCert || !this.isServer, !!this._rejectUnauthorized); + } +}; + TLSSocket.prototype.getSession = function getSession() { return this._handle?.getSession?.(); }; @@ -815,6 +831,7 @@ function Server(options, secureConnectionListener): void { this.ALPNProtocols = undefined; this._SNICallback = SNICallback; this.server = this; + this._contexts = []; let contexts: Map | null = null; @@ -830,6 +847,7 @@ function Server(options, secureConnectionListener): void { } else { if (!contexts) contexts = new Map(); contexts.set(hostname, context); + this._contexts.push(context); } }; @@ -933,7 +951,6 @@ function Server(options, secureConnectionListener): void { clientRenegotiationLimit: CLIENT_RENEG_LIMIT, clientRenegotiationWindow: CLIENT_RENEG_WINDOW, contexts: contexts, - foo: "bar", }, TLSSocket, ];