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

11

我目前正在尝试为我的小型电子邮件发送脚本(使用Python 3.x和python-gnupg模块)添加PGP签名支持。

签署信息的代码如下:

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

(这里的basemsgemail.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个回答

5
问题在于Python的email.generator模块在签名部分之前没有添加换行符。我已经向上游报告了这个问题,地址是http://bugs.python.org/issue14983
(该bug已在2014年修复了Python2.7和3.3+版本)

你是怎么解决它的?有没有一个地方可以轻松地添加换行符,还是你不得不猴子补丁email.generator?我也遇到了同样的问题。 - micah
@MicahLee 除了(猴子式地)修补email.generator,我没有找到其他方法。 - Dmitry Shachnev

3

basemsg的MIME结构是什么?它似乎有太多嵌套部分了。如果你从 Evolution导出已签名的邮件,你会发现它只有两个部分:正文和签名。

下面是一个示例,在标准输出上生成一个消息,可以在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!')

请注意,这里我假设您使用的是带有密码短语的密钥环,但您可能并不需要。

我的问题是如何签署多部分电子邮件。在您的情况下,basemsg 是一个简单的 MIMEText 消息,而不是多部分消息。我已经找到了问题的根源——这是因为 Python 中的 email.generator 在结束边界后没有附加换行符。我还不太确定;当我确定后,我会发布一个答案来描述如何修复它。 - Dmitry Shachnev
Dmitry Shachnev:啊,我没有仔细看。希望那个错误很快得到修复! - Fabian Fagerholm

-1

Python内置的email库存在更多问题。如果调用as_string程序,则仅在当前类和子类(_payload)中扫描标题以获取maxlinelength!就像这样:

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 中的头部没有被分块。当然,签名是无效的。

我花了两天时间来理解一切... 这是我的代码,它可以用于发送自动邮件:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接