Skip to content

Commit bb5ce43

Browse files
authored
Merge pull request #684 from nabla-c0d3/#680-plugin-for-ems-support
Plugin for Extended Master Secret support
2 parents 90e88f3 + aec8a6d commit bb5ce43

File tree

6 files changed

+234
-25
lines changed

6 files changed

+234
-25
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def get_include_files() -> List[Tuple[str, str]]:
9797
entry_points={"console_scripts": ["sslyze = sslyze.__main__:main"]},
9898
# Dependencies
9999
install_requires=[
100-
"nassl>=5.1,<6",
100+
"nassl>=5.3,<6",
101101
"cryptography>=43,<45",
102102
"tls-parser>=2,<3",
103103
"pydantic>=2.3,<3",

sslyze/mozilla_tls_profile/mozilla_config_checker.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,12 @@ class ServerScanResultIncomplete(Exception):
8989
ScanCommand.HEARTBLEED,
9090
ScanCommand.ROBOT,
9191
ScanCommand.OPENSSL_CCS_INJECTION,
92+
ScanCommand.TLS_FALLBACK_SCSV,
9293
ScanCommand.TLS_COMPRESSION,
9394
ScanCommand.SESSION_RENEGOTIATION,
9495
ScanCommand.CERTIFICATE_INFO,
9596
ScanCommand.ELLIPTIC_CURVES,
97+
ScanCommand.TLS_EXTENDED_MASTER_SECRET,
9698
# ScanCommand.HTTP_HEADERS, # Disabled for now; see below
9799
}
98100

