summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2018-01-10 12:31:17 +0100
committerKali Kaneko <kali@leap.se>2018-01-25 01:19:10 +0100
commit5ffa0c1710ce4038b94a026a58daf8f12aef2ec4 (patch)
tree61723ea8c2385b2930ed82f401f52d6239ecc221
parent5510c24e61046269e5b29df7d7ffb67a42bdc763 (diff)
[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.
-rw-r--r--docs/changelog.rst3
-rw-r--r--src/leap/bitmask/bonafide/_protocol.py39
-rw-r--r--src/leap/bitmask/bonafide/config.py41
-rw-r--r--src/leap/bitmask/bonafide/service.py4
-rw-r--r--src/leap/bitmask/bonafide/session.py26
-rw-r--r--src/leap/bitmask/vpn/_checks.py11
-rw-r--r--src/leap/bitmask/vpn/service.py97
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 <https://0xacab.org/leap/bitmask-dev/issues/8217>`_: renew OpenPGP keys before they expire.
- `#9074 <https://0xacab.org/leap/bitmask-dev/issues/9074>`_: pin provider ca certs.
-- `#6914 <https://0xacab.org/leap/bitmask-dev/issues/6914>`_: expose an API to retrive message status.
+- `#6914 <https://0xacab.org/leap/bitmask-dev/issues/6914>`_: expose an API to retreive message status.
- `#9188 <https://0xacab.org/leap/bitmask-dev/issues/9188>`_: try other gateways if the main one fails.
- `#9125 <https://0xacab.org/leap/bitmask-dev/issues/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: