Exemple #1
0
    def process_task(self, server_info: ServerConnectivityInfo,
                     scan_command: PluginScanCommand) -> 'EarlyDataScanResult':
        if not isinstance(scan_command, EarlyDataScanCommand):
            raise ValueError('Unexpected scan command')

        session = None
        is_early_data_supported = False
        ssl_connection = server_info.get_preconfigured_ssl_connection(
            override_ssl_version=OpenSslVersionEnum.TLSV1_3)
        try:
            # Perform an SSL handshake and keep the session
            ssl_connection.connect()
            # Send and receive data for the TLS session to be created
            ssl_connection.ssl_client.write(
                HttpRequestGenerator.get_request(host=server_info.hostname))
            ssl_connection.ssl_client.read(2048)
            session = ssl_connection.ssl_client.get_session()
        except SslHandshakeRejected:
            # TLS 1.3 not supported
            is_early_data_supported = False
        finally:
            ssl_connection.close()

        # Then try to re-use the session and send early data
        if session is not None:
            ssl_connection2 = server_info.get_preconfigured_ssl_connection(
                override_ssl_version=OpenSslVersionEnum.TLSV1_3)
            ssl_connection2.ssl_client.set_session(session)

            try:
                # Open a socket to the server but don't do the handshake
                ssl_connection2.do_pre_handshake(None)

                # Send one byte of early data
                ssl_connection2.ssl_client.write_early_data(b'E')
                ssl_connection2.ssl_client.do_handshake()
                if ssl_connection2.ssl_client.get_early_data_status(
                ) == OpenSslEarlyDataStatusEnum.ACCEPTED:
                    is_early_data_supported = True
                else:
                    is_early_data_supported = False

            except OpenSSLError as e:
                if 'function you should not call' in e.args[0]:
                    # This is what OpenSSL returns when the server did not enable early data
                    is_early_data_supported = False
                else:
                    raise

            finally:
                ssl_connection2.close()

        return EarlyDataScanResult(server_info, scan_command,
                                   is_early_data_supported)
    def process_task(
            self,
            server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand
    ) -> 'EarlyDataScanResult':
        if not isinstance(scan_command, EarlyDataScanCommand):
            raise ValueError('Unexpected scan command')

        session = None
        is_early_data_supported = False
        ssl_connection = server_info.get_preconfigured_ssl_connection(override_ssl_version=OpenSslVersionEnum.TLSV1_3)
        try:
            # Perform an SSL handshake and keep the session
            ssl_connection.connect()
            # Send and receive data for the TLS session to be created
            ssl_connection.ssl_client.write(HttpRequestGenerator.get_request(host=server_info.hostname))
            ssl_connection.ssl_client.read(2048)
            session = ssl_connection.ssl_client.get_session()
        except SslHandshakeRejected:
            # TLS 1.3 not supported
            is_early_data_supported = False
        finally:
            ssl_connection.close()

        # Then try to re-use the session and send early data
        if session is not None:
            ssl_connection2 = server_info.get_preconfigured_ssl_connection(
                override_ssl_version=OpenSslVersionEnum.TLSV1_3
            )
            ssl_connection2.ssl_client.set_session(session)

            try:
                # Open a socket to the server but don't do the handshake
                ssl_connection2.do_pre_handshake(None)

                # Send one byte of early data
                ssl_connection2.ssl_client.write_early_data(b'E')
                ssl_connection2.ssl_client.do_handshake()
                if ssl_connection2.ssl_client.get_early_data_status() == OpenSslEarlyDataStatusEnum.ACCEPTED:
                    is_early_data_supported = True
                else:
                    is_early_data_supported = False

            except OpenSSLError as e:
                if 'function you should not call' in e.args[0]:
                    # This is what OpenSSL returns when the server did not enable early data
                    is_early_data_supported = False
                else:
                    raise

            finally:
                ssl_connection2.close()

        return EarlyDataScanResult(server_info, scan_command, is_early_data_supported)
    def _resume_ssl_session(
            server_info: ServerConnectivityInfo,
            ssl_session: Optional[nassl._nassl.SSL_SESSION] = None,
            should_enable_tls_ticket: bool = False
    ) -> nassl._nassl.SSL_SESSION:
        """Connect to the server and returns the session object that was assigned for that connection.
        If ssl_session is given, tries to resume that session.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection()
        if not should_enable_tls_ticket:
            # Need to disable TLS tickets to test session IDs, according to rfc5077:
            # If a ticket is presented by the client, the server MUST NOT attempt
            # to use the Session ID in the ClientHello for stateful session resumption
            ssl_connection.ssl_client.disable_stateless_session_resumption(
            )  # Turning off TLS tickets.

        if ssl_session:
            ssl_connection.ssl_client.set_session(ssl_session)

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            new_session = ssl_connection.ssl_client.get_session(
            )  # Get session data
        finally:
            ssl_connection.close()

        return new_session
    def process_task(
            self,
            server_info: ServerConnectivityInfo,
            scan_command: plugin_base.PluginScanCommand
    ) -> 'OpenSslCcsInjectionScanResult':
        if not isinstance(scan_command, OpenSslCcsInjectionScanCommand):
            raise ValueError('Unexpected scan command')

        if server_info.highest_ssl_version_supported >= OpenSslVersionEnum.TLSV1_3:
            # The server uses a recent version of OpenSSL and it cannot be vulnerable to CCS Injection
            return OpenSslCcsInjectionScanResult(server_info, scan_command, False)

        ssl_connection = server_info.get_preconfigured_ssl_connection()
        # Replace nassl.sslClient.do_handshake() with a CCS checking SSL handshake so that all the SSLyze options
        # (startTLS, proxy, etc.) still work
        ssl_connection.ssl_client.do_handshake = types.MethodType(do_handshake_with_ccs_injection,
                                                                  ssl_connection.ssl_client)

        is_vulnerable = False
        try:
            # Start the SSL handshake
            ssl_connection.connect()
        except VulnerableToCcsInjection:
            # The test was completed and the server is vulnerable
            is_vulnerable = True
        except NotVulnerableToCcsInjection:
            # The test was completed and the server is NOT vulnerable
            pass
        finally:
            ssl_connection.close()

        return OpenSslCcsInjectionScanResult(server_info, scan_command, is_vulnerable)
Exemple #5
0
    def process_task(
            self, server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand) -> 'HeartbleedScanResult':
        if not isinstance(scan_command, HeartbleedScanCommand):
            raise ValueError('Unexpected scan command')

        ssl_connection = server_info.get_preconfigured_ssl_connection()
        # Replace nassl.sslClient.do_handshake() with a heartbleed checking SSL handshake so that all the SSLyze options
        # (startTLS, proxy, etc.) still work
        ssl_connection.ssl_client.do_handshake = types.MethodType(
            do_handshake_with_heartbleed, ssl_connection.ssl_client)

        is_vulnerable_to_heartbleed = False
        try:
            # Start the SSL handshake
            ssl_connection.connect()
        except VulnerableToHeartbleed:
            # The test was completed and the server is vulnerable
            is_vulnerable_to_heartbleed = True
        except NotVulnerableToHeartbleed:
            # The test was completed and the server is NOT vulnerable
            pass
        finally:
            ssl_connection.close()

        return HeartbleedScanResult(server_info, scan_command,
                                    is_vulnerable_to_heartbleed)
Exemple #6
0
    def _get_selected_cipher_suite(
        server_connectivity: ServerConnectivityInfo,
        ssl_version: OpenSslVersionEnum,
        openssl_cipher_str: str,
        should_use_legacy_openssl: Optional[bool],
    ) -> "AcceptedCipherSuite":
        """Given an OpenSSL cipher string (which may specify multiple cipher suites), return the cipher suite that was
        selected by the server during the SSL handshake.
        """
        ssl_connection = server_connectivity.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version,
            should_use_legacy_openssl=should_use_legacy_openssl)
        ssl_connection.ssl_client.set_cipher_list(openssl_cipher_str)

        # Perform the SSL handshake
        try:
            ssl_connection.connect()
            selected_cipher = AcceptedCipherSuite.from_ongoing_ssl_connection(
                ssl_connection, ssl_version)
        except ClientCertificateRequested:
            selected_cipher = AcceptedCipherSuite.from_ongoing_ssl_connection(
                ssl_connection, ssl_version)
        finally:
            ssl_connection.close()
        return selected_cipher
Exemple #7
0
    def _get_rsa_parameters(
            server_info: ServerConnectivityInfo,
            openssl_cipher_string: str
    ) -> Optional[Tuple[int, int]]:
        ssl_connection = server_info.get_preconfigured_ssl_connection()
        ssl_connection.ssl_client.set_cipher_list(openssl_cipher_string)
        parsed_cert = None
        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            certificate = ssl_connection.ssl_client.get_peer_certificate()
            parsed_cert = load_pem_x509_certificate(certificate.as_pem().encode('ascii'), backend=default_backend())
        except SslHandshakeRejected:
            # Server does not support RSA cipher suites?
            pass
        except ClientCertificateRequested:
            # AD: The server asked for a client cert. We could still retrieve the server certificate, but it is unclear
            # to me if the ROBOT check is supposed to work even if we do not provide a client cert. My guess is that
            # it should not work since it requires completing a full handshake, which we can't without a client cert.
            # Hence, propagate the error to make the check fail.
            raise
        finally:
            ssl_connection.close()

        if parsed_cert:
            return parsed_cert.public_key().public_numbers().n, parsed_cert.public_key().public_numbers().e
        else:
            return None
Exemple #8
0
    def _get_and_verify_certificate_chain(
            server_info: ServerConnectivityInfo,
            trust_store: TrustStore
    ) -> Tuple[List[Certificate], str, Optional[OcspResponse]]:
        """Connects to the target server and uses the supplied trust store to validate the server's certificate.
        Returns the server's certificate and OCSP response.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(ssl_verify_locations=trust_store.path)

        # Enable OCSP stapling
        ssl_connection.ssl_client.set_tlsext_status_ocsp()

        try:  # Perform the SSL handshake
            ssl_connection.connect()

            ocsp_response = ssl_connection.ssl_client.get_tlsext_status_ocsp_resp()
            x509_cert_chain = ssl_connection.ssl_client.get_peer_cert_chain()
            (_, verify_str) = ssl_connection.ssl_client.get_certificate_chain_verify_result()

        except ClientCertificateRequested:  # The server asked for a client cert
            # We can get the server cert anyway
            ocsp_response = ssl_connection.ssl_client.get_tlsext_status_ocsp_resp()
            x509_cert_chain = ssl_connection.ssl_client.get_peer_cert_chain()
            (_, verify_str) = ssl_connection.ssl_client.get_certificate_chain_verify_result()

        finally:
            ssl_connection.close()

        # Parse the certificates using the cryptography module
        parsed_x509_chain = [load_pem_x509_certificate(x509_cert.as_pem().encode('ascii'), backend=default_backend())
                             for x509_cert in x509_cert_chain]
        return parsed_x509_chain, verify_str, ocsp_response
    def _resume_ssl_session(
            server_info: ServerConnectivityInfo,
            ssl_version_to_use: OpenSslVersionEnum,
            ssl_session: Optional[nassl._nassl.SSL_SESSION] = None,
            should_enable_tls_ticket: bool = False
    ) -> nassl._nassl.SSL_SESSION:
        """Connect to the server and returns the session object that was assigned for that connection.
        If ssl_session is given, tries to resume that session.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(override_ssl_version=ssl_version_to_use)
        if not should_enable_tls_ticket:
            # Need to disable TLS tickets to test session IDs, according to rfc5077:
            # If a ticket is presented by the client, the server MUST NOT attempt
            # to use the Session ID in the ClientHello for stateful session resumption
            ssl_connection.ssl_client.disable_stateless_session_resumption()  # Turning off TLS tickets.

        if ssl_session:
            ssl_connection.ssl_client.set_session(ssl_session)

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            new_session = ssl_connection.ssl_client.get_session()  # Get session data
        finally:
            ssl_connection.close()

        return new_session
Exemple #10
0
    def process_task(
            self,
            server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand
    ) -> 'CompressionScanResult':
        if not isinstance(scan_command, CompressionScanCommand):
            raise ValueError('Unexpected scan command')

        ssl_connection = server_info.get_preconfigured_ssl_connection(should_use_legacy_openssl=True)

        # Make sure OpenSSL was built with support for compression to avoid false negatives
        if 'zlib compression' not in ssl_connection.ssl_client.get_available_compression_methods():
            raise RuntimeError('OpenSSL was not built with support for zlib / compression. '
                               'Did you build nassl yourself ?')

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            compression_name = ssl_connection.ssl_client.get_current_compression_method()
        except ClientCertificateRequested:
            # The server asked for a client cert
            compression_name = ssl_connection.ssl_client.get_current_compression_method()
        finally:
            ssl_connection.close()

        return CompressionScanResult(server_info, scan_command, compression_name)
    def process_task(
        self, server_info: ServerConnectivityInfo,
        scan_command: plugin_base.PluginScanCommand
    ) -> "OpenSslCcsInjectionScanResult":
        if not isinstance(scan_command, OpenSslCcsInjectionScanCommand):
            raise ValueError("Unexpected scan command")

        if server_info.highest_ssl_version_supported >= OpenSslVersionEnum.TLSV1_3:
            # The server uses a recent version of OpenSSL and it cannot be vulnerable to CCS Injection
            return OpenSslCcsInjectionScanResult(server_info, scan_command,
                                                 False)

        ssl_connection = server_info.get_preconfigured_ssl_connection()
        # Replace nassl.sslClient.do_handshake() with a CCS checking SSL handshake so that all the SSLyze options
        # (startTLS, proxy, etc.) still work
        ssl_connection.ssl_client.do_handshake = types.MethodType(
            do_handshake_with_ccs_injection, ssl_connection.ssl_client)

        is_vulnerable = False
        try:
            # Start the SSL handshake
            ssl_connection.connect()
        except VulnerableToCcsInjection:
            # The test was completed and the server is vulnerable
            is_vulnerable = True
        except NotVulnerableToCcsInjection:
            # The test was completed and the server is NOT vulnerable
            pass
        finally:
            ssl_connection.close()

        return OpenSslCcsInjectionScanResult(server_info, scan_command,
                                             is_vulnerable)
Exemple #12
0
    def process_task(
            self, server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand) -> 'CompressionScanResult':
        if not isinstance(scan_command, CompressionScanCommand):
            raise ValueError('Unexpected scan command')

        ssl_connection = server_info.get_preconfigured_ssl_connection(
            should_use_legacy_openssl=True)

        # Make sure OpenSSL was built with support for compression to avoid false negatives
        if 'zlib compression' not in ssl_connection.ssl_client.get_available_compression_methods(
        ):
            raise RuntimeError(
                'OpenSSL was not built with support for zlib / compression. '
                'Did you build nassl yourself ?')

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            compression_name = ssl_connection.ssl_client.get_current_compression_method(
            )
        except ClientCertificateRequested:
            # The server asked for a client cert
            compression_name = ssl_connection.ssl_client.get_current_compression_method(
            )
        finally:
            ssl_connection.close()

        return CompressionScanResult(server_info, scan_command,
                                     compression_name)
Exemple #13
0
    def process_task(
            self,
            server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand
    ) -> 'HeartbleedScanResult':
        if not isinstance(scan_command, HeartbleedScanCommand):
            raise ValueError('Unexpected scan command')

        ssl_connection = server_info.get_preconfigured_ssl_connection()
        # Replace nassl.sslClient.do_handshake() with a heartbleed checking SSL handshake so that all the SSLyze options
        # (startTLS, proxy, etc.) still work
        ssl_connection.ssl_client.do_handshake = types.MethodType(do_handshake_with_heartbleed,
                                                                  ssl_connection.ssl_client)

        is_vulnerable_to_heartbleed = False
        try:
            # Start the SSL handshake
            ssl_connection.connect()
        except VulnerableToHeartbleed:
            # The test was completed and the server is vulnerable
            is_vulnerable_to_heartbleed = True
        except NotVulnerableToHeartbleed:
            # The test was completed and the server is NOT vulnerable
            pass
        finally:
            ssl_connection.close()

        return HeartbleedScanResult(server_info, scan_command, is_vulnerable_to_heartbleed)
Exemple #14
0
    def _get_and_verify_certificate_chain(
            server_info: ServerConnectivityInfo,
            trust_store: TrustStore
    ) -> Tuple[List[Certificate], str, Optional[OcspResponse]]:
        """Connects to the target server and uses the supplied trust store to validate the server's certificate.
        Returns the server's certificate and OCSP response.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(ssl_verify_locations=trust_store.path)

        # Enable OCSP stapling
        ssl_connection.ssl_client.set_tlsext_status_ocsp()

        try:  # Perform the SSL handshake
            ssl_connection.connect()

            ocsp_response = ssl_connection.ssl_client.get_tlsext_status_ocsp_resp()
            x509_cert_chain = ssl_connection.ssl_client.get_peer_cert_chain()
            (_, verify_str) = ssl_connection.ssl_client.get_certificate_chain_verify_result()

        except ClientCertificateRequested:  # The server asked for a client cert
            # We can get the server cert anyway
            ocsp_response = ssl_connection.ssl_client.get_tlsext_status_ocsp_resp()
            x509_cert_chain = ssl_connection.ssl_client.get_peer_cert_chain()
            (_, verify_str) = ssl_connection.ssl_client.get_certificate_chain_verify_result()

        finally:
            ssl_connection.close()

        # Parse the certificates using the cryptography module
        parsed_x509_chain = [load_pem_x509_certificate(x509_cert.as_pem().encode('ascii'), backend=default_backend())
                             for x509_cert in x509_cert_chain]
        return parsed_x509_chain, verify_str, ocsp_response
