From a57093e04f448a7c91f2b0b5524ce6d134754dad Mon Sep 17 00:00:00 2001 From: Jacob Zak Date: Wed, 5 Mar 2025 12:08:03 +0100 Subject: [PATCH 1/4] feat: add cli option to use custom tls config Closes #686 Context: * no way to use custom profile for compliance checks --- sslyze/__main__.py | 2 +- sslyze/cli/command_line_parser.py | 30 +++++++++++++++++++ .../mozilla_config_checker.py | 9 ++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/sslyze/__main__.py b/sslyze/__main__.py index 6836acc0..0e819176 100644 --- a/sslyze/__main__.py +++ b/sslyze/__main__.py @@ -108,7 +108,7 @@ def main() -> None: f' Checking results against Mozilla\'s "{parsed_command_line.check_against_mozilla_config}"' f" configuration. See https://ssl-config.mozilla.org/ for more details.\n" ) - mozilla_checker = MozillaTlsConfigurationChecker.get_default() + mozilla_checker = MozillaTlsConfigurationChecker.create_from_commandline(parsed_command_line.custom_tls_profile) for server_scan_result in all_server_scan_results: try: mozilla_checker.check_server( diff --git a/sslyze/cli/command_line_parser.py b/sslyze/cli/command_line_parser.py index 95e04098..2b3913ee 100644 --- a/sslyze/cli/command_line_parser.py +++ b/sslyze/cli/command_line_parser.py @@ -1,3 +1,5 @@ +import json +import pydantic from dataclasses import dataclass from argparse import ArgumentParser from pathlib import Path @@ -12,6 +14,7 @@ ) from sslyze.connection_helpers.opportunistic_tls_helpers import ProtocolWithOpportunisticTlsEnum from sslyze.mozilla_tls_profile.mozilla_config_checker import ( + _MozillaTlsProfileAsJson, MozillaTlsConfigurationEnum, SCAN_COMMANDS_NEEDED_BY_MOZILLA_CHECKER, ) @@ -64,6 +67,9 @@ class ParsedCommandLine: per_server_concurrent_connections_limit: Optional[int] concurrent_server_scans_limit: Optional[int] + # Store the parsed profile object instead of just the path + custom_tls_profile: Optional[_MozillaTlsProfileAsJson] + # Mozilla compliance; None if shouldn't be run check_against_mozilla_config: Optional[MozillaTlsConfigurationEnum] @@ -98,6 +104,15 @@ def __init__(self, sslyze_version: str) -> None: action=scan_option.action, ) + # Add custom TLS profile option + self._parser.add_argument( + "--custom_profile", + help="Path to a custom TLS profile JSON file following Mozilla's format", + dest="custom_profile", + type=str, + default=None + ) + self._parser.add_argument( "--mozilla_config", action="store", @@ -120,6 +135,20 @@ def parse_command_line(self) -> ParsedCommandLine: TrustStoresRepository.update_default() raise TrustStoresUpdateCompleted() + # Handle custom profile if provided + custom_tls_profile = None + if args_command_list.custom_profile: + json_profile_path = Path(args_command_list.custom_profile).absolute() + if not json_profile_path.exists(): + raise CommandLineParsingError(f"Custom TLS profile file '{json_profile_path}' does not exist") + try: + json_profile_as_str = json_profile_path.read_text() + custom_tls_profile = _MozillaTlsProfileAsJson(**json.loads(json_profile_as_str)) + except (ValueError, pydantic.ValidationError) as e: + raise CommandLineParsingError( + f"Invalid custom TLS profile format in '{json_profile_path}': {str(e)}" + ) + # Handle the --targets_in command line and fill args_target_list if args_command_list.targets_in: try: # Read targets from a file @@ -318,6 +347,7 @@ def parse_command_line(self) -> ParsedCommandLine: should_disable_console_output=args_command_list.quiet or args_command_list.json_file == "-", concurrent_server_scans_limit=concurrent_server_scans_limit, per_server_concurrent_connections_limit=per_server_concurrent_connections_limit, + custom_tls_profile=custom_tls_profile, check_against_mozilla_config=check_against_mozilla_config, ) diff --git a/sslyze/mozilla_tls_profile/mozilla_config_checker.py b/sslyze/mozilla_tls_profile/mozilla_config_checker.py index a485e3e0..792a015f 100644 --- a/sslyze/mozilla_tls_profile/mozilla_config_checker.py +++ b/sslyze/mozilla_tls_profile/mozilla_config_checker.py @@ -110,6 +110,15 @@ def get_default(cls) -> "MozillaTlsConfigurationChecker": parsed_profile = _MozillaTlsProfileAsJson(**json.loads(json_profile_as_str)) return cls(parsed_profile) + @classmethod + def create_from_commandline( + cls, custom_profile: Optional[_MozillaTlsProfileAsJson] = None + ) -> "MozillaTlsConfigurationChecker": + """Create a checker instance using either the default or custom profile.""" + if custom_profile: + return cls(custom_profile) + return cls.get_default() + def check_server( self, against_config: MozillaTlsConfigurationEnum, From 0dab2ced520de4939b336b55c1c0e5aa5aefceea Mon Sep 17 00:00:00 2001 From: Alban D Date: Sun, 3 Aug 2025 16:09:32 +0200 Subject: [PATCH 2/4] [#686]Finalize custom TLS configuration/profile support --- README.md | 15 +- custom_tls_config_example.json | 59 +++++ sslyze/__main__.py | 39 ++-- sslyze/cli/command_line_parser.py | 102 +++++---- ...onfig_checker.py => tls_config_checker.py} | 214 +++++++++--------- tests/factories.py | 3 +- tests/test_main.py | 36 +++ .../test_custom_tls_config.py | 77 +++++++ ..._checker.py => test_mozilla_tls_config.py} | 89 ++++---- 9 files changed, 421 insertions(+), 213 deletions(-) create mode 100644 custom_tls_config_example.json rename sslyze/mozilla_tls_profile/{mozilla_config_checker.py => tls_config_checker.py} (68%) create mode 100644 tests/test_mozilla_tls_profile/test_custom_tls_config.py rename tests/test_mozilla_tls_profile/{test_mozilla_config_checker.py => test_mozilla_tls_config.py} (59%) diff --git a/README.md b/README.md index fc8388f0..3724c1aa 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,20 @@ mozilla.com:443: FAILED - Not compliant. * ciphers: Cipher suites {'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256', 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'} are supported, but should be rejected. ``` -This can be used to easily run an SSLyze scan as a CI/CD step. +Alternatively, you can check against your own custom TLS configuration by providing a JSON file that follows Mozilla's TLS configuration format: + +``` +$ python -m sslyze --custom_tls_config custom_tls_config_example.json mozilla.com +``` +``` +Checking results against custom TLS configuration. + +mozilla.com:443: OK - Compliant. +``` + +See `custom_tls_config_example.json` for an example a custom TLS configuration that can be used by SSLyze. + +**This functionality can be used to easily run an SSLyze scan as a CI/CD step in order to ensure TLS compliance.** Development environment ----------------------- diff --git a/custom_tls_config_example.json b/custom_tls_config_example.json new file mode 100644 index 00000000..19e4068a --- /dev/null +++ b/custom_tls_config_example.json @@ -0,0 +1,59 @@ +{ + "tls_versions": ["TLSv1.2", "TLSv1.3"], + "certificate_types": ["ecdsa", "rsa"], + "certificate_curves": ["prime256v1", "secp384r1"], + "certificate_signatures": [ + "ecdsa-with-SHA256", + "ecdsa-with-SHA384", + "sha256WithRSAEncryption", + "sha384WithRSAEncryption" + ], + "ciphersuites": [ + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_GCM_SHA256" + ], + "ciphers": { + "caddy": [ + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + ], + "go": [ + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" + ], + "iana": [ + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + ], + "openssl": [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305" + ] + }, + "tls_curves": ["X25519", "prime256v1", "secp384r1"], + "rsa_key_size": 2048, + "dh_param_size": 2048, + "ecdh_param_size": 256, + "hsts_min_age": 31536000, + "maximum_certificate_lifespan": 90, + "recommended_certificate_lifespan": 90, + "ocsp_staple": true, + "server_preferred_order": false +} \ No newline at end of file diff --git a/sslyze/__main__.py b/sslyze/__main__.py index 0e819176..edcbda9b 100644 --- a/sslyze/__main__.py +++ b/sslyze/__main__.py @@ -14,10 +14,11 @@ ServerConnectivityStatusEnum, ) from sslyze.json.json_output import InvalidServerStringAsJson -from sslyze.mozilla_tls_profile.mozilla_config_checker import ( - MozillaTlsConfigurationChecker, - ServerNotCompliantWithMozillaTlsConfiguration, +from sslyze.mozilla_tls_profile.tls_config_checker import ( + check_server_against_tls_configuration, + ServerNotCompliantWithTlsConfiguration, ServerScanResultIncomplete, + TlsConfigurationEnum, ) @@ -87,7 +88,7 @@ def main() -> None: json_output_as_str = json_output.model_dump_json(indent=2) json_file_out.write(json_output_as_str) - # If we printed the JSON results to the console, don't run the Mozilla compliance check so we return valid JSON + # If we printed the JSON results to the console, don't run the TLS compliance check so we return valid JSON if parsed_command_line.should_print_json_to_console: sys.exit(0) @@ -95,29 +96,37 @@ def main() -> None: # There are no results to present: all supplied server strings were invalid? sys.exit(0) - # Check the results against the Mozilla config if needed + # Check the results against the TLS config if needed are_all_servers_compliant = True # TODO(AD): Expose format_title method - title = ObserverToGenerateConsoleOutput._format_title("Compliance against Mozilla TLS configuration") + title = ObserverToGenerateConsoleOutput._format_title("Compliance against TLS configuration") print() print(title) - if not parsed_command_line.check_against_mozilla_config: - print(" Disabled; use --mozilla_config={old, intermediate, modern}.\n") - else: + if not parsed_command_line.tls_config_to_check_against_as_enum: print( - f' Checking results against Mozilla\'s "{parsed_command_line.check_against_mozilla_config}"' - f" configuration. See https://ssl-config.mozilla.org/ for more details.\n" + " Disabled; use --mozilla_config={old, intermediate, modern} or --custom_tls_config=path/to/profile.json.\n" ) - mozilla_checker = MozillaTlsConfigurationChecker.create_from_commandline(parsed_command_line.custom_tls_profile) + else: + assert parsed_command_line.tls_config_to_check_against, "Should always be set" + + if parsed_command_line.tls_config_to_check_against_as_enum == TlsConfigurationEnum.CUSTOM: + print(" Checking results against custom TLS configuration.\n") + else: + config_name = parsed_command_line.tls_config_to_check_against_as_enum.value + print( + f' Checking results against Mozilla\'s "{config_name}"' + f" configuration. See https://ssl-config.mozilla.org/ for more details.\n" + ) + for server_scan_result in all_server_scan_results: try: - mozilla_checker.check_server( - against_config=parsed_command_line.check_against_mozilla_config, + check_server_against_tls_configuration( server_scan_result=server_scan_result, + tls_config_to_check_against=parsed_command_line.tls_config_to_check_against, ) print(f" {server_scan_result.server_location.display_string}: OK - Compliant.\n") - except ServerNotCompliantWithMozillaTlsConfiguration as e: + except ServerNotCompliantWithTlsConfiguration as e: are_all_servers_compliant = False print(f" {server_scan_result.server_location.display_string}: FAILED - Not compliant.") for criteria, error_description in e.issues.items(): diff --git a/sslyze/cli/command_line_parser.py b/sslyze/cli/command_line_parser.py index 9b3c9fa1..974b8db1 100644 --- a/sslyze/cli/command_line_parser.py +++ b/sslyze/cli/command_line_parser.py @@ -1,4 +1,3 @@ -import json import pydantic from dataclasses import dataclass from argparse import ArgumentParser @@ -13,9 +12,10 @@ CommandLineServerStringParser, ) from sslyze.connection_helpers.opportunistic_tls_helpers import ProtocolWithOpportunisticTlsEnum -from sslyze.mozilla_tls_profile.mozilla_config_checker import ( - _MozillaTlsProfileAsJson, - MozillaTlsConfigurationEnum, +from sslyze.mozilla_tls_profile.tls_config_checker import ( + TlsConfigurationAsJson, + MozillaTlsConfiguration, + TlsConfigurationEnum, SCAN_COMMANDS_NEEDED_BY_MOZILLA_CHECKER, ) from sslyze.plugins import plugin_base @@ -67,11 +67,9 @@ class ParsedCommandLine: per_server_concurrent_connections_limit: Optional[int] concurrent_server_scans_limit: Optional[int] - # Store the parsed profile object instead of just the path - custom_tls_profile: Optional[_MozillaTlsProfileAsJson] - - # Mozilla compliance; None if shouldn't be run - check_against_mozilla_config: Optional[MozillaTlsConfigurationEnum] + # Check the server against a specific TLS configuration + tls_config_to_check_against_as_enum: Optional[TlsConfigurationEnum] # None if shouldn't be run + tls_config_to_check_against: Optional[TlsConfigurationAsJson] _STARTTLS_PROTOCOL_DICT = { @@ -106,18 +104,18 @@ def __init__(self, sslyze_version: str) -> None: # Add custom TLS profile option self._parser.add_argument( - "--custom_profile", - help="Path to a custom TLS profile JSON file following Mozilla's format", - dest="custom_profile", + "--custom_tls_config", + help="Path to a JSON file containing a specific TLS configuration to check the server against, following Mozilla's format. Cannot be used with --mozilla_config.", + dest="custom_tls_config", type=str, - default=None + default=None, ) self._parser.add_argument( "--mozilla_config", action="store", dest="mozilla_config", - choices=[config.value for config in MozillaTlsConfigurationEnum] + ["disable"], + choices=["modern", "intermediate", "old", "disable"], help="Shortcut to queue various scan commands needed to check the server's TLS configurations against one" ' of Mozilla\'s recommended TLS configurations. Set to "intermediate" by default. Use "disable" to disable' " this check.", @@ -135,20 +133,6 @@ def parse_command_line(self) -> ParsedCommandLine: TrustStoresRepository.update_default() raise TrustStoresUpdateCompleted() - # Handle custom profile if provided - custom_tls_profile = None - if args_command_list.custom_profile: - json_profile_path = Path(args_command_list.custom_profile).absolute() - if not json_profile_path.exists(): - raise CommandLineParsingError(f"Custom TLS profile file '{json_profile_path}' does not exist") - try: - json_profile_as_str = json_profile_path.read_text() - custom_tls_profile = _MozillaTlsProfileAsJson(**json.loads(json_profile_as_str)) - except (ValueError, pydantic.ValidationError) as e: - raise CommandLineParsingError( - f"Invalid custom TLS profile format in '{json_profile_path}': {str(e)}" - ) - # Handle the --targets_in command line and fill args_target_list if args_command_list.targets_in: try: # Read targets from a file @@ -166,8 +150,47 @@ def parse_command_line(self) -> ParsedCommandLine: if not args_target_list: raise CommandLineParsingError("No targets to scan.") - # Handle the case when no scan commands have been specified: run --mozilla-config=intermediate by default - if not args_command_list.mozilla_config: + if args_command_list.custom_tls_config and args_command_list.mozilla_config: + raise CommandLineParsingError( + "Cannot use --custom_tls_config and --mozilla_config. Please specify one of the two." + ) + + # Determine the TLS configuration to check against + tls_config_to_check_against_as_enum: Optional[TlsConfigurationEnum] = None + tls_config_to_check_against: Optional[TlsConfigurationAsJson] = None + if args_command_list.custom_tls_config: + # A custom TLS config was supplied + tls_config_to_check_against_as_enum = TlsConfigurationEnum.CUSTOM + json_tls_config_path = Path(args_command_list.custom_tls_config).absolute() + if not json_tls_config_path.exists(): + raise CommandLineParsingError(f"Custom TLS profile file '{json_tls_config_path}' does not exist") + try: + tls_config_to_check_against = TlsConfigurationAsJson.model_validate_json( + json_tls_config_path.read_text() + ) + except pydantic.ValidationError: + raise CommandLineParsingError(f"Could not parse custom TLS configuration file '{json_tls_config_path}'") + + elif args_command_list.mozilla_config: + # A Mozilla config was specified + mozilla_config_map = { + "modern": TlsConfigurationEnum.MOZILLA_MODERN, + "intermediate": TlsConfigurationEnum.MOZILLA_INTERMEDIATE, + "old": TlsConfigurationEnum.MOZILLA_OLD, + "disable": None, # Disable the Mozilla TLS configuration check + } + try: + tls_config_to_check_against_as_enum = mozilla_config_map[args_command_list.mozilla_config] + except KeyError: + raise CommandLineParsingError(f"Unknown value for --mozilla_config: {args_command_list.mozilla_config}") + + if tls_config_to_check_against_as_enum is not None: + # Load the corresponding Mozilla TLS configuration + tls_config_to_check_against = MozillaTlsConfiguration.get(tls_config_to_check_against_as_enum) + + else: + # Handle the case when no value was specified: run --mozilla-config=intermediate by default + # First check if the user was specific about which scan commands to run did_user_enable_some_scan_commands = False for scan_command in ScanCommandsRepository.get_all_scan_commands(): cli_connector_cls = ScanCommandsRepository.get_implementation_cls(scan_command).cli_connector_cls @@ -176,17 +199,14 @@ def parse_command_line(self) -> ParsedCommandLine: did_user_enable_some_scan_commands = True break + # If the user did not specify any scan commands, run the default scan commands + # and check the result against the Mozilla intermediate configuration if not did_user_enable_some_scan_commands: - setattr(args_command_list, "mozilla_config", MozillaTlsConfigurationEnum.INTERMEDIATE.value) - - # Enable the commands needed by --mozilla-config - check_against_mozilla_config: Optional[MozillaTlsConfigurationEnum] = None - if args_command_list.mozilla_config: - if args_command_list.mozilla_config == "disable": - check_against_mozilla_config = None - else: - check_against_mozilla_config = MozillaTlsConfigurationEnum(args_command_list.mozilla_config) + tls_config_to_check_against_as_enum = TlsConfigurationEnum.MOZILLA_INTERMEDIATE + tls_config_to_check_against = MozillaTlsConfiguration.get(tls_config_to_check_against_as_enum) + # Enable the commands needed by TLS configuration checking + if tls_config_to_check_against_as_enum: for scan_cmd in SCAN_COMMANDS_NEEDED_BY_MOZILLA_CHECKER: cli_connector_cls = ScanCommandsRepository.get_implementation_cls(scan_cmd).cli_connector_cls setattr(args_command_list, cli_connector_cls._cli_option, True) @@ -347,8 +367,8 @@ def parse_command_line(self) -> ParsedCommandLine: should_disable_console_output=args_command_list.quiet or args_command_list.json_file == "-", concurrent_server_scans_limit=concurrent_server_scans_limit, per_server_concurrent_connections_limit=per_server_concurrent_connections_limit, - custom_tls_profile=custom_tls_profile, - check_against_mozilla_config=check_against_mozilla_config, + tls_config_to_check_against_as_enum=tls_config_to_check_against_as_enum, + tls_config_to_check_against=tls_config_to_check_against, ) def _add_default_options(self) -> None: diff --git a/sslyze/mozilla_tls_profile/mozilla_config_checker.py b/sslyze/mozilla_tls_profile/tls_config_checker.py similarity index 68% rename from sslyze/mozilla_tls_profile/mozilla_config_checker.py rename to sslyze/mozilla_tls_profile/tls_config_checker.py index 05dd83e0..9079799a 100644 --- a/sslyze/mozilla_tls_profile/mozilla_config_checker.py +++ b/sslyze/mozilla_tls_profile/tls_config_checker.py @@ -50,7 +50,7 @@ def _convert_mozilla_curve_name_to_secg_name(mozilla_curves: Set[str]) -> Set[st return mozilla_curves_secg_names -class _MozillaTlsConfigurationAsJson(pydantic.BaseModel): +class TlsConfigurationAsJson(pydantic.BaseModel): certificate_curves: Annotated[Set[str], pydantic.AfterValidator(_convert_mozilla_curve_name_to_secg_name)] certificate_signatures: Set[str] certificate_types: Set[str] @@ -69,9 +69,9 @@ class _MozillaTlsConfigurationAsJson(pydantic.BaseModel): class _AllMozillaTlsConfigurationsAsJson(pydantic.BaseModel): - modern: _MozillaTlsConfigurationAsJson - intermediate: _MozillaTlsConfigurationAsJson - old: _MozillaTlsConfigurationAsJson + modern: TlsConfigurationAsJson + intermediate: TlsConfigurationAsJson + old: TlsConfigurationAsJson class _MozillaTlsProfileAsJson(pydantic.BaseModel): @@ -80,19 +80,20 @@ class _MozillaTlsProfileAsJson(pydantic.BaseModel): configurations: _AllMozillaTlsConfigurationsAsJson -class MozillaTlsConfigurationEnum(str, Enum): - MODERN = "modern" - INTERMEDIATE = "intermediate" - OLD = "old" +class TlsConfigurationEnum(str, Enum): + MOZILLA_MODERN = "modern" + MOZILLA_INTERMEDIATE = "intermediate" + MOZILLA_OLD = "old" + CUSTOM = "custom" -class ServerNotCompliantWithMozillaTlsConfiguration(Exception): +class ServerNotCompliantWithTlsConfiguration(Exception): def __init__( self, - mozilla_config: MozillaTlsConfigurationEnum, + tls_configuration: TlsConfigurationAsJson, issues: Dict[str, str], ): - self.mozilla_config = mozilla_config + self.tls_configuration = tls_configuration self.issues = issues @@ -120,92 +121,81 @@ class ServerScanResultIncomplete(Exception): } -class MozillaTlsConfigurationChecker: - def __init__(self, mozilla_tls_profile: _MozillaTlsProfileAsJson): - self._mozilla_tls_profile = mozilla_tls_profile +class MozillaTlsConfiguration: + _JSON_PROFILE_PATH = Path(__file__).parent.absolute() / "5.7.json" @classmethod - def get_default(cls) -> "MozillaTlsConfigurationChecker": - json_profile_path = Path(__file__).parent.absolute() / "5.7.json" - json_profile_as_str = json_profile_path.read_text() + def get(cls, tls_configuration_enum: TlsConfigurationEnum) -> TlsConfigurationAsJson: + json_profile_as_str = cls._JSON_PROFILE_PATH.read_text() parsed_profile = _MozillaTlsProfileAsJson(**json.loads(json_profile_as_str)) - return cls(parsed_profile) - - @classmethod - def create_from_commandline( - cls, custom_profile: Optional[_MozillaTlsProfileAsJson] = None - ) -> "MozillaTlsConfigurationChecker": - """Create a checker instance using either the default or custom profile.""" - if custom_profile: - return cls(custom_profile) - return cls.get_default() - - def check_server( - self, - against_config: MozillaTlsConfigurationEnum, - server_scan_result: ServerScanResult, - ) -> None: - # Ensure the scan was successful - if server_scan_result.scan_status != ServerScanStatusEnum.COMPLETED: - raise ServerScanResultIncomplete("The server scan was not completed.") - - # Ensure all the scan command we need were run successfully - for scan_command in SCAN_COMMANDS_NEEDED_BY_MOZILLA_CHECKER: - scan_cmd_attempt = getattr(server_scan_result.scan_result, scan_command.value) - if scan_cmd_attempt.status != ScanCommandAttemptStatusEnum.COMPLETED: - raise ServerScanResultIncomplete(f"The {scan_command.value} result is missing.") - - # Now let's check the server's scan results against the Mozilla config - mozilla_config: _MozillaTlsConfigurationAsJson = getattr( - self._mozilla_tls_profile.configurations, against_config.value + tls_config = getattr(parsed_profile.configurations, tls_configuration_enum.value) + return tls_config + + +def check_server_against_tls_configuration( + server_scan_result: ServerScanResult, + tls_config_to_check_against: TlsConfigurationAsJson, +) -> None: + # Ensure the scan was successful + if server_scan_result.scan_status != ServerScanStatusEnum.COMPLETED: + raise ServerScanResultIncomplete("The server scan was not completed.") + + # Ensure all the scan command we need were run successfully + for scan_command in SCAN_COMMANDS_NEEDED_BY_MOZILLA_CHECKER: + scan_cmd_attempt = getattr(server_scan_result.scan_result, scan_command.value) + if scan_cmd_attempt.status != ScanCommandAttemptStatusEnum.COMPLETED: + raise ServerScanResultIncomplete(f"The {scan_command.value} result is missing.") + + # Now look for issues + all_issues: Dict[str, str] = {} + + # Checks on the certificate + assert server_scan_result.scan_result + assert server_scan_result.scan_result.certificate_info + assert server_scan_result.scan_result.certificate_info.result + issues_with_certificates = _check_certificates( + cert_info_result=server_scan_result.scan_result.certificate_info.result, + tls_config=tls_config_to_check_against, + ) + all_issues.update(issues_with_certificates) + + # Checks on the TLS versions and cipher suites + assert server_scan_result.scan_result + issues_with_tls_ciphers = _check_tls_versions_and_ciphers( + server_scan_result.scan_result, tls_config_to_check_against + ) + all_issues.update(issues_with_tls_ciphers) + + # Checks on the TLS curves + assert server_scan_result.scan_result.elliptic_curves.result + issues_with_tls_curves = _check_tls_curves( + server_scan_result.scan_result.elliptic_curves.result, + tls_config_to_check_against, + ) + all_issues.update(issues_with_tls_curves) + + # Checks on TLS vulnerabilities + issues_with_tls_vulns = _check_tls_vulnerabilities(server_scan_result.scan_result) + all_issues.update(issues_with_tls_vulns) + + # TODO(AD): Re-enable this check. Right now nobody follows the recommendation of the Mozilla profile + # to have an HSTS max-age of 63072000 seconds (2 years). + # Check the HSTS header + # assert server_scan_result.scan_result.http_headers + # assert server_scan_result.scan_result.http_headers.result + # issue_with_hsts = _check_http_headers(server_scan_result.scan_result.http_headers.result, mozilla_config) + # all_issues.update(issue_with_hsts) + + if all_issues: + raise ServerNotCompliantWithTlsConfiguration( + tls_configuration=tls_config_to_check_against, + issues=all_issues, ) - all_issues: Dict[str, str] = {} - - # Checks on the certificate - assert server_scan_result.scan_result - assert server_scan_result.scan_result.certificate_info - assert server_scan_result.scan_result.certificate_info.result - issues_with_certificates = _check_certificates( - cert_info_result=server_scan_result.scan_result.certificate_info.result, - mozilla_config=mozilla_config, - ) - all_issues.update(issues_with_certificates) - - # Checks on the TLS versions and cipher suites - assert server_scan_result.scan_result - issues_with_tls_ciphers = _check_tls_versions_and_ciphers(server_scan_result.scan_result, mozilla_config) - all_issues.update(issues_with_tls_ciphers) - - # Checks on the TLS curves - assert server_scan_result.scan_result.elliptic_curves.result - issues_with_tls_curves = _check_tls_curves( - server_scan_result.scan_result.elliptic_curves.result, - mozilla_config, - ) - all_issues.update(issues_with_tls_curves) - - # Checks on TLS vulnerabilities - issues_with_tls_vulns = _check_tls_vulnerabilities(server_scan_result.scan_result) - all_issues.update(issues_with_tls_vulns) - - # TODO(AD): Re-enable this check. Right now nobody follows the recommendation of the Mozilla profile - # to have an HSTS max-age of 63072000 seconds (2 years). - # Check the HSTS header - # assert server_scan_result.scan_result.http_headers - # assert server_scan_result.scan_result.http_headers.result - # issue_with_hsts = _check_http_headers(server_scan_result.scan_result.http_headers.result, mozilla_config) - # all_issues.update(issue_with_hsts) - - if all_issues: - raise ServerNotCompliantWithMozillaTlsConfiguration( - mozilla_config=against_config, - issues=all_issues, - ) def _check_tls_curves( tls_curves_result: SupportedEllipticCurvesScanResult, - mozilla_config: _MozillaTlsConfigurationAsJson, + tls_config: TlsConfigurationAsJson, ) -> Dict[str, str]: issues_with_tls_curves = {} if tls_curves_result.supported_curves: @@ -213,7 +203,7 @@ def _check_tls_curves( else: supported_curves = set() - tls_curves_difference = supported_curves - mozilla_config.tls_curves + tls_curves_difference = supported_curves - tls_config.tls_curves if tls_curves_difference: issues_with_tls_curves["tls_curves"] = ( f"TLS curves {tls_curves_difference} are supported, but should be rejected." @@ -265,7 +255,7 @@ def _check_tls_vulnerabilities(scan_result: AllScanCommandsAttempts) -> Dict[str def _check_tls_versions_and_ciphers( scan_result: AllScanCommandsAttempts, - mozilla_config: _MozillaTlsConfigurationAsJson, + tls_config: TlsConfigurationAsJson, ) -> Dict[str, str]: # First parse the results related to TLS versions and ciphers tls_versions_supported = set() @@ -302,34 +292,34 @@ def _check_tls_versions_and_ciphers( # Then check the results issues_with_tls_ciphers = {} - tls_versions_difference = tls_versions_supported - mozilla_config.tls_versions + tls_versions_difference = tls_versions_supported - tls_config.tls_versions if tls_versions_difference: issues_with_tls_ciphers["tls_versions"] = ( f"TLS versions {tls_versions_difference} are supported, but should be rejected." ) - tls_1_3_cipher_suites_difference = tls_1_3_cipher_suites_supported - mozilla_config.ciphersuites + tls_1_3_cipher_suites_difference = tls_1_3_cipher_suites_supported - tls_config.ciphersuites if tls_1_3_cipher_suites_difference: issues_with_tls_ciphers["ciphersuites"] = ( f"TLS 1.3 cipher suites {tls_1_3_cipher_suites_difference} are supported, but should be rejected." ) - cipher_suites_difference = cipher_suites_supported - mozilla_config.ciphers.iana + cipher_suites_difference = cipher_suites_supported - tls_config.ciphers.iana if cipher_suites_difference: issues_with_tls_ciphers["ciphers"] = ( f"Cipher suites {cipher_suites_difference} are supported, but should be rejected." ) - if mozilla_config.ecdh_param_size and smallest_ecdh_param_size < mozilla_config.ecdh_param_size: + if tls_config.ecdh_param_size and smallest_ecdh_param_size < tls_config.ecdh_param_size: issues_with_tls_ciphers["ecdh_param_size"] = ( f"ECDH parameter size is {smallest_ecdh_param_size}," - f" should be superior or equal to {mozilla_config.ecdh_param_size}." + f" should be superior or equal to {tls_config.ecdh_param_size}." ) - if mozilla_config.dh_param_size and smallest_dh_param_size < mozilla_config.dh_param_size: + if tls_config.dh_param_size and smallest_dh_param_size < tls_config.dh_param_size: issues_with_tls_ciphers["dh_param_size"] = ( f"DH parameter size is {smallest_dh_param_size}," - f" should be superior or equal to {mozilla_config.dh_param_size}." + f" should be superior or equal to {tls_config.dh_param_size}." ) return issues_with_tls_ciphers @@ -337,7 +327,7 @@ def _check_tls_versions_and_ciphers( def _check_certificates( cert_info_result: CertificateInfoScanResult, - mozilla_config: _MozillaTlsConfigurationAsJson, + tls_config: TlsConfigurationAsJson, ) -> Dict[str, str]: issues_with_certificates = {} deployed_key_algorithms = set() @@ -354,17 +344,17 @@ def _check_certificates( public_key = leaf_cert.public_key() if isinstance(public_key, EllipticCurvePublicKey): deployed_key_algorithms.add("ecdsa") - if public_key.curve.name not in mozilla_config.certificate_curves: + if public_key.curve.name not in tls_config.certificate_curves: issues_with_certificates["certificate_curves"] = ( f"Certificate curve is {public_key.curve.name}," - f" should be one of {mozilla_config.certificate_curves}." + f" should be one of {tls_config.certificate_curves}." ) elif isinstance(public_key, RSAPublicKey): deployed_key_algorithms.add("rsa") - if mozilla_config.rsa_key_size and public_key.key_size < mozilla_config.rsa_key_size: + if tls_config.rsa_key_size and public_key.key_size < tls_config.rsa_key_size: issues_with_certificates["rsa_key_size"] = ( - f"RSA key size is {public_key.key_size}, minimum allowed is {mozilla_config.rsa_key_size}." + f"RSA key size is {public_key.key_size}, minimum allowed is {tls_config.rsa_key_size}." ) else: @@ -374,10 +364,10 @@ def _check_certificates( # Validate the cert's lifespan leaf_cert_lifespan = leaf_cert.not_valid_after_utc - leaf_cert.not_valid_before_utc - if leaf_cert_lifespan.days > mozilla_config.maximum_certificate_lifespan: + if leaf_cert_lifespan.days > tls_config.maximum_certificate_lifespan: issues_with_certificates["maximum_certificate_lifespan"] = ( f"Certificate life span is {leaf_cert_lifespan.days} days," - f" should be less than {mozilla_config.maximum_certificate_lifespan}." + f" should be less than {tls_config.maximum_certificate_lifespan}." ) # TODO(AD): It's unclear whether the Mozilla profile/configs takes into accounts servers with multiple leaf certs @@ -386,26 +376,26 @@ def _check_certificates( # Validate the public key algorithms # At least one of the Mozilla cert types should have been detected in the server's cert deployments found_cert_type = False - for key_algorithm in mozilla_config.certificate_types: + for key_algorithm in tls_config.certificate_types: if key_algorithm in deployed_key_algorithms: found_cert_type = True break if not found_cert_type: issues_with_certificates["certificate_types"] = ( f"Deployed certificate types are {deployed_key_algorithms}," - f" should have at least one of {mozilla_config.certificate_types}." + f" should have at least one of {tls_config.certificate_types}." ) # Validate the signature algorithms found_sig_algorithm = False - for sig_algorithm in mozilla_config.certificate_signatures: + for sig_algorithm in tls_config.certificate_signatures: if sig_algorithm in deployed_signature_algorithms: found_sig_algorithm = True break if not found_sig_algorithm: issues_with_certificates["certificate_signatures"] = ( f"Deployed certificate signatures are {deployed_signature_algorithms}," - f" should have at least one of {mozilla_config.certificate_signatures}." + f" should have at least one of {tls_config.certificate_signatures}." ) # TODO(AD): Maybe add check for ocsp_staple but that one seems optional in https://ssl-config.mozilla.org/ @@ -415,7 +405,7 @@ def _check_certificates( def _check_http_headers( http_headers_result: HttpHeadersScanResult, - mozilla_config: _MozillaTlsConfigurationAsJson, + tls_config: TlsConfigurationAsJson, ) -> Dict[str, str]: issues_with_http_headers = {} @@ -426,10 +416,10 @@ def _check_http_headers( issues_with_http_headers["hsts_min_age"] = "HSTS max-age directive is missing." else: - if http_headers_result.strict_transport_security_header.max_age < mozilla_config.hsts_min_age: + if http_headers_result.strict_transport_security_header.max_age < tls_config.hsts_min_age: issues_with_http_headers["hsts_min_age"] = ( f"HSTS max-age is {http_headers_result.strict_transport_security_header.max_age}," - f" should be superior or equal to {mozilla_config.hsts_min_age}." + f" should be superior or equal to {tls_config.hsts_min_age}." ) return issues_with_http_headers diff --git a/tests/factories.py b/tests/factories.py index d9479820..c0418de6 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -99,7 +99,8 @@ def create(): should_disable_console_output=False, per_server_concurrent_connections_limit=None, concurrent_server_scans_limit=None, - check_against_mozilla_config=None, + tls_config_to_check_against_as_enum=None, + tls_config_to_check_against=None, ) return cmd_line diff --git a/tests/test_main.py b/tests/test_main.py index b59ec18f..5ef278b3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,10 +1,14 @@ +from pathlib import Path import sys +from tempfile import NamedTemporaryFile from unittest import mock import pytest from sslyze import SslyzeOutputAsJson from sslyze.__main__ import main +from sslyze.cli.command_line_parser import CommandLineParser, CommandLineParsingError +from sslyze.mozilla_tls_profile.tls_config_checker import TlsConfigurationEnum class TestMain: @@ -42,3 +46,35 @@ def test_json_out_in_console(self, capsys): # And the JSON output has the expected format parsed_output = SslyzeOutputAsJson.model_validate_json(json_output) assert parsed_output + + def test_command_line_has_valid_custom_tls_config_file(self): + # Given a valid custom TLS configuration file + custom_config_path = Path(__file__).parent.parent / "custom_tls_config_example.json" + print(custom_config_path) + assert custom_config_path.exists() + + # When parsing a command line that specifies --custom_tls_config + command_line = ["sslyze", "--custom_tls_config", str(custom_config_path), "www.example.com"] + with mock.patch.object(sys, "argv", command_line): + parser = CommandLineParser("test") + parsed_command_line = parser.parse_command_line() + + # Then it should parse successfully + assert parsed_command_line.tls_config_to_check_against_as_enum == TlsConfigurationEnum.CUSTOM + assert parsed_command_line.tls_config_to_check_against + assert parsed_command_line.tls_config_to_check_against.tls_versions == {"TLSv1.2", "TLSv1.3"} + + def test_command_line_has_invalid_custom_tls_config_file(self): + # Given TLS configuration that actually contains invalid JSON + with NamedTemporaryFile(mode="w", suffix=".json", delete=False) as temp_file: + temp_file.write('{"invalid": json syntax}') + temp_file_path = temp_file.name + + # When parsing a command line that specifies --custom_tls_config + command_line = ["sslyze", "--custom_tls_config", temp_file_path, "www.example.com"] + with mock.patch.object(sys, "argv", command_line): + parser = CommandLineParser("test") + + # Then the invalid JSON file is rejected + with pytest.raises(CommandLineParsingError, match="Could not parse"): + parser.parse_command_line() diff --git a/tests/test_mozilla_tls_profile/test_custom_tls_config.py b/tests/test_mozilla_tls_profile/test_custom_tls_config.py new file mode 100644 index 00000000..f4367ce4 --- /dev/null +++ b/tests/test_mozilla_tls_profile/test_custom_tls_config.py @@ -0,0 +1,77 @@ +from sslyze import Scanner, ServerScanRequest, ServerNetworkLocation +from sslyze.mozilla_tls_profile.tls_config_checker import ( + TlsConfigurationAsJson, + _MozillaCiphersAsJson, + check_server_against_tls_configuration, + ServerNotCompliantWithTlsConfiguration, +) + + +class TestCustomTlsConfigurationChecker: + def test_custom_config_compliance_checking_noncompliant_server(self): + # Given a custom TLS configuration + lenient_config = TlsConfigurationAsJson( + tls_versions={"TLSv1.2", "TLSv1.3", "TLSv1.1", "TLSv1.0"}, + certificate_types={"ecdsa", "rsa"}, + certificate_curves={"secp256r1", "secp384r1", "secp521r1"}, + certificate_signatures={ + "ecdsa-with-SHA256", + "ecdsa-with-SHA384", + "ecdsa-with-SHA512", + "sha256WithRSAEncryption", + "sha384WithRSAEncryption", + "sha512WithRSAEncryption", + }, + ciphersuites={"TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_128_GCM_SHA256"}, + ciphers=_MozillaCiphersAsJson( + caddy={ + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + go={ + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + iana={ + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + openssl={ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + ), + tls_curves={"secp256r1", "secp384r1", "x25519"}, + rsa_key_size=2048, + dh_param_size=2048, + ecdh_param_size=256, + hsts_min_age=31536000, + maximum_certificate_lifespan=365, + recommended_certificate_lifespan=90, + ocsp_staple=False, + server_preferred_order=False, + ) + + # When checking a server that's not compliant with this configuration + scanner = Scanner() + scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="www.mozilla.com"))]) + server_scan_result = next(scanner.get_results()) + + # Then the compliance check runs successfully but raises an exception + was_exception_raised = False + try: + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=lenient_config + ) + except ServerNotCompliantWithTlsConfiguration: + was_exception_raised = True + + assert was_exception_raised diff --git a/tests/test_mozilla_tls_profile/test_mozilla_config_checker.py b/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py similarity index 59% rename from tests/test_mozilla_tls_profile/test_mozilla_config_checker.py rename to tests/test_mozilla_tls_profile/test_mozilla_tls_config.py index f783f0f5..2fc0cec0 100644 --- a/tests/test_mozilla_tls_profile/test_mozilla_config_checker.py +++ b/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py @@ -1,10 +1,11 @@ import pytest from sslyze import Scanner, ServerScanRequest, ServerNetworkLocation -from sslyze.mozilla_tls_profile.mozilla_config_checker import ( - MozillaTlsConfigurationChecker, - MozillaTlsConfigurationEnum, - ServerNotCompliantWithMozillaTlsConfiguration, +from sslyze.mozilla_tls_profile.tls_config_checker import ( + check_server_against_tls_configuration, + MozillaTlsConfiguration, + TlsConfigurationEnum, + ServerNotCompliantWithTlsConfiguration, ServerScanResultIncomplete, ) @@ -32,16 +33,18 @@ def test_badssl_compliant_with_old(self): # When checking if the server is compliant with the Mozilla "old" TLS config # It succeeds and the server is returned as compliant - checker = MozillaTlsConfigurationChecker.get_default() - checker.check_server( - against_config=MozillaTlsConfigurationEnum.OLD, - server_scan_result=server_scan_result, + tls_config = MozillaTlsConfiguration.get(TlsConfigurationEnum.MOZILLA_OLD) + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config ) # And the server is returned as NOT compliant for the other Mozilla configs - for mozilla_config in [MozillaTlsConfigurationEnum.INTERMEDIATE, MozillaTlsConfigurationEnum.MODERN]: - with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration): - checker.check_server(against_config=mozilla_config, server_scan_result=server_scan_result) + for mozilla_config_enum in [TlsConfigurationEnum.MOZILLA_INTERMEDIATE, TlsConfigurationEnum.MOZILLA_MODERN]: + tls_config = MozillaTlsConfiguration.get(mozilla_config_enum) + with pytest.raises(ServerNotCompliantWithTlsConfiguration): + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config + ) @pytest.mark.skip("Server needs to be updated; check https://github.com/chromium/badssl.com/issues/483") def test_badssl_compliant_with_intermediate(self): @@ -54,16 +57,18 @@ def test_badssl_compliant_with_intermediate(self): # When checking if the server is compliant with the Mozilla "intermediate" TLS config # It succeeds and the server is returned as compliant - checker = MozillaTlsConfigurationChecker.get_default() - checker.check_server( - against_config=MozillaTlsConfigurationEnum.INTERMEDIATE, - server_scan_result=server_scan_result, + tls_config = MozillaTlsConfiguration.get(TlsConfigurationEnum.MOZILLA_INTERMEDIATE) + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config ) # And the server is returned as NOT compliant for the other Mozilla configs - for mozilla_config in [MozillaTlsConfigurationEnum.OLD, MozillaTlsConfigurationEnum.MODERN]: - with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration): - checker.check_server(against_config=mozilla_config, server_scan_result=server_scan_result) + for mozilla_config in [TlsConfigurationEnum.MOZILLA_OLD, TlsConfigurationEnum.MOZILLA_MODERN]: + tls_config = MozillaTlsConfiguration.get(mozilla_config) + with pytest.raises(ServerNotCompliantWithTlsConfiguration): + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config + ) @pytest.mark.skip("Server needs to be updated; check https://github.com/chromium/badssl.com/issues/483") def test_badssl_compliant_with_modern(self): @@ -76,16 +81,18 @@ def test_badssl_compliant_with_modern(self): # When checking if the server is compliant with the Mozilla "modern" TLS config # It succeeds and the server is returned as compliant - checker = MozillaTlsConfigurationChecker.get_default() - checker.check_server( - against_config=MozillaTlsConfigurationEnum.MODERN, - server_scan_result=server_scan_result, + tls_config = MozillaTlsConfiguration.get(TlsConfigurationEnum.MOZILLA_MODERN) + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config ) # And the server is returned as NOT compliant for the other Mozilla configs - for mozilla_config in [MozillaTlsConfigurationEnum.OLD, MozillaTlsConfigurationEnum.INTERMEDIATE]: - with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration): - checker.check_server(against_config=mozilla_config, server_scan_result=server_scan_result) + for mozilla_config in [TlsConfigurationEnum.MOZILLA_OLD, TlsConfigurationEnum.MOZILLA_INTERMEDIATE]: + tls_config = MozillaTlsConfiguration.get(mozilla_config) + with pytest.raises(ServerNotCompliantWithTlsConfiguration): + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config + ) def test_solo_cert_deployment_compliant_with_old(self): # Given the scan results for a server that is compliant with the "old" Mozilla config @@ -95,10 +102,9 @@ def test_solo_cert_deployment_compliant_with_old(self): # When checking if the server is compliant with the Mozilla "old" TLS config # It succeeds and the server is returned as compliant - checker = MozillaTlsConfigurationChecker.get_default() - checker.check_server( - against_config=MozillaTlsConfigurationEnum.OLD, - server_scan_result=server_scan_result, + tls_config = MozillaTlsConfiguration.get(TlsConfigurationEnum.MOZILLA_OLD) + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config ) def test_multi_certs_deployment_compliant_with_old(self): @@ -108,25 +114,23 @@ def test_multi_certs_deployment_compliant_with_old(self): def test_multi_certs_deployment_not_compliant_with_intermediate(self, server_scan_result_for_google): # Give the scan results for google.com which has multiple leaf certificates # When checking if the server is compliant with the Mozilla "intermediate" TLS config - checker = MozillaTlsConfigurationChecker.get_default() + tls_config = MozillaTlsConfiguration.get(TlsConfigurationEnum.MOZILLA_INTERMEDIATE) # It succeeds and the server is returned as NOT compliant - with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration): - checker.check_server( - against_config=MozillaTlsConfigurationEnum.INTERMEDIATE, - server_scan_result=server_scan_result_for_google, + with pytest.raises(ServerNotCompliantWithTlsConfiguration): + check_server_against_tls_configuration( + server_scan_result=server_scan_result_for_google, tls_config_to_check_against=tls_config ) def test_multi_certs_deployment_not_compliant_with_modern(self, server_scan_result_for_google): # Give the scan results for google.com which has multiple leaf certificates # When checking if the server is compliant with the Mozilla "modern" TLS config - checker = MozillaTlsConfigurationChecker.get_default() + tls_config = MozillaTlsConfiguration.get(TlsConfigurationEnum.MOZILLA_MODERN) # It succeeds and the server is returned as NOT compliant - with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration): - checker.check_server( - against_config=MozillaTlsConfigurationEnum.MODERN, - server_scan_result=server_scan_result_for_google, + with pytest.raises(ServerNotCompliantWithTlsConfiguration): + check_server_against_tls_configuration( + server_scan_result=server_scan_result_for_google, tls_config_to_check_against=tls_config ) def test_incomplete_scan_result(self): @@ -134,10 +138,9 @@ def test_incomplete_scan_result(self): server_scan_result = ServerScanResultFactory.create() # When checking the server is compliant - checker = MozillaTlsConfigurationChecker.get_default() + tls_config = MozillaTlsConfiguration.get(TlsConfigurationEnum.MOZILLA_MODERN) # It fails with pytest.raises(ServerScanResultIncomplete): - checker.check_server( - against_config=MozillaTlsConfigurationEnum.MODERN, - server_scan_result=server_scan_result, + check_server_against_tls_configuration( + server_scan_result=server_scan_result, tls_config_to_check_against=tls_config ) From 2f39572ef93bc1a95852782bf41c7138c9f30844 Mon Sep 17 00:00:00 2001 From: Alban D Date: Sun, 3 Aug 2025 16:46:03 +0200 Subject: [PATCH 3/4] Try to fix flaky test --- tests/test_mozilla_tls_profile/test_mozilla_tls_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py b/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py index 2fc0cec0..e8f1ff76 100644 --- a/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py +++ b/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py @@ -97,7 +97,7 @@ def test_badssl_compliant_with_modern(self): def test_solo_cert_deployment_compliant_with_old(self): # Given the scan results for a server that is compliant with the "old" Mozilla config scanner = Scanner() - scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="www.mozilla.com"))]) + scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="www.google.com"))]) server_scan_result = next(scanner.get_results()) # When checking if the server is compliant with the Mozilla "old" TLS config From ebffde17a87b843d15afa1525893d7d8c1a74164 Mon Sep 17 00:00:00 2001 From: Alban D Date: Sun, 3 Aug 2025 18:31:03 +0200 Subject: [PATCH 4/4] Make it possible to see why a server is not compliant --- sslyze/mozilla_tls_profile/tls_config_checker.py | 3 +++ tests/test_mozilla_tls_profile/test_mozilla_tls_config.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sslyze/mozilla_tls_profile/tls_config_checker.py b/sslyze/mozilla_tls_profile/tls_config_checker.py index 9079799a..a98b353e 100644 --- a/sslyze/mozilla_tls_profile/tls_config_checker.py +++ b/sslyze/mozilla_tls_profile/tls_config_checker.py @@ -96,6 +96,9 @@ def __init__( self.tls_configuration = tls_configuration self.issues = issues + def __str__(self) -> str: + return f"Server is not compliant with the supplied TLS configuration due to: {self.issues}" + class ServerScanResultIncomplete(Exception): """The server scan result does not have enough information to check it against Mozilla's configuration.""" diff --git a/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py b/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py index e8f1ff76..2fc0cec0 100644 --- a/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py +++ b/tests/test_mozilla_tls_profile/test_mozilla_tls_config.py @@ -97,7 +97,7 @@ def test_badssl_compliant_with_modern(self): def test_solo_cert_deployment_compliant_with_old(self): # Given the scan results for a server that is compliant with the "old" Mozilla config scanner = Scanner() - scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="www.google.com"))]) + scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="www.mozilla.com"))]) server_scan_result = next(scanner.get_results()) # When checking if the server is compliant with the Mozilla "old" TLS config