cisagov / pshtt

Scan domains and return data based on HTTPS best practices
Creative Commons Zero v1.0 Universal
672 stars 80 forks source link

Sslyze version incompatibility #209

Open Ethanljf opened 4 years ago

Ethanljf commented 4 years ago

🐛 Bug Report

The latest changes to SSLYZE cause a few import issues within pshtt.py.

To Reproduce

Run pshtt in conjunction with the latest version of sslyze available via PIP

Expected behavior

Pshtt succesfully implements https_check() to figure out the reason an endpoint wouldn't verify.

Any helpful log output

Import errors stemming from pshtt.py

To resolve these issues, I recommend either pinning the sslyze requirement to a previous version or implementing the following changes:

Sslyze-related imports within pshtt.py should be as follows (to maintain existing functionality):

import sslyze
from sslyze.server_connectivity import ServerConnectivityTester
from sslyze.errors import ConnectionToServerFailed
from sslyze.scanner import Scanner, ServerScanRequest, ScanCommandExtraArgumentsDict
from sslyze.plugins.scan_commands import ScanCommand
from sslyze.server_setting import ServerNetworkLocation, ServerNetworkLocationViaDirectConnection

It is worth noting that the latest changes to sslyze support asynchronous scanning. However, to simply maintain existing asynchronous scanning, "https_check()" could be amended as follows:

def https_check(endpoint):
    """
    Uses sslyze to figure out the reason the endpoint wouldn't verify.
    """

    # remove the https:// from prefix for sslyze
    try:
        hostname = endpoint.url[8:]
        server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 443)
        server_tester = ServerConnectivityTester()
        server_info = server_tester.perform(server_location)
        endpoint.live = True
        ip = server_location.ip_address
        if endpoint.ip is None:
            endpoint.ip = ip
        else:
            if endpoint.ip != ip:
                logging.info("{}: Endpoint IP is already {}, but requests IP is {}.".format(endpoint.url, endpoint.ip, ip))
        if server_info.tls_probing_result.client_auth_requirement.name == 'REQUIRED':
            endpoint.https_client_auth_required = True
            logging.warning("{}: Client Authentication REQUIRED".format(endpoint.url))
    except ConnectionToServerFailed as err:
        endpoint.live = False
        endpoint.https_valid = False
        logging.exception("{}: Error in sslyze server connectivity check when connecting to {}".format(endpoint.url, err.server_info.hostname))
        return
    except Exception as err:
        endpoint.unknown_error = True
        logging.exception("{}: Unknown exception in sslyze server connectivity check.".format(endpoint.url))
        return

    try:
        cert_plugin_result = None
        command = ScanCommand.CERTIFICATE_INFO
        scanner = Scanner()
        scan_request = ServerScanRequest(server_info=server_info, scan_commands=[command])
        scanner.queue_scan(scan_request)
        # Retrieve results from generator object
        scan_result = [x for x in scanner.get_results()][0]
        cert_plugin_result = scan_result.scan_commands_results['certificate_info']
    except Exception as err:
        try:
            if "timed out" in str(err):
                logging.exception("{}: Retrying sslyze scanner certificate plugin.".format(endpoint.url))
                scanner.queue_scan(scan_request)
                # Retrieve results from generator object
                scan_result = [x for x in scanner.get_results()][0]
                cert_plugin_result = scan_result.scan_commands_results['certificate_info']
            else:
                logging.exception("{}: Unknown exception in sslyze scanner certificate plugin.".format(endpoint.url))
                endpoint.unknown_error = True
                # We could make this False, but there was an error so
                # we don't know
                endpoint.https_valid = None
                return
        except Exception:
            logging.exception("{}: Unknown exception in sslyze scanner certificate plugin.".format(endpoint.url))
            endpoint.unknown_error = True
            # We could make this False, but there was an error so we
            # don't know
            endpoint.https_valid = None
            return

    try:
        public_trust = True
        custom_trust = True
        public_not_trusted_string = ""
        validation_results = cert_plugin_result.certificate_deployments[0].path_validation_results
        for result in validation_results:
            if result.was_validation_successful:
                # We're assuming that it is trusted to start with
                pass
            else:
                if 'Custom' in result.trust_store.name:
                    custom_trust = False
                else:
                    public_trust = False
                    if len(public_not_trusted_string) > 0:
                        public_not_trusted_string += ", "
                    public_not_trusted_string += result.trust_store.name
        if public_trust:
            logging.warning("{}: Publicly trusted by common trust stores.".format(endpoint.url))
        else:
            logging.warning("{}: Not publicly trusted - not trusted by {}.".format(endpoint.url, public_not_trusted_string))
        custom_trust = None
        endpoint.https_public_trusted = public_trust
        endpoint.https_custom_trusted = custom_trust
    except Exception as err:
        # Ignore exception
        logging.exception("{}: Unknown exception examining trust.".format(endpoint.url))

    # Default endpoint assessments to False until proven True.
    endpoint.https_expired_cert = False
    endpoint.https_self_signed_cert = False
    endpoint.https_bad_chain = False
    endpoint.https_bad_hostname = False

    cert_chain = cert_plugin_result.certificate_deployments[0].received_certificate_chain

    # Check for missing SAN (Leaf certificate)
    leaf_cert = cert_chain[0]
    # Extract Subject Alternative Names
    san_list = extract_dns_subject_alternative_names(leaf_cert)
    # If an empty list was return, SAN(s) are missing. Bad hostname
    if isinstance(san_list, list) and len(san_list) == 0:
        endpoint.https_bad_hostname = True

    # If leaf certificate subject does NOT match hostname, bad hostname
    if not cert_plugin_result.certificate_deployments[0].leaf_certificate_subject_matches_hostname:
        endpoint.https_bad_hostname = True

    # Check for leaf certificate expiration/self-signature.
    if leaf_cert.not_valid_after < datetime.datetime.now():
        endpoint.https_expired_cert = True

    # Check to see if the cert is self-signed
    if leaf_cert.issuer is leaf_cert.subject:
        endpoint.https_self_signed_cert = True

    # Check certificate chain
    for cert in cert_chain[1:]:
        # Check for certificate expiration
        if cert.not_valid_after < datetime.datetime.now():
            endpoint.https_bad_chain = True

        # Check to see if the cert is self-signed
        if cert.issuer is (cert.subject or None):
            endpoint.https_bad_chain = True

    try:
        endpoint.https_cert_chain_len = len(cert_plugin_result.certificate_deployments[0].received_certificate_chain)
        if (
                endpoint.https_self_signed_cert is False and (
                    len(cert_plugin_result.certificate_deployments[0].received_certificate_chain) < 2
                )
        ):
            # *** TODO check that it is not a bad hostname and that the root cert is trusted before suggesting that it is an intermediate cert issue.
            endpoint.https_missing_intermediate_cert = True
            if(cert_plugin_result.verified_certificate_chain is None):
                logging.warning("{}: Untrusted certificate chain, probably due to missing intermediate certificate.".format(endpoint.url))
                logging.info("{}: Only {} certificates in certificate chain received.".format(endpoint.url, cert_plugin_result.received_certificate_chain.__len__()))
        else:
            endpoint.https_missing_intermediate_cert = False
    except Exception:
        logging.exception("Error while determining length of certificate chain")

    # If anything is wrong then https is not valid
    if (
        endpoint.https_expired_cert or
        endpoint.https_self_signed_cert or
        endpoint.https_bad_chain or
        endpoint.https_bad_hostname
    ):
        endpoint.https_valid = False
SaptakS commented 4 years ago

Any plans to update to sslyze v3? cryptography seems to have a CVE which is solved in Cryptography 3.2, but sslyze v2.x pins the dependency to 2.5 and hence not updating to sslyze v3 technically means using cryptography package with vulnerability.

harrislapiroff commented 3 years ago

Checking in again if there are any plans to upgrade to a newer sslyze.

This is becoming something of an issue for us at Freedom of the Press Foundation as the version of cryptography the existing dependency tree requires has been accumulating more known security issues. So far, fortunately, none of the issues have been problems for us, but someday one might be and we'd really like to be able to upgrade.

If it would speed things up, we'd be happy to volunteer some of our own engineering time to resolving this if you'd accept a pull request and, ideally, we could get some guidance from someone on your team :)