Exemple #15
0
    def _get_rsa_parameters(
            server_info: ServerConnectivityInfo,
            openssl_cipher_string: str) -> Optional[Tuple[int, int]]:
        ssl_connection = server_info.get_preconfigured_ssl_connection()
        ssl_connection.ssl_client.set_cipher_list(openssl_cipher_string)
        parsed_cert = None
        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            certificate = ssl_connection.ssl_client.get_peer_certificate()
            parsed_cert = load_pem_x509_certificate(
                certificate.as_pem().encode('ascii'),
                backend=default_backend())
        except SslHandshakeRejected:
            # Server does not support RSA cipher suites?
            pass
        except ClientCertificateRequested:
            # AD: The server asked for a client cert. We could still retrieve the server certificate, but it is unclear
            # to me if the ROBOT check is supposed to work even if we do not provide a client cert. My guess is that
            # it should not work since it requires completing a full handshake, which we can't without a client cert.
            # Hence, propagate the error to make the check fail.
            raise
        finally:
            ssl_connection.close()

        if parsed_cert:
            try:
                return parsed_cert.public_key().public_numbers(
                ).n, parsed_cert.public_key().public_numbers().e
            except AttributeError:
                return None
        else:
            return None
    def process_task(
            self,
            server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand
    ) -> 'HttpHeadersScanResult':
        if not isinstance(scan_command, HttpHeadersScanCommand):
            raise ValueError('Unexpected scan command')

        if server_info.tls_wrapped_protocol not in [TlsWrappedProtocolEnum.PLAIN_TLS, TlsWrappedProtocolEnum.HTTPS]:
            raise ValueError('Cannot test for HTTP headers on a StartTLS connection.')

        # Perform the SSL handshake
        mozilla_store = TrustStoresRepository.get_default().get_main_store()
        ssl_connection = server_info.get_preconfigured_ssl_connection(ssl_verify_locations=mozilla_store.path)
        try:
            ssl_connection.connect()
            try:
                verified_chain_as_pem = ssl_connection.ssl_client.get_verified_chain()
            except CouldNotBuildVerifiedChain:
                verified_chain_as_pem = None

            # Send an HTTP GET request to the server
            ssl_connection.ssl_client.write(HttpRequestGenerator.get_request(host=server_info.hostname))

            # We do not follow redirections because the security headers must be set on the first page according to
            # https://hstspreload.appspot.com/:
            # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS
            # header (rather than the page it redirects to)."
            http_response = HttpResponseParser.parse_from_ssl_connection(ssl_connection.ssl_client)
        finally:
            ssl_connection.close()

        if http_response.version == 9:
            # HTTP 0.9 => Probably not an HTTP response
            raise ValueError('Server did not return an HTTP response')

        # Parse the certificate chain
        verified_chain = [
            load_pem_x509_certificate(cert_as_pem.encode('ascii'), backend=default_backend())
            for cert_as_pem in verified_chain_as_pem
        ] if verified_chain_as_pem else None

        # Parse each header
        hsts_header = StrictTransportSecurityHeader.from_http_response(http_response)
        expect_ct_header = ExpectCtHeader.from_http_response(http_response)
        hpkp_header = PublicKeyPinsHeader.from_http_response(http_response)
        hpkp_report_only_header = PublicKeyPinsReportOnlyHeader.from_http_response(http_response)

        return HttpHeadersScanResult(
            server_info,
            scan_command,
            hsts_header,
            hpkp_header,
            hpkp_report_only_header,
            expect_ct_header,
            verified_chain
        )
    def _test_client_renegotiation(server_info: ServerConnectivityInfo, ssl_version_to_use: OpenSslVersionEnum) -> bool:
        """Check whether the server honors session renegotiation requests.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version_to_use, should_use_legacy_openssl=True
        )

        try:
            # Perform the SSL handshake
            ssl_connection.connect()

            try:
                # Let's try to renegotiate
                ssl_connection.ssl_client.do_renegotiate()
                accepts_client_renegotiation = True

            # Errors caused by a server rejecting the renegotiation
            except socket.timeout:
                # This is how Netty rejects a renegotiation - https://github.com/nabla-c0d3/sslyze/issues/114
                accepts_client_renegotiation = False
            except socket.error as e:
                if 'connection was forcibly closed' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'reset by peer' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'Nassl SSL handshake failed' in str(e.args):
                    accepts_client_renegotiation = False
                else:
                    raise
            except OpenSSLError as e:
                if 'handshake failure' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'no renegotiation' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'tlsv1 unrecognized name' in str(e.args):
                    # Yahoo's very own way of rejecting a renegotiation
                    accepts_client_renegotiation = False
                elif 'tlsv1 alert internal error' in str(e.args):
                    # Jetty server: https://github.com/nabla-c0d3/sslyze/issues/290
                    accepts_client_renegotiation = False
                else:
                    raise

            except ConnectionError:
                accepts_client_renegotiation = False

        finally:
            ssl_connection.close()

        return accepts_client_renegotiation
Exemple #18
0
    def _test_secure_renegotiation(server_info: ServerConnectivityInfo) -> bool:
        """Check whether the server supports secure renegotiation.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(should_use_legacy_openssl=True)

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            supports_secure_renegotiation = ssl_connection.ssl_client.get_secure_renegotiation_support()

        finally:
            ssl_connection.close()

        return supports_secure_renegotiation
    def _test_client_renegotiation(server_info: ServerConnectivityInfo, ssl_version_to_use: OpenSslVersionEnum) -> bool:
        """Check whether the server honors session renegotiation requests.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version_to_use, should_use_legacy_openssl=True
        )

        try:
            # Perform the SSL handshake
            ssl_connection.connect()

            try:
                # Let's try to renegotiate
                ssl_connection.ssl_client.do_renegotiate()
                accepts_client_renegotiation = True

            # Errors caused by a server rejecting the renegotiation
            except socket.timeout:
                # This is how Netty rejects a renegotiation - https://github.com/nabla-c0d3/sslyze/issues/114
                    accepts_client_renegotiation = False
            except socket.error as e:
                if 'connection was forcibly closed' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'reset by peer' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'Nassl SSL handshake failed' in str(e.args):
                    accepts_client_renegotiation = False
                else:
                    raise
            except OpenSSLError as e:
                if 'handshake failure' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'no renegotiation' in str(e.args):
                    accepts_client_renegotiation = False
                elif 'tlsv1 unrecognized name' in str(e.args):
                    # Yahoo's very own way of rejecting a renegotiation
                    accepts_client_renegotiation = False
                elif 'tlsv1 alert internal error' in str(e.args):
                    # Jetty server: https://github.com/nabla-c0d3/sslyze/issues/290
                    accepts_client_renegotiation = False
                else:
                    raise

            except ConnectionError:
                accepts_client_renegotiation = False

        finally:
            ssl_connection.close()

        return accepts_client_renegotiation
    def _test_secure_renegotiation(server_info: ServerConnectivityInfo, ssl_version_to_use: OpenSslVersionEnum) -> bool:
        """Check whether the server supports secure renegotiation.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version_to_use, should_use_legacy_openssl=True
        )

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            supports_secure_renegotiation = ssl_connection.ssl_client.get_secure_renegotiation_support()

        finally:
            ssl_connection.close()

        return supports_secure_renegotiation
    def process_task(
            self, server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand) -> "FallbackScsvScanResult":
        if not isinstance(scan_command, FallbackScsvScanCommand):
            raise ValueError("Unexpected scan command")

        if server_info.highest_ssl_version_supported.value <= OpenSslVersionEnum.SSLV3.value:
            raise ValueError(
                "Server only supports SSLv3; no downgrade attacks are possible"
            )

        # Try with TLS 1.2 even if the server supports TLS 1.3 or higher as there is no downgrade possible with TLS 1.3
        if server_info.highest_ssl_version_supported >= OpenSslVersionEnum.TLSV1_3:
            ssl_version_to_use = OpenSslVersionEnum.TLSV1_2
        else:
            ssl_version_to_use = server_info.highest_ssl_version_supported

        # Try to connect using a lower TLS version with the fallback cipher suite enabled
        ssl_version_downgrade = OpenSslVersionEnum(ssl_version_to_use.value -
                                                   1)  # type: ignore
        ssl_connection = server_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version_downgrade)
        ssl_connection.ssl_client.enable_fallback_scsv()

        supports_fallback_scsv = False
        try:
            # Perform the SSL handshake
            ssl_connection.connect()

        except _nassl.OpenSSLError as e:
            # This is the right, specific alert the server should return
            if "tlsv1 alert inappropriate fallback" in str(e.args):
                supports_fallback_scsv = True
            else:
                raise

        except SslHandshakeRejected:
            # If the handshake is rejected, we assume downgrade attacks are prevented (this is how F5 balancers do it)
            # although it could also be because the server does not support this version of TLS
            # https://github.com/nabla-c0d3/sslyze/issues/119
            supports_fallback_scsv = True

        finally:
            ssl_connection.close()

        return FallbackScsvScanResult(server_info, scan_command,
                                      supports_fallback_scsv)
    def _get_and_verify_certificate_chain(
            server_info: ServerConnectivityInfo,
            trust_store: TrustStore
    ) -> Tuple[List[str], Optional[List[str]], str, Optional[OcspResponse]]:
        """Connect to the target server and uses the supplied trust store to validate the server's certificate.
        """
        ssl_connection = server_info.get_preconfigured_ssl_connection(ssl_verify_locations=trust_store.path)

        # Enable OCSP stapling
        ssl_connection.ssl_client.set_tlsext_status_ocsp()

        try:  # Perform the SSL handshake
            ssl_connection.connect()

            ocsp_response = ssl_connection.ssl_client.get_tlsext_status_ocsp_resp()
            received_chain_as_pem = ssl_connection.ssl_client.get_received_chain()
            try:
                verified_chain_as_pem = ssl_connection.ssl_client.get_verified_chain()
            except CouldNotBuildVerifiedChain:
                verified_chain_as_pem = None
            except AttributeError:
                # Only the modern SSL Client can build the verified chain; hence we get here if the server only supports
                # an older version of TLS (pre 1.2)
                verified_chain_as_pem = None

            (_, verify_str) = ssl_connection.ssl_client.get_certificate_chain_verify_result()

        except ClientCertificateRequested:  # The server asked for a client cert
            # We can get the server cert anyway
            ocsp_response = ssl_connection.ssl_client.get_tlsext_status_ocsp_resp()
            received_chain_as_pem = ssl_connection.ssl_client.get_received_chain()
            try:
                verified_chain_as_pem = ssl_connection.ssl_client.get_verified_chain()
            except CouldNotBuildVerifiedChain:
                verified_chain_as_pem = None
            except AttributeError:
                # Only the modern SSL Client can build the verified chain; hence we get here if the server only supports
                # an older version of TLS (pre 1.2)
                verified_chain_as_pem = None

            (_, verify_str) = ssl_connection.ssl_client.get_certificate_chain_verify_result()

        finally:
            ssl_connection.close()

        return received_chain_as_pem, verified_chain_as_pem, verify_str, ocsp_response
    def process_task(
            self,
            server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand
    ) -> 'FallbackScsvScanResult':
        if not isinstance(scan_command, FallbackScsvScanCommand):
            raise ValueError('Unexpected scan command')

        if server_info.highest_ssl_version_supported.value <= OpenSslVersionEnum.SSLV3.value:
            raise ValueError('Server only supports SSLv3; no downgrade attacks are possible')

        # Try with TLS 1.2 even if the server supports TLS 1.3 or higher as there is no downgrade possible with TLS 1.3
        if server_info.highest_ssl_version_supported >= OpenSslVersionEnum.TLSV1_3:
            ssl_version_to_use = OpenSslVersionEnum.TLSV1_2
        else:
            ssl_version_to_use = server_info.highest_ssl_version_supported

        # Try to connect using a lower TLS version with the fallback cipher suite enabled
        ssl_version_downgrade = OpenSslVersionEnum(ssl_version_to_use.value - 1)  # type: ignore
        ssl_connection = server_info.get_preconfigured_ssl_connection(override_ssl_version=ssl_version_downgrade)
        ssl_connection.ssl_client.enable_fallback_scsv()

        supports_fallback_scsv = False
        try:
            # Perform the SSL handshake
            ssl_connection.connect()

        except _nassl.OpenSSLError as e:
            # This is the right, specific alert the server should return
            if 'tlsv1 alert inappropriate fallback' in str(e.args):
                supports_fallback_scsv = True
            else:
                raise

        except SslHandshakeRejected:
            # If the handshake is rejected, we assume downgrade attacks are prevented (this is how F5 balancers do it)
            # although it could also be because the server does not support this version of TLS
            # https://github.com/nabla-c0d3/sslyze/issues/119
            supports_fallback_scsv = True

        finally:
            ssl_connection.close()

        return FallbackScsvScanResult(server_info, scan_command, supports_fallback_scsv)
