From 1f44f85d3cb75227ee9331b0c02b64a046951ef9 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Wed, 28 Jan 2015 14:24:32 -0600 Subject: Add public key as attachment --- changes/feature-6617_attach_public_key | 1 + src/leap/mail/outgoing/service.py | 42 +++++++++++++++++++- src/leap/mail/outgoing/tests/test_outgoing.py | 55 +++++++++++++++++++++++++-- src/leap/mail/smtp/gateway.py | 1 - 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 changes/feature-6617_attach_public_key diff --git a/changes/feature-6617_attach_public_key b/changes/feature-6617_attach_public_key new file mode 100644 index 0000000..49b444b --- /dev/null +++ b/changes/feature-6617_attach_public_key @@ -0,0 +1 @@ +- add public key as attachment (Closes: #6617) diff --git a/src/leap/mail/outgoing/service.py b/src/leap/mail/outgoing/service.py index 88b8895..9777187 100644 --- a/src/leap/mail/outgoing/service.py +++ b/src/leap/mail/outgoing/service.py @@ -18,6 +18,8 @@ import re from StringIO import StringIO from email.parser import Parser from email.mime.application import MIMEApplication +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from OpenSSL import SSL @@ -250,10 +252,48 @@ class OutgoingMail: % (to_address, from_address)) signal(proto.SMTP_START_ENCRYPT_AND_SIGN, "%s,%s" % (self._from_address, to_address)) - d = self._encrypt_and_sign(origmsg, to_address, from_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) return d + def _maybe_attach_key(self, origmsg, from_address, to_address): + filename = "%s-email-key.asc" % (from_address,) + + def attach_if_address_hasnt_encrypted(to_key): + # if the sign_used flag is true that means that we got an encrypted + # email from this address, because we conly check signatures on + # encrypted emails. In this case we don't attach. + # XXX: this might not be true some time in the future + if to_key.sign_used: + return origmsg + + d = self._keymanager.get_key(from_address, OpenPGPKey, + fetch_remote=False) + d.addCallback(attach_key) + return d + + def attach_key(from_key): + msg = origmsg + if not origmsg.is_multipart(): + msg = MIMEMultipart() + for h, v in origmsg.items(): + msg.add_header(h, v) + msg.attach(MIMEText(origmsg.get_payload())) + + keymsg = MIMEApplication(from_key.key_data, _subtype='pgp-keys', + _encoder=lambda x: x) + keymsg.add_header('content-disposition', 'attachment', + filename=filename) + msg.attach(keymsg) + return msg + + d = self._keymanager.get_key(to_address, OpenPGPKey, + fetch_remote=False) + d.addCallback(attach_if_address_hasnt_encrypted) + d.addErrback(lambda _: origmsg) + return d + def _encrypt_and_sign(self, origmsg, encrypt_address, sign_address): """ Create an RFC 3156 compliang PGP encrypted and signed message using diff --git a/src/leap/mail/outgoing/tests/test_outgoing.py b/src/leap/mail/outgoing/tests/test_outgoing.py index d7423b6..0eb05c8 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 email.parser import Parser from datetime import datetime from twisted.internet.defer import fail from twisted.mail.smtp import User @@ -33,10 +34,14 @@ from leap.mail.tests import ( TestCaseWithKeyManager, ADDRESS, ADDRESS_2, + PUBLIC_KEY_2, ) from leap.keymanager import openpgp, errors +BEGIN_PUBLIC_KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----" + + class TestOutgoingMail(TestCaseWithKeyManager): EMAIL_DATA = ['HELO gateway.leap.se', 'MAIL FROM: <%s>' % ADDRESS_2, @@ -71,7 +76,7 @@ class TestOutgoingMail(TestCaseWithKeyManager): self._km, self._config['encrypted_only'], self.outgoing_mail).buildProtocol(('127.0.0.1', 0)) - self.dest = User(ADDRESS, 'gateway.leap.se', self.proto, ADDRESS) + self.dest = User(ADDRESS, 'gateway.leap.se', self.proto, ADDRESS_2) d = TestCaseWithKeyManager.setUp(self) d.addCallback(init_outgoing_and_proto) @@ -88,7 +93,10 @@ class TestOutgoingMail(TestCaseWithKeyManager): decrypted, 'Decrypted text differs from plaintext.') - d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest) + d = self._set_sign_used(ADDRESS) + d.addCallback( + lambda _: + self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest)) d.addCallback(self._assert_encrypted) d.addCallback(lambda message: self._km.decrypt( message.get_payload(1).get_payload(), ADDRESS, openpgp.OpenPGPKey)) @@ -109,7 +117,10 @@ class TestOutgoingMail(TestCaseWithKeyManager): self.assertTrue(ADDRESS_2 in signkey.address, "Verification failed") - d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest) + d = self._set_sign_used(ADDRESS) + d.addCallback( + lambda _: + self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest)) d.addCallback(self._assert_encrypted) d.addCallback(lambda message: self._km.decrypt( message.get_payload(1).get_payload(), ADDRESS, openpgp.OpenPGPKey, @@ -158,7 +169,7 @@ class TestOutgoingMail(TestCaseWithKeyManager): def assert_verify(key): self.assertTrue(ADDRESS_2 in key.address, - 'Signature could not be verified.') + 'Signature could not be verified.') d = self._km.verify( signed_text, ADDRESS_2, openpgp.OpenPGPKey, @@ -171,6 +182,42 @@ class TestOutgoingMail(TestCaseWithKeyManager): d.addCallback(verify) 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(lambda message: self._km.decrypt( + message.get_payload(1).get_payload(), ADDRESS, openpgp.OpenPGPKey)) + d.addCallback(check_attachment) + return d + + def _set_sign_used(self, address): + def set_sign(key): + key.sign_used = True + return self._km.put_key(key, address) + + d = self._km.get_key(address, openpgp.OpenPGPKey, fetch_remote=False) + d.addCallback(set_sign) + return d + def _assert_encrypted(self, res): message, _ = res self.assertTrue('Content-Type' in message) diff --git a/src/leap/mail/smtp/gateway.py b/src/leap/mail/smtp/gateway.py index 9d78474..954a7d0 100644 --- a/src/leap/mail/smtp/gateway.py +++ b/src/leap/mail/smtp/gateway.py @@ -309,5 +309,4 @@ class EncryptedMessage(object): signal(proto.SMTP_CONNECTION_LOST, self._user.dest.addrstr) # unexpected loss of connection; don't save - self._lines = [] -- cgit v1.2.3