def test_connectivity_error(self, mock_scan_commands): # Given a server to scan scan_request = ServerScanRequestFactory.create() # And the server will NOT be reachable error = ConnectionToServerFailed( server_location=scan_request.server_location, network_configuration=scan_request.network_configuration, error_message="testt", ) with mock.patch.object(_mass_connectivity_tester, "check_connectivity_to_server", side_effect=error): # And given an observer to monitor scans observer = _MockScannerObserver() # When running the scans with the observer scanner = Scanner(observers=[observer]) scanner.queue_scans([scan_request]) # It succeeds all_scan_results = [] for result in scanner.get_results(): all_scan_results.append(result) # And the right result was returned assert len(all_scan_results) == 1 assert all_scan_results[0].scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY # And the observer was called appropriately assert observer.server_connectivity_test_error_calls_count == 1 assert observer.server_connectivity_test_completed_calls_count == 0 assert observer.server_scan_completed_calls_count == 1 assert observer.all_server_scans_completed_calls_count == 1
def create(): return ConnectionToServerFailed( server_location=ServerNetworkLocationViaDirectConnectionFactory. create(), network_configuration=ServerNetworkConfiguration( tls_server_name_indication="a.com"), error_message="This is ân éè error", )
def _detect_support_for_tls_1_2_or_below( server_location: ServerNetworkLocation, network_config: ServerNetworkConfiguration, tls_version: TlsVersionEnum, ) -> _TlsVersionDetectionResult: # First try the default cipher list, and then all ciphers; this is to work around F5 network devices # that time out when the client hello is too long (ie. too many cipher suites enabled) # https://support.f5.com/csp/article/K14758 for cipher_list in ["DEFAULT", "ALL:COMPLEMENTOFALL:-PSK:-SRP"]: ssl_connection = SslConnection( server_location=server_location, network_configuration=network_config, tls_version=tls_version, should_ignore_client_auth=False, ) ssl_connection.ssl_client.set_cipher_list(cipher_list) try: # Only do one attempt when testing connectivity ssl_connection.connect(should_retry_connection=False) return _TlsVersionDetectionResult( tls_version_supported=tls_version, server_requested_client_cert=False, 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 return _TlsVersionDetectionResult( tls_version_supported=tls_version, server_requested_client_cert=True, # Calling ssl_connection.ssl_client.get_current_cipher_name() will fail in this situation so we just # store the whole cipher_list cipher_suite_supported=cipher_list, ) except TlsHandshakeFailed: # Try the next cipher list 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=network_config, error_message=f'Unexpected connection error: "{e.args}"', ) finally: ssl_connection.close() # If we get here, none of the handshakes were successful raise _TlsVersionNotSupported()
def _detect_support_for_tls_1_3( server_location: ServerNetworkLocation, network_config: ServerNetworkConfiguration, ) -> _TlsVersionDetectionResult: ssl_connection = SslConnection( server_location=server_location, network_configuration=network_config, tls_version=TlsVersionEnum.TLS_1_3, should_ignore_client_auth=False, ) try: ssl_connection.connect(should_retry_connection=False) return _TlsVersionDetectionResult( tls_version_supported=TlsVersionEnum.TLS_1_3, server_requested_client_cert=False, 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 return _TlsVersionDetectionResult( tls_version_supported=TlsVersionEnum.TLS_1_3, server_requested_client_cert=True, cipher_suite_supported=ssl_connection.ssl_client. get_current_cipher_name(), ) except TlsHandshakeFailed: 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=network_config, error_message=f'Unexpected connection error: "{e.args}"', ) finally: ssl_connection.close() # If we get here, none of the handshakes were successful raise _TlsVersionNotSupported()
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
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, )