diff options
| author | Kali Kaneko <kali@leap.se> | 2015-12-16 01:12:30 -0400 | 
|---|---|---|
| committer | Kali Kaneko <kali@leap.se> | 2015-12-18 11:13:50 -0400 | 
| commit | a8686ab9ec0c7d83a417ab5c0b05aa0ce76ec37d (patch) | |
| tree | 75db18f7ced1e97995d177640789d2f525394987 /mail/src | |
| parent | 7a227f4f525e90a6c2cff8a87d6f8816c6b844e1 (diff) | |
[feat] cred authentication for SMTP service
Diffstat (limited to 'mail/src')
| -rw-r--r-- | mail/src/leap/mail/cred.py | 80 | ||||
| -rw-r--r-- | mail/src/leap/mail/errors.py | 27 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/service/imap.py | 59 | ||||
| -rw-r--r-- | mail/src/leap/mail/outgoing/service.py | 32 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/__init__.py | 50 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/gateway.py | 159 | 
6 files changed, 257 insertions, 150 deletions
| diff --git a/mail/src/leap/mail/cred.py b/mail/src/leap/mail/cred.py new file mode 100644 index 0000000..7eab1f0 --- /dev/null +++ b/mail/src/leap/mail/cred.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# 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/>. +""" +Credentials handling. +""" + +from zope.interface import implementer +from twisted.cred.checkers import ICredentialsChecker +from twisted.cred.credentials import IUsernamePassword +from twisted.cred.error import UnauthorizedLogin +from twisted.internet import defer + + +@implementer(ICredentialsChecker) +class LocalSoledadTokenChecker(object): + +    """ +    A Credentials Checker for a LocalSoledad store. + +    It checks that: + +    1) The Local SoledadStorage has been correctly unlocked for the given +       user. This currently means that the right passphrase has been passed +       to the Local SoledadStorage. + +    2) The password passed in the credentials matches whatever token has +       been stored in the local encrypted SoledadStorage, associated to the +       Protocol that is requesting the authentication. +    """ + +    credentialInterfaces = (IUsernamePassword,) +    service = None + +    def __init__(self, soledad_sessions): +        """ +        :param soledad_sessions: a dict-like object, containing instances +                                 of a Store (soledad instances), indexed by +                                 userid. +        """ +        self._soledad_sessions = soledad_sessions + +    def requestAvatarId(self, credentials): +        if self.service is None: +            raise NotImplementedError( +                "this checker has not defined its service name") +        username, password = credentials.username, credentials.password +        d = self.checkSoledadToken(username, password, self.service) +        d.addErrback(lambda f: defer.fail(UnauthorizedLogin())) +        return d + +    def checkSoledadToken(self, username, password, service): +        soledad = self._soledad_sessions.get(username) +        if not soledad: +            return defer.fail(Exception("No soledad")) + +        def match_token(token): +            if token is None: +                raise RuntimeError('no token') +            if token == password: +                return username +            else: +                raise RuntimeError('bad token') + +        d = soledad.get_or_create_service_token(service) +        d.addCallback(match_token) +        return d diff --git a/mail/src/leap/mail/errors.py b/mail/src/leap/mail/errors.py new file mode 100644 index 0000000..2f18e87 --- /dev/null +++ b/mail/src/leap/mail/errors.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# errors.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/>. +""" +Exceptions for leap.mail +""" + + +class AuthenticationError(Exception): +    pass + + +class ConfigurationError(Exception): +    pass diff --git a/mail/src/leap/mail/imap/service/imap.py b/mail/src/leap/mail/imap/service/imap.py index 24fa865..9e34454 100644 --- a/mail/src/leap/mail/imap/service/imap.py +++ b/mail/src/leap/mail/imap/service/imap.py @@ -23,9 +23,6 @@ import os  from collections import defaultdict  from twisted.cred.portal import Portal, IRealm -from twisted.cred.credentials import IUsernamePassword -from twisted.cred.checkers import ICredentialsChecker -from twisted.cred.error import UnauthorizedLogin  from twisted.mail.imap4 import IAccount  from twisted.internet import defer  from twisted.internet import reactor @@ -35,6 +32,7 @@ from twisted.python import log  from zope.interface import implementer  from leap.common.events import emit_async, catalog +from leap.mail.cred import LocalSoledadTokenChecker  from leap.mail.imap.account import IMAPAccount  from leap.mail.imap.server import LEAPIMAPServer @@ -88,61 +86,6 @@ class LocalSoledadIMAPRealm(object):          return defer.succeed(soledad) -@implementer(ICredentialsChecker) -class LocalSoledadTokenChecker(object): - -    """ -    A Credentials Checker for a LocalSoledad store. - -    It checks that: - -    1) The Local SoledadStorage has been correctly unlocked for the given -       user. This currently means that the right passphrase has been passed -       to the Local SoledadStorage. - -    2) The password passed in the credentials matches whatever token has -       been stored in the local encrypted SoledadStorage, associated to the -       Protocol that is requesting the authentication. -    """ - -    credentialInterfaces = (IUsernamePassword,) -    service = None - -    def __init__(self, soledad_sessions): -        """ -        :param soledad_sessions: a dict-like object, containing instances -                                 of a Store (soledad instances), indexed by -                                 userid. -        """ -        self._soledad_sessions = soledad_sessions - -    def requestAvatarId(self, credentials): -        if self.service is None: -            raise NotImplementedError( -                "this checker has not defined its service name") -        username, password = credentials.username, credentials.password -        d = self.checkSoledadToken(username, password, self.service) -        d.addErrback(lambda f: defer.fail(UnauthorizedLogin())) -        return d - -    def checkSoledadToken(self, username, password, service): -        soledad = self._soledad_sessions.get(username) -        if not soledad: -            return defer.fail(Exception("No soledad")) - -        def match_token(token): -            if token is None: -                raise RuntimeError('no token') -            if token == password: -                return username -            else: -                raise RuntimeError('bad token') - -        d = soledad.get_or_create_service_token(service) -        d.addCallback(match_token) -        return d - -  class IMAPTokenChecker(LocalSoledadTokenChecker):      """A credentials checker that will lookup a token for the IMAP service."""      service = 'imap' diff --git a/mail/src/leap/mail/outgoing/service.py b/mail/src/leap/mail/outgoing/service.py index 7943c12..3e14fbd 100644 --- a/mail/src/leap/mail/outgoing/service.py +++ b/mail/src/leap/mail/outgoing/service.py @@ -14,6 +14,14 @@  #  # You should have received a copy of the GNU General Public License  # along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +OutgoingMail module. + +The OutgoingMail class allows to send mail, and encrypts/signs it if needed. +""" + +import os.path  import re  from StringIO import StringIO  from copy import deepcopy @@ -35,6 +43,7 @@ from leap.common.events import emit_async, catalog  from leap.keymanager.openpgp import OpenPGPKey  from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch  from leap.mail import __version__ +from leap.mail import errors  from leap.mail.utils import validate_address  from leap.mail.rfc3156 import MultipartEncrypted  from leap.mail.rfc3156 import MultipartSigned @@ -64,9 +73,23 @@ class SSLContextFactory(ssl.ClientContextFactory):          return ctx -class OutgoingMail: +def outgoingFactory(userid, keymanager, opts): + +    cert = unicode(opts.cert) +    key = unicode(opts.key) +    hostname = str(opts.hostname) +    port = opts.port + +    if not os.path.isfile(cert): +        raise errors.ConfigurationError( +            'No valid SMTP certificate could be found for %s!' % userid) + +    return OutgoingMail(str(userid), keymanager, cert, key, hostname, port) + + +class OutgoingMail(object):      """ -    A service for handling encrypted outgoing mail. +    Sends Outgoing Mail, encrypting and signing if needed.      """      def __init__(self, from_address, keymanager, cert, key, host, port): @@ -134,9 +157,10 @@ class OutgoingMail:          :type smtp_sender_result: tuple(int, list(tuple))          """          dest_addrstr = smtp_sender_result[1][0][0] -        log.msg('Message sent to %s' % dest_addrstr) +        fromaddr = self._from_address +        log.msg('Message sent from %s to %s' % (fromaddr, dest_addrstr))          emit_async(catalog.SMTP_SEND_MESSAGE_SUCCESS, -                   self._from_address, dest_addrstr) +                   fromaddr, dest_addrstr)      def sendError(self, failure):          """ diff --git a/mail/src/leap/mail/smtp/__init__.py b/mail/src/leap/mail/smtp/__init__.py index 7b62808..9fab70a 100644 --- a/mail/src/leap/mail/smtp/__init__.py +++ b/mail/src/leap/mail/smtp/__init__.py @@ -23,47 +23,35 @@ import os  from twisted.internet import reactor  from twisted.internet.error import CannotListenError -from leap.mail.outgoing.service import OutgoingMail  from leap.common.events import emit_async, catalog +  from leap.mail.smtp.gateway import SMTPFactory  logger = logging.getLogger(__name__) -def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port, -                       smtp_cert, smtp_key, encrypted_only): -    """ -    Setup SMTP gateway to run with Twisted. +SMTP_PORT = 2013 + -    This function sets up the SMTP gateway configuration and the Twisted -    reactor. +def run_service(soledad_sessions, keymanager_sessions, sendmail_opts, +                port=SMTP_PORT): +    """ +    Main entry point to run the service from the client. -    :param port: The port in which to run the server. -    :type port: int -    :param userid: The user currently logged in -    :type userid: str -    :param keymanager: A Key Manager from where to get recipients' public -                       keys. -    :type keymanager: leap.common.keymanager.KeyManager -    :param smtp_host: The hostname of the remote SMTP server. -    :type smtp_host: str -    :param smtp_port: The port of the remote SMTP server. -    :type smtp_port: int -    :param smtp_cert: The client certificate for authentication. -    :type smtp_cert: str -    :param smtp_key: The client key for authentication. -    :type smtp_key: str -    :param encrypted_only: Whether the SMTP gateway should send unencrypted -                           mail or not. -    :type encrypted_only: bool +    :param soledad_sessions: a dict-like object, containing instances +                             of a Store (soledad instances), indexed by userid. +    :param keymanager_sessions: a dict-like object, containing instances +                                of Keymanager, indexed by userid. +    :param sendmail_opts: a dict-like object of sendmailOptions. -    :returns: tuple of SMTPFactory, twisted.internet.tcp.Port +    :returns: the port as returned by the reactor when starts listening, and +              the factory for the protocol. +    :rtype: tuple      """ -    # configure the use of this service with twistd -    outgoing_mail = OutgoingMail( -        userid, keymanager, smtp_cert, smtp_key, smtp_host, smtp_port) -    factory = SMTPFactory(userid, keymanager, encrypted_only, outgoing_mail) +    factory = SMTPFactory(soledad_sessions, keymanager_sessions, +                          sendmail_opts) +      try:          interface = "localhost"          # don't bind just to localhost if we are running on docker since we @@ -71,8 +59,10 @@ def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port,          if os.environ.get("LEAP_DOCKERIZED"):              interface = '' +        # TODO Use Endpoints instead --------------------------------          tport = reactor.listenTCP(port, factory, interface=interface)          emit_async(catalog.SMTP_SERVICE_STARTED, str(port)) +          return factory, tport      except CannotListenError:          logger.error("STMP Service failed to start: " diff --git a/mail/src/leap/mail/smtp/gateway.py b/mail/src/leap/mail/smtp/gateway.py index 3c86d7e..85b1560 100644 --- a/mail/src/leap/mail/smtp/gateway.py +++ b/mail/src/leap/mail/smtp/gateway.py @@ -30,18 +30,26 @@ The following classes comprise the SMTP gateway service:        knows how to encrypt/sign itself before sending.  """ +from email.Header import Header +  from zope.interface import implements +from zope.interface import implementer + +from twisted.cred.portal import Portal, IRealm  from twisted.mail import smtp -from twisted.internet.protocol import ServerFactory +from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials +from twisted.internet import defer, protocol  from twisted.python import log -from email.Header import Header  from leap.common.check import leap_assert_type  from leap.common.events import emit_async, catalog -from leap.keymanager.openpgp import OpenPGPKey -from leap.keymanager.errors import KeyNotFound +from leap.mail import errors +from leap.mail.cred import LocalSoledadTokenChecker  from leap.mail.utils import validate_address  from leap.mail.rfc3156 import RFC3156CompliantGenerator +from leap.mail.outgoing.service import outgoingFactory +from leap.keymanager.openpgp import OpenPGPKey +from leap.keymanager.errors import KeyNotFound  # replace email generator with a RFC 3156 compliant one.  from email import generator @@ -49,87 +57,122 @@ from email import generator  generator.Generator = RFC3156CompliantGenerator -# TODO -- implement Queue using twisted.mail.mail.MailService +LOCAL_FQDN = "bitmask.local" -# -# Helper utilities -# +@implementer(IRealm) +class LocalSMTPRealm(object): -LOCAL_FQDN = "bitmask.local" +    _encoding = 'utf-8' + +    def __init__(self, keymanager_sessions, sendmail_opts): +        """ +        :param keymanager_sessions: a dict-like object, containing instances +                                 of a Keymanager objects, indexed by +                                 userid. +        """ +        self._keymanager_sessions = keymanager_sessions +        self._sendmail_opts = sendmail_opts +    def requestAvatar(self, avatarId, mind, *interfaces): +        if isinstance(avatarId, str): +            avatarId = avatarId.decode(self._encoding) -class SMTPHeloLocalhost(smtp.SMTP): -    """ -    An SMTP class that ensures a proper FQDN -    for localhost. +        def gotKeymanager(keymanager): -    This avoids a problem in which unproperly configured providers -    would complain about the helo not being a fqdn. -    """ +            # TODO use IMessageDeliveryFactory instead ? +            # it could reuse the connections. +            if smtp.IMessageDelivery in interfaces: +                userid = avatarId +                opts = self.getSendingOpts(userid) +                outgoing = outgoingFactory(userid, keymanager, opts) +                avatar = SMTPDelivery(userid, keymanager, False, outgoing) + +                return (smtp.IMessageDelivery, avatar, +                        getattr(avatar, 'logout', lambda: None)) + +            raise NotImplementedError(self, interfaces) + +        return self.lookupKeymanagerInstance(avatarId).addCallback( +            gotKeymanager) -    def __init__(self, *args): -        smtp.SMTP.__init__(self, *args) -        self.host = LOCAL_FQDN +    def lookupKeymanagerInstance(self, userid): +        try: +            keymanager = self._keymanager_sessions[userid] +        except: +            raise errors.AuthenticationError( +                'No keymanager session found for user %s. Is it authenticated?' +                % userid) +        # XXX this should return the instance after whenReady callback +        return defer.succeed(keymanager) + +    def getSendingOpts(self, userid): +        try: +            opts = self._sendmail_opts[userid] +        except KeyError: +            raise errors.ConfigurationError( +                'No sendingMail options found for user %s' % userid) +        return opts + + +class SMTPTokenChecker(LocalSoledadTokenChecker): +    """A credentials checker that will lookup a token for the SMTP service.""" +    service = 'smtp' + +    # TODO besides checking for token credential, +    # we could also verify the certificate here. + + +# TODO -- implement Queue using twisted.mail.mail.MailService +class LocalSMTPServer(smtp.ESMTP): +    def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts, +                 *args, **kw): -class SMTPFactory(ServerFactory): +        smtp.ESMTP.__init__(self, *args, **kw) + +        realm = LocalSMTPRealm(keymanager_sessions, sendmail_opts) +        portal = Portal(realm) +        checker = SMTPTokenChecker(soledad_sessions) +        self.checker = checker +        self.portal = portal +        portal.registerChecker(checker) + + +class SMTPFactory(protocol.ServerFactory):      """      Factory for an SMTP server with encrypted gatewaying capabilities.      """ -    domain = LOCAL_FQDN - -    def __init__(self, userid, keymanager, encrypted_only, outgoing_mail): -        """ -        Initialize the SMTP factory. -        :param userid: The user currently logged in -        :type userid: unicode -        :param keymanager: A Key Manager from where to get recipients' public -                           keys. -        :param encrypted_only: Whether the SMTP gateway should send unencrypted -                               mail or not. -        :type encrypted_only: bool -        :param outgoing_mail: The outgoing mail to send the message -        :type outgoing_mail: leap.mail.outgoing.service.OutgoingMail -        """ +    protocol = LocalSMTPServer +    domain = LOCAL_FQDN +    timeout = 600 -        leap_assert_type(encrypted_only, bool) -        # and store them -        self._userid = userid -        self._km = keymanager -        self._outgoing_mail = outgoing_mail -        self._encrypted_only = encrypted_only +    def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts): +        self._soledad_sessions = soledad_sessions +        self._keymanager_sessions = keymanager_sessions +        self._sendmail_opts = sendmail_opts      def buildProtocol(self, addr): -        """ -        Return a protocol suitable for the job. - -        :param addr: An address, e.g. a TCP (host, port). -        :type addr:  twisted.internet.interfaces.IAddress - -        @return: The protocol. -        @rtype: SMTPDelivery -        """ -        smtpProtocol = SMTPHeloLocalhost( -            SMTPDelivery( -                self._userid, self._km, self._encrypted_only, -                self._outgoing_mail)) -        smtpProtocol.factory = self -        return smtpProtocol +        p = self.protocol( +            self._soledad_sessions, self._keymanager_sessions, +            self._sendmail_opts) +        p.factory = self +        p.host = LOCAL_FQDN +        p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials} +        return p  #  # SMTPDelivery  # +@implementer(smtp.IMessageDelivery)  class SMTPDelivery(object):      """      Validate email addresses and handle message delivery.      """ -    implements(smtp.IMessageDelivery) -      def __init__(self, userid, keymanager, encrypted_only, outgoing_mail):          """          Initialize the SMTP delivery object. | 
