Skip to content

Commit f9bcb8a

Browse files
committed
[#680]Plugin for Extended Master Secret support
1 parent a0ec707 commit f9bcb8a

File tree

5 files changed

+209
-0
lines changed

5 files changed

+209
-0
lines changed

sslyze/mozilla_tls_profile/mozilla_config_checker.py

Lines changed: 14 additions & 0 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

@@ -202,6 +204,12 @@ def _check_tls_vulnerabilities(scan_result: AllScanCommandsAttempts) -> Dict[str
202204
"Server is vulnerable to the OpenSSL CCS injection attack."
203205
)
204206

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+
)
212+
205213
assert scan_result.heartbleed.result
206214
if scan_result.heartbleed.result.is_vulnerable_to_heartbleed:
207215
issues_with_tls_vulns["tls_vulnerability_heartbleed"] = "Server is vulnerable to the OpenSSL Heartbleed attack."
@@ -216,6 +224,12 @@ def _check_tls_vulnerabilities(scan_result: AllScanCommandsAttempts) -> Dict[str
216224
"Server is vulnerable to the insecure renegotiation attack."
217225
)
218226

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+
)
232+
219233
return issues_with_tls_vulns
220234

221235

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)