From 3b727dcc0cfea4ff3ae7e5a7b26510439b660079 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Tue, 28 Jul 2026 13:19:26 +0200 Subject: [PATCH 1/2] [1.11.x] Ref #2031 - Remove obsolete ScanCommand.ELLIPTIC_CURVES We do not actually use the output of this command. See #2031 #2099 Signed-off-by: Sasha Romijn --- checks/tasks/tls/scans.py | 1 - 1 file changed, 1 deletion(-) diff --git a/checks/tasks/tls/scans.py b/checks/tasks/tls/scans.py index 09eaa0e46..c553fd2fa 100644 --- a/checks/tasks/tls/scans.py +++ b/checks/tasks/tls/scans.py @@ -100,7 +100,6 @@ SSLYZE_SCAN_COMMANDS = { ScanCommand.TLS_COMPRESSION, ScanCommand.SESSION_RENEGOTIATION, - ScanCommand.ELLIPTIC_CURVES, } # TLS_1_3_EARLY_DATA only works for HTTPS - it sends an HTTP GET request # which breaks SMTP sessions. See #2055. From 142ba54cee4ac845342819c90ca256f2f3647f05 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Wed, 29 Jul 2026 19:38:20 +0200 Subject: [PATCH 2/2] Revert "[1.11.x] Fix #2031 - Rewrite cipher check to reduce TLS conns" This reverts commit c1200c34fce453c50a174bbd41c9dd9c596c3b7f. --- checks/tasks/tls/scans.py | 367 +++++++++++------------------- checks/tasks/tls/tasks_reports.py | 42 +--- checks/tasks/tls/tls_constants.py | 8 +- pyproject.toml | 2 +- uv.lock | 2 +- 5 files changed, 143 insertions(+), 278 deletions(-) diff --git a/checks/tasks/tls/scans.py b/checks/tasks/tls/scans.py index c553fd2fa..565348bc7 100644 --- a/checks/tasks/tls/scans.py +++ b/checks/tasks/tls/scans.py @@ -1,6 +1,5 @@ import binascii -import functools -import math +import concurrent.futures from binascii import hexlify from enum import Enum from pathlib import Path @@ -45,7 +44,6 @@ TlsHandshakeTimedOut, ConnectionToServerFailed, ServerHostnameCouldNotBeResolved, - ServerTlsConfigurationNotSupported, ) from sslyze.plugins.certificate_info._certificate_utils import ( parse_subject_alternative_name_extension, @@ -54,9 +52,10 @@ from sslyze.plugins.openssl_cipher_suites._test_cipher_suite import ( CipherSuiteAcceptedByServer, _set_cipher_suite_string, + connect_with_cipher_suite, ) -from sslyze.plugins.openssl_cipher_suites._tls12_workaround import WorkaroundForTls12ForCipherSuites from sslyze.plugins.openssl_cipher_suites.cipher_suites import CipherSuitesRepository +from sslyze.scanner.models import CipherSuitesScanAttempt from sslyze.server_connectivity import ServerConnectivityInfo from checks import scoring @@ -90,7 +89,7 @@ CERT_RSA_MIN_PHASE_OUT_KEY_SIZE, SIGNATURE_ALGORITHMS_BAD_HASH, SIGNATURE_ALGORITHMS_PHASE_OUT_HASH, - TLS_1_3_PROBE_CIPHERS, + TLS_1_3_BAD_CIPHERS, ) from internetnl import log @@ -104,9 +103,31 @@ # TLS_1_3_EARLY_DATA only works for HTTPS - it sends an HTTP GET request # which breaks SMTP sessions. See #2055. SSLYZE_WEB_SCAN_COMMANDS = {ScanCommand.TLS_1_3_EARLY_DATA} -# Some servers ignore ciphers past the 64th in a ClientHello, others reject overly -# large ClientHellos. nmap's ssl-enum-ciphers uses 64 too. -CIPHER_PROBE_CHUNK_SIZE = 64 +SSLYZE_SCAN_COMMANDS_FOR_TLS = { + TlsVersionEnum.SSL_2_0: ScanCommand.SSL_2_0_CIPHER_SUITES, + TlsVersionEnum.SSL_3_0: ScanCommand.SSL_3_0_CIPHER_SUITES, + TlsVersionEnum.TLS_1_0: ScanCommand.TLS_1_0_CIPHER_SUITES, + TlsVersionEnum.TLS_1_1: ScanCommand.TLS_1_1_CIPHER_SUITES, + TlsVersionEnum.TLS_1_2: ScanCommand.TLS_1_2_CIPHER_SUITES, + TlsVersionEnum.TLS_1_3: ScanCommand.TLS_1_3_CIPHER_SUITES, +} + + +def cipher_scan_commands_for_versions(supported_tls_versions: list[TlsVersionEnum]) -> set[ScanCommand]: + """ + Determine which cipher suite scan commands to run. + All TLS 1.3 ciphers but one are good/sufficient, so we skip the full scan; + `detect_tls_1_3_bad_ciphers` probes the one bad cipher separately (#2078). + For any non-1.3 version, only the highest is scanned. + Potentially differing ciphers on lower versions are not independently interesting, + because the TLS version test already fails on those. + See also #2031 + """ + non_tls13 = [v for v in supported_tls_versions if v != TlsVersionEnum.TLS_1_3] + if not non_tls13: + return set() + highest = max(non_tls13, key=lambda v: v.value) + return {SSLYZE_SCAN_COMMANDS_FOR_TLS[highest]} # Some of the code in this file calls @@ -512,20 +533,68 @@ def check_pubkey(certificates: list[Certificate], mode: ChecksMode): return pubkey_score, bad_pubkey, phase_out_pubkey -def connection_limit_for_mail_hostname(hostname: str) -> int: +def check_mail_tls_multiple(server_tuples) -> dict[str, dict[str, Any]]: + """ + Perform sslyze probing on all mail servers, in parallel. + """ + scans = [] + results = {} + ems_evaluations = {} + tls_versions_per_server = {} + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + future_to_server = {} + + for server in server_tuples: + future = executor.submit(_generate_mail_server_scan_request, server) + future_to_server[future] = server + + for future in concurrent.futures.as_completed(future_to_server): + server = future_to_server[future] + try: + scan_request, ems_evaluation, supported_tls_versions = future.result() + except Exception as exc: + log.error(f"Unexpected error generating scan request for mail server {server}: {exc}", exc_info=True) + results[server] = dict(server_reachable=False, tls_enabled=False) + continue + + if scan_request: + scans.append(scan_request) + ems_evaluations[scan_request.server_location.hostname] = ems_evaluation + tls_versions_per_server[scan_request.server_location.hostname] = supported_tls_versions + else: + results[server] = dict(server_reachable=False, tls_enabled=False) + + if not scans: + return results + connection_limit = connection_limit_for_scans(scans) + for all_suites, result, error in run_sslyze(scans, connection_limit=connection_limit): + hostname = result.server_location.hostname + if error: + log.info(f"sslyze scan for mail failed: {error}") + results[hostname] = dict(server_reachable=False, tls_enabled=False) + continue + log.debug(f"sslyze mail scan complete for {hostname}, other scans may be pending") + results[hostname] = check_mail_tls( + result, all_suites, ems_evaluations[hostname], tls_versions_per_server[hostname] + ) + log.debug(f"check_mail_tls complete for {hostname}") + return results + + +def connection_limit_for_scans(scans: list[ServerScanRequest]): """ - Determine the per-server sslyze connection limit for a mail server. - Some hosts (anti-spam services that throttle scanners) need a higher limit - to avoid the scan stalling; see MAIL_ALTERNATE_CONNLIMIT_HOST_SUBSTRS. + Determine the appropriate connection limit for a mail server. + Sometimes we set this higher, due to anti-spam slowness. """ + hostnames = [scan.server_location.hostname for scan in scans] for hostname_substr, limit in MAIL_ALTERNATE_CONNLIMIT_HOST_SUBSTRS.items(): - if hostname_substr in hostname: - log.info(f"conn limit raised to {limit} for {hostname_substr} found in {hostname}") + if any([hostname_substr in hostname for hostname in hostnames]): + log.info(f"conn limit raised to: {limit} for {hostname_substr} found in {hostnames}") return limit return 1 -def generate_mail_server_scan_request( +def _generate_mail_server_scan_request( mx_hostname: str, ) -> tuple[ServerScanRequest | None, TLSExtendedMasterSecretEvaluation, list[TlsVersionEnum]]: """ @@ -554,7 +623,7 @@ def generate_mail_server_scan_request( if not supported_tls_versions: log.info(f"no TLS version support found for MX host {mx_hostname}, marking server unreachable") return None, extended_master_secret_evaluation, [] - scan_commands = set(SSLYZE_SCAN_COMMANDS) + scan_commands = SSLYZE_SCAN_COMMANDS | cipher_scan_commands_for_versions(supported_tls_versions) return ( ServerScanRequest( @@ -574,6 +643,7 @@ def generate_mail_server_scan_request( def check_mail_tls( result: ServerScanResult, + all_suites: list[CipherSuitesScanAttempt], extended_master_secret_evaluation: TLSExtendedMasterSecretEvaluation, supported_tls_versions: list[TlsVersionEnum], ): @@ -587,7 +657,8 @@ def check_mail_tls( tls_probing_result=result.connectivity_result, ) - ciphers_accepted = find_accepted_ciphers(server_conn_info, supported_tls_versions) + ciphers_accepted = [cipher for suites in all_suites for cipher in suites.result.accepted_cipher_suites] + ciphers_accepted.extend(detect_tls_1_3_bad_ciphers(server_conn_info, supported_tls_versions, TLS_1_3_BAD_CIPHERS)) protocol_evaluation = TLSProtocolEvaluation.from_protocols_accepted(supported_tls_versions) fs_evaluation = TLSForwardSecrecyParameterEvaluation.from_ciphers_accepted(ciphers_accepted) @@ -673,25 +744,10 @@ def has_daneTA(tlsa_records): return False -def scan_one_mail_server(mx_hostname: str) -> dict: - """Generate the sslyze scan request, run sslyze, and evaluate the result for one MX server.""" - scan_request, ems_evaluation, supported_tls_versions = generate_mail_server_scan_request(mx_hostname) - if not scan_request: - return dict(server_reachable=False, tls_enabled=False) - connection_limit = connection_limit_for_mail_hostname(mx_hostname) - result, error = next(run_sslyze([scan_request], connection_limit=connection_limit)) - if error: - log.info(f"sslyze scan for mail server {mx_hostname} failed: {error}") - return dict(server_reachable=False, tls_enabled=False) - log.debug(f"sslyze mail scan complete for {mx_hostname}, evaluating") - return check_mail_tls(result, ems_evaluation, supported_tls_versions) - - def check_web_tls(url, af_ip_pair=None, *args, **kwargs): """ Check the webserver's TLS configuration. """ - log.debug(f"check_web_tls start for {url}/{af_ip_pair[1] if af_ip_pair else None}") server_location = ServerNetworkLocation(hostname=url, ip_address=af_ip_pair[1]) network_configuration = ServerNetworkConfiguration( tls_server_name_indication=url.rstrip("."), @@ -706,8 +762,13 @@ def check_web_tls(url, af_ip_pair=None, *args, **kwargs): tls_probing_result=FAKE_SERVER_TLS_PROBING_RESULT, ) ) - scan_commands = SSLYZE_SCAN_COMMANDS | SSLYZE_WEB_SCAN_COMMANDS | {ScanCommand.CERTIFICATE_INFO} - log.info(f"precheck on {server_location} supports {supported_tls_versions} {scan_commands=}") + scan_commands = ( + SSLYZE_SCAN_COMMANDS + | SSLYZE_WEB_SCAN_COMMANDS + | cipher_scan_commands_for_versions(supported_tls_versions) + | {ScanCommand.CERTIFICATE_INFO} + ) + log.info(f"==== precheck on {server_location} supports {supported_tls_versions} {scan_commands=}") scan = ServerScanRequest( server_location=server_location, network_configuration=network_configuration, @@ -719,8 +780,8 @@ def check_web_tls(url, af_ip_pair=None, *args, **kwargs): ), ), ) - result, error = next(run_sslyze([scan], connection_limit=25)) - if error and result.scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY: + all_suites, result, error = next(run_sslyze([scan], connection_limit=25)) + if error and not all_suites: log.info(f"sslyze scan for web on {url} failed: {error}") return dict(server_reachable=False, tls_enabled=False) if error: @@ -732,7 +793,8 @@ def check_web_tls(url, af_ip_pair=None, *args, **kwargs): tls_probing_result=result.connectivity_result, ) - ciphers_accepted = find_accepted_ciphers(server_conn_info, supported_tls_versions) + ciphers_accepted = [cipher for suites in all_suites for cipher in suites.result.accepted_cipher_suites] + ciphers_accepted.extend(detect_tls_1_3_bad_ciphers(server_conn_info, supported_tls_versions, TLS_1_3_BAD_CIPHERS)) protocol_evaluation = TLSProtocolEvaluation.from_protocols_accepted(supported_tls_versions) fs_evaluation = TLSForwardSecrecyParameterEvaluation.from_ciphers_accepted(ciphers_accepted) @@ -819,7 +881,7 @@ def check_web_tls(url, af_ip_pair=None, *args, **kwargs): def run_sslyze( scans: list[ServerScanRequest], connection_limit: int -) -> Generator[tuple[ServerScanResult, TLSException | None]]: +) -> Generator[tuple[list[CipherSuitesScanAttempt], ServerScanResult, TLSException | None]]: """ Run a set of sslyze scans in parallel. Starts each scan request at the same time, and yields them as soon as they are finished. @@ -833,16 +895,28 @@ def run_sslyze( for result in scanner.get_results(): log.debug(f"sslyze scan for {result.server_location} result: {result.scan_status}") if result.scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY: - yield result, TLSException(f"could not connect: {''.join(result.connectivity_error_trace.format())}") + yield [], result, TLSException(f"could not connect: {''.join(result.connectivity_error_trace.format())}") continue + all_suites = [ + suite + for suite in ( + result.scan_result.ssl_2_0_cipher_suites, + result.scan_result.ssl_3_0_cipher_suites, + result.scan_result.tls_1_0_cipher_suites, + result.scan_result.tls_1_1_cipher_suites, + result.scan_result.tls_1_2_cipher_suites, + result.scan_result.tls_1_3_cipher_suites, + ) + if suite and suite.result + ] # Error is caught and returned here, as we may be running many scans., # and don't want to abort all scans for one failure. try: raise_sslyze_errors(result) except TLSException as exc: - yield result, exc + yield all_suites, result, exc continue - yield result, None + yield all_suites, result, None def raise_sslyze_errors(result: ServerScanResult) -> None: @@ -1051,210 +1125,33 @@ def _check_cipher_suite_available(tls_version: TlsVersionEnum, cipher_suite: Cip return False -def tls_versions_for_cipher_detection(supported_tls_versions: list[TlsVersionEnum]) -> list[TlsVersionEnum]: - """ - Pick the TLS versions for which to probe accepted ciphers. - For non-1.3 versions, only the highest supported one. Differences on lower versions - aren't in themselves interesting since the TLS version test already fails on those. - TLS 1.3 uses a much smaller cipher set, so it's probed separately when supported. - """ - versions: list[TlsVersionEnum] = [] - non_tls13 = [v for v in supported_tls_versions if v != TlsVersionEnum.TLS_1_3] - if non_tls13: - versions.append(max(non_tls13, key=lambda v: v.value)) - if TlsVersionEnum.TLS_1_3 in supported_tls_versions: - versions.append(TlsVersionEnum.TLS_1_3) - return versions - - -def find_accepted_ciphers( +def detect_tls_1_3_bad_ciphers( server_conn_info: ServerConnectivityInfo, supported_tls_versions: list[TlsVersionEnum], + cipher_names: list[str], ) -> list[CipherSuiteAcceptedByServer]: """ - Iterative-removal probe over the TLS versions worth checking: for each chunk of - candidates, offer them all in one ClientHello; on success, record the negotiated - cipher, drop it, retry. A chunk stops on any non-success outcome. If the server - negotiates a cipher we didn't offer, that's a protocol violation and aborts the - whole probe via TLSException. O(accepted + chunks) connections instead of - O(candidates). - """ - hostname = server_conn_info.server_location.hostname - accepted: list[CipherSuiteAcceptedByServer] = [] - - for tls_version in tls_versions_for_cipher_detection(supported_tls_versions): - accepted_for_version: list[CipherSuiteAcceptedByServer] = [] - candidate_count = 0 - - for use_legacy_openssl, candidates in _candidate_groups_for_version(tls_version): - candidate_count += len(candidates) - for chunk in _balanced_chunks(candidates, CIPHER_PROBE_CHUNK_SIZE): - accepted_for_version.extend( - _test_accepted_ciphers(server_conn_info, tls_version, chunk, use_legacy_openssl) - ) - - log.info( - f"cipher probe on {hostname} for {tls_version.name} complete:" - f" {len(accepted_for_version)} cipher(s) accepted from {candidate_count} candidates" - ) - accepted.extend(accepted_for_version) - return accepted - - -# Cached wrapper: sslyze's requires_legacy_openssl instantiates a LegacySslClient on -# every call, which is expensive when partitioning a full TLS 1.2 cipher list. -@functools.cache -def _requires_legacy_openssl(openssl_name: str) -> bool: - return WorkaroundForTls12ForCipherSuites.requires_legacy_openssl(openssl_name) - - -def _candidate_groups_for_version( - tls_version: TlsVersionEnum, -) -> list[tuple[bool, list[CipherSuite]]]: - """ - Cipher candidates to probe for a given TLS version, grouped by whether they need - nassl's legacy OpenSSL build. Returns [(use_legacy_openssl, candidates), ...]. - - TLS 1.3 narrows to TLS_1_3_PROBE_CIPHERS (every other TLS 1.3 cipher is - good/sufficient). TLS 1.2 has to be partitioned per-cipher because weak ciphers - (CBC-SHA, RC4, etc.) are only offered by the legacy build. Older versions go - through the legacy build entirely. - """ - if tls_version == TlsVersionEnum.TLS_1_3: - candidates = [] - for name in TLS_1_3_PROBE_CIPHERS: - try: - candidates.append(CipherSuitesRepository.get_cipher_suite_with_openssl_name(tls_version, name)) - except ValueError: - log.critical(f"TLS 1.3 probe cipher {name!r} not found in sslyze's repository, skipping") - return [(False, candidates)] - - all_candidates = list(CipherSuitesRepository.get_all_cipher_suites(tls_version)) - - if tls_version == TlsVersionEnum.TLS_1_2: - legacy = [c for c in all_candidates if _requires_legacy_openssl(c.openssl_name)] - modern = [c for c in all_candidates if not _requires_legacy_openssl(c.openssl_name)] - return [(False, modern), (True, legacy)] - - # TLS versions below 1.2 always need the legacy build. - return [(True, all_candidates)] - - -def _balanced_chunks(items: list[CipherSuite], max_chunk_size: int) -> list[list[CipherSuite]]: - """ - Split `items` into chunks of size at most `max_chunk_size`, interleaved across the - input: chunk 0 gets items [0, N, 2N, ...], chunk 1 gets [1, N+1, 2N+1, ...], etc. - Interleaving spreads accepted ciphers across chunks rather than letting them cluster - in one, so if a single chunk runs into a server-side quirk it doesn't take a - disproportionate share down with it. + Targeted probes for TLS 1.3 ciphers, as TLS 1.3 is common, and all + but one cipher are good/sufficient. (#2078, #2050, #2031) """ - if not items: + if TlsVersionEnum.TLS_1_3 not in supported_tls_versions: return [] - n_chunks = math.ceil(len(items) / max_chunk_size) - return [items[i::n_chunks] for i in range(n_chunks)] - - -def _test_accepted_ciphers( - server_conn_info: ServerConnectivityInfo, - tls_version: TlsVersionEnum, - candidates: list[CipherSuite], - use_legacy_openssl: bool, -) -> list[CipherSuiteAcceptedByServer]: accepted: list[CipherSuiteAcceptedByServer] = [] - remaining = {c.openssl_name: c for c in candidates} - - while remaining: - result = _attempt_connect_with_cipher_string( - server_conn_info, tls_version, ":".join(remaining), use_legacy_openssl=use_legacy_openssl - ) - if result is None: - break - - negotiated_name, ephemeral_key = result - negotiated = remaining.pop(negotiated_name, None) - if negotiated is None: - # Server negotiated a cipher outside the offered set, a protocol violation. - # Be loud so we know how often this happens. - raise TLSException( - f"cipher probe on {server_conn_info.server_location.hostname} for {tls_version.name}:" - f" server reported negotiated cipher {negotiated_name!r} not in offered list" - ) - accepted.append(CipherSuiteAcceptedByServer(cipher_suite=negotiated, ephemeral_key=ephemeral_key)) - return accepted - - -# adapted from sslyze.plugins.openssl_cipher_suites._test_cipher_suite.connect_with_cipher_suite, -# but extended to offer multiple ciphers in one Hello (vs sslyze's per-cipher probes) -# and to make should_use_legacy_openssl a caller decision (vs sslyze's per-cipher dispatch). -def _attempt_connect_with_cipher_string( - server_conn_info: ServerConnectivityInfo, - tls_version: TlsVersionEnum, - cipher_suite_str: str, - *, - use_legacy_openssl: bool, -) -> tuple[str, Any] | None: - """ - Try to connect with the cipher string (colon-joined). On success, return - (negotiated openssl cipher name, ephemeral key info). Returns None on benign - non-success outcomes (rejection, handshake failure, timeout). Raises TLSException - on nassl protocol anomalies (sslyze signalled an accepted handshake but exposed - no cipher name). - """ - ssl_connection = server_conn_info.get_preconfigured_tls_connection( - override_tls_version=tls_version, should_use_legacy_openssl=use_legacy_openssl - ) - - try: - _set_cipher_suite_string(tls_version, cipher_suite_str, ssl_connection.ssl_client) - ssl_connection.connect() - negotiated_name = ssl_connection.ssl_client.get_current_cipher_name() - # get_ephemeral_key can raise for ciphers without an exposed ephemeral key - # (e.g. RSA key exchange). The handshake succeeded, so the cipher is accepted. + for cipher_name in cipher_names: try: - ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key() - except OpenSSLError: - ephemeral_key = None - return negotiated_name, ephemeral_key - - except ServerTlsConfigurationNotSupported: - # sslyze refused to complete the handshake because the server's TLS configuration - # (typically weak DH parameters) is below its minimum thresholds. The cipher itself - # was accepted, so report it with no ephemeral key info. sslyze does the same by default - negotiated_name = ssl_connection.ssl_client.get_current_cipher_name() - if not negotiated_name: - raise TLSException( - f"ServerTlsConfigurationNotSupported on {server_conn_info.server_location.hostname}" - f" for {tls_version.name} but nassl exposed no cipher name" + cipher_suite = CipherSuitesRepository.get_cipher_suite_with_openssl_name( + TlsVersionEnum.TLS_1_3, cipher_name ) - return negotiated_name, None - - except ClientCertificateRequested: - # In both TLS 1.2 (RFC5246 7.3) and TLS 1.3 (RFC8446 4.3.2) the server's cipher - # choice is carried in ServerHello, which arrives before CertificateRequest; the - # cipher is therefore already negotiated when this raises. - negotiated_name = ssl_connection.ssl_client.get_current_cipher_name() - if not negotiated_name: - raise TLSException( - f"ClientCertificateRequested on {server_conn_info.server_location.hostname}" - f" for {tls_version.name} but nassl exposed no cipher name" + result = connect_with_cipher_suite(server_conn_info, TlsVersionEnum.TLS_1_3, cipher_suite) + except Exception as exc: + log.warning( + f"TLS 1.3 cipher probe for {cipher_name} failed on" + f" {server_conn_info.server_location.hostname}: {exc}" ) - try: - ephemeral_key = ssl_connection.ssl_client.get_ephemeral_key() - except OpenSSLError: - ephemeral_key = None - return negotiated_name, ephemeral_key - - except (ConnectionToServerFailed, OpenSSLError, TlsHandshakeTimedOut, ValueError): - return None - - except Exception as exc: - log.warning( - f"cipher probe on {server_conn_info.server_location.hostname} for {tls_version.name}" - f" hit an unexpected error, stopping chunk: {exc!r}" - ) - return None - finally: - ssl_connection.close() + continue + if isinstance(result, CipherSuiteAcceptedByServer): + accepted.append(result) + return accepted def check_supported_tls_versions( diff --git a/checks/tasks/tls/tasks_reports.py b/checks/tasks/tls/tasks_reports.py index 8033589f5..adb432971 100644 --- a/checks/tasks/tls/tasks_reports.py +++ b/checks/tasks/tls/tasks_reports.py @@ -1,6 +1,5 @@ # Copyright: 2022, ECP, NLnet Labs and the Internet.nl contributors # SPDX-License-Identifier: Apache-2.0 -import concurrent.futures import itertools import time from timeit import default_timer as timer @@ -45,13 +44,7 @@ # Gevent does not have the same issue. from internetnl import log -from checks.tasks.tls.scans import ( - ChecksMode, - cert_checks, - has_daneTA, - check_web_tls, - scan_one_mail_server, -) +from checks.tasks.tls.scans import ChecksMode, cert_checks, has_daneTA, check_web_tls, check_mail_tls_multiple # Maximum number of tries on failure to establish a connection. # Useful on one-time errors on SMTP. @@ -819,19 +812,12 @@ def do_web_cert(af_ip_pairs, url, *args, **kwargs): def do_web_conn(af_ip_pairs, url, *args, **kwargs): """ Start all the TLS related checks for the web test. - IPv4 and IPv6 pairs are scanned in parallel. + """ - results = {} try: - with concurrent.futures.ThreadPoolExecutor(max_workers=max(len(af_ip_pairs), 1)) as executor: - future_to_ip = { - executor.submit(check_web_tls, url, af_ip_pair, args, kwargs): af_ip_pair[1] - for af_ip_pair in af_ip_pairs - } - for future in concurrent.futures.as_completed(future_to_ip): - ip = future_to_ip[future] - results[ip] = future.result() - log.debug(f"do_web_conn {url}: result for {ip} collected") + results = {} + for af_ip_pair in af_ip_pairs: + results[af_ip_pair[1]] = check_web_tls(url, af_ip_pair, args, kwargs) except SoftTimeLimitExceeded: log.debug("Soft time limit exceeded. Url: %s", url) for af_ip_pair in af_ip_pairs: @@ -841,24 +827,6 @@ def do_web_conn(af_ip_pairs, url, *args, **kwargs): return ("tls_conn", results) -def check_mail_tls_multiple(mx_hostnames) -> dict[str, dict]: - """ - Run per-MX TLS analysis in parallel across MX servers, with each MX handled end-to-end - on its own thread. - """ - results = {} - with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: - future_to_hostname = {executor.submit(scan_one_mail_server, mx): mx for mx in mx_hostnames} - for future in concurrent.futures.as_completed(future_to_hostname): - mx = future_to_hostname[future] - try: - results[mx] = future.result() - except Exception as exc: - log.error(f"unexpected error scanning mail server {mx}: {exc}", exc_info=True) - results[mx] = dict(server_reachable=False, tls_enabled=False) - return results - - def do_mail_smtp_starttls(mailservers, url, *args, **kwargs): """ Start all the TLS related checks for the mail test. diff --git a/checks/tasks/tls/tls_constants.py b/checks/tasks/tls/tls_constants.py index 21619e394..0739cec3f 100644 --- a/checks/tasks/tls/tls_constants.py +++ b/checks/tasks/tls/tls_constants.py @@ -73,10 +73,10 @@ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", ] -# TLS 1.3 ciphers we probe for. All other TLS 1.3 ciphers are good/sufficient -# (see CIPHERS_GOOD/CIPHERS_SUFFICIENT above), so we only check for this bad one. -# See #2078, #2050, #2031. -TLS_1_3_PROBE_CIPHERS = ["TLS_AES_128_CCM_8_SHA256"] +# TLS 1.3 is special: it's really common, and there is only one +# rare bad cipher. So we reverse the usual testing and only +# verify the one bad cipher (#2078, #2050, #2031) +TLS_1_3_BAD_CIPHERS = ["TLS_AES_128_CCM_8_SHA256"] CIPHERS_PHASE_OUT = [ "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", diff --git a/pyproject.toml b/pyproject.toml index 824fd9203..51134191e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "service-identity", "setuptools-rust", "setuptools-scm", - "sslyze>=6.3.0,<7", + "sslyze>=6.3.0", "statshog", "uwsgi", "wheel", diff --git a/uv.lock b/uv.lock index ad02e54c6..01c6b0e6a 100644 --- a/uv.lock +++ b/uv.lock @@ -985,7 +985,7 @@ requires-dist = [ { name = "service-identity" }, { name = "setuptools-rust" }, { name = "setuptools-scm" }, - { name = "sslyze", specifier = ">=6.3.0,<7" }, + { name = "sslyze", specifier = ">=6.3.0" }, { name = "statshog" }, { name = "uwsgi" }, { name = "wheel" },