summaryrefslogtreecommitdiff
path: root/service/src/pixelated/bitmask_libraries
diff options
context:
space:
mode:
Diffstat (limited to 'service/src/pixelated/bitmask_libraries')
-rw-r--r--service/src/pixelated/bitmask_libraries/__init__.py0
-rw-r--r--service/src/pixelated/bitmask_libraries/certs.py41
-rw-r--r--service/src/pixelated/bitmask_libraries/keymanager.py111
-rw-r--r--service/src/pixelated/bitmask_libraries/provider.py213
-rw-r--r--service/src/pixelated/bitmask_libraries/smtp.py24
5 files changed, 389 insertions, 0 deletions
diff --git a/service/src/pixelated/bitmask_libraries/__init__.py b/service/src/pixelated/bitmask_libraries/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/service/src/pixelated/bitmask_libraries/__init__.py
diff --git a/service/src/pixelated/bitmask_libraries/certs.py b/service/src/pixelated/bitmask_libraries/certs.py
new file mode 100644
index 00000000..9a76a01d
--- /dev/null
+++ b/service/src/pixelated/bitmask_libraries/certs.py
@@ -0,0 +1,41 @@
+#
+# 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 os
+
+from pixelated.config import leap_config
+
+
+class LeapCertificate(object):
+
+ LEAP_CERT = None
+ LEAP_FINGERPRINT = None
+
+ def __init__(self, provider):
+ self._server_name = provider.server_name
+ self._provider = provider
+
+ @staticmethod
+ def set_cert_and_fingerprint(cert_file=None, cert_fingerprint=None):
+ if cert_fingerprint is None:
+ LeapCertificate.LEAP_CERT = str(cert_file) if cert_file else True
+ LeapCertificate.LEAP_FINGERPRINT = None
+ else:
+ LeapCertificate.LEAP_FINGERPRINT = cert_fingerprint
+ LeapCertificate.LEAP_CERT = False
+
+ @property
+ def provider_web_cert(self):
+ return self.LEAP_CERT
diff --git a/service/src/pixelated/bitmask_libraries/keymanager.py b/service/src/pixelated/bitmask_libraries/keymanager.py
new file mode 100644
index 00000000..9a1b730e
--- /dev/null
+++ b/service/src/pixelated/bitmask_libraries/keymanager.py
@@ -0,0 +1,111 @@
+#
+# 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/>.
+
+from twisted.internet import defer
+from twisted.logger import Logger
+
+from leap.bitmask.keymanager import KeyManager, KeyNotFound
+
+from pixelated.config import leap_config
+
+logger = Logger()
+
+
+class UploadKeyError(Exception):
+ pass
+
+
+TWO_MONTHS = 60
+DEFAULT_EXTENSION_THRESHOLD = TWO_MONTHS
+
+
+class Keymanager(object):
+
+ def __init__(self, provider, soledad, email_address, token, uuid):
+ nicknym_url = provider._discover_nicknym_server()
+ self._email = email_address
+ self.keymanager = KeyManager(self._email, nicknym_url,
+ soledad,
+ token=token, ca_cert_path=provider.provider_api_cert, api_uri=provider.api_uri,
+ api_version=provider.api_version,
+ uid=uuid, gpgbinary=leap_config.gpg_binary,
+ combined_ca_bundle=provider.combined_cerfificates_path)
+
+ @defer.inlineCallbacks
+ def generate_openpgp_key(self):
+ current_key = yield self._key_exists(self._email)
+ if not current_key:
+ current_key = yield self._generate_key_and_send_to_leap()
+ elif current_key.needs_renewal(DEFAULT_EXTENSION_THRESHOLD):
+ current_key = yield self._regenerate_key_and_send_to_leap()
+
+ self._synchronize_remote_key(current_key)
+ logger.debug("Current key for {}: {}".format(self._email, current_key.fingerprint))
+
+ @defer.inlineCallbacks
+ def _synchronize_remote_key(self, current_key):
+ if not self._is_key_synchronized_with_server(current_key):
+ try:
+ yield self.keymanager.send_key()
+ except Exception as e:
+ raise UploadKeyError(e.message)
+
+ @defer.inlineCallbacks
+ def _is_key_synchronized_with_server(self, current_key):
+ remote_key = yield self.get_key(self._email, private=False, fetch_remote=True)
+ defer.returnValue(remote_key.fingerprint == current_key.fingerprint)
+
+ @defer.inlineCallbacks
+ def _regenerate_key_and_send_to_leap(self):
+ logger.info("Regenerating keys - this could take a while...")
+ key = yield self.keymanager.regenerate_key()
+ try:
+ yield self.keymanager.send_key()
+ defer.returnValue(key)
+ except Exception as e:
+ raise UploadKeyError(e.message)
+
+ @defer.inlineCallbacks
+ def _generate_key_and_send_to_leap(self):
+ logger.info("Generating keys - this could take a while...")
+ key = yield self.keymanager.gen_key()
+ try:
+ yield self.keymanager.send_key()
+ defer.returnValue(key)
+ except Exception as e:
+ yield self.delete_key_pair()
+ raise UploadKeyError(e.message)
+
+ @defer.inlineCallbacks
+ def _key_exists(self, email):
+ try:
+ current_key = yield self.get_key(email, private=True, fetch_remote=False)
+ defer.returnValue(current_key)
+ except KeyNotFound:
+ defer.returnValue(None)
+
+ @defer.inlineCallbacks
+ def get_key(self, email, private=False, fetch_remote=True):
+ key = yield self.keymanager.get_key(email, private=private, fetch_remote=fetch_remote)
+ defer.returnValue(key)
+
+ @defer.inlineCallbacks
+ def delete_key_pair(self):
+ private_key = yield self.get_key(self._email, private=True, fetch_remote=False)
+ public_key = yield self.get_key(self._email, private=False, fetch_remote=False)
+
+ self.keymanager.delete_key(private_key)
+ self.keymanager.delete_key(public_key)
diff --git a/service/src/pixelated/bitmask_libraries/provider.py b/service/src/pixelated/bitmask_libraries/provider.py
new file mode 100644
index 00000000..96935fbc
--- /dev/null
+++ b/service/src/pixelated/bitmask_libraries/provider.py
@@ -0,0 +1,213 @@
+#
+# 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 json
+import os
+import fileinput
+import tempfile
+import requests
+
+from leap.common.certs import get_digest
+from leap.common import ca_bundle
+from .certs import LeapCertificate
+from pixelated.config import leap_config
+from pixelated.support.tls_adapter import EnforceTLSv1Adapter
+
+REQUESTS_TIMEOUT = 15
+
+
+class LeapProvider(object):
+ def __init__(self, server_name):
+ self.server_name = server_name
+ self.local_ca_crt = '%s/ca.crt' % leap_config.leap_home
+ self.provider_json = self.fetch_provider_json()
+
+ @property
+ def provider_api_cert(self):
+ return str(os.path.join(leap_config.leap_home, 'providers', self.server_name, 'keys', 'client', 'api.pem'))
+
+ @property
+ def combined_cerfificates_path(self):
+ return str(os.path.join(leap_config.leap_home, 'providers', self.server_name, 'keys', 'client', 'ca_bundle'))
+
+ @property
+ def api_uri(self):
+ return self.provider_json.get('api_uri')
+
+ @property
+ def ca_cert_fingerprint(self):
+ return self.provider_json.get('ca_cert_fingerprint')
+
+ @property
+ def ca_cert_uri(self):
+ return self.provider_json.get('ca_cert_uri')
+
+ @property
+ def api_version(self):
+ return self.provider_json.get('api_version')
+
+ @property
+ def domain(self):
+ return self.provider_json.get('domain')
+
+ @property
+ def services(self):
+ return self.provider_json.get('services')
+
+ def __hash__(self):
+ return hash(self.server_name)
+
+ def __eq__(self, other):
+ return self.server_name == other.server_name
+
+ def ensure_supports_mx(self):
+ if 'mx' not in self.services:
+ raise Exception
+
+ def download_soledad_json(self):
+ self.soledad_json = self.fetch_soledad_json()
+
+ def download_smtp_json(self):
+ self.smtp_json = self.fetch_smtp_json()
+
+ def download_certificate(self, filename=None):
+ """
+ Downloads the server certificate, validates it against the provided fingerprint and stores it to file
+ """
+ path = filename or self.local_ca_crt
+
+ directory = self._extract_directory(path)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ cert = self.fetch_valid_certificate()
+ with open(path, 'w') as out:
+ out.write(cert)
+
+ def _extract_directory(self, path):
+ splited = path.split('/')
+ splited.pop(-1)
+ directory = '/'.join(splited)
+ return directory
+
+ def fetch_valid_certificate(self):
+ cert = self._fetch_certificate()
+ self.validate_certificate(cert)
+ return cert
+
+ def _fetch_certificate(self):
+ 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:
+ cert_data = self._fetch_certificate()
+
+ parts = str(self.ca_cert_fingerprint).split(':')
+ method = parts[0].strip()
+ fingerprint = parts[1].strip()
+
+ digest = get_digest(cert_data, method)
+
+ if fingerprint.strip() != digest:
+ raise Exception('Certificate fingerprints don\'t match! Expected [%s] but got [%s]' % (fingerprint.strip(), digest))
+
+ def smtp_info(self):
+ hosts = self.smtp_json['hosts']
+ hostname = hosts.keys()[0]
+ host = hosts[hostname]
+ return host['hostname'], host['port']
+
+ def _validated_get(self, url):
+ session = requests.session()
+ try:
+ session.mount('https://', EnforceTLSv1Adapter(assert_fingerprint=LeapCertificate.LEAP_FINGERPRINT))
+ response = session.get(url, verify=LeapCertificate(self).provider_web_cert, timeout=REQUESTS_TIMEOUT)
+ response.raise_for_status()
+ return response
+ finally:
+ session.close()
+
+ def fetch_provider_json(self):
+ url = '%s/provider.json' % self._provider_base_url()
+ response = self._validated_get(url)
+ json_data = json.loads(response.content)
+ return json_data
+
+ def fetch_soledad_json(self):
+ service_url = "%s/%s/config/soledad-service.json" % (
+ self.api_uri, self.api_version)
+ response = requests.get(service_url, verify=self.provider_api_cert, timeout=REQUESTS_TIMEOUT)
+ response.raise_for_status()
+ return json.loads(response.content)
+
+ def fetch_smtp_json(self):
+ service_url = '%s/%s/config/smtp-service.json' % (
+ self.api_uri, self.api_version)
+ response = requests.get(service_url, verify=self.provider_api_cert, timeout=REQUESTS_TIMEOUT)
+ response.raise_for_status()
+ return json.loads(response.content)
+
+ def _provider_base_url(self):
+ return 'https://%s' % self.server_name
+
+ def address_for(self, username):
+ return '%s@%s' % (username, self.domain)
+
+ def discover_soledad_server(self, user_uuid):
+ hosts = self.soledad_json['hosts']
+ host = hosts.keys()[0]
+ server_url = 'https://%s:%d/user-%s' % \
+ (hosts[host]['hostname'], hosts[host]['port'], user_uuid)
+ return server_url
+
+ def _discover_nicknym_server(self):
+ return 'https://nicknym.%s:6425/' % self.domain
+
+ def create_combined_bundle_file(self):
+ leap_ca_bundle = ca_bundle.where()
+
+ if self.provider_api_cert == leap_ca_bundle:
+ return self.provider_api_cert
+ elif not self.provider_api_cert:
+ return leap_ca_bundle
+
+ with open(self.combined_cerfificates_path, 'w') as fout:
+ fin = fileinput.input(files=(leap_ca_bundle, self.provider_api_cert))
+ for line in fin:
+ fout.write(line)
+ fin.close()
+
+ def setup_ca_bundle(self):
+ path = os.path.join(leap_config.leap_home, 'providers', self.server_name, 'keys', 'client')
+ if not os.path.isdir(path):
+ os.makedirs(path, 0700)
+ self._download_cert(self.provider_api_cert)
+
+ def _download_cert(self, cert_file_name):
+ cert = self.fetch_valid_certificate()
+ with open(cert_file_name, 'w') as file:
+ file.write(cert)
+
+ def setup_ca(self):
+ self.download_certificate()
+ self.setup_ca_bundle()
+ self.create_combined_bundle_file()
+
+ def download_settings(self):
+ self.download_soledad_json()
+ self.download_smtp_json()
diff --git a/service/src/pixelated/bitmask_libraries/smtp.py b/service/src/pixelated/bitmask_libraries/smtp.py
new file mode 100644
index 00000000..643d4d4a
--- /dev/null
+++ b/service/src/pixelated/bitmask_libraries/smtp.py
@@ -0,0 +1,24 @@
+#
+# 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/>.
+
+
+class LeapSMTPConfig(object):
+
+ def __init__(self, account_email, cert_path, remote_smtp_host, remote_smtp_port):
+ self.account_email = account_email
+ self.cert_path = cert_path
+ self.remote_smtp_host = remote_smtp_host
+ self.remote_smtp_port = remote_smtp_port