summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-09-03 21:26:58 -0400
committerKali Kaneko <kali@leap.se>2015-09-03 23:23:01 -0400
commitd1bbecdba0f65f726809989b3d5d323966bc3cc1 (patch)
treee6b98a3e84e816f5224f2213342b065ec2a2e40b /src
initial commit
skeleton for module
Diffstat (limited to 'src')
-rw-r--r--src/leap/__init__.py6
-rw-r--r--src/leap/bonafide/__init__.py0
-rw-r--r--src/leap/bonafide/bootstrap.py0
-rw-r--r--src/leap/bonafide/cred_srp.py158
-rw-r--r--src/leap/bonafide/provider.py58
-rw-r--r--src/leap/bonafide/services/TODO1
-rw-r--r--src/leap/bonafide/services/__init__.py0
-rw-r--r--src/leap/bonafide/services/eip.py0
-rw-r--r--src/leap/bonafide/services/mail.py0
-rw-r--r--src/leap/bonafide/services/soledad.py0
-rw-r--r--src/leap/bonafide/session.py164
-rw-r--r--src/leap/bonafide/srp_auth.py120
-rw-r--r--src/leap/bonafide/tests/__init__.py0
13 files changed, 507 insertions, 0 deletions
diff --git a/src/leap/__init__.py b/src/leap/__init__.py
new file mode 100644
index 0000000..f48ad10
--- /dev/null
+++ b/src/leap/__init__.py
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
diff --git a/src/leap/bonafide/__init__.py b/src/leap/bonafide/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bonafide/__init__.py
diff --git a/src/leap/bonafide/bootstrap.py b/src/leap/bonafide/bootstrap.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bonafide/bootstrap.py
diff --git a/src/leap/bonafide/cred_srp.py b/src/leap/bonafide/cred_srp.py
new file mode 100644
index 0000000..5188830
--- /dev/null
+++ b/src/leap/bonafide/cred_srp.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+# srp_cred.py
+# Copyright (C) 2015 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Credential module for authenticating SRP requests against the LEAP platform.
+"""
+
+# ----------------- DOC ------------------------------------------------------
+# See examples of cred modules:
+# https://github.com/oubiwann-unsupported/txBrowserID/blob/master/browserid/checker.py
+# http://stackoverflow.com/questions/19171686/book-twisted-network-programming-essentials-example-9-1-does-not-work
+# ----------------- DOC ------------------------------------------------------
+
+from zope.interface import implements, implementer, Interface, Attribute
+
+from twisted.cred import portal, credentials, error as credError
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.internet import defer, reactor
+
+from leap.bonafide.session import LeapSession
+
+
+@implementer(ICredentialsChecker)
+class SRPCredentialsChecker(object):
+
+ # TODO need to decide if the credentials that we pass here are per provider
+ # or not.
+ # I think it's better to have the credentials passed with the full user_id,
+ # and here split user/provider.
+ # XXX then we need to check if the provider is properly configured, to get
+ # the right api info AND the needed certificates.
+ # XXX might need to initialize credential checker with a ProviderAPI
+
+ credentialInterfaces = (credentials.IUsernamePassword,)
+
+ def requestAvatarId(self, credentials):
+ # TODO If we are already authenticated, we should just
+ # return the session object, somehow.
+ # XXX If not authenticated (ie, no cached credentials?)
+ # we pass credentials to srpauth.authenticate method
+ # another srpauth class should interface with the blocking srpauth
+ # library, and chain all the calls needed to do the handshake.
+ # Therefore:
+ # should keep reference to the srpauth instances somewhere
+ # TODO If we want to return an anonymous user (useful for getting the
+ # anon-vpn cert), we should return an empty tuple from here.
+
+ return defer.maybeDeferred(_get_leap_session(credentials)).addCallback(
+ self._check_srp_auth)
+
+ def _check_srp_auth(session, username):
+ if session.authenticated:
+ # is ok! --- should add it to some global cache?
+ return defer.succeed(username)
+ else:
+ return defer.fail(credError.UnauthorizedLogin(
+ "Bad username/password combination"))
+
+
+def _get_leap_session(credentials):
+ session = LeapSession(credentials)
+ d = session.handshake()
+ d.addCallback(lambda _: session.authenticate())
+ d.addCallback(lambda _: session)
+ return d
+
+
+class ILeapUserAvatar(Interface):
+
+ # TODO add attributes for username, uuid, token, session_id
+
+ def logout():
+ """
+ Clean up per-login resource allocated to this avatar.
+ """
+
+
+@implementer(ILeapUserAvatar)
+class LeapUserAvatar(object):
+
+ # TODO initialize with: username, uuid, token, session_id
+ # TODO initialize provider data (for api)
+ # TODO how does this relate to LeapSession? maybe we should get one passed?
+
+ def logout(self):
+
+ # TODO reset the (global?) srpauth object.
+ # https://leap.se/en/docs/design/bonafide#logout
+ # DELETE API_BASE/logout(.json)
+ pass
+
+
+class LeapAuthRealm(object):
+ """
+ The realm corresponds to an application domain and is in charge of avatars,
+ which are network-accessible business logic objects.
+ """
+
+ # TODO should be initialized with provider API objects.
+
+ implements(portal.IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+
+ if ILeapUserAvatar in interfaces:
+ # XXX how should we get the details for the requested avatar?
+ avatar = LeapUserAvatar()
+ return ILeapUserAvatar, avatar, avatar.logout
+
+ raise NotImplementedError(
+ "This realm only supports the ILeapUserAvatar interface.")
+
+
+if __name__ == '__main__':
+
+ # from the browser-id implementation
+ #import sys
+ #def _done(res):
+ #print res
+ #reactor.stop()
+ #assertion = sys.argv[1]
+ #d = defer.Deferred()
+ #reactor.callLater(0, d.callback, "http://localhost:8081")
+ #d.addCallback(SRPCredentialsChecker)
+ #d.addCallback(lambda c: c.requestAvatarId(assertion))
+ #d.addBoth(_done)
+ #reactor.run()
+
+ # XXX move boilerplate to some bitmask-core template.
+
+ leap_realm = LeapAuthRealm()
+ # XXX should pass a provider mapping to realm too?
+
+ leap_portal = portal.Portal(leap_realm)
+
+ # XXX should we add an offline credentials checker, that's able
+ # to unlock local soledad sqlcipher backend?
+ # XXX should pass a provider mapping to credentials checker too?
+ srp_checker = SRPCredentialsChecker()
+ leap_portal.registerChecker(srp_checker)
+
+ # XXX tie this to some sample server...
+ reactor.listenTCP(8000, EchoFactory(leap_portal))
+ reactor.run()
diff --git a/src/leap/bonafide/provider.py b/src/leap/bonafide/provider.py
new file mode 100644
index 0000000..ca2ea1d
--- /dev/null
+++ b/src/leap/bonafide/provider.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# provier.py
+# Copyright (C) 2015 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+LEAP Provider API.
+"""
+
+
+class LeapProviderApi(object):
+ # TODO when should the provider-api object be created?
+
+ # XXX separate in auth-needing actions?
+ # XXX version this mapping !!!
+
+ actions = {
+ 'signup': ('users', 'POST'),
+ 'handshake': ('sessions', 'POST'),
+ 'authenticate': ('sessions/{login}', 'PUT'),
+ 'update_user': ('users/{uid}', 'PUT'),
+ 'logout': ('logout', 'DELETE'),
+ 'get_vpn_cert': ('cert', 'POST'),
+ 'get_smtp_cert': ('smtp_cert', 'POST'),
+ }
+
+ def __init__(self, uri, version):
+ self.uri = uri
+ self.version = version
+
+ @property
+ def base_url(self):
+ return "https://{0}/{1}".format(self.uri, self.version)
+
+ # XXX split in two different methods?
+ def get_uri_and_method(self, action_name, **extra_params):
+ action = self.actions.get(action_name, None)
+ if not action:
+ raise ValueError("Requested a non-existent action for this API")
+ resource, method = action
+
+ uri = '{0}/{1}'.format(bytes(self.base_url), bytes(resource)).format(
+ **extra_params)
+ return uri, method
+
+ # XXX add a provider_domain property, just to check if it's the right
+ # provider domain?
diff --git a/src/leap/bonafide/services/TODO b/src/leap/bonafide/services/TODO
new file mode 100644
index 0000000..97dc639
--- /dev/null
+++ b/src/leap/bonafide/services/TODO
@@ -0,0 +1 @@
+# XXX this should be discoverable through the config.
diff --git a/src/leap/bonafide/services/__init__.py b/src/leap/bonafide/services/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bonafide/services/__init__.py
diff --git a/src/leap/bonafide/services/eip.py b/src/leap/bonafide/services/eip.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bonafide/services/eip.py
diff --git a/src/leap/bonafide/services/mail.py b/src/leap/bonafide/services/mail.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bonafide/services/mail.py
diff --git a/src/leap/bonafide/services/soledad.py b/src/leap/bonafide/services/soledad.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bonafide/services/soledad.py
diff --git a/src/leap/bonafide/session.py b/src/leap/bonafide/session.py
new file mode 100644
index 0000000..a113d0a
--- /dev/null
+++ b/src/leap/bonafide/session.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# session.py
+# Copyright (C) 2015 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+LEAP Session management.
+"""
+import cookielib
+import urllib
+
+from twisted.internet import defer, reactor, protocol
+from twisted.internet.ssl import Certificate
+from twisted.web.client import Agent, CookieAgent, HTTPConnectionPool
+from twisted.web.client import BrowserLikePolicyForHTTPS
+from twisted.web.http_headers import Headers
+from twisted.web.iweb import IBodyProducer
+from twisted.python import log
+from twisted.python.filepath import FilePath
+from twisted.python import log
+from zope.interface import implements
+
+from leap.bonafide import srp_auth
+
+
+class LeapSession(object):
+
+ def __init__(self, credentials, api, provider_cert):
+ # TODO check if an anonymous credentials is passed
+ # TODO -- we could decorate some methods so that they
+ # complain if we're not authenticated.
+
+ self.username = credentials.username
+ self.password = credentials.password
+
+ self._api = api
+ customPolicy = BrowserLikePolicyForHTTPS(
+ Certificate.loadPEM(FilePath(provider_cert).getContent()))
+
+ # BUG XXX See https://twistedmatrix.com/trac/ticket/7843
+ pool = HTTPConnectionPool(reactor, persistent=False)
+ agent = Agent(reactor, customPolicy, connectTimeout=30, pool=pool)
+ cookiejar = cookielib.CookieJar()
+ self._agent = CookieAgent(agent, cookiejar)
+
+ self._srp_auth = srp_auth.SRPAuthMechanism()
+ self._srp_user = None
+
+ @defer.inlineCallbacks
+ def authenticate(self):
+ srpuser, A = self._srp_auth.initialize(
+ self.username, self.password)
+ self._srp_user = srpuser
+
+ uri, method = self._api.get_uri_and_method('handshake')
+ log.msg("%s to %s" % (method, uri))
+ params = self._srp_auth.get_handshake_params(self.username, A)
+ handshake = yield httpRequest(self._agent, uri, values=params,
+ method=method)
+
+ M = self._srp_auth.process_handshake(srpuser, handshake)
+ uri, method = self._api.get_uri_and_method(
+ 'authenticate', login=self.username)
+ log.msg("%s to %s" % (method, uri))
+ params = self._srp_auth.get_authentication_params(M, A)
+ auth = yield httpRequest(self._agent, uri, values=params,
+ method=method)
+
+ uuid, token, M2 = self._srp_auth.process_authentication(auth)
+ self._srp_auth.verify_authentication(srpuser, M2)
+ defer.succeed('ok')
+ # XXX get_session_id??
+ # XXX return defer.succeed
+
+ def is_authenticated(self):
+ if not self._srp_user:
+ return False
+ return self._srp_user.authenticated()
+
+
+def httpRequest(agent, url, values={}, headers={}, method='POST'):
+ headers['Content-Type'] = ['application/x-www-form-urlencoded']
+ data = urllib.urlencode(values)
+ d = agent.request(method, url, Headers(headers),
+ StringProducer(data) if data else None)
+
+ def handle_response(response):
+ if response.code == 204:
+ d = defer.succeed('')
+ else:
+ class SimpleReceiver(protocol.Protocol):
+ def __init__(s, d):
+ s.buf = ''
+ s.d = d
+
+ def dataReceived(s, data):
+ print "----> handle response: GOT DATA"
+ s.buf += data
+
+ def connectionLost(s, reason):
+ print "CONNECTION LOST ---", reason
+ # TODO: test if reason is twisted.web.client.ResponseDone,
+ # if not, do an errback
+ s.d.callback(s.buf)
+
+ d = defer.Deferred()
+ response.deliverBody(SimpleReceiver(d))
+ return d
+
+ d.addCallback(handle_response)
+ return d
+
+
+class StringProducer(object):
+
+ implements(IBodyProducer)
+
+ def __init__(self, body):
+ self.body = body
+ self.length = len(body)
+
+ def startProducing(self, consumer):
+ consumer.write(self.body)
+ return defer.succeed(None)
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ pass
+
+if __name__ == "__main__":
+ from leap.bonafide import provider
+ from twisted.cred.credentials import UsernamePassword
+
+ api = provider.LeapProviderApi('api.cdev.bitmask.net:4430', 1)
+ credentials = UsernamePassword('test_deb_090', 'lalalala')
+
+ cdev_pem = '/home/kali/.config/leap/providers/cdev.bitmask.net/keys/ca/cacert.pem'
+ session = LeapSession(credentials, api, cdev_pem)
+
+ def print_result(result):
+ print "Auth OK"
+ print "result"
+
+ def cbShutDown(ignored):
+ reactor.stop()
+
+ d = session.authenticate()
+ d.addCallback(print_result)
+ d.addErrback(lambda f: log.err(f))
+ d.addBoth(cbShutDown)
+ reactor.run()
diff --git a/src/leap/bonafide/srp_auth.py b/src/leap/bonafide/srp_auth.py
new file mode 100644
index 0000000..ac2cd67
--- /dev/null
+++ b/src/leap/bonafide/srp_auth.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+# srp.py
+# Copyright (C) 2014 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+SRP Authentication.
+"""
+
+import binascii
+import logging
+import json
+
+import srp
+
+logger = logging.getLogger(__name__)
+
+
+class SRPAuthMechanism(object):
+
+ """
+ Implement a protocol-agnostic SRP Authentication mechanism.
+ """
+
+ def initialize(self, username, password):
+ srp_user = srp.User(username.encode('utf-8'),
+ password.encode('utf-8'),
+ srp.SHA256, srp.NG_1024)
+ _, A = srp_user.start_authentication()
+ return srp_user, A
+
+ def get_handshake_params(self, username, A):
+ return {'login': bytes(username), 'A': binascii.hexlify(A)}
+
+ def process_handshake(self, srp_user, handshake_response):
+ challenge = json.loads(handshake_response)
+ self._check_for_errors(challenge)
+ salt = challenge.get('salt', None)
+ B = challenge.get('B', None)
+ unhex_salt, unhex_B = self._unhex_salt_B(salt, B)
+ M = srp_user.process_challenge(unhex_salt, unhex_B)
+ return M
+
+ def get_authentication_params(self, M, A):
+ # I think A is not used in the server side
+ return {'client_auth': binascii.hexlify(M), 'A': binascii.hexlify(A)}
+
+ def process_authentication(self, authentication_response):
+ auth = json.loads(authentication_response)
+ uuid = auth.get('id', None)
+ token = auth.get('token', None)
+ M2 = auth.get('M2', None)
+ self._check_auth_params(uuid, token, M2)
+ return uuid, token, M2
+
+ def verify_authentication(self, srp_user, M2):
+ unhex_M2 = _safe_unhexlify(M2)
+ srp_user.verify_session(unhex_M2)
+ assert srp_user.authenticated()
+
+ def _check_for_errors(self, challenge):
+ if 'errors' in challenge:
+ msg = challenge['errors']['base']
+ raise SRPAuthError(msg)
+
+ def _unhex_salt_B(self, salt, B):
+ if salt is None:
+ raise SRPAuthNoSalt()
+ if B is None:
+ raise SRPAuthNoB()
+ try:
+ unhex_salt = _safe_unhexlify(salt)
+ unhex_B = _safe_unhexlify(B)
+ except (TypeError, ValueError) as e:
+ raise SRPAuthBadDataFromServer(str(e))
+ return unhex_salt, unhex_B
+
+ def _check_auth_params(self, uuid, token, M2):
+ if not all((uuid, token, M2)):
+ msg = '%r' % (M2, uuid, token,)
+ raise SRPAuthBadDataFromServer(msg)
+
+ #XXX move to session -----------------------
+ def get_session_id(self, cookies):
+ return cookies.get('_session_id', None)
+ #XXX move to session -----------------------
+
+
+def _safe_unhexlify(val):
+ return binascii.unhexlify(val) \
+ if (len(val) % 2 == 0) else binascii.unhexlify('0' + val)
+
+
+class SRPAuthError(Exception):
+ """
+ Base exception for srp authentication errors
+ """
+
+
+class SRPAuthNoSalt(SRPAuthError):
+ message = 'The server didn\'t send the salt parameter'
+
+
+class SRPAuthNoB(SRPAuthError):
+ message = 'The server didn\'t send the B parameter'
+
+class SRPAuthBadDataFromServer(SRPAuthError):
+ pass
diff --git a/src/leap/bonafide/tests/__init__.py b/src/leap/bonafide/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bonafide/tests/__init__.py