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

使用Python对多部分电子邮件进行PGP签名,python,email,digital-signature,gnupg,pgp,Python,Email,Digital Signature,Gnupg,Pgp,我目前正在尝试将PGP签名支持添加到(使用Python3.x和module) 为消息签名的代码是: gpg = gnupg.GPG() basetext = basemsg.as_string().replace('\n', '\r\n') signature = str(gpg.sign(basetext, detach=True)) if signature: signmsg = messageFromSignature(signature) msg = MIMEMultip

我目前正在尝试将PGP签名支持添加到(使用Python3.x和module)

为消息签名的代码是:

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
是一种类型。)

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/alternative
multipart/mixed
)时失败

根据相应的文本段手动验证签名有效,但Evolution和Mutt报告签名不正确


有人能指出我的错误吗?

BaseMG的MIME结构实际上是什么?它中的嵌套零件似乎太多。如果您从Evolution导出签名消息,您将看到它只有两部分:正文和签名

下面是一个示例,它在stdout上生成一条消息,该消息可以在mutt(
mutt-f test.mbox
)和Evolution(File->Import)上读取并验证签名


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

问题是Python的
email.generator
模块没有在签名部分之前添加换行符。我已经向上游报告了这一点


(该漏洞在2014年在Python2.7和3.3+中得到修复)

python内置的
电子邮件
库存在更多问题。 如果将
调用为字符串
过程,则仅在当前类中扫描标题的
maxlinelength
,而在child(
\u 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()
#导入
导入smtplib,gnupg
从电子邮件导入字符集,编码器
从email.mime.base导入MIMEBase
从email.mime.text导入MIMEText
从email.mime.multipart导入MIMEMultipart
从email.header导入头
从email.message导入消息
从email.generator导入\u生成\u边界
#常数
电子邮件\u SMTP=“localhost”
电子邮件发送自=“Fusion钱包”
电子邮件\u RETURN=“Fusion钱包支持”
addr='some_target_email@gmail.com'
主题=‘测试’
html='test'
txt=‘测试’
#字符集
Charset.add_Charset('utf-8',Charset.QP,Charset.QP',utf-8')
#MIME处理程序
msgTEXT=MIMEText(txt,“普通”、“UTF-8”)
msgHTML=MIMEText(html,'html','UTF-8')
msgRoot=MIMEMultipart(_subtype=“signed”,micalg=“pgp-sha512”,protocol=“application/pgp signature”)
msgMix=MIMEMultipart('mixed')
msgAlt=MIMEMultipart('备选方案')
msgSIGN=Message()
msgOWNKEY=MIMEBase('应用程序',“八位字节流”)
#资料
msgRoot.add_标题('From',EMAIL_From)
msgRoot.add_标题('To',addr)
msgRoot.add_标题('Reply-To',EMAIL_-FROM)
msgRoot.add_头('Reply-Path',EMAIL_RETURN)
msgRoot.add_标题('Subject',Subject)
msgMix.add_头('From',EMAIL_From)
msgMix.add_头('To',addr)
msgMix.add_头('Reply-To',EMAIL_-FROM)
msgMix.add_头('Reply-Path',EMAIL_RETURN)
msgMix.add_标题('Subject',Subject)
msgMix.add_头('protected-headers','v1')
#附上自己的钥匙
ownKey=gpg.导出密钥('6B6C0EBB6DC42AA4')
如果是ownKey:
msgOWNKEY.add_头(“Content ID”,“”)
msgOWNKEY.add_头(“内容处置”、“附件”,filename='0x6b6c0eb.asc')
msgOWNKEY.set_有效载荷(ownKey)
#附加
msgAlt.attach(msgTEXT)
msgAlt.attach(msgHTML)
msgMix.attach(msgAlt)
如果是ownKey:
msgMix.attach(msgOWNKEY)
#标志
gpg=gnupg.gpg()
msgSIGN.add_标题('Content-Type','application/pgp签名;name=“signature.asc')
msgSIGN.add_头('Content-Description','OpenPGP数字签名')
msgSIGN.add_标题(“内容处置”,“附件”,filename='signature.asc'))
originalSign=gpg.sign(msgMix.as_string().replace('\n','\r\n').strip()).data
spos=原始签名索引('----开始PGP签名------')
符号=原始符号[spos:]
msgSIGN.set_有效载荷(符号)
#创建新边界
msgRoot.set_boundary(_make_boundary(msgMix.as_string()))
#设置有效载荷
msgRoot.set\u有效载荷(
“--%(边界)s\n%(混合)s--%(边界)s\n%(符号)s\n--%(边界)s--\n”{
“边界”:msgRoot.get_boundary(),
“mix”:msgMix.as_string(),
“符号”:msgSIGN.as_string(),
}
)
#发送到SMTP
s=smtplib.SMTP(电子邮件\u SMTP)
s、 sendmail(EMAIL\u FROM,addr,msgRoot.as\u string())
s、 退出

我的问题是如何签署多部分电子邮件。在您的例子中,
basemsg
是一条简单的MIMEText消息,而不是多部分消息。我找到了问题的根源——这是因为Python中的
email.generator
没有在结束边界后追加换行符。我对此不太确定;德米特里·沙切涅夫:啊,我没有仔细观察。希望这个错误很快得到修复!你是怎么修好的?是否有地方可以轻松添加换行符,或者您是否必须使用monkeypatch email.generator?我也有同样的问题。@MicahLee除了(猴子)修补
email.generator
,我还没有找到其他方法。
#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()