def _test_early_data_support(server_info: ServerConnectivityInfo) -> bool: session = None is_early_data_supported = False ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=TlsVersionEnum.TLS_1_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.server_location.hostname)) ssl_connection.ssl_client.read(2048) session = ssl_connection.ssl_client.get_session() except ServerRejectedTlsHandshake: # TLS 1.3 not supported is_early_data_supported = False except TlsHandshakeTimedOut: # Sometimes triggered by servers that don't support TLS 1.3 at all, such as Amazon Cloudfront is_early_data_supported = False except socket.timeout: # Some servers just don't answer the read() call 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_tls_connection( override_tls_version=TlsVersionEnum.TLS_1_3) if not isinstance(ssl_connection2.ssl_client, SslClient): raise RuntimeError("Should never happen") ssl_connection2.ssl_client.set_session(session) try: # Open a socket to the server but don't do the actual TLS handshake ssl_connection2._do_pre_handshake() # 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 is_early_data_supported
def get_certificate_chain( server_info: ServerConnectivityInfo, custom_ca_file: Optional[Path], tls_version: Optional[TlsVersionEnum], openssl_cipher_string: Optional[str], ) -> Tuple[List[str], Optional[nassl.ocsp_response.OcspResponse], Optional[Path]]: ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version) if openssl_cipher_string: ssl_connection.ssl_client.set_cipher_list(openssl_cipher_string) # Enable OCSP stapling ssl_connection.ssl_client.set_tlsext_status_ocsp() try: 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() except ClientCertificateRequested: ocsp_response = ssl_connection.ssl_client.get_tlsext_status_ocsp_resp() received_chain_as_pem = ssl_connection.ssl_client.get_received_chain() finally: ssl_connection.close() return received_chain_as_pem, ocsp_response, custom_ca_file
def _test_heartbleed(server_info: ServerConnectivityInfo) -> bool: if server_info.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value: # The server uses a recent version of OpenSSL and it cannot be vulnerable to Heartbleed return False # Disable SNI for this check because some legacy servers don't support sending the heartbleed payload and SNI # See https://github.com/nabla-c0d3/sslyze/issues/202 ssl_connection = server_info.get_preconfigured_tls_connection( should_enable_server_name_indication=False) # 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( # type: ignore _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 is_vulnerable_to_heartbleed
def _test_for_ccs_injection(server_info: ServerConnectivityInfo) -> bool: if server_info.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value: # The server uses a recent version of OpenSSL and it cannot be vulnerable to CCS Injection return False ssl_connection = server_info.get_preconfigured_tls_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( # type: ignore _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 is_vulnerable
def _get_rsa_parameters( server_info: ServerConnectivityInfo, tls_version: TlsVersionEnum, openssl_cipher_string: str ) -> Optional[RSAPublicNumbers]: ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version, should_use_legacy_openssl=True, ) 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 ServerRejectedTlsHandshake: # 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: public_key = parsed_cert.public_key() if isinstance(public_key, RSAPublicKey): return public_key.public_numbers() else: return None else: return None
def resume_tls_session( server_info: ServerConnectivityInfo, tls_version_to_use: OpenSslVersionEnum, tls_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_tls_connection( override_tls_version=tls_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 tls_session: ssl_connection.ssl_client.set_session(tls_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 _test_secure_renegotiation(server_info: ServerConnectivityInfo) -> Tuple[_ScanJobResultEnum, bool]: """Check whether the server supports secure renegotiation. """ # Try with TLS 1.2 even if the server supports TLS 1.3 or higher as there is no reneg with TLS 1.3 if server_info.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value: tls_version_to_use = TlsVersionEnum.TLS_1_2 downgraded_from_tls_1_3 = True else: tls_version_to_use = server_info.tls_probing_result.highest_tls_version_supported downgraded_from_tls_1_3 = False ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version_to_use, should_use_legacy_openssl=True, # Only the legacy SSL client has methods to check for secure reneg ) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") try: # Perform the TLS handshake ssl_connection.connect() supports_secure_renegotiation = ssl_connection.ssl_client.get_secure_renegotiation_support() # Should only happen when the server only supports TLS 1.3 except ServerRejectedTlsHandshake: if downgraded_from_tls_1_3: supports_secure_renegotiation = True # Technically TLS 1.3 has no renegotiation therefore it is secure else: raise finally: ssl_connection.close() return _ScanJobResultEnum.SUPPORTS_SECURE_RENEG, supports_secure_renegotiation
def _test_client_renegotiation( server_info: ServerConnectivityInfo, tls_version_to_use: TlsVersionEnum) -> Tuple[_ScanJobResultEnum, bool]: """Check whether the server honors session renegotiation requests. """ ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version_to_use, should_use_legacy_openssl=True) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") 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 ConnectionError: accepts_client_renegotiation = False except OSError as e: # OSError is the parent of all (non-TLS) socket/connection errors so it should be last if "Nassl SSL handshake failed" in e.args[0]: # Special error returned by nassl accepts_client_renegotiation = False else: raise except OpenSSLError as e: if "handshake failure" in e.args[0]: accepts_client_renegotiation = False elif "no renegotiation" in e.args[0]: accepts_client_renegotiation = False elif "tlsv1 unrecognized name" in e.args[0]: # Yahoo's very own way of rejecting a renegotiation accepts_client_renegotiation = False elif "tlsv1 alert internal error" in e.args[0]: # Jetty server: https://github.com/nabla-c0d3/sslyze/issues/290 accepts_client_renegotiation = False elif "decryption failed or bad record mac" in e.args[0]: # Some servers such as reddit.com accepts_client_renegotiation = False elif "sslv3 alert unexpected message" in e.args[0]: # traefik https://github.com/nabla-c0d3/sslyze/issues/422 accepts_client_renegotiation = False else: raise finally: ssl_connection.close() return _ScanJobResultEnum.ACCEPTS_CLIENT_RENEG, accepts_client_renegotiation
def _send_robot_payload( server_info: ServerConnectivityInfo, tls_version_to_use: TlsVersionEnum, rsa_cipher_string: str, robot_payload_enum: RobotPmsPaddingPayloadEnum, robot_should_finish_handshake: bool, rsa_modulus: int, rsa_exponent: int, ) -> str: # Do a handshake which each record and keep track of what the server returned ssl_connection = server_info.get_preconfigured_tls_connection(override_tls_version=tls_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( # type: ignore do_handshake_with_robot, ssl_connection.ssl_client ) ssl_connection.ssl_client.set_cipher_list(rsa_cipher_string) # Compute the payload tls_parser_tls_version: tls_parser.tls_version.TlsVersionEnum if tls_version_to_use == TlsVersionEnum.SSL_3_0: tls_parser_tls_version = tls_parser.tls_version.TlsVersionEnum.SSLV3 elif tls_version_to_use == TlsVersionEnum.TLS_1_0: tls_parser_tls_version = tls_parser.tls_version.TlsVersionEnum.TLSV1 elif tls_version_to_use == TlsVersionEnum.TLS_1_1: tls_parser_tls_version = tls_parser.tls_version.TlsVersionEnum.TLSV1_1 elif tls_version_to_use == TlsVersionEnum.TLS_1_2: tls_parser_tls_version = tls_parser.tls_version.TlsVersionEnum.TLSV1_2 else: raise ValueError("Should never happen") cke_payload = _RobotTlsRecordPayloads.get_client_key_exchange_record( robot_payload_enum, tls_parser_tls_version, 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 # type: ignore ssl_connection.ssl_client._robot_should_finish_handshake = robot_should_finish_handshake # type: ignore 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 server_response
def connect_with_cipher_suite( server_connectivity_info: ServerConnectivityInfo, tls_version: TlsVersionEnum, cipher_suite: CipherSuite ) -> Union[CipherSuiteAcceptedByServer, CipherSuiteRejectedByServer]: """Initiates a SSL handshake with the server using the SSL version and the cipher suite specified. """ requires_legacy_openssl = True if tls_version == TlsVersionEnum.TLS_1_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( cipher_suite.openssl_name) elif tls_version == TlsVersionEnum.TLS_1_3: requires_legacy_openssl = False ssl_connection = server_connectivity_info.get_preconfigured_tls_connection( override_tls_version=tls_version, should_use_legacy_openssl=requires_legacy_openssl) _set_cipher_suite_string(tls_version, cipher_suite.openssl_name, ssl_connection.ssl_client) ephemeral_key = None try: # Perform the SSL handshake ssl_connection.connect() ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key() except ServerTlsConfigurationNotSupported: # SSLyze rejected the handshake because the server's DH config was too insecure; this means the # cipher suite is actually supported pass except ClientCertificateRequested: # When the handshake failed due to ClientCertificateRequested ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key() pass except ServerRejectedTlsHandshake as e: return CipherSuiteRejectedByServer(cipher_suite=cipher_suite, error_message=e.error_message) except TlsHandshakeTimedOut as e: # Sometimes triggered by servers that don't support (at all) a specific version of TLS # Amazon Cloudfront does that with TLS 1.3 # There's no easy way to differentiate this error from a network glitch/timeout return CipherSuiteRejectedByServer(cipher_suite=cipher_suite, error_message=e.error_message) finally: ssl_connection.close() return CipherSuiteAcceptedByServer(cipher_suite=cipher_suite, ephemeral_key=ephemeral_key)
def _test_client_renegotiation( server_info: ServerConnectivityInfo, tls_version_to_use: OpenSslVersionEnum ) -> Tuple[_ScanJobResultEnum, bool]: """Check whether the server honors session renegotiation requests. """ ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version_to_use, should_use_legacy_openssl=True) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") 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 finally: ssl_connection.close() return _ScanJobResultEnum.ACCEPTS_CLIENT_RENEG, accepts_client_renegotiation
def _get_selected_cipher_suite(server_connectivity: ServerConnectivityInfo, tls_version: OpenSslVersionEnum, openssl_cipher_string: str) -> str: ssl_connection = server_connectivity.get_preconfigured_tls_connection( override_tls_version=tls_version) ssl_connection.ssl_client.set_cipher_list(openssl_cipher_string) # Perform the SSL handshake try: ssl_connection.connect() return ssl_connection.ssl_client.get_current_cipher_name() except ClientCertificateRequested: # TODO(AD): Sometimes get_current_cipher_name() called in from_ongoing_ssl_connection() will return None return ssl_connection.ssl_client.get_current_cipher_name() finally: ssl_connection.close()
def _retrieve_and_analyze_http_response( server_info: ServerConnectivityInfo) -> HttpHeadersScanResult: # Send HTTP requests until we no longer received an HTTP redirection, but allow only 4 redirections max redirections_count = 0 next_location_path: Optional[str] = "/" while next_location_path and redirections_count < 4: ssl_connection = server_info.get_preconfigured_tls_connection() try: # Perform the TLS handshake ssl_connection.connect() # Send an HTTP GET request to the server ssl_connection.ssl_client.write( HttpRequestGenerator.get_request( host=server_info.network_configuration. tls_server_name_indication, path=next_location_path)) 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") # Handle redirection if there is one next_location_path = _detect_http_redirection( http_response=http_response, server_host_name=server_info.network_configuration. tls_server_name_indication, server_port=server_info.server_location.port, ) redirections_count += 1 # Parse and return each header return HttpHeadersScanResult( strict_transport_security_header=_parse_hsts_header_from_http_response( http_response), public_key_pins_header=_parse_hpkp_header_from_http_response( http_response), public_key_pins_report_only_header= _parse_hpkp_report_only_header_from_http_response(http_response), expect_ct_header=_parse_expect_ct_header_from_http_response( http_response), )
def _test_compression_support(server_info: ServerConnectivityInfo) -> bool: # 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.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value: tls_version_to_use = TlsVersionEnum.TLS_1_2 downgraded_from_tls_1_3 = True else: tls_version_to_use = server_info.tls_probing_result.highest_tls_version_supported downgraded_from_tls_1_3 = False ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version_to_use, should_use_legacy_openssl= True, # Only the legacy SSL client has methods to check for compression support ) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") # 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 ?" ) compression_name: Optional[str] try: # Perform the TLS handshake ssl_connection.connect() compression_name = ssl_connection.ssl_client.get_current_compression_method( ) except ClientCertificateRequested: compression_name = ssl_connection.ssl_client.get_current_compression_method( ) # Should only happen when the server only supports TLS 1.3, which does not support compression except ServerRejectedTlsHandshake: if downgraded_from_tls_1_3: compression_name = None else: raise finally: ssl_connection.close() return True if compression_name else False
def _test_scsv(server_info: ServerConnectivityInfo) -> bool: # 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.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value: ssl_version_to_use = TlsVersionEnum.TLS_1_2 else: ssl_version_to_use = server_info.tls_probing_result.highest_tls_version_supported # Try to connect using a lower TLS version with the fallback cipher suite enabled ssl_version_downgrade = TlsVersionEnum(ssl_version_to_use.value - 1) ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=ssl_version_downgrade, # Only the legacy client has enable_fallback_scsv() should_use_legacy_openssl=True, ) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") 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 ServerRejectedTlsHandshake: # 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 except TlsHandshakeTimedOut: # Sometimes triggered by servers that don't support (at all) a specific version of TLS # Amazon Cloudfront does that with TLS 1.3 supports_fallback_scsv = True finally: ssl_connection.close() return supports_fallback_scsv
def retrieve_tls_session( server_info: ServerConnectivityInfo, session_to_resume: 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. """ # Try with TLS 1.2 even if the server supports TLS 1.3 or higher as there is no session resumption (with IDs or # tickets) with TLS 1.3 if server_info.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value: tls_version_to_use = TlsVersionEnum.TLS_1_2 downgraded_from_tls_1_3 = True else: tls_version_to_use = server_info.tls_probing_result.highest_tls_version_supported downgraded_from_tls_1_3 = False ssl_connection = server_info.get_preconfigured_tls_connection(override_tls_version=tls_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 session_to_resume: ssl_connection.ssl_client.set_session(session_to_resume) try: # Perform the TLS handshake ssl_connection.connect() new_session = ssl_connection.ssl_client.get_session() # Get session data except ServerRejectedTlsHandshake: if downgraded_from_tls_1_3: raise ServerOnlySupportsTls13() else: raise finally: ssl_connection.close() return new_session
def _test_secure_renegotiation( server_info: ServerConnectivityInfo, tls_version_to_use: TlsVersionEnum) -> Tuple[_ScanJobResultEnum, bool]: """Check whether the server supports secure renegotiation. """ ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version_to_use, should_use_legacy_openssl=True) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") try: # Perform the SSL handshake ssl_connection.connect() supports_secure_renegotiation = ssl_connection.ssl_client.get_secure_renegotiation_support( ) finally: ssl_connection.close() return _ScanJobResultEnum.SUPPORTS_SECURE_RENEG, supports_secure_renegotiation
def _retrieve_and_analyze_http_response( server_info: ServerConnectivityInfo) -> HttpHeadersScanResult: # Perform the TLS handshake ssl_connection = server_info.get_preconfigured_tls_connection() try: ssl_connection.connect() # Send an HTTP GET request to the server ssl_connection.ssl_client.write( HttpRequestGenerator.get_request( host=server_info.network_configuration. tls_server_name_indication)) # 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 and return each header return HttpHeadersScanResult( strict_transport_security_header=_parse_hsts_header_from_http_response( http_response), public_key_pins_header=_parse_hpkp_header_from_http_response( http_response), public_key_pins_report_only_header= _parse_hpkp_report_only_header_from_http_response(http_response), expect_ct_header=_parse_expect_ct_header_from_http_response( http_response), )
def _test_compression_support(server_info: ServerConnectivityInfo) -> bool: # 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.tls_probing_result.highest_tls_version_supported >= OpenSslVersionEnum.TLSV1_3: ssl_version_to_use = OpenSslVersionEnum.TLSV1_2 else: ssl_version_to_use = server_info.tls_probing_result.highest_tls_version_supported ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=ssl_version_to_use, should_use_legacy_openssl=True) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") # 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 ?" ) compression_name: Optional[str] = None 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 ServerRejectedTlsHandshake: # Should only happen when the server only supports TLS 1.3, which does not support compression pass finally: ssl_connection.close() return True if compression_name else False
def _test_client_renegotiation( server_info: ServerConnectivityInfo ) -> Tuple[_ScanJobResultEnum, bool]: """Check whether the server honors session renegotiation requests. """ # Try with TLS 1.2 even if the server supports TLS 1.3 or higher as there is no reneg with TLS 1.3 if server_info.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value: tls_version_to_use = TlsVersionEnum.TLS_1_2 downgraded_from_tls_1_3 = True else: tls_version_to_use = server_info.tls_probing_result.highest_tls_version_supported downgraded_from_tls_1_3 = False ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version_to_use, should_use_legacy_openssl= True, # Only the legacy SSL client has methods to trigger a reneg ) if not isinstance(ssl_connection.ssl_client, LegacySslClient): raise RuntimeError("Should never happen") try: # Perform the TLS handshake ssl_connection.connect() # Should only happen when the server only supports TLS 1.3 except ServerRejectedTlsHandshake: if downgraded_from_tls_1_3: accepts_client_renegotiation = False # Technically TLS 1.3 has no renegotiation therefore it is secure else: raise # The initial TLS handshake went well; let's try to do a renegotiation else: try: # Do a reneg multiple times in a row to be 100% sure that the server has no mitigations in place # https://github.com/nabla-c0d3/sslyze/issues/473 for i in range(10): 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 ConnectionError: accepts_client_renegotiation = False except OSError as e: # OSError is the parent of all (non-TLS) socket/connection errors so it should be last if "Nassl SSL handshake failed" in e.args[0]: # Special error returned by nassl accepts_client_renegotiation = False else: raise except OpenSSLError as e: if "handshake failure" in e.args[0]: accepts_client_renegotiation = False elif "no renegotiation" in e.args[0]: accepts_client_renegotiation = False elif "tlsv1 unrecognized name" in e.args[0]: # Yahoo's very own way of rejecting a renegotiation accepts_client_renegotiation = False elif "tlsv1 alert internal error" in e.args[0]: # Jetty server: https://github.com/nabla-c0d3/sslyze/issues/290 accepts_client_renegotiation = False elif "decryption failed or bad record mac" in e.args[0]: # Some servers such as reddit.com accepts_client_renegotiation = False elif "sslv3 alert unexpected message" in e.args[0]: # traefik https://github.com/nabla-c0d3/sslyze/issues/422 accepts_client_renegotiation = False elif "shut down by peer" in e.args[0]: # Cloudfront accepts_client_renegotiation = False elif "unexpected record" in e.args[0]: # Indy TCP Server with special RSA Token authentication https://github.com/nabla-c0d3/sslyze/issues/483 accepts_client_renegotiation = False else: raise finally: ssl_connection.close() return _ScanJobResultEnum.IS_VULNERABLE_TO_CLIENT_RENEG_DOS, accepts_client_renegotiation
def _test_curve(server_info: ServerConnectivityInfo, curve_nid: OpenSslEcNidEnum) -> _EllipticCurveResult: if not server_info.tls_probing_result.supports_ecdh_key_exchange: raise RuntimeError("Should never happen") tls_version = server_info.tls_probing_result.highest_tls_version_supported ssl_connection = server_info.get_preconfigured_tls_connection( override_tls_version=tls_version, should_use_legacy_openssl=False) if not isinstance(ssl_connection.ssl_client, SslClient): raise RuntimeError( "Should never happen: specified should_use_legacy_openssl=False but didn't get the modern SSL client" ) # Set curve to test whether it is supported by the server enable_ecdh_cipher_suites(tls_version, ssl_connection.ssl_client) ssl_connection.ssl_client.set_groups([curve_nid]) try: ssl_connection.connect() negotiated_ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key( ) # Error handling here mis similar to test_cipher_suite.py except ClientCertificateRequested: negotiated_ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key( ) except (TlsHandshakeTimedOut, ServerRejectedTlsHandshake): negotiated_ephemeral_key = None except OpenSSLError as e: # The following errors can be triggered by some servers when they don't support the specific curve enabled # in the client if "ossl_statem_client_read_transition:unexpected message" in e.args[ 0]: # Related to https://github.com/nabla-c0d3/sslyze/issues/466 negotiated_ephemeral_key = None elif "tls_process_ske_ecdhe:wrong curve" in e.args[0]: # https://github.com/nabla-c0d3/sslyze/issues/490 negotiated_ephemeral_key = None elif "sslv3 alert unexpected message" in e.args[0]: # https://github.com/nabla-c0d3/sslyze/issues/490 negotiated_ephemeral_key = None else: raise finally: ssl_connection.close() # If no error occurred check if the curve was really used try: curve_name = _OPENSSL_NID_TO_SECG_ANSI_X9_62[ curve_nid] # TODO(AD): Make this public in nassl except KeyError: curve_name = f"unknown-curve-with-openssl-id-{curve_nid.value}" if negotiated_ephemeral_key: if isinstance(negotiated_ephemeral_key, EcDhEphemeralKeyInfo): if negotiated_ephemeral_key.curve != curve_nid: raise RuntimeError("Should never happen") return _EllipticCurveResult( curve=EllipticCurve(name=curve_name, openssl_nid=curve_nid.value), was_accepted_by_server=True, ) return _EllipticCurveResult( curve=EllipticCurve(name=curve_name, openssl_nid=curve_nid.value), was_accepted_by_server=False, )
def _retrieve_and_analyze_http_response(server_info: ServerConnectivityInfo) -> HttpHeadersScanResult: # Send HTTP requests until we no longer received an HTTP redirection, but allow only 4 redirections max _logger.info(f"Retrieving HTTP headers from {server_info}") redirections_count = 0 next_location_path: Optional[str] = "/" http_error_trace = None while next_location_path and redirections_count < 4: _logger.info(f"Sending HTTP request to {next_location_path}") http_path_redirected_to = next_location_path # Perform the TLS handshake ssl_connection = server_info.get_preconfigured_tls_connection() ssl_connection.connect() try: # Send an HTTP GET request to the server ssl_connection.ssl_client.write( HttpRequestGenerator.get_request( host=server_info.network_configuration.tls_server_name_indication, path=next_location_path ) ) http_response = HttpResponseParser.parse_from_ssl_connection(ssl_connection.ssl_client) except (OSError, NotAValidHttpResponseError, SslError) as e: # The server closed/rejected the connection, or didn't return a valid HTTP response http_error_trace = TracebackException.from_exception(e) finally: ssl_connection.close() if http_error_trace: break # Handle redirection if there is one next_location_path = _detect_http_redirection( http_response=http_response, server_host_name=server_info.network_configuration.tls_server_name_indication, server_port=server_info.server_location.port, ) redirections_count += 1 # Prepare the results initial_http_request = HttpRequestGenerator.get_request( host=server_info.network_configuration.tls_server_name_indication, path="/" ).decode("ascii") if http_error_trace: # If the server errored when receiving an HTTP request, return the error as the result return HttpHeadersScanResult( http_request_sent=initial_http_request, http_error_trace=http_error_trace, http_path_redirected_to=None, strict_transport_security_header=None, expect_ct_header=None, ) else: # If no HTTP error happened, parse and return each header return HttpHeadersScanResult( http_request_sent=initial_http_request, http_path_redirected_to=http_path_redirected_to, http_error_trace=None, strict_transport_security_header=_parse_hsts_header_from_http_response(http_response), expect_ct_header=_parse_expect_ct_header_from_http_response(http_response), )
def connect_with_cipher_suite( server_connectivity_info: ServerConnectivityInfo, tls_version: OpenSslVersionEnum, cipher_suite: CipherSuite ) -> Union[CipherSuiteAcceptedByServer, CipherSuiteRejectedByServer]: """Initiates a SSL handshake with the server using the SSL version and the cipher suite specified. """ requires_legacy_openssl = True if tls_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( cipher_suite.openssl_name) elif tls_version == OpenSslVersionEnum.TLSV1_3: requires_legacy_openssl = False ssl_connection = server_connectivity_info.get_preconfigured_tls_connection( override_tls_version=tls_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 isinstance(ssl_connection.ssl_client, SslClient): # With the modern OpenSSL client we have to manage TLS 1.3-specific cipher functions if tls_version == OpenSslVersionEnum.TLSV1_3: legacy_openssl_cipher_string = "" tls1_3_openssl_cipher_string = cipher_suite.openssl_name else: legacy_openssl_cipher_string = cipher_suite.openssl_name tls1_3_openssl_cipher_string = "" ssl_connection.ssl_client.set_ciphersuites( tls1_3_openssl_cipher_string) # TLS 1.3 method ssl_connection.ssl_client.set_cipher_list( legacy_openssl_cipher_string) # Legacy method elif isinstance(ssl_connection.ssl_client, LegacySslClient): # With the legacy OpenSSL client, nothing special to do ssl_connection.ssl_client.set_cipher_list(cipher_suite.openssl_name) else: raise RuntimeError("Should never happen") if len(ssl_connection.ssl_client.get_cipher_list()) != 1: raise ValueError( f'Passed an OpenSSL string for multiple cipher suites: "{cipher_suite.openssl_name}": ' f"{str(ssl_connection.ssl_client.get_cipher_list())}") ephemeral_key = None try: # Perform the SSL handshake ssl_connection.connect() ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key() except ServerTlsConfigurationNotSupported: # SSLyze rejected the handshake because the server's DH config was too insecure; this means the # cipher suite is actually supported pass except ClientCertificateRequested: # When the handshake failed due to ClientCertificateRequested ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key() pass except ServerRejectedTlsHandshake as e: return CipherSuiteRejectedByServer(cipher_suite=cipher_suite, error_message=e.error_message) finally: ssl_connection.close() return CipherSuiteAcceptedByServer(cipher_suite=cipher_suite, ephemeral_key=ephemeral_key)