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:

    Output: