Example #1
0
 def create():
     cmd_line = ParsedCommandLine(
         invalid_servers=[
             InvalidServerStringError(server_string="www.badpãrsing.com",
                                      error_message="Pãrsing err")
         ],
         servers_to_scans=[
             (
                 ServerNetworkLocationViaDirectConnectionFactory.create(),
                 ServerNetworkConfiguration(
                     tls_server_name_indication="a.com"),
             ),
             (
                 ServerNetworkLocationViaHttpProxyFactory.create(),
                 ServerNetworkConfiguration(
                     tls_server_name_indication="a.com"),
             ),
         ],
         scan_commands={
             ScanCommand.TLS_COMPRESSION, ScanCommand.HTTP_HEADERS
         },
         scan_commands_extra_arguments={},
         json_file_out=None,
         should_disable_console_output=False,
         per_server_concurrent_connections_limit=None,
         concurrent_server_scans_limit=None,
     )
     return cmd_line
Example #2
0
    def test_works_when_client_auth_succeeded(self):
        # Given a server that requires client authentication
        with LegacyOpenSslServer(
                client_auth_config=ClientAuthConfigEnum.REQUIRED) as server:
            server_location = ServerNetworkLocation(
                hostname=server.hostname,
                ip_address=server.ip_address,
                port=server.port)
            # And sslyze provides a client certificate
            network_config = ServerNetworkConfiguration(
                tls_server_name_indication=server.hostname,
                tls_client_auth_credentials=ClientAuthenticationCredentials(
                    certificate_chain_path=server.get_client_certificate_path(
                    ),
                    key_path=server.get_client_key_path()),
            )
            server_info = check_connectivity_to_server_and_return_info(
                server_location, network_config)

            # When scanning for HTTP headers, it succeeds
            result: HttpHeadersScanResult = HttpHeadersImplementation.scan_server(
                server_info)

            assert not result.strict_transport_security_header
            assert not result.expect_ct_header
    def test_works_when_client_auth_succeeded(self):
        # Given a server that does NOT support SCSV and that requires client authentication
        with LegacyOpenSslServer(
                client_auth_config=ClientAuthConfigEnum.REQUIRED) as server:
            server_location = ServerNetworkLocationViaDirectConnection(
                hostname=server.hostname,
                ip_address=server.ip_address,
                port=server.port)
            # And sslyze provides a client certificate
            network_config = ServerNetworkConfiguration(
                tls_server_name_indication=server.hostname,
                tls_client_auth_credentials=ClientAuthenticationCredentials(
                    certificate_chain_path=server.get_client_certificate_path(
                    ),
                    key_path=server.get_client_key_path()),
            )
            server_info = ServerConnectivityTester().perform(
                server_location, network_config)

            # When testing for SCSV, it succeeds
            result: FallbackScsvScanResult = FallbackScsvImplementation.scan_server(
                server_info)

        # And the server is reported as NOT supporting SCSV
        assert not result.supports_fallback_scsv
Example #4
0
    def test_works_when_client_auth_succeeded(self):
        # Given a server that is vulnerable and that requires client authentication
        with LegacyOpenSslServer(
                client_auth_config=ClientAuthConfigEnum.REQUIRED) as server:
            server_location = ServerNetworkLocationViaDirectConnection(
                hostname=server.hostname,
                ip_address=server.ip_address,
                port=server.port)
            # And sslyze provides a client certificate
            network_config = ServerNetworkConfiguration(
                tls_server_name_indication=server.hostname,
                tls_client_auth_credentials=ClientAuthenticationCredentials(
                    certificate_chain_path=server.get_client_certificate_path(
                    ),
                    key_path=server.get_client_key_path()),
            )
            server_info = ServerConnectivityTester().perform(
                server_location, network_config)

            # When testing for insecure reneg, it succeeds
            result: SessionRenegotiationScanResult = SessionRenegotiationImplementation.scan_server(
                server_info)

            # And the results are correct
            assert result.supports_secure_renegotiation
            assert result.is_vulnerable_to_client_renegotiation_dos
