Source code for mailman_pgp.pgp.mime_multisig
# Copyright (C) 2017 Jan Jancar
#
# This file is a part of the Mailman PGP plugin.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""MIMEWrapper with multiple signature as per draft-ietf-openpgp-multsig-02."""
import copy
from email import message_from_string
from email.encoders import encode_7or8bit
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.utils import collapse_rfc2231_value
from mailman.email.message import Message
from pgpy import PGPSignature
from mailman_pgp.pgp.mime import MIMEWrapper
from mailman_pgp.utils.email import copy_headers
[docs]class MIMEMultiSigWrapper(MIMEWrapper):
"""https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02"""
_signature_preamble = \
'This is an OpepPGP/MIME signed message' \
'(RFC 4880, 3156 and draft-ietf-openpgp-multsig).\n' \
'see https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02' \
'for more details.'
[docs] def is_signed(self):
"""
Whether the message is signed as per draft-ietf-openpgp-multsig-02.
:return: If the message is MIME signed.
:rtype: bool
"""
if not self._is_mime():
return False
second_part = self.msg.get_payload(1)
second_type = second_part.get_content_type()
protocol_param = collapse_rfc2231_value(self.msg.get_param('protocol',
''))
content_subtype = self.msg.get_content_subtype()
return (second_part.is_multipart() and
second_type == 'multipart/mixed' and
content_subtype == 'signed' and
protocol_param == 'multipart/mixed' and
all(part.get_content_type() == MIMEWrapper._signed_type
for part in second_part.get_payload()))
[docs] def get_signature(self):
"""
:return:
:rtype: typing.Generator[pgpy.PGPSignature]
"""
for part in self.msg.get_payload(1).get_payload():
try:
sig = PGPSignature.from_blob(part.get_payload())
except:
continue
yield sig
[docs] def strip_signature(self):
pass
[docs] def sign(self, key, **kwargs):
"""
Sign a message with key.
:param key: The key to sign with.
:type key: pgpy.PGPKey
:return:
:rtype: MIMEMultiSigWrapper
"""
if self.is_signed():
signed = next(iter(self.get_signed()))
signature = key.sign(signed, **kwargs)
sig_part = MIMEApplication(_data=str(signature),
_subtype=MIMEWrapper._signature_subtype,
_encoder=encode_7or8bit,
name='signature.asc')
sig_part.add_header('Content-Description',
'OpenPGP digital signature')
sig_part.add_header('Content-Disposition', 'attachment',
filename='signature.asc')
micalg = self.msg.get_param('micalg')
micalg += ',' + self._micalg(signature.hash_algorithm)
self.msg.set_param('micalg', micalg)
self.msg.get_payload(1).attach(sig_part)
else:
original_msg = copy.deepcopy(self.msg)
to_sign = next(iter(self.get_payload()))
signature = key.sign(to_sign, **kwargs)
self.msg.set_payload([])
self.msg.attach(original_msg)
self.msg.set_type('multipart/signed')
self.msg['MIME-Version'] = '1.0'
self.msg.set_param('protocol', 'multipart/mixed')
self.msg.set_param('micalg',
self._micalg(signature.hash_algorithm))
sig_part = MIMEApplication(_data=str(signature),
_subtype=MIMEWrapper._signature_subtype,
_encoder=encode_7or8bit,
name='signature.asc')
sig_part.add_header('Content-Description',
'OpenPGP digital signature')
sig_part.add_header('Content-Disposition', 'attachment',
filename='signature.asc')
second_part = MIMEMultipart()
second_part.attach(sig_part)
self.msg.attach(second_part)
self.msg.preamble = MIMEMultiSigWrapper._signature_preamble
return self
[docs] def verify(self, key):
"""
Verify the signatures of this message with key.
:param key: The key to verify with.
:type key: pgpy.PGPKey
:return: The verified signature.
:rtype: Generator[pgpy.types.SignatureVerification]
"""
clear_text = next(iter(self.get_signed()))
for signature in self.get_signature():
try:
verification = key.verify(clear_text, signature)
except:
continue
yield verification
[docs] def decrypt(self, key):
"""
Decrypt this message with key.
:param key: The key to decrypt with.
:type key: pgpy.PGPKey
:return:
:rtype: MIMEMultiSigWrapper
"""
pmsg = next(iter(self.get_encrypted()))
decrypted = key.decrypt(pmsg)
dmsg = decrypted.message
if isinstance(dmsg, bytearray):
dmsg = dmsg.decode(decrypted.charset or 'utf-8')
out = message_from_string(dmsg, _class=Message)
if decrypted.is_signed:
# result should be a multisig signed thing, so [out, ]
self.msg.set_payload([])
self.msg.attach(out)
self.msg.set_type('multipart/signed')
self.msg['MIME-Version'] = '1.0'
self.msg.set_param('protocol', 'multipart/mixed')
micalg = ', '.join(self._micalg(sig.hash_algorithm)
for sig in decrypted.signatures)
self.msg.set_param('micalg', micalg)
second_part = MIMEMultipart()
for signature in decrypted.signatures:
sig = MIMEApplication(_data=str(signature),
_subtype=MIMEWrapper._signature_subtype,
_encoder=encode_7or8bit,
name='signature.asc')
sig.add_header('Content-Description',
'OpenPGP digital signature')
sig.add_header('Content-Disposition', 'attachment',
filename='signature.asc')
second_part.attach(sig)
self.msg.attach(second_part)
else:
# result should be
self.msg.set_payload(out.get_payload())
copy_headers(out, self.msg, True)
return self