|
19 | 19 | from dns.name import EmptyLabel |
20 | 20 | from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, LifetimeTimeout |
21 | 21 | from nassl._nassl import OpenSSLError |
| 22 | +from nassl.ephemeral_key_info import OpenSslEvpPkeyEnum |
22 | 23 | from nassl.ssl_client import ClientCertificateRequested, OpenSslDigestNidEnum |
23 | 24 | from sslyze import ( |
24 | 25 | ScanCommand, |
|
78 | 79 | CERT_CURVE_MIN_KEY_SIZE, |
79 | 80 | CERT_EC_CURVES_GOOD, |
80 | 81 | CERT_EC_CURVES_PHASE_OUT, |
81 | | - SIGNATURE_ALGORITHMS_SHA2, |
82 | 82 | MAIL_ALTERNATE_CONNLIMIT_HOST_SUBSTRS, |
| 83 | + SIGNATURE_ALGORITHMS_BAD_HASH, |
| 84 | + SIGNATURE_ALGORITHMS_PHASE_OUT_HASH, |
83 | 85 | ) |
84 | 86 | from internetnl import log |
85 | 87 |
|
@@ -611,6 +613,14 @@ def check_mail_tls(result: ServerScanResult, all_suites: List[CipherSuitesScanAt |
611 | 613 | ) |
612 | 614 | cert_results = cert_checks(result.server_location.hostname, ChecksMode.MAIL) |
613 | 615 |
|
| 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 | + |
614 | 624 | # HACK for DANE-TA(2) and hostname mismatch! |
615 | 625 | # Give a good hosmatch score if DANE-TA *is not* present. |
616 | 626 | 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 |
665 | 675 | if result.scan_result.tls_1_3_early_data.result.supports_early_data |
666 | 676 | else scoring.WEB_TLS_ZERO_RTT_GOOD |
667 | 677 | ), |
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, |
670 | 680 | ) |
671 | 681 | results.update(cert_results) |
672 | 682 | return results |
@@ -855,38 +865,59 @@ def test_key_exchange_hash( |
855 | 865 | server_connectivity_info: ServerConnectivityInfo, |
856 | 866 | ) -> KeyExchangeHashFunctionEvaluation: |
857 | 867 | """ |
858 | | - Test the SHA2 key exchange per NCSC table 5. |
| 868 | + Test key exchange hashes per NCSC 3.3.5. |
859 | 869 | Note that this is not the certificate hash, or TLS cipher hash. |
860 | 870 | There are few or no hosts that do not meet this requirement. |
861 | 871 | """ |
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})") |
877 | 875 | return KeyExchangeHashFunctionEvaluation( |
878 | 876 | status=KexHashFuncStatus.bad, |
879 | 877 | score=scoring.WEB_TLS_KEX_HASH_FUNC_BAD, |
880 | 878 | ) |
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 | + ) |
883 | 889 |
|
884 | 890 | return KeyExchangeHashFunctionEvaluation( |
885 | 891 | status=KexHashFuncStatus.good, |
886 | 892 | score=scoring.WEB_TLS_KEX_HASH_FUNC_GOOD, |
887 | 893 | ) |
888 | 894 |
|
889 | 895 |
|
| 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 | + |
890 | 921 | def test_cipher_order( |
891 | 922 | server_connectivity_info: ServerConnectivityInfo, |
892 | 923 | tls_versions: List[TlsVersionEnum], |
|
0 commit comments