From 275c435ee45463ed6011f8988d5c7f90cb42fd01 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 7 Nov 2013 17:09:56 -0200 Subject: Change SMTP "relay" to "gateway". --- src/leap/mail/smtp/smtprelay.py | 585 ---------------------------------------- 1 file changed, 585 deletions(-) delete mode 100644 src/leap/mail/smtp/smtprelay.py (limited to 'src/leap/mail/smtp/smtprelay.py') diff --git a/src/leap/mail/smtp/smtprelay.py b/src/leap/mail/smtp/smtprelay.py deleted file mode 100644 index 474fc3b..0000000 --- a/src/leap/mail/smtp/smtprelay.py +++ /dev/null @@ -1,585 +0,0 @@ -# -*- coding: utf-8 -*- -# smtprelay.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 . - -""" -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 -from StringIO import StringIO -from email.Header import Header -from email.utils import parseaddr -from email.parser import Parser -from email.mime.application import MIMEApplication - -from zope.interface import implements -from OpenSSL import SSL -from twisted.mail import smtp -from twisted.internet.protocol import ServerFactory -from twisted.internet import reactor, ssl -from twisted.internet import defer -from twisted.python import log - -from leap.common.check import leap_assert, leap_assert_type -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.mail.smtp.rfc3156 import ( - MultipartSigned, - MultipartEncrypted, - PGPEncrypted, - PGPSignature, - RFC3156CompliantGenerator, - encode_base64_rec, -) - -# replace email generator with a RFC 3156 compliant one. -from email import generator -generator.Generator = RFC3156CompliantGenerator - - -# -# Helper utilities -# - -def validate_address(address): - """ - Validate C{address} as defined in RFC 2822. - - :param address: The address to be validated. - :type address: str - - @return: A valid address. - @rtype: str - - @raise smtp.SMTPBadRcpt: Raised if C{address} is invalid. - """ - leap_assert_type(address, str) - # in the following, the address is parsed as described in RFC 2822 and - # ('', '') is returned if the parse fails. - _, address = parseaddr(address) - if address == '': - raise smtp.SMTPBadRcpt(address) - return address - - -# -# SMTPFactory -# - -class SMTPFactory(ServerFactory): - """ - Factory for an SMTP server with encrypted relaying capabilities. - """ - - def __init__(self, userid, keymanager, host, port, cert, key, - encrypted_only): - """ - Initialize the SMTP factory. - - :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 relay should send unencrypted mail - or not. - :type encrypted_only: bool - """ - # assert params - leap_assert_type(keymanager, KeyManager) - leap_assert_type(host, str) - leap_assert(host != '') - leap_assert_type(port, int) - leap_assert(port is not 0) - leap_assert_type(cert, str) - leap_assert(cert != '') - leap_assert_type(key, str) - leap_assert(key != '') - leap_assert_type(encrypted_only, bool) - # and store them - self._userid = userid - self._km = keymanager - 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 - - @return: The protocol. - @rtype: SMTPDelivery - """ - smtpProtocol = smtp.SMTP(SMTPDelivery( - self._userid, self._km, self._host, self._port, self._cert, - self._key, self._encrypted_only)) - smtpProtocol.factory = self - return smtpProtocol - - -# -# SMTPDelivery -# - -class SMTPDelivery(object): - """ - Validate email addresses and handle message delivery. - """ - - implements(smtp.IMessageDelivery) - - def __init__(self, userid, keymanager, host, port, cert, key, - encrypted_only): - """ - Initialize the SMTP delivery object. - - :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 relay should send unencrypted mail - or not. - :type encrypted_only: bool - """ - self._userid = userid - self._km = keymanager - 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 - 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 - bound. - :type: list of twisted.mail.smtp.User - - @return: The full "Received" header string. - :type: str - """ - myHostname, clientIP = helo - headerValue = "by %s from %s with ESMTP ; %s" % ( - myHostname, clientIP, smtp.rfc822date()) - # email.Header.Header used for automatic wrapping of long lines - return "Received: %s" % Header(headerValue) - - def validateTo(self, user): - """ - Validate the address of C{user}, a recipient of the message. - - This method is called once for each recipient and validates the - C{user}'s address against the RFC 2822 definition. If the - configuration option ENCRYPTED_ONLY_KEY is True, it also asserts the - existence of the user's key. - - 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 - - @return: A Deferred which becomes, or a callable which takes no - arguments and returns an object implementing IMessage. This will - be called and the returned object used to deliver the message when - it arrives. - @rtype: no-argument callable - - @raise SMTPBadRcpt: Raised if messages to the address are not to be - accepted. - """ - # try to find recipient's public key - try: - address = validate_address(user.dest.addrstr) - # 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._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 " - "encrypted_only' is set to False).") - signal( - proto.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED, user.dest.addrstr) - return lambda: EncryptedMessage( - 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 - 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 - - @raise twisted.mail.smtp.SMTPBadSender: Raised if messages from this - address are not to be accepted. - """ - # accept mail from anywhere. To reject an address, raise - # smtp.SMTPBadSender here. - if str(origin) != str(self._userid): - log.msg("Rejecting sender {0}, expected {1}".format(origin, - self._userid)) - raise smtp.SMTPBadSender(origin) - self._origin = origin - return origin - - -# -# EncryptedMessage -# - -class SSLContextFactory(ssl.ClientContextFactory): - def __init__(self, cert, key): - self.cert = cert - self.key = key - - def getContext(self): - self.method = SSL.TLSv1_METHOD # SSLv23_METHOD - ctx = ssl.ClientContextFactory.getContext(self) - ctx.use_certificate_file(self.cert) - ctx.use_privatekey_file(self.key) - return ctx - - -def move_headers(origmsg, newmsg): - headers = origmsg.items() - unwanted_headers = ['content-type', 'mime-version', 'content-disposition', - 'content-transfer-encoding'] - headers = filter(lambda x: x[0].lower() not in unwanted_headers, headers) - for hkey, hval in headers: - newmsg.add_header(hkey, hval) - del(origmsg[hkey]) - - -class EncryptedMessage(object): - """ - Receive plaintext from client, encrypt it and send message to a - recipient. - """ - implements(smtp.IMessage) - - 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 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) - # and store them - self._fromAddress = fromAddress - self._user = user - self._km = keymanager - self._host = host - self._port = port - self._cert = cert - self._key = key - # initialize list for message's lines - self.lines = [] - - # - # methods from smtp.IMessage - # - - def lineReceived(self, line): - """ - Handle another line. - - :param line: The received line. - :type line: str - """ - self.lines.append(line) - - def eomReceived(self): - """ - Handle end of message. - - This method will encrypt and send the message. - """ - log.msg("Message data complete.") - self.lines.append('') # add a trailing newline - try: - self._maybe_encrypt_and_sign() - return self.sendMessage() - except KeyNotFound: - return None - - def parseMessage(self): - """ - Separate message headers from body. - """ - parser = Parser() - return parser.parsestr('\r\n'.join(self.lines)) - - def connectionLost(self): - """ - Log an error when the connection is lost. - """ - log.msg("Connection lost unexpectedly!") - log.err() - signal(proto.SMTP_CONNECTION_LOST, self._user.dest.addrstr) - # unexpected loss of connection; don't save - self.lines = [] - - def sendSuccess(self, r): - """ - Callback for a successful send. - - :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) - - def sendError(self, e): - """ - Callback for an unsuccessfull send. - - :param e: The result from the last errback. - :type e: anything - """ - log.msg(e) - log.err() - signal(proto.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr) - - def sendMessage(self): - """ - Send the message. - - This method will prepare the message (headers and possibly encrypted - body) and send it using the ESMTPSenderFactory. - - @return: A deferred with callbacks for error and success of this - message send. - @rtype: twisted.internet.defer.Deferred - """ - msg = self._msg.as_string(False) - - 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 - # 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. - self._fromAddress.addrstr, - self._user.dest.addrstr, - StringIO(msg), - d, - requireAuthentication=False, - requireTransportSecurity=True) - signal(proto.SMTP_SEND_MESSAGE_START, self._user.dest.addrstr) - reactor.connectSSL( - self._host, self._port, factory, - contextFactory=SSLContextFactory(self._cert, self._key)) - d.addCallback(self.sendSuccess) - d.addErrback(self.sendError) - return d - - # - # encryption methods - # - - def _encrypt_and_sign(self, pubkey, signkey): - """ - 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 - """ - # 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(self._origmsg, newmsg) - # create 'application/octet-stream' encrypted message - encmsg = MIMEApplication( - 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', - filename='msg.asc') - # create meta message - metamsg = PGPEncrypted() - metamsg.add_header('Content-Disposition', 'attachment') - # attach pgp message parts to new message - newmsg.attach(metamsg) - newmsg.attach(encmsg) - self._msg = newmsg - - def _sign(self, signkey): - """ - 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 - """ - # 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(self._origmsg, newmsg) - # apply base64 content-transfer-encoding - 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(self._origmsg) - msgtext = re.sub('\r?\n', '\r\n', fp.getvalue()) - # make sure signed message ends with \r\n as per OpenPGP stantard. - if self._origmsg.is_multipart(): - if not msgtext.endswith("\r\n"): - msgtext += "\r\n" - # calculate signature - signature = self._km.sign(msgtext, signkey, digest_algo='SHA512', - clearsign=False, detach=True, binary=False) - sigmsg = PGPSignature(signature) - # attach original message and signature to new message - newmsg.attach(self._origmsg) - newmsg.attach(sigmsg) - self._msg = newmsg - - def _maybe_encrypt_and_sign(self): - """ - Attempt to encrypt and sign the outgoing message. - - The behaviour of this method depends on: - - 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 relay: - - +---------------------------------------------------+----------------+ - | 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) - to_address = validate_address(self._user.dest.addrstr) - try: - # try to get the recipient pubkey - pubkey = self._km.get_key(to_address, OpenPGPKey) - log.msg("Will encrypt the message to %s." % pubkey.fingerprint) - signal(proto.SMTP_START_ENCRYPT_AND_SIGN, - "%s,%s" % (self._fromAddress.addrstr, to_address)) - self._encrypt_and_sign(pubkey, signkey) - signal(proto.SMTP_END_ENCRYPT_AND_SIGN, - "%s,%s" % (self._fromAddress.addrstr, to_address)) - except KeyNotFound: - # at this point we _can_ send unencrypted mail, because if the - # configuration said the opposite the address would have been - # rejected in SMTPDelivery.validateTo(). - log.msg('Will send unencrypted message to %s.' % to_address) - signal(proto.SMTP_START_SIGN, self._fromAddress.addrstr) - self._sign(signkey) - signal(proto.SMTP_END_SIGN, self._fromAddress.addrstr) -- cgit v1.2.3