Skip to content

Commit 6eec86b

Browse files
committed
rudimentary cipher order checks
1 parent 8ca4abe commit 6eec86b

File tree

2 files changed

+118
-9
lines changed

2 files changed

+118
-9
lines changed

checks/tasks/tls.py

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from django.core.cache import cache
2828
from django.db import transaction
2929
from nassl.ephemeral_key_info import DhEphemeralKeyInfo, EcDhEphemeralKeyInfo, OpenSslEvpPkeyEnum
30+
from nassl.ssl_client import ClientCertificateRequested
3031
from sslyze import (
3132
Scanner,
3233
ServerScanRequest,
@@ -38,13 +39,19 @@
3839
ServerNetworkConfiguration,
3940
ProtocolWithOpportunisticTlsEnum,
4041
ScanCommandsExtraArguments,
41-
CertificateInfoExtraArgument, CipherSuite,
42+
CertificateInfoExtraArgument,
43+
CipherSuite,
4244
)
45+
from sslyze.errors import ServerTlsConfigurationNotSupported, ServerRejectedTlsHandshake, TlsHandshakeTimedOut
4346

4447
from sslyze.plugins.certificate_info._certificate_utils import (
4548
parse_subject_alternative_name_extension,
4649
get_common_names,
4750
)
51+
from sslyze.plugins.openssl_cipher_suites._test_cipher_suite import _set_cipher_suite_string
52+
from sslyze.plugins.openssl_cipher_suites._tls12_workaround import WorkaroundForTls12ForCipherSuites
53+
from sslyze.plugins.openssl_cipher_suites.cipher_suites import CipherSuitesRepository
54+
from sslyze.server_connectivity import ServerConnectivityInfo
4855

