Esempio n. 1
0
    def connect(self, should_retry_connection: bool = True) -> None:
        max_attempts_nb = self._network_configuration.network_max_retries if should_retry_connection else 1
        connection_attempts_nb = 0
        delay_for_next_attempt = 0

        # First try to connect to the server, and do retries if there are timeouts
        while True:
            # Sleep if it's a retry attempt
            time.sleep(delay_for_next_attempt)
            try:
                self._do_pre_handshake()
            except socket.timeout:
                # Attempt to retry connection if a network error occurred during connection or the handshake
                connection_attempts_nb += 1
                if connection_attempts_nb >= max_attempts_nb:
                    # Exhausted the number of retry attempts, give up
                    raise ConnectionToServerTimedOut(
                        server_location=self._server_location,
                        network_configuration=self._network_configuration,
                        error_message="Connection to the server timed out",
                    )
                elif connection_attempts_nb == 1:
                    # Start with a 1 second delay
                    delay_for_next_attempt = 1
                else:
                    # Exponential back off; cap maximum delay at 6 seconds
                    delay_for_next_attempt = min(6, 2 * delay_for_next_attempt)
            except ConnectionError:
                raise ServerRejectedConnection(
                    server_location=self._server_location,
                    network_configuration=self._network_configuration,
                    error_message="Server rejected the connection",
                )
            except OSError:
                # OSError is the parent class of all socket (ie. non-TLS) connection errors such as socket.timeout or
                # ConnectionError; hence this is the most generic error handler and should always be defined last
                raise ConnectionToServerFailed(
                    server_location=self._server_location,
                    network_configuration=self._network_configuration,
                    error_message="Connection to the server failed",
                )

            else:
                # No network error occurred
                break

        # After successfully connecting to the server, perform the TLS handshake
        try:
            self.ssl_client.do_handshake()

        except ClientCertificateRequested:
            # Server expected a client certificate and we didn't provide one
            raise
        except socket.timeout:
            # Network timeout, propagate the error
            raise TlsHandshakeTimedOut(
                server_location=self._server_location,
                network_configuration=self._network_configuration,
                error_message=
                "Connection to server timed out during the TLS handshake",
            )
        except ConnectionError:
            raise ServerRejectedTlsHandshake(
                server_location=self._server_location,
                network_configuration=self._network_configuration,
                error_message="Server rejected the connection",
            )
        except OSError as e:
            # OSError is the parent of all (non-TLS) socket/connection errors so it should be last
            if "Nassl SSL handshake failed" in e.args[0]:
                # Special error returned by nassl
                raise ServerRejectedTlsHandshake(
                    server_location=self._server_location,
                    network_configuration=self._network_configuration,
                    error_message="Server rejected the connection",
                )
            # Unknown connection error
            raise
        except _nassl.OpenSSLError as e:
            openssl_error_message = e.args[0]
            if "dh key too small" in openssl_error_message:
                # This is when SSLyze's OpenSSL rejects DH parameters (to protect against Logjam); this actually
                # means the server supports whatever cipher suite was used
                raise ServerTlsConfigurationNotSupported(
                    server_location=self._server_location,
                    network_configuration=self._network_configuration,
                    error_message="DH key too small",
                )

            if "no ciphers available" in openssl_error_message:
                # This one is returned by OpenSSL when a cipher set via set_cipher_list() is not actually supported
                # Should never happen (SSLyze bugs)
                raise NoCiphersAvailableBugInSSlyze(
                    f"Set a cipher that is not supported by nassl: {self.ssl_client.get_cipher_list()}"
                )

            for error_msg in _HANDSHAKE_REJECTED_TLS_ERRORS.keys():
                if error_msg in openssl_error_message:
                    raise ServerRejectedTlsHandshake(
                        server_location=self._server_location,
                        network_configuration=self._network_configuration,
                        error_message=_HANDSHAKE_REJECTED_TLS_ERRORS[
                            error_msg],
                    )

            # Unknown SSL error if we get there
            raise
Esempio n. 2
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,
        )
Esempio n. 3
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

                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,
        )
Esempio n. 4
0
def check_connectivity_to_server(
    server_location: ServerNetworkLocation,
    network_configuration: ServerNetworkConfiguration
) -> ServerTlsProbingResult:
    """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:
        ServerTlsProbingResult

    Raises:
        ServerConnectivityError: If the server was not reachable or an SSL/TLS handshake could not be completed.
    """
    # Try to complete an SSL handshake to figure out the SSL/TLS version and cipher supported by the server
    tls_detection_result: Optional[_TlsVersionDetectionResult] = None

    # Fist try TLS 1.3
    try:
        tls_detection_result = _detect_support_for_tls_1_3(
            server_location=server_location,
            network_config=network_configuration,
        )
    except _TlsVersionNotSupported:
        pass

    # If TLS 1.3 is not supported, try lower versions of SSL/TLS
    if tls_detection_result is None:
        for tls_version in [
                # Order is important here as we want to detect the highest version of TLS that's supported
                TlsVersionEnum.TLS_1_2,
                TlsVersionEnum.TLS_1_1,
                TlsVersionEnum.TLS_1_0,
                TlsVersionEnum.SSL_3_0,
        ]:
            try:
                tls_detection_result = _detect_support_for_tls_1_2_or_below(
                    server_location=server_location,
                    network_config=network_configuration,
                    tls_version=tls_version,
                )
                break
            except _TlsVersionNotSupported:
                # Try the next TLS version
                pass

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

    # If the server requested a client certificate, detect if the client cert is optional or required
    client_auth_requirement = ClientAuthRequirementEnum.DISABLED
    if tls_detection_result.server_requested_client_cert:
        if tls_detection_result.tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value:
            client_auth_requirement = _detect_client_auth_requirement_with_tls_1_3(
                server_location=server_location,
                network_config=network_configuration,
            )
        else:
            client_auth_requirement = _detect_client_auth_requirement_with_tls_1_2_or_below(
                server_location=server_location,
                network_config=network_configuration,
                tls_version=tls_detection_result.tls_version_supported,
                cipher_list=tls_detection_result.cipher_suite_supported,
            )

    # Check if ECDH key exchanges are supported, for the elliptic curves plugin
    if "ECDH" in tls_detection_result.cipher_suite_supported:
        is_ecdh_key_exchange_supported = True
    else:
        is_ecdh_key_exchange_supported = _detect_ecdh_support(
            server_location=server_location,
            network_config=network_configuration,
            tls_version=tls_detection_result.tls_version_supported,
        )

    # All done with TLS probing
    return ServerTlsProbingResult(
        highest_tls_version_supported=tls_detection_result.
        tls_version_supported,
        cipher_suite_supported=tls_detection_result.cipher_suite_supported,
        client_auth_requirement=client_auth_requirement,
        supports_ecdh_key_exchange=is_ecdh_key_exchange_supported,
    )