11

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?

Dmitry Shachnev
  • 550
  • 6
  • 17

3 Answers3

5

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)

snakecharmerb
  • 47,570
  • 11
  • 100
  • 153
Dmitry Shachnev
  • 550
  • 6
  • 17
  • How did you end up fixing it? Is there an place to add a newline easily, or did you have to monkeypatch email.generator? I'm having the same issue. – micah Nov 06 '13 at 21:37
  • @MicahLee I haven't found any way besides (monkey-)patching `email.generator`. – Dmitry Shachnev Nov 07 '13 at 05:44
3

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'] = "sender@example.com"
    msg['To'] = "recipient@example.com"
    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.

Fabian Fagerholm
  • 4,099
  • 1
  • 35
  • 45
  • My question was on how to sign **multipart** e-mails. In your case, `basemsg` is a simple MIMEText message, not multipart message. I've found the root of my problem — it happens because `email.generator` in Python doesn't append a newline after the ending boundary. I'm not quite sure about that; when I'll become sure I'll post an answer describing how to fix that. – Dmitry Shachnev May 31 '12 at 12:53
  • Dmitry Shachnev: Ah, I wasn't looking closely enough. Hope that bug gets fixed soon! – Fabian Fagerholm Jun 16 '12 at 20:38
-1

There are much more problem with the python built-in email library. If you call the as_string procedure, the headers will be scanned for maxlinelength only in the current class, and in the childs (_payload) not! Like this:

msgRoot (You call `to_string` during sending to smtp and headers will be checked)
->msgMix (headers will be not checked for maxlinelength)
-->msgAlt (headers will be not checked for maxlinelength)
--->msgText (headers will be not checked for maxlinelength)
--->msgHtml (headers will be not checked for maxlinelength)
-->msgSign (headers will be not checked for maxlinelength)

I have signed msgMix.to_string() and then attached the signed message to the msgRoot. But during sending to the SMTP the msgMix part was different, the headers in msgMix was not chucked. Ofc, the sign was invalid.

It has taken two days for me to understand everything.. Here is my code what works and I use for sending automatic emails:

#imports
import smtplib, gnupg
from email import Charset, Encoders
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.message import Message
from email.generator import _make_boundary
#constants
EMAIL_SMTP = "localhost"
EMAIL_FROM = "Fusion Wallet <no-reply@fusionwallet.io>"
EMAIL_RETURN = "Fusion Wallet Support <support@fusionwallet.io>"
addr = 'some_target_email@gmail.com'
subject = 'test'
html = '<b>test</b>'
txt = 'test'
#character set
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
#MIME handlers
msgTEXT = MIMEText(txt, 'plain', 'UTF-8')
msgHTML = MIMEText(html, 'html', 'UTF-8')
msgRoot = MIMEMultipart(_subtype="signed", micalg="pgp-sha512", protocol="application/pgp-signature")
msgMix = MIMEMultipart('mixed')
msgAlt = MIMEMultipart('alternative')
msgSIGN = Message()
msgOWNKEY = MIMEBase('application', "octet-stream")
#Data
msgRoot.add_header('From', EMAIL_FROM)
msgRoot.add_header('To', addr)
msgRoot.add_header('Reply-To', EMAIL_FROM)
msgRoot.add_header('Reply-Path', EMAIL_RETURN)
msgRoot.add_header('Subject', subject)
msgMix.add_header('From', EMAIL_FROM)
msgMix.add_header('To', addr)
msgMix.add_header('Reply-To', EMAIL_FROM)
msgMix.add_header('Reply-Path', EMAIL_RETURN)
msgMix.add_header('Subject', subject)
msgMix.add_header('protected-headers', 'v1')
#Attach own key
ownKey = gpg.export_keys('6B6C0EBB6DC42AA4')
if ownKey:
    msgOWNKEY.add_header("Content-ID", "<0x6B6C0EBB.asc>")
    msgOWNKEY.add_header("Content-Disposition", "attachment", filename='0x6B6C0EBB.asc')
    msgOWNKEY.set_payload(ownKey)
#Attaching
msgAlt.attach(msgTEXT)
msgAlt.attach(msgHTML)
msgMix.attach(msgAlt)
if ownKey:
    msgMix.attach(msgOWNKEY)
#Sign
gpg = gnupg.GPG()
msgSIGN.add_header('Content-Type', 'application/pgp-signature; name="signature.asc"')
msgSIGN.add_header('Content-Description', 'OpenPGP digital signature')
msgSIGN.add_header("Content-Disposition", "attachment", filename='signature.asc')
originalSign = gpg.sign(msgMix.as_string().replace('\n', '\r\n').strip()).data
spos = originalSign.index('-----BEGIN PGP SIGNATURE-----')
sign = originalSign[spos:]
msgSIGN.set_payload(sign)
#Create new boundary
msgRoot.set_boundary(_make_boundary(msgMix.as_string()))
#Set the payload
msgRoot.set_payload(
    "--%(boundary)s\n%(mix)s--%(boundary)s\n%(sign)s\n--%(boundary)s--\n" % {
        'boundary':msgRoot.get_boundary(),
        'mix':msgMix.as_string(),
        'sign':msgSIGN.as_string(),
    }
)
#Send to SMTP
s = smtplib.SMTP(EMAIL_SMTP)
s.sendmail(EMAIL_FROM, addr, msgRoot.as_string())
s.quit()
iFA88
  • 69
  • 2
  • 1