Compare commits

...

22 Commits

Author SHA1 Message Date
Kai Tamkun
8496625917 Remove unused identifier 2025-07-01 18:17:54 -07:00
Kai Tamkun
5717a2beba Merge branch 'main' into kai/tls-sni 2025-07-01 18:17:22 -07:00
Kai Tamkun
6c9643d077 Some progress on unbreaking things 2025-06-16 19:52:18 -07:00
Kai Tamkun
101f0e973c Add more passing tests 2025-06-12 17:54:35 -07:00
Kai Tamkun
7f66114af0 Fix test-tls-sni-server-client.js 2025-06-12 17:47:57 -07:00
Kai Tamkun
43e9a89743 utf8() instead of span8() 2025-06-12 16:34:36 -07:00
Kai Tamkun
47594eeb60 How'd that get there 2025-06-12 15:33:23 -07:00
Kai Tamkun
8e2c440d14 oops 2025-06-12 15:33:05 -07:00
Kai Tamkun
9aecb95c88 setCert 2025-06-12 15:03:06 -07:00
Kai Tamkun
09818db8bb test-tls-sni-servername.js 2025-06-12 14:18:51 -07:00
Kai Tamkun
3ad73252f6 A bit of progress on SNI 2025-06-10 18:44:51 -07:00
Kai Tamkun
2672264096 Merge branch 'main' into kai/tls-sni 2025-06-10 15:40:17 -07:00
Kai Tamkun
af5c0bdea6 Initial work on cert callbacks 2025-06-10 15:39:10 -07:00
Kai Tamkun
1ec0718e05 Pass options to SClass 2025-06-10 15:37:24 -07:00
Kai Tamkun
1bc25d3150 addRootCerts 2025-06-05 19:53:39 -07:00
Kai Tamkun
b0a7c945eb setCiphers, addCACert, setECDHCurve 2025-06-05 19:15:50 -07:00
Kai Tamkun
12da7944ad Add missing NodeTLSSecureContext::createStructure 2025-06-05 18:00:55 -07:00
Kai Tamkun
2cda3c9314 Initial work on secure contexts 2025-06-05 16:54:14 -07:00
Kai Tamkun
c5b32ef56d Merge branch 'pfg/tls-checkserveridentity' into kai/tls-sni 2025-06-03 14:08:21 -07:00
pfg
3f620e314c done 2025-06-03 13:57:45 -07:00
pfg
939fa03d8c 2/3 2025-06-02 21:09:04 -07:00
pfg
28c870c5dc intro 2025-06-02 20:41:27 -07:00
19 changed files with 1655 additions and 44 deletions

View File

