diff options
author | Folker Bernitt <fbernitt@thoughtworks.com> | 2015-03-31 13:50:43 +0200 |
---|---|---|
committer | Folker Bernitt <fbernitt@thoughtworks.com> | 2015-03-31 13:53:13 +0200 |
commit | a1fc37326a79b95cdb056a100b321586f1c1fb7b (patch) | |
tree | 91584a2bcbaae7f883d338a953ac94de77a7f035 /service/pixelated | |
parent | faad044b8b576b6d84d88608fa5a57171e3d6169 (diff) |
Added support for ssl fingerprint validation.
- Issue #333
- Needed to patch urrlib3 for older requests versions
- Use --leap-cert-fingerprint <SHA1> to validate fingerprint
Diffstat (limited to 'service/pixelated')
-rw-r--r-- | service/pixelated/bitmask_libraries/certs.py | 9 | ||||
-rw-r--r-- | service/pixelated/bitmask_libraries/provider.py | 31 | ||||
-rw-r--r-- | service/pixelated/config/__init__.py | 1 | ||||
-rw-r--r-- | service/pixelated/config/args.py | 1 | ||||
-rw-r--r-- | service/pixelated/config/leap_cert.py | 7 | ||||
-rw-r--r-- | service/pixelated/support/ext_requests_urllib3.py | 79 | ||||
-rw-r--r-- | service/pixelated/support/tls_adapter.py | 47 |
7 files changed, 157 insertions, 18 deletions
diff --git a/service/pixelated/bitmask_libraries/certs.py b/service/pixelated/bitmask_libraries/certs.py index 6b12bce4..ed09e4a3 100644 --- a/service/pixelated/bitmask_libraries/certs.py +++ b/service/pixelated/bitmask_libraries/certs.py @@ -22,16 +22,19 @@ from leap.common import ca_bundle from .config import AUTO_DETECT_CA_BUNDLE LEAP_CERT = None +LEAP_FINGERPRINT = None def which_bundle(provider): - if LEAP_CERT: - return LEAP_CERT return str(LeapCertificate(provider).provider_ca_bundle()) +def which_bootstrap_fingerprint(provider): + return LEAP_FINGERPRINT + + def which_bootstrap_bundle(provider): - if LEAP_CERT: + if LEAP_CERT is not None: return LEAP_CERT return str(LeapCertificate(provider).auto_detect_bootstrap_ca_bundle()) diff --git a/service/pixelated/bitmask_libraries/provider.py b/service/pixelated/bitmask_libraries/provider.py index 5304e662..34e426d7 100644 --- a/service/pixelated/bitmask_libraries/provider.py +++ b/service/pixelated/bitmask_libraries/provider.py @@ -17,7 +17,8 @@ import json from leap.common.certs import get_digest import requests -from .certs import which_bootstrap_bundle, which_bundle +from .certs import which_bootstrap_bundle, which_bundle, which_bootstrap_fingerprint +from pixelated.support.tls_adapter import EnforceTLSv1Adapter class LeapProvider(object): @@ -75,16 +76,10 @@ class LeapProvider(object): return cert def _fetch_certificate(self): - session = requests.session() - try: - cert_url = '%s/ca.crt' % self._provider_base_url() - response = session.get(cert_url, verify=which_bootstrap_bundle(self), timeout=self.config.timeout_in_s) - response.raise_for_status() - - cert_data = response.content - return cert_data - finally: - session.close() + cert_url = '%s/ca.crt' % self._provider_base_url() + response = self._validated_get(cert_url) + cert_data = response.content + return cert_data def validate_certificate(self, cert_data=None): if cert_data is None: @@ -99,11 +94,19 @@ class LeapProvider(object): if fingerprint.strip() != digest: raise Exception('Certificate fingerprints don\'t match') + def _validated_get(self, url): + session = requests.session() + try: + session.mount('https://', EnforceTLSv1Adapter(assert_fingerprint=which_bootstrap_fingerprint(self))) + response = session.get(url, verify=which_bootstrap_bundle(self), timeout=self.config.timeout_in_s) + response.raise_for_status() + return response + finally: + session.close() + def fetch_provider_json(self): url = '%s/provider.json' % self._provider_base_url() - response = requests.get(url, verify=which_bootstrap_bundle(self), timeout=self.config.timeout_in_s) - response.raise_for_status() - + response = self._validated_get(url) json_data = json.loads(response.content) return json_data diff --git a/service/pixelated/config/__init__.py b/service/pixelated/config/__init__.py index 2045354e..af264c77 100644 --- a/service/pixelated/config/__init__.py +++ b/service/pixelated/config/__init__.py @@ -37,6 +37,7 @@ import pixelated.support.ext_sqlcipher import pixelated.support.ext_esmtp_sender_factory import pixelated.support.ext_fetch import pixelated.support.ext_keymanager_fetch_key +import pixelated.support.ext_requests_urllib3 def initialize(): diff --git a/service/pixelated/config/args.py b/service/pixelated/config/args.py index 48f7b6df..d3284fab 100644 --- a/service/pixelated/config/args.py +++ b/service/pixelated/config/args.py @@ -30,6 +30,7 @@ def parse(): parser.add_argument('-sk', '--sslkey', metavar='<server.key>', default=None, help='use specified file as web server\'s SSL key (when using the user-agent together with the pixelated-dispatcher)') parser.add_argument('-sc', '--sslcert', metavar='<server.crt>', default=None, help='use specified file as web server\'s SSL certificate (when using the user-agent together with the pixelated-dispatcher)') parser.add_argument('-lc', '--leap-cert', metavar='<leap.crt>', default=None, help='use specified file for LEAP cert authority certificate (url https://<provider-domain>/ca.crt)') + parser.add_argument('--leap-cert-fingerprint', metavar='<leap certificate fingerprint>', default=None, help='use specified fingerprint to validate connection with leap provider', dest='leap_cert_fingerprint') parser.add_argument('--register', metavar=('provider', 'username'), nargs=2, help='register a new username on the desired provider') args = parser.parse_args() diff --git a/service/pixelated/config/leap_cert.py b/service/pixelated/config/leap_cert.py index 9e6dfc01..3172c953 100644 --- a/service/pixelated/config/leap_cert.py +++ b/service/pixelated/config/leap_cert.py @@ -18,4 +18,9 @@ import pixelated.bitmask_libraries.certs as certs def init_leap_cert(args): - certs.LEAP_CERT = args.leap_cert + if args.leap_cert_fingerprint is None: + certs.LEAP_CERT = args.leap_cert + certs.LEAP_FINGERPRINT = None + else: + certs.LEAP_FINGERPRINT = args.leap_cert_fingerprint + certs.LEAP_CERT = False diff --git a/service/pixelated/support/ext_requests_urllib3.py b/service/pixelated/support/ext_requests_urllib3.py new file mode 100644 index 00000000..a836d6fd --- /dev/null +++ b/service/pixelated/support/ext_requests_urllib3.py @@ -0,0 +1,79 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + +import requests +import requests.packages.urllib3.connectionpool +from socket import error as SocketError, timeout as SocketTimeout +from requests.packages.urllib3.packages.ssl_match_hostname import CertificateError, match_hostname +import socket +import ssl + +from requests.packages.urllib3.exceptions import ( + ClosedPoolError, + ConnectTimeoutError, + EmptyPoolError, + HostChangedError, + MaxRetryError, + SSLError, + ReadTimeoutError, + ProxyError, +) + +from requests.packages.urllib3.util import ( + assert_fingerprint, + get_host, + is_connection_dropped, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, + Timeout, +) + + +def patched_connect(self): + # Add certificate verification + try: + sock = socket.create_connection(address=(self.host, self.port), timeout=self.timeout) + except SocketTimeout: + raise ConnectTimeoutError(self, "Connection to %s timed out. (connect timeout=%s)" % (self.host, self.timeout)) + + resolved_cert_reqs = resolve_cert_reqs(self.cert_reqs) + resolved_ssl_version = resolve_ssl_version(self.ssl_version) + + if self._tunnel_host: + self.sock = sock + # Calls self._set_hostport(), so self.host is + # self._tunnel_host below. + self._tunnel() + + # Wrap socket using verification with the root certs in + # trusted_root_certs + self.sock = ssl_wrap_socket(sock, self.key_file, self.cert_file, + cert_reqs=resolved_cert_reqs, + ca_certs=self.ca_certs, + server_hostname=self.host, + ssl_version=resolved_ssl_version) + + if self.assert_fingerprint: + assert_fingerprint(self.sock.getpeercert(binary_form=True), + self.assert_fingerprint) + elif resolved_cert_reqs != ssl.CERT_NONE and self.assert_hostname is not False: + match_hostname(self.sock.getpeercert(), + self.assert_hostname or self.host) + + +if requests.__version__ == '2.0.0': + requests.packages.urllib3.connectionpool.VerifiedHTTPSConnection.connect = patched_connect diff --git a/service/pixelated/support/tls_adapter.py b/service/pixelated/support/tls_adapter.py new file mode 100644 index 00000000..f543bf4d --- /dev/null +++ b/service/pixelated/support/tls_adapter.py @@ -0,0 +1,47 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + +import ssl +from requests.adapters import HTTPAdapter +try: + from urllib3.poolmanager import PoolManager +except: + from requests.packages.urllib3.poolmanager import PoolManager + +VERIFY_HOSTNAME = None + + +def latest_available_ssl_version(): + try: + return ssl.PROTOCOL_TLSv1_2 + except AttributeError: + return ssl.PROTOCOL_TLSv1 + + +class EnforceTLSv1Adapter(HTTPAdapter): + __slots__ = ('_assert_hostname', '_assert_fingerprint') + + def __init__(self, assert_hostname=VERIFY_HOSTNAME, assert_fingerprint=None): + self._assert_hostname = assert_hostname + self._assert_fingerprint = assert_fingerprint + super(EnforceTLSv1Adapter, self).__init__() + + def init_poolmanager(self, connections, maxsize, block=False): + self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize, + block=block, ssl_version=latest_available_ssl_version(), + assert_hostname=self._assert_hostname, + assert_fingerprint=self._assert_fingerprint, + cert_reqs=ssl.CERT_REQUIRED) |