Example #5
0
    def create(
        server_location: Optional[ServerNetworkLocation] = None,
        tls_probing_result: Optional[ServerTlsProbingResult] = None,
    ) -> ServerConnectivityInfo:
        if server_location:
            final_server_location = server_location
        else:
            final_server_location = ServerNetworkLocationViaDirectConnectionFactory.create(
            )

        if tls_probing_result:
            final_tls_probing_result = tls_probing_result
        else:
            final_tls_probing_result = ServerTlsProbingResult(
                highest_tls_version_supported=TlsVersionEnum.TLS_1_2,
                cipher_suite_supported="AES",
                client_auth_requirement=ClientAuthRequirementEnum.DISABLED,
            )

        return ServerConnectivityInfo(
            server_location=final_server_location,
            network_configuration=ServerNetworkConfiguration(
                tls_server_name_indication=final_server_location.hostname),
            tls_probing_result=final_tls_probing_result,
        )
Example #6
0
    def get_supported_tls(self, highest_supported):

        supported = [highest_supported]

        for version, method in {
                "SSL_2_0": TlsVersionEnum.SSLV2,
                "SSL_3_0": TlsVersionEnum.SSLV3,
                "TLS_1_0": TlsVersionEnum.TLSV1,
                "TLS_1_1": TlsVersionEnum.TLSV1_1,
                "TLS_1_2": TlsVersionEnum.TLSV1_2,
        }.items():

            # Only test SSL/TLS connections with lesser versions
            if highest_supported == version:
                break

            try:
                # Attempt connection
                # If connection fails, exception will be raised, causing the failure to be
                # logged and the version to not be appended to the supported list
                ctx = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(
                    self.domain, 443)
                cfg = ServerNetworkConfiguration(self.domain)
                connx = SslConnection(ctx, cfg, method, True)
                connx.connect(self.domain)
                supported.append(version)
            except Exception as e:
                logging.info(
                    f"Failed to connect using %{version}: ({type(e)}) - {e}")

        return supported
    def test_works_when_client_auth_succeeded(self):
        # Given a server that requires client authentication
        with ModernOpenSslServer(
                client_auth_config=ClientAuthConfigEnum.REQUIRED) as server:
            server_location = ServerNetworkLocation(
                hostname=server.hostname,
                ip_address=server.ip_address,
                port=server.port)
            # And sslyze provides a client certificate
            network_config = ServerNetworkConfiguration(
                tls_server_name_indication=server.hostname,
                tls_client_auth_credentials=ClientAuthenticationCredentials(
                    certificate_chain_path=server.get_client_certificate_path(
                    ),
                    key_path=server.get_client_key_path()),
            )
            server_info = check_connectivity_to_server_and_return_info(
                server_location, network_config)

            # When testing for resumption, it succeeds
            result: SessionResumptionSupportScanResult = SessionResumptionSupportImplementation.scan_server(
                server_info)

        assert result.session_id_successful_resumptions_count
        assert result.session_id_resumption_result == TlsResumptionSupportEnum.FULLY_SUPPORTED
Example #8
0
 def create():
     return ConnectionToServerFailed(
         server_location=ServerNetworkLocationViaDirectConnectionFactory.
         create(),
         network_configuration=ServerNetworkConfiguration(
             tls_server_name_indication="a.com"),
         error_message="This is ân éè error",
     )
Example #9
0
    def test_smtp(self):
        # Given an SMTP server to scan
        hostname = "smtp.gmail.com"
        server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 587)
        network_configuration = ServerNetworkConfiguration(
            tls_server_name_indication=hostname, tls_opportunistic_encryption=ProtocolWithOpportunisticTlsEnum.SMTP
        )
        server_info = ServerConnectivityTester().perform(server_location, network_configuration)

        # When scanning for cipher suites, it succeeds
        result: CipherSuitesScanResult = Tlsv12ScanImplementation.scan_server(server_info)
        assert result.accepted_cipher_suites
Example #10
0
    def test_xmpp_but_server_rejected_opportunistic_tls(self):
        # Given an XMPP server
        hostname = "jabber.org"
        server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname=hostname, port=5222)
        network_configuration = ServerNetworkConfiguration(
            # But we provide a wrong XMPP setting
            xmpp_to_hostname="lol.lol",
            tls_server_name_indication=hostname,
            tls_opportunistic_encryption=ProtocolWithOpportunisticTlsEnum.XMPP,
        )

        # When testing connectivity, it fails with the right error
        with pytest.raises(ServerRejectedOpportunisticTlsNegotiation):
            ServerConnectivityTester().perform(server_location, network_configuration)
