From d1bbecdba0f65f726809989b3d5d323966bc3cc1 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 3 Sep 2015 21:26:58 -0400 Subject: initial commit skeleton for module --- src/leap/__init__.py | 6 ++ src/leap/bonafide/__init__.py | 0 src/leap/bonafide/bootstrap.py | 0 src/leap/bonafide/cred_srp.py | 158 +++++++++++++++++++++++++++++++ src/leap/bonafide/provider.py | 58 ++++++++++++ src/leap/bonafide/services/TODO | 1 + src/leap/bonafide/services/__init__.py | 0 src/leap/bonafide/services/eip.py | 0 src/leap/bonafide/services/mail.py | 0 src/leap/bonafide/services/soledad.py | 0 src/leap/bonafide/session.py | 164 +++++++++++++++++++++++++++++++++ src/leap/bonafide/srp_auth.py | 120 ++++++++++++++++++++++++ src/leap/bonafide/tests/__init__.py | 0 13 files changed, 507 insertions(+) create mode 100644 src/leap/__init__.py create mode 100644 src/leap/bonafide/__init__.py create mode 100644 src/leap/bonafide/bootstrap.py create mode 100644 src/leap/bonafide/cred_srp.py create mode 100644 src/leap/bonafide/provider.py create mode 100644 src/leap/bonafide/services/TODO create mode 100644 src/leap/bonafide/services/__init__.py create mode 100644 src/leap/bonafide/services/eip.py create mode 100644 src/leap/bonafide/services/mail.py create mode 100644 src/leap/bonafide/services/soledad.py create mode 100644 src/leap/bonafide/session.py create mode 100644 src/leap/bonafide/srp_auth.py create mode 100644 src/leap/bonafide/tests/__init__.py (limited to 'src/leap') 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 diff --git a/src/leap/bonafide/bootstrap.py b/src/leap/bonafide/bootstrap.py new file mode 100644 index 0000000..e69de29 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 . + +""" +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 . +""" +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 diff --git a/src/leap/bonafide/services/eip.py b/src/leap/bonafide/services/eip.py new file mode 100644 index 0000000..e69de29 diff --git a/src/leap/bonafide/services/mail.py b/src/leap/bonafide/services/mail.py new file mode 100644 index 0000000..e69de29 diff --git a/src/leap/bonafide/services/soledad.py b/src/leap/bonafide/services/soledad.py new file mode 100644 index 0000000..e69de29 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 . +""" +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 . + +""" +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 -- cgit v1.2.3