@@ -183,9 +185,9 @@ def _check_tls_curves(
183185

184186
tls_curves_difference = supported_curves - mozilla_config.tls_curves
185187
if tls_curves_difference:
186-
issues_with_tls_curves[
187-
"tls_curves"
188-
] = f"TLS curves {tls_curves_difference} are supported, but should be rejected."
188+
issues_with_tls_curves["tls_curves"] = (
189+
f"TLS curves {tls_curves_difference} are supported, but should be rejected."
190+
)
189191

190192
return issues_with_tls_curves
191193

@@ -198,9 +200,15 @@ def _check_tls_vulnerabilities(scan_result: AllScanCommandsAttempts) -> Dict[str
198200

199201
assert scan_result.openssl_ccs_injection.result
200202
if scan_result.openssl_ccs_injection.result.is_vulnerable_to_ccs_injection:
201-
issues_with_tls_vulns[
202-
"tls_vulnerability_ccs_injection"
203-
] = "Server is vulnerable to the OpenSSL CCS injection attack."
203+
issues_with_tls_vulns["tls_vulnerability_ccs_injection"] = (
204+
"Server is vulnerable to the OpenSSL CCS injection attack."
205+
)
206+
207+
assert scan_result.tls_fallback_scsv.result
208+
if not scan_result.tls_fallback_scsv.result.supports_fallback_scsv:
209+
issues_with_tls_vulns["tls_vulnerability_fallback_scsv"] = (
210+
"Server is vulnerable to TLS downgrade attacks because it does not support the TLS_FALLBACK_SCSV mechanism."
211+
)
204212

205213
assert scan_result.heartbleed.result
206214
if scan_result.heartbleed.result.is_vulnerable_to_heartbleed:
@@ -212,9 +220,15 @@ def _check_tls_vulnerabilities(scan_result: AllScanCommandsAttempts) -> Dict[str
212220

213221
assert scan_result.session_renegotiation.result
214222
if not scan_result.session_renegotiation.result.supports_secure_renegotiation:
215-
issues_with_tls_vulns[
216-
"tls_vulnerability_renegotiation"
217-
] = "Server is vulnerable to the insecure renegotiation attack."
223+
issues_with_tls_vulns["tls_vulnerability_renegotiation"] = (
224+
"Server is vulnerable to the insecure renegotiation attack."
225+
)
226+
227+
assert scan_result.tls_extended_master_secret.result
228+
if not scan_result.tls_extended_master_secret.result.supports_ems_extension:
229+
issues_with_tls_vulns["tls_vulnerability_extended_master_secret"] = (
230+
"Server does not support the Extended Master Secret TLS extension."
231+
)
218232

219233
return issues_with_tls_vulns
220234

@@ -260,21 +274,21 @@ def _check_tls_versions_and_ciphers(
260274
issues_with_tls_ciphers = {}
261275
tls_versions_difference = tls_versions_supported - mozilla_config.tls_versions
262276
if tls_versions_difference:
263-
issues_with_tls_ciphers[
264-
"tls_versions"
265-
] = f"TLS versions {tls_versions_difference} are supported, but should be rejected."
277+
issues_with_tls_ciphers["tls_versions"] = (
278+
f"TLS versions {tls_versions_difference} are supported, but should be rejected."
279+
)
266280

267281
tls_1_3_cipher_suites_difference = tls_1_3_cipher_suites_supported - mozilla_config.ciphersuites
268282
if tls_1_3_cipher_suites_difference:
269-
issues_with_tls_ciphers[
270-
"ciphersuites"
271-
] = f"TLS 1.3 cipher suites {tls_1_3_cipher_suites_difference} are supported, but should be rejected."
283+
issues_with_tls_ciphers["ciphersuites"] = (
284+
f"TLS 1.3 cipher suites {tls_1_3_cipher_suites_difference} are supported, but should be rejected."
285+
)
272286

273287
cipher_suites_difference = cipher_suites_supported - mozilla_config.ciphers.iana
274288
if cipher_suites_difference:
275-
issues_with_tls_ciphers[
276-
"ciphers"
277-
] = f"Cipher suites {cipher_suites_difference} are supported, but should be rejected."
289+
issues_with_tls_ciphers["ciphers"] = (
290+
f"Cipher suites {cipher_suites_difference} are supported, but should be rejected."
291+
)
278292

279293
if mozilla_config.ecdh_param_size and smallest_ecdh_param_size < mozilla_config.ecdh_param_size:
280294
issues_with_tls_ciphers["ecdh_param_size"] = (
@@ -302,9 +316,9 @@ def _check_certificates(
302316
# Validate certificate trust
303317
leaf_cert = cert_deployment.received_certificate_chain[0]
304318
if not cert_deployment.verified_certificate_chain:
305-
issues_with_certificates[
306-
"certificate_path_validation"
307-
] = f"Certificate path validation failed for {leaf_cert.subject.rfc4514_string()}."
319+
issues_with_certificates["certificate_path_validation"] = (
320+
f"Certificate path validation failed for {leaf_cert.subject.rfc4514_string()}."
321+
)
308322

309323
# Validate the public key
310324
public_key = leaf_cert.public_key()
@@ -319,9 +333,9 @@ def _check_certificates(
319333
elif isinstance(public_key, RSAPublicKey):
320334
deployed_key_algorithms.add("rsa")
321335
if mozilla_config.rsa_key_size and public_key.key_size < mozilla_config.rsa_key_size:
322-
issues_with_certificates[
323-
"rsa_key_size"
324-
] = f"RSA key size is {public_key.key_size}, minimum allowed is {mozilla_config.rsa_key_size}."
336+
issues_with_certificates["rsa_key_size"] = (
337+
f"RSA key size is {public_key.key_size}, minimum allowed is {mozilla_config.rsa_key_size}."
338+
)
325339

326340
else:
327341
deployed_key_algorithms.add(public_key.__class__.__name__)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from dataclasses import dataclass
2+
from typing import List, Optional
3+
4+
from nassl.ssl_client import SslClient, ExtendedMasterSecretSupportEnum
5+
6+
from sslyze.json.pydantic_utils import BaseModelWithOrmModeAndForbid
7+
from sslyze.json.scan_attempt_json import ScanCommandAttemptAsJson
8+
from sslyze.plugins.plugin_base import (
9+
ScanCommandResult,
10+
ScanCommandImplementation,
11+
ScanCommandExtraArgument,
12+
ScanJob,
13+
ScanCommandWrongUsageError,
14+
ScanCommandCliConnector,
15+
ScanJobResult,
16+
)
17+
from sslyze.server_connectivity import ServerConnectivityInfo, TlsVersionEnum
18+
19+
20+
@dataclass(frozen=True)
21+
class EmsExtensionScanResult(ScanCommandResult):
22+
"""The result of testing a server for TLS Extended Master Secret extension support.
23+
24+
Attributes:
25+
supports_ems_extension: True if the server supports the TLS Extended Master Secret extension.
26+
"""
27+
28+
supports_ems_extension: bool
29+
30+
31+
class EmsExtensionScanResultAsJson(BaseModelWithOrmModeAndForbid):
32+
supports_ems_extension: bool
33+
34+
35+
class EmsExtensionScanAttemptAsJson(ScanCommandAttemptAsJson):
36+
result: Optional[EmsExtensionScanResultAsJson]
37+
38+
39+
class _EmsExtensionCliConnector(ScanCommandCliConnector[EmsExtensionScanResult, None]):
40+
_cli_option = "ems"
41+
_cli_description = "Test a server for TLS Extended Master Secret extension support."
42+
43+
@classmethod
44+
def result_to_console_output(cls, result: EmsExtensionScanResult) -> List[str]:
45+
result_as_txt = [cls._format_title("TLS Extended Master Secret Extension")]
46+
downgrade_txt = "OK - Supported" if result.supports_ems_extension else "VULNERABLE - EMS not supported"
47+
result_as_txt.append(cls._format_field("", downgrade_txt))
48+
return result_as_txt
49+
50+
51+
class EmsExtensionImplementation(ScanCommandImplementation[EmsExtensionScanResult, None]):
52+
"""Test a server for TLS Extended Master Secret extension support."""
53+
54+
cli_connector_cls = _EmsExtensionCliConnector
55+
56+
@classmethod
57+
def scan_jobs_for_scan_command(
58+
cls, server_info: ServerConnectivityInfo, extra_arguments: Optional[ScanCommandExtraArgument] = None
59+
) -> List[ScanJob]:
60+
if extra_arguments:
61+
raise ScanCommandWrongUsageError("This plugin does not take extra arguments")
62+
63+
return [ScanJob(function_to_call=_test_ems, function_arguments=[server_info])]
64+
65+
@classmethod
66+
def result_for_completed_scan_jobs(
67+
cls, server_info: ServerConnectivityInfo, scan_job_results: List[ScanJobResult]
68+
) -> EmsExtensionScanResult:
69+
if len(scan_job_results) != 1:
70+
raise RuntimeError(f"Unexpected number of scan jobs received: {scan_job_results}")
71+
72+
return EmsExtensionScanResult(supports_ems_extension=scan_job_results[0].get_result())
73+
74+
75+
def _test_ems(server_info: ServerConnectivityInfo) -> bool:
76+
# The Extended Master Secret extension is not relevant to TLS 1.3 and later
77+
if server_info.tls_probing_result.highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_3.value:
78+
return True
79+
80+
ssl_connection = server_info.get_preconfigured_tls_connection(
81+
# Only the modern client has EMS support
82+
should_use_legacy_openssl=False,
83+
)
84+
if not isinstance(ssl_connection.ssl_client, SslClient):
85+
raise RuntimeError("Should never happen")
86+
87+
# Perform the SSL handshake
88+
try:
89+
ssl_connection.connect()
90+
ems_support_enum = ssl_connection.ssl_client.get_extended_master_secret_support()
91+
finally:
92+
ssl_connection.close()
93+
94+
# Return the result
95+
if ems_support_enum == ExtendedMasterSecretSupportEnum.NOT_USED_IN_CURRENT_SESSION:
96+
return False
97+
elif ems_support_enum == ExtendedMasterSecretSupportEnum.USED_IN_CURRENT_SESSION:
98+
return True
99+
else:
100+
raise ValueError("Could not determine Extended Master Secret Extension support")

sslyze/plugins/scan_commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sslyze.plugins.certificate_info.implementation import CertificateInfoImplementation
88
from sslyze.plugins.compression_plugin import CompressionImplementation
99
from sslyze.plugins.early_data_plugin import EarlyDataImplementation
10+
from sslyze.plugins.ems_extension_plugin import EmsExtensionImplementation
1011
from sslyze.plugins.fallback_scsv_plugin import FallbackScsvImplementation
1112
from sslyze.plugins.heartbleed_plugin import HeartbleedImplementation
1213
from sslyze.plugins.http_headers_plugin import HttpHeadersImplementation
@@ -45,6 +46,7 @@ class ScanCommand(str, Enum):
4546
SESSION_RENEGOTIATION = "session_renegotiation"
4647
HTTP_HEADERS = "http_headers"
4748
ELLIPTIC_CURVES = "elliptic_curves"
49+
TLS_EXTENDED_MASTER_SECRET = "tls_extended_master_secret"
4850

4951

5052
class ScanCommandsRepository:
@@ -75,4 +77,5 @@ def get_all_scan_commands() -> Set[ScanCommand]:
7577
ScanCommand.SESSION_RENEGOTIATION: SessionRenegotiationImplementation,
7678
ScanCommand.HTTP_HEADERS: HttpHeadersImplementation,
7779
ScanCommand.ELLIPTIC_CURVES: SupportedEllipticCurvesImplementation,
80+
ScanCommand.TLS_EXTENDED_MASTER_SECRET: EmsExtensionImplementation,
7881
}

sslyze/scanner/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sslyze.plugins.certificate_info.implementation import CertificateInfoScanResult, CertificateInfoExtraArgument
1212
from sslyze.plugins.compression_plugin import CompressionScanResult
1313
from sslyze.plugins.early_data_plugin import EarlyDataScanResult
14+
from sslyze.plugins.ems_extension_plugin import EmsExtensionScanResult
1415
from sslyze.plugins.fallback_scsv_plugin import FallbackScsvScanResult
1516
from sslyze.plugins.heartbleed_plugin import HeartbleedScanResult
1617
from sslyze.plugins.http_headers_plugin import HttpHeadersScanResult
@@ -132,6 +133,10 @@ class SupportedEllipticCurvesScanAttempt(ScanCommandAttempt[SupportedEllipticCur
132133
pass
133134

134135

136+
class EmsExtensionScanAttempt(ScanCommandAttempt[EmsExtensionScanResult]):
137+
pass
138+
139+
135140
@dataclass(frozen=True)
136141
class AllScanCommandsAttempts:
137142
"""The result of every scan command supported by SSLyze."""
@@ -153,6 +158,7 @@ class AllScanCommandsAttempts:
153158
session_resumption: SessionResumptionSupportScanAttempt
154159
elliptic_curves: SupportedEllipticCurvesScanAttempt
155160
http_headers: HttpHeadersScanAttempt
161+
tls_extended_master_secret: EmsExtensionScanAttempt
156162

157163

158164
_SCAN_CMD_FIELD_NAME_TO_CLS: dict[str, Type[ScanCommandAttempt]] = {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from nassl.ssl_client import ClientCertificateRequested
2+
3+
from sslyze.plugins.ems_extension_plugin import (
4+
EmsExtensionImplementation,
5+
EmsExtensionScanResult,
6+
EmsExtensionScanResultAsJson,
7+
)
8+
9+
from sslyze.server_setting import (
10+
ServerNetworkLocation,
11+
ServerNetworkConfiguration,
12+
ClientAuthenticationCredentials,
13+
)
14+
from tests.connectivity_utils import check_connectivity_to_server_and_return_info
15+
from tests.markers import can_only_run_on_linux_64
16+
from tests.openssl_server import LegacyOpenSslServer, ClientAuthConfigEnum
17+
import pytest
18+
19+
20+
class TestFallbackScsvPlugin:
21+
def test_good(self) -> None:
22+
# Given a server that supports Extended Master Secret
23+
server_location = ServerNetworkLocation("www.google.com", 443)
24+
server_info = check_connectivity_to_server_and_return_info(server_location)
25+
26+
# When testing for EMS support, it succeeds with the expected result
27+
result: EmsExtensionScanResult = EmsExtensionImplementation.scan_server(server_info)
28+
assert result.supports_ems_extension
29+
30+
# And a CLI output can be generated
31+
assert EmsExtensionImplementation.cli_connector_cls.result_to_console_output(result)
32+
33+
# And the result can be converted to JSON
34+
result_as_json = EmsExtensionScanResultAsJson.model_validate(result).model_dump_json()
35+
assert result_as_json
36+
37+
@can_only_run_on_linux_64
38+
def test_bad(self) -> None:
39+
# Given a server that does NOT support EMS
40+
with LegacyOpenSslServer() as server:
41+
server_location = ServerNetworkLocation(
42+
hostname=server.hostname, ip_address=server.ip_address, port=server.port
43+
)
44+
server_info = check_connectivity_to_server_and_return_info(server_location)
45+
46+
# When testing for EMS, it succeeds
47+
result: EmsExtensionScanResult = EmsExtensionImplementation.scan_server(server_info)
48+
49+
# And the server is reported as NOT supporting it
50+
assert not result.supports_ems_extension
51+
52+
@can_only_run_on_linux_64
53+
def test_fails_when_client_auth_failed(self) -> None:
54+
# Given a server that does NOT support EMS and that requires client authentication
55+
with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server:
56+
# And sslyze does NOT provide a client certificate
57+
server_location = ServerNetworkLocation(
58+
hostname=server.hostname, ip_address=server.ip_address, port=server.port
59+
)
60+
server_info = check_connectivity_to_server_and_return_info(server_location)
61+
62+
# When testing, it fails as a client cert was not supplied
63+
with pytest.raises(ClientCertificateRequested):
64+
EmsExtensionImplementation.scan_server(server_info)
65+
66+
@can_only_run_on_linux_64
67+
def test_works_when_client_auth_succeeded(self) -> None:
68+
# Given a server that does NOT support EMS and that requires client authentication
69+
with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server:
70+
server_location = ServerNetworkLocation(
71+
hostname=server.hostname, ip_address=server.ip_address, port=server.port
72+
)
73+
# And sslyze provides a client certificate
74+
network_config = ServerNetworkConfiguration(
75+
tls_server_name_indication=server.hostname,
76+
tls_client_auth_credentials=ClientAuthenticationCredentials(
77+
certificate_chain_path=server.get_client_certificate_path(), key_path=server.get_client_key_path()
78+
),
79+
)
80+
server_info = check_connectivity_to_server_and_return_info(server_location, network_config)
81+
82+
# When testing for EMS, it succeeds
83+
result: EmsExtensionScanResult = EmsExtensionImplementation.scan_server(server_info)
84+
85+
# And the server is reported as NOT supporting EMS
86+
assert not result.supports_ems_extension

0 commit comments

Comments
 (0)