4956
from checks import categories, scoring
5057
from checks.http_client import http_get_ip
@@ -60,7 +67,6 @@
6067
WebTestTls,
6168
ZeroRttStatus,
6269
)
63-
from checks.scoring import Score
6470
from checks.tasks import SetupUnboundContext
6571
from checks.tasks.dispatcher import check_registry, post_callback_hook
6672
from checks.tasks.http_headers import (
@@ -1374,6 +1380,7 @@ def has_daneTA(tlsa_records):
13741380
return True
13751381
return False
13761382

1383+
13771384
def check_web_tls(url, af_ip_pair=None, *args, **kwargs):
13781385
"""
13791386
Check the webserver's TLS configuration.
@@ -1398,7 +1405,16 @@ def check_web_tls(url, af_ip_pair=None, *args, **kwargs):
13981405
prots_bad, prots_phase_out, prots_good, prots_sufficient, prots_score = evaluate_tls_protocols(prots_accepted)
13991406
dh_param, ec_param, fs_bad, fs_phase_out, fs_score = evaluate_tls_fs_params(ciphers_accepted)
14001407
cipher_evaluation = TLSCipherEvaluation.from_ciphers_accepted(ciphers_accepted)
1401-
cipher_order_violation, cipher_order_status, cipher_order_score = test_cipher_order(ciphers_accepted)
1408+
# TODO: pick best TLS version
1409+
cipher_order_violation, cipher_order_status, cipher_order_score = test_cipher_order(
1410+
ServerConnectivityInfo(
1411+
server_location=result.server_location,
1412+
network_configuration=result.network_configuration,
1413+
tls_probing_result=result.connectivity_result,
1414+
),
1415+
prots_accepted[0],
1416+
cipher_evaluation,
1417+
)
14021418

14031419
ocsp_status = OcspStatus.ok
14041420
if any(
@@ -1585,15 +1601,18 @@ def from_ciphers_accepted(cls, ciphers_accepted: List[CipherSuiteAcceptedByServe
15851601
elif suite.cipher_suite.name in CIPHERS_PHASE_OUT:
15861602
ciphers_phase_out.append(suite.cipher_suite)
15871603
else:
1588-
ciphers_bad.append(f"{suite.cipher_suite.openssl_name} ({suite.cipher_suite.name})")
1604+
ciphers_bad.append(suite.cipher_suite)
15891605
return cls(
1590-
ciphers_good=ciphers_good, ciphers_sufficient=ciphers_sufficient, ciphers_phase_out=ciphers_phase_out,
1606+
ciphers_good=ciphers_good,
1607+
ciphers_sufficient=ciphers_sufficient,
1608+
ciphers_phase_out=ciphers_phase_out,
15911609
ciphers_bad=ciphers_bad,
15921610
ciphers_good_str=cls._format_str(ciphers_good),
15931611
ciphers_sufficient_str=cls._format_str(ciphers_sufficient),
15941612
ciphers_phase_out_str=cls._format_str(ciphers_phase_out),
15951613
ciphers_bad_str=cls._format_str(ciphers_bad),
15961614
)
1615+
15971616
@staticmethod
15981617
def _format_str(suites: List[CipherSuite]) -> List[str]:
15991618
# TODO: remove IANA name, just here for debugging now
@@ -1604,13 +1623,99 @@ def score(self) -> scoring.Score:
16041623
return scoring.WEB_TLS_SUITES_BAD if self.ciphers_bad else scoring.WEB_TLS_SUITES_GOOD
16051624

16061625

1607-
def test_cipher_order(cipher_evaluation: TLSCipherEvaluation) -> Tuple[List[str], CipherOrderStatus, scoring.Score]:
1626+
def test_cipher_order(
1627+
server_connectivity_info: ServerConnectivityInfo,
1628+
tls_version: TlsVersionEnum,
1629+
cipher_evaluation: TLSCipherEvaluation,
1630+
) -> Tuple[List[str], CipherOrderStatus, scoring.Score]:
16081631
cipher_order_violation = []
1609-
cipher_order_status = CipherOrderStatus.na
1632+
cipher_order_status = CipherOrderStatus.good
16101633
cipher_order_score = scoring.WEB_TLS_CIPHER_ORDER_OK
1634+
if (
1635+
not cipher_evaluation.ciphers_bad
1636+
and not cipher_evaluation.ciphers_phase_out_str
1637+
and not cipher_evaluation.ciphers_sufficient
1638+
):
1639+
cipher_order_status = CipherOrderStatus.na
1640+
return cipher_order_violation, cipher_order_status, cipher_order_score
1641+
1642+
order_tuples = [
1643+
(
1644+
cipher_evaluation.ciphers_bad + cipher_evaluation.ciphers_phase_out + cipher_evaluation.ciphers_sufficient,
1645+
cipher_evaluation.ciphers_good,
1646+
),
1647+
(cipher_evaluation.ciphers_bad + cipher_evaluation.ciphers_phase_out, cipher_evaluation.ciphers_sufficient),
1648+
(cipher_evaluation.ciphers_bad, cipher_evaluation.ciphers_phase_out),
1649+
]
1650+
for expected_less_preferred, expected_more_preferred in order_tuples:
1651+
print(
1652+
f"evaluating less {[s.openssl_name for s in expected_less_preferred]} vs "
1653+
f"more {[s.openssl_name for s in expected_more_preferred]} TLS {tls_version}"
1654+
)
1655+
if not expected_less_preferred or not expected_more_preferred:
1656+
continue
1657+
preferred_suite = find_most_preferred_cipher_suite(
1658+
server_connectivity_info, tls_version, expected_less_preferred + expected_more_preferred
1659+
)
1660+
if preferred_suite not in expected_more_preferred:
1661+
cipher_order_violation = [preferred_suite.openssl_name] # TODO: check which name to report
1662+
cipher_order_status = CipherOrderStatus.bad
1663+
cipher_order_score = scoring.WEB_TLS_CIPHER_ORDER_BAD
1664+
16111665
return cipher_order_violation, cipher_order_status, cipher_order_score
16121666

16131667

1668+
# TODO: maybe move to a utils module?
1669+
# adapted from sslyze.plugins.openssl_cipher_suites._test_cipher_suite.connect_with_cipher_suite
1670+
def find_most_preferred_cipher_suite(
1671+
server_connectivity_info: ServerConnectivityInfo, tls_version: TlsVersionEnum, cipher_suites: List[CipherSuite]
1672+
) -> CipherSuite:
1673+
suite_names = [suite.openssl_name for suite in cipher_suites]
1674+
requires_legacy_openssl = True
1675+
if tls_version == TlsVersionEnum.TLS_1_2:
1676+
# For TLS 1.2, we need to pick the right version of OpenSSL depending on which cipher suite
1677+
requires_legacy_openssl = any(
1678+
[WorkaroundForTls12ForCipherSuites.requires_legacy_openssl(name) for name in suite_names]
1679+
)
1680+
elif tls_version == TlsVersionEnum.TLS_1_3:
1681+
requires_legacy_openssl = False
1682+
1683+
ssl_connection = server_connectivity_info.get_preconfigured_tls_connection(
1684+
override_tls_version=tls_version, should_use_legacy_openssl=requires_legacy_openssl
1685+
)
1686+
_set_cipher_suite_string(tls_version, ":".join(suite_names), ssl_connection.ssl_client)
1687+
1688+
try:
1689+
# Perform the SSL handshake
1690+
ssl_connection.connect()
1691+
1692+
except ServerTlsConfigurationNotSupported:
1693+
# SSLyze rejected the handshake because the server's DH config was too insecure; this means the
1694+
# cipher suite is actually supported
1695+
pass
1696+
1697+
except ClientCertificateRequested:
1698+
# When the handshake failed due to ClientCertificateRequested
1699+
pass
1700+
except ServerRejectedTlsHandshake:
1701+
return False
1702+
1703+
except TlsHandshakeTimedOut:
1704+
# Sometimes triggered by servers that don't support (at all) a specific version of TLS
1705+
# Amazon Cloudfront does that with TLS 1.3
1706+
# There's no easy way to differentiate this error from a network glitch/timeout
1707+
return False
1708+
1709+
finally:
1710+
ssl_connection.close()
1711+
1712+
selected_cipher = CipherSuitesRepository.get_cipher_suite_with_openssl_name(
1713+
tls_version, ssl_connection.ssl_client.get_current_cipher_name()
1714+
)
1715+
print(f"from CS {suite_names} selected {selected_cipher}")
1716+
return selected_cipher
1717+
1718+
16141719
def do_web_http(af_ip_pairs, url, task, *args, **kwargs):
16151720
"""
16161721
Start all the HTTP related checks for the web test.

checks/tasks/tls_constants.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,22 @@
3232
OpenSslEcNidEnum.SECP224R1,
3333
]
3434

35+
# ECDHE-RSA-AES256-GCM-SHA384
3536
CIPHERS_GOOD = [
3637
"TLS_AES_256_GCM_SHA384",
3738
"TLS_CHACHA20_POLY1305_SHA256",
3839
"TLS_AES_128_GCM_SHA256",
39-
]
40-
CIPHERS_SUFFICIENT = [
40+
# NCSC appendix C lists these as sufficient, but read
41+
# footnote 52 carefully. As we test TLS version separate
42+
# from cipher list, we consider them good.
4143
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
4244
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
4345
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
4446
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
4547
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
4648
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
49+
]
50+
CIPHERS_SUFFICIENT = [
4751
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
4852
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
4953
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",

0 commit comments

Comments
 (0)