Example #11
0
    def test(self, hostname, port, protocol):
        # Given some server using a non-HTTP protocol with Opportunistic TLS
        server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, port)
        network_configuration = ServerNetworkConfiguration(
            tls_server_name_indication=hostname, tls_opportunistic_encryption=protocol
        )

        # When testing connectivity against it
        server_info = ServerConnectivityTester().perform(server_location, network_configuration)

        # It succeeds
        assert server_info.tls_probing_result
        assert server_info.tls_probing_result.client_auth_requirement
        assert server_info.tls_probing_result.highest_tls_version_supported
        assert server_info.tls_probing_result.cipher_suite_supported
Example #12
0
    def create(
        server_location: Optional[ServerNetworkLocation] = None,
        scan_status: ServerScanStatusEnum = ServerScanStatusEnum.COMPLETED,
        scan_result: Optional[AllScanCommandsAttempts] = None,
    ) -> ServerScanResult:
        final_server_location: ServerNetworkLocation
        if server_location is None:
            final_server_location = ServerNetworkLocationViaDirectConnectionFactory.create(
            )
        else:
            final_server_location = server_location

        network_configuration = ServerNetworkConfiguration.default_for_server_location(
            final_server_location)

        connectivity_result: Optional[ServerTlsProbingResult]
        if scan_status == ServerScanStatusEnum.COMPLETED:
            connectivity_status = ServerConnectivityStatusEnum.COMPLETED
            connectivity_error_trace = None
            connectivity_result = ServerTlsProbingResultFactory.create()
            if scan_result is None:
                final_scan_result = AllScanCommandsAttemptsFactory.create()
            else:
                final_scan_result = scan_result

        elif scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY:
            connectivity_error_trace = TracebackExceptionFactory.create()
            connectivity_status = ServerConnectivityStatusEnum.ERROR
            connectivity_result = None
            final_scan_result = None

        else:
            raise ValueError("Should never happen")

        return ServerScanResult(
            uuid=uuid4(),
            server_location=final_server_location,
            network_configuration=network_configuration,
            connectivity_status=connectivity_status,
            connectivity_error_trace=connectivity_error_trace,
            connectivity_result=connectivity_result,
            scan_status=scan_status,
            scan_result=final_scan_result,
        )
Example #13
0
    def test(self, hostname, port, protocol):
        # Given some server using a non-HTTP protocol with Opportunistic TLS
        server_location = ServerNetworkLocation(hostname, port)
        network_configuration = ServerNetworkConfiguration(
            tls_server_name_indication=hostname,
            tls_opportunistic_encryption=protocol)

        # When testing connectivity against it
        tls_probing_result = check_connectivity_to_server(
            server_location=server_location,
            network_configuration=network_configuration,
        )

        # It succeeds
        assert tls_probing_result
        assert tls_probing_result.client_auth_requirement
        assert tls_probing_result.highest_tls_version_supported
        assert tls_probing_result.cipher_suite_supported

        # And the result can be converted to JSON
        tls_probing_result_as_json = _ServerTlsProbingResultAsJson.from_orm(
            tls_probing_result)
        assert tls_probing_result_as_json.json()
