diff --git a/spec/std/openssl/ssl/socket_spec.cr b/spec/std/openssl/ssl/socket_spec.cr index 1b667532793d..df3a34a61135 100644 --- a/spec/std/openssl/ssl/socket_spec.cr +++ b/spec/std/openssl/ssl/socket_spec.cr @@ -36,9 +36,32 @@ describe OpenSSL::SSL::Socket do end end + it "accepts clients that only write then close the connection" do + tcp_server = TCPServer.new(0) + server_context, client_context = ssl_context_pair + # in tls 1.3, if clients don't read anything and close the connection + # the server still try and write to it a ticket, resulting in a "pipe failure" + # this context method disables the tickets which allows the behavior: + server_context.disable_session_resume_tickets + + OpenSSL::SSL::Server.open(tcp_server, server_context) do |server| + spawn do + # the :sync_close aspect, as implemented in crystal, effects a unidirectional socket close from the client + OpenSSL::SSL::Socket::Client.open(TCPSocket.new(tcp_server.local_address.address, tcp_server.local_address.port), client_context, hostname: "example.com", sync_close: true) do |socket| + # doesn't read anything, just write and close connection immediately + socket.puts "hello" + end + end + + client = server.accept # shouldn't raise "Broken pipe (Errno)" + client.close + end + end + it "closes connection to server that doesn't properly terminate SSL session" do tcp_server = TCPServer.new(0) server_context, client_context = ssl_context_pair + server_context.disable_session_resume_tickets # avoid Broken pipe client_successfully_closed_socket = Channel(Nil).new spawn do diff --git a/src/openssl/lib_ssl.cr b/src/openssl/lib_ssl.cr index f663c3ea418d..455e83c41ab5 100644 --- a/src/openssl/lib_ssl.cr +++ b/src/openssl/lib_ssl.cr @@ -206,6 +206,12 @@ lib LibSSL # Hostname validation for OpenSSL <= 1.0.1 fun ssl_ctx_set_cert_verify_callback = SSL_CTX_set_cert_verify_callback(ctx : SSLContext, callback : CertVerifyCallback, arg : Void*) + # control TLS 1.3 session ticket generation + {% if compare_versions(OPENSSL_VERSION, "1.1.1") >= 0 %} + fun ssl_ctx_set_num_tickets = SSL_CTX_set_num_tickets(ctx : SSLContext, larg : LibC::SizeT) : Int + fun ssl_set_num_tickets = SSL_set_num_tickets(ctx : SSL, larg : LibC::SizeT) : Int + {% end %} + {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} fun tls_method = TLS_method : SSLMethod {% else %} diff --git a/src/openssl/ssl/context.cr b/src/openssl/ssl/context.cr index cad1934b67aa..68e497c5eab8 100644 --- a/src/openssl/ssl/context.cr +++ b/src/openssl/ssl/context.cr @@ -191,6 +191,20 @@ abstract class OpenSSL::SSL::Context def self.from_hash(params) : self super(params) end + + # Disables all session ticket generation for this context. + # Tickets are used to resume earlier sessions more quickly, + # but in TLS 1.3 if the client connects, sends data, and closes the connection + # unidirectionally, the server connects, then sends a ticket + # after the connect handshake, the ticket send can fail with Broken Pipe. + # So if you have that kind of behavior (clients that never read) call this method. + def disable_session_resume_tickets + add_options(OpenSSL::SSL::Options::NO_TICKET) # TLS v1.2 and below + {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.1.1") >= 0 %} + ret = LibSSL.ssl_ctx_set_num_tickets(self, 0) # TLS v1.3 + raise OpenSSL::Error.new("SSL_CTX_set_num_tickets") if ret != 1 + {% end %} + end end protected def initialize(method : LibSSL::SSLMethod)