Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------
Expand Down
59 changes: 59 additions & 0 deletions custom_tls_config_example.json
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 24 additions & 15 deletions sslyze/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down Expand Up @@ -87,37 +88,45 @@ 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)

if {res.connectivity_status for res in all_server_scan_results} in [set(), {ServerConnectivityStatusEnum.ERROR}]:
# 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.get_default()
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():
Expand Down
84 changes: 67 additions & 17 deletions sslyze/cli/command_line_parser.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pydantic
from dataclasses import dataclass
from argparse import ArgumentParser
from pathlib import Path
Expand All @@ -11,8 +12,10 @@
CommandLineServerStringParser,
)
from sslyze.connection_helpers.opportunistic_tls_helpers import ProtocolWithOpportunisticTlsEnum
from sslyze.mozilla_tls_profile.mozilla_config_checker import (
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
Expand Down Expand Up @@ -64,8 +67,9 @@ class ParsedCommandLine:
per_server_concurrent_connections_limit: Optional[int]
concurrent_server_scans_limit: Optional[int]

# 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 = {
Expand Down Expand Up @@ -98,11 +102,20 @@ def __init__(self, sslyze_version: str) -> None:
action=scan_option.action,
)

# Add custom TLS profile option
self._parser.add_argument(
"--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,
)

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.",
Expand Down Expand Up @@ -137,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
Expand All @@ -147,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)
Expand Down Expand Up @@ -318,7 +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,
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:
Expand Down
Loading
Loading