Search code examples
pythonsocketsssltls1.2

How to get the TLS client supported TLS versions in python ssl


In python ssl, one can configure the TLS client's ciphersuites and versions. The ciphersuites are set using context.set_ciphers(ciphers) and the versions using context.options.

To make sure from the setup, one can get the ciphers in a client (even before the handshake, this is for setting up the client) using context.get_ciphers().

My question: how can I get the client's supported protocols. Please note that I am not using the default versions. I changed them by excluding some versions using context.options. For example, this statement excludes TLS 1.1 from my client:

context.options |= ssl.OP_NO_TLSv1_1

I want to make sure form my client TLS versions in the same way as I did in the ciphers using context.get_ciphers(). Is there any way I can do so?


Solution

  • The functionality you're after is available (at least partially) in Python 3.6 (and newer). Check [Python.Docs]: ssl - TLS/SSL wrapper for socket objects for more details:

    >>> import ssl
    >>> import sys
    >>>
    >>> "Python {:s} on {:s}".format(sys.version, sys.platform)
    'Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 17:00:18) [MSC v.1900 64 bit (AMD64)] on win32'
    >>> ctx0 = ssl.create_default_context()
    >>> ctx0.options
    <Options.OP_NO_SSLv3|OP_NO_SSLv2|OP_CIPHER_SERVER_PREFERENCE|OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|OP_NO_COMPRESSION|OP_ALL: -2091252737>
    

    For older versions, some code (which also works on newer ones) is required.

    code00.py:

    #!/usr/bin/env python
    
    import ssl
    import sys
    from pprint import pprint as pp
    
    
    __PROTO_TAG = "PROTOCOL_"
    __OP_NO_TAG = "OP_NO_"
    __OP_NO_TAG_LEN = len(__OP_NO_TAG)
    _PROTOS_DATA = list()
    for item_name in dir(ssl):
        if item_name.startswith(__OP_NO_TAG) and item_name[-1].isdigit():
            op_no_item = getattr(ssl, item_name)
            if op_no_item:
                proto_name = item_name[__OP_NO_TAG_LEN:]
                _PROTOS_DATA.append((proto_name, getattr(ssl, __PROTO_TAG + proto_name, -1), op_no_item))
    del __OP_NO_TAG_LEN
    del __OP_NO_TAG
    del __PROTO_TAG
    
    
    def get_protocols(ctx):
        supported_classes = (ssl.SSLContext,)
        if not isinstance(ctx, supported_classes):
            raise TypeError("Argument must be an instance of `{:}`".format(supported_classes[0] if len(supported_classes) == 1 else supported_classes))
        protocols = list()
        for proto_data in _PROTOS_DATA:
            if ctx.options & proto_data[-1] != proto_data[-1]:
                protocols.append(proto_data[:-1])
        return protocols
    
    
    def print_data(ctx):
        print("Options: {:08X} ({!r})".format(ctx.options, ctx.options))
        print("Protocols:")
        for proto in get_protocols(ctx):
            print("    {:s} - {:d}".format(*proto))
        print()
    
    
    def main(*argv):
        print("{:s}\n".format(ssl.OPENSSL_VERSION))
        ctx0 = ssl.create_default_context()
        print_data(ctx0)
        print("--- Removing TLSv1_1...")
        ctx0.options |= ssl.OP_NO_TLSv1_1
        print_data(ctx0)
        print("--- Adding SSLv3...")
        ctx0.options -= ssl.OP_NO_SSLv3  # !!! N.B.: Due to the fact that ssl.OP_NO_* flags only have one bit set, this works, but DON'T DO IT !!!
        print_data(ctx0)
        print("\nComputed protocols:")
        pp([item[:-1] + (hex(item[-1]),) for item in _PROTOS_DATA])
    
    
    if __name__ == "__main__":
        print(
            "Python {:s} {:03d}bit on {:s}\n".format(
                " ".join(elem.strip() for elem in sys.version.split("\n")),
                64 if sys.maxsize > 0x100000000 else 32,
                sys.platform,
            )
        )
        rc = main(*sys.argv[1:])
        print("\nDone.\n")
        sys.exit(rc)
    

    Notes:

    • As I worked extensively in this area, I have lots of Python versions on a variety of OSes, built against various OpenSSL versions (as seen in the outputs below)

    • Tried to keep everything as general as possible

    • Basing the code on (ssl) module attributes only; due to the fact that each Python version is built with a particular OpenSSL version, surprises might arise when using custom combinations (I could hardcode the OP_NO_* constants - which are consistent over OpenSSL versions, but that wouldn't be scalable)

    • There is the ssl module implementation (specific to Python version, which relies on a specific OpenSSL version - as stated above), plus the OpenSSL version (which might might not have some stuff) actually used to build the ssl module. That's why running the same code on various combinations, yields (slightly) different results (check outputs below)

    • On Win things are simpler, as (by default) OpenSSL is statically linked in _ssl.pyd (starting with Python 3.7, this no longer applies, the OpenSSL .dlls are also shipped as part of Python), but on Nix, the OpenSSL libs (that are installed on the system) are loaded at runtime

    • Code walkthrough:

      • _PROTOS_DATA - computed at module import time: it's a list of protocol entries, based on ssl module attributes. Each entry has 3 fields:

        • Protocol name

        • Protocol identifier in the ssl module (-1 if not present: e.g. SSLv2)

        • The OP_NO_ constant used to disable this protocol

        It's displayed at the end of each run for clarity

      • get_protocols - determines the "active" supported protocols for an ssl.SSLContext

      • print_data - helper function

    Output:

    • Win 10 pc064

      [cfati@cfati-5510-0:e:\Work\Dev\StackOverflow\q049788677]> "C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python36_64\python.exe" ./code00.py
      Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 17:00:18) [MSC v.1900 64 bit (AMD64)] 064bit on win32
      
      OpenSSL 1.0.2k  26 Jan 2017
      
      Options: -7CA5FC01 (<Options.OP_NO_SSLv3|OP_NO_SSLv2|OP_CIPHER_SERVER_PREFERENCE|OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|OP_NO_COMPRESSION|OP_ALL: -2091252737>)
      Protocols:
          TLSv1 - 3
          TLSv1_1 - 4
          TLSv1_2 - 5
      
      --- Removing TLSv1_1...
      Options: -6CA5FC01 (<Options.OP_NO_TLSv1_1|OP_NO_SSLv3|OP_NO_SSLv2|OP_CIPHER_SERVER_PREFERENCE|OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|OP_NO_COMPRESSION|OP_ALL: -1822817281>)
      Protocols:
          TLSv1 - 3
          TLSv1_2 - 5
      
      --- Adding SSLv3...
      Options: -6EA5FC01 (<Options.OP_NO_TLSv1_1|OP_NO_SSLv2|OP_CIPHER_SERVER_PREFERENCE|OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|OP_NO_COMPRESSION|OP_ALL: -1856371713>)
      Protocols:
          SSLv3 - 1
          TLSv1 - 3
          TLSv1_2 - 5
      
      
      Computed protocols:
      [('SSLv2', -1, '0x1000000'),
       ('SSLv3', <_SSLMethod.PROTOCOL_SSLv3: 1>, '0x2000000'),
       ('TLSv1', <_SSLMethod.PROTOCOL_TLSv1: 3>, '0x4000000'),
       ('TLSv1_1', <_SSLMethod.PROTOCOL_TLSv1_1: 4>, '0x10000000'),
       ('TLSv1_2', <_SSLMethod.PROTOCOL_TLSv1_2: 5>, '0x8000000')]
      
      Done.
      
      
      [cfati@cfati-5510-0:e:\Work\Dev\StackOverflow\q049788677]>
      [cfati@cfati-5510-0:e:\Work\Dev\StackOverflow\q049788677]> "e:\Work\Dev\VEnvs\py34x64_test\Scripts\python.exe" ./code00.py
      Python 3.4.4 (v3.4.4:737efcadf5a6, Dec 20 2015, 20:20:57) [MSC v.1600 64 bit (AMD64)] 064bit on win32
      
      OpenSSL 1.0.2d 9 Jul 2015
      
      Options: -7CFDFC01 (-2097019905)
      Protocols:
          TLSv1 - 3
          TLSv1_1 - 4
          TLSv1_2 - 5
      
      --- Removing TLSv1_1...
      Options: -6CFDFC01 (-1828584449)
      Protocols:
          TLSv1 - 3
          TLSv1_2 - 5
      
      --- Adding SSLv3...
      Options: -6EFDFC01 (-1862138881)
      Protocols:
          SSLv3 - 1
          TLSv1 - 3
          TLSv1_2 - 5
      
      
      Computed protocols:
      [('SSLv2', 0, '0x1000000'),
       ('SSLv3', 1, '0x2000000'),
       ('TLSv1', 3, '0x4000000'),
       ('TLSv1_1', 4, '0x10000000'),
       ('TLSv1_2', 5, '0x8000000')]
      
      Done.
      
      
      [cfati@cfati-5510-0:e:\Work\Dev\StackOverflow\q049788677]>
      [cfati@cfati-5510-0:e:\Work\Dev\StackOverflow\q049788677]> "c:\Install\x64\Python\Python\3.7\python.exe" ./code00.py
      Python 3.7.0b4 (v3.7.0b4:eb96c37699, May  2 2018, 19:02:22) [MSC v.1913 64 bit (AMD64)] 064bit on win32
      
      OpenSSL 1.1.0h  27 Mar 2018
      
      Options: -7DBDFFAC (<Options.OP_NO_SSLv3|OP_CIPHER_SERVER_PREFERENCE|OP_NO_COMPRESSION|OP_ALL: -2109603756>)
      Protocols:
          TLSv1 - 3
          TLSv1_1 - 4
          TLSv1_2 - 5
      
      --- Removing TLSv1_1...
      Options: -6DBDFFAC (<Options.OP_NO_TLSv1_1|OP_NO_SSLv3|OP_CIPHER_SERVER_PREFERENCE|OP_NO_COMPRESSION|OP_ALL: -1841168300>)
      Protocols:
          TLSv1 - 3
          TLSv1_2 - 5
      
       --- Adding SSLv3...
      Options: -6FBDFFAC (<Options.OP_NO_TLSv1_1|OP_CIPHER_SERVER_PREFERENCE|OP_NO_COMPRESSION|OP_ALL: -1874722732>)
      Protocols:
          SSLv3 - -1
          TLSv1 - 3
          TLSv1_2 - 5
      
      
      Computed protocols:
      [('SSLv3', -1, '0x2000000'),
       ('TLSv1', <_SSLMethod.PROTOCOL_TLSv1: 3>, '0x4000000'),
       ('TLSv1_1', <_SSLMethod.PROTOCOL_TLSv1_1: 4>, '0x10000000'),
       ('TLSv1_2', <_SSLMethod.PROTOCOL_TLSv1_2: 5>, '0x8000000')]
      
      Done.
      
    • OSX 9 pc064:

      [cfati@cfati-macosx9x64-1:~/Work/Dev/StackOverflow/q049788677]> python ./code00.py
      Python 2.7.10 (default, Oct 14 2015, 05:51:29)
      [GCC 4.8.2] 064bit on darwin
      
      OpenSSL 1.0.1p-fips 9 Jul 2015
      
      Options: 830203FF (2197947391L)
      Protocols:
          TLSv1 - 3
          TLSv1_1 - 4
          TLSv1_2 - 5
      ()
      --- Removing TLSv1_1...
      Options: 930203FF (2466382847L)
      Protocols:
          TLSv1 - 3
          TLSv1_2 - 5
      ()
      --- Adding SSLv3...
      Options: 910203FF (2432828415L)
      Protocols:
          SSLv3 - 1
          TLSv1 - 3
          TLSv1_2 - 5
      ()
      
      Computed protocols:
      [('SSLv2', 0, '0x1000000'),
       ('SSLv3', 1, '0x2000000'),
       ('TLSv1', 3, '0x4000000'),
       ('TLSv1_1', 4, '0x10000000'),
       ('TLSv1_2', 5, '0x8000000')]
      
      Done.
      
    • Ubuntu:

      • 16 pc064:

        [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q049788677]> python3 ./code00.py
        Python 3.5.2 (default, Nov 23 2017, 16:37:01)
        [GCC 5.4.0 20160609] pc064 on linux
        
        OpenSSL 1.0.2g  1 Mar 2016
        
        Options: 830203FF (2197947391)
        Protocols:
            TLSv1 - 3
            TLSv1_1 - 4
            TLSv1_2 - 5
        
        --- Removing TLSv1_1...
        Options: 930203FF (2466382847)
        Protocols:
            TLSv1 - 3
            TLSv1_2 - 5
        
        --- Adding SSLv3...
        Options: 930203FF (2466382847)
        Protocols:
            TLSv1 - 3
            TLSv1_2 - 5
        
        
        Computed protocols:
        [('SSLv2', -1, '0x1000000'),
         ('SSLv3', -1, '0x2000000'),
         ('TLSv1', <_SSLMethod.PROTOCOL_TLSv1: 3>, '0x4000000'),
         ('TLSv1_1', <_SSLMethod.PROTOCOL_TLSv1_1: 4>, '0x10000000'),
         ('TLSv1_2', <_SSLMethod.PROTOCOL_TLSv1_2: 5>, '0x8000000')]
        
        Done.
        
        [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q049788677]>
        [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q049788677]> LD_LIBRARY_PATH=../q049493537/Python-3.6.4:../q049320993/ssl/build/lib ../q049493537/Python-3.6.4/python ./code00.py
        Python 3.6.4 (default, Mar 28 2018, 23:34:25)
        [GCC 5.4.0 20160609] pc064 on linux
        
        OpenSSL 1.0.2h-fips  3 May 2016
        
        Options: 835A03FF (<Options.OP_ALL|OP_NO_SSLv3|OP_NO_SSLv2|OP_CIPHER_SERVER_PREFERENCE|OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|OP_NO_COMPRESSION: 2203714559>)
        Protocols:
            TLSv1 - 3
            TLSv1_1 - 4
            TLSv1_2 - 5
        
        --- Removing TLSv1_1...
        Options: 935A03FF (<Options.OP_ALL|OP_NO_TLSv1_1|OP_NO_SSLv3|OP_NO_SSLv2|OP_CIPHER_SERVER_PREFERENCE|OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|OP_NO_COMPRESSION: 2472150015>)
        Protocols:
            TLSv1 - 3
            TLSv1_2 - 5
        
        --- Adding SSLv3...
        Options: 915A03FF (<Options.OP_ALL|OP_NO_TLSv1_1|OP_NO_SSLv2|OP_CIPHER_SERVER_PREFERENCE|OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|OP_NO_COMPRESSION: 2438595583>)
        Protocols:
            SSLv3 - -1
            TLSv1 - 3
            TLSv1_2 - 5
        
        
        Computed protocols:
        [('SSLv2', -1, '0x1000000'),
         ('SSLv3', -1, '0x2000000'),
         ('TLSv1', <_SSLMethod.PROTOCOL_TLSv1: 3>, '0x4000000'),
         ('TLSv1_1', <_SSLMethod.PROTOCOL_TLSv1_1: 4>, '0x10000000'),
         ('TLSv1_2', <_SSLMethod.PROTOCOL_TLSv1_2: 5>, '0x8000000')]
        
        Done.
        
      • 22 pc064:

        (qaic-env) [cfati@cfati-5510-0:/mnt/e/Work/Dev/StackExchange/StackOverflow/q049788677]> LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/usr/local/pc064/openssl/openssl/3.0.14/lib python ./code00.py
        Python 3.8.19 (default, Apr  6 2024, 17:58:10) [GCC 11.4.0] 064bit on linux
        
        OpenSSL 3.0.14 4 Jun 2024
        
        Options: 825200D0 (<Options.OP_ALL|OP_NO_SSLv3|OP_CIPHER_SERVER_PREFERENCE|OP_ENABLE_MIDDLEBOX_COMPAT|OP_NO_COMPRESSION|OP_IGNORE_UNEXPECTED_EOF: 2186412240>)
        Protocols:
            TLSv1 - 3
            TLSv1_1 - 4
            TLSv1_2 - 5
            TLSv1_3 - -1
        
        --- Removing TLSv1_1...
        Options: 925200D0 (<Options.OP_ALL|OP_NO_TLSv1_1|OP_NO_SSLv3|OP_CIPHER_SERVER_PREFERENCE|OP_ENABLE_MIDDLEBOX_COMPAT|OP_NO_COMPRESSION|OP_IGNORE_UNEXPECTED_EOF:         >    2454847696>)
        Protocols:
            TLSv1 - 3
            TLSv1_2 - 5
            TLSv1_3 - -1
        
        --- Adding SSLv3...
        Options: 905200D0 (<Options.OP_ALL|OP_NO_TLSv1_1|OP_CIPHER_SERVER_PREFERENCE|OP_ENABLE_MIDDLEBOX_COMPAT|OP_NO_COMPRESSION|OP_IGNORE_UNEXPECTED_EOF: 2421293264>)
        Protocols:
            SSLv3 - -1
            TLSv1 - 3
            TLSv1_2 - 5
            TLSv1_3 - -1
        
        
        Computed protocols:
        [('SSLv3', -1, '0x2000000'),
         ('TLSv1', <_SSLMethod.PROTOCOL_TLSv1: 3>, '0x4000000'),
         ('TLSv1_1', <_SSLMethod.PROTOCOL_TLSv1_1: 4>, '0x10000000'),
         ('TLSv1_2', <_SSLMethod.PROTOCOL_TLSv1_2: 5>, '0x8000000'),
         ('TLSv1_3', -1, '0x20000000')]
        
        Done.
        
        (qaic-env) [cfati@cfati-5510-0:/mnt/e/Work/Dev/StackExchange/StackOverflow/q049788677]> 
        (qaic-env) [cfati@cfati-5510-0:/mnt/e/Work/Dev/StackExchange/StackOverflow/q049788677]> LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/usr/local/pc032/openssl/openssl/3.3.1/lib /usr/local/pc032/python/python/3.10.13/bin/python ./code00.py 
        Python 3.10.13 (tags/v3.10.13-dirty:49965601d6, Sep 12 2023, 19:27:34) [GCC 11.4.0] 032bit on linux
        
        OpenSSL 3.3.1 4 Jun 2024
        
        Options: -7DADFFB0 (<Options.OP_NO_SSLv3|OP_CIPHER_SERVER_PREFERENCE|OP_ENABLE_MIDDLEBOX_COMPAT|OP_NO_COMPRESSION|OP_ALL: -2108555184>)
        Protocols:
            TLSv1 - 3
            TLSv1_1 - 4
            TLSv1_2 - 5
            TLSv1_3 - -1
        
        --- Removing TLSv1_1...
        /mnt/e/Work/Dev/StackExchange/StackOverflow/q049788677/./code00.py:47: DeprecationWarning: ssl.OP_NO_SSL*/ssl.OP_NO_TLS* options are deprecated
          ctx0.options |= ssl.OP_NO_TLSv1_1
        Options: -6DADFFB0 (<Options.OP_NO_TLSv1_1|OP_NO_SSLv3|OP_CIPHER_SERVER_PREFERENCE|OP_ENABLE_MIDDLEBOX_COMPAT|OP_NO_COMPRESSION|OP_ALL: -1840119728>)
        Protocols:
            TLSv1 - 3
            TLSv1_2 - 5
            TLSv1_3 - -1
        
        --- Adding SSLv3...
        Options: -6FADFFB0 (<Options.OP_NO_TLSv1_1|OP_CIPHER_SERVER_PREFERENCE|OP_ENABLE_MIDDLEBOX_COMPAT|OP_NO_COMPRESSION|OP_ALL: -1873674160>)
        Protocols:
            SSLv3 - -1
            TLSv1 - 3
            TLSv1_2 - 5
            TLSv1_3 - -1
        
        
        Computed protocols:
        [('SSLv3', -1, '0x2000000'),
         ('TLSv1', <_SSLMethod.PROTOCOL_TLSv1: 3>, '0x4000000'),
         ('TLSv1_1', <_SSLMethod.PROTOCOL_TLSv1_1: 4>, '0x10000000'),
         ('TLSv1_2', <_SSLMethod.PROTOCOL_TLSv1_2: 5>, '0x8000000'),
         ('TLSv1_3', -1, '0x20000000')]
        
        Done.