@@ -86,6 +86,8 @@ pub fn NewSocket(comptime ssl: bool) type {
server_name: ?[]const u8 = null,
buffered_data_for_node_net: bun.ByteList = .{},
bytes_written: u64 = 0,
sni_callback: JSC.Strong.Optional = .empty,
cert_callback: JSC.Strong.Optional = .empty,
// TODO: switch to something that uses `visitAggregate` and have the
// `Listener` keep a list of all the sockets JSValue in there
@@ -440,13 +442,39 @@ pub fn NewSocket(comptime ssl: bool) type {
}
}
}
const ctx = BoringSSL.SSL_get_SSL_CTX(ssl_ptr);
_ = BoringSSL.SSL_set_app_data(ssl_ptr, this);
if (this.protos) |protos| {
if (this.handlers.is_server) {
BoringSSL.SSL_CTX_set_alpn_select_cb(BoringSSL.SSL_get_SSL_CTX(ssl_ptr), selectALPNCallback, bun.cast(*anyopaque, this));
BoringSSL.SSL_CTX_set_alpn_select_cb(ctx, selectALPNCallback, bun.cast(*anyopaque, this));
} else {
_ = BoringSSL.SSL_set_alpn_protos(ssl_ptr, protos.ptr, @as(c_uint, @intCast(protos.len)));
}
}
if (this.handlers.is_server) {
_ = BoringSSL.SSL_CTX_set_tlsext_servername_callback(ctx, struct {
fn cb(cb_ssl: ?*BoringSSL.SSL, _: [*c]c_int, _: ?*anyopaque) callconv(.C) c_int {
const servername: [*c]const u8 = BoringSSL.SSL_get_servername(cb_ssl, BoringSSL.TLSEXT_NAMETYPE_host_name);
if (servername == null) {
return BoringSSL.SSL_TLSEXT_ERR_NOACK;
}
const cb_this: *This = @alignCast(@ptrCast(BoringSSL.SSL_get_app_data(cb_ssl)));
return cb_this.onSNI(servername[0..std.mem.len(servername)]);
}
}.cb);
_ = BoringSSL.SSL_set_cert_cb(ssl_ptr, struct {
fn cb(cb_ssl: ?*BoringSSL.SSL, _: ?*anyopaque) callconv(.C) c_int {
const servername: [*c]const u8 = BoringSSL.SSL_get_servername(cb_ssl, BoringSSL.TLSEXT_NAMETYPE_host_name) orelse "";
const cb_this: *This = @alignCast(@ptrCast(BoringSSL.SSL_get_app_data(cb_ssl)));
return cb_this.onCert(servername[0..std.mem.len(servername)]);
}
}.cb, bun.cast(*anyopaque, this));
}
}
}
}
@@ -607,7 +635,7 @@ pub fn NewSocket(comptime ssl: bool) type {
pub fn onClose(this: *This, _: Socket, err: c_int, _: ?*anyopaque) void {
JSC.markBinding(@src());
log("onClose {s}", .{if (this.handlers.is_server) "S" else "C"});
log("onClose {s} {*}", .{ if (this.handlers.is_server) "S" else "C", this });
this.detachNativeCallback();
this.socket.detach();
defer this.deref();
@@ -651,6 +679,100 @@ pub fn NewSocket(comptime ssl: bool) type {
};
}
pub fn onSNI(this: *This, servername: []const u8) c_int {
if (comptime ssl == false) {
return BoringSSL.SSL_TLSEXT_ERR_NOACK;
}
JSC.markBinding(@src());
log("onSNI {s} ({s})", .{ if (this.handlers.is_server) "S" else "C", servername });
if (this.socket.isDetached()) return BoringSSL.SSL_TLSEXT_ERR_NOACK;
if (this.sni_callback.get()) |callback| {
const globalObject = this.handlers.globalObject;
const this_value = this.getThisValue(globalObject);
_ = callback.call(globalObject, this_value, &[_]JSValue{
this_value,
ZigString.init(servername).toJS(globalObject),
}) catch |err| {
_ = this.handlers.callErrorHandler(this_value, &.{ this_value, globalObject.takeError(err) });
};
}
return BoringSSL.SSL_TLSEXT_ERR_OK;
}
pub fn onCert(this: *This, servername: []const u8) c_int {
if (comptime ssl == false) {
return 1;
}
if (!this.handlers.is_server or !this.flags.is_waiting_cert_cb) {
return 1;
}
if (this.flags.cert_cb_running) {
return -1;
}
JSC.markBinding(@src());
log("onCert {s} ({s})", .{ if (this.handlers.is_server) "S" else "C", servername });
if (this.socket.isDetached()) return -1;
this.flags.cert_cb_running = true;
// Presence already verified by isWaitingCertCb
const callback = this.cert_callback.get().?;
const globalObject = this.handlers.globalObject;
const this_value = this.getThisValue(globalObject);
_ = callback.call(globalObject, this_value, &[_]JSValue{
ZigString.init(servername).toJS(globalObject),
}) catch |err| {
_ = this.handlers.callErrorHandler(this_value, &.{ this_value, globalObject.takeError(err) });
};
return if (this.flags.cert_cb_running) -1 else 1;
}
extern fn Bun__NodeTLS__certCallbackDone(sni_context: JSValue, ssl_ptr: *BoringSSL.SSL, globalObject: *JSC.JSGlobalObject) callconv(.C) c_int;
pub fn enableCertCallback(this: *This, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue {
this.flags.is_waiting_cert_cb = true;
return .js_undefined;
}
pub fn certCallbackDone(this: *This, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
_ = callframe;
bun.assert(this.flags.is_waiting_cert_cb and this.flags.cert_cb_running);
const this_value: JSC.JSValue = this.getThisValue(globalObject);
const ssl_ptr = this.socket.ssl() orelse return JSValue.jsBoolean(false);
const sni_context = try this_value.get(globalObject, "sni_context") orelse return JSValue.jsBoolean(false);
const cpp_result = Bun__NodeTLS__certCallbackDone(sni_context, ssl_ptr, globalObject);
switch (cpp_result) {
0 => {
return this.handlers.onError.call(globalObject, this_value, &[_]JSValue{ this_value, globalObject.toTypeError(JSC.Error.INVALID_ARG_TYPE, "Invalid SNI context", .{}) });
},
1 => {},
else => return error.JSError, // C++ code threw
}
this.flags.cert_cb_running = false;
// TODO(@heimskr): do the equivalent of TLSWrap::Cycle() here.
this.flags.is_waiting_cert_cb = false;
return JSValue.jsBoolean(true);
}
pub fn onData(this: *This, _: Socket, data: []const u8) void {
JSC.markBinding(@src());
if (this.socket.isDetached()) return;
@@ -1230,6 +1352,14 @@ pub fn NewSocket(comptime ssl: bool) type {
return .js_undefined;
}
pub fn setSNICallback(this: *This, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
this.sni_callback.set(globalObject, value);
}
pub fn setCertCallback(this: *This, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
this.cert_callback.set(globalObject, value);
}
pub fn terminate(this: *This, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue {
JSC.markBinding(@src());
this.closeAndDetach(.failure);
@@ -1251,6 +1381,7 @@ pub fn NewSocket(comptime ssl: bool) type {
pub fn close(this: *This, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
JSC.markBinding(@src());
_ = callframe;
_ = this.cert_callback.swap();
this.socket.close(.normal);
this.socket.detach();
this.poll_ref.unref(globalObject.bunVM());
@@ -1699,7 +1830,9 @@ const Flags = packed struct(u16) {
owned_protos: bool = true,
is_paused: bool = false,
allow_half_open: bool = false,
_: u7 = 0,
is_waiting_cert_cb: bool = false,
cert_cb_running: bool = false,
_: u5 = 0,
};
pub const WrappedSocket = extern struct {

View File

@@ -237,6 +237,20 @@ const sslOnly = {
fn: "getX509Certificate",
length: 0,
},
SNICallback: {
setter: "setSNICallback",
},
certCallback: {
setter: "setCertCallback",
},
certCallbackDone: {
fn: "certCallbackDone",
length: 0,
},
enableCertCallback: {
fn: "enableCertCallback",
length: 0,
},
} as const;
export default [
generate(true),

View File

@@ -23,6 +23,8 @@
namespace Bun {
void determineSpecificType(JSC::VM& vm, JSC::JSGlobalObject* globalObject, WTF::StringBuilder& builder, JSValue value);
class ErrorCodeCache : public JSC::JSInternalFieldObjectImpl<NODE_ERROR_COUNT> {
public:
using Base = JSInternalFieldObjectImpl<NODE_ERROR_COUNT>;

View File

@@ -268,6 +268,13 @@ JSC::EncodedJSValue throwArgumentTypeError(JSC::JSGlobalObject& lexicalGlobalObj
return Bun::throwError(&lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, makeArgumentTypeErrorMessage(argumentIndex, argumentName, functionInterfaceName, functionName, "an instance of "_s, expectedType));
}
JSC::EncodedJSValue throwArgumentValueError(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, ASCIILiteral argumentName, JSValue actualValue)
{
WTF::StringBuilder builder;
Bun::determineSpecificType(JSC::getVM(&lexicalGlobalObject), &lexicalGlobalObject, builder, actualValue);
return Bun::throwError(&lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_VALUE, makeString("The \""_s, argumentName, "\" argument is invalid. Received "_s, builder.toString()));
}
void throwAttributeTypeError(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, ASCIILiteral interfaceName, ASCIILiteral attributeName, ASCIILiteral expectedType)
{
throwTypeError(lexicalGlobalObject, scope, makeString("The "_s, interfaceName, '.', attributeName, " attribute must be an instance of "_s, expectedType));

View File

@@ -51,6 +51,7 @@ WEBCORE_EXPORT JSC::EncodedJSValue throwArgumentMustBeEnumError(JSC::JSGlobalObj
WEBCORE_EXPORT JSC::EncodedJSValue throwArgumentMustBeFunctionError(JSC::JSGlobalObject&, JSC::ThrowScope&, unsigned argumentIndex, ASCIILiteral argumentName, ASCIILiteral functionInterfaceName, ASCIILiteral functionName);
WEBCORE_EXPORT JSC::EncodedJSValue throwArgumentMustBeObjectError(JSC::JSGlobalObject&, JSC::ThrowScope&, unsigned argumentIndex, ASCIILiteral argumentName, ASCIILiteral functionInterfaceName, ASCIILiteral functionName);
WEBCORE_EXPORT JSC::EncodedJSValue throwArgumentTypeError(JSC::JSGlobalObject&, JSC::ThrowScope&, unsigned argumentIndex, ASCIILiteral argumentName, ASCIILiteral functionInterfaceName, ASCIILiteral functionName, ASCIILiteral expectedType);
WEBCORE_EXPORT JSC::EncodedJSValue throwArgumentValueError(JSC::JSGlobalObject&, JSC::ThrowScope&, ASCIILiteral argumentName, JSC::JSValue actualValue);
WEBCORE_EXPORT JSC::EncodedJSValue throwRequiredMemberTypeError(JSC::JSGlobalObject&, JSC::ThrowScope&, ASCIILiteral memberName, ASCIILiteral dictionaryName, ASCIILiteral expectedType);
JSC::EncodedJSValue throwConstructorScriptExecutionContextUnavailableError(JSC::JSGlobalObject&, JSC::ThrowScope&, ASCIILiteral interfaceName);

View File

@@ -1,37 +1,755 @@
#include "config.h"
#include "NodeTLS.h"
#include "AsyncContextFrame.h"
#include "JavaScriptCore/JSObject.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "JavaScriptCore/ArrayConstructor.h"
#include "libusockets.h"
#include "JavaScriptCore/FunctionPrototype.h"
#include "JavaScriptCore/FunctionConstructor.h"
#include "JavaScriptCore/LazyClassStructure.h"
#include "JavaScriptCore/LazyClassStructureInlines.h"
#include "ErrorCode.h"
#include "ErrorCode+List.h"
#include "JSDOMExceptionHandling.h"
#include "ZigGlobalObject.h"
#include "ErrorCode.h"
#include "openssl/base.h"
#include "openssl/bio.h"
#include "../../packages/bun-usockets/src/crypto/root_certs_header.h"
#include "libusockets.h"
#include "wtf/Scope.h"
namespace Bun {
using namespace JSC;
JSC_DEFINE_HOST_FUNCTION(getBundledRootCertificates, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
JSC::JSValue createNodeTLSBinding(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
JSFinalObject* obj = constructEmptyObject(globalObject);
struct us_cert_string_t* out;
auto size = us_raw_root_certs(&out);
if (size < 0) {
return JSValue::encode(jsUndefined());
}
auto rootCertificates = JSC::JSArray::create(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), size);
for (auto i = 0; i < size; i++) {
auto raw = out[i];
auto str = WTF::String::fromUTF8(std::span { raw.str, raw.len });
rootCertificates->putDirectIndex(globalObject, i, JSC::jsString(vm, str));
obj->putDirect(vm,
JSC::PropertyName(JSC::Identifier::fromString(vm, "canonicalizeIP"_s)),
JSC::JSFunction::create(vm, globalObject, 1, "canonicalizeIP"_s, Bun__canonicalizeIP, ImplementationVisibility::Public, NoIntrinsic),
0);
obj->putDirect(vm,
JSC::PropertyName(JSC::Identifier::fromString(vm, "SecureContext"_s)),
defaultGlobalObject(globalObject)->NodeTLSSecureContext(),
0);
obj->putDirect(vm,
JSC::PropertyName(JSC::Identifier::fromString(vm, "SSL_OP_CIPHER_SERVER_PREFERENCE"_s)),
JSC::jsNumber(SSL_OP_CIPHER_SERVER_PREFERENCE),
0);
obj->putDirect(vm,
JSC::PropertyName(JSC::Identifier::fromString(vm, "TLS1_3_VERSION"_s)),
JSC::jsNumber(TLS1_3_VERSION),
0);
obj->putDirect(vm,
JSC::PropertyName(JSC::Identifier::fromString(vm, "TLS1_2_VERSION"_s)),
JSC::jsNumber(TLS1_2_VERSION),
0);
obj->putDirect(vm,
JSC::PropertyName(JSC::Identifier::fromString(vm, "TLS1_1_VERSION"_s)),
JSC::jsNumber(TLS1_1_VERSION),
0);
obj->putDirect(vm,
JSC::PropertyName(JSC::Identifier::fromString(vm, "TLS1_VERSION"_s)),
JSC::jsNumber(TLS1_VERSION),
0);
return obj;
}
void configureNodeTLS(JSC::VM& vm, Zig::GlobalObject* globalObject)
{
globalObject->m_NodeTLSSecureContextClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto prototype = NodeTLSSecureContext::createPrototype(init.vm, init.global);
auto* structure = NodeTLSSecureContext::createStructure(init.vm, init.global, prototype);
auto* constructorStructure = NodeTLSSecureContextConstructor::createStructure(
init.vm, init.global, init.global->m_functionPrototype.get());
auto* constructor = NodeTLSSecureContextConstructor::create(
init.vm, init.global, constructorStructure, prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});
}
static EncodedJSValue throwCryptoError(JSGlobalObject* globalObject, ThrowScope& scope, uint32_t err, const char* message)
{
char message_buffer[128] {};
if (err != 0 || message == nullptr) {
ERR_error_string_n(err, message_buffer, sizeof(message_buffer));
message = message_buffer;
}
return JSValue::encode(JSC::objectConstructorFreeze(globalObject, rootCertificates));
RELEASE_ASSERT(*message != '\0');
throwException(globalObject, scope, jsString(globalObject->vm(), String::fromUTF8(message)));
return {};
}
NodeTLSSecureContext* NodeTLSSecureContext::create(VM& vm, JSGlobalObject* globalObject, ArgList args)
{
auto scope = DECLARE_THROW_SCOPE(vm);
auto* zigGlobalObject = defaultGlobalObject(globalObject);
NodeTLSSecureContext* ptr = new (NotNull, allocateCell<NodeTLSSecureContext>(vm)) NodeTLSSecureContext(vm, zigGlobalObject->NodeTLSSecureContextStructure());
ptr->finishCreation(vm);
return ptr;
}
NodeTLSSecureContext::NodeTLSSecureContext(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}
NodeTLSSecureContext::~NodeTLSSecureContext() = default;
void NodeTLSSecureContext::setCACert(const ncrypto::BIOPointer& bio)
{
ASSERT(bio);
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);
}
}
void NodeTLSSecureContext::setRootCerts()
{
ncrypto::ClearErrorOnReturn clearErrorOnReturn;
X509_STORE* store = getCertStore();
X509_STORE_up_ref(store);
SSL_CTX_set_cert_store(context(), store);
}
bool NodeTLSSecureContext::applySNI(SSL* ssl)
{
SSL_CTX* ctx = context();
X509* x509 = [ctx] {
ncrypto::ClearErrorOnReturn clearErrorOnReturn;
return SSL_CTX_get0_certificate(ctx);
}();
if (!x509) {
return false;
}
EVP_PKEY* pkey = SSL_CTX_get0_privatekey(ctx);
STACK_OF(X509) * chain;
int success = SSL_CTX_get0_chain_certs(ctx, &chain);
if (success == 1) {
success = SSL_use_certificate(ssl, x509);
}
if (success == 1) {
success = SSL_use_PrivateKey(ssl, pkey);
}
if (success == 1 && chain != nullptr) {
success = SSL_set1_chain(ssl, chain);
}
return success == 1;
}
int NodeTLSSecureContext::setCACerts(SSL* ssl)
{
int err = SSL_set1_verify_cert_store(ssl, SSL_CTX_get_cert_store(context()));
if (err != 1) {
return err;
}
STACK_OF(X509_NAME)* list = SSL_dup_CA_list(SSL_CTX_get_client_CA_list(context()));
SSL_set_client_CA_list(ssl, list);
return 1;
}
void NodeTLSSecureContext::setX509StoreFlag(unsigned long flags)
{
RELEASE_ASSERT(X509_STORE_set_flags(getCertStore(), flags) == 1);
}
X509_STORE* NodeTLSSecureContext::getCertStore() const
{
if (m_certStore == nullptr) {
// TODO(@heimskr): complete implementation.
m_certStore = { X509_STORE_new(), X509_STORE_free };
SSL_CTX_set_cert_store(m_context.get(), m_certStore.get());
}
return m_certStore.get();
}
int NodeTLSSecureContext::ticketCompatibilityCallback(SSL* ssl, unsigned char* name, unsigned char* iv, EVP_CIPHER_CTX* ectx, HMAC_CTX* hctx, int enc)
{
auto* secureContext = static_cast<NodeTLSSecureContext*>(SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl)));
if (enc) {
memcpy(name, secureContext->m_ticketKeyName, sizeof(secureContext->m_ticketKeyName));
if (!ncrypto::CSPRNG(iv, 16) || EVP_EncryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr, secureContext->m_ticketKeyAES, iv) <= 0 || HMAC_Init_ex(hctx, secureContext->m_ticketKeyHMAC, sizeof(secureContext->m_ticketKeyHMAC), EVP_sha256(), nullptr) <= 0) {
return -1;
}
return 1;
}
if (memcmp(name, secureContext->m_ticketKeyName, sizeof(secureContext->m_ticketKeyName)) != 0) {
// The ticket key name does not match. Discard the ticket.
return 0;
}
if (EVP_DecryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr, secureContext->m_ticketKeyAES, iv) <= 0 || HMAC_Init_ex(hctx, secureContext->m_ticketKeyHMAC, sizeof(secureContext->m_ticketKeyHMAC), EVP_sha256(), nullptr) <= 0) {
return -1;
}
return 1;
}
// 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_);
RELEASE_ASSERT(!*cert);
X509* issuer = nullptr;
int ret = SSL_CTX_use_certificate(ctx, x.get());
if (ret) {
SSL_CTX_clear_extra_chain_certs(ctx);
for (int i = 0; i < sk_X509_num(extra_certs); i++) {
X509* ca = sk_X509_value(extra_certs, i);
if (!SSL_CTX_add1_chain_cert(ctx, ca)) {
ret = 0;
issuer = nullptr;
break;
}
if (issuer != nullptr || X509_check_issued(ca, x.get()) != X509_V_OK) {
continue;
}
issuer = ca;
}
}
if (ret) {
if (issuer == nullptr) {
*issuer_ = ncrypto::X509Pointer::IssuerFrom(ctx, x.view());
} else {
issuer_->reset(X509_dup(issuer));
if (!issuer_) {
ret = 0;
}
}
}
if (ret && x != nullptr) {
cert->reset(X509_dup(x.get()));
if (!*cert) {
ret = 0;
}
}
return ret;
}
// 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();
ncrypto::X509Pointer x(PEM_read_bio_X509_AUX(in.get(), nullptr, ncrypto::NoPasswordCallback, nullptr));
if (!x) {
return 0;
}
ncrypto::StackOfX509 extra_certs(sk_X509_new_null());
if (!extra_certs) {
return 0;
}
while (ncrypto::X509Pointer extra { PEM_read_bio_X509(in.get(), nullptr, ncrypto::NoPasswordCallback, nullptr) }) {
if (sk_X509_push(extra_certs.get(), extra.get())) {
extra.release();
continue;
}
return 0;
}
// When the while loop ends, it's usually just EOF.
uint32_t err = ERR_peek_last_error();
if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
ERR_clear_error();
} else {
// some real error
return 0;
}
return useCertificateChain(ctx, std::move(x), extra_certs.get(), cert, issuer);
}
ncrypto::BIOPointer NodeTLSSecureContext::loadBIO(JSGlobalObject* globalObject, JSValue value)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
ncrypto::BIOPointer bio = ncrypto::BIOPointer::NewSecMem();
if (!bio) {
scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Error creating BIO"_s));
return {};
}
int written {};
size_t expected {};
if (value.isString()) {
String string = value.toWTFString(globalObject);
expected = string.length();
written = ncrypto::BIOPointer::Write(&bio, string);
} else if (auto* view = JSC::jsDynamicCast<JSC::JSArrayBufferView*>(value)) {
written = ncrypto::BIOPointer::Write(&bio, view->span());
expected = view->byteLength();
} else {
scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, "Invalid certificate"_s));
return {};
}
if (written < 0 || static_cast<size_t>(written) != expected) {
scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Error writing to BIO"_s));
return {};
}
return bio;
}
bool NodeTLSSecureContext::addCert(JSGlobalObject* globalObject, ThrowScope& scope, ncrypto::BIOPointer bio)
{
ncrypto::ClearErrorOnReturn clearErrorOnReturn;
if (!bio) {
return false;
}
if (useCertificateChain(context(), std::move(bio), &m_cert, &m_issuer) == 0) {
throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to set certificate");
return false;
}
return true;
}
JSC_DEFINE_HOST_FUNCTION(secureContextInit, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto* thisObject = jsCast<NodeTLSSecureContext*>(callFrame->thisValue());
auto scope = DECLARE_THROW_SCOPE(vm);
ArgList args(callFrame);
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);
if (secureProtocol == "SSLv2_method" || secureProtocol == "SSLv2_server_method" || secureProtocol == "SSLv2_client_method") {
throwException(globalObject, scope, createError(globalObject, ErrorCode::ERR_TLS_INVALID_PROTOCOL_METHOD, "SSLv2 methods disabled"_s));
return {};
}
if (secureProtocol == "SSLv3_method" || secureProtocol == "SSLv3_server_method" || secureProtocol == "SSLv3_client_method") {
throwException(globalObject, scope, createError(globalObject, ErrorCode::ERR_TLS_INVALID_PROTOCOL_METHOD, "SSLv3 methods disabled"_s));
return {};
}
constexpr int maxSupportedVersion = TLS1_3_VERSION;
if (secureProtocol == "SSLv23_method") {
maxVersion = TLS1_2_VERSION;
} else if (secureProtocol == "SSLv23_server_method") {
maxVersion = TLS1_2_VERSION;
method = TLS_server_method();
} else if (secureProtocol == "SSLv23_client_method") {
maxVersion = TLS1_2_VERSION;
method = TLS_client_method();
} else if (secureProtocol == "TLS_method") {
minVersion = 0;
maxVersion = maxSupportedVersion;
} else if (secureProtocol == "TLS_server_method") {
minVersion = 0;
maxVersion = maxSupportedVersion;
method = TLS_server_method();
} else if (secureProtocol == "TLS_client_method") {
minVersion = 0;
maxVersion = maxSupportedVersion;
method = TLS_client_method();
} else if (secureProtocol == "TLSv1_method") {
minVersion = TLS1_VERSION;
maxVersion = TLS1_VERSION;
} else if (secureProtocol == "TLSv1_server_method") {
minVersion = TLS1_VERSION;
maxVersion = TLS1_VERSION;
method = TLS_server_method();
} else if (secureProtocol == "TLSv1_client_method") {
minVersion = TLS1_VERSION;
maxVersion = TLS1_VERSION;
method = TLS_client_method();
} else if (secureProtocol == "TLSv1_1_method") {
minVersion = TLS1_1_VERSION;
maxVersion = TLS1_1_VERSION;
} else if (secureProtocol == "TLSv1_1_server_method") {
minVersion = TLS1_1_VERSION;
maxVersion = TLS1_1_VERSION;
method = TLS_server_method();
} else if (secureProtocol == "TLSv1_1_client_method") {
minVersion = TLS1_1_VERSION;
maxVersion = TLS1_1_VERSION;
method = TLS_client_method();
} else if (secureProtocol == "TLSv1_2_method") {
minVersion = TLS1_2_VERSION;
maxVersion = TLS1_2_VERSION;
} else if (secureProtocol == "TLSv1_2_server_method") {
minVersion = TLS1_2_VERSION;
maxVersion = TLS1_2_VERSION;
method = TLS_server_method();
} else if (secureProtocol == "TLSv1_2_client_method") {
minVersion = TLS1_2_VERSION;
maxVersion = TLS1_2_VERSION;
method = TLS_client_method();
} else {
throwException(globalObject, scope, createError(globalObject, ErrorCode::ERR_TLS_INVALID_PROTOCOL_METHOD, makeString("Unknown method: "_s, secureProtocol)));
return {};
}
}
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();
if (!context) {
return throwCryptoError(globalObject, scope, ERR_get_error(), "SSL_CTX_new");
}
SSL_CTX_set_app_data(context, thisObject);
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);
#endif
SSL_CTX_clear_mode(context, SSL_MODE_NO_AUTO_CHAIN);
SSL_CTX_set_session_cache_mode(context, SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL | SSL_SESS_CACHE_NO_AUTO_CLEAR);
RELEASE_ASSERT(SSL_CTX_set_min_proto_version(context, minVersion));
RELEASE_ASSERT(SSL_CTX_set_max_proto_version(context, maxVersion));
if (!ncrypto::CSPRNG(thisObject->m_ticketKeyName, sizeof(thisObject->m_ticketKeyName)) || !ncrypto::CSPRNG(thisObject->m_ticketKeyHMAC, sizeof(thisObject->m_ticketKeyHMAC)) || !ncrypto::CSPRNG(thisObject->m_ticketKeyAES, sizeof(thisObject->m_ticketKeyAES))) {
throwException(globalObject, scope, createError(globalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Error generating ticket keys"_s));
return {};
}
SSL_CTX_set_tlsext_ticket_key_cb(context, NodeTLSSecureContext::ticketCompatibilityCallback);
return JSC::encodedJSUndefined();
}
JSC_DEFINE_HOST_FUNCTION(secureContextSetCiphers, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto* thisObject = jsCast<NodeTLSSecureContext*>(callFrame->thisValue());
auto scope = DECLARE_THROW_SCOPE(vm);
ArgList args(callFrame);
JSValue ciphersValue = args.at(0);
if (!ciphersValue.isString()) {
return throwArgumentTypeError(*globalObject, scope, 0, "ciphers"_s, "SecureContext"_s, "setCiphers"_s, "string"_s);
}
CString ciphers = ciphersValue.toWTFString(globalObject).utf8();
if (!SSL_CTX_set_cipher_list(thisObject->context(), ciphers.data())) {
unsigned long err = ERR_get_error();
if (ciphers.length() == 0 && ERR_GET_REASON(err) == SSL_R_NO_CIPHER_MATCH) {
return JSC::encodedJSUndefined();
}
return throwCryptoError(globalObject, scope, err, "Failed to set ciphers");
}
return JSC::encodedJSUndefined();
}
JSC_DEFINE_HOST_FUNCTION(secureContextAddCACert, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto* thisObject = jsCast<NodeTLSSecureContext*>(callFrame->thisValue());
auto scope = DECLARE_THROW_SCOPE(vm);
ArgList args(callFrame);
JSValue certValue = args.at(0);
auto* arrayBufferView = JSC::jsDynamicCast<JSC::JSArrayBufferView*>(certValue);
CString cert;
if (certValue.isString()) {
cert = certValue.toWTFString(globalObject).utf8();
} else if (arrayBufferView != nullptr && !arrayBufferView->isDetached()) {
cert = arrayBufferView->span();
} else {
return throwArgumentTypeError(*globalObject, scope, 0, "cert"_s, "SecureContext"_s, "addCACert"_s, "string or ArrayBuffer"_s);
}
if (cert.length() > INT_MAX) {
return JSC::encodedJSUndefined();
}
ncrypto::BIOPointer bio = ncrypto::BIOPointer::NewSecMem();
if (!bio) {
return JSC::encodedJSUndefined();
}
int written = ncrypto::BIOPointer::Write(&bio, cert.span());
if (written < 0 || static_cast<size_t>(written) != cert.length()) {
return JSValue::encode(jsBoolean(false));
}
thisObject->setCACert(bio);
return JSValue::encode(jsBoolean(true));
}
JSC_DEFINE_HOST_FUNCTION(secureContextSetECDHCurve, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto* thisObject = jsCast<NodeTLSSecureContext*>(callFrame->thisValue());
auto scope = DECLARE_THROW_SCOPE(vm);
ArgList args(callFrame);
JSValue curveValue = args.at(0);
if (!curveValue.isString()) {
return throwArgumentTypeError(*globalObject, scope, 0, "curve"_s, "SecureContext"_s, "setECDHCurve"_s, "string"_s);
}
String curve = curveValue.toWTFString(globalObject);
if (curve != "auto" && !SSL_CTX_set1_curves_list(thisObject->context(), curve.utf8().data())) {
return throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to set ECDH curve");
}
return JSC::encodedJSUndefined();
}
JSC_DEFINE_HOST_FUNCTION(secureContextAddRootCerts, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto* thisObject = jsCast<NodeTLSSecureContext*>(callFrame->thisValue());
thisObject->setRootCerts();
return JSC::encodedJSUndefined();
}
JSC_DEFINE_HOST_FUNCTION(secureContextSetCert, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsCast<NodeTLSSecureContext*>(callFrame->thisValue());
ncrypto::BIOPointer bio = thisObject->loadBIO(globalObject, callFrame->argument(0));
thisObject->addCert(globalObject, scope, std::move(bio));
RETURN_IF_EXCEPTION(scope, {});
return JSC::encodedJSUndefined();
}
JSC_DEFINE_HOST_FUNCTION(secureContextSetKey, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsCast<NodeTLSSecureContext*>(callFrame->thisValue());
ncrypto::BIOPointer bio = thisObject->loadBIO(globalObject, callFrame->argument(0));
if (!bio) {
return JSC::encodedJSUndefined();
}
ncrypto::Buffer<const char> passphrase;
CString string;
if (callFrame->argument(1).isString()) {
string = callFrame->argument(1).toWTFString(globalObject).utf8();
passphrase = ncrypto::Buffer<const char>::from(string.span());
}
ncrypto::EVPKeyPointer key { PEM_read_bio_PrivateKey(bio.get(), nullptr, ncrypto::PasswordCallback, &passphrase) };
if (!key) {
return throwCryptoError(globalObject, scope, ERR_get_error(), "PEM_read_bio_PrivateKey");
}
if (!SSL_CTX_use_PrivateKey(thisObject->context(), key.get())) {
return throwCryptoError(globalObject, scope, ERR_get_error(), "SSL_CTX_use_PrivateKey");
}
return JSValue::encode(jsBoolean(true));
}
static const HashTableValue NodeTLSSecureContextPrototypeTableValues[] = {
{ "init"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, secureContextInit, 3 } },
{ "setCiphers"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, secureContextSetCiphers, 1 } },
{ "addCACert"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, secureContextAddCACert, 1 } },
{ "setECDHCurve"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, secureContextSetECDHCurve, 1 } },
{ "addRootCerts"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, secureContextAddRootCerts, 0 } },
{ "setCert"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, secureContextSetCert, 1 } },
{ "setKey"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, secureContextSetKey, 2 } },
};
static EncodedJSValue constructSecureContext(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newTarget = {})
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
ArgList args(callFrame);
NodeTLSSecureContext* secureContext = NodeTLSSecureContext::create(vm, globalObject, args);
return JSValue::encode(secureContext);
}
JSC_DEFINE_HOST_FUNCTION(secureContextConstructorCall, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return constructSecureContext(globalObject, callFrame);
}
JSC_DEFINE_HOST_FUNCTION(secureContextConstructorConstruct, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return constructSecureContext(globalObject, callFrame, callFrame->newTarget());
}
NodeTLSSecureContextConstructor* NodeTLSSecureContextConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* prototype)
{
NodeTLSSecureContextConstructor* ptr = new (NotNull, allocateCell<NodeTLSSecureContextConstructor>(vm)) NodeTLSSecureContextConstructor(vm, structure);
ptr->finishCreation(vm, prototype);
return ptr;
}
NodeTLSSecureContextConstructor::NodeTLSSecureContextConstructor(VM& vm, Structure* structure)
: NodeTLSSecureContextConstructor::Base(vm, structure, secureContextConstructorCall, secureContextConstructorConstruct)
{
}
void NodeTLSSecureContextConstructor::finishCreation(VM& vm, JSObject* prototype)
{
Base::finishCreation(vm, 1, "SecureContext"_s, PropertyAdditionMode::WithStructureTransition);
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
ASSERT(inherits(info()));
}
void NodeTLSSecureContextPrototype::finishCreation(VM& vm)
{
Base::finishCreation(vm);
ASSERT(inherits(info()));
reifyStaticProperties(vm, info(), NodeTLSSecureContextPrototypeTableValues, *this);
this->structure()->setMayBePrototype(true);
}
template<typename Visitor>
void NodeTLSSecureContext::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
auto* vmModule = jsCast<NodeTLSSecureContext*>(cell);
ASSERT_GC_OBJECT_INHERITS(vmModule, info());
Base::visitChildren(vmModule, visitor);
}
DEFINE_VISIT_CHILDREN(NodeTLSSecureContext);
const ClassInfo NodeTLSSecureContext::s_info = { "NodeTLSSecureContext"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeTLSSecureContext) };
const ClassInfo NodeTLSSecureContextPrototype::s_info = { "NodeTLSSecureContext"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeTLSSecureContextPrototype) };
const ClassInfo NodeTLSSecureContextConstructor::s_info = { "SecureContext"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeTLSSecureContextConstructor) };
extern "C" int Bun__NodeTLS__certCallbackDone(EncodedJSValue encoded_sni_context, SSL* ssl, JSGlobalObject* globalObject)
{
// Returns to certCallbackDone in socket.zig
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue sni_context_value = JSValue::decode(encoded_sni_context);
auto* sni_context = jsDynamicCast<NodeTLSSecureContext*>(sni_context_value);
if (!sni_context) {
if (sni_context_value.isObject()) {
return 0; // emit "Invalid SNI context" error
}
} else if (sni_context->applySNI(ssl) && !sni_context->setCACerts(ssl)) {
throwCryptoError(globalObject, scope, ERR_get_error(), "CertCbDone");
return 2; // threw
}
return 1; // all good
}
JSC_DEFINE_HOST_FUNCTION(getExtraCACertificates, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
@@ -72,4 +790,23 @@ JSC_DEFINE_HOST_FUNCTION(getExtraCACertificates, (JSC::JSGlobalObject * globalOb
return JSValue::encode(JSC::objectConstructorFreeze(globalObject, rootCertificates));
}
JSC_DEFINE_HOST_FUNCTION(getBundledRootCertificates, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
VM& vm = globalObject->vm();
struct us_cert_string_t* out;
auto size = us_raw_root_certs(&out);
if (size < 0) {
return JSValue::encode(jsUndefined());
}
auto rootCertificates = JSC::JSArray::create(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), size);
for (auto i = 0; i < size; i++) {
auto raw = out[i];
auto str = WTF::String::fromUTF8(std::span { raw.str, raw.len });
rootCertificates->putDirectIndex(globalObject, i, JSC::jsString(vm, str));
}
return JSValue::encode(JSC::objectConstructorFreeze(globalObject, rootCertificates));
}
} // namespace Bun

