summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFolker Bernitt <fbernitt@thoughtworks.com>2015-03-31 13:50:43 +0200
committerFolker Bernitt <fbernitt@thoughtworks.com>2015-03-31 13:53:13 +0200
commita1fc37326a79b95cdb056a100b321586f1c1fb7b (patch)
tree91584a2bcbaae7f883d338a953ac94de77a7f035
parentfaad044b8b576b6d84d88608fa5a57171e3d6169 (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
-rw-r--r--service/pixelated/bitmask_libraries/certs.py9
-rw-r--r--service/pixelated/bitmask_libraries/provider.py31
-rw-r--r--service/pixelated/config/__init__.py1
-rw-r--r--service/pixelated/config/args.py1
-rw-r--r--service/pixelated/config/leap_cert.py7
-rw-r--r--service/pixelated/support/ext_requests_urllib3.py79
-rw-r--r--service/pixelated/support/tls_adapter.py47
-rw-r--r--service/test/unit/bitmask_libraries/test_provider.py20
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)