Exemple #24
0
    def _send_robot_payload(
        server_info: ServerConnectivityInfo,
        ssl_version_to_use: OpenSslVersionEnum,
        rsa_cipher_string: str,
        robot_payload_enum: RobotPmsPaddingPayloadEnum,
        robot_should_finish_handshake: bool,
        rsa_modulus: int,
        rsa_exponent: int,
    ) -> Tuple[RobotPmsPaddingPayloadEnum, str]:
        # Do a handshake which each record and keep track of what the server returned
        ssl_connection = server_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version_to_use)

        # Replace nassl.sslClient.do_handshake() with a ROBOT checking SSL handshake so that all the SSLyze
        # options (startTLS, proxy, etc.) still work
        ssl_connection.ssl_client.do_handshake = types.MethodType(
            do_handshake_with_robot, ssl_connection.ssl_client)
        ssl_connection.ssl_client.set_cipher_list(rsa_cipher_string)

        # Compute the  payload
        cke_payload = RobotTlsRecordPayloads.get_client_key_exchange_record(
            robot_payload_enum, TlsVersionEnum[ssl_version_to_use.name],
            rsa_modulus, rsa_exponent)

        # H4ck: we need to pass some arguments to the handshake but there is no simple way to do it; we use an attribute
        ssl_connection.ssl_client._robot_cke_record = cke_payload
        ssl_connection.ssl_client._robot_should_finish_handshake = robot_should_finish_handshake

        server_response = ""
        try:
            # Start the SSL handshake
            ssl_connection.connect()
        except ServerResponseToRobot as e:
            # Should always be thrown
            server_response = e.server_response
        except socket.timeout:
            # https://github.com/nabla-c0d3/sslyze/issues/361
            server_response = "Connection timed out"
        finally:
            ssl_connection.close()

        return robot_payload_enum, server_response
    def _get_security_headers(
        cls, server_info: ServerConnectivityInfo
    ) -> Tuple[Optional[str], Optional[str], Optional[str], bool,
               List[Certificate]]:
        hpkp_report_only = False

        # Perform the SSL handshake
        ssl_connection = server_info.get_preconfigured_ssl_connection()
        try:
            ssl_connection.connect()
            certificate_chain = [
                load_pem_x509_certificate(x509_cert.as_pem().encode('ascii'),
                                          backend=default_backend()) for
                x509_cert in ssl_connection.ssl_client.get_peer_cert_chain()
            ]
            # Send an HTTP GET request to the server
            ssl_connection.ssl_client.write(
                HttpRequestGenerator.get_request(host=server_info.hostname))
            http_resp = HttpResponseParser.parse_from_ssl_connection(
                ssl_connection.ssl_client)
        finally:
            ssl_connection.close()

        if http_resp.version == 9:
            # HTTP 0.9 => Probably not an HTTP response
            raise ValueError('Server did not return an HTTP response')
        else:
            hsts_header = http_resp.getheader('strict-transport-security',
                                              None)
            hpkp_header = http_resp.getheader('public-key-pins', None)
            expect_ct_header = http_resp.getheader('expect-ct', None)
            if hpkp_header is None:
                hpkp_report_only = True
                hpkp_header = http_resp.getheader(
                    'public-key-pins-report-only', None)

        # We do not follow redirections because the security headers must be set on the first page according to
        # https://hstspreload.appspot.com/:
        # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS
        # header (rather than the page it redirects to)."
        return hsts_header, hpkp_header, expect_ct_header, hpkp_report_only, certificate_chain
Exemple #26
0
    def process_task(
            self,
            server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand
    ) -> 'CompressionScanResult':
        if not isinstance(scan_command, CompressionScanCommand):
            raise ValueError('Unexpected scan command')

        # Try with TLS 1.2 even if the server supports TLS 1.3 or higher as there is no compression with TLS 1.3
        if server_info.highest_ssl_version_supported >= OpenSslVersionEnum.TLSV1_3:
            ssl_version_to_use = OpenSslVersionEnum.TLSV1_2
        else:
            ssl_version_to_use = server_info.highest_ssl_version_supported

        ssl_connection = server_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version_to_use, should_use_legacy_openssl=True
        )

        # Make sure OpenSSL was built with support for compression to avoid false negatives
        if 'zlib compression' not in ssl_connection.ssl_client.get_available_compression_methods():
            raise RuntimeError(
                'OpenSSL was not built with support for zlib / compression. Did you build nassl yourself ?'
            )

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            compression_name = ssl_connection.ssl_client.get_current_compression_method()
        except ClientCertificateRequested:
            # The server asked for a client cert
            compression_name = ssl_connection.ssl_client.get_current_compression_method()
        except SslHandshakeRejected:
            # Should only happen when the server only supports TLS 1.3, which does not support compression
            compression_name = ''
        finally:
            ssl_connection.close()

        return CompressionScanResult(server_info, scan_command, compression_name)
    def _get_selected_cipher_suite(
            server_connectivity: ServerConnectivityInfo,
            ssl_version: OpenSslVersionEnum,
            openssl_cipher_str: str,
            should_use_legacy_openssl: Optional[bool]
    ) -> 'AcceptedCipherSuite':
        """Given an OpenSSL cipher string (which may specify multiple cipher suites), return the cipher suite that was
        selected by the server during the SSL handshake.
        """
        ssl_connection = server_connectivity.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version, should_use_legacy_openssl=should_use_legacy_openssl
        )
        ssl_connection.ssl_client.set_cipher_list(openssl_cipher_str)

        # Perform the SSL handshake
        try:
            ssl_connection.connect()
            selected_cipher = AcceptedCipherSuite.from_ongoing_ssl_connection(ssl_connection, ssl_version)
        except ClientCertificateRequested:
            selected_cipher = AcceptedCipherSuite.from_ongoing_ssl_connection(ssl_connection, ssl_version)
        finally:
            ssl_connection.close()
        return selected_cipher
