Example #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)
Example #2
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()
            cert_as_pem = ssl_connection.ssl_client.get_received_chain()[0]
            parsed_cert = load_pem_x509_certificate(
                cert_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
Example #3
0
    def process_task(
            self, server_info: ServerConnectivityInfo,
            scan_command: PluginScanCommand) -> "HeartbleedScanResult":
        if not isinstance(scan_command, HeartbleedScanCommand):
            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 Heartbleed
            return HeartbleedScanResult(server_info, scan_command, False)

        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)
    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
Example #5
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
Example #6
0
    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,
        )
Example #7
0
    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/sslyzeslow/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/sslyzeslow/issues/290
                    accepts_client_renegotiation = False
                else:
                    raise

            except ConnectionError:
                accepts_client_renegotiation = False

        finally:
            ssl_connection.close()

        return accepts_client_renegotiation
Example #8
0
    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/sslyzeslow/issues/119
            supports_fallback_scsv = True

        finally:
            ssl_connection.close()

        return FallbackScsvScanResult(server_info, scan_command,
                                      supports_fallback_scsv)
Example #9
0
    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
Example #10
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/sslyzeslow/issues/361
            server_response = "Connection timed out"
        finally:
            ssl_connection.close()

        return robot_payload_enum, server_response
Example #11
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 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 ServerHostnameCouldNotBeResolved(self)

        # 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 ConnectionToServerTimedOut(self)
        except ConnectionError:
            raise ServerRejectedConnection(self)

        # 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()
        for ssl_version in [
                OpenSslVersionEnum.TLSV1_3,
                OpenSslVersionEnum.TLSV1_2,
                OpenSslVersionEnum.TLSV1_1,
                OpenSslVersionEnum.TLSV1,
                OpenSslVersionEnum.SSLV3,
                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 sslyzeslow
                    # 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
                    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,
                "Could not complete an SSL/TLS handshake with the server")

        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,
        )
Example #13
0
    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
Example #14
0
    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