diff options
author | Tomás Touceda <chiiph@leap.se> | 2013-11-11 12:07:53 -0300 |
---|---|---|
committer | Tomás Touceda <chiiph@leap.se> | 2013-11-11 12:07:53 -0300 |
commit | 26832fd082dab729f3e0c1cca0b3736a960f4d0f (patch) | |
tree | 34e43795074dfe1c9b1965ffafd2e164709fc0bf | |
parent | d4f35a4fbf3dc9fccd1a0bcb9f21144612ff1af3 (diff) | |
parent | 8d1018c2e6636349a12aeb1f9de595dabfed8096 (diff) |
Merge remote-tracking branch 'drebs/feature/4324_prevent-double-encryption-when-relaying-2' into develop
-rw-r--r-- | mail/changes/bug_4416-change-smtp-relay-to-gateway | 1 | ||||
-rw-r--r-- | mail/changes/feature-4324_prevent-double-encryption-when-relaying | 2 | ||||
-rw-r--r-- | mail/src/leap/mail/imap/tests/test_imap.py | 4 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/README.rst | 38 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/__init__.py | 47 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/gateway.py (renamed from mail/src/leap/mail/smtp/smtprelay.py) | 328 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/tests/__init__.py | 6 | ||||
-rw-r--r-- | mail/src/leap/mail/smtp/tests/test_gateway.py (renamed from mail/src/leap/mail/smtp/tests/test_smtprelay.py) | 72 |
8 files changed, 233 insertions, 265 deletions
diff --git a/mail/changes/bug_4416-change-smtp-relay-to-gateway b/mail/changes/bug_4416-change-smtp-relay-to-gateway new file mode 100644 index 0000000..08bead7 --- /dev/null +++ b/mail/changes/bug_4416-change-smtp-relay-to-gateway @@ -0,0 +1 @@ + o Change SMTP service name from "relay" to "gateway". Closes #4416. diff --git a/mail/changes/feature-4324_prevent-double-encryption-when-relaying b/mail/changes/feature-4324_prevent-double-encryption-when-relaying new file mode 100644 index 0000000..a3d70a9 --- /dev/null +++ b/mail/changes/feature-4324_prevent-double-encryption-when-relaying @@ -0,0 +1,2 @@ + o Prevent already encrypted outgoing messages from being encrypted again. + Closes #4324. diff --git a/mail/src/leap/mail/imap/tests/test_imap.py b/mail/src/leap/mail/imap/tests/test_imap.py index 3411795..ad11315 100644 --- a/mail/src/leap/mail/imap/tests/test_imap.py +++ b/mail/src/leap/mail/imap/tests/test_imap.py @@ -91,7 +91,7 @@ def initialize_soledad(email, gnupg_home, tempdir): """ uuid = "foobar-uuid" - passphrase = "verysecretpassphrase" + passphrase = u"verysecretpassphrase" secret_path = os.path.join(tempdir, "secret.gpg") local_db_path = os.path.join(tempdir, "soledad.u1db") server_url = "http://provider" @@ -101,6 +101,8 @@ def initialize_soledad(email, gnupg_home, tempdir): get_doc = Mock(return_value=None) put_doc = Mock() + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) def __call__(self): return self diff --git a/mail/src/leap/mail/smtp/README.rst b/mail/src/leap/mail/smtp/README.rst index 2b2a118..f625441 100644 --- a/mail/src/leap/mail/smtp/README.rst +++ b/mail/src/leap/mail/smtp/README.rst @@ -1,43 +1,23 @@ -Leap SMTP Relay -=============== +Leap SMTP Gateway +================= Outgoing mail workflow: * LEAP client runs a thin SMTP proxy on the user's device, bound to localhost. - * User's MUA is configured outgoing SMTP to localhost - * When SMTP proxy receives an email from MUA + * User's MUA is configured outgoing SMTP to localhost. + * When SMTP proxy receives an email from MUA: * SMTP proxy queries Key Manager for the user's private key and public - keys of all recipients + keys of all recipients. * Message is signed by sender and encrypted to recipients. * If recipient's key is missing, email goes out in cleartext (unless - user has configured option to send only encrypted email) - * Finally, message is relayed to provider's SMTP relay - - -Dependencies ------------- - -Leap SMTP Relay depends on the following python libraries: - - * Twisted 12.3.0 [1] - * zope.interface 4.0.3 [2] - -[1] http://pypi.python.org/pypi/Twisted/12.3.0 -[2] http://pypi.python.org/pypi/zope.interface/4.0.3 - - -How to run ----------- - -To launch the SMTP relay, run the following command: - - twistd -y smtprelay.tac + user has configured option to send only encrypted email). + * Finally, message is gatewayed to provider's SMTP server. Running tests ------------- -Tests are run using Twisted's Trial API, like this: +Tests are run using Twisted's Trial API, like this:: - trial leap.email.smtp.tests + python setup.py test -s leap.mail.gateway.tests diff --git a/mail/src/leap/mail/smtp/__init__.py b/mail/src/leap/mail/smtp/__init__.py index be568b8..d3eb9e8 100644 --- a/mail/src/leap/mail/smtp/__init__.py +++ b/mail/src/leap/mail/smtp/__init__.py @@ -16,7 +16,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -SMTP relay helper function. +SMTP gateway helper function. """ import logging @@ -26,15 +26,15 @@ from twisted.internet.error import CannotListenError logger = logging.getLogger(__name__) from leap.common.events import proto, signal -from leap.mail.smtp.smtprelay import SMTPFactory +from leap.mail.smtp.gateway import SMTPFactory -def setup_smtp_relay(port, userid, keymanager, smtp_host, smtp_port, +def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port, smtp_cert, smtp_key, encrypted_only): """ - Setup SMTP relay to run with Twisted. + Setup SMTP gateway to run with Twisted. - This function sets up the SMTP relay configuration and the Twisted + This function sets up the SMTP gateway configuration and the Twisted reactor. :param port: The port in which to run the server. @@ -46,48 +46,29 @@ def setup_smtp_relay(port, userid, keymanager, smtp_host, smtp_port, :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. + :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 relay should send unencrypted mail + :param encrypted_only: Whether the SMTP gateway should send unencrypted mail or not. :type encrypted_only: bool :returns: tuple of SMTPFactory, twisted.internet.tcp.Port """ - # The configuration for the SMTP relay is a dict with the following - # format: - # - # { - # 'host': '<host>', - # 'port': <int>, - # 'cert': '<cert path>', - # 'key': '<key path>', - # 'encrypted_only': <True/False> - # } - config = { - 'host': smtp_host, - 'port': smtp_port, - 'cert': smtp_cert, - 'key': smtp_key, - 'encrypted_only': encrypted_only - } - # configure the use of this service with twistd - factory = SMTPFactory(userid, keymanager, config) + factory = SMTPFactory(userid, keymanager, smtp_host, smtp_port, smtp_cert, + smtp_key, encrypted_only) try: - tport = reactor.listenTCP(port, factory, - interface="localhost") - signal(proto.SMTP_SERVICE_STARTED, str(smtp_port)) + tport = reactor.listenTCP(port, factory, interface="localhost") + signal(proto.SMTP_SERVICE_STARTED, str(port)) return factory, tport except CannotListenError: logger.error("STMP Service failed to start: " - "cannot listen in port %s" % ( - smtp_port,)) - signal(proto.SMTP_SERVICE_FAILED_TO_START, str(smtp_port)) + "cannot listen in port %s" % port) + signal(proto.SMTP_SERVICE_FAILED_TO_START, str(port)) except Exception as exc: - logger.error("Unhandled error while launching smtp relay service") + logger.error("Unhandled error while launching smtp gateway service") logger.exception(exc) diff --git a/mail/src/leap/mail/smtp/smtprelay.py b/mail/src/leap/mail/smtp/gateway.py index 92a9f0e..06405b4 100644 --- a/mail/src/leap/mail/smtp/smtprelay.py +++ b/mail/src/leap/mail/smtp/gateway.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# smtprelay.py +# gateway.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify @@ -16,9 +16,9 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -LEAP SMTP encrypted relay. +LEAP SMTP encrypted gateway. -The following classes comprise the SMTP relay service: +The following classes comprise the SMTP gateway service: * SMTPFactory - A twisted.internet.protocol.ServerFactory that provides the SMTPDelivery protocol. @@ -68,67 +68,15 @@ generator.Generator = RFC3156CompliantGenerator # -# Exceptions -# - -class MalformedConfig(Exception): - """ - Raised when the configuration dictionary passed as parameter is malformed. - """ - pass - - -# # Helper utilities # -HOST_KEY = 'host' -PORT_KEY = 'port' -CERT_KEY = 'cert' -KEY_KEY = 'key' -ENCRYPTED_ONLY_KEY = 'encrypted_only' - - -def assert_config_structure(config): - """ - Assert that C{config} is a dict with the following structure: - - { - HOST_KEY: '<str>', - PORT_KEY: <int>, - CERT_KEY: '<str>', - KEY_KEY: '<str>', - ENCRYPTED_ONLY_KEY: <bool>, - } - - @param config: The dictionary to check. - @type config: dict - """ - # assert smtp config structure is valid - leap_assert_type(config, dict) - leap_assert(HOST_KEY in config) - leap_assert_type(config[HOST_KEY], str) - leap_assert(PORT_KEY in config) - leap_assert_type(config[PORT_KEY], int) - leap_assert(CERT_KEY in config) - leap_assert_type(config[CERT_KEY], (str, unicode)) - leap_assert(KEY_KEY in config) - leap_assert_type(config[KEY_KEY], (str, unicode)) - leap_assert(ENCRYPTED_ONLY_KEY in config) - leap_assert_type(config[ENCRYPTED_ONLY_KEY], bool) - # assert received params are not empty - leap_assert(config[HOST_KEY] != '') - leap_assert(config[PORT_KEY] is not 0) - leap_assert(config[CERT_KEY] != '') - leap_assert(config[KEY_KEY] != '') - - def validate_address(address): """ Validate C{address} as defined in RFC 2822. - @param address: The address to be validated. - @type address: str + :param address: The address to be validated. + :type address: str @return: A valid address. @rtype: str @@ -150,48 +98,63 @@ def validate_address(address): class SMTPFactory(ServerFactory): """ - Factory for an SMTP server with encrypted relaying capabilities. + Factory for an SMTP server with encrypted gatewaying capabilities. """ - def __init__(self, userid, keymanager, config): + def __init__(self, userid, keymanager, host, port, cert, key, + encrypted_only): """ Initialize the SMTP factory. - @param keymanager: A KeyManager for retrieving recipient's keys. - @type keymanager: leap.common.keymanager.KeyManager - @param config: A dictionary with smtp configuration. Should have - the following structure: - { - HOST_KEY: '<str>', - PORT_KEY: <int>, - CERT_KEY: '<str>', - KEY_KEY: '<str>', - ENCRYPTED_ONLY_KEY: <bool>, - } - @type config: dict - @param userid: The user currently logged in - @type userid: unicode + :param userid: The user currently logged in + :type userid: unicode + :param keymanager: A KeyManager for retrieving recipient's keys. + :type keymanager: leap.common.keymanager.KeyManager + :param host: The hostname of the remote SMTP server. + :type host: str + :param port: The port of the remote SMTP server. + :type port: int + :param cert: The client certificate for authentication. + :type cert: str + :param key: The client key for authentication. + :type key: str + :param encrypted_only: Whether the SMTP gateway should send unencrypted + mail or not. + :type encrypted_only: bool """ # assert params leap_assert_type(keymanager, KeyManager) - assert_config_structure(config) + leap_assert_type(host, str) + leap_assert(host != '') + leap_assert_type(port, int) + leap_assert(port is not 0) + leap_assert_type(cert, unicode) + leap_assert(cert != '') + leap_assert_type(key, unicode) + leap_assert(key != '') + leap_assert_type(encrypted_only, bool) # and store them self._userid = userid self._km = keymanager - self._config = config + self._host = host + self._port = port + self._cert = cert + self._key = key + self._encrypted_only = encrypted_only 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 + :param addr: An address, e.g. a TCP (host, port). + :type addr: twisted.internet.interfaces.IAddress @return: The protocol. @rtype: SMTPDelivery """ - smtpProtocol = smtp.SMTP(SMTPDelivery(self._userid, self._km, - self._config)) + smtpProtocol = smtp.SMTP(SMTPDelivery( + self._userid, self._km, self._host, self._port, self._cert, + self._key, self._encrypted_only)) smtpProtocol.factory = self return smtpProtocol @@ -207,49 +170,53 @@ class SMTPDelivery(object): implements(smtp.IMessageDelivery) - def __init__(self, userid, keymanager, config): + def __init__(self, userid, keymanager, host, port, cert, key, + encrypted_only): """ Initialize the SMTP delivery object. - @param keymanager: A KeyManager for retrieving recipient's keys. - @type keymanager: leap.common.keymanager.KeyManager - @param config: A dictionary with smtp configuration. Should have - the following structure: - { - HOST_KEY: '<str>', - PORT_KEY: <int>, - CERT_KEY: '<str>', - KEY_KEY: '<str>', - ENCRYPTED_ONLY_KEY: <bool>, - } - @type config: dict - @param userid: The user currently logged in - @type userid: unicode + :param userid: The user currently logged in + :type userid: unicode + :param keymanager: A KeyManager for retrieving recipient's keys. + :type keymanager: leap.common.keymanager.KeyManager + :param host: The hostname of the remote SMTP server. + :type host: str + :param port: The port of the remote SMTP server. + :type port: int + :param cert: The client certificate for authentication. + :type cert: str + :param key: The client key for authentication. + :type key: str + :param encrypted_only: Whether the SMTP gateway should send unencrypted + mail or not. + :type encrypted_only: bool """ - # assert params - leap_assert_type(keymanager, KeyManager) - assert_config_structure(config) - # and store them self._userid = userid self._km = keymanager - self._config = config + self._userid = userid + self._km = keymanager + self._host = host + self._port = port + self._cert = cert + self._key = key + self._encrypted_only = encrypted_only self._origin = None def receivedHeader(self, helo, origin, recipients): """ Generate the 'Received:' header for a message. - @param helo: The argument to the HELO command and the client's IP + :param helo: The argument to the HELO command and the client's IP address. - @type helo: (str, str) - @param origin: The address the message is from. - @type origin: twisted.mail.smtp.Address - @param recipients: A list of the addresses for which this message is + :type helo: (str, str) + :param origin: The address the message is from. + :type origin: twisted.mail.smtp.Address + :param recipients: A list of the addresses for which this message is bound. - @type: list of twisted.mail.smtp.User + :type: list of twisted.mail.smtp.User @return: The full "Received" header string. - @type: str + :type: str """ myHostname, clientIP = helo headerValue = "by %s from %s with ESMTP ; %s" % ( @@ -269,8 +236,8 @@ class SMTPDelivery(object): In the end, it returns an encrypted message object that is able to send itself to the C{user}'s address. - @param user: The user whose address we wish to validate. - @type: twisted.mail.smtp.User + :param user: The user whose address we wish to validate. + :type: twisted.mail.smtp.User @return: A Deferred which becomes, or a callable which takes no arguments and returns an object implementing IMessage. This will @@ -284,12 +251,13 @@ class SMTPDelivery(object): # try to find recipient's public key try: address = validate_address(user.dest.addrstr) - pubkey = self._km.get_key(address, OpenPGPKey) + # verify if recipient key is available in keyring + self._km.get_key(address, OpenPGPKey) # might raise KeyNotFound log.msg("Accepting mail for %s..." % user.dest.addrstr) signal(proto.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, user.dest.addrstr) except KeyNotFound: # if key was not found, check config to see if will send anyway. - if self._config[ENCRYPTED_ONLY_KEY]: + if self._encrypted_only: signal(proto.SMTP_RECIPIENT_REJECTED, user.dest.addrstr) raise smtp.SMTPBadRcpt(user.dest.addrstr) log.msg("Warning: will send an unencrypted message (because " @@ -297,17 +265,18 @@ class SMTPDelivery(object): signal( proto.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED, user.dest.addrstr) return lambda: EncryptedMessage( - self._origin, user, self._km, self._config) + self._origin, user, self._km, self._host, self._port, self._cert, + self._key) def validateFrom(self, helo, origin): """ Validate the address from which the message originates. - @param helo: The argument to the HELO command and the client's IP + :param helo: The argument to the HELO command and the client's IP address. - @type: (str, str) - @param origin: The address the message is from. - @type origin: twisted.mail.smtp.Address + :type: (str, str) + :param origin: The address the message is from. + :type origin: twisted.mail.smtp.Address @return: origin or a Deferred whose callback will be passed origin. @rtype: Deferred or Address @@ -359,36 +328,36 @@ class EncryptedMessage(object): """ implements(smtp.IMessage) - def __init__(self, fromAddress, user, keymanager, config): + def __init__(self, fromAddress, user, keymanager, host, port, cert, key): """ Initialize the encrypted message. - @param fromAddress: The address of the sender. - @type fromAddress: twisted.mail.smtp.Address - @param user: The recipient of this message. - @type user: twisted.mail.smtp.User - @param keymanager: A KeyManager for retrieving recipient's keys. - @type keymanager: leap.common.keymanager.KeyManager - @param config: A dictionary with smtp configuration. Should have - the following structure: - { - HOST_KEY: '<str>', - PORT_KEY: <int>, - CERT_KEY: '<str>', - KEY_KEY: '<str>', - ENCRYPTED_ONLY_KEY: <bool>, - } - @type config: dict + :param fromAddress: The address of the sender. + :type fromAddress: twisted.mail.smtp.Address + :param user: The recipient of this message. + :type user: twisted.mail.smtp.User + :param keymanager: A KeyManager for retrieving recipient's keys. + :type keymanager: leap.common.keymanager.KeyManager + :param host: The hostname of the remote SMTP server. + :type host: str + :param port: The port of the remote SMTP server. + :type port: int + :param cert: The client certificate for authentication. + :type cert: str + :param key: The client key for authentication. + :type key: str """ # assert params leap_assert_type(user, smtp.User) leap_assert_type(keymanager, KeyManager) - assert_config_structure(config) # and store them self._fromAddress = fromAddress self._user = user self._km = keymanager - self._config = config + self._host = host + self._port = port + self._cert = cert + self._key = key # initialize list for message's lines self.lines = [] @@ -400,8 +369,8 @@ class EncryptedMessage(object): """ Handle another line. - @param line: The received line. - @type line: str + :param line: The received line. + :type line: str """ self.lines.append(line) @@ -440,8 +409,8 @@ class EncryptedMessage(object): """ Callback for a successful send. - @param r: The result from the last previous callback in the chain. - @type r: anything + :param r: The result from the last previous callback in the chain. + :type r: anything """ log.msg(r) signal(proto.SMTP_SEND_MESSAGE_SUCCESS, self._user.dest.addrstr) @@ -450,8 +419,8 @@ class EncryptedMessage(object): """ Callback for an unsuccessfull send. - @param e: The result from the last errback. - @type e: anything + :param e: The result from the last errback. + :type e: anything """ log.msg(e) log.err() @@ -470,8 +439,7 @@ class EncryptedMessage(object): """ msg = self._msg.as_string(False) - log.msg("Connecting to SMTP server %s:%s" % (self._config[HOST_KEY], - self._config[PORT_KEY])) + log.msg("Connecting to SMTP server %s:%s" % (self._host, self._port)) d = defer.Deferred() # we don't pass an ssl context factory to the ESMTPSenderFactory @@ -487,11 +455,8 @@ class EncryptedMessage(object): requireTransportSecurity=True) signal(proto.SMTP_SEND_MESSAGE_START, self._user.dest.addrstr) reactor.connectSSL( - self._config[HOST_KEY], - self._config[PORT_KEY], - factory, - contextFactory=SSLContextFactory(self._config[CERT_KEY], - self._config[KEY_KEY])) + self._host, self._port, factory, + contextFactory=SSLContextFactory(self._cert, self._key)) d.addCallback(self.sendSuccess) d.addErrback(self.sendError) return d @@ -505,20 +470,18 @@ class EncryptedMessage(object): Create an RFC 3156 compliang PGP encrypted and signed message using C{pubkey} to encrypt and C{signkey} to sign. - @param pubkey: The public key used to encrypt the message. - @type pubkey: leap.common.keymanager.openpgp.OpenPGPKey - @param signkey: The private key used to sign the message. - @type signkey: leap.common.keymanager.openpgp.OpenPGPKey + :param pubkey: The public key used to encrypt the message. + :type pubkey: leap.common.keymanager.openpgp.OpenPGPKey + :param signkey: The private key used to sign the message. + :type signkey: leap.common.keymanager.openpgp.OpenPGPKey """ - # parse original message from received lines - origmsg = self.parseMessage() # create new multipart/encrypted message with 'pgp-encrypted' protocol newmsg = MultipartEncrypted('application/pgp-encrypted') # move (almost) all headers from original message to the new message - move_headers(origmsg, newmsg) + move_headers(self._origmsg, newmsg) # create 'application/octet-stream' encrypted message encmsg = MIMEApplication( - self._km.encrypt(origmsg.as_string(unixfrom=False), pubkey, + self._km.encrypt(self._origmsg.as_string(unixfrom=False), pubkey, sign=signkey), _subtype='octet-stream', _encoder=lambda x: x) encmsg.add_header('content-disposition', 'attachment', @@ -535,25 +498,23 @@ class EncryptedMessage(object): """ Create an RFC 3156 compliant PGP signed MIME message using C{signkey}. - @param signkey: The private key used to sign the message. - @type signkey: leap.common.keymanager.openpgp.OpenPGPKey + :param signkey: The private key used to sign the message. + :type signkey: leap.common.keymanager.openpgp.OpenPGPKey """ - # parse original message from received lines - origmsg = self.parseMessage() # create new multipart/signed message newmsg = MultipartSigned('application/pgp-signature', 'pgp-sha512') # move (almost) all headers from original message to the new message - move_headers(origmsg, newmsg) + move_headers(self._origmsg, newmsg) # apply base64 content-transfer-encoding - encode_base64_rec(origmsg) + encode_base64_rec(self._origmsg) # get message text with headers and replace \n for \r\n fp = StringIO() g = RFC3156CompliantGenerator( fp, mangle_from_=False, maxheaderlen=76) - g.flatten(origmsg) + g.flatten(self._origmsg) msgtext = re.sub('\r?\n', '\r\n', fp.getvalue()) # make sure signed message ends with \r\n as per OpenPGP stantard. - if origmsg.is_multipart(): + if self._origmsg.is_multipart(): if not msgtext.endswith("\r\n"): msgtext += "\r\n" # calculate signature @@ -561,23 +522,46 @@ class EncryptedMessage(object): clearsign=False, detach=True, binary=False) sigmsg = PGPSignature(signature) # attach original message and signature to new message - newmsg.attach(origmsg) + newmsg.attach(self._origmsg) newmsg.attach(sigmsg) self._msg = newmsg def _maybe_encrypt_and_sign(self): """ - Encrypt the message body. + Attempt to encrypt and sign the outgoing message. - Fetch the recipient key and encrypt the content to the - recipient. If a key is not found, then the behaviour depends on the - configuration parameter ENCRYPTED_ONLY_KEY. If it is False, the message - is sent unencrypted and a warning is logged. If it is True, the - encryption fails with a KeyNotFound exception. + The behaviour of this method depends on: - @raise KeyNotFound: Raised when the recipient key was not found and - the ENCRYPTED_ONLY_KEY configuration parameter is set to True. + 1. the original message's content-type, and + 2. the availability of the recipient's public key. + + If the original message's content-type is "multipart/encrypted", then + the original message is not altered. For any other content-type, the + method attempts to fetch the recipient's public key. If the + recipient's public key is available, the message is encrypted and + signed; otherwise it is only signed. + + Note that, if the C{encrypted_only} configuration is set to True and + the recipient's public key is not available, then the recipient + address would have been rejected in SMTPDelivery.validateTo(). + + The following table summarizes the overall behaviour of the gateway: + + +---------------------------------------------------+----------------+ + | content-type | rcpt pubkey | enforce encr. | action | + +---------------------+-------------+---------------+----------------+ + | multipart/encrypted | any | any | pass | + | other | available | any | encrypt + sign | + | other | unavailable | yes | reject | + | other | unavailable | no | sign | + +---------------------+-------------+---------------+----------------+ """ + # pass if the original message's content-type is "multipart/encrypted" + self._origmsg = self.parseMessage() + if self._origmsg.get_content_type() == 'multipart/encrypted': + self._msg = self._origmsg + return + from_address = validate_address(self._fromAddress.addrstr) signkey = self._km.get_key(from_address, OpenPGPKey, private=True) log.msg("Will sign the message with %s." % signkey.fingerprint) diff --git a/mail/src/leap/mail/smtp/tests/__init__.py b/mail/src/leap/mail/smtp/tests/__init__.py index 9b54de3..62b015f 100644 --- a/mail/src/leap/mail/smtp/tests/__init__.py +++ b/mail/src/leap/mail/smtp/tests/__init__.py @@ -17,7 +17,7 @@ """ -Base classes and keys for SMTP relay tests. +Base classes and keys for SMTP gateway tests. """ import os @@ -59,7 +59,7 @@ class TestCaseWithKeyManager(BaseLeapTest): # setup our own stuff address = 'leap@leap.se' # user's address in the form user@provider uuid = 'leap@leap.se' - passphrase = '123' + passphrase = u'123' secrets_path = os.path.join(self.tempdir, 'secret.gpg') local_db_path = os.path.join(self.tempdir, 'soledad.u1db') server_url = 'http://provider/' @@ -88,6 +88,8 @@ class TestCaseWithKeyManager(BaseLeapTest): get_doc = Mock(return_value=None) put_doc = Mock(side_effect=_put_doc_side_effect) + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) def __call__(self): return self diff --git a/mail/src/leap/mail/smtp/tests/test_smtprelay.py b/mail/src/leap/mail/smtp/tests/test_gateway.py index 7fefe77..f9ea027 100644 --- a/mail/src/leap/mail/smtp/tests/test_smtprelay.py +++ b/mail/src/leap/mail/smtp/tests/test_gateway.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# test_smtprelay.py +# test_gateway.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify @@ -17,7 +17,7 @@ """ -SMTP relay tests. +SMTP gateway tests. """ @@ -33,7 +33,7 @@ from twisted.mail.smtp import ( ) from mock import Mock -from leap.mail.smtp.smtprelay import ( +from leap.mail.smtp.gateway import ( SMTPFactory, EncryptedMessage, ) @@ -52,9 +52,9 @@ HOSTNAME_REGEX = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" + \ IP_OR_HOST_REGEX = '(' + IP_REGEX + '|' + HOSTNAME_REGEX + ')' -class TestSmtpRelay(TestCaseWithKeyManager): +class TestSmtpGateway(TestCaseWithKeyManager): - EMAIL_DATA = ['HELO relay.leap.se', + EMAIL_DATA = ['HELO gateway.leap.se', 'MAIL FROM: <%s>' % ADDRESS_2, 'RCPT TO: <%s>' % ADDRESS, 'DATA', @@ -90,7 +90,7 @@ class TestSmtpRelay(TestCaseWithKeyManager): self.assertEqual(text, decrypted, "Decrypted text differs from plaintext.") - def test_relay_accepts_valid_email(self): + def test_gateway_accepts_valid_email(self): """ Test if SMTP server responds correctly for valid interaction. """ @@ -102,26 +102,32 @@ class TestSmtpRelay(TestCaseWithKeyManager): '250 Sender address accepted', '250 Recipient address accepted', '354 Continue'] - proto = SMTPFactory( - self._km, self._config).buildProtocol(('127.0.0.1', 0)) + proto = SMTPFactory(u'anotheruser@leap.se', + self._km, self._config['host'], self._config['port'], + self._config['cert'], self._config['key'], + self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0)) transport = proto_helpers.StringTransport() proto.makeConnection(transport) for i, line in enumerate(self.EMAIL_DATA): proto.lineReceived(line + '\r\n') self.assertMatch(transport.value(), '\r\n'.join(SMTP_ANSWERS[0:i + 1]), - 'Did not get expected answer from relay.') + 'Did not get expected answer from gateway.') proto.setTimeout(None) def test_message_encrypt(self): """ Test if message gets encrypted to destination email. """ - proto = SMTPFactory( - self._km, self._config).buildProtocol(('127.0.0.1', 0)) + proto = SMTPFactory(u'anotheruser@leap.se', + self._km, self._config['host'], self._config['port'], + self._config['cert'], self._config['key'], + self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0)) fromAddr = Address(ADDRESS_2) - dest = User(ADDRESS, 'relay.leap.se', proto, ADDRESS) - m = EncryptedMessage(fromAddr, dest, self._km, self._config) + dest = User(ADDRESS, 'gateway.leap.se', proto, ADDRESS) + m = EncryptedMessage( + fromAddr, dest, self._km, self._config['host'], + self._config['port'], self._config['cert'], self._config['key']) for line in self.EMAIL_DATA[4:12]: m.lineReceived(line) m.eomReceived() @@ -149,11 +155,15 @@ class TestSmtpRelay(TestCaseWithKeyManager): Test if message gets encrypted to destination email and signed with sender key. """ - proto = SMTPFactory( - self._km, self._config).buildProtocol(('127.0.0.1', 0)) - user = User(ADDRESS, 'relay.leap.se', proto, ADDRESS) + proto = SMTPFactory(u'anotheruser@leap.se', + self._km, self._config['host'], self._config['port'], + self._config['cert'], self._config['key'], + self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0)) + user = User(ADDRESS, 'gateway.leap.se', proto, ADDRESS) fromAddr = Address(ADDRESS_2) - m = EncryptedMessage(fromAddr, user, self._km, self._config) + m = EncryptedMessage( + fromAddr, user, self._km, self._config['host'], + self._config['port'], self._config['cert'], self._config['key']) for line in self.EMAIL_DATA[4:12]: m.lineReceived(line) # trigger encryption and signing @@ -185,11 +195,15 @@ class TestSmtpRelay(TestCaseWithKeyManager): """ # mock the key fetching self._km.fetch_keys_from_server = Mock(return_value=[]) - proto = SMTPFactory( - self._km, self._config).buildProtocol(('127.0.0.1', 0)) - user = User('ihavenopubkey@nonleap.se', 'relay.leap.se', proto, ADDRESS) + proto = SMTPFactory(u'anotheruser@leap.se', + self._km, self._config['host'], self._config['port'], + self._config['cert'], self._config['key'], + self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0)) + user = User('ihavenopubkey@nonleap.se', 'gateway.leap.se', proto, ADDRESS) fromAddr = Address(ADDRESS_2) - m = EncryptedMessage(fromAddr, user, self._km, self._config) + m = EncryptedMessage( + fromAddr, user, self._km, self._config['host'], + self._config['port'], self._config['cert'], self._config['key']) for line in self.EMAIL_DATA[4:12]: m.lineReceived(line) # trigger signing @@ -237,8 +251,10 @@ class TestSmtpRelay(TestCaseWithKeyManager): # mock the key fetching self._km.fetch_keys_from_server = Mock(return_value=[]) # prepare the SMTP factory - proto = SMTPFactory( - self._km, self._config).buildProtocol(('127.0.0.1', 0)) + proto = SMTPFactory(u'anotheruser@leap.se', + self._km, self._config['host'], self._config['port'], + self._config['cert'], self._config['key'], + self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0)) transport = proto_helpers.StringTransport() proto.makeConnection(transport) proto.lineReceived(self.EMAIL_DATA[0] + '\r\n') @@ -263,11 +279,11 @@ class TestSmtpRelay(TestCaseWithKeyManager): pgp.delete_key(pubkey) # mock the key fetching self._km.fetch_keys_from_server = Mock(return_value=[]) - # change the configuration - self._config['encrypted_only'] = False - # prepare the SMTP factory - proto = SMTPFactory( - self._km, self._config).buildProtocol(('127.0.0.1', 0)) + # prepare the SMTP factory with encrypted only equal to false + proto = SMTPFactory(u'anotheruser@leap.se', + self._km, self._config['host'], self._config['port'], + self._config['cert'], self._config['key'], + False).buildProtocol(('127.0.0.1', 0)) transport = proto_helpers.StringTransport() proto.makeConnection(transport) proto.lineReceived(self.EMAIL_DATA[0] + '\r\n') |