diff options
Diffstat (limited to 'service/src/pixelated/bitmask_libraries')
-rw-r--r-- | service/src/pixelated/bitmask_libraries/__init__.py | 0 | ||||
-rw-r--r-- | service/src/pixelated/bitmask_libraries/certs.py | 41 | ||||
-rw-r--r-- | service/src/pixelated/bitmask_libraries/keymanager.py | 111 | ||||
-rw-r--r-- | service/src/pixelated/bitmask_libraries/provider.py | 213 | ||||
-rw-r--r-- | service/src/pixelated/bitmask_libraries/smtp.py | 24 |
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 |