Exemple #28
0
    def _send_robot_payload(
            server_info: ServerConnectivityInfo,
            rsa_cipher_string: str,
            robot_payload_enum: RobotPmsPaddingPayloadEnum,
            robot_should_finish_handshake: bool,
            rsa_modulus: int,
            rsa_exponent: int,
    ) -> Tuple[RobotPmsPaddingPayloadEnum, str]:
        # Do a handshake which each record and keep track of what the server returned
        ssl_connection = server_info.get_preconfigured_ssl_connection()

        # Replace nassl.sslClient.do_handshake() with a ROBOT checking SSL handshake so that all the SSLyze
        # options (startTLS, proxy, etc.) still work
        ssl_connection.ssl_client.do_handshake = types.MethodType(do_handshake_with_robot,
                                                                  ssl_connection.ssl_client)
        ssl_connection.ssl_client.set_cipher_list(rsa_cipher_string)

        # Compute the  payload
        cke_payload = RobotTlsRecordPayloads.get_client_key_exchange_record(
            robot_payload_enum, server_info.highest_ssl_version_supported, rsa_modulus, rsa_exponent
        )

        # H4ck: we need to pass some arguments to the handshake but there is no simple way to do it; we use an attribute
        ssl_connection.ssl_client._robot_cke_record = cke_payload
        ssl_connection.ssl_client._robot_should_finish_handshake = robot_should_finish_handshake

        server_response = ''
        try:
            # Start the SSL handshake
            ssl_connection.connect()
        except ServerResponseToRobot as e:
            # Should always be thrown
            server_response = e.server_response
        finally:
            ssl_connection.close()

        return robot_payload_enum, server_response
