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)
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)
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 _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
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
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
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)
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: 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)
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
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_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)
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
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
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
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 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 _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 _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_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, )