diff options
| -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 | ||||
| -rw-r--r-- | service/test/unit/bitmask_libraries/test_provider.py | 20 | 
8 files changed, 174 insertions, 21 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) diff --git a/service/test/unit/bitmask_libraries/test_provider.py b/service/test/unit/bitmask_libraries/test_provider.py index 8c0cf97e..a1e69543 100644 --- a/service/test/unit/bitmask_libraries/test_provider.py +++ b/service/test/unit/bitmask_libraries/test_provider.py @@ -15,7 +15,7 @@  # along with Pixelated. If not, see <http://www.gnu.org/licenses/>.  import json -from mock import patch, MagicMock +from mock import patch, MagicMock, ANY  from httmock import all_requests, HTTMock, urlmatch  from requests import HTTPError  from pixelated.bitmask_libraries.config import LeapConfig @@ -202,8 +202,8 @@ class LeapProviderTest(AbstractLeapTest):                      provider = LeapProvider('some-provider.test', self.config)                      provider.fetch_valid_certificate() -        get_func.assert_called_once_with('https://some-provider.test/provider.json', verify=BOOTSTRAP_CA_CERT, timeout=15) -        session.get.assert_called_once_with('https://some-provider.test/ca.crt', verify=BOOTSTRAP_CA_CERT, timeout=15) +        session.get.assert_any_call('https://some-provider.test/ca.crt', verify=BOOTSTRAP_CA_CERT, timeout=15) +        session.get.assert_any_call('https://some-provider.test/provider.json', verify=BOOTSTRAP_CA_CERT, timeout=15)      def test_that_provider_cert_is_used_to_fetch_soledad_json(self):          get_func = MagicMock(wraps=requests.get) @@ -214,3 +214,17 @@ class LeapProviderTest(AbstractLeapTest):                  provider.fetch_soledad_json()          get_func.assert_called_with('https://api.some-provider.test:4430/1/config/soledad-service.json', verify=CA_CERT, timeout=15) + +    def test_that_leap_fingerprint_is_validated(self): +        session = MagicMock(wraps=requests.session()) +        session_func = MagicMock(return_value=session) + +        with patch('pixelated.bitmask_libraries.provider.which_bootstrap_fingerprint', return_value='some fingerprint'): +            with patch('pixelated.bitmask_libraries.provider.which_bootstrap_bundle', return_value=False): +                with patch('pixelated.bitmask_libraries.provider.requests.session', new=session_func): +                    with HTTMock(provider_json_mock, ca_cert_mock, not_found_mock): +                        provider = LeapProvider('some-provider.test', self.config) +                        provider.fetch_valid_certificate() + +        session.get.assert_any_call('https://some-provider.test/ca.crt', verify=False, timeout=15) +        session.mount.assert_called_with('https://', ANY)  | 
