mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
22 Commits
jarred/try
...
kai/tls-sn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8496625917 | ||
|
|
5717a2beba | ||
|
|
6c9643d077 | ||
|
|
101f0e973c | ||
|
|
7f66114af0 | ||
|
|
43e9a89743 | ||
|
|
47594eeb60 | ||
|
|
8e2c440d14 | ||
|
|
9aecb95c88 | ||
|
|
09818db8bb | ||
|
|
3ad73252f6 | ||
|
|
2672264096 | ||
|
|
af5c0bdea6 | ||
|
|
1ec0718e05 | ||
|
|
1bc25d3150 | ||
|
|
b0a7c945eb | ||
|
|
12da7944ad | ||
|
|
2cda3c9314 | ||
|
|
c5b32ef56d | ||
|
|
3f620e314c | ||
|
|
939fa03d8c | ||
|
|
28c870c5dc |
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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\)/ });
|
||||
}
|
||||
58
test/js/node/test/parallel/test-tls-no-sslv23.js
Normal file
58
test/js/node/test/parallel/test-tls-no-sslv23.js
Normal 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' });
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
56
test/js/node/test/parallel/test-tls-sni-servername.js
Normal file
56
test/js/node/test/parallel/test-tls-sni-servername.js
Normal 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
|
||||
});
|
||||
24
test/js/node/test/parallel/test-tls-snicallback-error.js
Normal file
24
test/js/node/test/parallel/test-tls-snicallback-error.js
Normal 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',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user