From 5ffa0c1710ce4038b94a026a58daf8f12aef2ec4 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 10 Jan 2018 12:31:17 +0100 Subject: [feat] support anonymous vpn honor the anonymous certificate for the providers that offer it. this still needs a change in bonafide, in which if provider supports anonymous access we still have to download eip-service.json for testing, I assume this has been already manually downloaded. --- docs/changelog.rst | 3 +- src/leap/bitmask/bonafide/_protocol.py | 39 ++++++++------ src/leap/bitmask/bonafide/config.py | 41 +++++++++----- src/leap/bitmask/bonafide/service.py | 4 +- src/leap/bitmask/bonafide/session.py | 26 ++++----- src/leap/bitmask/vpn/_checks.py | 11 ++-- src/leap/bitmask/vpn/service.py | 97 +++++++++++++++++++++++++--------- 7 files changed, 150 insertions(+), 71 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6a6a8125..69b314ee 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,10 +9,11 @@ Features ~~~~~~~~ - `#8217 `_: renew OpenPGP keys before they expire. - `#9074 `_: pin provider ca certs. -- `#6914 `_: expose an API to retrive message status. +- `#6914 `_: expose an API to retreive message status. - `#9188 `_: try other gateways if the main one fails. - `#9125 `_: port to use qtwebengine for rendering UI. - Set a windows title, so that Bitmask windows can be programmatically manipulated. +- Support anonymous vpn. Bugfixes ~~~~~~~~ diff --git a/src/leap/bitmask/bonafide/_protocol.py b/src/leap/bitmask/bonafide/_protocol.py index e044875f..ceb29efd 100644 --- a/src/leap/bitmask/bonafide/_protocol.py +++ b/src/leap/bitmask/bonafide/_protocol.py @@ -25,7 +25,8 @@ from leap.bitmask.bonafide.provider import Api from leap.bitmask.bonafide.session import Session, OK from leap.common.config import get_path_prefix -from twisted.cred.credentials import UsernamePassword +from twisted.cred.credentials import UsernamePassword, Anonymous +from twisted.cred.checkers import ANONYMOUS from twisted.internet.defer import fail from twisted.logger import Logger @@ -52,13 +53,16 @@ class BonafideProtocol(object): self._apis[provider.domain] = api return api - def _get_session(self, provider, full_id, password=""): + def _get_or_create_session(self, provider, full_id, password=""): if full_id in self._sessions: return self._sessions[full_id] - - # TODO if password/username null, then pass AnonymousCreds - username, provider_id = config.get_username_and_provider(full_id) - credentials = UsernamePassword(username, password) + if full_id == ANONYMOUS: + credentials = Anonymous() + provider_id = provider.domain + else: + username, provider_id = config.get_username_and_provider( + full_id) + credentials = UsernamePassword(username, password) api = self._get_api(provider) provider_pem = config.get_ca_cert_path(_preffix, provider_id) session = Session(credentials, api, provider_pem) @@ -90,7 +94,7 @@ class BonafideProtocol(object): return user username, _ = config.get_username_and_provider(full_id) - session = self._get_session(provider, full_id, password) + session = self._get_or_create_session(provider, full_id, password) d = session.signup(username, password, invite) d.addCallback(return_user) d.addErrback(self._del_session_errback, full_id) @@ -102,7 +106,7 @@ class BonafideProtocol(object): provider = config.Provider.get(provider_id, autoconf=autoconf) def maybe_finish_provider_bootstrap(result): - session = self._get_session(provider, full_id, password) + session = self._get_or_create_session(provider, full_id, password) d = provider.download_services_config_with_auth(session) d.addCallback(lambda _: result) return d @@ -121,7 +125,7 @@ class BonafideProtocol(object): self.log.debug('AUTH for %s' % full_id) - session = self._get_session(provider, full_id, password) + session = self._get_or_create_session(provider, full_id, password) d = session.authenticate() d.addCallback(return_token_and_uuid, session) d.addErrback(self._del_session_errback, full_id) @@ -171,7 +175,6 @@ class BonafideProtocol(object): return config.delete_provider(provider_id) def do_provider_list(self, seeded=False): - # TODO: seeded, we don't have pinned providers yet providers = config.list_providers() return [{"domain": p} for p in providers] @@ -182,11 +185,17 @@ class BonafideProtocol(object): d = self._sessions[full_id].get_smtp_cert() return d - def do_get_vpn_cert(self, full_id): - if (full_id not in self._sessions or - not self._sessions[full_id].is_authenticated): - return fail(RuntimeError("There is no session for such user")) - d = self._sessions[full_id].get_vpn_cert() + def do_get_vpn_cert(self, full_id, anonymous=False): + if anonymous: + _, provider_id = full_id.split('@') + provider = config.Provider.get(provider_id, autoconf=True) + d = self._get_or_create_session( + provider, ANONYMOUS).get_vpn_cert() + else: + if (full_id not in self._sessions or + not self._sessions[full_id].is_authenticated): + return fail(RuntimeError("There is no session for such user")) + d = self._sessions[full_id].get_vpn_cert() return d def do_update_user(self): diff --git a/src/leap/bitmask/bonafide/config.py b/src/leap/bitmask/bonafide/config.py index 222726b7..fe40f277 100644 --- a/src/leap/bitmask/bonafide/config.py +++ b/src/leap/bitmask/bonafide/config.py @@ -31,13 +31,15 @@ from cryptography.hazmat.primitives import hashes from cryptography.x509 import load_pem_x509_certificate from urlparse import urlparse +from twisted.cred.credentials import Anonymous from twisted.internet import defer from twisted.logger import Logger from twisted.web.client import downloadPage from leap.bitmask.bonafide._http import httpRequest -from leap.bitmask.bonafide.provider import Discovery from leap.bitmask.bonafide.errors import NotConfiguredError, NetworkError +from leap.bitmask.bonafide.provider import Discovery +from leap.bitmask.bonafide.session import Session from leap.bitmask.util import here, STANDALONE from leap.common.check import leap_assert @@ -266,6 +268,10 @@ class Provider(object): self.log.debug('Bootstrapping provider %s' % domain) def first_bootstrap_done(ignored): + if self._allows_anonymous: + # we continue bootstrapping, we do not + # need to wait for authentication. + return try: self.first_bootstrap.callback('got config') except defer.AlreadyCalledError: @@ -282,6 +288,14 @@ class Provider(object): d.addCallback(self.maybe_download_services_config) self.ongoing_bootstrap = d + def _allows_anonymous(self): + try: + anon = self._provider_config.get( + 'service').get('allows_anonymous') + except ValueError: + anon = False + return anon + def callWhenMainConfigReady(self, cb, *args, **kw): d = self.first_bootstrap d.addCallback(lambda _: cb(*args, **kw)) @@ -388,17 +402,23 @@ class Provider(object): return os.path.isfile(self._get_configs_path()) def maybe_download_services_config(self, ignored): - # TODO --- currently, some providers (mail.bitmask.net) raise 401 # UNAUTHENTICATED if we try to get the services # See: # https://leap.se/code/issues/7906 + def first_bootstrap_done(ignored): + try: + self.first_bootstrap.callback('got config') + except defer.AlreadyCalledError: + pass + uri, met, path = self._get_configs_download_params() d = httpRequest( self._http._agent, uri, method=met, saveto=path) d.addCallback(lambda _: self._load_provider_json()) d.addCallback( lambda _: self._get_config_for_all_services(session=None)) + d.addCallback(first_bootstrap_done) d.addErrback(lambda _: 'ok for now') return d @@ -499,6 +519,10 @@ class Provider(object): self._disco.netloc = parsed.netloc def _get_config_for_all_services(self, session): + if session is None: + provider_cert = self._get_ca_cert_path() + session = Session(Anonymous(), self.api_uri, provider_cert) + services_dict = self._load_provider_configs() configs_path = self._get_configs_path() with open(configs_path) as jsonf: @@ -510,12 +534,8 @@ class Provider(object): for subservice in self.SERVICES_MAP[service]: uri = base + str(services_dict[subservice]) path = self._get_service_config_path(subservice) - if session: - d = session.fetch_provider_configs( - uri, path, method='GET') - else: - d = self._fetch_provider_configs_unauthenticated( - uri, path, method='GET') + d = session.fetch_provider_configs( + uri, path, method='GET') pending.append(d) return defer.gatherResults(pending) @@ -525,11 +545,6 @@ class Provider(object): services_dict = Record(**json.load(jsonf)).services return services_dict - def _fetch_provider_configs_unauthenticated(self, uri, path): - self.log.info('Downloading config for %s...' % uri) - return httpRequest( - self._http._agent, uri, saveto=path) - class Record(object): def __init__(self, **kw): diff --git a/src/leap/bitmask/bonafide/service.py b/src/leap/bitmask/bonafide/service.py index 43f51768..5856a263 100644 --- a/src/leap/bitmask/bonafide/service.py +++ b/src/leap/bitmask/bonafide/service.py @@ -131,12 +131,12 @@ class BonafideService(HookableService): def do_provider_list(self, seeded=False): return self._bonafide.do_provider_list(seeded) - def do_get_vpn_cert(self, username): + def do_get_vpn_cert(self, username, anonymous=False): if not username: return defer.fail( RuntimeError('No username, cannot get VPN cert.')) - d = self._bonafide.do_get_vpn_cert(username) + d = self._bonafide.do_get_vpn_cert(username, anonymous=anonymous) d.addCallback(lambda response: (username, response)) return d diff --git a/src/leap/bitmask/bonafide/session.py b/src/leap/bitmask/bonafide/session.py index 988cbb99..d6a39447 100644 --- a/src/leap/bitmask/bonafide/session.py +++ b/src/leap/bitmask/bonafide/session.py @@ -17,6 +17,7 @@ """ LEAP Session management. """ +from twisted.cred.credentials import IAnonymous, IUsernamePassword from twisted.internet import defer, reactor from twisted.logger import Logger @@ -47,7 +48,6 @@ class Session(object): log = Logger() def __init__(self, credentials, api, provider_cert): - # TODO check if an anonymous credentials is passed. # TODO move provider_cert to api object. # On creation, it should be able to retrieve all the info it needs # (calling bootstrap). @@ -56,9 +56,12 @@ class Session(object): # and a "autoconfig" attribute passed on initialization. # TODO get a file-descriptor for password if not in credentials # TODO merge self._request with config.Provider._http_request ? - - self.username = credentials.username - self.password = credentials.password + if IAnonymous.providedBy(credentials): + self.username = None + self.password = None + elif IUsernamePassword.providedBy(credentials): + self.username = credentials.username + self.password = credentials.password self._provider_cert = provider_cert self._api = api self._initialize_session() @@ -86,7 +89,10 @@ class Session(object): @property def is_authenticated(self): - return self._srp_auth.srp_user.authenticated() + if self.username is None: + return False + else: + return self._srp_auth.srp_user.authenticated() @defer.inlineCallbacks def authenticate(self): @@ -133,8 +139,8 @@ class Session(object): met = self._api.get_update_user_method() params = self._srp_password.get_password_params( self.username, password) - update = yield self._request(self._agent, uri, values=params, - method=met) + yield self._request(self._agent, uri, values=params, + method=met) self.password = password self._srp_auth = _srp.SRPAuthMechanism(self.username, password) defer.returnValue(OK) @@ -153,16 +159,12 @@ class Session(object): # User certificates def get_vpn_cert(self): - # TODO pass it to the provider object so that it can save it in the - # right path. uri = self._api.get_vpn_cert_uri() met = self._api.get_vpn_cert_method() return self._request(self._agent, uri, method=met) @_auth_required def get_smtp_cert(self): - # TODO pass it to the provider object so that it can save it in the - # right path. uri = self._api.get_smtp_cert_uri() met = self._api.get_smtp_cert_method() return self._request(self._agent, uri, method=met) @@ -198,7 +200,7 @@ class Session(object): @defer.inlineCallbacks def fetch_provider_configs(self, uri, path, method='GET'): - config = yield self._request( + yield self._request( self._agent, uri, method=method, saveto=path) defer.returnValue('ok') diff --git a/src/leap/bitmask/vpn/_checks.py b/src/leap/bitmask/vpn/_checks.py index c8f7dd36..9586d096 100644 --- a/src/leap/bitmask/vpn/_checks.py +++ b/src/leap/bitmask/vpn/_checks.py @@ -17,14 +17,19 @@ class ImproperlyConfigured(Exception): pass -def is_service_ready(provider): +def get_failure_for(provider): if not _has_valid_cert(provider): raise ImproperlyConfigured('Missing VPN certificate') - if IS_LINUX and not is_pkexec_in_system(): - log.warn('System has no pkexec') raise NoPkexecAvailable() + +def is_service_ready(provider): + if not _has_valid_cert(provider): + return False + if IS_LINUX and not is_pkexec_in_system(): + log.warn('System has no pkexec') + return False return True diff --git a/src/leap/bitmask/vpn/service.py b/src/leap/bitmask/vpn/service.py index 922cfaea..609d70ee 100644 --- a/src/leap/bitmask/vpn/service.py +++ b/src/leap/bitmask/vpn/service.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # service.py -# Copyright (C) 2015-2017 LEAP +# Copyright (C) 2015-2018 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -35,6 +35,7 @@ from leap.bitmask.vpn._checks import ( get_vpn_cert_path, cert_expires ) + from leap.bitmask.vpn import privilege, helpers from leap.common.config import get_path_prefix from leap.common.files import check_and_fix_urw_only @@ -79,13 +80,22 @@ class VPNService(HookableService): except ValueError: self._loc = [] + _autostart = self._cfg.get('autostart', False) + self._autostart = _autostart + + _anonymous = self._cfg.get('anonymous', True) + self._anonymous_enabled = _anonymous + if helpers.check() and self._firewall.is_up(): self._firewall.stop() def startService(self): - # TODO this could trigger a check for validity of the certificates, - # etc. + # TODO trigger a check for validity of the certificates, + # and schedule a re-download if needed. + # TODO start a watchDog service (to push status events) super(VPNService, self).startService() + if self._autostart: + self.start_vpn() def stopService(self): try: @@ -100,18 +110,21 @@ class VPNService(HookableService): exc = Exception('VPN already started') exc.expected = True raise exc - if not domain: + if domain is None: domain = self._read_last() - if not domain: + if domain is None: exc = Exception("VPN can't start, a provider is needed") exc.expected = True raise exc + + yield self._setup(domain) if not is_service_ready(domain): - exc = Exception("VPN is not ready") + exc = Exception('VPN is not ready') exc.expected = True raise exc - yield self._setup(domain) + # XXX we can signal status to frontend, use + # get_failure_for(provider) -- no polkit, etc. fw_ok = self._firewall.start() if not fw_ok: @@ -177,7 +190,7 @@ class VPNService(HookableService): return ret @defer.inlineCallbacks - def do_get_cert(self, username): + def do_get_cert(self, username, anonymous=False): try: _, provider = username.split('@') except ValueError: @@ -188,7 +201,8 @@ class VPNService(HookableService): # fetch vpn cert and store bonafide = self.parent.getServiceNamed("bonafide") - _, cert_str = yield bonafide.do_get_vpn_cert(username) + _, cert_str = yield bonafide.do_get_vpn_cert( + username, anonymous=anonymous) cert_path = get_vpn_cert_path(provider) cert_dir = os.path.dirname(cert_path) @@ -242,32 +256,29 @@ class VPNService(HookableService): return self._cco @defer.inlineCallbacks - def _setup(self, provider): + def _setup(self, provider_id): """Set up ConfiguredTunnel for a specified provider. :param provider: the provider to use, e.g. 'demo.bitmask.net' :type provider: str""" bonafide = self.parent.getServiceNamed('bonafide') - config = yield bonafide.do_provider_read(provider, 'eip') - sorted_gateways = GatewaySelector( - config.gateways, config.locations, - preferred={'cc': self._cco, 'loc': self._loc} - ).select_gateways() + # bootstrap if not yet done + if provider_id not in bonafide.do_provider_list(seeded=False): + yield bonafide.do_provider_create(provider_id) - extra_flags = config.openvpn_configuration + provider = yield bonafide.do_provider_read(provider_id) + config = yield bonafide.do_provider_read(provider_id, 'eip') - prefix = os.path.join( - self._basepath, "leap", "providers", provider, "keys") - cert_path = key_path = os.path.join(prefix, "client", "openvpn.pem") - ca_path = os.path.join(prefix, "ca", "cacert.pem") + sorted_gateways = self._get_gateways(config) + extra_flags = config.openvpn_configuration + cert_path, ca_path = self._get_cert_paths(provider_id) + key_path = cert_path + anonvpn = self._has_anonvpn(provider) if not os.path.isfile(cert_path): - gotcert = yield self.do_get_cert('ignored@%s' % provider) - if gotcert['get_cert'] != 'ok': - raise ImproperlyConfigured( - 'Cannot find client certificate. Please get one') + yield self._maybe_get_anon_cert(anonvpn, provider_id) if not os.path.isfile(ca_path): raise ImproperlyConfigured( @@ -277,9 +288,45 @@ class VPNService(HookableService): # TODO add remote ports, according to preferred sequence remotes = tuple([(ip, '443') for ip in sorted_gateways]) self._tunnel = ConfiguredTunnel( - provider, remotes, cert_path, key_path, ca_path, extra_flags) + provider_id, remotes, cert_path, key_path, ca_path, extra_flags) self._firewall = FirewallManager(remotes) + def _get_gateways(self, config): + return GatewaySelector( + config.gateways, config.locations, + preferred={'cc': self._cco, 'loc': self._loc} + ).select_gateways() + + def _get_cert_paths(self, provider_id): + prefix = os.path.join( + self._basepath, "leap", "providers", provider_id, "keys") + cert_path = os.path.join(prefix, "client", "openvpn.pem") + ca_path = os.path.join(prefix, "ca", "cacert.pem") + return cert_path, ca_path + + def _has_anonvpn(self, provider): + try: + allows_anonymous = provider.get('service').get('allow_anonymous') + except (ValueError,): + allows_anonymous = False + return self._anonymous_enabled and allows_anonymous + + @defer.inlineCallbacks + def _maybe_get_anon_cert(self, anonvpn, provider_id): + if anonvpn: + self.log.debug('Getting anon VPN cert') + gotcert = yield self.do_get_cert( + 'anonymous@%s' % provider_id, anonymous=True) + if gotcert['get_cert'] != 'ok': + raise ImproperlyConfigured( + '(anon) Could not get client certificate. ' + 'Please get one') + else: + # this should instruct get_cert to get a session from bonafide + # if we are authenticated. + raise ImproperlyConfigured( + 'Cannot find client certificate. Please get one') + def _write_last(self, domain): path = os.path.join(self._basepath, self._last_vpn_path) with open(path, 'w') as f: -- cgit v1.2.3