diff --git a/dev-requirements.txt b/dev-requirements.txt index d7784b82..3c9f25be 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,5 +10,8 @@ black==22.3.0 pytest-cov faker +# For mypy +types-pyOpenSSL + # For building the Windows executable cx-freeze; sys.platform == 'win32' diff --git a/setup.py b/setup.py index 22c0b755..24146f43 100644 --- a/setup.py +++ b/setup.py @@ -97,10 +97,11 @@ def get_include_files() -> List[Tuple[str, str]]: entry_points={"console_scripts": ["sslyze = sslyze.__main__:main"]}, # Dependencies install_requires=[ - "nassl>=4.0.1,<5.0.0", - "cryptography>=2.6,<39.0.0", - "tls-parser>=2.0.0,<3.0.0", + "nassl>=4.0.1,<5", + "cryptography>=2.6,<39", + "tls-parser>=2,<3", "pydantic>=1.7,<1.11", + "pyOpenSSL>=20,<23", ], # cx_freeze info for Windows builds with Python embedded options={"build_exe": {"packages": ["cffi", "cryptography"], "include_files": get_include_files()}}, diff --git a/sslyze/mozilla_tls_profile/mozilla_config_checker.py b/sslyze/mozilla_tls_profile/mozilla_config_checker.py index c171a423..f0190147 100644 --- a/sslyze/mozilla_tls_profile/mozilla_config_checker.py +++ b/sslyze/mozilla_tls_profile/mozilla_config_checker.py @@ -324,7 +324,7 @@ def _check_certificates( else: deployed_key_algorithms.add(public_key.__class__.__name__) - deployed_signature_algorithms.add(leaf_cert.signature_algorithm_oid._name) + deployed_signature_algorithms.add(leaf_cert.signature_algorithm_oid._name) # type: ignore # Validate the cert's lifespan leaf_cert_lifespan = leaf_cert.not_valid_after - leaf_cert.not_valid_before diff --git a/sslyze/plugins/certificate_info/_cert_chain_analyzer.py b/sslyze/plugins/certificate_info/_cert_chain_analyzer.py index 03b38c44..3a79f96f 100644 --- a/sslyze/plugins/certificate_info/_cert_chain_analyzer.py +++ b/sslyze/plugins/certificate_info/_cert_chain_analyzer.py @@ -1,8 +1,7 @@ from dataclasses import dataclass -from pathlib import Path from ssl import CertificateError, match_hostname -from typing import Optional, List, cast, Dict +from typing import Optional, List, cast import cryptography from cryptography.hazmat.backends import default_backend @@ -10,38 +9,11 @@ from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509 import ExtensionNotFound, ExtensionOID, Certificate, load_pem_x509_certificate, TLSFeature from cryptography.x509.ocsp import load_der_ocsp_response, OCSPResponseStatus, OCSPResponse -from nassl._nassl import X509 -from nassl.cert_chain_verifier import CertificateChainVerifier, CertificateChainVerificationFailed import nassl.ocsp_response from sslyze.plugins.certificate_info._certificate_utils import extract_dns_subject_alternative_names, get_common_names from sslyze.plugins.certificate_info._symantec import SymantecDistructTester -from sslyze.plugins.certificate_info.trust_stores.trust_store import TrustStore - - -@dataclass(frozen=True) -class PathValidationResult: - """The result of trying to validate a server's certificate chain using a specific trust store. - - Attributes: - trust_stores: The trust store used for validation. - verified_certificate_chain: The verified certificate chain returned by OpenSSL. - Index 0 is the leaf certificate and the last element is the anchor/CA certificate from the trust store. - Will be None if the validation failed or the verified chain could not be built. - Each certificate is parsed using the cryptography module; documentation is available at - https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object. - openssl_error_string: The result string returned by OpenSSL's validation function; None if validation was - successful. - was_validation_successful: Whether the certificate chain is trusted when using supplied the trust_stores. - """ - - trust_store: TrustStore - verified_certificate_chain: Optional[List[Certificate]] - openssl_error_string: Optional[str] - - @property - def was_validation_successful(self) -> bool: - return True if self.verified_certificate_chain else False +from sslyze.plugins.certificate_info.trust_stores.trust_store import TrustStore, PathValidationResult @dataclass(frozen=True) @@ -221,7 +193,7 @@ 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 = _verify_certificate_chain(self.server_certificate_chain_as_pem, trust_store) + path_validation_result = trust_store.verify_certificate_chain(self.server_certificate_chain_as_pem) 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 @@ -318,52 +290,3 @@ def _certificate_matches_hostname(certificate: Certificate, server_hostname: str return True except CertificateError: return False - - -# TODO(AD): There is probably a memory leak in nassl.X509 or nassl.X509_STORE_CTX -# https://github.com/nabla-c0d3/sslyze/issues/560 -# It might be due to bad reference counting in nassl_X509_STORE_CTX_set0_trusted_stack() -# More specifically the call to X509_chain_up_ref() - is there corresponding call to decrease ref count? -# As a workaround, we cache the (huge) list of trusted certificates, for each trust store -_cache_for_trusted_certificates_per_file: Dict[Path, List[X509]] = {} - - -def _convert_and_cache_pem_certs_to_x509s(trusted_certificates_path: Path) -> List[X509]: - certs_as_509s = _cache_for_trusted_certificates_per_file.get(trusted_certificates_path) - if certs_as_509s: - return certs_as_509s - - # Parse the PEM certificate in the file - all_certs_as_pem: List[str] = [] - with trusted_certificates_path.open() as file_content: - for pem_segment in file_content.read().split("-----BEGIN CERTIFICATE-----")[1::]: - pem_content = pem_segment.split("-----END CERTIFICATE-----")[0] - pem_cert = f"-----BEGIN CERTIFICATE-----{pem_content}-----END CERTIFICATE-----" - all_certs_as_pem.append(pem_cert) - - # Convert them to X509 objects and save that in the cache - all_certs_as_509s = [X509(cert_pem) for cert_pem in all_certs_as_pem] - _cache_for_trusted_certificates_per_file[trusted_certificates_path] = all_certs_as_509s - return all_certs_as_509s - - -def _verify_certificate_chain(server_certificate_chain: List[str], trust_store: TrustStore) -> PathValidationResult: - server_chain_as_x509s = [X509(pem_cert) for pem_cert in server_certificate_chain] - trust_store_as_x509s = _convert_and_cache_pem_certs_to_x509s(trust_store.path) - chain_verifier = CertificateChainVerifier(trust_store_as_x509s) - - verified_chain: Optional[List[Certificate]] - try: - openssl_verify_str = None - verified_chain_as_509s = chain_verifier.verify(server_chain_as_x509s) - verified_chain = [ - load_pem_x509_certificate(x509_cert.as_pem().encode("ascii"), backend=default_backend()) - for x509_cert in verified_chain_as_509s - ] - except CertificateChainVerificationFailed as e: - verified_chain = None - openssl_verify_str = e.openssl_error_string - - return PathValidationResult( - trust_store=trust_store, verified_certificate_chain=verified_chain, openssl_error_string=openssl_verify_str - ) diff --git a/sslyze/plugins/certificate_info/_certificate_utils.py b/sslyze/plugins/certificate_info/_certificate_utils.py index a2f14f75..84a906a9 100644 --- a/sslyze/plugins/certificate_info/_certificate_utils.py +++ b/sslyze/plugins/certificate_info/_certificate_utils.py @@ -3,8 +3,7 @@ from cryptography import x509 from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat -from cryptography.x509 import ExtensionOID, DNSName, ExtensionNotFound, NameOID -from cryptography.x509.extensions import DuplicateExtension # type: ignore +from cryptography.x509 import ExtensionOID, DNSName, ExtensionNotFound, NameOID, DuplicateExtension def extract_dns_subject_alternative_names(certificate: x509.Certificate) -> List[str]: diff --git a/sslyze/plugins/certificate_info/json_output.py b/sslyze/plugins/certificate_info/json_output.py index 26dcdfe3..4bad8a7e 100644 --- a/sslyze/plugins/certificate_info/json_output.py +++ b/sslyze/plugins/certificate_info/json_output.py @@ -4,13 +4,10 @@ from typing import Any, List, Optional import pydantic -from cryptography import x509 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from cryptography.hazmat.primitives.serialization import Encoding -from cryptography.x509 import NameAttribute -from cryptography.x509.ocsp import OCSPResponseStatus -from cryptography.x509.oid import ObjectIdentifier # type: ignore +from cryptography.x509 import NameAttribute, ObjectIdentifier, Name, Certificate, ocsp from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey from sslyze import ( @@ -78,7 +75,10 @@ class _ObjectIdentifierAsJson(_BaseModelWithOrmMode): @classmethod def from_orm(cls, oid: ObjectIdentifier) -> "_ObjectIdentifierAsJson": - return cls(name=oid._name, dotted_string=oid.dotted_string) + return cls( + name=oid._name, # type: ignore + dotted_string=oid.dotted_string + ) class _NameAttributeAsJson(_BaseModelWithOrmMode): @@ -100,7 +100,7 @@ class _X509NameAsJson(_BaseModelWithOrmMode): attributes: List[_NameAttributeAsJson] @classmethod - def from_orm(cls, name: x509.name.Name) -> "_X509NameAsJson": + def from_orm(cls, name: Name) -> "_X509NameAsJson": return cls( rfc4514_string=name.rfc4514_string(), attributes=[_NameAttributeAsJson.from_orm(attr) for attr in name] ) @@ -143,7 +143,7 @@ class _CertificateAsJson(_BaseModelWithOrmMode): public_key: _PublicKeyAsJson @classmethod - def from_orm(cls, certificate: x509.Certificate) -> "_CertificateAsJson": + def from_orm(cls, certificate: Certificate) -> "_CertificateAsJson": signature_hash_algorithm: Optional[_HashAlgorithmAsJson] if certificate.signature_hash_algorithm: signature_hash_algorithm = _HashAlgorithmAsJson.from_orm(certificate.signature_hash_algorithm) @@ -194,9 +194,9 @@ class _OcspResponseAsJson(_BaseModelWithOrmMode): serial_number: Optional[int] @classmethod - def from_orm(cls, ocsp_response: x509.ocsp.OCSPResponse) -> "_OcspResponseAsJson": + def from_orm(cls, ocsp_response: ocsp.OCSPResponse) -> "_OcspResponseAsJson": response_status = ocsp_response.response_status.name - if ocsp_response.response_status != OCSPResponseStatus.SUCCESSFUL: + if ocsp_response.response_status != ocsp.OCSPResponseStatus.SUCCESSFUL: return cls( response_status=response_status, certificate_status=None, diff --git a/sslyze/plugins/certificate_info/trust_stores/trust_store.py b/sslyze/plugins/certificate_info/trust_stores/trust_store.py index fde4faf4..2bf65e6d 100644 --- a/sslyze/plugins/certificate_info/trust_stores/trust_store.py +++ b/sslyze/plugins/certificate_info/trust_stores/trust_store.py @@ -1,15 +1,40 @@ from dataclasses import dataclass from pathlib import Path -from cryptography.x509.base import Certificate -from cryptography.x509.extensions import ExtensionNotFound, CertificatePolicies -from cryptography.x509.oid import ObjectIdentifier -from cryptography.x509.oid import ExtensionOID +from OpenSSL import crypto +from cryptography.x509 import Certificate +from cryptography.x509 import ExtensionNotFound, CertificatePolicies +from cryptography.x509 import ObjectIdentifier +from cryptography.x509 import ExtensionOID from typing import List, cast from typing import Optional @dataclass(frozen=True) +class PathValidationResult: + """The result of trying to validate a server's certificate chain using a specific trust store. + + Attributes: + trust_store: The trust store used for validation. + verified_certificate_chain: The verified certificate chain returned by OpenSSL. + Index 0 is the leaf certificate and the last element is the anchor/CA certificate from the trust store. + Will be None if the validation failed or the verified chain could not be built. + Each certificate is parsed using the cryptography module; documentation is available at + https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object. + openssl_error_string: The result string returned by OpenSSL's validation function; None if validation was + successful. + was_validation_successful: Whether the certificate chain is trusted when using supplied the trust_stores. + """ + + trust_store: "TrustStore" + verified_certificate_chain: Optional[List[Certificate]] + openssl_error_string: Optional[str] + + @property + def was_validation_successful(self) -> bool: + return True if self.verified_certificate_chain else False + + class TrustStore: """A set of root certificates to be used for certificate validation. @@ -19,10 +44,14 @@ class TrustStore: version: The human-readable version or date of the trust store (such as "09/2016"). """ - path: Path - name: str - version: str - ev_oids: Optional[List[ObjectIdentifier]] = None + def __init__(self, path: Path, name: str, version: str, ev_oids: Optional[List[ObjectIdentifier]] = None) -> None: + self.path = path + self.name = name + self.version = version + self.ev_oids = ev_oids + + self._x509_store = crypto.X509Store() + self._x509_store.load_locations(cafile=self.path) def is_certificate_extended_validation(self, certificate: Certificate) -> bool: """Is the supplied server certificate EV?""" @@ -39,3 +68,27 @@ def is_certificate_extended_validation(self, certificate: Certificate) -> bool: if policy.policy_identifier in self.ev_oids: return True return False + + def verify_certificate_chain(self, certificate_chain_as_pem: List[str]) -> PathValidationResult: + certificate = crypto.load_certificate( + buffer=certificate_chain_as_pem[0].encode("ascii"), type=crypto.FILETYPE_PEM + ) + chain = [ + crypto.load_certificate(buffer=cert.encode("ascii"), type=crypto.FILETYPE_PEM) + for cert in certificate_chain_as_pem[1::] + ] + x509_store_ctx = crypto.X509StoreContext(store=self._x509_store, certificate=certificate, chain=chain) + + verified_chain: Optional[List[Certificate]] + error_message: Optional[str] + try: + verified_chain_as_x509s = x509_store_ctx.get_verified_chain() + verified_chain = [x509.to_cryptography() for x509 in verified_chain_as_x509s] + error_message = None + except crypto.X509StoreContextError as exc: + verified_chain = None + error_message = exc.args[0] + + return PathValidationResult( + trust_store=self, verified_certificate_chain=verified_chain, openssl_error_string=error_message + ) diff --git a/sslyze/plugins/certificate_info/trust_stores/trust_store_repository.py b/sslyze/plugins/certificate_info/trust_stores/trust_store_repository.py index cca4debe..9e36c094 100644 --- a/sslyze/plugins/certificate_info/trust_stores/trust_store_repository.py +++ b/sslyze/plugins/certificate_info/trust_stores/trust_store_repository.py @@ -10,7 +10,7 @@ import sys from os.path import realpath -from cryptography.hazmat._oid import ObjectIdentifier +from cryptography.x509 import ObjectIdentifier from sslyze.plugins.certificate_info.trust_stores.trust_store import TrustStore from typing import List diff --git a/tests/plugins_tests/certificate_info/test_cert_chain_analyzer.py b/tests/plugins_tests/certificate_info/test_cert_chain_analyzer.py deleted file mode 100644 index 6064336b..00000000 --- a/tests/plugins_tests/certificate_info/test_cert_chain_analyzer.py +++ /dev/null @@ -1,21 +0,0 @@ -from sslyze.plugins.certificate_info.trust_stores.trust_store_repository import TrustStoresRepository -from sslyze.plugins.certificate_info._cert_chain_analyzer import ( - _cache_for_trusted_certificates_per_file, - _convert_and_cache_pem_certs_to_x509s, -) - - -class TestMemoryLeakWorkaroundWithX509Cache: - def test(self): - # Given a path to a file with a list of PEM certificates - trusted_certificates_path = TrustStoresRepository.get_default().get_main_store().path - - # And the file's content has not been cached yet - assert trusted_certificates_path not in _cache_for_trusted_certificates_per_file - - # When converting the content of the file to X509 objects - certs_as_x509s = _convert_and_cache_pem_certs_to_x509s(trusted_certificates_path) - - # It succeeds, and the x509 objects were cached - assert certs_as_x509s - assert trusted_certificates_path in _cache_for_trusted_certificates_per_file diff --git a/tests/plugins_tests/certificate_info/test_trust_store.py b/tests/plugins_tests/certificate_info/test_trust_store.py new file mode 100644 index 00000000..7a33e47d --- /dev/null +++ b/tests/plugins_tests/certificate_info/test_trust_store.py @@ -0,0 +1,145 @@ +from datetime import datetime + +from sslyze import TrustStore +from sslyze.plugins.certificate_info.trust_stores.trust_store_repository import TrustStoresRepository + + +GOOGLE_DOT_COM_CERT_CHAIN_ON_11_2022 = [ + # www.google.com + """-----BEGIN CERTIFICATE----- +MIIEhjCCA26gAwIBAgIRAIGnSAxq9cl3Enfafpo9aMEwDQYJKoZIhvcNAQELBQAw +RjELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBM +TEMxEzARBgNVBAMTCkdUUyBDQSAxQzMwHhcNMjIxMDE3MDgxODU3WhcNMjMwMTA5 +MDgxODU2WjAZMRcwFQYDVQQDEw53d3cuZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABBnrjpwWorUsTwEB7fA8wodHqOqRKdyBQ406AkkPiRjp7bWM +TUROxua8tVUi0QctADBPrb103J+e2Ee3o/dZMy6jggJlMIICYTAOBgNVHQ8BAf8E +BAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4E +FgQUJbBPZaQP5Z2lTXWnLIvPj2AM3SYwHwYDVR0jBBgwFoAUinR/r4XN7pXNPZzQ +4kYU83E1HScwagYIKwYBBQUHAQEEXjBcMCcGCCsGAQUFBzABhhtodHRwOi8vb2Nz +cC5wa2kuZ29vZy9ndHMxYzMwMQYIKwYBBQUHMAKGJWh0dHA6Ly9wa2kuZ29vZy9y +ZXBvL2NlcnRzL2d0czFjMy5kZXIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20w +IQYDVR0gBBowGDAIBgZngQwBAgEwDAYKKwYBBAHWeQIFAzA8BgNVHR8ENTAzMDGg +L6AthitodHRwOi8vY3Jscy5wa2kuZ29vZy9ndHMxYzMvbW9WRGZJU2lhMmsuY3Js +MIIBAgYKKwYBBAHWeQIEAgSB8wSB8ADuAHUArfe++nz/EMiLnT2cHj4YarRnKV3P +sQwkyoWGNOvcgooAAAGD5T05NAAABAMARjBEAiBNo4cn2Eyc/ge6R+qbdDg/kD8P +f0XupuzxZlLy2OvIvAIgI0wX7tEkHeynWZV5RRNwrW7vOjZBpKuyelE2BTr2gxcA +dQB6MoxU2LcttiDqOOBSHumEFnAyE4VNO9IrwTpXo1LrUgAAAYPlPTk3AAAEAwBG +MEQCIB774m1pakzCQKhDSmAUP51XrIrfrSHE/BrgBU34Jn/CAiBIsCkm/nILUGKj +YkyDbencNU6gkTLxMmofOMQWam6A6DANBgkqhkiG9w0BAQsFAAOCAQEA8LO4cgmV +iPSxMl2g94yxNGmdPHiSfgz9tSxKv6/njAuNJmDbMR0PDXBrYRIrWGGiR99e+zbD +beKwUoJbWfFJf4weWLEXLdKOcAGJT6nWT46Y2KRGfZ520W+AlqU3+QVshVTmSoub +/k93A4QKLODRns2567ulr7tRgerFwf4GpODsPMz0Nsdh/EIWgQaeK1dLjE5D64WB +tC1b0D2/JxFiD1BjXMCkYToe2/ltqCY1SHSCjGIBTFO1dLiG353k1jDRwxfXZOA4 +I7Ei1SA5Jz3My5rX3vReT8mf4JHiFw35+YFPN/ppfQbhKRq4q1mOpnQUPn3fuX5i +NRIMSi+Bf8U56Q== +-----END CERTIFICATE-----""", + # GTS CA 1C3 + """-----BEGIN CERTIFICATE----- +MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAw +MDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzp +kgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsX +lOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcm +BA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKA +gOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwL +tmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0T +AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYD +VR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYG +CCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcw +AoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQt +MCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcG +A1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3Br +aS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcN +AQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQ +cSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrL +RklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U ++o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2Yr +PxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IER +lQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGs +Yye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjO +z23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJG +AJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKw +juDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl +1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd +-----END CERTIFICATE-----""", + # GTS Root R1 + """-----BEGIN CERTIFICATE----- +MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX +MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE +CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx +OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT +GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63 +ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS +iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k +KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ +DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk +j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5 +cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW +CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499 +iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei +Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap +sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b +9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf +BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw +JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH +MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al +oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy +MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF +AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9 +NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9 +WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw +9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy ++qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi +d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8= +-----END CERTIFICATE-----""" +] + + +def _create_trust_store() -> TrustStore: + return TrustStore( + path=TrustStoresRepository._DEFAULT_TRUST_STORES_PATH / "mozilla_nss.pem", + name="Mozilla", + version="123", + ev_oids=[] + ) + + +class TestTrustStore: + + def test_verify_certificate_chain(self): + # Given a trust store and a certificate chain to verify + trust_store = _create_trust_store() + certificate_chain_as_pem = GOOGLE_DOT_COM_CERT_CHAIN_ON_11_2022 + + # And at the time of the verification, the certificate chain is expected to be valid + trust_store._x509_store.set_time(datetime(year=2022, month=11, day=6)) + + # When running the verification, it succeeds + result = trust_store.verify_certificate_chain(certificate_chain_as_pem) + + # And the certificate chain was reported as being valid + assert result.was_validation_successful + assert result.verified_certificate_chain + assert result.openssl_error_string is None + + def test_verify_certificate_chain_but_verification_fails(self): + # Given a trust store and a certificate chain to verify + trust_store = _create_trust_store() + certificate_chain_as_pem = GOOGLE_DOT_COM_CERT_CHAIN_ON_11_2022 + + # And at the time of the verification, the certificate chain is expected to be INVALID + trust_store._x509_store.set_time(datetime(year=2030, month=1, day=1)) + + # When running the verification, it succeeds + result = trust_store.verify_certificate_chain(certificate_chain_as_pem) + + # And the certificate chain was reported as being INVALID + assert not result.was_validation_successful + assert not result.verified_certificate_chain + assert result.openssl_error_string