Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PGP-signing multipart e-mails with Python

I'm currently trying to add PGP signing support to my small e-mail sending script (which uses Python 3.x and python-gnupg module).

The code that signs message is:

gpg = gnupg.GPG()
basetext = basemsg.as_string().replace('\n', '\r\n')
signature = str(gpg.sign(basetext, detach=True))
if signature:
    signmsg = messageFromSignature(signature)
    msg = MIMEMultipart(_subtype="signed", micalg="pgp-sha1",
    protocol="application/pgp-signature")
    msg.attach(basemsg)
    msg.attach(signmsg)
else:
    print('Warning: failed to sign the message!')

(Here basemsg is of email.message.Message type.)

And messageFromSignature function is:

def messageFromSignature(signature):
    message = Message()
    message['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
    message['Content-Description'] = 'OpenPGP digital signature'
    message.set_payload(signature)
    return message

Then I add all the needed headers to the message (msg) and send it.

This works well for non-multipart messages, but fails when basemsg is multipart (multipart/alternative or multipart/mixed).

Manually verifying the signature against the corresponding piece of text works, but Evolution and Mutt report that the signature is bad.

Can anybody please point me to my mistake?

like image 733
Dmitry Shachnev Avatar asked May 08 '12 10:05

Dmitry Shachnev


2 Answers

The problem is that Python's email.generator module doesn't add a newline before the signature part. I've reported that upstream as http://bugs.python.org/issue14983.

(The bug was fixed in Python2.7 and 3.3+ in 2014)

like image 113
Dmitry Shachnev Avatar answered Oct 16 '22 05:10

Dmitry Shachnev


What is actually the MIME structure of basemsg? It appears that it has too many nested parts in it. If you export a signed message from e.g. Evolution, you'll see that it has just two parts: the body and the signature.

Here's an example which generates a message on stdout that can be read and the signature verified on both mutt (mutt -f test.mbox) and Evolution (File -> Import).

import gnupg
from email.message import Message
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

body = """
This is the original message text.

:)
"""

gpg_passphrase = "xxxx"

basemsg = MIMEText(body)

def messageFromSignature(signature):
    message = Message()
    message['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
    message['Content-Description'] = 'OpenPGP digital signature'
    message.set_payload(signature)
    return message

gpg = gnupg.GPG()
basetext = basemsg.as_string().replace('\n', '\r\n')
signature = str(gpg.sign(basetext, detach=True, passphrase=gpg_passphrase))
if signature:
    signmsg = messageFromSignature(signature)
    msg = MIMEMultipart(_subtype="signed", micalg="pgp-sha1",
    protocol="application/pgp-signature")
    msg.attach(basemsg)
    msg.attach(signmsg)
    msg['Subject'] = "Test message"
    msg['From'] = "[email protected]"
    msg['To'] = "[email protected]"
    print(msg.as_string(unixfrom=True)) # or send
else:
    print('Warning: failed to sign the message!')

Note that here, I'm assuming a keyring with a passphrase, but you may not need that.

like image 34
Fabian Fagerholm Avatar answered Oct 16 '22 04:10

Fabian Fagerholm