From 07df10c11fa092af4abfe09dbc7584fc22e614a6 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 11 Jul 2017 15:55:13 +0200 Subject: [feat] add fallback on trust sources for ssl verification With the merge of platformTrust in twisted, the situation for cert chain verification in linux improved a lot. This patch implements fallbacks to do the following: - Try to use whatever trust sources are found in the system. This means that if ca-certificates is installed, pyopenssl will have a valid set of root certificates and verification will likely work (twisted uses platformTrust for this). - If that fails, try to use certifi. We could/should depend on that from now on, *but* it's not packaged before stretch. - So, I'm not deprecating its usage right now, but this one should be the last cacert.pem bundle that we ship with leap.common. - If the cacert.pem from leap.common fails to be found, well, there's nothing you can do. Your TOFU attempt with a cert coming from the CArtel will fail. Most of this MR should be sent as a patch upstream, see https://twistedmatrix.com/trac/ticket/6934 Also related: https://twistedmatrix.com/trac/ticket/9209 I think proper testing will depend on merging https://github.com/pyca/pyopenssl/pull/473 - Resolves: #8958 - Release: 0.6.0 --- src/leap/common/http.py | 144 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 11 deletions(-) (limited to 'src/leap/common/http.py') diff --git a/src/leap/common/http.py b/src/leap/common/http.py index 0dee3a2..f6a7f7e 100644 --- a/src/leap/common/http.py +++ b/src/leap/common/http.py @@ -16,8 +16,13 @@ # along with this program. If not, see . """ Twisted HTTP/HTTPS client. +This module will be deprecated and slowly migrated to use treq instead. """ +import os +import re + + try: import twisted assert twisted @@ -26,21 +31,28 @@ except ImportError: print "Twisted is needed to use leap.common.http module" print "" print "Install the extra requirement of the package:" - print "$ pip install leap.common[Twisted]" + print "$ pip install leap.common[http]" import sys sys.exit(1) +from leap.common import ca_bundle -from leap.common.certs import get_compatible_ssl_context_factory -from leap.common.check import leap_assert -from zope.interface import implements +from OpenSSL.crypto import X509StoreContext +from OpenSSL.crypto import X509StoreContextError +from OpenSSL.SSL import Context +from OpenSSL.SSL import TLSv1_METHOD from twisted.internet import reactor from twisted.internet import defer +from twisted.internet.ssl import Certificate, trustRootFromCertificates +from twisted.internet.ssl import ClientContextFactory +from twisted.logger import Logger from twisted.python import failure +from twisted.python.filepath import FilePath from twisted.web.client import Agent +from twisted.web.client import BrowserLikePolicyForHTTPS from twisted.web.client import HTTPConnectionPool from twisted.web.client import _HTTP11ClientFactory as HTTP11ClientFactory from twisted.web.client import readBody @@ -48,15 +60,82 @@ from twisted.web.http_headers import Headers from twisted.web.iweb import IBodyProducer from twisted.web._newclient import HTTP11ClientProtocol +from zope.interface import implements __all__ = ["HTTPClient"] +log = Logger() + + # A default HTTP timeout is used for 2 distinct purposes: # 1. as HTTP connection timeout, prior to connection estabilshment. # 2. as data reception timeout, after the connection has been established. + DEFAULT_HTTP_TIMEOUT = 30 # seconds +SKIP_SSL_CHECK = os.environ.get('SKIP_TWISTED_SSL_CHECK', False) + + +def certsFromBundle(path, x509=False): + PEM_RE = re.compile( + "-----BEGIN CERTIFICATE-----\r?.+?\r?" + "-----END CERTIFICATE-----\r?\n?""", + re.DOTALL) + if not os.path.isfile(path): + log.warn("Attempted to load non-existent certificate bundle path %s" + % path) + return [] + + pems = FilePath(path).getContent() + cstr = [match.group(0) for match in PEM_RE.finditer(pems)] + certs = [Certificate.loadPEM(cert) for cert in cstr] + if x509: + certs = [cert.original for cert in certs] + return certs + + +def hasUsablePlatformTrust(): + + _knownchain = certsFromBundle(ca_bundle.where('EFFchain.pem'), x509=True) + _knowncert = _knownchain[0] + _knowninterm = _knownchain[1:] + + def _verify_test_cert(store, cert): + store_ctx = X509StoreContext(store, cert) + try: + assert store_ctx.verify_certificate() is None + except (X509StoreContextError, AssertionError): + return False + else: + return True + + def _add_intermediates(store, intermediates): + for _cert in intermediates: + store.add_cert(_cert) + + ctx = Context(TLSv1_METHOD) + ctx.set_default_verify_paths() + store = ctx.get_cert_store() + _add_intermediates(store, _knowninterm) + + return _verify_test_cert(store, _knowncert) + + +def getCertifiTrustRoot(): + try: + import certifi + bundle = certifi.where() + except ImportError: + log.warn("certifi was not found. Using leap.common bundle") + bundle = ca_bundle.where() + if bundle is None: + log.error("Cannot find an usable cacert bundle. " + "Certificate verification will fail") + return None + cacerts = certsFromBundle(bundle) + return trustRootFromCertificates(cacerts) + class _HTTP11ClientFactory(HTTP11ClientFactory): """ @@ -102,6 +181,39 @@ class _HTTPConnectionPool(HTTPConnectionPool): return endpoint.connect(factory) +# TODO deprecate this in favor of treq. +# We need treq to have support for: + +# [ ] timeout +# [ ] retries +# [ ] download/upload pool. + + +def getPolicyForHTTPS(trustRoot=None): + + if SKIP_SSL_CHECK: + log.info("---------------------------------------") + log.info("SKIPPING SSL CERT VERIFICATION!!!") + log.info("I assume you know WHAT YOU ARE DOING...") + log.info("---------------------------------------") + + class WebClientContextFactory(ClientContextFactory): + """ + A web context factory which ignores the hostname and port and does + no certificate verification. + """ + def getContext(self, hostname, port): + return ClientContextFactory.getContext(self) + + contextFactory = WebClientContextFactory() + return contextFactory + + if isinstance(trustRoot, str): + trustRoot = Certificate.loadPEM(FilePath(trustRoot).getContent()) + + return BrowserLikePolicyForHTTPS(trustRoot) + + class HTTPClient(object): """ HTTP client done the twisted way, with a main focus on pinning the SSL @@ -122,13 +234,14 @@ class HTTPClient(object): maxPersistentPerHost=10 ) - def __init__(self, cert_file=None, + def __init__(self, cert_path=None, timeout=DEFAULT_HTTP_TIMEOUT, pool=None): """ Init the HTTP client - :param cert_file: The path to the certificate file, if None given the - system's CAs will be used. + :param cert_file: The path to the ca certificate file to verify + certificates, if None given the system's CAs will be + used. :type cert_file: str :param timeout: The amount of time that this Agent will wait for the peer to accept a connection and for each request to be @@ -139,9 +252,19 @@ class HTTPClient(object): self._timeout = timeout self._pool = pool if pool is not None else self._pool + + if cert_path is None: + if hasUsablePlatformTrust(): + # Twisted Knows What To Do + trustRoot = None + else: + trustRoot = getCertifiTrustRoot() + else: + trustRoot = cert_path + self._agent = Agent( reactor, - get_compatible_ssl_context_factory(cert_file), + contextFactory=getPolicyForHTTPS(trustRoot), pool=self._pool, connectTimeout=self._timeout) self._semaphore = defer.DeferredSemaphore( @@ -205,9 +328,7 @@ class HTTPClient(object): :return: A deferred that fires with the body of the request. :rtype: twisted.internet.defer.Deferred """ - leap_assert( - callable(callback), - message="The callback parameter should be a callable!") + assert callable(callback), "The callback parameter should be a callable!" return self._semaphore.run(self._request, url, method, body, headers, callback) @@ -217,6 +338,7 @@ class HTTPClient(object): """ self._pool.closeCachedConnections() + # # An IBodyProducer to write the body of an HTTP request as a string. # -- cgit v1.2.3