Example #14
0
    def perform(
        self,
        server_location: ServerNetworkLocation,
        network_configuration: Optional[ServerNetworkConfiguration] = None
    ) -> ServerConnectivityInfo:
        """Attempt to perform a full SSL/TLS handshake with the server.

        This method will ensure that the server can be reached, and will also identify one SSL/TLS version and one
        cipher suite that is supported by the server.

        Args:
            server_location
            network_configuration

        Returns:
            An object encapsulating all the information needed to connect to the server, to be passed to a `Scanner` in
            order to run scan commands against the server.

        Raises:
            ServerConnectivityError: If the server was not reachable or an SSL/TLS handshake could not be completed.
        """
        if network_configuration is None:
            final_network_config = ServerNetworkConfiguration.default_for_server_location(
                server_location)
        else:
            final_network_config = network_configuration

        # Try to complete an SSL handshake to figure out the SSL version and cipher supported by the server
        highest_tls_version_supported = None
        cipher_suite_supported = None
        client_auth_requirement = ClientAuthRequirementEnum.DISABLED

        # TODO(AD): Switch to using the protocol discovery logic available in OpenSSL 1.1.0 with TLS_client_method()
        for tls_version in [
                OpenSslVersionEnum.TLSV1_3,
                OpenSslVersionEnum.TLSV1_2,
                OpenSslVersionEnum.TLSV1_1,
                OpenSslVersionEnum.TLSV1,
                OpenSslVersionEnum.SSLV3,
        ]:
            # First try the default cipher list, and then all ciphers
            for cipher_list in [None, "ALL:COMPLEMENTOFALL:-PSK:-SRP"]:
                ssl_connection = SslConnection(
                    server_location=server_location,
                    network_configuration=final_network_config,
                    tls_version=tls_version,
                    should_ignore_client_auth=False,
                )
                if cipher_list:
                    if tls_version == OpenSslVersionEnum.TLSV1_3:
                        # Skip the second attempt with all ciphers enabled as these ciphers don't exist in TLS 1.3
                        continue

                    ssl_connection.ssl_client.set_cipher_list(cipher_list)

                try:
                    # Only do one attempt when testing connectivity
                    ssl_connection.connect(should_retry_connection=False)
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = ssl_connection.ssl_client.get_current_cipher_name(
                    )
                except ClientCertificateRequested:
                    # Connection successful but the servers wants a client certificate which wasn't supplied to sslyze
                    # Store the SSL version and cipher list that is supported
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = cipher_list
                    # Close the current connection and try again but ignore client authentication
                    ssl_connection.close()

                    # Try a new connection to see if client authentication is optional
                    ssl_connection_auth = SslConnection(
                        server_location=server_location,
                        network_configuration=final_network_config,
                        tls_version=tls_version,
                        should_ignore_client_auth=True,
                    )
                    if cipher_list:
                        ssl_connection_auth.ssl_client.set_cipher_list(
                            cipher_list)
                    try:
                        ssl_connection_auth.connect(
                            should_retry_connection=False)
                        cipher_suite_supported = ssl_connection_auth.ssl_client.get_current_cipher_name(
                        )
                        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

                    # If client authentication is required, we either get a ClientCertificateRequested
                    except ClientCertificateRequested:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    # Or a ServerRejectedTlsHandshake
                    except ServerRejectedTlsHandshake:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    finally:
                        ssl_connection_auth.close()

                except TlsHandshakeFailed:
                    # This TLS version did not work; keep going
                    pass

                finally:
                    ssl_connection.close()

            if cipher_suite_supported:
                # A handshake was successful
                break

        if highest_tls_version_supported is None or cipher_suite_supported is None:
            raise ServerTlsConfigurationNotSupported(
                server_location=server_location,
                network_configuration=final_network_config,
                error_message=
                "Probing failed: could not find a TLS version and cipher suite supported by the server",
            )
        tls_probing_result = ServerTlsProbingResult(
            highest_tls_version_supported=highest_tls_version_supported,
            cipher_suite_supported=cipher_suite_supported,
            client_auth_requirement=client_auth_requirement,
        )

        return ServerConnectivityInfo(
            server_location=server_location,
            network_configuration=final_network_config,
            tls_probing_result=tls_probing_result,
        )
