Skip to content

Commit

Permalink
[#627][#638] Swith to cryptography's cert validation function
Browse files Browse the repository at this point in the history
  • Loading branch information
nabla-c0d3 committed Feb 24, 2024
1 parent 0c88af2 commit 351ca80
Show file tree
Hide file tree
Showing 12 changed files with 17,630 additions and 106 deletions.
15 changes: 5 additions & 10 deletions json_output_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,7 @@
"type": "object"
},
"_CertificateDeploymentAnalysisResultAsJson": {
"description": "The result of analyzing a server's certificate to verify its validity.\n\nAny certificate available within the fields that follow is parsed as a ``Certificate`` object using the cryptography\nmodule; documentation is available at\nhttps://cryptography.io/en/latest/x509/reference.html?highlight=Certificate#cryptography.x509.Certificate\n\nAttributes:\n received_certificate_chain: The certificate chain sent by the server; index 0 is the leaf certificate.\n verified_certificate_chain: The verified certificate chain returned by OpenSSL for one of the trust stores\n packaged within SSLyze. Will be ``None`` if the validation failed with all of the available trust stores\n (Apple, Mozilla, etc.). This is essentially a shortcut to\n ``path_validation_result_list[0].verified_certificate_chain``.\n path_validation_results: The result of validating the server's\n certificate chain using each trust store that is packaged with SSLyze (Mozilla, Apple, etc.).\n If for a given trust store, the validation was successful, the verified certificate chain built by OpenSSL\n can be retrieved from the ``PathValidationResult``.\n leaf_certificate_subject_matches_hostname: ``True`` if the leaf certificate's Common Name or Subject Alternative\n Names match the server's hostname.\n leaf_certificate_is_ev: ``True`` if the leaf certificate is Extended Validation, according to Mozilla.\n leaf_certificate_has_must_staple_extension: ``True`` if the OCSP must-staple extension is present in the leaf\n certificate.\n leaf_certificate_signed_certificate_timestamps_count: The number of Signed Certificate\n Timestamps (SCTs) for Certificate Transparency embedded in the leaf certificate. ``None`` if the version of\n OpenSSL installed on the system is too old to be able to parse the SCT extension.\n received_chain_has_valid_order: ``True`` if the certificate chain returned by the server was sent in the right\n order. `None`` if any of the certificates in the chain could not be parsed.\n received_chain_contains_anchor_certificate: ``True`` if the server included the anchor/root\n certificate in the chain it sends back to clients. ``None`` if the verified chain could not be built.\n verified_chain_has_sha1_signature: ``True`` if any of the leaf or intermediate certificates are\n signed using the SHA-1 algorithm. ``None`` if the verified chain could not be built.\n verified_chain_has_legacy_symantec_anchor: ``True`` if the certificate chain contains a distrusted Symantec\n anchor\n (https://blog.qualys.com/ssllabs/2017/09/26/google-and-mozilla-deprecating-existing-symantec-certificates).\n ``None`` if the verified chain could not be built.\n ocsp_response: The OCSP response returned by the server. ``None`` if no response was sent by the server or if\n the scan was run through an HTTP proxy (the proxy will not forward the server's OCSP response). If present,\n the OCSP response is an ``OCSPResponse`` object parsed using the cryptography module; documentation is\n available at\n https://cryptography.io/en/latest/x509/ocsp.html?highlight=OCSPResponse#cryptography.x509.ocsp.OCSPResponse\n ocsp_response_is_trusted: ``True`` if the OCSP response is trusted using the Mozilla trust store.\n ``None`` if no OCSP response was sent by the server.",
"description": "The result of analyzing a server's certificate to verify its validity.\n\nAny certificate available within the fields that follow is parsed as a ``Certificate`` object using the cryptography\nmodule; documentation is available at\nhttps://cryptography.io/en/latest/x509/reference.html?highlight=Certificate#cryptography.x509.Certificate\n\nAttributes:\n received_certificate_chain: The certificate chain sent by the server; index 0 is the leaf certificate.\n verified_certificate_chain: The verified certificate chain returned by OpenSSL for one of the trust stores\n packaged within SSLyze. Will be ``None`` if the validation failed with all of the available trust stores\n (Apple, Mozilla, etc.). This is essentially a shortcut to\n ``path_validation_result_list[0].verified_certificate_chain``.\n path_validation_results: The result of validating the server's\n certificate chain using each trust store that is packaged with SSLyze (Mozilla, Apple, etc.).\n If for a given trust store, the validation was successful, the verified certificate chain can be\n retrieved from the ``PathValidationResult``.\n leaf_certificate_is_ev: ``True`` if the leaf certificate is Extended Validation, according to Mozilla.\n leaf_certificate_has_must_staple_extension: ``True`` if the OCSP must-staple extension is present in the leaf\n certificate.\n leaf_certificate_signed_certificate_timestamps_count: The number of Signed Certificate\n Timestamps (SCTs) for Certificate Transparency embedded in the leaf certificate. ``None`` if the version of\n OpenSSL installed on the system is too old to be able to parse the SCT extension.\n received_chain_has_valid_order: ``True`` if the certificate chain returned by the server was sent in the right\n order. `None`` if any of the certificates in the chain could not be parsed.\n received_chain_contains_anchor_certificate: ``True`` if the server included the anchor/root\n certificate in the chain it sends back to clients. ``None`` if the verified chain could not be built.\n verified_chain_has_sha1_signature: ``True`` if any of the leaf or intermediate certificates are\n signed using the SHA-1 algorithm. ``None`` if the verified chain could not be built.\n verified_chain_has_legacy_symantec_anchor: ``True`` if the certificate chain contains a distrusted Symantec\n anchor\n (https://blog.qualys.com/ssllabs/2017/09/26/google-and-mozilla-deprecating-existing-symantec-certificates).\n ``None`` if the verified chain could not be built.\n ocsp_response: The OCSP response returned by the server. ``None`` if no response was sent by the server or if\n the scan was run through an HTTP proxy (the proxy will not forward the server's OCSP response). If present,\n the OCSP response is an ``OCSPResponse`` object parsed using the cryptography module; documentation is\n available at\n https://cryptography.io/en/latest/x509/ocsp.html?highlight=OCSPResponse#cryptography.x509.ocsp.OCSPResponse\n ocsp_response_is_trusted: ``True`` if the OCSP response is trusted using the Mozilla trust store.\n ``None`` if no OCSP response was sent by the server.",
"properties": {
"received_certificate_chain": {
"items": {
Expand All @@ -1209,10 +1209,6 @@
"title": "Received Certificate Chain",
"type": "array"
},
"leaf_certificate_subject_matches_hostname": {
"title": "Leaf Certificate Subject Matches Hostname",
"type": "boolean"
},
"leaf_certificate_has_must_staple_extension": {
"title": "Leaf Certificate Has Must Staple Extension",
"type": "boolean"
Expand Down Expand Up @@ -1321,7 +1317,6 @@
},
"required": [
"received_certificate_chain",
"leaf_certificate_subject_matches_hostname",
"leaf_certificate_has_must_staple_extension",
"leaf_certificate_is_ev",
"leaf_certificate_signed_certificate_timestamps_count",
Expand Down Expand Up @@ -1724,7 +1719,7 @@
"type": "object"
},
"_PathValidationResultAsJson": {
"description": "The result of trying to validate a server's certificate chain using a specific trust store.\n\nAttributes:\n trust_store: The trust store used for validation.\n verified_certificate_chain: The verified certificate chain returned by OpenSSL.\n Index 0 is the leaf certificate and the last element is the anchor/CA certificate from the trust store.\n Will be None if the validation failed or the verified chain could not be built.\n Each certificate is parsed using the cryptography module; documentation is available at\n https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object.\n openssl_error_string: The result string returned by OpenSSL's validation function; None if validation was\n successful.\n was_validation_successful: Whether the certificate chain is trusted when using supplied the trust_stores.",
"description": "The result of trying to validate a server's certificate chain using a specific trust store.\n\nAttributes:\n trust_store: The trust store used for validation.\n verified_certificate_chain: The verified certificate chain returned by OpenSSL.\n Index 0 is the leaf certificate and the last element is the anchor/CA certificate from the trust store.\n Will be None if the validation failed or the verified chain could not be built.\n Each certificate is parsed using the cryptography module; documentation is available at\n https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object.\n validation_error: The error returned by the cryptography module's validation function; None if validation was\n successful.\n was_validation_successful: Whether the certificate chain is trusted when using supplied the trust_stores.",
"properties": {
"trust_store": {
"$ref": "#/$defs/_TrustStoreAsJson"
Expand All @@ -1743,7 +1738,7 @@
],
"title": "Verified Certificate Chain"
},
"openssl_error_string": {
"validation_error": {
"anyOf": [
{
"type": "string"
Expand All @@ -1752,7 +1747,7 @@
"type": "null"
}
],
"title": "Openssl Error String"
"title": "Validation Error"
},
"was_validation_successful": {
"title": "Was Validation Successful",
Expand All @@ -1762,7 +1757,7 @@
"required": [
"trust_store",
"verified_certificate_chain",
"openssl_error_string",
"validation_error",
"was_validation_successful"
],
"title": "_PathValidationResultAsJson",
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,9 @@ def get_include_files() -> List[Tuple[str, str]]:
# Dependencies
install_requires=[
"nassl>=5.1,<6",
"cryptography>=2.6,<42",
"cryptography>42,<43",
"tls-parser>=2,<3",
"pydantic>=2.2,<2.7",
"pyOpenSSL>=23,<24",
],
# cx_freeze info for Windows builds with Python embedded
options={"build_exe": {"packages": ["cffi", "cryptography"], "include_files": get_include_files()}},
Expand Down
4 changes: 0 additions & 4 deletions sslyze/mozilla_tls_profile/mozilla_config_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,6 @@ def _check_certificates(
for cert_deployment in cert_info_result.certificate_deployments:
# Validate certificate trust
leaf_cert = cert_deployment.received_certificate_chain[0]
if not cert_deployment.leaf_certificate_subject_matches_hostname:
issues_with_certificates[
"certificate_hostname_validation"
] = f"Certificate hostname validation failed for {leaf_cert.subject.rfc4514_string()}."
if not cert_deployment.verified_certificate_chain:
issues_with_certificates[
"certificate_path_validation"
Expand Down
44 changes: 6 additions & 38 deletions sslyze/plugins/certificate_info/_cert_chain_analyzer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from dataclasses import dataclass

from ssl import CertificateError, match_hostname
from typing import Optional, List, cast

import cryptography
Expand All @@ -11,10 +10,6 @@
from cryptography.x509.ocsp import load_der_ocsp_response, OCSPResponseStatus, OCSPResponse
import nassl.ocsp_response

from sslyze.plugins.certificate_info._certificate_utils import (
parse_subject_alternative_name_extension,
get_common_names,
)
from sslyze.plugins.certificate_info._symantec import SymantecDistructTester
from sslyze.plugins.certificate_info.trust_stores.trust_store import TrustStore, PathValidationResult

Expand All @@ -35,10 +30,8 @@ class CertificateDeploymentAnalysisResult:
``path_validation_result_list[0].verified_certificate_chain``.
path_validation_results: The result of validating the server's
certificate chain using each trust store that is packaged with SSLyze (Mozilla, Apple, etc.).
If for a given trust store, the validation was successful, the verified certificate chain built by OpenSSL
can be retrieved from the ``PathValidationResult``.
leaf_certificate_subject_matches_hostname: ``True`` if the leaf certificate's Common Name or Subject Alternative
Names match the server's hostname.
If for a given trust store, the validation was successful, the verified certificate chain can be
retrieved from the ``PathValidationResult``.
leaf_certificate_is_ev: ``True`` if the leaf certificate is Extended Validation, according to Mozilla.
leaf_certificate_has_must_staple_extension: ``True`` if the OCSP must-staple extension is present in the leaf
certificate.
Expand Down Expand Up @@ -66,7 +59,6 @@ class CertificateDeploymentAnalysisResult:
"""

received_certificate_chain: List[Certificate]
leaf_certificate_subject_matches_hostname: bool
leaf_certificate_has_must_staple_extension: bool
leaf_certificate_is_ev: bool
leaf_certificate_signed_certificate_timestamps_count: Optional[int]
Expand Down Expand Up @@ -196,14 +188,16 @@ def perform(self) -> CertificateDeploymentAnalysisResult:
# Try to generate the verified certificate chain using each trust store
all_path_validation_results = []
for trust_store in self.trust_stores_for_validation:
path_validation_result = trust_store.verify_certificate_chain(self.server_certificate_chain_as_pem)
path_validation_result = trust_store.verify_certificate_chain(
self.server_certificate_chain_as_pem, self.server_hostname
)
all_path_validation_results.append(path_validation_result)

# Keep one trust store that was able to build the verified chain to then run additional checks
trust_store_that_can_build_verified_chain = None
verified_certificate_chain = None

# But first tort the path validation results so the same trust_store always get picked for a given server
# But first sort the path validation results so the same trust_store always get picked for a given server
def sort_function(path_validation: PathValidationResult) -> str:
return path_validation.trust_store.name.lower()

Expand Down Expand Up @@ -260,7 +254,6 @@ def sort_function(path_validation: PathValidationResult) -> str:
# All done
return CertificateDeploymentAnalysisResult(
received_certificate_chain=received_certificate_chain,
leaf_certificate_subject_matches_hostname=_certificate_matches_hostname(leaf_cert, self.server_hostname),
leaf_certificate_has_must_staple_extension=has_ocsp_must_staple,
leaf_certificate_is_ev=is_leaf_certificate_ev,
leaf_certificate_signed_certificate_timestamps_count=number_of_scts,
Expand All @@ -272,28 +265,3 @@ def sort_function(path_validation: PathValidationResult) -> str:
ocsp_response=final_ocsp_response,
ocsp_response_is_trusted=is_ocsp_response_trusted,
)


def _certificate_matches_hostname(certificate: Certificate, server_hostname: str) -> bool:
"""Verify that the certificate was issued for the given hostname."""
# Extract the names from the certificate to create the properly-formatted dictionary
try:
cert_subject = certificate.subject
except ValueError:
# Cryptography could not parse the certificate https://github.com/nabla-c0d3/sslyze/issues/495
return False

subj_alt_name_ext = parse_subject_alternative_name_extension(certificate)
subj_alt_name_as_list = [("DNS", name) for name in subj_alt_name_ext.dns_names]
subj_alt_name_as_list.extend([("IP Address", ip) for ip in subj_alt_name_ext.ip_addresses])

certificate_names = {
"subject": (tuple([("commonName", name) for name in get_common_names(cert_subject)]),),
"subjectAltName": tuple(subj_alt_name_as_list),
}
# CertificateError is raised on failure
try:
match_hostname(certificate_names, server_hostname) # type: ignore
return True
except CertificateError:
return False
13 changes: 3 additions & 10 deletions sslyze/plugins/certificate_info/_cli_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,6 @@ def _cert_deployment_to_console_output(
deployment_as_txt.append("")
deployment_as_txt.append(cls._format_subtitle(f"Certificate #{index} - Trust"))

hostname_validation_text = (
"OK - Certificate matches server hostname"
if cert_deployment.leaf_certificate_subject_matches_hostname
else "FAILED - Certificate does NOT match server hostname"
)
deployment_as_txt.append(cls._format_field("Hostname Validation:", hostname_validation_text))

# Path validation that was successfully tested
for path_result in cert_deployment.path_validation_results:
if path_result.was_validation_successful:
Expand All @@ -118,7 +111,7 @@ def _cert_deployment_to_console_output(
path_txt = f"OK - Certificate is trusted{ev_txt}"

else:
path_txt = f"FAILED - Certificate is NOT Trusted: {path_result.openssl_error_string}"
path_txt = f"FAILED - Certificate is NOT Trusted: {path_result.validation_error}"

deployment_as_txt.append(
cls._format_field(
Expand Down Expand Up @@ -277,8 +270,8 @@ def _get_basic_certificate_text(cls, certificate: Certificate) -> List[str]:
cls._format_field("Common Name:", _get_subject_as_short_text(certificate)),
cls._format_field("Issuer:", _get_issuer_as_short_text(certificate)),
cls._format_field("Serial Number:", str(certificate.serial_number)),
cls._format_field("Not Before:", certificate.not_valid_before.date().isoformat()),
cls._format_field("Not After:", certificate.not_valid_after.date().isoformat()),
cls._format_field("Not Before:", certificate.not_valid_before_utc.date().isoformat()),
cls._format_field("Not After:", certificate.not_valid_after_utc.date().isoformat()),
cls._format_field("Public Key Algorithm:", certificate.public_key().__class__.__name__),
]

Expand Down
3 changes: 1 addition & 2 deletions sslyze/plugins/certificate_info/json_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ class _TrustStoreAsJson(BaseModelWithOrmMode):
class _PathValidationResultAsJson(BaseModelWithOrmMode):
trust_store: _TrustStoreAsJson
verified_certificate_chain: Optional[List[_CertificateAsJson]]
openssl_error_string: Optional[str]
validation_error: Optional[str]
was_validation_successful: bool


Expand All @@ -239,7 +239,6 @@ class _PathValidationResultAsJson(BaseModelWithOrmMode):

class _CertificateDeploymentAnalysisResultAsJson(BaseModelWithOrmMode):
received_certificate_chain: List[_CertificateAsJson]
leaf_certificate_subject_matches_hostname: bool
leaf_certificate_has_must_staple_extension: bool
leaf_certificate_is_ev: bool
leaf_certificate_signed_certificate_timestamps_count: Optional[int]
Expand Down

0 comments on commit 351ca80

Please sign in to comment.