diff --git a/.gitignore b/.gitignore index 0ee2a8534bc245..6e2447569461b5 100644 --- a/.gitignore +++ b/.gitignore @@ -183,4 +183,7 @@ codegen-for-zig-team.tar.gz *.sock scratch*.{js,ts,tsx,cjs,mjs} -*.bun-build \ No newline at end of file +*.bun-build +**/.claude/settings.local.json + +/.tmp diff --git a/.vscode/settings.json b/.vscode/settings.json index 167a601132bbf6..c9552f4cd7bd7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,7 @@ "zig.zls.path": "${workspaceFolder}/vendor/zig/zls.exe", "zig.formattingProvider": "zls", "zig.zls.enableInlayHints": false, + "[zig]": { "editor.tabSize": 4, "editor.useTabStops": false, diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index 261ed849bd41be..9a96d33cc1563f 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -408,8 +408,8 @@ src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp -src/bun.js/bindings/webcrypto/CryptoAlgorithmX25519.cpp src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp +src/bun.js/bindings/webcrypto/CryptoAlgorithmX25519.cpp src/bun.js/bindings/webcrypto/CryptoDigest.cpp src/bun.js/bindings/webcrypto/CryptoKey.cpp src/bun.js/bindings/webcrypto/CryptoKeyAES.cpp @@ -443,7 +443,6 @@ src/bun.js/bindings/webcrypto/JSHkdfParams.cpp src/bun.js/bindings/webcrypto/JSHmacKeyParams.cpp src/bun.js/bindings/webcrypto/JSJsonWebKey.cpp src/bun.js/bindings/webcrypto/JSPbkdf2Params.cpp -src/bun.js/bindings/webcrypto/JSX25519Params.cpp src/bun.js/bindings/webcrypto/JSRsaHashedImportParams.cpp src/bun.js/bindings/webcrypto/JSRsaHashedKeyGenParams.cpp src/bun.js/bindings/webcrypto/JSRsaKeyGenParams.cpp @@ -451,6 +450,7 @@ src/bun.js/bindings/webcrypto/JSRsaOaepParams.cpp src/bun.js/bindings/webcrypto/JSRsaOtherPrimesInfo.cpp src/bun.js/bindings/webcrypto/JSRsaPssParams.cpp src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp +src/bun.js/bindings/webcrypto/JSX25519Params.cpp src/bun.js/bindings/webcrypto/OpenSSLUtilities.cpp src/bun.js/bindings/webcrypto/PhonyWorkQueue.cpp src/bun.js/bindings/webcrypto/SerializedCryptoKeyWrapOpenSSL.cpp diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 0aff5f740fbe83..53f77a6d24971a 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -164,6 +164,7 @@ src/bun.js/node/node_http_binding.zig src/bun.js/node/node_net_binding.zig src/bun.js/node/node_os.zig src/bun.js/node/node_process.zig +src/bun.js/node/node_tls_binding.zig src/bun.js/node/node_util_binding.zig src/bun.js/node/node_zlib_binding.zig src/bun.js/node/nodejs_error_code.zig @@ -581,6 +582,7 @@ src/system_timer.zig src/test/fixtures.zig src/test/recover.zig src/thread_pool.zig +src/tls.zig src/tmp.zig src/toml/toml_lexer.zig src/toml/toml_parser.zig diff --git a/packages/bun-types/overrides.d.ts b/packages/bun-types/overrides.d.ts index 27e4f9700bdc55..cd6c6e35bc49c9 100644 --- a/packages/bun-types/overrides.d.ts +++ b/packages/bun-types/overrides.d.ts @@ -2,7 +2,7 @@ export {}; declare global { namespace NodeJS { - interface ProcessEnv extends Bun.Env, ImportMetaEnv {} + interface ProcessEnv extends Bun.Env {} interface Process { readonly version: string; diff --git a/packages/bun-usockets/src/crypto/openssl.c b/packages/bun-usockets/src/crypto/openssl.c index 4649f743ba2b71..1119d5ba7df3d2 100644 --- a/packages/bun-usockets/src/crypto/openssl.c +++ b/packages/bun-usockets/src/crypto/openssl.c @@ -66,6 +66,7 @@ struct loop_ssl_data { struct us_internal_ssl_socket_context_t { struct us_socket_context_t sc; + struct us_bun_socket_context_options_t options; // this thing can be shared with other socket contexts via socket transfer! // maybe instead of holding once you hold many, a vector or set @@ -285,7 +286,6 @@ int us_internal_handle_shutdown(struct us_internal_ssl_socket_t *s, int force_fa // we got some error here, but we dont care about it, we are closing the socket int err = SSL_get_error(s->ssl, ret); if (err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL) { - // clear ERR_clear_error(); s->fatal_error = 1; // Fatal error occurred, we should close the socket imeadiatly @@ -326,6 +326,41 @@ int us_internal_ssl_socket_is_closed(struct us_internal_ssl_socket_t *s) { return us_socket_is_closed(0, &s->s); } +/** + * Override the protocol error if the secure_protocol_method is set. This is to match Node's + * behaviour + * Will modify the verify_error struct to override the error code and reason if necessary. + + * Returns 1 if the protocol error was overridden, 0 otherwise +*/ +static int should_override_protocol_error(const char *proto, int is_server, int openssl_reason, struct us_bun_verify_error_t *verify_error) { + if (!proto) return 0; + if (is_server) { + if (strcmp(proto, "TLSv1_method") == 0 || strcmp(proto, "TLSv1_1_method") == 0) { + if (openssl_reason == SSL_R_WRONG_VERSION_NUMBER) { + verify_error->code = "ERR_SSL_WRONG_VERSION_NUMBER"; + verify_error->reason = "Wrong version number on server"; + } else if (openssl_reason == SSL_R_UNSUPPORTED_PROTOCOL) { + verify_error->code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + verify_error->reason = "Unsupported protocol on server"; + } + + verify_error->error = -1; + ERR_clear_error(); + return 1; + } + } else { + if (strcmp(proto, "SSLv23_method") == 0) { + verify_error->code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + verify_error->reason = "Unsupported protocol"; + verify_error->error = -1; + ERR_clear_error(); + return 1; + } + } + + return 0; +} void us_internal_trigger_handshake_callback_econnreset(struct us_internal_ssl_socket_t *s) { struct us_internal_ssl_socket_context_t *context = @@ -348,6 +383,72 @@ void us_internal_trigger_handshake_callback(struct us_internal_ssl_socket_t *s, if (context->on_handshake != NULL) { struct us_bun_verify_error_t verify_error = us_internal_verify_error(s); + + if (!success && (verify_error.code == NULL || verify_error.code[0] == 0)) { + const char *proto = context->options.secure_protocol_method; + unsigned long err = ERR_peek_error(); + int reason = ERR_GET_REASON(err); + if (should_override_protocol_error(proto, SSL_is_server(s->ssl), reason, &verify_error)) { + context->on_handshake(s, success, verify_error, context->handshake_data); + return; + } + + if (context->options.secure_protocol_method) { + if (SSL_is_server(s->ssl)) { + unsigned long err = ERR_peek_error(); + int reason = ERR_GET_REASON(err); + if ((strcmp(proto, "TLSv1_1_method") == 0 || strcmp(proto, "TLSv1_method") == 0)) { + if (reason == SSL_R_WRONG_VERSION_NUMBER) { + verify_error.code = "ERR_SSL_WRONG_VERSION_NUMBER"; + verify_error.reason = "Wrong version number on server"; + verify_error.error = -1; + ERR_clear_error(); + context->on_handshake(s, success, verify_error, context->handshake_data); + return; + } else if (reason == SSL_R_UNSUPPORTED_PROTOCOL) { + verify_error.code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + verify_error.reason = "Unsupported protocol on server"; + verify_error.error = -1; + ERR_clear_error(); + context->on_handshake(s, success, verify_error, context->handshake_data); + return; + } + } + } else { + if (strcmp(proto, "SSLv23_method") == 0) { + verify_error.code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + verify_error.reason = "Unsupported protocol"; + verify_error.error = -1; + ERR_clear_error(); + context->on_handshake(s, success, verify_error, context->handshake_data); + return; + } + } + } + + if (verify_error.error == 0) { + verify_error.error = -1; + + unsigned long err = ERR_peek_error(); + int reason = ERR_GET_REASON(err); + + if (reason == SSL_R_TLSV1_ALERT_PROTOCOL_VERSION) { + verify_error.code = "ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION"; + verify_error.reason = "TLSv1 alert protocol version"; + } else if (reason == SSL_R_UNSUPPORTED_PROTOCOL) { + verify_error.code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + verify_error.reason = SSL_is_server(s->ssl) ? "Unsupported protocol on server" : "Unsupported protocol on client"; + } else if (reason == SSL_R_WRONG_VERSION_NUMBER) { + verify_error.code = "ERR_SSL_WRONG_VERSION_NUMBER"; + verify_error.reason = "Wrong version number on server"; + } else { + verify_error.code = "ERR_SSL_UNSUPPORTED_PROTOCOL"; + verify_error.reason = "Unsupported protocol"; + } + ERR_clear_error(); + } + } + context->on_handshake(s, success, verify_error, context->handshake_data); } } @@ -400,8 +501,7 @@ void us_internal_update_handshake(struct us_internal_ssl_socket_t *s) { if (us_internal_ssl_socket_is_closed(s) || us_internal_ssl_socket_is_shut_down(s) || (s->ssl && SSL_get_shutdown(s->ssl) & SSL_RECEIVED_SHUTDOWN)) { - - us_internal_trigger_handshake_callback(s, 0); + us_internal_trigger_handshake_callback_econnreset(s); return; } @@ -507,6 +607,7 @@ struct us_internal_ssl_socket_t *ssl_on_data(struct us_internal_ssl_socket_t *s, if (just_read <= 0) { int err = SSL_get_error(s->ssl, just_read); + // as far as I know these are the only errors we want to handle if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { if (err == SSL_ERROR_WANT_RENEGOTIATE) { @@ -540,12 +641,11 @@ struct us_internal_ssl_socket_t *ssl_on_data(struct us_internal_ssl_socket_t *s, } if (err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL) { - // clear per thread error queue if it may contain something - ERR_clear_error(); s->fatal_error = 1; + us_internal_trigger_handshake_callback(s, 0); } - // terminate connection here + // Terminate connection after reporting the handshake error us_internal_ssl_socket_close(s, 0, NULL); return NULL; // stop processing data } else { @@ -1140,14 +1240,31 @@ SSL_CTX *create_ssl_context_from_bun_options( /* Create the context */ SSL_CTX *ssl_context = SSL_CTX_new(TLS_method()); + /* Default options we rely on - changing these will break our logic */ SSL_CTX_set_read_ahead(ssl_context, 1); /* we should always accept moving write buffer so we can retry writes with a * buffer allocated in a different address */ SSL_CTX_set_mode(ssl_context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + if (options.min_tls_version > 0) { + if (!SSL_CTX_set_min_proto_version(ssl_context, options.min_tls_version)) { + free_ssl_context(ssl_context); + return NULL; + } + } else { + + if (!SSL_CTX_set_min_proto_version(ssl_context, TLS1_2_VERSION)) { + free_ssl_context(ssl_context); + return NULL; + } + } - /* Anything below TLS 1.2 is disabled */ - SSL_CTX_set_min_proto_version(ssl_context, TLS1_2_VERSION); + if (options.max_tls_version > 0) { + if (!SSL_CTX_set_max_proto_version(ssl_context, options.max_tls_version)) { + free_ssl_context(ssl_context); + return NULL; + } + } /* The following are helpers. You may easily implement whatever you want by * using the native handle directly */ @@ -1545,6 +1662,7 @@ us_internal_bun_create_ssl_socket_context( context->ssl_context = ssl_context; // create_ssl_context_from_options(options); context->is_parent = 1; + context->options = options; context->on_handshake = NULL; context->handshake_data = NULL; diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index 6128d855f1dbd4..77f2c946015f9c 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -237,6 +237,9 @@ struct us_bun_socket_context_options_t { int request_cert; unsigned int client_renegotiation_limit; unsigned int client_renegotiation_window; + unsigned int min_tls_version; + unsigned int max_tls_version; + const char *secure_protocol_method; }; /* Return 15-bit timestamp for this context */ diff --git a/packages/bun-uws/src/App.h b/packages/bun-uws/src/App.h index 8d246636c383f2..c03211f178d313 100644 --- a/packages/bun-uws/src/App.h +++ b/packages/bun-uws/src/App.h @@ -78,6 +78,9 @@ namespace uWS { int request_cert = 0; unsigned int client_renegotiation_limit = 3; unsigned int client_renegotiation_window = 600; + unsigned int min_tls_version = 0; + unsigned int max_tls_version = 0; + const char **secure_protocol_method = nullptr; /* Conversion operator used internally */ operator struct us_bun_socket_context_options_t() const { diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 8f2003ee4a6e79..053fb06ee7291b 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -715,6 +715,12 @@ pub const Listener = struct { return globalObject.throwValue(err); }; + if (ssl_enabled and create_err != .none) { + const js_err = create_err.toJS(globalObject); + uws.us_socket_context_free(1, socket_context); + return globalObject.throwValue(js_err); + } + if (ssl_enabled) { if (ssl.?.protos) |p| { protos = p[0..ssl.?.protos_len]; @@ -1217,6 +1223,12 @@ pub const Listener = struct { return globalObject.throwValue(err.toErrorInstance(globalObject)); }; + if (ssl_enabled and create_err != .none) { + const js_err = create_err.toJS(globalObject); + uws.us_socket_context_free(1, socket_context); + return globalObject.throwValue(js_err); + } + if (ssl_enabled) { if (ssl.?.protos) |p| { protos = p[0..ssl.?.protos_len]; diff --git a/src/bun.js/api/bun/ssl_wrapper.zig b/src/bun.js/api/bun/ssl_wrapper.zig index c1f561ce2aef96..835d6a046ea1c4 100644 --- a/src/bun.js/api/bun/ssl_wrapper.zig +++ b/src/bun.js/api/bun/ssl_wrapper.zig @@ -282,6 +282,7 @@ pub fn SSLWrapper(comptime T: type) type { if (this.flags.closed_notified) return; this.flags.authorized = success; + // trigger the handshake callback this.handlers.onHandshake(this.handlers.ctx, success, result); } @@ -311,8 +312,25 @@ pub fn SSLWrapper(comptime T: type) type { if (this.isShutdown()) { return .{}; } + const ssl = this.ssl orelse return .{}; - return uws.us_ssl_socket_verify_error_from_ssl(ssl); + + const peek_err = BoringSSL.ERR_peek_error(); + var verr = uws.us_ssl_socket_verify_error_from_ssl(ssl); + + // no certificate verification = handshake error + if (verr.code == null and peek_err != 0) { + const reason_ptr = BoringSSL.ERR_reason_error_string(peek_err); + + verr = uws.us_bun_verify_error_t{ + .error_no = @intCast(peek_err), + .code = reason_ptr, + .reason = reason_ptr, + }; + BoringSSL.ERR_clear_error(); + } + + return verr; } /// Update the handshake state diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index c6fa18d59efccc..76f3e20d19dcc5 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -693,6 +693,10 @@ pub const ServerConfig = struct { client_renegotiation_limit: u32 = 0, client_renegotiation_window: u32 = 0, + min_version: ?u16 = null, + max_version: ?u16 = null, + secure_protocol_method: ?[*:0]const u8 = null, + const log = Output.scoped(.SSLConfig, false); pub fn asUSockets(this: SSLConfig) uws.us_bun_socket_context_options_t { @@ -729,6 +733,18 @@ pub const ServerConfig = struct { ctx_opts.request_cert = this.request_cert; ctx_opts.reject_unauthorized = this.reject_unauthorized; + if (this.min_version) |version| { + ctx_opts.min_tls_version = version; + } + + if (this.max_version) |version| { + ctx_opts.max_tls_version = version; + } + + if (this.secure_protocol_method != null) { + ctx_opts.secure_protocol_method = this.secure_protocol_method; + } + return ctx_opts; } @@ -743,6 +759,7 @@ pub const ServerConfig = struct { "passphrase", "ssl_ciphers", "protos", + "secure_protocol_method", }; inline for (fields) |field| { @@ -759,7 +776,7 @@ pub const ServerConfig = struct { { //numbers - const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" }; + const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode", "min_version", "max_version" }; inline for (fields) |field| { const lhs = @field(thisConfig, field); @@ -1084,6 +1101,26 @@ pub const ServerConfig = struct { any = true; } + if (try obj.getTruthy(global, "minVersion")) |min_version| { + result.min_version = @as(u16, @intCast(min_version.toInt32())); + any = true; + } + + if (try obj.getTruthy(global, "maxVersion")) |max_version| { + result.max_version = @as(u16, @intCast(max_version.toInt32())); + any = true; + } + + if (try obj.getTruthy(global, "secureProtocol")) |proto| { + var sliced = try proto.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.secure_protocol_method = try bun.default_allocator.dupeZ(u8, sliced.slice()); + any = true; + result.requires_custom_request_ctx = true; + } + } + if (try obj.getTruthy(global, "ciphers")) |ssl_ciphers| { var sliced = try ssl_ciphers.toSlice(global, bun.default_allocator); defer sliced.deinit(); diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 021cb0e4cbc822..e96a35c9756820 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -241,6 +241,7 @@ const errors: ErrorCodeMapping = [ ["ERR_TLS_PSK_SET_IDENTITY_HINT_FAILED", Error], ["ERR_TLS_RENEGOTIATION_DISABLED", Error], ["ERR_TLS_SNI_FROM_SERVER", Error], + ["ERR_SSL_UNSUPPORTED_PROTOCOL", Error], ["ERR_UNAVAILABLE_DURING_EXIT", Error], ["ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET", Error], ["ERR_UNESCAPED_CHARACTERS", TypeError], diff --git a/src/bun.js/node/node_tls_binding.zig b/src/bun.js/node/node_tls_binding.zig new file mode 100644 index 00000000000000..01fa93a37c063c --- /dev/null +++ b/src/bun.js/node/node_tls_binding.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +const bun = @import("bun"); +const JSC = bun.JSC; + +pub fn getDefaultMinTLSVersionFromCLIFlag(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { + if (bun.tls.min_tls_version_from_cli_flag) |version| { + return JSC.JSValue.jsNumber(version); + } + + return JSC.JSValue.jsNull(); +} + +pub fn getDefaultMaxTLSVersionFromCLIFlag(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { + if (bun.tls.max_tls_version_from_cli_flag) |version| { + return JSC.JSValue.jsNumber(version); + } + + return JSC.JSValue.jsNull(); +} diff --git a/src/bun.zig b/src/bun.zig index 761ee0bba422ef..c54bcce1979802 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -599,6 +599,7 @@ pub const Bunfig = @import("./bunfig.zig").Bunfig; pub const HTTPThread = @import("./http.zig").HTTPThread; pub const http = @import("./http.zig"); +pub const tls = @import("./tls.zig"); pub const Analytics = @import("./analytics/analytics_thread.zig"); diff --git a/src/cli.zig b/src/cli.zig index e05b30b49157cd..125ab52ba3c9c1 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -247,6 +247,12 @@ pub const Arguments = struct { clap.parseParam("--zero-fill-buffers Boolean to force Buffer.allocUnsafe(size) to be zero-filled.") catch unreachable, clap.parseParam("--redis-preconnect Preconnect to $REDIS_URL at startup") catch unreachable, clap.parseParam("--no-addons Throw an error if process.dlopen is called, and disable export condition \"node-addons\"") catch unreachable, + clap.parseParam("--tls-max-v1.2 Set the maximum TLS version to 1.2") catch unreachable, + clap.parseParam("--tls-max-v1.3 Set the maximum TLS version to 1.3") catch unreachable, + clap.parseParam("--tls-min-v1.0 Set the minimum TLS version to 1") catch unreachable, + clap.parseParam("--tls-min-v1.1 Set the minimum TLS version to 1.1") catch unreachable, + clap.parseParam("--tls-min-v1.2 Set the minimum TLS version to 1.2") catch unreachable, + clap.parseParam("--tls-min-v1.3 Set the minimum TLS version to 1.3") catch unreachable, }; const auto_or_run_params = [_]ParamType{ @@ -754,6 +760,27 @@ pub const Arguments = struct { } } + // TLS version flags here are specified in ascending order for MAX, and descending order for MIN + // because Node will use the maximum value for --tls-max and the minimum value for --tls-min + // See comments on: + // - https://bun.sh/reference/node/tls/DEFAULT_MAX_VERSION + // - https://bun.sh/reference/node/tls/DEFAULT_MIN_VERSION + + // if (args.flag("--tls-max-v1.0")) bun.tls.max_tls_version = 0x0301; + // if (args.flag("--tls-max-v1.1")) bun.tls.max_tls_version = 0x0302; + if (args.flag("--tls-max-v1.2")) bun.tls.max_tls_version_from_cli_flag = 0x0303; + if (args.flag("--tls-max-v1.3")) bun.tls.max_tls_version_from_cli_flag = 0x0304; + + if (args.flag("--tls-min-v1.3")) bun.tls.min_tls_version_from_cli_flag = 0x0304; + if (args.flag("--tls-min-v1.2")) bun.tls.min_tls_version_from_cli_flag = 0x0303; + if (args.flag("--tls-min-v1.1")) bun.tls.min_tls_version_from_cli_flag = 0x0302; + if (args.flag("--tls-min-v1.0")) bun.tls.min_tls_version_from_cli_flag = 0x0301; + + if (bun.tls.min_tls_version_from_cli_flag != null and bun.tls.max_tls_version_from_cli_flag != null) { + Output.errGeneric("either --tls-min-v1.x or --tls-max-v1.x can be used, not both", .{}); + Global.exit(1); + } + ctx.debug.offline_mode_setting = if (args.flag("--prefer-offline")) Bunfig.OfflineMode.offline else if (args.flag("--prefer-latest")) diff --git a/src/deps/uws.zig b/src/deps/uws.zig index dcb260921a5f10..6b95d0cc5f8266 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2552,6 +2552,9 @@ pub const us_bun_socket_context_options_t = extern struct { request_cert: i32 = 0, client_renegotiation_limit: u32 = 3, client_renegotiation_window: u32 = 600, + min_tls_version: u32 = 0, + max_tls_version: u32 = 0, + secure_protocol_method: [*c]const u8 = null, }; pub const create_bun_socket_error_t = enum(c_int) { @@ -2581,15 +2584,32 @@ pub const us_bun_verify_error_t = extern struct { reason: [*c]const u8 = null, pub fn toJS(this: *const us_bun_verify_error_t, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - const code = if (this.code == null) "" else this.code[0..bun.len(this.code)]; - const reason = if (this.reason == null) "" else this.reason[0..bun.len(this.reason)]; + const message_slice = if (this.reason != null and bun.len(this.reason) > 0) + this.reason[0..bun.len(this.reason)] + else if (this.code != null and bun.len(this.code) > 0) + this.code[0..bun.len(this.code)] + else + "TLS Error"; + + const code_slice = if (this.code != null and bun.len(this.code) > 0) + this.code[0..bun.len(this.code)] + else + ""; - const fallback = JSC.SystemError{ - .code = bun.String.createUTF8(code), - .message = bun.String.createUTF8(reason), + const sys_error_details = JSC.SystemError{ + .message = bun.String.createUTF8(message_slice), + .code = bun.String.createUTF8(code_slice), + .errno = this.error_no, }; - return fallback.toErrorInstance(globalObject); + const js_error_value = sys_error_details.toErrorInstance(globalObject); + + if (code_slice.len > 0) { + const js_error_obj = js_error_value.toObject(globalObject) catch bun.outOfMemory(); + js_error_obj.put(globalObject, JSC.ZigString.static("code"), bun.String.createUTF8ForJS(globalObject, code_slice)) catch bun.outOfMemory(); + } + + return js_error_value; } }; pub extern fn us_ssl_socket_verify_error_from_ssl(ssl: *BoringSSL.SSL) us_bun_verify_error_t; diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 518d81b91fa08c..ac76d6a4919868 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -682,6 +682,7 @@ declare function $makeAbortError(message?: string, options?: { cause: Error }): */ declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedType: string, actualValue: any): TypeError; declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedTypes: string[], actualValue: any): TypeError; +declare function $ERR_INVALID_ARG_TYPE(message: string): TypeError; declare function $ERR_INVALID_ARG_VALUE(name: string, value: any, reason?: string): TypeError; declare function $ERR_UNKNOWN_ENCODING(enc: string): TypeError; declare function $ERR_STREAM_DESTROYED(method: string): Error; @@ -695,7 +696,7 @@ declare function $ERR_MISSING_ARGS(...args: [string, ...string[]]): TypeError; */ declare function $ERR_MISSING_ARGS(oneOf: string[]): TypeError; declare function $ERR_INVALID_RETURN_VALUE(expected_type: string, name: string, actual_value: any): TypeError; -declare function $ERR_TLS_INVALID_PROTOCOL_VERSION(a: string, b: string): TypeError; +declare function $ERR_TLS_INVALID_PROTOCOL_VERSION(a: import("tls").SecureVersion, b: "maximum" | "minimum"): TypeError; declare function $ERR_TLS_PROTOCOL_VERSION_CONFLICT(a: string, b: string): TypeError; declare function $ERR_INVALID_IP_ADDRESS(ip: any): TypeError; declare function $ERR_INVALID_ADDRESS_FAMILY(addressType, host, port): RangeError; diff --git a/src/js/internal/tls.ts b/src/js/internal/tls.ts index f02c0edb93547f..08f375279cbaa6 100644 --- a/src/js/internal/tls.ts +++ b/src/js/internal/tls.ts @@ -1,5 +1,33 @@ const { isTypedArray, isArrayBuffer } = require("node:util/types"); +const getDefaultMinTLSVersionFromCLIFlag = $newZigFunction( + "node_tls_binding.zig", + "getDefaultMinTLSVersionFromCLIFlag", + 0, +) as () => number | null; + +const getDefaultMaxTLSVersionFromCLIFlag = $newZigFunction( + "node_tls_binding.zig", + "getDefaultMaxTLSVersionFromCLIFlag", + 0, +) as () => number | null; + +const TLS_VERSION_MAP = { + "TLSv1": 0x0301, + "TLSv1.1": 0x0302, + "TLSv1.2": 0x0303, + "TLSv1.3": 0x0304, +} as const satisfies Record; + +const TLS_VERSION_REVERSE_MAP: { + [Key in keyof typeof TLS_VERSION_MAP as (typeof TLS_VERSION_MAP)[Key]]: Key; +} = { + 0x0301: "TLSv1", + 0x0302: "TLSv1.1", + 0x0303: "TLSv1.2", + 0x0304: "TLSv1.3", +}; + function isPemObject(obj: unknown): obj is { pem: unknown } { return $isObject(obj) && "pem" in obj; } @@ -50,4 +78,254 @@ function isValidTLSArray(obj: unknown) { const VALID_TLS_ERROR_MESSAGE_TYPES = "string or an instance of Buffer, TypedArray, DataView, or BunFile"; -export { VALID_TLS_ERROR_MESSAGE_TYPES, isValidTLSArray, isValidTLSItem, throwOnInvalidTLSArray }; +function getTlsVersionOrDefault(version: number | null, fallback: import("node:tls").SecureVersion) { + if (!version) return fallback; + const asString = TLS_VERSION_REVERSE_MAP[version]; + if (!asString) return fallback; + return asString; +} + +const DEFAULT_MIN_VERSION: import("node:tls").SecureVersion = getTlsVersionOrDefault( + getDefaultMinTLSVersionFromCLIFlag(), + "TLSv1.2", +); +const DEFAULT_MAX_VERSION: import("node:tls").SecureVersion = getTlsVersionOrDefault( + getDefaultMaxTLSVersionFromCLIFlag(), + "TLSv1.3", +); + +// const forbiddenProtocols = ["SSLv23_method", "TLSv1_1_method", "TLSv1_method"]; + +function resolveTLSVersions(options: import("node:tls").TLSSocketOptions): [min: number, max: number] { + // if (typeof options?.secureProtocol === "string" && forbiddenProtocols.includes(options.secureProtocol)) { + // throw $ERR_SSL_UNSUPPORTED_PROTOCOL(`Protocol method ${options.secureProtocol} is not supported`); + // } + + const secureProtocol = options?.secureProtocol; + const maybeConflictVersion = options.minVersion || options.maxVersion; + if (secureProtocol && maybeConflictVersion) { + throw $ERR_TLS_PROTOCOL_VERSION_CONFLICT(maybeConflictVersion, secureProtocol); + } + + let minVersionName: import("node:tls").SecureVersion = DEFAULT_MIN_VERSION; + let maxVersionName: import("node:tls").SecureVersion = DEFAULT_MAX_VERSION; + + // Node's C++ logic: https://github.com/nodejs/node/blob/main/src/crypto/crypto_context.cc + if (typeof secureProtocol === "string") { + if ( + secureProtocol === "SSLv2_method" || + secureProtocol === "SSLv2_server_method" || + secureProtocol === "SSLv2_client_method" + ) { + throw $ERR_TLS_INVALID_PROTOCOL_METHOD("SSLv2 methods disabled"); + } else if ( + secureProtocol === "SSLv3_method" || + secureProtocol === "SSLv3_server_method" || + secureProtocol === "SSLv3_client_method" + ) { + throw $ERR_TLS_INVALID_PROTOCOL_METHOD("SSLv3 methods disabled"); + } else if (secureProtocol === "SSLv23_method") { + minVersionName = DEFAULT_MIN_VERSION; + maxVersionName = DEFAULT_MAX_VERSION; + } else if (secureProtocol === "SSLv23_server_method") { + minVersionName = DEFAULT_MIN_VERSION; + maxVersionName = DEFAULT_MAX_VERSION; + } else if (secureProtocol === "SSLv23_client_method") { + minVersionName = DEFAULT_MIN_VERSION; + maxVersionName = DEFAULT_MAX_VERSION; + // method = TLS_client_method(); + } else if (secureProtocol === "TLS_method") { + minVersionName = "TLSv1"; + maxVersionName = "TLSv1.3"; + } else if (secureProtocol === "TLS_server_method") { + minVersionName = "TLSv1"; + maxVersionName = "TLSv1.3"; + // method = TLS_server_method(); + } else if (secureProtocol === "TLS_client_method") { + minVersionName = "TLSv1"; + maxVersionName = "TLSv1.3"; + // method = TLS_client_method(); + } else if (secureProtocol === "TLSv1_method") { + minVersionName = maxVersionName = "TLSv1"; + } else if (secureProtocol === "TLSv1_server_method") { + minVersionName = maxVersionName = "TLSv1"; + // method = TLS_server_method(); + } else if (secureProtocol === "TLSv1_client_method") { + minVersionName = maxVersionName = "TLSv1"; + // method = TLS_client_method(); + } else if (secureProtocol === "TLSv1_1_method") { + minVersionName = maxVersionName = "TLSv1.1"; + } else if (secureProtocol === "TLSv1_1_server_method") { + minVersionName = maxVersionName = "TLSv1.1"; + // method = TLS_server_method(); + } else if (secureProtocol === "TLSv1_1_client_method") { + minVersionName = maxVersionName = "TLSv1.1"; + // method = TLS_client_method(); + } else if (secureProtocol === "TLSv1_2_method") { + minVersionName = maxVersionName = "TLSv1.2"; + } else if (secureProtocol === "TLSv1_2_server_method") { + minVersionName = maxVersionName = "TLSv1.2"; + // method = TLS_server_method(); + } else if (secureProtocol === "TLSv1_2_client_method") { + minVersionName = maxVersionName = "TLSv1.2"; + // method = TLS_client_method(); + } else if (secureProtocol === "TLSv1_3_method") { + minVersionName = maxVersionName = "TLSv1.3"; + } else if (secureProtocol === "TLSv1_3_server_method") { + minVersionName = maxVersionName = "TLSv1.3"; + // method = TLS_server_method(); + } else if (secureProtocol === "TLSv1_3_client_method") { + minVersionName = maxVersionName = "TLSv1.3"; + // method = TLS_client_method(); + } else { + throw $ERR_TLS_INVALID_PROTOCOL_METHOD(`Unknown method: ${secureProtocol}`); + } + } else { + minVersionName = options && options.minVersion !== undefined ? options.minVersion : DEFAULT_MIN_VERSION; + maxVersionName = options && options.maxVersion !== undefined ? options.maxVersion : DEFAULT_MAX_VERSION; + } + + let minVersion: number; + let maxVersion: number; + + if (typeof minVersionName === "string") { + if (!(minVersionName in TLS_VERSION_MAP)) { + throw $ERR_TLS_INVALID_PROTOCOL_VERSION(minVersionName, "minimum"); + } + minVersion = TLS_VERSION_MAP[minVersionName]; + } else { + throw $ERR_INVALID_ARG_TYPE("options.minVersion", "string", minVersionName); + } + + if (typeof maxVersionName === "string") { + if (!(maxVersionName in TLS_VERSION_MAP)) { + throw $ERR_TLS_INVALID_PROTOCOL_VERSION(maxVersionName, "maximum"); + } + maxVersion = TLS_VERSION_MAP[maxVersionName]; + } else { + throw $ERR_INVALID_ARG_TYPE("options.maxVersion", "string", maxVersionName); + } + + return [minVersion, maxVersion]; +} + +function validateTLSOptions(options: any) { + if (!options || typeof options !== "object") return; + + let cert = options.cert; + if (cert) throwOnInvalidTLSArray("options.cert", cert); + + let key = options.key; + if (key) throwOnInvalidTLSArray("options.key", key); + + let ca = options.ca; + if (ca) throwOnInvalidTLSArray("options.ca", ca); + + if (!$isUndefinedOrNull(options.privateKeyIdentifier)) { + if ($isUndefinedOrNull(options.privateKeyEngine)) { + throw $ERR_INVALID_ARG_VALUE("options.privateKeyEngine", options.privateKeyEngine); + } else if (typeof options.privateKeyEngine !== "string") { + throw $ERR_INVALID_ARG_TYPE( + "options.privateKeyEngine", + ["string", "null", "undefined"], + options.privateKeyEngine, + ); + } + if (typeof options.privateKeyIdentifier !== "string") { + throw $ERR_INVALID_ARG_TYPE( + "options.privateKeyIdentifier", + ["string", "null", "undefined"], + options.privateKeyIdentifier, + ); + } + } + + const ciphers = options.ciphers; + if (ciphers !== undefined && typeof ciphers !== "string") { + throw $ERR_INVALID_ARG_TYPE("options.ciphers", "string", ciphers); + } + + const passphrase = options.passphrase; + if (passphrase !== undefined && typeof passphrase !== "string") { + throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase); + } + + const servername = options.servername; + if (servername !== undefined && typeof servername !== "string") { + throw $ERR_INVALID_ARG_TYPE("options.servername", "string", servername); + } + + const ecdhCurve = options.ecdhCurve; + if (ecdhCurve !== undefined && typeof ecdhCurve !== "string") { + throw $ERR_INVALID_ARG_TYPE("options.ecdhCurve", "string", ecdhCurve); + } + + const handshakeTimeout = options.handshakeTimeout; + if (handshakeTimeout !== undefined && typeof handshakeTimeout !== "number") { + throw $ERR_INVALID_ARG_TYPE("options.handshakeTimeout", "number", handshakeTimeout); + } + + const sessionTimeout = options.sessionTimeout; + if (sessionTimeout !== undefined && typeof sessionTimeout !== "number") { + throw $ERR_INVALID_ARG_TYPE("options.sessionTimeout", "number", sessionTimeout); + } + + const ticketKeys = options.ticketKeys; + if (ticketKeys !== undefined) { + if (!Buffer.isBuffer(ticketKeys)) { + throw $ERR_INVALID_ARG_TYPE("options.ticketKeys", "Buffer", ticketKeys); + } + if (ticketKeys.length !== 48) { + throw $ERR_INVALID_ARG_VALUE( + "options.ticketKeys", + ticketKeys.length, + "The property 'options.ticketKeys' must be exactly 48 bytes", + ); + } + } + + const secureOptions = options.secureOptions || 0; + if (secureOptions && typeof secureOptions !== "number") { + throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions); + } + + const requestCert = options.requestCert; + if (requestCert !== undefined && typeof requestCert !== "boolean") { + throw $ERR_INVALID_ARG_TYPE("options.requestCert", "boolean", requestCert); + } + + const rejectUnauthorized = options.rejectUnauthorized; + if (rejectUnauthorized !== undefined && typeof rejectUnauthorized !== "boolean") { + throw $ERR_INVALID_ARG_TYPE("options.rejectUnauthorized", "boolean", rejectUnauthorized); + } +} + +let warnOnAllowUnauthorized = true; + +function getAllowUnauthorized(): boolean { + const allowUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0"; + + if (allowUnauthorized && warnOnAllowUnauthorized) { + warnOnAllowUnauthorized = false; + process.emitWarning( + "Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS " + + "connections and HTTPS requests insecure by disabling certificate verification.", + ); + } + return allowUnauthorized; +} + +export default { + getAllowUnauthorized, + isValidTLSArray, + isValidTLSItem, + resolveTLSVersions, + throwOnInvalidTLSArray, + VALID_TLS_ERROR_MESSAGE_TYPES, + DEFAULT_MIN_VERSION, + DEFAULT_MAX_VERSION, + validateTLSOptions, + + TLS_VERSION_REVERSE_MAP, + TLS_VERSION_MAP, +}; diff --git a/src/js/node/net.ts b/src/js/node/net.ts index c573a351d1ae92..ab8b4348f59b1a 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -22,6 +22,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. const { Duplex } = require("node:stream"); const EventEmitter = require("node:events"); +const { getAllowUnauthorized } = require("internal/tls"); const [addServerName, upgradeDuplexToTLS, isNamedPipeSocket, getBufferedAmount] = $zig( "socket.zig", "createNodeTLSBinding", @@ -100,6 +101,7 @@ const kclosed = Symbol("closed"); const kended = Symbol("ended"); const kwriteCallback = Symbol("writeCallback"); const kSocketClass = Symbol("kSocketClass"); +const kSocketOptions = Symbol("kSocketOptions"); function endNT(socket, callback, err) { socket.$end(); @@ -272,15 +274,21 @@ const SocketHandlers: SocketHandler = { self.emit("secure", self); self.alpnProtocol = socket.alpnProtocol; const { checkServerIdentity } = self[bunTLSConnectOptions]; + if (!verifyError && typeof checkServerIdentity === "function" && self.servername) { const cert = self.getPeerCertificate(true); verifyError = checkServerIdentity(self.servername, cert); } + if (self._requestCert || self._rejectUnauthorized) { if (verifyError) { self.authorized = false; self.authorizationError = verifyError.code || verifyError.message; if (self._rejectUnauthorized) { + self.emit("error", verifyError); + self.emit("secure", self); + self.emit("_tlsError", verifyError); + self.server.emit("tlsClientError", verifyError, self); self.destroy(verifyError); return; } @@ -290,7 +298,9 @@ const SocketHandlers: SocketHandler = { } else { self.authorized = true; } - self.emit("secureConnect", verifyError); + if (success) { + self.emit("secureConnect", verifyError); + } self.removeListener("end", onConnectEnd); }, timeout(socket) { @@ -350,8 +360,18 @@ const ServerHandlers: SocketHandler = { const self = this.data; socket[kServerSocket] = self._handle; const options = self[bunSocketServerOptions]; - const { pauseOnConnect, connectionListener, [kSocketClass]: SClass, requestCert, rejectUnauthorized } = options; - const _socket = new SClass({}); + + const { + pauseOnConnect, + connectionListener, + [kSocketClass]: SClass, + [kSocketOptions]: socketOptions = {}, + requestCert, + rejectUnauthorized, + } = options; + + const _socket = new SClass(socketOptions); + _socket.isServer = true; _socket.server = self; _socket._requestCert = requestCert; @@ -394,48 +414,63 @@ const ServerHandlers: SocketHandler = { handshake(socket, success, verifyError) { const { data: self } = socket; + if (!success && verifyError?.code === "ECONNRESET") { + if (self._hadError) return; const err = new ConnResetException("socket hang up"); self.emit("_tlsError", err); self.server.emit("tlsClientError", err, self); self._hadError = true; + // error before handshake on the server side will only be emitted using tlsClientError self.destroy(); return; } + + if (!success) { + const err = verifyError || $ERR_SSL_UNSUPPORTED_PROTOCOL("TLS handshake failed"); + + self._hadError = true; + self.emit("_tlsError", err); + self.server.emit("tlsClientError", err, self); + self.destroy(); + return; + } + self._securePending = false; self.secureConnecting = false; self._secureEstablished = !!success; self.servername = socket.getServername(); const server = self.server; self.alpnProtocol = socket.alpnProtocol; + if (self._requestCert || self._rejectUnauthorized) { if (verifyError) { self.authorized = false; self.authorizationError = verifyError.code || verifyError.message; - server.emit("tlsClientError", verifyError, self); if (self._rejectUnauthorized) { - // if we reject we still need to emit secure - self.emit("secure", self); + self.emit("_tlsError", verifyError); + self.server.emit("tlsClientError", verifyError, self); self.destroy(verifyError); return; } - } else { - self.authorized = true; } - } else { self.authorized = true; } + const connectionListener = server[bunSocketServerOptions]?.connectionListener; if (typeof connectionListener === "function") { connectionListener.$call(server, self); } - server.emit("secureConnection", self); - // after secureConnection event we emmit secure and secureConnect - self.emit("secure", self); - self.emit("secureConnect", verifyError); - if (!server.pauseOnConnect) { - self.resume(); + + if (success) { + server.emit("secureConnection", self); + self.emit("secure", self); + self.emit("secureConnect", verifyError); + + if (!server.pauseOnConnect) { + self.resume(); + } } }, error(socket, error) { @@ -735,7 +770,13 @@ Socket.prototype.connect = function connect(...args) { const bunTLS = this[bunTlsSymbol]; var tls = undefined; if (typeof bunTLS === "function") { - tls = bunTLS.$call(this, port, host, true); + tls = bunTLS.$call( + this, + port, + host, + true, // isClient + ); + // Client always request Cert this._requestCert = true; if (tls) { @@ -743,7 +784,9 @@ Socket.prototype.connect = function connect(...args) { this._rejectUnauthorized = rejectUnauthorized; tls.rejectUnauthorized = rejectUnauthorized; } else { - this._rejectUnauthorized = tls.rejectUnauthorized; + const allowUnauth = getAllowUnauthorized(); + this._rejectUnauthorized = !allowUnauth; + tls.rejectUnauthorized = !allowUnauth; } tls.requestCert = true; tls.session = session || tls.session; @@ -1451,7 +1494,15 @@ Server.prototype.listen = function listen(port, hostname, onListen) { if (typeof bunTLS === "function") { [tls, TLSSocketClass] = bunTLS.$call(this, port, hostname, false); options.servername = tls.serverName; + options.minVersion = tls.minVersion; + options.maxVersion = tls.maxVersion; + options[kSocketClass] = TLSSocketClass; + options[kSocketOptions] = { + minVersion: tls.minVersionName, + maxVersion: tls.maxVersionName, + }; + contexts = tls.contexts; if (!tls.requestCert) { tls.rejectUnauthorized = false; diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index be8962060ec35f..9c9a4f07f3b183 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -1,10 +1,15 @@ -// Hardcoded module "node:tls" const { isArrayBufferView, isTypedArray } = require("node:util/types"); const net = require("node:net"); const { Duplex } = require("node:stream"); const [addServerName] = $zig("socket.zig", "createNodeTLSBinding"); const { throwNotImplemented } = require("internal/shared"); -const { throwOnInvalidTLSArray } = require("internal/tls"); +const { + TLS_VERSION_REVERSE_MAP, + resolveTLSVersions, + DEFAULT_MIN_VERSION, + DEFAULT_MAX_VERSION, + validateTLSOptions, +} = require("internal/tls"); const { Server: NetServer, Socket: NetSocket } = net; @@ -202,62 +207,36 @@ var InternalSecureContext = class SecureContext { passphrase; servername; secureOptions; + ciphers; + + secureProtocol: string | undefined; + minVersion: number | undefined; + maxVersion: number | undefined; constructor(options) { const context = {}; if (options) { + validateTLSOptions(options); + let cert = options.cert; - if (cert) { - throwOnInvalidTLSArray("options.cert", cert); - this.cert = cert; - } + if (cert) this.cert = cert; let key = options.key; - if (key) { - throwOnInvalidTLSArray("options.key", key); - this.key = key; - } - - let ca = options.ca; - if (ca) { - throwOnInvalidTLSArray("options.ca", ca); - this.ca = ca; - } - - let passphrase = options.passphrase; - if (passphrase && typeof passphrase !== "string") { - throw new TypeError("passphrase argument must be an string"); - } - this.passphrase = passphrase; - - let servername = options.servername; - if (servername && typeof servername !== "string") { - throw new TypeError("servername argument must be an string"); - } - this.servername = servername; - - let secureOptions = options.secureOptions || 0; - if (secureOptions && typeof secureOptions !== "number") { - throw new TypeError("secureOptions argument must be an number"); - } + if (key) this.key = key; - this.secureOptions = secureOptions; + const ca = options.ca; + if (ca) this.ca = ca; - if (!$isUndefinedOrNull(options.privateKeyIdentifier)) { - if ($isUndefinedOrNull(options.privateKeyEngine)) { - // prettier-ignore - throw $ERR_INVALID_ARG_VALUE("options.privateKeyEngine", options.privateKeyEngine); - } else if (typeof options.privateKeyEngine !== "string") { - // prettier-ignore - throw $ERR_INVALID_ARG_TYPE("options.privateKeyEngine", ["string", "null", "undefined"], options.privateKeyEngine); - } + this.ciphers = options.ciphers; + this.passphrase = options.passphrase; + this.servername = options.servername; + this.secureOptions = options.secureOptions || 0; + this.secureProtocol = options.secureProtocol; - if (typeof options.privateKeyIdentifier !== "string") { - // prettier-ignore - throw $ERR_INVALID_ARG_TYPE("options.privateKeyIdentifier", ["string", "null", "undefined"], options.privateKeyIdentifier); - } - } + const [minVersion, maxVersion] = resolveTLSVersions(options); + this.minVersion = minVersion; + this.maxVersion = maxVersion; } this.context = context; @@ -304,6 +283,8 @@ function TLSSocket(socket?, options?) { this.authorizationError; this[krenegotiationDisabled] = undefined; this.encrypted = true; + this.ciphers = undefined; + this.ecdhCurve = undefined; const isNetSocketOrDuplex = socket instanceof Duplex; @@ -313,6 +294,12 @@ function TLSSocket(socket?, options?) { if (typeof options === "object") { const { ALPNProtocols } = options; + + // use `in` check because passing undefined should throw according to node tests + if ("checkServerIdentity" in options && typeof options.checkServerIdentity !== "function") { + throw $ERR_INVALID_ARG_TYPE("options.checkServerIdentity", "function", options.checkServerIdentity); + } + if (ALPNProtocols) { convertALPNProtocols(ALPNProtocols, this); } @@ -481,6 +468,9 @@ TLSSocket.prototype[buntls] = function (port, host) { session: this[ksession], rejectUnauthorized: this._rejectUnauthorized, requestCert: this._requestCert, + minVersionName: TLS_VERSION_REVERSE_MAP[this[ksecureContext].minVersion], + maxVersionName: TLS_VERSION_REVERSE_MAP[this[ksecureContext].maxVersion], + secureProtocol: this[ksecureContext].secureProtocol, ...this[ksecureContext], }; }; @@ -505,6 +495,10 @@ function Server(options, secureConnectionListener): void { this.servername = undefined; this.ALPNProtocols = undefined; + const [minVersion, maxVersion] = resolveTLSVersions(options || {}); + this.minVersion = minVersion; + this.maxVersion = maxVersion; + let contexts: Map | null = null; this.addContext = function (hostname, context) { @@ -527,6 +521,7 @@ function Server(options, secureConnectionListener): void { options = options.context; } if (options) { + validateTLSOptions(options); const { ALPNProtocols } = options; if (ALPNProtocols) { @@ -534,48 +529,26 @@ function Server(options, secureConnectionListener): void { } let cert = options.cert; - if (cert) { - throwOnInvalidTLSArray("options.cert", cert); - this.cert = cert; - } + if (cert) this.cert = cert; let key = options.key; - if (key) { - throwOnInvalidTLSArray("options.key", key); - this.key = key; - } + if (key) this.key = key; let ca = options.ca; - if (ca) { - throwOnInvalidTLSArray("options.ca", ca); - this.ca = ca; - } + if (ca) this.ca = ca; - let passphrase = options.passphrase; - if (passphrase && typeof passphrase !== "string") { - throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase); - } - this.passphrase = passphrase; - - let servername = options.servername; - if (servername && typeof servername !== "string") { - throw $ERR_INVALID_ARG_TYPE("options.servername", "string", servername); - } - this.servername = servername; - - let secureOptions = options.secureOptions || 0; - if (secureOptions && typeof secureOptions !== "number") { - throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions); - } - this.secureOptions = secureOptions; + this.ciphers = options.ciphers; + this.ecdhCurve = options.ecdhCurve; + this.passphrase = options.passphrase; + this.servername = options.servername; + this.secureOptions = options.secureOptions || 0; + this.secureProtocol = options.secureProtocol; const requestCert = options.requestCert || false; - if (requestCert) this._requestCert = requestCert; else this._requestCert = undefined; const rejectUnauthorized = options.rejectUnauthorized; - if (typeof rejectUnauthorized !== "undefined") { this._rejectUnauthorized = rejectUnauthorized; } else this._rejectUnauthorized = rejectUnauthorizedDefault; @@ -586,7 +559,11 @@ function Server(options, secureConnectionListener): void { throw Error("Not implented in Bun yet"); }; - Server.prototype.setTicketKeys = function () { + Server.prototype.setTicketKeys = function (ticketKeys) { + if (!Buffer.isBuffer(ticketKeys) || ticketKeys.length !== 48) { + throw $ERR_INVALID_ARG_TYPE("Session ticket keys must be a 48-byte buffer"); + } + throw Error("Not implented in Bun yet"); }; @@ -598,6 +575,11 @@ function Server(options, secureConnectionListener): void { cert: this.cert, ca: this.ca, passphrase: this.passphrase, + minVersion: this.minVersion, + maxVersion: this.maxVersion, + secureProtocol: this.secureProtocol, + minVersionName: TLS_VERSION_REVERSE_MAP[this.minVersion], + maxVersionName: TLS_VERSION_REVERSE_MAP[this.maxVersion], secureOptions: this.secureOptions, rejectUnauthorized: this._rejectUnauthorized, requestCert: isClient ? true : this._requestCert, @@ -620,12 +602,11 @@ function createServer(options, connectionListener) { const DEFAULT_ECDH_CURVE = "auto", // https://github.com/Jarred-Sumner/uSockets/blob/fafc241e8664243fc0c51d69684d5d02b9805134/src/crypto/openssl.c#L519-L523 DEFAULT_CIPHERS = - "DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", - DEFAULT_MIN_VERSION = "TLSv1.2", - DEFAULT_MAX_VERSION = "TLSv1.3"; + "DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"; function normalizeConnectArgs(listArgs) { - const args = net._normalizeArgs(listArgs); + // Cast to any to use internal _normalizeArgs helper as in Node.js implementation + const args = (net as any)._normalizeArgs(listArgs); $assert($isObject(args[0])); // If args[0] was options, then normalize dealt with it. @@ -648,10 +629,13 @@ function normalizeConnectArgs(listArgs) { function connect(...args) { let normal = normalizeConnectArgs(args); const options = normal[0]; + const { ALPNProtocols } = options; + if (ALPNProtocols) { convertALPNProtocols(ALPNProtocols, options); } + return new TLSSocket(options).connect(normal); } @@ -669,9 +653,11 @@ function convertProtocols(protocols) { (p, c, i) => { const len = Buffer.byteLength(c); if (len > 255) { - throw new RangeError( + const err = new RangeError( `The byte length of the protocol at index ${i} exceeds the maximum length. It must be <= 255. Received ${len}`, ); + err.code = "ERR_OUT_OF_RANGE"; + throw err; } lens[i] = len; return p + 1 + len; @@ -696,7 +682,9 @@ function convertALPNProtocols(protocols, out) { out.ALPNProtocols = convertProtocols(protocols); } else if (isTypedArray(protocols)) { // Copy new buffer not to be modified by user. - out.ALPNProtocols = Buffer.from(protocols); + out.ALPNProtocols = Buffer.from( + protocols.buffer.slice(protocols.byteOffset, protocols.byteOffset + protocols.byteLength), + ); } else if (isArrayBufferView(protocols)) { out.ALPNProtocols = Buffer.from( protocols.buffer.slice(protocols.byteOffset, protocols.byteOffset + protocols.byteLength), diff --git a/src/tls.zig b/src/tls.zig new file mode 100644 index 00000000000000..29835cdbcc0b20 --- /dev/null +++ b/src/tls.zig @@ -0,0 +1,12 @@ +const bun = @import("bun"); + +// These variables are set by the CLI parser. + +// These get read by node_tls_binding.zig for `tls.ts` to consume and use +// as the values for `tls.DEFAULT_MIN_VERSION` and `tls.DEFAULT_MAX_VERSION` + +// A null value means no CLI flag was provided, and the *default* defaults +// are defined in src/js/node/tls.ts + +pub var min_tls_version_from_cli_flag: ?u16 = null; +pub var max_tls_version_from_cli_flag: ?u16 = null; diff --git a/test/harness.ts b/test/harness.ts index e5acad6f1df009..229d57d5e36afc 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -7,9 +7,10 @@ import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun"; import { heapStats } from "bun:jsc"; -import { fork, ChildProcess } from "child_process"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { readFile, readlink, writeFile, readdir, rm } from "fs/promises"; +import { ChildProcess, fork } from "child_process"; +import detectLibc from "detect-libc"; +import { readdir, readFile, readlink, rm, writeFile } from "fs/promises"; import fs, { closeSync, openSync, rmSync } from "node:fs"; import os from "node:os"; import { dirname, isAbsolute, join } from "path"; diff --git a/test/integration/bun-types/fixture/env.ts b/test/integration/bun-types/fixture/env.ts index 5ac301905c7c4e..f2ec9e1eec09b3 100644 --- a/test/integration/bun-types/fixture/env.ts +++ b/test/integration/bun-types/fixture/env.ts @@ -33,9 +33,7 @@ declare global { } } expectType(Bun.env.BAZ).is<"BAZ">(); -expectType(process.env.BAZ).is<"BAZ">(); expectType(import.meta.env.BAZ).is<"BAZ">(); -expectType(node_env.BAZ).is<"BAZ">(); expectType(bun_env.BAZ).is<"BAZ">(); expectType(Bun.env.OTHER).is(); @@ -44,16 +42,14 @@ expectType(import.meta.env.OTHER).is(); expectType(node_env.OTHER).is(); expectType(bun_env.OTHER).is(); -function isAllSame(a: T, b: T, c: T, d: T, e: T) { - return a === b && b === c && c === d && d === e; -} +declare function isAllSame(...args: T[]): void; //prettier-ignore { isAllSame <"FOO"> (process.env.FOO, Bun.env.FOO, import.meta.env.FOO, node_env.FOO, bun_env.FOO); isAllSame <"BAR"> (process.env.BAR, Bun.env.BAR, import.meta.env.BAR, node_env.BAR, bun_env.BAR); - isAllSame <"BAZ"> (process.env.BAZ, Bun.env.BAZ, import.meta.env.BAZ, node_env.BAZ, bun_env.BAZ); + isAllSame <"BAZ"> ( Bun.env.BAZ, import.meta.env.BAZ, bun_env.BAZ); // process.env doesn't extend import.meta.env isAllSame (process.env.OTHER, Bun.env.OTHER, import.meta.env.OTHER, node_env.OTHER, bun_env.OTHER); } diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index a9038a1840d5ac..81b12e3f1a7511 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -34,7 +34,7 @@ const words: Record [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 241, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, - "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1849 }, + "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1856 }, "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 180 }, "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 103 }, diff --git a/test/js/node/test/fixtures/tls-connect.js b/test/js/node/test/fixtures/tls-connect.js index 51c0b328e97e27..fa972691537bff 100644 --- a/test/js/node/test/fixtures/tls-connect.js +++ b/test/js/node/test/fixtures/tls-connect.js @@ -105,4 +105,4 @@ exports.connect = function connect(options, callback) { if (client.conn) client.conn.end(); } -}; +}; \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-basic-validations.js b/test/js/node/test/parallel/test-tls-basic-validations.js new file mode 100644 index 00000000000000..e795349abcb303 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-basic-validations.js @@ -0,0 +1,137 @@ +'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({ ciphers: 1 }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.ciphers" property must be of type string.' + + ' Received type number (1)' + }); + +assert.throws( + () => tls.createServer({ ciphers: 1 }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.ciphers" property must be of type string.' + + ' Received type number (1)' + }); + +assert.throws( + () => tls.createSecureContext({ key: 'dummykey', passphrase: 1 }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "options\.passphrase" property must be of type string/ + }); + +assert.throws( + () => tls.createServer({ key: 'dummykey', passphrase: 1 }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "options\.passphrase" property must be of type string/ + }); + +assert.throws( + () => tls.createServer({ ecdhCurve: 1 }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "options\.ecdhCurve" property must be of type string/ + }); + +assert.throws( + () => tls.createServer({ handshakeTimeout: 'abcd' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.handshakeTimeout" property must be of type number.' + + " Received type string ('abcd')" + } +); + +assert.throws( + () => tls.createServer({ sessionTimeout: 'abcd' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "options\.sessionTimeout" property must be of type number/ + }); + +assert.throws( + () => tls.createServer({ ticketKeys: 'abcd' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "options\.ticketKeys" property must be an instance of/ + }); + +assert.throws(() => tls.createServer({ ticketKeys: Buffer.alloc(0) }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The property 'options\.ticketKeys' must be exactly 48 bytes/ +}); + +{ + const buffer = Buffer.from('abcd'); + const out = {}; + tls.convertALPNProtocols(buffer, out); + out.ALPNProtocols.write('efgh'); + assert(buffer.equals(Buffer.from('abcd'))); + assert(out.ALPNProtocols.equals(Buffer.from('efgh'))); +} + +{ + const arrayBufferViewStr = 'abcd'; + const inputBuffer = Buffer.from(arrayBufferViewStr.repeat(8), 'utf8'); + for (const expectView of common.getArrayBufferViews(inputBuffer)) { + const out = {}; + const expected = Buffer.from(expectView.buffer.slice(), + expectView.byteOffset, + expectView.byteLength); + tls.convertALPNProtocols(expectView, out); + assert(out.ALPNProtocols.equals(expected)); + } +} + +{ + const protocols = [(new String('a')).repeat(500)]; + const out = {}; + assert.throws( + () => tls.convertALPNProtocols(protocols, out), + { + code: 'ERR_OUT_OF_RANGE', + message: 'The byte length of the protocol at index 0 exceeds the ' + + 'maximum length. It must be <= 255. Received 500' + } + ); +} + +assert.throws(() => { tls.createSecureContext({ minVersion: 'fhqwhgads' }); }, + { + code: 'ERR_TLS_INVALID_PROTOCOL_VERSION', + name: 'TypeError' + }); + +assert.throws(() => { tls.createSecureContext({ maxVersion: 'fhqwhgads' }); }, + { + code: 'ERR_TLS_INVALID_PROTOCOL_VERSION', + name: 'TypeError' + }); + +for (const checkServerIdentity of [undefined, null, 1, true]) { + assert.throws(() => { + tls.connect({ checkServerIdentity }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); +} \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-cli-max-version-1.2.js b/test/js/node/test/parallel/test-tls-cli-max-version-1.2.js new file mode 100644 index 00000000000000..d9887c611ec515 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cli-max-version-1.2.js @@ -0,0 +1,15 @@ +// Flags: --tls-max-v1.2 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-max-v1.2` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.2'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-cli-max-version-1.3.js b/test/js/node/test/parallel/test-tls-cli-max-version-1.3.js new file mode 100644 index 00000000000000..43a56617f25f69 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cli-max-version-1.3.js @@ -0,0 +1,15 @@ +// Flags: --tls-max-v1.3 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-max-v1.3` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.3'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.2'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-cli-min-max-conflict.js b/test/js/node/test/parallel/test-tls-cli-min-max-conflict.js new file mode 100644 index 00000000000000..79185282071292 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cli-min-max-conflict.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that conflicting TLS protocol versions are not allowed + +const assert = require('assert'); +const child_process = require('child_process'); + +const args = ['--tls-min-v1.3', '--tls-max-v1.2', '-p', 'process.version']; +child_process.execFile(process.argv[0], args, (err) => { + assert(err); + assert.match(err.message, /not both/); +}); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-cli-min-version-1.0.js b/test/js/node/test/parallel/test-tls-cli-min-version-1.0.js new file mode 100644 index 00000000000000..6f29425dddb225 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cli-min-version-1.0.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.0 --tls-min-v1.1 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that `node --tls-v1.0` is supported, and overrides --tls-v1.1. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.3'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-cli-min-version-1.1.js b/test/js/node/test/parallel/test-tls-cli-min-version-1.1.js new file mode 100644 index 00000000000000..38bbf05ffff4f9 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cli-min-version-1.1.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.1 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-v1.1` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.3'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.1'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-cli-min-version-1.2.js b/test/js/node/test/parallel/test-tls-cli-min-version-1.2.js new file mode 100644 index 00000000000000..a604bb0b22fe3a --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cli-min-version-1.2.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.2 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-min-v1.2` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.3'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.2'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-cli-min-version-1.3.js b/test/js/node/test/parallel/test-tls-cli-min-version-1.3.js new file mode 100644 index 00000000000000..5afabe8b3d2027 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-cli-min-version-1.3.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.3 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-min-v1.3` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.3'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.3'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-client-reject-12.js b/test/js/node/test/parallel/test-tls-client-reject-12.js new file mode 100644 index 00000000000000..5cb98ff3a22af1 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-client-reject-12.js @@ -0,0 +1,13 @@ +'use strict'; + +// test-tls-client-reject specifically for TLS1.2. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); + +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +require('./test-tls-client-reject.js'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-client-reject.js b/test/js/node/test/parallel/test-tls-client-reject.js new file mode 100644 index 00000000000000..68922e3690eac0 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-client-reject.js @@ -0,0 +1,107 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('rsa_private.pem'), + cert: fixtures.readKey('rsa_cert.crt') +}; + +const server = tls.createServer(options, function(socket) { + socket.pipe(socket); + // Pipe already ends... but leaving this here tests .end() after .end(). + socket.on('end', () => socket.end()); +}).listen(0, common.mustCall(function() { + unauthorized(); +})); + +function unauthorized() { + console.log('connect unauthorized'); + const socket = tls.connect({ + port: server.address().port, + servername: 'localhost', + rejectUnauthorized: false + }, common.mustCall(function() { + let _data; + assert(!socket.authorized); + socket.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'ok'); + _data = data; + })); + socket.on('end', common.mustCall(() => { + assert(_data, 'data failed to echo!'); + })); + socket.on('end', () => rejectUnauthorized()); + })); + socket.once('session', common.mustCall()); + socket.on('error', common.mustNotCall()); + socket.end('ok'); +} + +function rejectUnauthorized() { + console.log('reject unauthorized'); + const socket = tls.connect(server.address().port, { + servername: 'localhost' + }, common.mustNotCall()); + socket.on('data', common.mustNotCall()); + socket.on('error', common.mustCall(function(err) { + rejectUnauthorizedUndefined(); + })); + socket.end('ng'); +} + +function rejectUnauthorizedUndefined() { + console.log('reject unauthorized undefined'); + const socket = tls.connect(server.address().port, { + servername: 'localhost', + rejectUnauthorized: undefined + }, common.mustNotCall()); + socket.on('data', common.mustNotCall()); + socket.on('error', common.mustCall(function(err) { + authorized(); + })); + socket.end('ng'); +} + +function authorized() { + console.log('connect authorized'); + const socket = tls.connect(server.address().port, { + ca: [fixtures.readKey('rsa_cert.crt')], + servername: 'localhost' + }, common.mustCall(function() { + console.log('... authorized'); + assert(socket.authorized); + socket.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'ok'); + })); + socket.on('end', () => server.close()); + })); + socket.on('error', common.mustNotCall()); + socket.end('ok'); +} diff --git a/test/js/node/test/parallel/test-tls-min-max-version.js b/test/js/node/test/parallel/test-tls-min-max-version.js new file mode 100644 index 00000000000000..9f10e72d119c01 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-min-max-version.js @@ -0,0 +1,283 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} +const { + hasOpenSSL, + hasOpenSSL3, +} = require('../common/crypto'); +const fixtures = require('../common/fixtures'); +const { inspect } = require('util'); + +// Check min/max protocol versions. + +const { + assert, connect, keys, tls +} = require(fixtures.path('tls-connect')); +const DEFAULT_MIN_VERSION = tls.DEFAULT_MIN_VERSION; +const DEFAULT_MAX_VERSION = tls.DEFAULT_MAX_VERSION; + + +function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { + assert(proto || cerr || serr, 'test missing any expectations'); + + let ciphers; + if (hasOpenSSL3 && (proto === 'TLSv1' || proto === 'TLSv1.1' || + proto === 'TLSv1_1_method' || proto === 'TLSv1_method' || + sprot === 'TLSv1_1_method' || sprot === 'TLSv1_method')) { + if (serr !== 'ERR_SSL_UNSUPPORTED_PROTOCOL') + ciphers = 'ALL@SECLEVEL=0'; + } + if (hasOpenSSL(3, 1) && cerr === 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION') { + ciphers = 'DEFAULT@SECLEVEL=0'; + } + // Report where test was called from. Strip leading garbage from + // at Object. (file:line) + // from the stack location, we only want the file:line part. + const where = inspect(new Error()).split('\n')[2].replace(/[^(]*/, ''); + connect({ + client: { + checkServerIdentity: (servername, cert) => { }, + ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, + minVersion: cmin, + maxVersion: cmax, + secureProtocol: cprot, + ciphers: ciphers, + host: '127.0.0.1', // Required because Bun implements happy eyeballs + }, + server: { + cert: keys.agent6.cert, + key: keys.agent6.key, + minVersion: smin, + maxVersion: smax, + secureProtocol: sprot, + ciphers: ciphers, + host: '127.0.0.1', // Required because Bun implements happy eyeballs + }, + }, common.mustCall((err, pair, cleanup) => { + function u(_) { return _ === undefined ? 'U' : _; } + console.log('test:', u(cmin), u(cmax), u(cprot), u(smin), u(smax), u(sprot), + u(ciphers), 'expect', u(proto), u(cerr), u(serr)); + console.log(' ', where); + if (!proto) { + console.log('client', pair.client.err ? pair.client.err.code : undefined); + console.log('server', pair.server.err ? pair.server.err.code : undefined); + if (cerr) { + assert(pair.client.err); + // Accept these codes as aliases, the one reported depends on the + // OpenSSL version. + if (cerr === 'ERR_SSL_UNSUPPORTED_PROTOCOL' && + pair.client.err.code === 'ERR_SSL_VERSION_TOO_LOW') + cerr = 'ERR_SSL_VERSION_TOO_LOW'; + assert.strictEqual(pair.client.err.code, cerr); + } + if (serr) { + assert(pair.server.err); + assert.strictEqual(pair.server.err.code, serr); + } + return cleanup(); + } + + assert.ifError(err); + assert.ifError(pair.server.err); + assert.ifError(pair.client.err); + assert(pair.server.conn); + assert(pair.client.conn); + assert.strictEqual(pair.client.conn.getProtocol(), proto); + assert.strictEqual(pair.server.conn.getProtocol(), proto); + return cleanup(); + })); +} + +const U = undefined; + +// Default protocol is the max version. +test(U, U, U, U, U, U, DEFAULT_MAX_VERSION); + +// Insecure or invalid protocols cannot be enabled. +test(U, U, U, U, U, 'SSLv2_method', + U, U, 'ERR_TLS_INVALID_PROTOCOL_METHOD'); +test(U, U, U, U, U, 'SSLv3_method', + U, U, 'ERR_TLS_INVALID_PROTOCOL_METHOD'); +test(U, U, 'SSLv2_method', U, U, U, + U, 'ERR_TLS_INVALID_PROTOCOL_METHOD'); +test(U, U, 'SSLv3_method', U, U, U, + U, 'ERR_TLS_INVALID_PROTOCOL_METHOD'); +test(U, U, 'hokey-pokey', U, U, U, + U, 'ERR_TLS_INVALID_PROTOCOL_METHOD'); +test(U, U, U, U, U, 'hokey-pokey', + U, U, 'ERR_TLS_INVALID_PROTOCOL_METHOD'); + +// Regression test: this should not crash because node should not pass the error +// message (including unsanitized user input) to a printf-like function. +test(U, U, U, U, U, '%s_method', + U, U, 'ERR_TLS_INVALID_PROTOCOL_METHOD'); + +// Cannot use secureProtocol and min/max versions simultaneously. +test(U, U, U, U, 'TLSv1.2', 'TLS1_2_method', + U, U, 'ERR_TLS_PROTOCOL_VERSION_CONFLICT'); +test(U, U, U, 'TLSv1.2', U, 'TLS1_2_method', + U, U, 'ERR_TLS_PROTOCOL_VERSION_CONFLICT'); +test(U, 'TLSv1.2', 'TLS1_2_method', U, U, U, + U, 'ERR_TLS_PROTOCOL_VERSION_CONFLICT'); +test('TLSv1.2', U, 'TLS1_2_method', U, U, U, + U, 'ERR_TLS_PROTOCOL_VERSION_CONFLICT'); + +// TLS_method means "any supported protocol". +test(U, U, 'TLSv1_2_method', U, U, 'TLS_method', 'TLSv1.2'); +test(U, U, 'TLSv1_1_method', U, U, 'TLS_method', 'TLSv1.1'); +test(U, U, 'TLSv1_method', U, U, 'TLS_method', 'TLSv1'); +test(U, U, 'TLS_method', U, U, 'TLSv1_2_method', 'TLSv1.2'); +test(U, U, 'TLS_method', U, U, 'TLSv1_1_method', 'TLSv1.1'); +test(U, U, 'TLS_method', U, U, 'TLSv1_method', 'TLSv1'); + +// OpenSSL 1.1.1 and 3.0 use a different error code and alert (sent to the +// client) when no protocols are enabled on the server. +const NO_PROTOCOLS_AVAILABLE_SERVER = hasOpenSSL3 ? + 'ERR_SSL_NO_PROTOCOLS_AVAILABLE' : 'ERR_SSL_INTERNAL_ERROR'; +const NO_PROTOCOLS_AVAILABLE_SERVER_ALERT = hasOpenSSL3 ? + 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION' : 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR'; + +// SSLv23 also means "any supported protocol" greater than the default +// minimum (which is configurable via command line). +if (DEFAULT_MIN_VERSION === 'TLSv1.3') { + test(U, U, 'TLSv1_2_method', U, U, 'SSLv23_method', + U, NO_PROTOCOLS_AVAILABLE_SERVER_ALERT, NO_PROTOCOLS_AVAILABLE_SERVER); +} else { + test(U, U, 'TLSv1_2_method', U, U, 'SSLv23_method', 'TLSv1.2'); +} + +if (DEFAULT_MIN_VERSION === 'TLSv1.3') { + test(U, U, 'TLSv1_1_method', U, U, 'SSLv23_method', + U, NO_PROTOCOLS_AVAILABLE_SERVER_ALERT, NO_PROTOCOLS_AVAILABLE_SERVER); + test(U, U, 'TLSv1_method', U, U, 'SSLv23_method', + U, NO_PROTOCOLS_AVAILABLE_SERVER_ALERT, NO_PROTOCOLS_AVAILABLE_SERVER); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_1_method', + U, 'ERR_SSL_NO_PROTOCOLS_AVAILABLE', 'ERR_SSL_UNEXPECTED_MESSAGE'); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_method', + U, 'ERR_SSL_NO_PROTOCOLS_AVAILABLE', 'ERR_SSL_UNEXPECTED_MESSAGE'); +} + +if (DEFAULT_MIN_VERSION === 'TLSv1.2') { + test(U, U, 'TLSv1_1_method', U, U, 'SSLv23_method', + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + test(U, U, 'TLSv1_method', U, U, 'SSLv23_method', + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + // test(U, U, 'SSLv23_method', U, U, 'TLSv1_1_method', + // U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); + // test(U, U, 'SSLv23_method', U, U, 'TLSv1_method', + // U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); +} + +if (DEFAULT_MIN_VERSION === 'TLSv1.1') { + test(U, U, 'TLSv1_1_method', U, U, 'SSLv23_method', 'TLSv1.1'); + test(U, U, 'TLSv1_method', U, U, 'SSLv23_method', + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_1_method', 'TLSv1.1'); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_method', + U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); +} + +if (DEFAULT_MIN_VERSION === 'TLSv1') { + test(U, U, 'TLSv1_1_method', U, U, 'SSLv23_method', 'TLSv1.1'); + test(U, U, 'TLSv1_method', U, U, 'SSLv23_method', 'TLSv1'); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_1_method', 'TLSv1.1'); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_method', 'TLSv1'); +} + +// TLSv1 thru TLSv1.2 are only supported with explicit configuration with API or +// CLI (--tls-v1.0 and --tls-v1.1). +test(U, U, 'TLSv1_2_method', U, U, 'TLSv1_2_method', 'TLSv1.2'); +test(U, U, 'TLSv1_1_method', U, U, 'TLSv1_1_method', 'TLSv1.1'); +test(U, U, 'TLSv1_method', U, U, 'TLSv1_method', 'TLSv1'); + +// The default default. +if (DEFAULT_MIN_VERSION === 'TLSv1.2') { + test(U, U, 'TLSv1_1_method', U, U, U, + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + test(U, U, 'TLSv1_method', U, U, U, + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + + if (DEFAULT_MAX_VERSION === 'TLSv1.2') { + test(U, U, U, U, U, 'TLSv1_1_method', + U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); + test(U, U, U, U, U, 'TLSv1_method', + U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); + } else { + // TLS1.3 client hellos are are not understood by TLS1.1 or below. + test(U, U, U, U, U, 'TLSv1_1_method', + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + test(U, U, U, U, U, 'TLSv1_method', + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + } +} + +// The default with --tls-v1.1. +if (DEFAULT_MIN_VERSION === 'TLSv1.1') { + test(U, U, 'TLSv1_1_method', U, U, U, 'TLSv1.1'); + test(U, U, 'TLSv1_method', U, U, U, + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + test(U, U, U, U, U, 'TLSv1_1_method', 'TLSv1.1'); + + if (DEFAULT_MAX_VERSION === 'TLSv1.2') { + test(U, U, U, U, U, 'TLSv1_method', + U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); + } else { + // TLS1.3 client hellos are are not understood by TLS1.1 or below. + test(U, U, U, U, U, 'TLSv1_method', + U, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + } +} + +// The default with --tls-v1.0. +if (DEFAULT_MIN_VERSION === 'TLSv1') { + test(U, U, 'TLSv1_1_method', U, U, U, 'TLSv1.1'); + test(U, U, 'TLSv1_method', U, U, U, 'TLSv1'); + test(U, U, U, U, U, 'TLSv1_1_method', 'TLSv1.1'); + test(U, U, U, U, U, 'TLSv1_method', 'TLSv1'); +} + +// TLS min/max are respected when set with no secureProtocol. +test('TLSv1', 'TLSv1.2', U, U, U, 'TLSv1_method', 'TLSv1'); +test('TLSv1', 'TLSv1.2', U, U, U, 'TLSv1_1_method', 'TLSv1.1'); +test('TLSv1', 'TLSv1.2', U, U, U, 'TLSv1_2_method', 'TLSv1.2'); +test('TLSv1', 'TLSv1.2', U, U, U, 'TLS_method', 'TLSv1.2'); + +test(U, U, 'TLSv1_method', 'TLSv1', 'TLSv1.2', U, 'TLSv1'); +test(U, U, 'TLSv1_1_method', 'TLSv1', 'TLSv1.2', U, 'TLSv1.1'); +test(U, U, 'TLSv1_2_method', 'TLSv1', 'TLSv1.2', U, 'TLSv1.2'); + +test('TLSv1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.2', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.2', U, 'TLSv1', 'TLSv1.1', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1', 'TLSv1.1', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1', U, 'TLSv1', 'TLSv1.1', U, 'TLSv1'); +test('TLSv1', 'TLSv1.2', U, 'TLSv1', 'TLSv1', U, 'TLSv1'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1', 'TLSv1', U, 'TLSv1'); +test('TLSv1.1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.2', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.2', U, 'TLSv1.1', 'TLSv1.1', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.2', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.2'); + +// v-any client can connect to v-specific server +test('TLSv1', 'TLSv1.3', U, 'TLSv1.3', 'TLSv1.3', U, 'TLSv1.3'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1.2', 'TLSv1.3', U, 'TLSv1.3'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1.2', 'TLSv1.2', U, 'TLSv1.2'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1.1', 'TLSv1.1', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1', 'TLSv1', U, 'TLSv1'); + +// v-specific client can connect to v-any server +test('TLSv1.3', 'TLSv1.3', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.3'); +test('TLSv1.2', 'TLSv1.2', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.2'); +test('TLSv1.1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1'); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-no-sslv23.js b/test/js/node/test/parallel/test-tls-no-sslv23.js new file mode 100644 index 00000000000000..b462885849b866 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-no-sslv23.js @@ -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' }); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-tls-ticket-invalid-arg.js b/test/js/node/test/parallel/test-tls-ticket-invalid-arg.js new file mode 100644 index 00000000000000..1977f7a4c225f5 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-ticket-invalid-arg.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const assert = require('assert'); +const tls = require('tls'); + +const server = new tls.Server(); + +[null, undefined, 0, 1, 1n, Symbol(), {}, [], true, false, '', () => {}] + .forEach((arg) => + assert.throws( + () => server.setTicketKeys(arg), + { code: 'ERR_INVALID_ARG_TYPE' } + )); + +[new Uint8Array(1), Buffer.from([1]), new DataView(new ArrayBuffer(2))].forEach( + (arg) => + assert.throws(() => { + server.setTicketKeys(arg); + }, /Session ticket keys must be a 48-byte buffer/) +); \ No newline at end of file diff --git a/test/js/node/tls/node-tls-reject-unauthorized-env.ts b/test/js/node/tls/node-tls-reject-unauthorized-env.ts new file mode 100644 index 00000000000000..dd7f68d0729023 --- /dev/null +++ b/test/js/node/tls/node-tls-reject-unauthorized-env.ts @@ -0,0 +1,5 @@ +import { describe, it } from "bun:test"; + +describe("Bun respects the NODE_TLS_REJECT_UNAUTHORIZED environment variables", () => { + it.todo("should reject unauthorized certificates by default"); +}); diff --git a/test/js/node/tls/tls-min-max-cli-args.test.ts b/test/js/node/tls/tls-min-max-cli-args.test.ts new file mode 100644 index 00000000000000..5faf7bfca85545 --- /dev/null +++ b/test/js/node/tls/tls-min-max-cli-args.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +const PRINT_MIN = ["-p", "tls.DEFAULT_MIN_VERSION"]; +const PRINT_MAX = ["-p", "tls.DEFAULT_MAX_VERSION"]; + +const TLS_VERSION_TO_SECUREVERSION: Record<`${number}.${number}`, import("tls").SecureVersion> = { + "1.0": "TLSv1", + "1.1": "TLSv1.1", + "1.2": "TLSv1.2", + "1.3": "TLSv1.3", +}; + +describe("TLS min/max CLI args", () => { + test.each(["1.0", "1.1", "1.2", "1.3"])("TLSv%s", async version => { + const child = Bun.spawn({ + cmd: [bunExe(), `--tls-min-v${version}`, ...PRINT_MIN], + stdio: ["pipe", "pipe", "pipe"], + env: bunEnv, + }); + + const stdout = await Bun.readableStreamToText(child.stdout); + + expect(stdout.trim()).toBe(TLS_VERSION_TO_SECUREVERSION[version]); + }); + + test.each(["1.2", "1.3"])("TLSv%s", async version => { + const child = Bun.spawn({ + cmd: [bunExe(), `--tls-max-v${version}`, ...PRINT_MAX], + stdio: ["pipe", "pipe", "pipe"], + env: bunEnv, + }); + + const stdout = await Bun.readableStreamToText(child.stdout); + + expect(stdout.trim()).toBe(`TLSv${version}`); + }); + + test("Specifying both min and max should exit with error code 1", async () => { + const child = Bun.spawn({ + cmd: [bunExe(), "--tls-min-v1.3", "--tls-max-v1.3"], + stdio: ["pipe", "pipe", "pipe"], + env: bunEnv, + }); + + const stderr = await Bun.readableStreamToText(child.stderr); + expect(stderr.trim()).toMatch(/not both/); + + expect(await child.exited).toBe(1); + }); + + test("Specifying multiple max flags should use the highest version", async () => { + // Node.js docs: + // If multiple of the options are provided, the highest maximum is used. + + const child = Bun.spawn({ + cmd: [bunExe(), "--tls-max-v1.3", "--tls-max-v1.2", ...PRINT_MAX], + stdio: ["pipe", "pipe", "pipe"], + env: bunEnv, + }); + + const stdout = await Bun.readableStreamToText(child.stdout); + expect(stdout.trim()).toBe("TLSv1.3"); + }); + + test("Specifying multiple min flags should use the lowest version", async () => { + // Node.js docs: + // If multiple of the options are provided, the lowest minimum is used. + + const child = Bun.spawn({ + cmd: [bunExe(), "--tls-min-v1.3", "--tls-min-v1.2", ...PRINT_MIN], + stdio: ["pipe", "pipe", "pipe"], + env: bunEnv, + }); + + const stdout = await Bun.readableStreamToText(child.stdout); + expect(stdout.trim()).toBe("TLSv1.2"); + }); + + test("invalid min/max vals should do nothing since the flags don't exist in the CLI parser", async () => { + const { DEFAULT_MAX_VERSION, DEFAULT_MIN_VERSION } = await import("tls"); + + { + const child = Bun.spawn({ + cmd: [bunExe(), "--tls-max-v1.9999", ...PRINT_MAX], + env: bunEnv, + }); + + const stdout = await Bun.readableStreamToText(child.stdout); + expect(stdout.trim()).toBe(DEFAULT_MAX_VERSION); + } + + { + const child = Bun.spawn({ + cmd: [bunExe(), "--tls-min-v1.9999", ...PRINT_MIN], + env: bunEnv, + }); + + const stdout = await Bun.readableStreamToText(child.stdout); + expect(stdout.trim()).toBe(DEFAULT_MIN_VERSION); + } + }); +});