From ed80b75ef6436d4c341509b6108d2829722b287f Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 2 Feb 2015 14:21:31 -0600 Subject: Attach key for addresses without known key This seems to fix the problem with some headers dissapearing (#6692) --- src/leap/mail/outgoing/service.py | 43 ++++++++++------- src/leap/mail/outgoing/tests/test_outgoing.py | 68 ++++++++++++++++++--------- 2 files changed, 72 insertions(+), 39 deletions(-) diff --git a/src/leap/mail/outgoing/service.py b/src/leap/mail/outgoing/service.py index 9777187..c881830 100644 --- a/src/leap/mail/outgoing/service.py +++ b/src/leap/mail/outgoing/service.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import re from StringIO import StringIO +from copy import deepcopy from email.parser import Parser from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart @@ -33,7 +34,7 @@ from leap.common.check import leap_assert_type, leap_assert from leap.common.events import proto, signal from leap.keymanager import KeyManager from leap.keymanager.openpgp import OpenPGPKey -from leap.keymanager.errors import KeyNotFound +from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch from leap.mail import __version__ from leap.mail.utils import validate_address from leap.mail.smtp.rfc3156 import MultipartEncrypted @@ -229,32 +230,37 @@ class OutgoingMail: username, domain = from_address.split('@') to_address = validate_address(recipient.dest.addrstr) + def maybe_encrypt_and_sign(message): + d = self._encrypt_and_sign(message, to_address, from_address) + d.addCallbacks(signal_encrypt_sign, + if_key_not_found_send_unencrypted, + errbackArgs=(message,)) + return d + def signal_encrypt_sign(newmsg): signal(proto.SMTP_END_ENCRYPT_AND_SIGN, "%s,%s" % (self._from_address, to_address)) return newmsg, recipient + def if_key_not_found_send_unencrypted(failure, message): + failure.trap(KeyNotFound, KeyAddressMismatch) + + log.msg('Will send unencrypted message to %s.' % to_address) + signal(proto.SMTP_START_SIGN, self._from_address) + d = self._sign(message, from_address) + d.addCallback(signal_sign) + return d + def signal_sign(newmsg): signal(proto.SMTP_END_SIGN, self._from_address) return newmsg, recipient - def if_key_not_found_send_unencrypted(failure): - if failure.check(KeyNotFound): - log.msg('Will send unencrypted message to %s.' % to_address) - signal(proto.SMTP_START_SIGN, self._from_address) - d = self._sign(origmsg, from_address) - d.addCallback(signal_sign) - return d - else: - return failure - log.msg("Will encrypt the message with %s and sign with %s." % (to_address, from_address)) signal(proto.SMTP_START_ENCRYPT_AND_SIGN, "%s,%s" % (self._from_address, to_address)) d = self._maybe_attach_key(origmsg, from_address, to_address) - d.addCallback(self._encrypt_and_sign, to_address, from_address) - d.addCallbacks(signal_encrypt_sign, if_key_not_found_send_unencrypted) + d.addCallback(maybe_encrypt_and_sign) return d def _maybe_attach_key(self, origmsg, from_address, to_address): @@ -267,7 +273,9 @@ class OutgoingMail: # XXX: this might not be true some time in the future if to_key.sign_used: return origmsg + return get_key_and_attach(None) + def get_key_and_attach(_): d = self._keymanager.get_key(from_address, OpenPGPKey, fetch_remote=False) d.addCallback(attach_key) @@ -290,7 +298,7 @@ class OutgoingMail: d = self._keymanager.get_key(to_address, OpenPGPKey, fetch_remote=False) - d.addCallback(attach_if_address_hasnt_encrypted) + d.addCallbacks(attach_if_address_hasnt_encrypted, get_key_and_attach) d.addErrback(lambda _: origmsg) return d @@ -386,7 +394,7 @@ class OutgoingMail: d.addCallback(create_signed_message) return d - def _fix_headers(self, origmsg, newmsg, sign_address): + def _fix_headers(self, msg, newmsg, sign_address): """ Move some headers from C{origmsg} to C{newmsg}, delete unwanted headers from C{origmsg} and add new headers to C{newms}. @@ -416,8 +424,8 @@ class OutgoingMail: - User-Agent - :param origmsg: The original message. - :type origmsg: email.message.Message + :param msg: The original message. + :type msg: email.message.Message :param newmsg: The new message being created. :type newmsg: email.message.Message :param sign_address: The address used to sign C{newmsg} @@ -428,6 +436,7 @@ class OutgoingMail: original Message with headers removed) :rtype: Deferred """ + origmsg = deepcopy(msg) # move headers from origmsg to newmsg headers = origmsg.items() passthrough = [ diff --git a/src/leap/mail/outgoing/tests/test_outgoing.py b/src/leap/mail/outgoing/tests/test_outgoing.py index 0eb05c8..2376da9 100644 --- a/src/leap/mail/outgoing/tests/test_outgoing.py +++ b/src/leap/mail/outgoing/tests/test_outgoing.py @@ -21,6 +21,7 @@ SMTP gateway tests. """ import re +from StringIO import StringIO from email.parser import Parser from datetime import datetime from twisted.internet.defer import fail @@ -29,6 +30,7 @@ from twisted.mail.smtp import User from mock import Mock from leap.mail.smtp.gateway import SMTPFactory +from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator from leap.mail.outgoing.service import OutgoingMail from leap.mail.tests import ( TestCaseWithKeyManager, @@ -149,8 +151,11 @@ class TestOutgoingMail(TestCaseWithKeyManager): message.get_param('protocol')) self.assertEqual('pgp-sha512', message.get_param('micalg')) # assert content of message + body = (message.get_payload(0) + .get_payload(0) + .get_payload(decode=True)) self.assertEqual(self.expected_body, - message.get_payload(0).get_payload(decode=True)) + body) # assert content of signature self.assertTrue( message.get_payload(1).get_payload().startswith( @@ -164,8 +169,12 @@ class TestOutgoingMail(TestCaseWithKeyManager): def verify(message): # replace EOL before verifying (according to rfc3156) + fp = StringIO() + g = RFC3156CompliantGenerator( + fp, mangle_from_=False, maxheaderlen=76) + g.flatten(message.get_payload(0)) signed_text = re.sub('\r?\n', '\r\n', - message.get_payload(0).as_string()) + fp.getvalue()) def assert_verify(key): self.assertTrue(ADDRESS_2 in key.address, @@ -183,32 +192,47 @@ class TestOutgoingMail(TestCaseWithKeyManager): return d def test_attach_key(self): - def check_headers(message): - msgstr = message.as_string(unixfrom=False) - for header in self.EMAIL_DATA[4:8]: - self.assertTrue(header in msgstr, - "Missing header: %s" % (header,)) - return message - - def check_attachment((decrypted, _)): - msg = Parser().parsestr(decrypted) - for payload in msg.get_payload(): - if 'application/pgp-keys' == payload.get_content_type(): - keylines = PUBLIC_KEY_2.split('\n') - key = BEGIN_PUBLIC_KEY + '\n\n' + '\n'.join(keylines[4:-1]) - self.assertTrue(key in payload.get_payload(), - "Key attachment don't match") - return - self.fail("No public key attachment found") - d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest) d.addCallback(self._assert_encrypted) - d.addCallback(check_headers) + d.addCallback(self._check_headers, self.lines[:4]) d.addCallback(lambda message: self._km.decrypt( message.get_payload(1).get_payload(), ADDRESS, openpgp.OpenPGPKey)) - d.addCallback(check_attachment) + d.addCallback(lambda (decrypted, _): + self._check_key_attachment(Parser().parsestr(decrypted))) return d + def test_attach_key_not_known(self): + address = "someunknownaddress@somewhere.com" + lines = self.lines + lines[1] = "To: <%s>" % (address,) + raw = '\r\n'.join(lines) + dest = User(address, 'gateway.leap.se', self.proto, ADDRESS_2) + + d = self.outgoing_mail._maybe_encrypt_and_sign(raw, dest) + d.addCallback(lambda (message, _): + self._check_headers(message, lines[:4])) + d.addCallback(self._check_key_attachment) + return d + + def _check_headers(self, message, headers): + msgstr = message.as_string(unixfrom=False) + for header in headers: + self.assertTrue(header in msgstr, + "Missing header: %s" % (header,)) + return message + + def _check_key_attachment(self, message): + for payload in message.get_payload(): + if payload.is_multipart(): + return self._check_key_attachment(payload) + if 'application/pgp-keys' == payload.get_content_type(): + keylines = PUBLIC_KEY_2.split('\n') + key = BEGIN_PUBLIC_KEY + '\n\n' + '\n'.join(keylines[4:-1]) + self.assertTrue(key in payload.get_payload(decode=True), + "Key attachment don't match") + return + self.fail("No public key attachment found") + def _set_sign_used(self, address): def set_sign(key): key.sign_used = True -- cgit v1.2.3