Skip to content

Commit 9025102

Browse files
committed
Update SHA2 key exchange check to new requirements (must reject SHA1 and older, sha224 to phase out).
Note labels have changed slightly.
1 parent 96956b0 commit 9025102

File tree

5 files changed

+94
-36
lines changed

5 files changed

+94
-36
lines changed

checks/categories.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,18 +1488,23 @@ def __init__(self):
14881488
def result_good(self):
14891489
self._status(STATUS_SUCCESS)
14901490
self.verdict = "detail web tls kex-hash-func verdict good"
1491-
self.tech_data = "detail tech data yes"
1491+
self.tech_data = "detail tech data good"
14921492

14931493
def result_bad(self):
14941494
self._status(STATUS_FAIL)
1495-
self.verdict = "detail web tls kex-hash-func verdict phase-out"
1496-
self.tech_data = "detail tech data no"
1495+
self.verdict = "detail web tls kex-hash-func verdict bad"
1496+
self.tech_data = "detail tech data insufficient"
14971497

14981498
def result_unknown(self):
14991499
self._status(STATUS_INFO)
15001500
self.verdict = "detail web tls kex-hash-func verdict other"
15011501
self.tech_data = "detail tech data not-applicable"
15021502

1503+
def result_phase_out(self):
1504+
self._status(STATUS_NOTICE)
1505+
self.verdict = "detail web tls kex-hash-func verdict phase-out"
1506+
self.tech_data = "detail tech data phase-out"
1507+
15031508

15041509
class MailTlsStarttlsExists(Subtest):
15051510
def __init__(self):
@@ -2072,6 +2077,11 @@ def result_unknown(self):
20722077
self.verdict = "detail mail tls kex-hash-func verdict other"
20732078
self.tech_data = "detail tech data not-applicable"
20742079

2080+
def result_phase_out(self):
2081+
self._status(STATUS_NOTICE)
2082+
self.verdict = "detail web tls kex-hash-func verdict phase-out"
2083+
self.tech_data = "detail tech data phase-out"
2084+
20752085

20762086
class MailTlsDaneExists(Subtest):
20772087
def __init__(self):

checks/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class KexHashFuncStatus(Enum):
9999
bad = 0
100100
good = 1
101101
unknown = 2
102+
phase_out = 3
102103

103104

104105
class CipherOrderStatus(Enum):

checks/tasks/tls/scans.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from dns.name import EmptyLabel
2020
from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, LifetimeTimeout
2121
from nassl._nassl import OpenSSLError
22+
from nassl.ephemeral_key_info import OpenSslEvpPkeyEnum
2223
from nassl.ssl_client import ClientCertificateRequested, OpenSslDigestNidEnum
2324
from sslyze import (
2425
ScanCommand,
@@ -78,8 +79,9 @@
7879
CERT_CURVE_MIN_KEY_SIZE,
7980
CERT_EC_CURVES_GOOD,
8081
CERT_EC_CURVES_PHASE_OUT,
81-
SIGNATURE_ALGORITHMS_SHA2,
8282
MAIL_ALTERNATE_CONNLIMIT_HOST_SUBSTRS,
83+
SIGNATURE_ALGORITHMS_BAD_HASH,
84+
SIGNATURE_ALGORITHMS_PHASE_OUT_HASH,
8385
)
8486
from internetnl import log
8587

@@ -611,6 +613,14 @@ def check_mail_tls(result: ServerScanResult, all_suites: List[CipherSuitesScanAt
611613
)
612614
cert_results = cert_checks(result.server_location.hostname, ChecksMode.MAIL)
613615

616+
key_exchange_hash_evaluation = test_key_exchange_hash(
617+
ServerConnectivityInfo(
618+
server_location=result.server_location,
619+
network_configuration=result.network_configuration,
620+
tls_probing_result=result.connectivity_result,
621+
),
622+
)
623+
614624
# HACK for DANE-TA(2) and hostname mismatch!
615625
# Give a good hosmatch score if DANE-TA *is not* present.
616626
if cert_results["tls_cert"] and not has_daneTA(cert_results["dane_records"]) and cert_results["hostmatch_bad"]:
@@ -665,8 +675,8 @@ def check_mail_tls(result: ServerScanResult, all_suites: List[CipherSuitesScanAt
665675
if result.scan_result.tls_1_3_early_data.result.supports_early_data
666676
else scoring.WEB_TLS_ZERO_RTT_GOOD
667677
),
668-
kex_hash_func=KexHashFuncStatus.good,
669-
kex_hash_func_score=scoring.WEB_TLS_KEX_HASH_FUNC_OK,
678+
kex_hash_func=key_exchange_hash_evaluation.status,
679+
kex_hash_func_score=key_exchange_hash_evaluation.score,
670680
)
671681
results.update(cert_results)
672682
return results
@@ -855,38 +865,59 @@ def test_key_exchange_hash(
855865
server_connectivity_info: ServerConnectivityInfo,
856866
) -> KeyExchangeHashFunctionEvaluation:
857867
"""
858-
Test the SHA2 key exchange per NCSC table 5.
868+
Test key exchange hashes per NCSC 3.3.5.
859869
Note that this is not the certificate hash, or TLS cipher hash.
860870
There are few or no hosts that do not meet this requirement.
861871
"""
862-
ssl_connection = server_connectivity_info.get_preconfigured_tls_connection(should_use_legacy_openssl=False)
863-
ssl_connection.ssl_client.set_sigalgs(SIGNATURE_ALGORITHMS_SHA2)
864-
865-
try:
866-
ssl_connection.connect()
867-
if ssl_connection.ssl_client.get_peer_signature_nid() == OpenSslDigestNidEnum.SHA1:
868-
log.info("Failed SHA2 key exchange check: negotiated SHA1 even when only offering SHA2")
869-
return KeyExchangeHashFunctionEvaluation(
870-
status=KexHashFuncStatus.bad,
871-
score=scoring.WEB_TLS_KEX_HASH_FUNC_BAD,
872-
)
873-
except ClientCertificateRequested:
874-
pass
875-
except (ServerRejectedTlsHandshake, TlsHandshakeTimedOut, OpenSSLError) as exc:
876-
log.info(f"Failed SHA2 key exchange check: {exc}")
872+
bad_hash_result = _test_connection_with_limited_sigalgs(server_connectivity_info, SIGNATURE_ALGORITHMS_BAD_HASH)
873+
if bad_hash_result:
874+
log.info(f"SHA2 key exchange check: negotiated bad hash ({bad_hash_result})")
877875
return KeyExchangeHashFunctionEvaluation(
878876
status=KexHashFuncStatus.bad,
879877
score=scoring.WEB_TLS_KEX_HASH_FUNC_BAD,
880878
)
881-
finally:
882-
ssl_connection.close()
879+
880+
phase_out_hash_result = _test_connection_with_limited_sigalgs(
881+
server_connectivity_info, SIGNATURE_ALGORITHMS_PHASE_OUT_HASH
882+
)
883+
if bad_hash_result:
884+
log.info(f"SHA2 key exchange check: negotiated phase_out hash ({bad_hash_result})")
885+
return KeyExchangeHashFunctionEvaluation(
886+
status=KexHashFuncStatus.phase_out,
887+
score=scoring.WEB_TLS_KEX_HASH_FUNC_OK,
888+
)
883889

884890
return KeyExchangeHashFunctionEvaluation(
885891
status=KexHashFuncStatus.good,
886892
score=scoring.WEB_TLS_KEX_HASH_FUNC_GOOD,
887893
)
888894

889895

896+
def _test_connection_with_limited_sigalgs(
897+
server_connectivity_info: ServerConnectivityInfo, sigalgs: list[tuple[OpenSslDigestNidEnum, OpenSslEvpPkeyEnum]]
898+
) -> Optional[tuple[OpenSslDigestNidEnum, OpenSslEvpPkeyEnum]]:
899+
"""
900+
Test whether the server accepts a connection with limited sigalgs through the signature_algorithms extension.
901+
Returns a (NID, EVP PKEY) if a match was found, None otherwise.
902+
"""
903+
ssl_connection = server_connectivity_info.get_preconfigured_tls_connection(should_use_legacy_openssl=False)
904+
ssl_connection.ssl_client.set_sigalgs(SIGNATURE_ALGORITHMS_BAD_HASH)
905+
906+
try:
907+
ssl_connection.connect()
908+
sigalg_nid = ssl_connection.ssl_client.get_peer_signature_nid()
909+
# Extra check as some servers will ignore the client, and force a secure hash anyways.
910+
# OpenSSL will accept this, as it does know about the secure hash.
911+
if sigalg_nid in sigalgs:
912+
return sigalg_nid
913+
except (ClientCertificateRequested, ServerRejectedTlsHandshake, TlsHandshakeTimedOut, OpenSSLError) as exc:
914+
pass
915+
finally:
916+
ssl_connection.close()
917+
918+
return None
919+
920+
890921
def test_cipher_order(
891922
server_connectivity_info: ServerConnectivityInfo,
892923
tls_versions: List[TlsVersionEnum],

checks/tasks/tls/tasks_reports.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,8 @@ def annotate_and_combine_all(good_items, sufficient_items, bad_items, phaseout_i
570570
category.subtests["kex_hash_func"].result_bad()
571571
elif dttls.kex_hash_func == KexHashFuncStatus.unknown:
572572
category.subtests["kex_hash_func"].result_unknown()
573+
elif dttls.kex_hash_func == KexHashFuncStatus.phase_out:
574+
category.subtests["kex_hash_func"].result_phase_out()
573575

574576
elif isinstance(category, categories.MailTls):
575577
if dttls.could_not_test_smtp_starttls:
@@ -730,6 +732,8 @@ def annotate_and_combine_all(good_items, sufficient_items, bad_items, phaseout_i
730732
category.subtests["kex_hash_func"].result_bad()
731733
elif dttls.kex_hash_func == KexHashFuncStatus.unknown:
732734
category.subtests["kex_hash_func"].result_unknown()
735+
elif dttls.kex_hash_func == KexHashFuncStatus.phase_out:
736+
category.subtests["kex_hash_func"].result_phase_out()
733737

734738
dttls.report = category.gen_report()
735739

checks/tasks/tls/tls_constants.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,22 +115,34 @@
115115
]
116116
PROTOCOLS_PHASE_OUT = []
117117

118-
# NCSC table 5
119-
# This is eventually passed to openssl's SSL_set1_sigalgs,
118+
# These are eventually passed to openssl's SSL_set1_sigalgs,
120119
# which requires the NIDs in digest+pubkey algorithm tuples.
121-
SIGNATURE_ALGORITHMS_SHA2 = [
122-
(OpenSslDigestNidEnum.SHA512, OpenSslEvpPkeyEnum.EC),
123-
(OpenSslDigestNidEnum.SHA384, OpenSslEvpPkeyEnum.EC),
124-
(OpenSslDigestNidEnum.SHA256, OpenSslEvpPkeyEnum.EC),
120+
# NCSC 3.3.5, connection with this set means bad hashes are enabled
121+
SIGNATURE_ALGORITHMS_BAD_HASH = [
122+
(OpenSslDigestNidEnum.MD5, OpenSslEvpPkeyEnum.EC),
123+
(OpenSslDigestNidEnum.SHA1, OpenSslEvpPkeyEnum.EC),
124+
(OpenSslDigestNidEnum.MD5, OpenSslEvpPkeyEnum.RSA),
125+
(OpenSslDigestNidEnum.SHA1, OpenSslEvpPkeyEnum.RSA),
126+
(OpenSslDigestNidEnum.MD5, OpenSslEvpPkeyEnum.RSA_PSS),
127+
(OpenSslDigestNidEnum.SHA1, OpenSslEvpPkeyEnum.RSA_PSS),
128+
(OpenSslDigestNidEnum.MD5, OpenSslEvpPkeyEnum.DSA),
129+
(OpenSslDigestNidEnum.SHA1, OpenSslEvpPkeyEnum.DSA),
130+
]
131+
SIGNATURE_ALGORITHMS_PHASE_OUT_HASH = [
132+
(OpenSslDigestNidEnum.SHA224, OpenSslEvpPkeyEnum.EC),
133+
(OpenSslDigestNidEnum.SHA224, OpenSslEvpPkeyEnum.RSA),
134+
(OpenSslDigestNidEnum.SHA224, OpenSslEvpPkeyEnum.RSA_PSS),
135+
(OpenSslDigestNidEnum.SHA224, OpenSslEvpPkeyEnum.DSA),
136+
]
137+
# NCSC 3.3.2.1: RSA PKCS must not be used.
138+
# Failing these algs means the server has no RSA or RSA in PSS only, either is fine.
139+
SIGNATURE_ALGORITHMS_RSA_PKCS = [
140+
(OpenSslDigestNidEnum.MD5, OpenSslEvpPkeyEnum.RSA),
141+
(OpenSslDigestNidEnum.SHA1, OpenSslEvpPkeyEnum.RSA),
142+
(OpenSslDigestNidEnum.SHA224, OpenSslEvpPkeyEnum.RSA),
125143
(OpenSslDigestNidEnum.SHA512, OpenSslEvpPkeyEnum.RSA),
126144
(OpenSslDigestNidEnum.SHA384, OpenSslEvpPkeyEnum.RSA),
127145
(OpenSslDigestNidEnum.SHA256, OpenSslEvpPkeyEnum.RSA),
128-
(OpenSslDigestNidEnum.SHA512, OpenSslEvpPkeyEnum.RSA_PSS),
129-
(OpenSslDigestNidEnum.SHA384, OpenSslEvpPkeyEnum.RSA_PSS),
130-
(OpenSslDigestNidEnum.SHA256, OpenSslEvpPkeyEnum.RSA_PSS),
131-
(OpenSslDigestNidEnum.SHA512, OpenSslEvpPkeyEnum.DSA),
132-
(OpenSslDigestNidEnum.SHA384, OpenSslEvpPkeyEnum.DSA),
133-
(OpenSslDigestNidEnum.SHA256, OpenSslEvpPkeyEnum.DSA),
134146
]
135147

136148
# Mail servers with an increased connection limit,

0 commit comments

Comments
 (0)