Skip to content

Commit

Permalink
[#566] Switch to pyOpenSSL for validating certificate chain
Browse files Browse the repository at this point in the history
  • Loading branch information
nabla-c0d3 committed Nov 6, 2022
1 parent 8ad73ec commit 95dcccf
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 108 deletions.
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()}},
Expand Down
83 changes: 3 additions & 80 deletions sslyze/plugins/certificate_info/_cert_chain_analyzer.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,19 @@
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
from cryptography.hazmat.primitives import hashes
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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
61 changes: 57 additions & 4 deletions sslyze/plugins/certificate_info/trust_stores/trust_store.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from pathlib import Path

from OpenSSL import crypto
from cryptography.x509.base import Certificate
from cryptography.x509.extensions import ExtensionNotFound, CertificatePolicies
from cryptography.x509.oid import ObjectIdentifier
Expand All @@ -10,6 +11,30 @@


@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.
Expand All @@ -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?"""
Expand All @@ -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
)
21 changes: 0 additions & 21 deletions tests/plugins_tests/certificate_info/test_cert_chain_analyzer.py

This file was deleted.

145 changes: 145 additions & 0 deletions tests/plugins_tests/certificate_info/test_trust_store.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 95dcccf

Please sign in to comment.