Подписание PGP составных электронных писем с помощью Python

В настоящее время я пытаюсь добавить поддержку подписи 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/alternative или multipart/mixed).

Ручная проверка подписи по соответствующему фрагменту текста работает, но Evolution и Mutt сообщают, что подпись плохая.

Может ли кто-нибудь указать мне на мою ошибку?


person Dmitry Shachnev    schedule 08.05.2012    source источник


Ответы (3)


Проблема в том, что модуль Python email.generator не добавляет новую строку перед частью подписи. Я сообщил об этом вверх по течению как http://bugs.python.org/issue14983.

(Ошибка была исправлена ​​в Python 2.7 и 3.3+ в 2014 г.)

person Dmitry Shachnev    schedule 08.06.2012
comment
Как вы в итоге починили? Есть ли место, где можно легко добавить новую строку, или вам нужно было пропатчить email.generator? У меня такая же проблема. - person micah; 07.11.2013
comment
@MicahLee Я не нашел никакого способа, кроме (обезьяньего) исправления email.generator. - person Dmitry Shachnev; 07.11.2013

Какова на самом деле структура MIME basemsg? Похоже, что в нем слишком много вложенных частей. Если вы экспортируете подписанное сообщение, например, из Evolution, вы увидите, что он состоит всего из двух частей: тела и подписи.

Вот пример, который создает сообщение на стандартном выходе, которое можно прочитать, и подпись проверяется как на mutt (mutt -f test.mbox), так и на Evolution (Файл -> Импорт).

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

Обратите внимание, что здесь я предполагаю связку ключей с кодовой фразой, но вам это может не понадобиться.

person Fabian Fagerholm    schedule 31.05.2012
comment
Мой вопрос касался того, как подписывать составные электронные письма. В вашем случае basemsg - это простое сообщение MIMEText, а не составное сообщение. Я нашел корень своей проблемы — это происходит потому, что email.generator в Python не добавляет новую строку после конечной границы. Я не совсем уверен в этом; когда я буду уверен, я опубликую ответ, описывающий, как это исправить. - person Dmitry Shachnev; 31.05.2012
comment
Дмитрий Шачнев: А, я невнимательно смотрел. Надеюсь, эта ошибка скоро будет исправлена! - person Fabian Fagerholm; 17.06.2012

Со встроенной библиотекой email python гораздо больше проблем. Если вызвать процедуру 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 не отбрасывались. Конечно, знак недействителен.

Мне потребовалось два дня, чтобы все понять. Вот мой код, который работает, и я использую его для отправки автоматических писем:

#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 <[email protected]>"
EMAIL_RETURN = "Fusion Wallet Support <[email protected]>"
addr = '[email protected]'
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()
person iFA88    schedule 20.03.2018