Example #15
0
    def parse_command_line(self) -> ParsedCommandLine:
        """Parses the command line used to launch SSLyze.
        """
        (args_command_list, args_target_list) = self._parser.parse_args()

        if args_command_list.update_trust_stores:
            # Just update the trust stores and do nothing
            TrustStoresRepository.update_default()
            raise TrustStoresUpdateCompleted()

        # Handle the --targets_in command line and fill args_target_list
        if args_command_list.targets_in:
            if args_target_list:
                raise CommandLineParsingError(
                    "Cannot use --targets_list and specify targets within the command line."
                )

            try:  # Read targets from a file
                with open(args_command_list.targets_in) as f:
                    for target in f.readlines():
                        if target.strip():  # Ignore empty lines
                            if not target.startswith(
                                    "#"):  # Ignore comment lines
                                args_target_list.append(target.strip())
            except IOError:
                raise CommandLineParsingError(
                    "Can't read targets from input file '{}.".format(
                        args_command_list.targets_in))

        if not args_target_list:
            raise CommandLineParsingError("No targets to scan.")

        # Handle the --regular command line parameter as a shortcut
        if self._parser.has_option("--regular"):
            if getattr(args_command_list, "regular"):
                setattr(args_command_list, "regular", False)
                for cmd in self.REGULAR_CMD:
                    setattr(args_command_list, cmd, True)

        # Handle JSON settings
        should_print_json_to_console = False
        json_path_out: Optional[Path] = None
        if args_command_list.json_file:
            if args_command_list.json_file == "-":
                if args_command_list.quiet:
                    raise CommandLineParsingError(
                        "Cannot use --quiet with --json_out -.")
                should_print_json_to_console = True
            else:
                json_path_out = Path(args_command_list.json_file).absolute()

        # Sanity checks on the client cert options
        client_auth_creds = None
        if bool(args_command_list.cert) ^ bool(args_command_list.key):
            raise CommandLineParsingError(
                "No private key or certificate file were given. See --cert and --key."
            )

        elif args_command_list.cert:
            # Private key formats
            if args_command_list.keyform == "DER":
                key_type = OpenSslFileTypeEnum.ASN1
            elif args_command_list.keyform == "PEM":
                key_type = OpenSslFileTypeEnum.PEM
            else:
                raise CommandLineParsingError(
                    "--keyform should be DER or PEM.")

            # Let's try to open the cert and key files
            try:
                client_auth_creds = ClientAuthenticationCredentials(
                    certificate_chain_path=Path(args_command_list.cert),
                    key_path=Path(args_command_list.key),
                    key_password=args_command_list.keypass,
                    key_type=key_type,
                )
            except ValueError as e:
                raise CommandLineParsingError(
                    "Invalid client authentication settings: {}.".format(
                        e.args[0]))

        # HTTP CONNECT proxy
        http_proxy_settings = None
        if args_command_list.https_tunnel:
            try:
                http_proxy_settings = HttpProxySettings.from_url(
                    args_command_list.https_tunnel)
            except ValueError as e:
                raise CommandLineParsingError(
                    "Invalid proxy URL for --https_tunnel: {}.".format(
                        e.args[0]))

        # Create the server location objects for each specified servers
        good_servers: List[Tuple[ServerNetworkLocation,
                                 ServerNetworkConfiguration]] = []
        invalid_server_strings: List[InvalidServerStringError] = []
        for server_string in args_target_list:
            try:
                # Parse the string supplied via the CLI for this server
                hostname, ip_address, port = CommandLineServerStringParser.parse_server_string(
                    server_string)
            except InvalidServerStringError as e:
                # The server string is malformed
                invalid_server_strings.append(e)
                continue

            # If not port number was supplied, assume 443
            final_port = port if port else 443

            # Figure out how we're going to connect to the server
            server_location: ServerNetworkLocation
            if http_proxy_settings:
                # Connect to the server via an HTTP proxy
                # A limitation when using the CLI is that only one http_proxy_settings can be specified for all servers
                server_location = ServerNetworkLocationViaHttpProxy(
                    hostname=hostname,
                    port=final_port,
                    http_proxy_settings=http_proxy_settings)
            else:
                # Connect to the server directly
                if ip_address:
                    server_location = ServerNetworkLocationViaDirectConnection(
                        hostname=hostname,
                        port=final_port,
                        ip_address=ip_address)
                else:
                    # No IP address supplied - do a DNS lookup
                    try:
                        server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(
                            hostname=hostname, port=final_port)
                    except ServerHostnameCouldNotBeResolved:
                        invalid_server_strings.append(
                            InvalidServerStringError(
                                server_string=f"{hostname}:{final_port}",
                                error_message=
                                f"Could not resolve hostname {hostname}",
                            ))
                        continue

            # Figure out extra network config for this server
            # Opportunistic TLS
            opportunistic_tls: Optional[
                ProtocolWithOpportunisticTlsEnum] = None
            if args_command_list.starttls:
                if args_command_list.starttls == "auto":
                    # Special value to auto-derive the protocol from the port number
                    opportunistic_tls = ProtocolWithOpportunisticTlsEnum.from_default_port(
                        final_port)
                elif args_command_list.starttls in _STARTTLS_PROTOCOL_DICT:
                    opportunistic_tls = _STARTTLS_PROTOCOL_DICT[
                        args_command_list.starttls]
                else:
                    raise CommandLineParsingError(self.START_TLS_USAGE)

            try:
                sni_hostname = args_command_list.sni if args_command_list.sni else hostname
                network_config = ServerNetworkConfiguration(
                    tls_opportunistic_encryption=opportunistic_tls,
                    tls_server_name_indication=sni_hostname,
                    tls_client_auth_credentials=client_auth_creds,
                    xmpp_to_hostname=args_command_list.xmpp_to,
                )
                good_servers.append((server_location, network_config))
            except InvalidServerNetworkConfigurationError as e:
                raise CommandLineParsingError(e.args[0])

        # Figure out global network settings
        concurrent_server_scans_limit = None
        per_server_concurrent_connections_limit = None
        if args_command_list.https_tunnel:
            # All the connections will go through a single proxy; only scan one server at a time to not DOS the proxy
            concurrent_server_scans_limit = 1
        if args_command_list.slow_connection:
            # Go easy on the servers; only open 2 concurrent connections against each server
            per_server_concurrent_connections_limit = 2

        # Figure out the scan commands that are enabled
        scan_commands: Set[ScanCommandType] = set()
        scan_commands_extra_arguments: ScanCommandExtraArgumentsDict = {}
        for scan_command in ScanCommandsRepository.get_all_scan_commands():
            cli_connector_cls = ScanCommandsRepository.get_implementation_cls(
                scan_command).cli_connector_cls
            is_scan_cmd_enabled, extra_args = cli_connector_cls.find_cli_options_in_command_line(
                args_command_list.__dict__)
            if is_scan_cmd_enabled:
                scan_commands.add(scan_command)
                if extra_args:
                    scan_commands_extra_arguments[
                        scan_command] = extra_args  # type: ignore

        return ParsedCommandLine(
            invalid_servers=invalid_server_strings,
            servers_to_scans=good_servers,
            scan_commands=scan_commands,
            scan_commands_extra_arguments=scan_commands_extra_arguments,
            should_print_json_to_console=should_print_json_to_console,
            json_path_out=json_path_out,
            should_disable_console_output=args_command_list.quiet
            or args_command_list.json_file == "-",
            concurrent_server_scans_limit=concurrent_server_scans_limit,
            per_server_concurrent_connections_limit=
            per_server_concurrent_connections_limit,
        )
Example #16
0
    def perform(
        self,
        server_location: ServerNetworkLocation,
        network_configuration: Optional[ServerNetworkConfiguration] = None
    ) -> ServerConnectivityInfo:
        """Attempt to perform a full SSL/TLS handshake with the server.

        This method will ensure that the server can be reached, and will also identify one SSL/TLS version and one
        cipher suite that is supported by the server.

        Args:
            server_location
            network_configuration

        Returns:
            An object encapsulating all the information needed to connect to the server, to be passed to a `Scanner` in
            order to run scan commands against the server.

        Raises:
            ServerConnectivityError: If the server was not reachable or an SSL/TLS handshake could not be completed.
        """
        if network_configuration is None:
            final_network_config = ServerNetworkConfiguration.default_for_server_location(
                server_location)
        else:
            final_network_config = network_configuration

        # Try to complete an SSL handshake to figure out the SSL version and cipher supported by the server
        highest_tls_version_supported = None
        cipher_suite_supported = None
        client_auth_requirement = ClientAuthRequirementEnum.DISABLED

        # TODO(AD): Switch to using the protocol discovery logic available in OpenSSL 1.1.0 with TLS_client_method()
        for tls_version in [
                TlsVersionEnum.TLS_1_3,
                TlsVersionEnum.TLS_1_2,
                TlsVersionEnum.TLS_1_1,
                TlsVersionEnum.TLS_1_0,
                TlsVersionEnum.SSL_3_0,
        ]:
            # First try the default cipher list, and then all ciphers
            for cipher_list in [None, "ALL:COMPLEMENTOFALL:-PSK:-SRP"]:
                ssl_connection = SslConnection(
                    server_location=server_location,
                    network_configuration=final_network_config,
                    tls_version=tls_version,
                    should_ignore_client_auth=False,
                )
                if cipher_list:
                    if tls_version == TlsVersionEnum.TLS_1_3:
                        # Skip the second attempt with all ciphers enabled as these ciphers don't exist in TLS 1.3
                        continue

                    ssl_connection.ssl_client.set_cipher_list(cipher_list)

                try:
                    # Only do one attempt when testing connectivity
                    ssl_connection.connect(should_retry_connection=False)
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = ssl_connection.ssl_client.get_current_cipher_name(
                    )
                except ClientCertificateRequested:
                    # Connection successful but the servers wants a client certificate which wasn't supplied to sslyze
                    # Store the SSL version and cipher list that is supported
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = cipher_list
                    # Close the current connection and try again but ignore client authentication
                    ssl_connection.close()

                    # Try a new connection to see if client authentication is optional
                    ssl_connection_auth = SslConnection(
                        server_location=server_location,
                        network_configuration=final_network_config,
                        tls_version=tls_version,
                        should_ignore_client_auth=True,
                    )
                    if cipher_list:
                        ssl_connection_auth.ssl_client.set_cipher_list(
                            cipher_list)
                    try:
                        ssl_connection_auth.connect(
                            should_retry_connection=False)
                        cipher_suite_supported = ssl_connection_auth.ssl_client.get_current_cipher_name(
                        )
                        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

                    # If client authentication is required, we either get a ClientCertificateRequested
                    except ClientCertificateRequested:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    # Or a ServerRejectedTlsHandshake
                    except ServerRejectedTlsHandshake:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    finally:
                        ssl_connection_auth.close()

                except TlsHandshakeFailed:
                    # This TLS version did not work; keep going
                    pass

                except (OSError, _nassl.OpenSSLError) as e:
                    # If these errors get propagated here, it means they're not part of the known/normal errors that
                    # can happen when trying to connect to a server and defined in tls_connection.py
                    # Hence we re-raise these as "unknown" connection errors; might be caused by bad connectivity to
                    # the server (random disconnects, etc.) and the scan against this server should not be performed
                    raise ConnectionToServerFailed(
                        server_location=server_location,
                        network_configuration=final_network_config,
                        error_message=
                        f'Unexpected connection error: "{e.args}"',
                    )

                finally:
                    ssl_connection.close()

            if cipher_suite_supported:
                # A handshake was successful
                break

        if highest_tls_version_supported is None or cipher_suite_supported is None:
            raise ServerTlsConfigurationNotSupported(
                server_location=server_location,
                network_configuration=final_network_config,
                error_message=
                "Probing failed: could not find a TLS version and cipher suite supported by the server",
            )

        # Check if ECDH key exchanges are supported
        is_ecdh_key_exchange_supported = False
        if "ECDH" in cipher_suite_supported:
            is_ecdh_key_exchange_supported = True
        else:
            if highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_2.value:
                ssl_connection = SslConnection(
                    server_location=server_location,
                    network_configuration=final_network_config,
                    tls_version=highest_tls_version_supported,
                    should_use_legacy_openssl=False,
                    should_ignore_client_auth=True,
                )
                if not isinstance(ssl_connection.ssl_client, SslClient):
                    raise RuntimeError(
                        "Should never happen: specified should_use_legacy_openssl=False but didn't get the modern"
                        " SSL client")

                # Set the right elliptic curve cipher suites
                enable_ecdh_cipher_suites(highest_tls_version_supported,
                                          ssl_connection.ssl_client)
                try:
                    ssl_connection.connect(should_retry_connection=False)
                    is_ecdh_key_exchange_supported = True
                except ClientCertificateRequested:
                    is_ecdh_key_exchange_supported = True
                except ServerRejectedTlsHandshake:
                    is_ecdh_key_exchange_supported = False
                finally:
                    ssl_connection.close()

        tls_probing_result = ServerTlsProbingResult(
            highest_tls_version_supported=highest_tls_version_supported,
            cipher_suite_supported=cipher_suite_supported,
            client_auth_requirement=client_auth_requirement,
            supports_ecdh_key_exchange=is_ecdh_key_exchange_supported,
        )

        return ServerConnectivityInfo(
            server_location=server_location,
            network_configuration=final_network_config,
            tls_probing_result=tls_probing_result,
        )