def test_json_serializer_functions(self): # Given a completed scan for a server with the CERTIFICATE_INFO scan command server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "www.hotmail.com", 443) server_info = ServerConnectivityTester().perform(server_location) plugin_result = CertificateInfoImplementation.perform(server_info) scan_results = {ScanCommandEnum.CERTIFICATE_INFO: plugin_result} scan_result = ServerScanResultFactory.create( scan_commands_results=scan_results) # When generating the JSON output for this server scan with StringIO() as file_out: json_generator = JsonOutputGenerator(file_to=file_out) json_generator.server_scan_completed(scan_result) # We call scans_completed() because this is when the output actually gets written to the file json_generator.scans_completed(0.2) final_output = file_out.getvalue() # It succeeds assert final_output # And complex object like certificates were properly serialized assert "notBefore" in final_output assert "issuer" in final_output assert "subject" in final_output
def test_error_client_certificate_needed(self): # Given a server that requires client authentication with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: # And sslyze does NOT provide a client certificate server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port ) server_info = ServerConnectivityTester().perform(server_location) server_scan = ServerScanRequest( server_info=server_info, scan_commands={ # And a scan command that cannot be completed without a client certificate ScanCommand.HTTP_HEADERS, }, ) # When queuing the scan scanner = Scanner() scanner.queue_scan(server_scan) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) assert len(all_results) == 1 # And the error was properly returned error = all_results[0].scan_commands_errors[ScanCommand.HTTP_HEADERS] assert error.reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED
def test_tlsv1_2_enabled(self): # Given a server to scan that supports TLS 1.2 server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.google.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When scanning for cipher suites, it succeeds result: CipherSuitesScanResult = Tlsv12ScanImplementation.scan_server(server_info) # And the result confirms that TLS 1.2 is supported expected_ciphers = { "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", } assert expected_ciphers == { accepted_cipher.cipher_suite.name for accepted_cipher in result.accepted_cipher_suites }
def test_null_cipher_suites(self): # Given a server to scan that supports NULL cipher suites server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("null.badssl.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When scanning for cipher suites, it succeeds result: CipherSuitesScanResult = Tlsv12ScanImplementation.scan_server(server_info) # And the NULL/Anon cipher suites were detected expected_ciphers = { "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", "TLS_DH_anon_WITH_AES_256_CBC_SHA256", "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", "TLS_DH_anon_WITH_AES_256_GCM_SHA384", "TLS_DH_anon_WITH_AES_256_CBC_SHA", "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", "TLS_DH_anon_WITH_AES_128_CBC_SHA256", "TLS_DH_anon_WITH_AES_128_CBC_SHA", "TLS_DH_anon_WITH_AES_128_GCM_SHA256", "TLS_DH_anon_WITH_SEED_CBC_SHA", "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_NULL_SHA", "TLS_ECDH_anon_WITH_NULL_SHA", "TLS_RSA_WITH_NULL_SHA256", "TLS_RSA_WITH_NULL_SHA", } assert expected_ciphers == { accepted_cipher.cipher_suite.name for accepted_cipher in result.accepted_cipher_suites }
def test_works_when_client_auth_succeeded(self): # Given a server that requires client authentication with ModernOpenSslServer( client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port) # And sslyze provides a client certificate network_config = ServerNetworkConfiguration( tls_server_name_indication=server.hostname, tls_client_auth_credentials=ClientAuthenticationCredentials( certificate_chain_path=server.get_client_certificate_path( ), key_path=server.get_client_key_path()), ) server_info = ServerConnectivityTester().perform( server_location, network_config) # When testing for resumption, it succeeds result: SessionResumptionSupportScanResult = SessionResumptionSupportImplementation.scan_server( server_info) assert result.successful_session_id_resumptions_count assert result.is_session_id_resumption_supported
def test_works_when_client_auth_succeeded(self): # Given a server that requires client authentication with LegacyOpenSslServer( client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port) # And sslyze provides a client certificate network_config = ServerNetworkConfiguration( tls_server_name_indication=server.hostname, tls_client_auth_credentials=ClientAuthenticationCredentials( certificate_chain_path=server.get_client_certificate_path( ), key_path=server.get_client_key_path()), ) server_info = ServerConnectivityTester().perform( server_location, network_config) # When scanning for HTTP headers, it succeeds result: HttpHeadersScanResult = HttpHeadersImplementation.scan_server( server_info) assert not result.strict_transport_security_header assert not result.public_key_pins_header assert not result.public_key_pins_report_only_header assert not result.expect_ct_header
def test_cipher_suite_preferred_by_server(self): # Given an ordered list of cipher suites configured_cipher_suites = [ "ECDHE-RSA-CHACHA20-POLY1305", "ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-RSA-AES128-SHA256", "ECDHE-RSA-AES256-SHA384", "ECDHE-RSA-AES128-SHA", "ECDHE-RSA-AES256-SHA", "AES128-GCM-SHA256", "AES256-GCM-SHA384", "AES128-SHA256", "AES256-SHA256", "AES128-SHA", "AES256-SHA", ] random.shuffle(configured_cipher_suites) cipher_string = ":".join(configured_cipher_suites) # And a server that is configured with this list as its prefered cipher suites with ModernOpenSslServer( openssl_cipher_string=cipher_string, should_enable_server_cipher_preference=True ) as server: server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port ) server_info = ServerConnectivityTester().perform(server_location) # When scanning for cipher suites, it succeeds result: CipherSuitesScanResult = Tlsv12ScanImplementation.scan_server(server_info) # And the server's cipher suite preference was detected assert result.cipher_suite_preferred_by_server assert configured_cipher_suites[0] == result.cipher_suite_preferred_by_server.cipher_suite.openssl_name
def test_ca_file(self): # Given a server to scan server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "www.hotmail.com", 443) server_info = ServerConnectivityTester().perform(server_location) # And a valid path to a custom CA file ca_file_path = Path( __file__ ).parent / ".." / ".." / "certificates" / "wildcard-self-signed.pem" # When running the scan with the custom CA file enabled plugin_result = CertificateInfoImplementation.scan_server( server_info, CertificateInfoExtraArguments(custom_ca_file=ca_file_path)) # It succeeds assert len(plugin_result.certificate_deployments[0]. path_validation_results) >= 6 for path_validation_result in plugin_result.certificate_deployments[ 0].path_validation_results: if path_validation_result.trust_store.path == ca_file_path: assert not path_validation_result.was_validation_successful else: assert path_validation_result.was_validation_successful
def test_invalid_chain(self): # Given a server to scan that has a self-signed certificate server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "self-signed.badssl.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan plugin_result = CertificateInfoImplementation.scan_server(server_info) # A verified chain cannot be built assert not plugin_result.certificate_deployments[ 0].verified_certificate_chain assert plugin_result.certificate_deployments[ 0].verified_chain_has_sha1_signature is None # And the result has other details about the certificate chain assert plugin_result.certificate_deployments[0].ocsp_response is None assert len(plugin_result.certificate_deployments[0]. received_certificate_chain) == 1 assert len(plugin_result.certificate_deployments[0]. path_validation_results) >= 5 for path_validation_result in plugin_result.certificate_deployments[ 0].path_validation_results: assert not path_validation_result.was_validation_successful assert plugin_result.certificate_deployments[ 0].leaf_certificate_signed_certificate_timestamps_count == 0 assert plugin_result.certificate_deployments[ 0].leaf_certificate_subject_matches_hostname assert plugin_result.certificate_deployments[ 0].received_chain_has_valid_order assert plugin_result.certificate_deployments[ 0].received_chain_contains_anchor_certificate is None
def test_1000_sans_chain(self): # Given a server to scan that has a leaf cert with 1000 SANs server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("1000-sans.badssl.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan, it succeeds CertificateInfoImplementation.scan_server(server_info)
def test_valid_chain_with_ev_cert(self): # Given a server to scan that has an EV certificate server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "www.digicert.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan plugin_result = CertificateInfoImplementation.scan_server(server_info) # The result returns that the certificate is EV assert plugin_result.certificate_deployments[0].leaf_certificate_is_ev # And the result has other details about the certificate chain assert len(plugin_result.certificate_deployments[0]. received_certificate_chain) assert len(plugin_result.certificate_deployments[0]. verified_certificate_chain) assert not plugin_result.certificate_deployments[ 0].received_chain_contains_anchor_certificate assert len(plugin_result.certificate_deployments[0]. path_validation_results) == 5 for path_validation_result in plugin_result.certificate_deployments[ 0].path_validation_results: assert path_validation_result.was_validation_successful assert plugin_result.certificate_deployments[ 0].leaf_certificate_subject_matches_hostname assert plugin_result.certificate_deployments[ 0].received_chain_has_valid_order
def test_http_error(self): # Given a server to scan with ModernOpenSslServer( # And the server will trigger an error when receiving an HTTP request should_reply_to_http_requests=False ) as server: server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port ) server_info = ServerConnectivityTester().perform(server_location) # When scanning for HTTP headers, it succeeds result: HttpHeadersScanResult = HttpHeadersImplementation.scan_server(server_info) # And the result mention the error returned by the server when sending an HTTP request assert result.http_error_trace assert result.http_request_sent # And the other result fields are not set assert not result.http_path_redirected_to assert not result.public_key_pins_header assert not result.public_key_pins_report_only_header assert not result.expect_ct_header # And a CLI output can be generated assert HttpHeadersImplementation.cli_connector_cls.result_to_console_output(result) # And the result can be converted to JSON result_as_json = json.dumps(asdict(result), cls=JsonEncoder) assert result_as_json
def test_ecdsa_certificate(self): # Given a server to scan that has an ECDSA certificate server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.cloudflare.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan, it succeeds CertificateInfoImplementation.perform(server_info)
def test_works_when_client_auth_succeeded(self): # Given a server that does NOT support SCSV and that requires client authentication with LegacyOpenSslServer( client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port) # And sslyze provides a client certificate network_config = ServerNetworkConfiguration( tls_server_name_indication=server.hostname, tls_client_auth_credentials=ClientAuthenticationCredentials( certificate_chain_path=server.get_client_certificate_path( ), key_path=server.get_client_key_path()), ) server_info = ServerConnectivityTester().perform( server_location, network_config) # When testing for SCSV, it succeeds result: FallbackScsvScanResult = FallbackScsvImplementation.scan_server( server_info) # And the server is reported as NOT supporting SCSV assert not result.supports_fallback_scsv
def test_works_when_client_auth_succeeded(self): # Given a server that is vulnerable and that requires client authentication with LegacyOpenSslServer( client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port) # And sslyze provides a client certificate network_config = ServerNetworkConfiguration( tls_server_name_indication=server.hostname, tls_client_auth_credentials=ClientAuthenticationCredentials( certificate_chain_path=server.get_client_certificate_path( ), key_path=server.get_client_key_path()), ) server_info = ServerConnectivityTester().perform( server_location, network_config) # When testing for insecure reneg, it succeeds result: SessionRenegotiationScanResult = SessionRenegotiationImplementation.scan_server( server_info) # And the results are correct assert result.supports_secure_renegotiation assert result.is_vulnerable_to_client_renegotiation_dos
def get_supported_tls(self, highest_supported): supported = [highest_supported] for version, method in { "SSL_2_0": TlsVersionEnum.SSLV2, "SSL_3_0": TlsVersionEnum.SSLV3, "TLS_1_0": TlsVersionEnum.TLSV1, "TLS_1_1": TlsVersionEnum.TLSV1_1, "TLS_1_2": TlsVersionEnum.TLSV1_2, }.items(): # Only test SSL/TLS connections with lesser versions if highest_supported == version: break try: # Attempt connection # If connection fails, exception will be raised, causing the failure to be # logged and the version to not be appended to the supported list ctx = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( self.domain, 443) cfg = ServerNetworkConfiguration(self.domain) connx = SslConnection(ctx, cfg, method, True) connx.connect(self.domain) supported.append(version) except Exception as e: logging.info( f"Failed to connect using %{version}: ({type(e)}) - {e}") return supported
def test_via_direct_connection_but_server_tls_config_not_supported(self): # Given a server location for a server that only supports DH settings that SSLyze can't use server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( hostname="dh480.badssl.com", port=443) # When testing connectivity, it fails with the right error with pytest.raises(ServerTlsConfigurationNotSupported): ServerConnectivityTester().perform(server_location)
def test_via_direct_connection_but_server_rejected_connection(self): # Given a server location for a server that's offline server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( hostname="localhost", port=1234) # When testing connectivity, it fails with the right error with pytest.raises(ServerRejectedConnection): ServerConnectivityTester().perform(server_location)
def test_robot_attack_good(self): # Validate the bug fix for https://github.com/nabla-c0d3/sslyze/issues/282 # Given a server to scan that is not vulnerable to ROBOT server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "guide.duo.com", 443) server_info = ServerConnectivityTester().perform(server_location) result: RobotScanResult = RobotImplementation.perform(server_info) assert result.robot_result == RobotScanResultEnum.NOT_VULNERABLE_NO_ORACLE
def main() -> None: # First validate that we can connect to the servers we want to scan servers_to_scan = [] for hostname in ["cloudflare.com", "google.com"]: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 443) try: server_info = ServerConnectivityTester().perform(server_location) servers_to_scan.append(server_info) except ConnectionToServerFailed as e: print(f"Error connecting to {server_location.hostname}:{server_location.port}: {e.error_message}") return scanner = Scanner() # Then queue some scan commands for each server for server_info in servers_to_scan: server_scan_req = ServerScanRequest( server_info=server_info, scan_commands={ ScanCommandEnum.TLS_1_0_CIPHER_SUITES, ScanCommandEnum.TLS_1_1_CIPHER_SUITES, ScanCommandEnum.TLS_1_2_CIPHER_SUITES, ScanCommandEnum.CERTIFICATE_INFO, ScanCommandEnum.TLS_COMPRESSION, }, ) scanner.queue_scan(server_scan_req) # Then retrieve the result of the scan commands for each server for server_scan_result in scanner.get_results(): print(f"\nResults for {server_scan_result.server_info.server_location.hostname}:") # Scan commands that were run with no errors for scan_command, result in server_scan_result.scan_commands_results.items(): if scan_command in [ ScanCommandEnum.TLS_1_0_CIPHER_SUITES, ScanCommandEnum.TLS_1_1_CIPHER_SUITES, ScanCommandEnum.TLS_1_2_CIPHER_SUITES, ]: typed_result = cast(CipherSuitesScanResult, result) print(f"\nAccepted cipher suites for {scan_command.name}:") for accepted_cipher_suite in typed_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") elif scan_command == ScanCommandEnum.CERTIFICATE_INFO: typed_result = cast(CertificateInfoScanResult, result) print("\nCertificate info:") for cert_deployment in typed_result.certificate_deployments: print(f"Leaf certificate: \n{cert_deployment.verified_certificate_chain_as_pem[0]}") elif scan_command == ScanCommandEnum.TLS_COMPRESSION: typed_result = cast(CompressionScanResult, result) print(f"\nCompression / CRIME: {typed_result.supports_compression}") # Scan commands that were run with errors for scan_command, error in server_scan_result.scan_commands_errors.items(): print(f"\nError when running {scan_command}:\n{error.exception_trace}")
def test_via_direct_connection_but_server_timed_out(self): # Given a server location for a server that's offline server_location = ServerNetworkLocationViaDirectConnection( hostname="notarealdomain.not.real.notreal.not", port=1234, ip_address="123.123.123.123" ) # When testing connectivity, it fails with the right error with pytest.raises(ConnectionToServerTimedOut): ServerConnectivityTester().perform(server_location)
def test_certificate_with_no_subject(self): # Given a server to scan that has a certificate with no Subject server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("no-subject.badssl.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan, it succeeds plugin_result = CertificateInfoImplementation.scan_server(server_info) assert plugin_result.certificate_deployments[0].verified_certificate_chain
def test_ca_file_bad_file(self): # Given a server to scan server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.hotmail.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When trying to enable a custom CA file but the path is wrong, it fails with pytest.raises(ValueError): CertificateInfoImplementation.scan_server( server_info, CertificateInfoExtraArguments(custom_ca_file=Path("doesntexist")) )
def test_sha256_chain(self): # Given a server to scan that has a SHA256-signed certificate server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("sha256.badssl.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan plugin_result = CertificateInfoImplementation.scan_server(server_info) # No SHA1 signature is detected assert not plugin_result.certificate_deployments[0].verified_chain_has_sha1_signature
def test_chain_with_anchor(self): # Given a server to scan that has its anchor certificate returned in its chain server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.verizon.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan, it succeeds plugin_result = CertificateInfoImplementation.scan_server(server_info) # And the anchor certificate was detected assert plugin_result.certificate_deployments[0].received_chain_contains_anchor_certificate
def test_certificate_with_scts(self): # Given a server to scan that has a certificate with SCTS server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.apple.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan, it succeeds plugin_result = CertificateInfoImplementation.scan_server(server_info) # And the SCTS were detected assert plugin_result.certificate_deployments[0].leaf_certificate_signed_certificate_timestamps_count > 1
def test_follows_client_cipher_suite_preference(self): # Given a server to scan that follows client cipher suite preference server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.hotmail.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When scanning for cipher suites, it succeeds result: CipherSuitesScanResult = Tlsv12ScanImplementation.scan_server(server_info) # And the server is detected as following the client's preference assert result.cipher_suite_preferred_by_server
def test_multiple_certificates(self): # Given a server to scan that exposes multiple certificates for maximum compatibility server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.facebook.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When running the scan, it succeeds plugin_result = CertificateInfoImplementation.scan_server(server_info) # And multiple certificates were detected assert len(plugin_result.certificate_deployments) > 1
def test_sslv3_disabled(self): # Given a server to scan that does not support SSL 3.0 server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.google.com", 443) server_info = ServerConnectivityTester().perform(server_location) # When scanning for cipher suites, it succeeds result: CipherSuitesScanResult = Sslv30ScanImplementation.scan_server(server_info) # And the result confirms that SSL 3.0 is not supported assert not result.accepted_cipher_suites assert result.rejected_cipher_suites
def test(self): # Given a completed scan for a CERTIFICATE_INFO scan command server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "www.facebook.com", 443) server_info = ServerConnectivityTester().perform(server_location) plugin_result = CertificateInfoImplementation.scan_server(server_info) # When generating the CLI output for this result, it succeeds result_as_txt = CertificateInfoImplementation.cli_connector_cls.result_to_console_output( plugin_result) assert result_as_txt