Signing Mails With Attachment in Python

Back

13 Apr 2013

Some time ago, I was asked to implement automated sending of singed mails with attachment. Although, that the process of signing message is well known and it may look like easy thing to do. The implementation deatials are a bit tricky, so I decided to share my solution written in Python.

For signing emails is used asymetric cypher, which has pair of keys public and private. Private key should be secret only known by owner public is made available to everybody else. When somebody encrypts a message by your public key and send it over network, anybody who wants to read the message need private key (corresponding coresponding to public key by, which was the message encrypted). When you are signing message you encrypt message with your private key and send it along with non encrypted version of message over network. When somebody needs to verify it was realy you, he takes your public key and decypher message. If non-encrypted part of message and decyphered part of message are same the message is authentic.

Signature of mail is basicaly just attachment, which contain original body of a mail encrypted by private key of signer. Whole mail body than looks like this:

Content-Type: multipart/signed;
   protocol="application/pkcs7-signature";
   micalg=sha1; boundary=boundary42

--boundary42
Content-Type: text/plain

This is a clear-signed message.

--boundary42
Content-Type: application/pkcs7-signature; name=smime.p7s
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=smime.p7s

ghyHhHUujhJhjH77n8HHGTrfvbnj756tbB9HG4VQpfyF467GhIGfHfYT6
4VQpfyF467GhIGfHfYT6jH77n8HHGghyHhHUujhJh756tbB9HGTrfvbnj
n8HHGTrfvhJhjH776tbB9HG4VQbnj7567GhIGfHfYT6ghyHhHUujpfyF4
7GhIGfHfYT64VQbnj756

--boundary42--

The format of such mail is called S/MIME and if you desire peer more into depth, you may look into RFC1847 and RFC5751.

More trouble starts when you want to sign message with attachment. In this case you need to create normal multipart message and than embedd it in S/MIME format along with signature generated from whole multipart message. Result should look like this:

Content-Type: multipart/signed; protocol="application/x-pkcs7-signature";
    micalg="sha1"; boundary="----58855F45B1D35B37586B46C0CB514C55"

This is an S/MIME signed message

------58855F45B1D35B37586B46C0CB514C55
Content-Type: multipart/mixed; boundary="===============5660683776552890559=="
MIME-Version: 1.0

--===============5660683776552890559==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

I am sending you a clear message
--===============5660683776552890559==
Content-Type: application/octet-stream
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="pixel.png"

iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAL
EwAACxMBAJqcGAAAAAd0SU1FB90CBxUmM+OATSEAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQg
d2l0aCBHSU1QZC5lBwAAAAxJREFUCNdj+P//PwAF/gL+3MxZ5wAAAABJRU5ErkJggg==
--===============5660683776552890559==--
------58855F45B1D35B37586B46C0CB514C55
Content-Type: application/x-pkcs7-signature; name="smime.p7s"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="smime.p7s"

MIIKiAYJKoZIhvcNAQcCoIIKeTCCCnUCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
DQEHAaCCBncwggZzMIIFW6ADAgECAgcrXtqle1BMMA0GCSqGSIb3DQEBBQUAMIHK
MQswCQYDVQQGEwJVUzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRz
ZGFsZTEaMBgGA1UEChMRR29EYWRkeS5jb20sIEluYy4xMzAxBgNVBAsTKmh0dHA6

------58855F45B1D35B37586B46C0CB514C55--

When it comes to implementation, we need to do following:

  • create multipart message
  • add text of the mail
  • convert each file to be sent in attachment into base64 and put it into MIME format
  • attach them to multipart message
  • sign the message
  • add header and send it

... and here it is the code in Python:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013, Peter Facka
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import os
import os.path
import smtplib
import datetime

from M2Crypto import BIO, Rand, SMIME
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
from email import Encoders

# we need to have access to both keys
ssl_key = 'foo.bar.key'
ssl_cert = 'foo.bar.pem'


def send_mail_ssl(server, sender, to, subject, text, files=[], attachments={}, bcc=[]):
    """
    Sends SSL signed mail

    server - mailserver domain name eg. smtp.foo.bar
    sender - content of From field eg. "No Reply" <noreply@foo.bar>
    to - list of strings with email addresses of recipents
    subject - subject of a mail
    text - text of email
    files - list of strings with paths to file to be attached
    attachmets - dict where keys are file names and values are content of files
    to be attached
    bcc - list of strings with blind copy addresses
    """

    if isinstance(to,str):
        to = [to]

    # create multipart message
    msg = MIMEMultipart()

    # attach message text as first attachment
    msg.attach( MIMEText(text) )

    # attach files to be read from file system
    for file in files:
        part = MIMEBase('application', "octet-stream")
        part.set_payload( open(file,"rb").read() )
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="%s"'
                       % os.path.basename(file))
        msg.attach(part)

    # attach filest read from dictionary
    for name in attachments:
        part = MIMEBase('application', "octet-stream")
        part.set_payload(attachments[name])
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="%s"' % name)
        msg.attach(part)

    # put message with attachments into into SSL' I/O buffer
    msg_str = msg.as_string()
    buf = BIO.MemoryBuffer(msg_str)

    # load seed file for PRNG
    Rand.load_file('/tmp/randpool.dat', -1)

    smime = SMIME.SMIME()

    # load certificate
    smime.load_key(ssl_key, ssl_cert)

    # sign whole message
    p7 = smime.sign(buf, SMIME.PKCS7_DETACHED)

    # create buffer for final mail and write header
    out = BIO.MemoryBuffer()
    out.write('From: %s\n' % sender)
    out.write('To: %s\n' % COMMASPACE.join(to))
    out.write('Date: %s\n' % formatdate(localtime=True))
    out.write('Subject: %s\n' % subject)
    out.write('Auto-Submitted: %s\n' % 'auto-generated')

    # convert message back into string
    buf = BIO.MemoryBuffer(msg_str)

    # append signed message and original message to mail header
    smime.write(out, p7, buf)

    # load save seed file for PRNG
    Rand.save_file('/tmp/randpool.dat')

    # extend list of recipents with bcc adresses
    to.extend(bcc)

    # finaly send mail
    smtp = smtplib.SMTP(server)
    smtp.sendmail(sender, to, out.read() )
    smtp.close()

To get it working you need key and certificate encoded in PEM format and M2Crypto library.

.