使用Python进行PGP签名的多部分电子邮件

时间:2012-05-08 10:32:33

标签: python email digital-signature gnupg pgp

我目前正在尝试向my small e-mail sending script添加PGP签名支持(使用Python 3.x和python-gnupg模块)。

签署消息的代码是:

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!')

(此处basemsg属于email.message.Message类型。)

messageFromSignature功能是:

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

然后我将所有必需的标题添加到邮件(msg)并发送。

这适用于非多部分消息,但在basemsg为多部分(multipart/alternativemultipart/mixed)时失败。

根据相应的文本手动验证签名是否有效,但Evolution和Mutt报告签名错误。

任何人都可以指出我的错误吗?

3 个答案:

答案 0 :(得分:5)

问题是Python的email.generator模块不会在签名部分之前添加换行符。我已将上游报告为http://bugs.python.org/issue14983

(该错误已于2014年在Python2.7和3.3+中修复)

答案 1 :(得分:2)

basemsg的MIME结构实际上是什么?它似乎有太多的嵌套部分。如果您从例如导出签名的邮件进化,你会发现它只有两个部分:身体和签名。

这是一个在stdout上生成可以读取的消息并在mutt(mutt -f test.mbox)和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!')

请注意,在这里,我假设一个带密码的密钥环,但你可能不需要它。

答案 2 :(得分:-1)

python内置email库存在更多问题。 如果您调用as_string程序,则只会在当前班级和孩子(maxlinelength)中扫描标题_payload!像这样:

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)

我已签名msgMix.to_string(),然后将签名的邮件附加到msgRoot。但在发送到SMTP期间,msgMix部分不同,msgMix中的标题未被删除。 Ofc,标志无效。

我花了两天的时间来理解所有内容。这是我的代码,它用于发送自动电子邮件:

#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()