pythonssl

Change TLS version used by Python


I want to connect with a service whose owner told me I need to connect with TLS 1.2.

The problem is that my Python uses TLS 1.3 I checked it with this command python -c "import requests; print(requests.get('https://www.howsmyssl.com/a/check', verify=False).json()['tls_version'])"

Is it possible to downgrade TLS to 1.2?


Solution

  • Well,

    First

    You should know ssl or today called tls, is a handshake "agree each other" process, at first open of the TCP socket, then the communication through this socket will be hiddenly encrypted or you can sniff with wireshark off course, because of a "wrapping" of the socket which is the normal method used when using python or other languages i mean just an intermediate class doing the encription for you and you don't even take care, finally the shared encripted-suite used between client and server was previously "agreed" on this mentioned handshake process.

    Second

    HTTP is just another application layer over TCP but the real test will be always over Transport layer and i'll share my own tls/ssl program-tester on the next lines to mitigate theory flaws.

    You can run it like python3.x.exe program.py -ip www.whatever.com -puerto 443 BUT you should put two files next to this program.py, the local.crt and local.key files as defaults for my program, however you can always specify them with option -tls ../location/other.crt ../location/other.key

    The reasons for local.crt and local.key are basically the handshake is a process of an interchange of your keys (local.xxx files) and the remote server files ALSO, SO in my next tester example, i have always custom ssl context variable based on those local.xxx files, please note use of .load_cert_chain on it

    Anyways you can generate them very easily using openssl command like this openssl req -newkey rsa:2048 -new -x509 -nodes -days 3650 -keyout llave.key -out cert.crt -subj "/C=CO/ST=ANT/L=Medellin/O=YourCompany/OU=YourArea/CN=YourDevice/emailAddress=joseveland@gmail.com" or a website similar to https://www.ssl.com/online-csr-and-key-generator/ just save them on separate .crt and .key file of yours

    import os, platform, socket, ssl
    if platform.system().lower() == 'linux':    # Linux OS ..
        windows = False
        slash_principal = '/'
        slash_secundario = '\\'
        limpiar_pantalla = lambda :os.system('clear')
    else:   # Windows OS ..
        windows = True
        slash_principal = '\\'
        slash_secundario = '/'
        limpiar_pantalla = lambda :os.system('cls')
    
    import argparse, errno
    limpiar_pantalla()
    filepath = os.path.dirname(os.path.abspath(__file__))
    ssl_files_def = [filepath+slash_principal+'local.crt', filepath+slash_principal+'local.key']
    
    parser = argparse.ArgumentParser( description="Probador de TLS")
    parser.add_argument( '-ip', nargs=1, metavar='IP', default=['www.google.com'], type=str, help=' IP del socket TCP a probar')    # 34.196.130.67 EPSA Pruebas
    parser.add_argument( '-puerto', nargs=1, metavar='PUERTO', default=[443], type=int, help=' Puerto del socket TCP a probar')
    parser.add_argument( '-version', nargs=1, metavar='TLS_VERSION', type=int, choices=[0,1,2,3], help=' Version TLS a probar si no se especifica se hara AUTO')
    parser.add_argument( '-tls', nargs=2, metavar=('CRT', 'KEY'), default=ssl_files_def, type=str, help=' Ruta al archivo ".crt" ..y.. ruta al archivo ".key" locales, para ejecutar el handshake/intercambio TLS con alguien remoto')
    parser_opts = parser.parse_args()
    #print("Argumentos -->", parser_opts._get_kwargs())
    #print()
    
    
    # ---------------- Logica ---------------- #
    
    sock_pair = ( parser_opts.ip[0], parser_opts.puerto[0] )
    sock_tls_ver = ssl.PROTOCOL_TLS     # Auto
    if parser_opts.version:             # Se ingreso en el parse? (!= None) ya que no es obligatoria
        if parser_opts.version[0] == 0:
            sock_tls_ver = ssl.PROTOCOL_TLSv1
        elif parser_opts.version[0] == 1:
            sock_tls_ver = ssl.PROTOCOL_TLSv1_1
        elif parser_opts.version[0] == 2:
            sock_tls_ver = ssl.PROTOCOL_TLSv1_2
        #elif parser_opts.version[0] == 3:
        #   sock_tls_ver = ssl.PROTOCOL_TLSv1_3
    
    sslCntx = ssl.SSLContext(sock_tls_ver)  # https://docs.python.org/3/library/ssl.html#ssl.SSLContext
    # Con las siguientes opciones se evitan suites SSL inseguras (Al final solo permitira >= TLSv1..)
    sslCntx.options |= ssl.OP_NO_SSLv2
    sslCntx.options |= ssl.OP_NO_SSLv3
    if parser_opts.version:             # Se ingreso en el parse? (!= None) ya que no es obligatoria
        if parser_opts.version[0] >= 1:
            sslCntx.options |= ssl.OP_NO_TLSv1      # Evite la version 0 de TLS al conectar/handshake (version >= v1.1) 
        if parser_opts.version[0] >= 2:
            sslCntx.options |= ssl.OP_NO_TLSv1_1    # Evite la version 1 de TLS al conectar/handshake (version >= v1.2) 
        if parser_opts.version[0] >= 3:
            sslCntx.options |= ssl.OP_NO_TLSv1_2    # Evite la version 2 de TLS al conectar/handshake (version >= v1.3)
    sslCntx.load_cert_chain(*ssl_files_def)         # Finalmente cargue mis llaves con las que hare el handshake.
    
    print()
    print('Versiones SSL (Obsoleto, demostrativo):', int(ssl.PROTOCOL_SSLv23))  # OBSOLETO HACE RATO.
    print('Versiones TLS:', int(ssl.PROTOCOL_TLSv1), int(ssl.PROTOCOL_TLSv1_1), int(ssl.PROTOCOL_TLSv1_2))#, ssl.PROTOCOL_TLSv1_3)
    print('Mi Contexto:', sslCntx.options, int(sslCntx.minimum_version), int(sslCntx.maximum_version), sslCntx.verify_flags, sslCntx.verify_mode, sslCntx.get_ca_certs())#, sslCntx.get_ciphers(), dir(sslCntx))
    print('Remoto:', sock_pair)
    print()
    
    s = socket.socket()
    s_tls = sslCntx.wrap_socket(s)  # Wrap lo convierte en un socket que hara handshake TLS y la comunicacion sera encriptada por ello.
    try:
        print('Socket Ok:', s_tls.version(), s_tls.session, s_tls.shared_ciphers() )#, dir(s_tls))
        print()
        s_tls.connect(sock_pair)
        
        print('Conexion Ok:', s_tls.version(), s_tls.session.timeout)
        print( s_tls.shared_ciphers() )#, dir(s_tls.session), dir(s_tls))
        print()
        s_tls.close()
    
    except socket.error as e:
        print('Socket FALLO (', os.strerror(e.errno), ')')
    
    except Exception as e:
        print('Conexion FALLO:', type(e).__name__, e)
    
    print()
    

    NOTE: this program can also be used to test whatever socket/page you want with specify tls version as an option -version 0, -version 1, -version 2, -version 3 to force a specific TLS version and is useful to detect downgradable (Insecure) servers

    Third

    Concepts clear, if you already want to test a TLS through application-layer python modules, you would want to put or specify this custom sslCntx (ssl context) variable of the previous code over this desired module so it can vary depending on the documentation of it.

    I mean replace this ...

    s = socket.socket()
    s_tls = sslCntx.wrap_socket(s)  # Wrap lo convierte en un socket que hara handshake TLS y la comunicacion sera encriptada por ello.
    try:
        print('Socket Ok:', s_tls.session, s_tls.version(), s_tls.shared_ciphers(), s_tls )#, dir(s_tls))
        print()
        s_tls.connect(sock_pair)
        
        print('Conexion Ok:', s_tls.session.timeout, s_tls.version(), s_tls.shared_ciphers(), s_tls )#, dir(s_tls.session), dir(s_tls))
        print()
        s_tls.close()
    

    ... With the specifics of your desired module, for example on http module will be the argument "context" like this ...

    from http.client import HTTPSConnection
    try:
        conn = HTTPSConnection(*sock_pair, context=sslCntx)
        print('HTTP Ok:', conn.host, conn.port)#, dir(conn))
        print()
        conn.request( 'GET', '/' )
        ans = conn.getresponse()
        print('GET', ans.reason, ':', ans.status)
        print(ans.headers)#, dir(ans))
        conn.close()
        print()
    

    And run it like python3.x.exe program.py -ip www.whatever.com -puerto 443 -version 2 because on the question you asked to force the use of TLSv1.2

    Fourth

    On the other hand when NOT using -version option, the default python parameter "sock_tls_ver = ssl.PROTOCOL_TLS" will do the job for you like it should try to connect on all tls versions, so if your remote server only support TLSv1.2 even if your python is compiled with TLSv1.3 it will use TLSv1.2 and will connect as expected (Downgrading to TLS1.2 with no troubles).

    You can check you have support of TLS version printing the booleans ...

    import ssl
    print(ssl.HAS_TLSv1)
    print(ssl.HAS_TLSv1_1)
    print(ssl.HAS_TLSv1_2)
    print(ssl.HAS_TLSv1_3)
    

    Hope this helps to demistify SSL/TLS is just a little process (to interchange security concerns between client and server) before any socket valuable data (like http requests) is send.

    By the way SSL changed name to TLS and was hackable over the years and that's why we have so many versions (So internet is not as secure as you think)