Skip to content

Commit d3d95e1

Browse files
committed
rudimentary cipher order checks
1 parent e69c893 commit d3d95e1

File tree

2 files changed

+113
-9
lines changed

2 files changed

+113
-9
lines changed

checks/tasks/tls.py

Lines changed: 107 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 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,
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,94 @@ 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_versions: List[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+
1635+
if (
1636+
not cipher_evaluation.ciphers_bad
1637+
and not cipher_evaluation.ciphers_phase_out
1638+
and not cipher_evaluation.ciphers_sufficient
1639+
) or tls_versions == [TlsVersionEnum.TLS_1_3]:
1640+
cipher_order_status = CipherOrderStatus.na
1641+
return cipher_order_violation, cipher_order_status, cipher_order_score
1642+
1643+
tls_version = sorted([t for t in tls_versions if t != TlsVersionEnum.TLS_1_3], key=lambda t: t.value)[-1]
1644+
1645+
order_tuples = [
1646+
(
1647+
cipher_evaluation.ciphers_bad + cipher_evaluation.ciphers_phase_out + cipher_evaluation.ciphers_sufficient,
1648+
cipher_evaluation.ciphers_good,
1649+
),
1650+
(cipher_evaluation.ciphers_bad + cipher_evaluation.ciphers_phase_out, cipher_evaluation.ciphers_sufficient),
1651+
(cipher_evaluation.ciphers_bad, cipher_evaluation.ciphers_phase_out),
1652+
]
1653+
for expected_less_preferred, expected_more_preferred_list in order_tuples:
1654+
if cipher_order_status == CipherOrderStatus.bad:
1655+
break
1656+
for expected_more_preferred in expected_more_preferred_list:
1657+
print(
1658+
f"evaluating less {[s.name for s in expected_less_preferred]} vs "
1659+
f"more {expected_more_preferred.name} TLS {tls_version}"
1660+
)
1661+
if not expected_less_preferred or not expected_more_preferred:
1662+
continue
1663+
preferred_suite = find_most_preferred_cipher_suite(
1664+
server_connectivity_info, tls_version, expected_less_preferred + [expected_more_preferred]
1665+
)
1666+
if preferred_suite != expected_more_preferred:
1667+
# TODO: check which name to report
1668+
cipher_order_violation = [preferred_suite.name, expected_more_preferred.name]
1669+
cipher_order_status = CipherOrderStatus.bad
1670+
cipher_order_score = scoring.WEB_TLS_CIPHER_ORDER_BAD
1671+
break
1672+
16111673
return cipher_order_violation, cipher_order_status, cipher_order_score
16121674

16131675

1676+
# TODO: maybe move to a utils module?
1677+
# adapted from sslyze.plugins.openssl_cipher_suites._test_cipher_suite.connect_with_cipher_suite
1678+
def find_most_preferred_cipher_suite(
1679+
server_connectivity_info: ServerConnectivityInfo, tls_version: TlsVersionEnum, cipher_suites: List[CipherSuite]
1680+
) -> CipherSuite:
1681+
suite_names = [suite.openssl_name for suite in cipher_suites]
1682+
requires_legacy_openssl = True
1683+
if tls_version == TlsVersionEnum.TLS_1_2:
1684+
# For TLS 1.2, we need to pick the right version of OpenSSL depending on which cipher suite
1685+
requires_legacy_openssl = any(
1686+
[WorkaroundForTls12ForCipherSuites.requires_legacy_openssl(name) for name in suite_names]
1687+
)
1688+
elif tls_version == TlsVersionEnum.TLS_1_3:
1689+
requires_legacy_openssl = False
1690+
1691+
ssl_connection = server_connectivity_info.get_preconfigured_tls_connection(
1692+
override_tls_version=tls_version, should_use_legacy_openssl=requires_legacy_openssl
1693+
)
1694+
_set_cipher_suite_string(tls_version, ":".join(suite_names), ssl_connection.ssl_client)
1695+
1696+
try:
1697+
ssl_connection.connect()
1698+
except ClientCertificateRequested:
1699+
pass
1700+
except (ServerRejectedTlsHandshake, TlsHandshakeTimedOut) as exc:
1701+
raise TLSException(
1702+
f"Unable to connect with (previously accepted) cipher suites {suite_names} to determine cipher order: {exc}"
1703+
)
1704+
finally:
1705+
ssl_connection.close()
1706+
1707+
selected_cipher = CipherSuitesRepository.get_cipher_suite_with_openssl_name(
1708+
tls_version, ssl_connection.ssl_client.get_current_cipher_name()
1709+
)
1710+
print(f"from CS {suite_names} selected {selected_cipher}")
1711+
return selected_cipher
1712+
1713+
16141714
def do_web_http(af_ip_pairs, url, task, *args, **kwargs):
16151715
"""
16161716
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)