View File

@@ -1,8 +1,149 @@
#include "config.h"
#include "ZigGlobalObject.h"
#include "ncrypto.h"
#include "JavaScriptCore/WriteBarrier.h"
namespace Bun {
JSC::JSValue createNodeTLSBinding(Zig::GlobalObject*);
void configureNodeTLS(JSC::VM& vm, Zig::GlobalObject* globalObject);
class NodeTLSSecureContextPrototype final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
DECLARE_INFO;
static NodeTLSSecureContextPrototype* create(VM& vm, Structure* structure)
{
auto* prototype = new (NotNull, allocateCell<NodeTLSSecureContextPrototype>(vm)) NodeTLSSecureContextPrototype(vm, structure);
prototype->finishCreation(vm);
return prototype;
}
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
return &vm.plainObjectSpace();
}
static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
}
private:
NodeTLSSecureContextPrototype(VM& vm, Structure* structure)
: Base(vm, structure)
{
}
void finishCreation(VM& vm);
};
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeTLSSecureContextPrototype, NodeTLSSecureContextPrototype::Base);
class NodeTLSSecureContextConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
DECLARE_EXPORT_INFO;
static NodeTLSSecureContextConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSObject* prototype);
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, Base::StructureFlags), info());
}
private:
NodeTLSSecureContextConstructor(JSC::VM& vm, JSC::Structure* structure);
void finishCreation(JSC::VM&, JSC::JSObject* prototype);
};
class NodeTLSSecureContext final : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
static NodeTLSSecureContext* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, ArgList args);
template<typename, JSC::SubspaceAccess Mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (Mode == JSC::SubspaceAccess::Concurrently) {
return nullptr;
} else {
return WebCore::subspaceForImpl<NodeTLSSecureContext, WebCore::UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForNodeTLSSecureContext.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNodeTLSSecureContext = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForNodeTLSSecureContext.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForNodeTLSSecureContext = std::forward<decltype(space)>(space); });
}
}
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static JSObject* createPrototype(VM& vm, JSGlobalObject* globalObject)
{
return NodeTLSSecureContextPrototype::create(vm, NodeTLSSecureContextPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
}
static void destroy(JSC::JSCell* cell)
{
static_cast<NodeTLSSecureContext*>(cell)->NodeTLSSecureContext::~NodeTLSSecureContext();
}
DECLARE_EXPORT_INFO;
DECLARE_VISIT_CHILDREN;
SSL_CTX* context() { return m_context.get(); }
void context(SSL_CTX* ctx) { m_context = { ctx, SSL_CTX_free }; }
JSC::JSValue wrapper() const { return m_wrapper.get(); }
JSC::JSValue certCallback() const { return m_certCallback.get(); }
void setCACert(const ncrypto::BIOPointer& bio);
void setRootCerts();
bool applySNI(SSL* ssl);
int setCACerts(SSL* ssl);
ncrypto::BIOPointer loadBIO(JSGlobalObject*, JSValue);
bool addCert(JSGlobalObject* globalObject, ThrowScope& scope, ncrypto::BIOPointer);
private:
WriteBarrier<Unknown> m_wrapper;
WriteBarrier<Unknown> m_certCallback;
std::unique_ptr<SSL_CTX, decltype(&SSL_CTX_free)> m_context { nullptr, nullptr };
mutable std::unique_ptr<X509_STORE, decltype(&X509_STORE_free)> m_certStore { nullptr, nullptr };
ncrypto::X509Pointer m_cert;
ncrypto::X509Pointer m_issuer;
unsigned char m_ticketKeyName[16] {};
unsigned char m_ticketKeyAES[16] {};
unsigned char m_ticketKeyHMAC[16] {};
NodeTLSSecureContext(JSC::VM& vm, JSC::Structure* structure);
~NodeTLSSecureContext();
void finishCreation(JSC::VM& vm)
{
Base::finishCreation(vm);
ASSERT(inherits(info()));
}
void setX509StoreFlag(unsigned long flags);
X509_STORE* getCertStore() const;
static int ticketCompatibilityCallback(SSL* ssl, unsigned char* name, unsigned char* iv, EVP_CIPHER_CTX* ectx, HMAC_CTX* hctx, int enc);
friend EncodedJSValue secureContextInit(JSGlobalObject* globalObject, CallFrame* callFrame);
};
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeTLSSecureContextConstructor, NodeTLSSecureContextConstructor::Base);
BUN_DECLARE_HOST_FUNCTION(Bun__canonicalizeIP);
JSC_DECLARE_HOST_FUNCTION(getBundledRootCertificates);
JSC_DECLARE_HOST_FUNCTION(getExtraCACertificates);

View File

@@ -140,6 +140,7 @@
#include "napi.h"
#include "NodeHTTP.h"
#include "NodeVM.h"
#include "NodeTLS.h"
#include "Performance.h"
#include "ProcessBindingConstants.h"
#include "ProcessBindingTTYWrap.h"
@@ -3431,6 +3432,7 @@ void GlobalObject::finishCreation(VM& vm)
});
configureNodeVM(vm, this);
configureNodeTLS(vm, this);
#if ENABLE(REMOTE_INSPECTOR)
setInspectable(false);

View File

@@ -258,6 +258,10 @@ public:
JSC::JSObject* NodeVMSyntheticModule() const { return m_NodeVMSyntheticModuleClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue NodeVMSyntheticModulePrototype() const { return m_NodeVMSyntheticModuleClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* NodeTLSSecureContextStructure() const { return m_NodeTLSSecureContextClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* NodeTLSSecureContext() const { return m_NodeTLSSecureContextClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue NodeTLSSecureContextPrototype() const { return m_NodeTLSSecureContextClassStructure.prototypeInitializedOnMainThread(this); }
JSC::JSMap* readableStreamNativeMap() const { return m_lazyReadableStreamPrototypeMap.getInitializedOnMainThread(this); }
JSC::JSMap* requireMap() const { return m_requireMap.getInitializedOnMainThread(this); }
JSC::JSMap* esmRegistryMap() const { return m_esmRegistryMap.getInitializedOnMainThread(this); }
@@ -529,6 +533,7 @@ public:
V(public, LazyClassStructure, m_NodeVMScriptClassStructure) \
V(public, LazyClassStructure, m_NodeVMSourceTextModuleClassStructure) \
V(public, LazyClassStructure, m_NodeVMSyntheticModuleClassStructure) \
V(public, LazyClassStructure, m_NodeTLSSecureContextClassStructure) \
V(public, LazyClassStructure, m_JSX509CertificateClassStructure) \
V(public, LazyClassStructure, m_JSSignClassStructure) \
V(public, LazyClassStructure, m_JSVerifyClassStructure) \

View File

@@ -39,6 +39,7 @@ public:
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNodeVMScript;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNodeVMSourceTextModule;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNodeVMSyntheticModule;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNodeTLSSecureContext;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSCommonJSModule;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSCommonJSExtensions;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSMockImplementation;

View File

@@ -39,6 +39,7 @@ public:
std::unique_ptr<IsoSubspace> m_subspaceForNodeVMScript;
std::unique_ptr<IsoSubspace> m_subspaceForNodeVMSourceTextModule;
std::unique_ptr<IsoSubspace> m_subspaceForNodeVMSyntheticModule;
std::unique_ptr<IsoSubspace> m_subspaceForNodeTLSSecureContext;
std::unique_ptr<IsoSubspace> m_subspaceForJSCommonJSModule;
std::unique_ptr<IsoSubspace> m_subspaceForJSCommonJSExtensions;
std::unique_ptr<IsoSubspace> m_subspaceForJSMockImplementation;

View File

@@ -31,22 +31,22 @@ pub const us_socket_t = opaque {
}
pub fn pause(this: *us_socket_t, ssl: bool) void {
debug("us_socket_pause({d})", .{@intFromPtr(this)});
debug("us_socket_pause({*})", .{this});
c.us_socket_pause(@intFromBool(ssl), this);
}
pub fn @"resume"(this: *us_socket_t, ssl: bool) void {
debug("us_socket_resume({d})", .{@intFromPtr(this)});
debug("us_socket_resume({*})", .{this});
c.us_socket_resume(@intFromBool(ssl), this);
}
pub fn close(this: *us_socket_t, ssl: bool, code: CloseCode) void {
debug("us_socket_close({d}, {s})", .{ @intFromPtr(this), @tagName(code) });
debug("us_socket_close({*}, {s})", .{ this, @tagName(code) });
_ = c.us_socket_close(@intFromBool(ssl), this, code, null);
}
pub fn shutdown(this: *us_socket_t, ssl: bool) void {
debug("us_socket_shutdown({d})", .{@intFromPtr(this)});
debug("us_socket_shutdown({*})", .{this});
c.us_socket_shutdown(@intFromBool(ssl), this);
}

View File

@@ -172,7 +172,7 @@ function onConnectEnd() {
error.host = options.host;
error.port = options.port;
error.localAddress = options.localAddress;
this.destroy(error);
this.destroySoon(error);
}
}
@@ -361,10 +361,10 @@ const ServerHandlers: SocketHandler<NetSocket> = {
socket[kServerSocket] = self._handle;
const options = self[bunSocketServerOptions];
const { pauseOnConnect, connectionListener, [kSocketClass]: SClass, requestCert, rejectUnauthorized } = options;
const _socket = new SClass({}) 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);
@@ -2614,6 +2614,7 @@ function initSocketHandle(self) {
// Handle creation may be deferred to bind() or connect() time.
if (self._handle) {
self._handle[owner_symbol] = self;
self._configureHandle?.();
}
}

View File

@@ -5,10 +5,25 @@ const { Duplex } = require("node:stream");
const addServerName = $newZigFunction("Listener.zig", "jsAddServerName", 3);
const { throwNotImplemented } = require("internal/shared");
const { throwOnInvalidTLSArray, DEFAULT_CIPHERS, validateCiphers } = require("internal/tls");
const { validateString } = require("internal/validators");
const {
validateFunction,
validateObject,
validateString,
validateInt32,
validateBuffer,
} = require("internal/validators");
const { Server: NetServer, Socket: NetSocket } = net;
const {
SecureContext: NodeTLSSecureContext,
SSL_OP_CIPHER_SERVER_PREFERENCE,
TLS1_3_VERSION,
TLS1_2_VERSION,
TLS1_1_VERSION,
TLS1_VERSION,
} = $cpp("NodeTLS.cpp", "createNodeTLSBinding");
const getBundledRootCertificates = $newCppFunction("NodeTLS.cpp", "getBundledRootCertificates", 1);
const getExtraCACertificates = $newCppFunction("NodeTLS.cpp", "getExtraCACertificates", 1);
const canonicalizeIP = $newCppFunction("NodeTLS.cpp", "Bun__canonicalizeIP", 1);
@@ -17,6 +32,7 @@ const SymbolReplace = Symbol.replace;
const RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace];
const RegExpPrototypeExec = RegExp.prototype.exec;
const ObjectAssign = Object.assign;
const ObjectFreeze = Object.freeze;
const StringPrototypeStartsWith = String.prototype.startsWith;
const StringPrototypeSlice = String.prototype.slice;
@@ -34,8 +50,8 @@ const ArrayPrototypeForEach = Array.prototype.forEach;
const ArrayPrototypePush = Array.prototype.push;
const ArrayPrototypeSome = Array.prototype.some;
const ArrayPrototypeReduce = Array.prototype.reduce;
const ObjectFreeze = Object.freeze;
const ArrayPrototypeFilter = Array.prototype.filter;
const ArrayIsArray = Array.isArray;
function parseCertString() {
// Removed since JAN 2022 Node v18.0.0+ https://github.com/nodejs/node/pull/41479
@@ -149,8 +165,7 @@ function splitEscapedAltNames(altNames) {
}
function checkServerIdentity(hostname, cert) {
const subject = cert.subject;
const altNames = cert.subjectaltname;
const altNames = cert?.subjectaltname;
const dnsNames = [];
const ips = [];
@@ -176,7 +191,7 @@ function checkServerIdentity(hostname, cert) {
if (net.isIP(hostname)) {
valid = ArrayPrototypeIncludes.$call(ips, canonicalizeIP(hostname));
if (!valid) reason = `IP: ${hostname} is not in the cert's list: ` + ArrayPrototypeJoin.$call(ips, ", ");
} else if (dnsNames.length > 0 || subject?.CN) {
} else if (dnsNames.length > 0 || cert?.subject?.CN) {
const hostParts = splitHost(hostname);
const wildcard = pattern => check(hostParts, pattern, true);
@@ -185,7 +200,7 @@ function checkServerIdentity(hostname, cert) {
if (!valid) reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
} else {
// Match against Common Name only if no supported identifiers exist.
const cn = subject.CN;
const cn = cert?.subject?.CN;
if (Array.isArray(cn)) valid = ArrayPrototypeSome.$call(cn, wildcard);
else if (cn) valid = wildcard(cn);
@@ -210,42 +225,48 @@ var InternalSecureContext = class SecureContext {
secureOptions;
constructor(options) {
const context = {};
const { honorCipherOrder, minVersion, maxVersion } = options;
this.context = new NodeTLSSecureContext(options);
if (options) {
let cert = options.cert;
let { cert } = options;
if (cert) {
throwOnInvalidTLSArray("options.cert", cert);
this.cert = cert;
}
let key = options.key;
let { key } = options;
if (key) {
throwOnInvalidTLSArray("options.key", key);
this.key = key;
}
let ca = options.ca;
let { ca } = options;
if (ca) {
throwOnInvalidTLSArray("options.ca", ca);
this.ca = ca;
}
let passphrase = options.passphrase;
let { passphrase } = options;
if (passphrase && typeof passphrase !== "string") {
throw new TypeError("passphrase argument must be an string");
throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase);
}
this.passphrase = passphrase;
let servername = options.servername;
let { servername } = options;
if (servername && typeof servername !== "string") {
throw new TypeError("servername argument must be an string");
throw $ERR_INVALID_ARG_TYPE("options.servername", "string", servername);
}
this.servername = servername;
let secureOptions = options.secureOptions || 0;
let { secureOptions } = options;
if (secureOptions && typeof secureOptions !== "number") {
throw new TypeError("secureOptions argument must be an number");
throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions);
}
if (honorCipherOrder) {
secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE;
}
this.secureOptions = secureOptions;
@@ -266,7 +287,13 @@ var InternalSecureContext = class SecureContext {
}
}
this.context = context;
this.context.init(
options,
toV("minimum", minVersion, DEFAULT_MIN_VERSION),
toV("maximum", maxVersion, DEFAULT_MAX_VERSION),
);
configureSecureContext(this.context, options);
}
};
@@ -280,6 +307,265 @@ function createSecureContext(options) {
return new SecureContext(options);
}
function configureSecureContext(context, options) {
validateObject(options, "options");
const {
allowPartialTrustChain,
ca,
cert,
ciphers = require("internal/tls").DEFAULT_CIPHERS_LIST,
clientCertEngine,
crl,
dhparam,
ecdhCurve = DEFAULT_ECDH_CURVE,
key,
passphrase,
pfx,
privateKeyIdentifier,
privateKeyEngine,
sessionIdContext,
sessionTimeout,
sigalgs,
ticketKeys,
} = options;
if (ciphers !== undefined && ciphers !== null && !ArrayIsArray(ciphers)) {
validateString(ciphers, "options.ciphers");
}
const { cipherList, cipherSuites } = processCiphers(ciphers, "options.ciphers");
if (cipherSuites !== "") {
context.setCipherSuites(cipherSuites);
}
context.setCiphers(cipherList);
if (cipherList === "" && context.getMinProto() < TLS1_3_VERSION && context.getMaxProto() > TLS1_2_VERSION) {
context.setMinProto(TLS1_3_VERSION);
}
if (ca) {
addCACerts(context, ArrayIsArray(ca) ? ca : [ca], "options.ca");
} else {
context.addRootCerts();
}
if (allowPartialTrustChain) {
context.setAllowPartialTrustChain();
}
if (cert) {
setCerts(context, ArrayIsArray(cert) ? cert : [cert], "options.cert");
}
// Set the key after the cert.
// `ssl_set_pkey` returns `0` when the key does not match the cert, but
// `ssl_set_cert` returns `1` and nullifies the key in the SSL structure
// which leads to the crash later on.
if (key) {
if (ArrayIsArray(key)) {
for (let i = 0; i < key.length; ++i) {
const val = key[i];
const pem = val?.pem !== undefined ? val.pem : val;
const pass = val?.passphrase !== undefined ? val.passphrase : passphrase;
setKey(context, pem, pass, "options");
}
} else {
setKey(context, key, passphrase, "options");
}
}
if (sigalgs !== undefined && sigalgs !== null) {
validateString(sigalgs, "options.sigalgs");
if (sigalgs === "") {
throw $ERR_INVALID_ARG_VALUE("options.sigalgs", sigalgs);
}
context.setSigalgs(sigalgs);
}
if (privateKeyIdentifier !== undefined && privateKeyIdentifier !== null) {
if (privateKeyEngine === undefined || privateKeyEngine === null) {
// Engine is required when privateKeyIdentifier is present
throw $ERR_INVALID_ARG_VALUE("options.privateKeyEngine", privateKeyEngine);
}
if (key) {
// Both data key and engine key can't be set at the same time
throw $ERR_INVALID_ARG_VALUE("options.privateKeyIdentifier", privateKeyIdentifier);
}
if (typeof privateKeyIdentifier === "string" && typeof privateKeyEngine === "string") {
if (context.setEngineKey) {
context.setEngineKey(privateKeyIdentifier, privateKeyEngine);
} else {
throw $ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED("Custom engines not supported by this OpenSSL");
}
} else if (typeof privateKeyIdentifier !== "string") {
throw $ERR_INVALID_ARG_TYPE(
"options.privateKeyIdentifier",
["string", "null", "undefined"],
privateKeyIdentifier,
);
} else {
throw $ERR_INVALID_ARG_TYPE("options.privateKeyEngine", ["string", "null", "undefined"], privateKeyEngine);
}
}
validateString(ecdhCurve, "options.ecdhCurve");
context.setECDHCurve(ecdhCurve);
if (dhparam !== undefined && dhparam !== null) {
validateKeyOrCertOption("options.dhparam", dhparam);
const warning = context.setDHParam(dhparam === "auto" || dhparam);
if (warning) {
process.emitWarning(warning, "SecurityWarning");
}
}
if (crl !== undefined && crl !== null) {
if (ArrayIsArray(crl)) {
for (const val of crl) {
validateKeyOrCertOption("options.crl", val);
context.addCRL(val);
}
} else {
validateKeyOrCertOption("options.crl", crl);
context.addCRL(crl);
}
}
if (sessionIdContext !== undefined && sessionIdContext !== null) {
validateString(sessionIdContext, "options.sessionIdContext");
context.setSessionIdContext(sessionIdContext);
}
if (pfx !== undefined && pfx !== null) {
if (ArrayIsArray(pfx)) {
ArrayPrototypeForEach.$call(pfx, val => {
const raw = val.buf || val;
const pass = val.passphrase || passphrase;
if (pass !== undefined && pass !== null) {
context.loadPKCS12(toBuf(raw), toBuf(pass));
} else {
context.loadPKCS12(toBuf(raw));
}
});
} else if (passphrase) {
context.loadPKCS12(toBuf(pfx), toBuf(passphrase));
} else {
context.loadPKCS12(toBuf(pfx));
}
}
if (typeof clientCertEngine === "string") {
if (typeof context.setClientCertEngine !== "function") {
throw $ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED("Custom engines not supported by this OpenSSL");
} else {
context.setClientCertEngine(clientCertEngine);
}
} else if (clientCertEngine !== undefined && clientCertEngine !== null) {
throw $ERR_INVALID_ARG_TYPE("options.clientCertEngine", ["string", "null", "undefined"], clientCertEngine);
}
if (ticketKeys !== undefined && ticketKeys !== null) {
validateBuffer(ticketKeys, "options.ticketKeys");
if (ticketKeys.byteLength !== 48) {
throw $ERR_INVALID_ARG_VALUE("options.ticketKeys", ticketKeys.byteLength, "must be exactly 48 bytes");
}
context.setTicketKeys(ticketKeys);
}
if (sessionTimeout !== undefined && sessionTimeout !== null) {
validateInt32(sessionTimeout, "options.sessionTimeout", 0);
context.setSessionTimeout(sessionTimeout);
}
}
function toV(which, v, def) {
v ??= def;
if (v === "TLSv1") return TLS1_VERSION;
if (v === "TLSv1.1") return TLS1_1_VERSION;
if (v === "TLSv1.2") return TLS1_2_VERSION;
if (v === "TLSv1.3") return TLS1_3_VERSION;
throw $ERR_TLS_INVALID_PROTOCOL_VERSION(v, which);
}
function toBuf(val, encoding?: BufferEncoding | "buffer") {
if (typeof val === "string") {
if (encoding === "buffer") {
encoding = "utf8";
}
return Buffer.from(val, encoding);
}
return val;
}
function addCACerts(context, certs, name) {
ArrayPrototypeForEach.$call(certs, cert => {
validateKeyOrCertOption(name, cert);
context.addCACert(cert);
});
}
function setCerts(context, certs, name) {
ArrayPrototypeForEach.$call(certs, cert => {
validateKeyOrCertOption(name, cert);
context.setCert(cert);
});
}
function validateKeyOrCertOption(name, value) {
if (typeof value !== "string" && !isArrayBufferView(value)) {
throw $ERR_INVALID_ARG_TYPE(name, ["string", "Buffer", "TypedArray", "DataView"], value);
}
}
function setKey(context, key, passphrase, name) {
validateKeyOrCertOption(`${name}.key`, key);
if (passphrase !== undefined && passphrase !== null) {
validateString(passphrase, `${name}.passphrase`);
}
context.setKey(key, passphrase);
}
function processCiphers(ciphers, name) {
if (typeof ciphers === "string" || !ciphers) {
ciphers = StringPrototypeSplit.$call(ciphers || require("internal/tls").DEFAULT_CIPHERS, ":");
}
const cipherList = ArrayPrototypeJoin.$call(
ArrayPrototypeFilter.$call(ciphers, cipher => {
if (cipher.length === 0) return false;
if (StringPrototypeStartsWith.$call(cipher, "TLS_")) return false;
if (StringPrototypeStartsWith.$call(cipher, "!TLS_")) return false;
return true;
}),
":",
);
const cipherSuites = ArrayPrototypeJoin.$call(
ArrayPrototypeFilter.$call(ciphers, cipher => {
if (cipher.length === 0) return false;
if (StringPrototypeStartsWith.$call(cipher, "TLS_")) return true;
if (StringPrototypeStartsWith.$call(cipher, "!TLS_")) return true;
return false;
}),
":",
);
// Specifying empty cipher suites for both TLS1.2 and TLS1.3 is invalid, its
// not possible to handshake with no suites.
if (cipherSuites === "" && cipherList === "") {
throw $ERR_INVALID_ARG_VALUE(name, ciphers);
}
return { cipherList, cipherSuites };
}
// Translate some fields from the handle's C-friendly format into more idiomatic
// javascript object representations before passing them back to the user. Can
// be used on any cert object, but changing the name would be semver-major.
@@ -306,7 +592,7 @@ function TLSSocket(socket?, options?) {
this._newSessionPending = undefined;
this._controlReleased = undefined;
this.secureConnecting = false;
this._SNICallback = undefined;
this._SNICallback = null;
this.servername = undefined;
this.authorized = false;
this.authorizationError;
@@ -325,17 +611,31 @@ function TLSSocket(socket?, options?) {
}
if (typeof options === "object") {
const { ALPNProtocols } = options;
const { ALPNProtocols, SNICallback: sni, rejectUnauthorized, requestCert } = options;
if (ALPNProtocols) {
convertALPNProtocols(ALPNProtocols, this);
}
if (sni) {
validateFunction(sni, "options.SNICallback");
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;
@@ -343,6 +643,16 @@ function TLSSocket(socket?, options?) {
this._securePending = true;
this[kcheckServerIdentity] = options.checkServerIdentity || checkServerIdentity;
this[ksession] = options.session || null;
this.once("connect", socket => {
if (socket) {
socket._handle?.setVerifyMode(!!socket._requestCert || !socket.isServer, !!socket._rejectUnauthorized);
if (socket.isServer) {
socket._handle.certCallback = loadSNI.bind(socket);
socket._handle.enableCertCallback();
}
}
});
}
$toClass(TLSSocket, "TLSSocket", NetSocket);
@@ -351,6 +661,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?.();
};
@@ -518,6 +834,9 @@ function Server(options, secureConnectionListener): void {
this._requestCert = undefined;
this.servername = undefined;
this.ALPNProtocols = undefined;
this._SNICallback = SNICallback;
this.server = this;
this._contexts = [];
let contexts: Map<string, typeof InternalSecureContext> | null = null;
@@ -533,6 +852,7 @@ function Server(options, secureConnectionListener): void {
} else {
if (!contexts) contexts = new Map();
contexts.set(hostname, context);
this._contexts.push(context);
}
};
@@ -565,6 +885,12 @@ function Server(options, secureConnectionListener): void {
this.ca = ca;
}
let sniCallback = options.SNICallback;
if (sniCallback) {
validateFunction(sniCallback, "options.SNICallback");
this._SNICallback = sniCallback;
}
let passphrase = options.passphrase;
if (passphrase && typeof passphrase !== "string") {
throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase);
@@ -626,6 +952,7 @@ function Server(options, secureConnectionListener): void {
rejectUnauthorized: this._rejectUnauthorized,
requestCert: isClient ? true : this._requestCert,
ALPNProtocols: this.ALPNProtocols,
SNICallback: this._SNICallback,
clientRenegotiationLimit: CLIENT_RENEG_LIMIT,
clientRenegotiationWindow: CLIENT_RENEG_WINDOW,
contexts: contexts,
@@ -730,6 +1057,52 @@ function convertALPNProtocols(protocols, out) {
}
}
function SNICallback(servername, callback) {
const contexts = this.server._contexts;
for (let i = contexts.length - 1; i >= 0; --i) {
const elem = contexts[i];
if (elem[0].test(servername)) {
callback(null, elem[1]);
return;
}
}
callback(null, undefined);
}
function loadSNI(servername) {
if (!servername || !this._SNICallback) {
return this._handle.certCallbackDone();
}
let once = false;
this._SNICallback(servername, (err, context) => {
if (once) {
this.destroySoon($ERR_MULTIPLE_CALLBACK());
return this;
}
once = true;
if (err) {
this.destroySoon(err);
return this;
}
if (this._handle === null) {
this.destroySoon($ERR_SOCKET_CLOSED());
return this;
}
if (context) {
this._handle.sni_context = context.context || context;
}
return this._handle.certCallbackDone();
});
}
let bundledRootCertificates: string[] | undefined;
function cacheBundledRootCertificates(): string[] {
bundledRootCertificates ||= getBundledRootCertificates() as string[];

View File

@@ -0,0 +1,15 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
{
assert.throws(
() => { tls.createSecureContext({ clientCertEngine: 0 }); },
{ code: 'ERR_INVALID_ARG_TYPE',
message: / Received type number \(0\)/ });
}

View File

@@ -0,0 +1,58 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
assert.throws(function() {
tls.createSecureContext({ secureProtocol: 'blargh' });
}, {
code: 'ERR_TLS_INVALID_PROTOCOL_METHOD',
message: 'Unknown method: blargh',
});
const errMessageSSLv2 = /SSLv2 methods disabled/;
assert.throws(function() {
tls.createSecureContext({ secureProtocol: 'SSLv2_method' });
}, errMessageSSLv2);
assert.throws(function() {
tls.createSecureContext({ secureProtocol: 'SSLv2_client_method' });
}, errMessageSSLv2);
assert.throws(function() {
tls.createSecureContext({ secureProtocol: 'SSLv2_server_method' });
}, errMessageSSLv2);
const errMessageSSLv3 = /SSLv3 methods disabled/;
assert.throws(function() {
tls.createSecureContext({ secureProtocol: 'SSLv3_method' });
}, errMessageSSLv3);
assert.throws(function() {
tls.createSecureContext({ secureProtocol: 'SSLv3_client_method' });
}, errMessageSSLv3);
assert.throws(function() {
tls.createSecureContext({ secureProtocol: 'SSLv3_server_method' });
}, errMessageSSLv3);
// Note that SSLv2 and SSLv3 are disallowed but SSLv2_method and friends are
// still accepted. They are OpenSSL's way of saying that all known protocols
// are supported unless explicitly disabled (which we do for SSLv2 and SSLv3.)
tls.createSecureContext({ secureProtocol: 'SSLv23_method' });
tls.createSecureContext({ secureProtocol: 'SSLv23_client_method' });
tls.createSecureContext({ secureProtocol: 'SSLv23_server_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_client_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_server_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_1_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_1_client_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_1_server_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_2_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_2_client_method' });
tls.createSecureContext({ secureProtocol: 'TLSv1_2_server_method' });

View File

@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
const util = require('util');
const fixtures = require('../common/fixtures');
const sent = 'hello world';
const serverOptions = {
isServer: true,
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem')
};
let ssl = null;
process.on('exit', function() {
assert.ok(ssl !== null);
// If the internal pointer to stream_ isn't cleared properly then this
// will abort.
util.inspect(ssl);
});
const server = tls.createServer(serverOptions, function(s) {
s.on('data', function() { });
s.on('end', function() {
server.close();
s.destroy();
});
}).listen(0, function() {
const c = new tls.TLSSocket();
ssl = c.ssl;
c.connect(this.address().port, function() {
c.end(sent);
});
});

View File

@@ -0,0 +1,56 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
// We could get the `tlsSocket.servername` even if the event of "tlsClientError"
// is emitted.
const serverOptions = {
requestCert: true,
rejectUnauthorized: false,
SNICallback: function(servername, callback) {
if (servername === 'c.another.com') {
callback(null, {});
} else {
callback(new Error('Invalid SNI context'), null);
}
}
};
function test(options) {
const server = tls.createServer(serverOptions, common.mustNotCall());
server.on('tlsClientError', common.mustCall((err, socket) => {
assert.strictEqual(err.message, 'Invalid SNI context');
// The `servername` should match.
assert.strictEqual(socket.servername, options.servername);
}));
server.listen(0, () => {
options.port = server.address().port;
const client = tls.connect(options, common.mustNotCall());
client.on('error', common.mustCall((err) => {
assert.strictEqual(err.message, 'Client network socket' +
' disconnected before secure TLS connection was established');
}));
client.on('close', common.mustCall(() => server.close()));
});
}
test({
port: undefined,
servername: 'c.another.com',
rejectUnauthorized: false
});
test({
port: undefined,
servername: 'c.wrong.com',
rejectUnauthorized: false
});

View File

@@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const net = require('net');
const tls = require('tls');
for (const SNICallback of ['fhqwhgads', 42, {}, []]) {
assert.throws(() => {
tls.createServer({ SNICallback });
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
});
assert.throws(() => {
new tls.TLSSocket(new net.Socket(), { isServer: true, SNICallback });
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
});
}