diff options
Diffstat (limited to 'src/leap/mail')
-rw-r--r-- | src/leap/mail/__init__.py | 9 | ||||
-rw-r--r-- | src/leap/mail/load_tests.py | 32 | ||||
-rw-r--r-- | src/leap/mail/smtp/smtprelay.py | 31 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/__init__.py | 4 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/cert/server.crt | 29 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/cert/server.key | 51 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/test_smtprelay.py | 59 |
7 files changed, 183 insertions, 32 deletions
diff --git a/src/leap/mail/__init__.py b/src/leap/mail/__init__.py index 5b5ba9b..4b25fe6 100644 --- a/src/leap/mail/__init__.py +++ b/src/leap/mail/__init__.py @@ -17,17 +17,10 @@ """ -Provide function for loading tests. +Client mail bits. """ -# Do not force the unittest dependency -# import unittest - - -# def load_tests(): -# return unittest.defaultTestLoader.discover('./src/leap/mail') - from ._version import get_versions __version__ = get_versions()['version'] del get_versions diff --git a/src/leap/mail/load_tests.py b/src/leap/mail/load_tests.py new file mode 100644 index 0000000..ee89fcc --- /dev/null +++ b/src/leap/mail/load_tests.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# tests.py +# Copyright (C) 2013 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/>. + + +""" +Provide a function for loading tests. +""" + +import unittest + + +def load_tests(): + suite = unittest.TestSuite() + for test in unittest.defaultTestLoader.discover( + './src/leap/mail/', + top_level_dir='./src/'): + suite.addTest(test) + return suite diff --git a/src/leap/mail/smtp/smtprelay.py b/src/leap/mail/smtp/smtprelay.py index d9bbbf9..fca66c0 100644 --- a/src/leap/mail/smtp/smtprelay.py +++ b/src/leap/mail/smtp/smtprelay.py @@ -17,6 +17,20 @@ """ LEAP SMTP encrypted relay. + +The following classes comprise the SMTP relay service: + + * SMTPFactory - A twisted.internet.protocol.ServerFactory that provides + the SMTPDelivery protocol. + * SMTPDelivery - A twisted.mail.smtp.IMessageDelivery implementation. It + knows how to validate sender and receiver of messages and it generates + an EncryptedMessage for each recipient. + * SSLContextFactory - Contains the relevant ssl information for the + connection. + * EncryptedMessage - An implementation of twisted.mail.smtp.IMessage that + knows how to encrypt/sign itself before sending. + + """ import re @@ -173,7 +187,6 @@ class SMTPFactory(ServerFactory): @return: The protocol. @rtype: SMTPDelivery """ - # If needed, we might use ESMTPDelivery here instead. smtpProtocol = smtp.SMTP(SMTPDelivery(self._km, self._config)) smtpProtocol.factory = self return smtpProtocol @@ -305,7 +318,7 @@ class SMTPDelivery(object): # EncryptedMessage # -class CtxFactory(ssl.ClientContextFactory): +class SSLContextFactory(ssl.ClientContextFactory): def __init__(self, cert, key): self.cert = cert self.key = key @@ -450,6 +463,8 @@ class EncryptedMessage(object): self._config[PORT_KEY])) d = defer.Deferred() + # we don't pass an ssl context factory to the ESMTPSenderFactory + # because ssl will be handled by reactor.connectSSL() below. factory = smtp.ESMTPSenderFactory( "", # username is blank because server does not use auth. "", # password is blank because server does not use auth. @@ -457,15 +472,15 @@ class EncryptedMessage(object): self._user.dest.addrstr, StringIO(msg), d, - contextFactory=CtxFactory(self._config[CERT_KEY], - self._config[KEY_KEY]), - requireAuthentication=False) + requireAuthentication=False, + requireTransportSecurity=True) signal(proto.SMTP_SEND_MESSAGE_START, self._user.dest.addrstr) - reactor.connectTCP( + reactor.connectSSL( self._config[HOST_KEY], self._config[PORT_KEY], - factory - ) + factory, + contextFactory=SSLContextFactory(self._config[CERT_KEY], + self._config[KEY_KEY])) d.addCallback(self.sendSuccess) d.addErrback(self.sendError) return d diff --git a/src/leap/mail/smtp/tests/__init__.py b/src/leap/mail/smtp/tests/__init__.py index 7fed7da..9b54de3 100644 --- a/src/leap/mail/smtp/tests/__init__.py +++ b/src/leap/mail/smtp/tests/__init__.py @@ -113,8 +113,8 @@ class TestCaseWithKeyManager(BaseLeapTest): 'username': address, 'password': '<password>', 'encrypted_only': True, - 'cert': 'blah', - 'key': 'bleh', + 'cert': 'src/leap/mail/smtp/tests/cert/server.crt', + 'key': 'src/leap/mail/smtp/tests/cert/server.key', } class Response(object): diff --git a/src/leap/mail/smtp/tests/cert/server.crt b/src/leap/mail/smtp/tests/cert/server.crt new file mode 100644 index 0000000..a27391c --- /dev/null +++ b/src/leap/mail/smtp/tests/cert/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFBjCCAu4CCQCWn3oMoQrDJTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTEzMTAyMzE0NDUwNFoXDTE2MDcxOTE0NDUwNFowRTELMAkG +A1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +APexTvEvG7cSmZdAERHt9TB11cSor54Y/F7NmYMdSOJNi4Y0kwkSslpdfipi+mt/ +NFg/uGKi1mcgvuXdVbVPZ9rCgVpIzMncO8RAP7a5+I2zKUzqMCCbLH16sYpo/rDk +VQ5V15TwLsTzOFGG8Cgp68TR8zHuZ4Edf2zMGC1IaiJ6W38LTnJgsowYOCFDAF3z +L36kxMO5gNGEUYV6tjltx+rAcXka3po+xiAgvW6q65UUgDHcIdEGG2dc9bkxxPl7 +RkprF2RwwADNzYS7Tn+Hpmjy06pfYZHNME+Iw515bCRF3GQFUU4BpGnY7EO+h4P9 +Kb1h948gUT9/oswXG+q2Kwk8AoggMJkUOWDFiCa5UjW1GBoxxb7VtZ+QTJXxlFWc +M2VzT7M/HX+P4b05vY4MXJjxPAFKrAGS7J8DKW8WJNUnXa9XSDBHg5qijDzZ/zGm +HTdG6iADnJLmOHBQgFQ12a/n9mYV2GPVC6FlgDzG9f0/SUPBUCafyWYz1LwKY4VM +2NLx/iwYMQsNIMSZQfNmufNDBr70+BShe3ZpbmKB/J33d87AuJd2HjnsThTEAAr+ +6CejyYmwFutoDUCF8IaKGJEp7OGP2//ub4nt5WwW8DYLRi8EqtzEnxPo5ZiayHMY +GHR1jpX1O5JVJFUE79bZCFFHKmtJc4kVZS4m4rTLsk83AgMBAAEwDQYJKoZIhvcN +AQEFBQADggIBAEt4PIRqVuALQSdgZ+GiZYuvEVjxoDVtMSc/ym93Gi8R7DDivFH9 +4suQc5QUiuEF8lpEtkmh+PZ+oFdQkjhBH80h7p4BUSyBy5Yi6dy7ATTlBAqwzCYZ +4wzHeJzu1SI6FinZLksoULbcw04n410aGHkLa6I9O3vCC4kXSnBlwU1sUsJphxM2 +3pkHBpvv79XYf5kFqZPzF16aO7rxFuVvqgXLyzwuyP9kH5zMA21Kioxs/pNyg1lm +5h0VinpHLPse+4tYih1L1WLMpEZiSwZgFhoRtlcdIVXokZPaX4G2EkdrMmSQruWg +Uz8Av6LEYHmRfbYwYM2kEX/+AF8thpTQDbvxjqYk5oyGX4wpKGpih1ac/jYu3O8B +VLhbxZlBYcLxCqqNsGJrWaiHj2Jf4GhUB0O9hXfaZDMqEGXT9GzOz0yF6b3pDQVy +H0lKIBb+kQzB/jhZKu4vrTAowXtt/av5d7D+rpAU1SxfUhBOPNSRoJUI5NSBbokp +a7u4azdB2IQETX3d2rhDk09EbG1XmMi5Vg1oa8nxfMOWXZnDMusJoZClKjrthmwd +rtR5et44XYhX6p217RBkYMDOVFT7aZpu4SaFeqZIuarVYodSmgXToOFXPsrLppRQ +adOT0FpU64RPNrQz5NF1bSIjqrHSaRVacf8yr7qqxNnpMsrtkDJzsMBz +-----END CERTIFICATE----- diff --git a/src/leap/mail/smtp/tests/cert/server.key b/src/leap/mail/smtp/tests/cert/server.key new file mode 100644 index 0000000..197a449 --- /dev/null +++ b/src/leap/mail/smtp/tests/cert/server.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/src/leap/mail/smtp/tests/test_smtprelay.py b/src/leap/mail/smtp/tests/test_smtprelay.py index a529c93..7fefe77 100644 --- a/src/leap/mail/smtp/tests/test_smtprelay.py +++ b/src/leap/mail/smtp/tests/test_smtprelay.py @@ -23,8 +23,8 @@ SMTP relay tests. import re - from datetime import datetime +from gnupg._util import _make_binary_stream from twisted.test import proto_helpers from twisted.mail.smtp import ( User, @@ -33,7 +33,6 @@ from twisted.mail.smtp import ( ) from mock import Mock - from leap.mail.smtp.smtprelay import ( SMTPFactory, EncryptedMessage, @@ -45,7 +44,6 @@ from leap.mail.smtp.tests import ( ) from leap.keymanager import openpgp - # some regexps IP_REGEX = "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}" + \ "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])" @@ -127,11 +125,22 @@ class TestSmtpRelay(TestCaseWithKeyManager): for line in self.EMAIL_DATA[4:12]: m.lineReceived(line) m.eomReceived() + # assert structure of encrypted message + self.assertTrue('Content-Type' in m._msg) + self.assertEqual('multipart/encrypted', m._msg.get_content_type()) + self.assertEqual('application/pgp-encrypted', + m._msg.get_param('protocol')) + self.assertEqual(2, len(m._msg.get_payload())) + self.assertEqual('application/pgp-encrypted', + m._msg.get_payload(0).get_content_type()) + self.assertEqual('application/octet-stream', + m._msg.get_payload(1).get_content_type()) privkey = self._km.get_key( ADDRESS, openpgp.OpenPGPKey, private=True) - decrypted = self._km.decrypt(m._message.get_payload(), privkey) + decrypted = self._km.decrypt( + m._msg.get_payload(1).get_payload(), privkey) self.assertEqual( - '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', + '\n' + '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', decrypted, 'Decrypted text differs from plaintext.') @@ -149,14 +158,24 @@ class TestSmtpRelay(TestCaseWithKeyManager): m.lineReceived(line) # trigger encryption and signing m.eomReceived() + # assert structure of encrypted message + self.assertTrue('Content-Type' in m._msg) + self.assertEqual('multipart/encrypted', m._msg.get_content_type()) + self.assertEqual('application/pgp-encrypted', + m._msg.get_param('protocol')) + self.assertEqual(2, len(m._msg.get_payload())) + self.assertEqual('application/pgp-encrypted', + m._msg.get_payload(0).get_content_type()) + self.assertEqual('application/octet-stream', + m._msg.get_payload(1).get_content_type()) # decrypt and verify privkey = self._km.get_key( ADDRESS, openpgp.OpenPGPKey, private=True) pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) decrypted = self._km.decrypt( - m._message.get_payload(), privkey, verify=pubkey) + m._msg.get_payload(1).get_payload(), privkey, verify=pubkey) self.assertEqual( - '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', + '\n' + '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n', decrypted, 'Decrypted text differs from plaintext.') @@ -175,22 +194,34 @@ class TestSmtpRelay(TestCaseWithKeyManager): m.lineReceived(line) # trigger signing m.eomReceived() + # assert structure of signed message + self.assertTrue('Content-Type' in m._msg) + self.assertEqual('multipart/signed', m._msg.get_content_type()) + self.assertEqual('application/pgp-signature', + m._msg.get_param('protocol')) + self.assertEqual('pgp-sha512', m._msg.get_param('micalg')) # assert content of message + self.assertEqual( + m._msg.get_payload(0).get_payload(decode=True), + '\r\n'.join(self.EMAIL_DATA[9:13])) + # assert content of signature self.assertTrue( - m._message.get_payload().startswith( - '-----BEGIN PGP SIGNED MESSAGE-----\n' + - 'Hash: SHA1\n\n' + - ('\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n' + - '-----BEGIN PGP SIGNATURE-----\n')), + m._msg.get_payload(1).get_payload().startswith( + '-----BEGIN PGP SIGNATURE-----\n'), 'Message does not start with signature header.') self.assertTrue( - m._message.get_payload().endswith( + m._msg.get_payload(1).get_payload().endswith( '-----END PGP SIGNATURE-----\n'), 'Message does not end with signature footer.') # assert signature is valid pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) + # replace EOL before verifying (according to rfc3156) + signed_text = re.sub('\r?\n', '\r\n', + m._msg.get_payload(0).as_string()) self.assertTrue( - self._km.verify(m._message.get_payload(), pubkey), + self._km.verify(signed_text, + pubkey, + detached_sig=m._msg.get_payload(1).get_payload()), 'Signature could not be verified.') def test_missing_key_rejects_address(self): |