Search code examples
pythonsslmultiprocessing

TLS version mismach between curl and server. How I can accept multiple tls versions?


I made the following servers:

import socket
import threading
import queue
import time
import ssl
import multiprocessing

class SocketServer:
    """
        Basic Socket Server in python
    """

    def __init__(self,host,port,max_threads):
        print("Create Server For Http")
        self.host = host
        self.port = port
        self.server_socket = self.__initSocket()
        self.max_threads = max_threads
        self.request_queue = queue.Queue()        

    def __initSocket(self):
        return socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def __postProcessSocket(self,client_socket):
        return client_socket

    def __accept(self):
        self.server_socket.listen(5)
        while True:
            client_socket, client_address = self.server_socket.accept()
            actual_client_socket = self.__postProcessSocket(client_socket)
            self.request_queue.put((actual_client_socket, client_address))


    def __handle(self):
        while True:
            # Dequeue a request and process it
            client_socket, address = self.request_queue.get()    
            print(address)
            # Read HTTP Request
            # Log Http Request
            # Manipulate Http Request
            # Forward or respond

            content = '<html><body>Hello World</body></html>\r\n'.encode()
            headers = f'HTTP/1.1 200 OK\r\nContent-Length: {len(content)}\r\nContent-Type: text/html\r\n\r\n'.encode()
            client_socket.sendall(headers + content)
            client_socket.close()
            self.request_queue.task_done()


    def __initThreads(self):
        for _ in range(self.max_threads):
            threading.Thread(target=self.__handle, daemon=True).start()


    def start(self):
        self.server_socket.bind((self.host, self.port))
        self.__initThreads()
        self.__accept()


class TLSSocketServer(SocketServer):
    def __init__(self,host,port,max_threads):
        super().__init__(host,port,max_threads)
        self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    
    def __postProcessSocket(self,client_socket):
        # @todo load SNI
        self.context.load_cert_chain(certfile="/etc/certs/cert.crt", keyfile="/etc/certs/key.key")
        return self.context.wrap_socker(client_socket,server=true)

if __name__ == "__main__":

    # @todo read settings file
    host = "0.0.0.0"
    port = 80
    tls_port=443
    max_threads = 5
    
    server = SocketServer(host, port, max_threads)
    server_process = multiprocessing.Process(target=server.start)
    server_process.start()

    # Add other main application code here if needed
    tls_server = TLSSocketServer(host, tls_port, max_threads)
    tls_server_process = multiprocessing.Process(target=tls_server.start)
    tls_server_process.start()

    tls_server_process.join()

And ad you can see I am running them into multiple processes in python. But once I do:

$  curl -vvv https://10.0.0.2

I get this error:


*   Trying 10.0.0.2:443...
* Connected to 10.0.0.2 (10.0.0.2) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* (5454) (IN), , Unknown (72):
* error:0A00010B:SSL routines::wrong version number
* Closing connection 0
curl: (35) error:0A00010B:SSL routines::wrong version number

How I can make accept any tls version. I am doing this as a local debug/analysis tool instead of something that will run remotely. Therefore, I want to use any available tls version, but the latest to be prefered if possible.

How I can do this?

If I cannot do how I can fix this error? I also tried with firefox and I get: SSL_ERROR_RX_RECORD_TOO_LONG

In order to mitigate the issue I also tried fruitlessly:

class TLSSocketServer(SocketServer):
    def __initSocket(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        print("Load TLS")
        context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
        context.load_cert_chain(certfile='/etc/certs/cert.crt', keyfile='/etc/certs/key.key')
        return context.wrap_socket(sock, server_side=True)

Furthermore, I tried:

class TLSSocketServer(SocketServer):
    def __initSocket(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        print("Load TLS")
        context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        context.load_cert_chain(certfile='/etc/certs/cert.crt', keyfile='/etc/certs/key.key')
        return context.wrap_socket(sock, server_side=True)

And tried to enforce TLS 1.3 but I still got the error:

$ curl -vvv -k --tlsv1.3 https://10.0.0.2
*   Trying 10.0.0.2:443...
* Connected to 10.0.0.2 (10.0.0.2) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* (5454) (IN), , Unknown (72):
* error:0A00010B:SSL routines::wrong version number
* Closing connection 0
curl: (35) error:0A00010B:SSL routines::wrong version number

Solution

  • This is not an actual TLS protocol version problem even if it confusingly looks like one. Instead the seemingly clear error messages comes from the server not sending a TLS response in the first place but instead a plain response. And this plain response is then interpreted as TLS and it tries to interpret bytes 1 and 2 of the incoming data as TLS version based on how the TLS record layer is defined.

    The reason why no TLS is send comes from the wrong use of private methods in the code, i.e. methods starting with __. From the documentation:

    Since there is a valid use-case for class-private members (namely to avoid name clashes of names with names defined by subclasses), there is limited support for such a mechanism, called name mangling. Any identifier of the form __spam (at least two leading underscores, at most one trailing underscore) is textually replaced with _classname__spam, where classname is the current class name with leading underscore(s) stripped.

    This is true not only for variables but also for methods.

    This means what is actually called is not the intended TLSSocketServer.__postProcessSocket but instead SocketServer.__postProcessSocket using the name _SocketServer__postProcessSocket.

    To fix this stop using private methods, i.e. rename the method to a public one. This means using postProcessSocket instead of __postProcessSocket. Once this is done one will realize that there are several errors in this function, which were never not noticed before because the code was never called until now. The correct code should be:

        def postProcessSocket(self,client_socket):
            return client_socket
    
        def __accept(self):
            self.server_socket.listen(5)
            while True:
                client_socket, client_address = self.server_socket.accept()
                actual_client_socket = self.postProcessSocket(client_socket)
        ...
    class TLSSocketServer(SocketServer):
        ...
        def postProcessSocket(self,client_socket):
            # @todo load SNI
            self.context.load_cert_chain(certfile="server.pem", keyfile="key.pem")
            return self.context.wrap_socket(client_socket,server_side=True)