Exemple #29
0
    def _get_security_headers(
            cls,
            server_info: ServerConnectivityInfo
    ) -> Tuple[Optional[str], Optional[str], Optional[str], bool, List[Certificate]]:
        hpkp_report_only = False

        # Perform the SSL handshake
        ssl_connection = server_info.get_preconfigured_ssl_connection()
        try:
            ssl_connection.connect()
            certificate_chain = [
                load_pem_x509_certificate(x509_cert.as_pem().encode('ascii'), backend=default_backend())
                for x509_cert in ssl_connection.ssl_client.get_peer_cert_chain()
            ]
            # Send an HTTP GET request to the server
            ssl_connection.ssl_client.write(HttpRequestGenerator.get_request(host=server_info.hostname))
            http_resp = HttpResponseParser.parse_from_ssl_connection(ssl_connection.ssl_client)
        finally:
            ssl_connection.close()

        if http_resp.version == 9:
            # HTTP 0.9 => Probably not an HTTP response
            raise ValueError('Server did not return an HTTP response')
        else:
            hsts_header = http_resp.getheader('strict-transport-security', None)
            hpkp_header = http_resp.getheader('public-key-pins', None)
            expect_ct_header = http_resp.getheader('expect-ct', None)
            if hpkp_header is None:
                hpkp_report_only = True
                hpkp_header = http_resp.getheader('public-key-pins-report-only', None)

        # We do not follow redirections because the security headers must be set on the first page according to
        # https://hstspreload.appspot.com/:
        # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS
        # header (rather than the page it redirects to)."
        return hsts_header, hpkp_header, expect_ct_header, hpkp_report_only, certificate_chain
    def _test_cipher_suite(server_connectivity_info: ServerConnectivityInfo,
                           ssl_version: OpenSslVersionEnum,
                           openssl_cipher_name: str) -> 'CipherSuite':
        """Initiates a SSL handshake with the server using the SSL version and the cipher suite specified.
        """
        requires_legacy_openssl = True
        if ssl_version == OpenSslVersionEnum.TLSV1_2:
            # For TLS 1.2, we need to pick the right version of OpenSSL depending on which cipher suite
            requires_legacy_openssl = WorkaroundForTls12ForCipherSuites.requires_legacy_openssl(
                openssl_cipher_name)
        elif ssl_version == OpenSslVersionEnum.TLSV1_3:
            requires_legacy_openssl = False

        ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version,
            should_use_legacy_openssl=requires_legacy_openssl)

        # Only enable the cipher suite to test; not trivial anymore since OpenSSL 1.1.1 and TLS 1.3
        if ssl_version == OpenSslVersionEnum.TLSV1_3:
            # The function to control cipher suites is different for TLS 1.3
            # Disable the default, non-TLS 1.3 cipher suites
            ssl_connection.ssl_client.set_cipher_list('')
            # Enable the one TLS 1.3 cipher suite we want to test
            ssl_connection.ssl_client.set_ciphersuites(openssl_cipher_name)
        else:
            if not requires_legacy_openssl:
                # Disable the TLS 1.3 cipher suites if we are using the modern client
                ssl_connection.ssl_client.set_ciphersuites('')

            ssl_connection.ssl_client.set_cipher_list(openssl_cipher_name)

        if len(ssl_connection.ssl_client.get_cipher_list()) != 1:
            raise ValueError(
                f'Passed an OpenSSL string for multiple cipher suites: "{openssl_cipher_name}": '
                f'{str(ssl_connection.ssl_client.get_cipher_list())}')

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            cipher_result: CipherSuite = AcceptedCipherSuite.from_ongoing_ssl_connection(
                ssl_connection, ssl_version)

        except SslHandshakeRejected as e:
            cipher_result = RejectedCipherSuite(openssl_cipher_name,
                                                ssl_version, str(e))

        except ClientCertificateRequested:
            # TODO(AD): Sometimes get_current_cipher_name() called in from_ongoing_ssl_connection() will return None
            # When the handshake failed due to ClientCertificateRequested
            # We need to rewrite this logic to not use OpenSSL for looking up key size and RFC names as it is
            # too complicated
            # cipher_result = AcceptedCipherSuite.from_ongoing_ssl_connection(ssl_connection, ssl_version)
            # The ClientCertificateRequested exception already proves that the cipher suite was accepted
            # Workaround here:
            cipher_result = AcceptedCipherSuite(openssl_cipher_name,
                                                ssl_version, None, None)

        except Exception as e:
            cipher_result = ErroredCipherSuite(openssl_cipher_name,
                                               ssl_version, e)

        finally:
            ssl_connection.close()

        return cipher_result
    def process_task(
            self, server_connectivity_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand) -> 'CipherSuiteScanResult':
        if not isinstance(scan_command, CipherSuiteScanCommand):
            raise ValueError('Unexpected scan command')

        ssl_version = self.SSL_VERSIONS_MAPPING[scan_command.__class__]
        # Get the list of available cipher suites for the given ssl version
        cipher_list: List[str] = []
        if ssl_version == OpenSslVersionEnum.TLSV1_2:
            # For TLS 1.2, we have to use both the legacy and modern OpenSSL to cover all cipher suites
            ssl_connection_legacy = server_connectivity_info.get_preconfigured_ssl_connection(
                override_ssl_version=ssl_version,
                should_use_legacy_openssl=True)
            ssl_connection_legacy.ssl_client.set_cipher_list(
                'ALL:COMPLEMENTOFALL:-PSK:-SRP')
            cipher_list.extend(
                ssl_connection_legacy.ssl_client.get_cipher_list())

            ssl_connection_modern = server_connectivity_info.get_preconfigured_ssl_connection(
                override_ssl_version=ssl_version,
                should_use_legacy_openssl=False)
            # Disable the TLS 1.3 cipher suites with the new OpenSSL API
            ssl_connection_modern.ssl_client.set_ciphersuites('')
            # Enable all other cipher suites
            ssl_connection_modern.ssl_client.set_cipher_list(
                'ALL:COMPLEMENTOFALL:-PSK:-SRP')
            cipher_list.extend(
                ssl_connection_modern.ssl_client.get_cipher_list())

            # And remove duplicates (ie. supported by both legacy and modern OpenSSL)
            cipher_list = list(set(cipher_list))
        elif ssl_version == OpenSslVersionEnum.TLSV1_3:
            # TLS 1.3 only has 5 cipher suites so we can hardcode them
            cipher_list = [
                'TLS_AES_256_GCM_SHA384',
                'TLS_CHACHA20_POLY1305_SHA256',
                'TLS_AES_128_GCM_SHA256',
                'TLS_AES_128_CCM_8_SHA256',
                'TLS_AES_128_CCM_SHA256',
            ]
        else:
            ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection(
                override_ssl_version=ssl_version)
            # Disable SRP and PSK cipher suites as they need a special setup in the client and are never used
            ssl_connection.ssl_client.set_cipher_list(
                'ALL:COMPLEMENTOFALL:-PSK:-SRP')
            # And remove TLS 1.3 cipher suites
            cipher_list = [
                cipher
                for cipher in ssl_connection.ssl_client.get_cipher_list()
                if 'TLS13' not in cipher
            ]

        # Scan for every available cipher suite
        thread_pool = ThreadPool()
        for cipher in cipher_list:
            thread_pool.add_job(
                (self._test_cipher_suite,
                 [server_connectivity_info, ssl_version, cipher]))

        # Start processing the jobs; One thread per cipher
        thread_pool.start(nb_threads=min(len(cipher_list), self.MAX_THREADS))

        accepted_cipher_list = []
        rejected_cipher_list = []
        errored_cipher_list = []

        # Store the results as they come
        for completed_job in thread_pool.get_result():
            (job, cipher_result) = completed_job
            if isinstance(cipher_result, AcceptedCipherSuite):
                accepted_cipher_list.append(cipher_result)
            elif isinstance(cipher_result, RejectedCipherSuite):
                rejected_cipher_list.append(cipher_result)
            elif isinstance(cipher_result, ErroredCipherSuite):
                errored_cipher_list.append(cipher_result)
            else:
                raise ValueError('Unexpected result')

        # Store thread pool errors; only something completely unexpected would trigger an error
        for failed_job in thread_pool.get_error():
            (_, exception) = failed_job
            raise exception

        thread_pool.join()

        # Test for the cipher suite preference
        preferred_cipher = self._get_preferred_cipher_suite(
            server_connectivity_info, ssl_version, accepted_cipher_list)

        # Generate the results
        plugin_result = CipherSuiteScanResult(server_connectivity_info,
                                              scan_command, preferred_cipher,
                                              accepted_cipher_list,
                                              rejected_cipher_list,
                                              errored_cipher_list)
        return plugin_result
    def process_task(
            self, server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand) -> "HttpHeadersScanResult":
        if not isinstance(scan_command, HttpHeadersScanCommand):
            raise ValueError("Unexpected scan command")

        if server_info.tls_wrapped_protocol not in [
                TlsWrappedProtocolEnum.PLAIN_TLS, TlsWrappedProtocolEnum.HTTPS
        ]:
            raise ValueError(
                "Cannot test for HTTP headers on a StartTLS connection.")

        # Perform the SSL handshake
        mozilla_store = TrustStoresRepository.get_default().get_main_store()
        ssl_connection = server_info.get_preconfigured_ssl_connection(
            ssl_verify_locations=mozilla_store.path)
        try:
            ssl_connection.connect()
            try:
                verified_chain_as_pem = ssl_connection.ssl_client.get_verified_chain(
                )
            except CouldNotBuildVerifiedChain:
                verified_chain_as_pem = None

            # Send an HTTP GET request to the server
            ssl_connection.ssl_client.write(
                HttpRequestGenerator.get_request(host=server_info.hostname))

            # We do not follow redirections because the security headers must be set on the first page according to
            # https://hstspreload.appspot.com/:
            # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS
            # header (rather than the page it redirects to)."
            http_response = HttpResponseParser.parse_from_ssl_connection(
                ssl_connection.ssl_client)
        finally:
            ssl_connection.close()

        if http_response.version == 9:
            # HTTP 0.9 => Probably not an HTTP response
            raise ValueError("Server did not return an HTTP response")

        # Parse the certificate chain
        verified_chain = ([
            load_pem_x509_certificate(cert_as_pem.encode("ascii"),
                                      backend=default_backend())
            for cert_as_pem in verified_chain_as_pem
        ] if verified_chain_as_pem else None)

        # Parse each header
        hsts_header = StrictTransportSecurityHeader.from_http_response(
            http_response)
        expect_ct_header = ExpectCtHeader.from_http_response(http_response)
        hpkp_header = PublicKeyPinsHeader.from_http_response(http_response)
        hpkp_report_only_header = PublicKeyPinsReportOnlyHeader.from_http_response(
            http_response)

        return HttpHeadersScanResult(
            server_info,
            scan_command,
            hsts_header,
            hpkp_header,
            hpkp_report_only_header,
            expect_ct_header,
            verified_chain,
        )
    def perform(
            self,
            network_timeout: Optional[int] = 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:
            network_timeout: Network timeout value in seconds passed to the underlying socket.

        Returns:
            An object encapsulating all the information needed to connect to the server, to be
            passed to a `SynchronousScanner` or `ConcurrentScanner` in order to run scan commands on the server.

        Raises:
            ServerConnectivityError: If the server was not reachable or an SSL/TLS handshake could not be completed.
        """
        # First do a DNS lookup if we don't already have an IP address and we are not using a proxy
        if not self.ip_address and not self.http_tunneling_settings:
            try:
                self.ip_address = self._do_dns_lookup(self.hostname, self.port)
            except (socket.gaierror, IndexError, ConnectionError):
                raise ServerNotReachableError(
                    self, self.CONNECTIVITY_ERROR_NAME_NOT_RESOLVED)

        # Then try to connect
        client_auth_requirement = ClientAuthenticationServerConfigurationEnum.DISABLED
        ssl_connection = SslConnectionConfigurator.get_connection(
            ssl_version=OpenSslVersionEnum.SSLV23,
            server_info=self,
            should_ignore_client_auth=True,
        )

        # First only try a socket connection
        try:
            ssl_connection.do_pre_handshake(network_timeout=network_timeout)

        # Socket errors
        except socket.timeout:  # Host is down
            raise ServerNotReachableError(self,
                                          self.CONNECTIVITY_ERROR_TIMEOUT)
        except ConnectionError:
            raise ServerNotReachableError(self,
                                          self.CONNECTIVITY_ERROR_REJECTED)

        # StartTLS errors
        except StartTlsError as e:
            raise ServerTlsConfigurationNotSuportedError(self, e.args[0])

        # Proxy errors
        except ProxyError as e:
            raise ProxyConnectivityError(self, e.args[0])

        # Other errors
        except Exception as e:
            raise ServerConnectivityError(
                self, '{0}: {1}'.format(str(type(e).__name__), e.args[0]))

        finally:
            ssl_connection.close()

        # Then try to complete an SSL handshake to figure out the SSL version and cipher supported by the server
        ssl_version_supported = None
        ssl_cipher_supported = None

        # TODO(AD): Switch to using the protocol discovery logic available in OpenSSL 1.1.0 with TLS_client_method()
        # TODO(AD): Once TLS 1.3 is widely used, put it at the beginning of the list
        for ssl_version in [
                OpenSslVersionEnum.TLSV1_2, OpenSslVersionEnum.TLSV1_1,
                OpenSslVersionEnum.TLSV1, OpenSslVersionEnum.SSLV3,
                OpenSslVersionEnum.TLSV1_3, OpenSslVersionEnum.SSLV23
        ]:
            # First try the default cipher list, and then all ciphers
            for cipher_list in [
                    SslConnectionConfigurator.DEFAULT_SSL_CIPHER_LIST,
                    'ALL:COMPLEMENTOFALL:-PSK:-SRP'
            ]:
                ssl_connection = SslConnectionConfigurator.get_connection(
                    ssl_version=ssl_version,
                    server_info=self,
                    openssl_cipher_string=cipher_list,
                    should_ignore_client_auth=False,
                )
                try:
                    # Only do one attempt when testing connectivity
                    ssl_connection.connect(network_timeout=network_timeout,
                                           network_max_retries=0)
                    ssl_version_supported = ssl_version
                    ssl_cipher_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
                    ssl_version_supported = ssl_version
                    ssl_cipher_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 = SslConnectionConfigurator.get_connection(
                        ssl_version=ssl_version,
                        server_info=self,
                        openssl_cipher_string=cipher_list,
                        should_ignore_client_auth=True,
                    )
                    try:
                        ssl_connection_auth.connect(
                            network_timeout=network_timeout,
                            network_max_retries=0)
                        ssl_cipher_supported = ssl_connection_auth.ssl_client.get_current_cipher_name(
                        )
                        client_auth_requirement = ClientAuthenticationServerConfigurationEnum.OPTIONAL

                    # If client authentication is required, we either get a ClientCertificateRequested
                    except ClientCertificateRequested:
                        client_auth_requirement = ClientAuthenticationServerConfigurationEnum.REQUIRED
                    # Or a SSLHandshakeRejected
                    except SslHandshakeRejected:
                        client_auth_requirement = ClientAuthenticationServerConfigurationEnum.REQUIRED
                    # Or a bad certificate alert (https://github.com/nabla-c0d3/sslyze/issues/313 )
                    except OpenSSLError as e:
                        if 'alert bad certificate' in e.args[0]:
                            client_auth_requirement = ClientAuthenticationServerConfigurationEnum.REQUIRED
                    except Exception:
                        # Could not complete a handshake with this server
                        pass
                    finally:
                        ssl_connection_auth.close()

                except Exception:
                    # Could not complete a handshake with this server
                    pass
                finally:
                    ssl_connection.close()

            if ssl_cipher_supported:
                # A handshake was successful
                break

        if ssl_version_supported is None or ssl_cipher_supported is None:
            raise ServerTlsConfigurationNotSuportedError(
                self, self.CONNECTIVITY_ERROR_HANDSHAKE_ERROR)

        return ServerConnectivityInfo(
            hostname=self.hostname,
            port=self.port,
            ip_address=self.ip_address,
            tls_wrapped_protocol=self.tls_wrapped_protocol,
            tls_server_name_indication=self.tls_server_name_indication,
            highest_ssl_version_supported=ssl_version_supported,
            openssl_cipher_string_supported=ssl_cipher_supported,
            client_auth_requirement=client_auth_requirement,
            xmpp_to_hostname=self.xmpp_to_hostname,
            client_auth_credentials=self.client_auth_credentials,
            http_tunneling_settings=self.http_tunneling_settings)
    def _test_cipher_suite(
            server_connectivity_info: ServerConnectivityInfo,
            ssl_version: OpenSslVersionEnum,
            openssl_cipher_name: str
    ) -> 'CipherSuite':
        """Initiates a SSL handshake with the server using the SSL version and the cipher suite specified.
        """
        requires_legacy_openssl = True
        if ssl_version == OpenSslVersionEnum.TLSV1_2:
            # For TLS 1.2, we need to pick the right version of OpenSSL depending on which cipher suite
            requires_legacy_openssl = WorkaroundForTls12ForCipherSuites.requires_legacy_openssl(openssl_cipher_name)
        elif ssl_version == OpenSslVersionEnum.TLSV1_3:
            requires_legacy_openssl = False

        ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection(
            override_ssl_version=ssl_version,
            should_use_legacy_openssl=requires_legacy_openssl
        )

        # Only enable the cipher suite to test; not trivial anymore since OpenSSL 1.1.1 and TLS 1.3
        if ssl_version == OpenSslVersionEnum.TLSV1_3:
            # The function to control cipher suites is different for TLS 1.3
            # Disable the default, non-TLS 1.3 cipher suites
            ssl_connection.ssl_client.set_cipher_list('')
            # Enable the one TLS 1.3 cipher suite we want to test
            ssl_connection.ssl_client.set_ciphersuites(openssl_cipher_name)
        else:
            if not requires_legacy_openssl:
                # Disable the TLS 1.3 cipher suites if we are using the modern client
                ssl_connection.ssl_client.set_ciphersuites('')

            ssl_connection.ssl_client.set_cipher_list(openssl_cipher_name)

        if len(ssl_connection.ssl_client.get_cipher_list()) != 1:
            raise ValueError(f'Passed an OpenSSL string for multiple cipher suites: "{openssl_cipher_name}": '
                             f'{str(ssl_connection.ssl_client.get_cipher_list())}')

        try:
            # Perform the SSL handshake
            ssl_connection.connect()
            cipher_result: CipherSuite = AcceptedCipherSuite.from_ongoing_ssl_connection(ssl_connection, ssl_version)

        except SslHandshakeRejected as e:
            cipher_result = RejectedCipherSuite(openssl_cipher_name, ssl_version, str(e))

        except ClientCertificateRequested:
            # TODO(AD): Sometimes get_current_cipher_name() called in from_ongoing_ssl_connection() will return None
            # When the handshake failed due to ClientCertificateRequested
            # We need to rewrite this logic to not use OpenSSL for looking up key size and RFC names as it is
            # too complicated
            # cipher_result = AcceptedCipherSuite.from_ongoing_ssl_connection(ssl_connection, ssl_version)
            # The ClientCertificateRequested exception already proves that the cipher suite was accepted
            # Workaround here:
            cipher_result = AcceptedCipherSuite(openssl_cipher_name, ssl_version, None, None)

        except Exception as e:
            cipher_result = ErroredCipherSuite(openssl_cipher_name, ssl_version, e)

        finally:
            ssl_connection.close()

        return cipher_result
    def process_task(
            self,
            server_connectivity_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand
    ) -> 'CipherSuiteScanResult':
        if not isinstance(scan_command, CipherSuiteScanCommand):
            raise ValueError('Unexpected scan command')

        ssl_version = self.SSL_VERSIONS_MAPPING[scan_command.__class__]
        # Get the list of available cipher suites for the given ssl version
        cipher_list: List[str] = []
        if ssl_version == OpenSslVersionEnum.TLSV1_2:
            # For TLS 1.2, we have to use both the legacy and modern OpenSSL to cover all cipher suites
            ssl_connection_legacy = server_connectivity_info.get_preconfigured_ssl_connection(
                override_ssl_version=ssl_version, should_use_legacy_openssl=True
            )
            ssl_connection_legacy.ssl_client.set_cipher_list('ALL:COMPLEMENTOFALL:-PSK:-SRP')
            cipher_list.extend(ssl_connection_legacy.ssl_client.get_cipher_list())

            ssl_connection_modern = server_connectivity_info.get_preconfigured_ssl_connection(
                override_ssl_version=ssl_version, should_use_legacy_openssl=False
            )
            # Disable the TLS 1.3 cipher suites with the new OpenSSL API
            ssl_connection_modern.ssl_client.set_ciphersuites('')
            # Enable all other cipher suites
            ssl_connection_modern.ssl_client.set_cipher_list('ALL:COMPLEMENTOFALL:-PSK:-SRP')
            cipher_list.extend(ssl_connection_modern.ssl_client.get_cipher_list())

            # And remove duplicates (ie. supported by both legacy and modern OpenSSL)
            cipher_list = list(set(cipher_list))
        elif ssl_version == OpenSslVersionEnum.TLSV1_3:
            # TLS 1.3 only has 5 cipher suites so we can hardcode them
            cipher_list = [
                'TLS_AES_256_GCM_SHA384',
                'TLS_CHACHA20_POLY1305_SHA256',
                'TLS_AES_128_GCM_SHA256',
                'TLS_AES_128_CCM_8_SHA256',
                'TLS_AES_128_CCM_SHA256',
            ]
        else:
            ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection(override_ssl_version=ssl_version)
            # Disable SRP and PSK cipher suites as they need a special setup in the client and are never used
            ssl_connection.ssl_client.set_cipher_list('ALL:COMPLEMENTOFALL:-PSK:-SRP')
            # And remove TLS 1.3 cipher suites
            cipher_list = [cipher for cipher in ssl_connection.ssl_client.get_cipher_list() if 'TLS13' not in cipher]

        # Scan for every available cipher suite
        thread_pool = ThreadPool()
        for cipher in cipher_list:
            thread_pool.add_job((self._test_cipher_suite, [server_connectivity_info, ssl_version, cipher]))

        # Start processing the jobs; One thread per cipher
        thread_pool.start(nb_threads=min(len(cipher_list), self.MAX_THREADS))

        accepted_cipher_list = []
        rejected_cipher_list = []
        errored_cipher_list = []

        # Store the results as they come
        for completed_job in thread_pool.get_result():
            (job, cipher_result) = completed_job
            if isinstance(cipher_result, AcceptedCipherSuite):
                accepted_cipher_list.append(cipher_result)
            elif isinstance(cipher_result, RejectedCipherSuite):
                rejected_cipher_list.append(cipher_result)
            elif isinstance(cipher_result, ErroredCipherSuite):
                errored_cipher_list.append(cipher_result)
            else:
                raise ValueError('Unexpected result')

        # Store thread pool errors; only something completely unexpected would trigger an error
        for failed_job in thread_pool.get_error():
            (_, exception) = failed_job
            raise exception

        thread_pool.join()

        # Test for the cipher suite preference
        preferred_cipher = self._get_preferred_cipher_suite(server_connectivity_info, ssl_version, accepted_cipher_list)

        # Generate the results
        plugin_result = CipherSuiteScanResult(server_connectivity_info, scan_command, preferred_cipher,
                                              accepted_cipher_list, rejected_cipher_list, errored_cipher_list)
        return plugin_result