From aa00267275f5d8fb7a5c44490cd849c747454791 Mon Sep 17 00:00:00 2001 From: J08nY Date: Thu, 27 Jul 2017 20:25:34 +0200 Subject: Add multisig handling as per draft-ietf-openpgp-multsig. --- src/mailman_pgp/pgp/inline.py | 7 ++- src/mailman_pgp/pgp/mime.py | 74 ++++++++++++++++++++++++++---- src/mailman_pgp/pgp/tests/mime_multisig.py | 16 +++++++ src/mailman_pgp/pgp/wrapper.py | 3 +- 4 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 src/mailman_pgp/pgp/tests/mime_multisig.py diff --git a/src/mailman_pgp/pgp/inline.py b/src/mailman_pgp/pgp/inline.py index 81e0839..2fe22cf 100644 --- a/src/mailman_pgp/pgp/inline.py +++ b/src/mailman_pgp/pgp/inline.py @@ -218,8 +218,11 @@ class InlineWrapper: out = copy.deepcopy(self.msg) for part in walk(out): if not part.is_multipart(): - payload = str(part.get_payload()) - pmsg = PGPMessage.new(payload, cleartext=True) + if self._is_signed(part): + pmsg = PGPMessage.from_blob(part.get_payload()) + else: + payload = str(part.get_payload()) + pmsg = PGPMessage.new(payload, cleartext=True) smsg = self._sign(pmsg, key, hash) part.set_payload(str(smsg)) return out diff --git a/src/mailman_pgp/pgp/mime.py b/src/mailman_pgp/pgp/mime.py index bdb7943..a791a1d 100644 --- a/src/mailman_pgp/pgp/mime.py +++ b/src/mailman_pgp/pgp/mime.py @@ -21,10 +21,11 @@ from email import message_from_string from email.encoders import encode_7or8bit from email.iterators import walk 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, MultipartDigestMessage -from pgpy import PGPKey, PGPMessage, PGPSignature +from pgpy import PGPDetachedSignature, PGPKey, PGPMessage from pgpy.constants import HashAlgorithm, SymmetricKeyAlgorithm from public import public @@ -47,15 +48,24 @@ class MIMEWrapper: 'This is an OpenPGP/MIME signed message (RFC 4880 and 3156)' _encryption_preamble = \ 'This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)' + _multiple_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.' - def __init__(self, msg): + def __init__(self, msg, allow_draft_multisig=True): """ Wrap the given message. :param msg: The message to wrap. :type msg: mailman.email.message.Message + :param allow_draft_multisig: Whether to allow creating multisigs as per + https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02. + :type allow_draft_multisig: bool """ self.msg = msg + self.allow_draft_multisig = allow_draft_multisig def get_payload(self): yield self.msg.as_string() @@ -98,10 +108,11 @@ class MIMEWrapper: """ :return: - :rtype: typing.Generator[pgpy.PGPSignature] + :rtype: typing.Generator[pgpy.PGPDetachedSignature] """ try: - msg = PGPSignature.from_blob(self.msg.get_payload(1).get_payload()) + msg = PGPDetachedSignature.from_blob( + self.msg.get_payload(1).get_payload()) except: return yield msg @@ -229,6 +240,13 @@ class MIMEWrapper: return 'pgp-' + algs[hash_algo] def _wrap_signed(self, msg, signature): + """ + As per RFC1847 and RFC3156. + + :param msg: + :param signature: + :return: + """ micalg = self._micalg(signature.hash_algorithm) out = MultipartDigestMessage('signed', micalg=micalg, protocol=MIMEWrapper._signed_type) @@ -248,6 +266,36 @@ class MIMEWrapper: copy_headers(msg, out) return out + def _wrap_signed_multiple(self, msg, signature): + """ + As per https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02. + + :param msg: + :param signature: + :return: + """ + micalg = ', '.join(self._micalg(sig.hash_algorithm) + for sig in signature) + out = MultipartDigestMessage('signed', micalg=micalg, + protocol=MIMEWrapper._signed_type) + out.preamble = MIMEWrapper._signature_preambleň + + second_part = MIMEMultipart() + for sig in signature: + sig_part = MIMEApplication(_data=str(sig), + _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.attach(sig_part) + out.attach(copy.deepcopy(msg)) + out.attach(second_part) + copy_headers(msg, out) + return out + def sign(self, key, hash=None): """ Sign a message with key. @@ -259,9 +307,15 @@ class MIMEWrapper: :return: The signed message. :rtype: mailman.email.message.Message """ - payload = next(iter(self.get_payload())) - signature = key.sign(payload, hash=hash) - return self._wrap_signed(self.msg, signature) + if self.is_signed() and self.allow_draft_multisig: + payload = next(iter(self.get_signed())) + signature = next(iter(self.get_signature())) + signature |= key.sign(payload, hash=hash) + return self._wrap_signed_multiple(self.msg, signature) + else: + payload = next(iter(self.get_payload())) + signature = key.sign(payload, hash=hash) + return self._wrap_signed(self.msg, signature) def decrypt(self, key): """ @@ -281,7 +335,11 @@ class MIMEWrapper: out = message_from_string(dmsg, _class=Message) if decrypted.is_signed: - out = self._wrap_signed(out, decrypted.signatures.pop()) + if len(decrypted.signatures) != 1 and self.allow_draft_multisig: + out = self._wrap_signed_multiple(out, + decrypted.detached_signature) + else: + out = self._wrap_signed(out, decrypted.signatures.pop()) copy_headers(self.msg, out) return out diff --git a/src/mailman_pgp/pgp/tests/mime_multisig.py b/src/mailman_pgp/pgp/tests/mime_multisig.py new file mode 100644 index 0000000..56dd01d --- /dev/null +++ b/src/mailman_pgp/pgp/tests/mime_multisig.py @@ -0,0 +1,16 @@ +# 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 . \ No newline at end of file diff --git a/src/mailman_pgp/pgp/wrapper.py b/src/mailman_pgp/pgp/wrapper.py index af6ce44..59f0fdd 100644 --- a/src/mailman_pgp/pgp/wrapper.py +++ b/src/mailman_pgp/pgp/wrapper.py @@ -85,7 +85,8 @@ class PGPWrapper(): """ :return: - :rtype: typing.Generator[pgpy.PGPMessage|pgpy.PGPSignature] + :rtype: typing.Generator[pgpy.PGPMessage|pgpy.PGPSignature| + pgpy.PGPDetachedSignature] """ if self.mime.is_signed(): yield from self.mime.get_signature() -- cgit v1.2.3-70-g09d2 From 25134df48508444c6a31ca299341bd09dca1ac82 Mon Sep 17 00:00:00 2001 From: J08nY Date: Thu, 27 Jul 2017 20:52:46 +0200 Subject: Separate multisig handling into MIMEMultiSigWrapper. --- src/mailman_pgp/pgp/mime.py | 60 ++----------------- src/mailman_pgp/pgp/mime_multisig.py | 94 ++++++++++++++++++++++++++++++ src/mailman_pgp/pgp/tests/mime_multisig.py | 16 ----- 3 files changed, 99 insertions(+), 71 deletions(-) create mode 100644 src/mailman_pgp/pgp/mime_multisig.py delete mode 100644 src/mailman_pgp/pgp/tests/mime_multisig.py diff --git a/src/mailman_pgp/pgp/mime.py b/src/mailman_pgp/pgp/mime.py index a791a1d..3674f85 100644 --- a/src/mailman_pgp/pgp/mime.py +++ b/src/mailman_pgp/pgp/mime.py @@ -21,7 +21,6 @@ from email import message_from_string from email.encoders import encode_7or8bit from email.iterators import walk 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, MultipartDigestMessage @@ -48,24 +47,15 @@ class MIMEWrapper: 'This is an OpenPGP/MIME signed message (RFC 4880 and 3156)' _encryption_preamble = \ 'This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)' - _multiple_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.' - def __init__(self, msg, allow_draft_multisig=True): + def __init__(self, msg): """ Wrap the given message. :param msg: The message to wrap. :type msg: mailman.email.message.Message - :param allow_draft_multisig: Whether to allow creating multisigs as per - https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02. - :type allow_draft_multisig: bool """ self.msg = msg - self.allow_draft_multisig = allow_draft_multisig def get_payload(self): yield self.msg.as_string() @@ -266,36 +256,6 @@ class MIMEWrapper: copy_headers(msg, out) return out - def _wrap_signed_multiple(self, msg, signature): - """ - As per https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02. - - :param msg: - :param signature: - :return: - """ - micalg = ', '.join(self._micalg(sig.hash_algorithm) - for sig in signature) - out = MultipartDigestMessage('signed', micalg=micalg, - protocol=MIMEWrapper._signed_type) - out.preamble = MIMEWrapper._signature_preambleň - - second_part = MIMEMultipart() - for sig in signature: - sig_part = MIMEApplication(_data=str(sig), - _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.attach(sig_part) - out.attach(copy.deepcopy(msg)) - out.attach(second_part) - copy_headers(msg, out) - return out - def sign(self, key, hash=None): """ Sign a message with key. @@ -307,15 +267,9 @@ class MIMEWrapper: :return: The signed message. :rtype: mailman.email.message.Message """ - if self.is_signed() and self.allow_draft_multisig: - payload = next(iter(self.get_signed())) - signature = next(iter(self.get_signature())) - signature |= key.sign(payload, hash=hash) - return self._wrap_signed_multiple(self.msg, signature) - else: - payload = next(iter(self.get_payload())) - signature = key.sign(payload, hash=hash) - return self._wrap_signed(self.msg, signature) + payload = next(iter(self.get_payload())) + signature = key.sign(payload, hash=hash) + return self._wrap_signed(self.msg, signature) def decrypt(self, key): """ @@ -335,11 +289,7 @@ class MIMEWrapper: out = message_from_string(dmsg, _class=Message) if decrypted.is_signed: - if len(decrypted.signatures) != 1 and self.allow_draft_multisig: - out = self._wrap_signed_multiple(out, - decrypted.detached_signature) - else: - out = self._wrap_signed(out, decrypted.signatures.pop()) + out = self._wrap_signed(out, decrypted.signatures.pop()) copy_headers(self.msg, out) return out diff --git a/src/mailman_pgp/pgp/mime_multisig.py b/src/mailman_pgp/pgp/mime_multisig.py new file mode 100644 index 0000000..ff9039e --- /dev/null +++ b/src/mailman_pgp/pgp/mime_multisig.py @@ -0,0 +1,94 @@ +# 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 . + +"""""" +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 mailman.email.message import MultipartDigestMessage, Message + +from mailman_pgp.pgp.mime import MIMEWrapper +from mailman_pgp.utils.email import copy_headers + + +class MIMEMultiSigWrapper(MIMEWrapper): + """""" + _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.' + + def _wrap_signed_multiple(self, msg, signature): + """ + As per https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02. + + :param msg: + :param signature: + :return: + """ + micalg = ', '.join(self._micalg(sig.hash_algorithm) + for sig in signature) + out = MultipartDigestMessage('signed', micalg=micalg, + protocol=MIMEWrapper._signed_type) + out.preamble = MIMEMultiSigWrapper._signature_preamble + + second_part = MIMEMultipart() + for sig in signature: + sig_part = MIMEApplication(_data=str(sig), + _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.attach(sig_part) + out.attach(copy.deepcopy(msg)) + out.attach(second_part) + copy_headers(msg, out) + return out + + def sign(self, key, hash=None): + if self.is_signed(): + payload = next(iter(self.get_signed())) + signature = next(iter(self.get_signature())) + signature |= key.sign(payload, hash=hash) + return self._wrap_signed_multiple(self.msg, signature) + else: + super().sign(key, hash) + + def decrypt(self, key): + 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: + if len(decrypted.signatures) != 1: + out = self._wrap_signed_multiple(out, + decrypted.detached_signature) + else: + out = self._wrap_signed(out, decrypted.signatures.pop()) + copy_headers(self.msg, out) + return out diff --git a/src/mailman_pgp/pgp/tests/mime_multisig.py b/src/mailman_pgp/pgp/tests/mime_multisig.py deleted file mode 100644 index 56dd01d..0000000 --- a/src/mailman_pgp/pgp/tests/mime_multisig.py +++ /dev/null @@ -1,16 +0,0 @@ -# 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 . \ No newline at end of file -- cgit v1.2.3-70-g09d2 From a6e81421f0db2e741baee2ec0cedd2a285c6f233 Mon Sep 17 00:00:00 2001 From: J08nY Date: Thu, 27 Jul 2017 21:14:35 +0200 Subject: Add some more MIMEMultiSigWrapper methods that differ from MIMEWrapper. --- src/mailman_pgp/pgp/mime.py | 4 +- src/mailman_pgp/pgp/mime_multisig.py | 77 ++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/mailman_pgp/pgp/mime.py b/src/mailman_pgp/pgp/mime.py index 3674f85..33ec93e 100644 --- a/src/mailman_pgp/pgp/mime.py +++ b/src/mailman_pgp/pgp/mime.py @@ -101,11 +101,11 @@ class MIMEWrapper: :rtype: typing.Generator[pgpy.PGPDetachedSignature] """ try: - msg = PGPDetachedSignature.from_blob( + sig = PGPDetachedSignature.from_blob( self.msg.get_payload(1).get_payload()) except: return - yield msg + yield sig def is_encrypted(self): """ diff --git a/src/mailman_pgp/pgp/mime_multisig.py b/src/mailman_pgp/pgp/mime_multisig.py index ff9039e..ced90fa 100644 --- a/src/mailman_pgp/pgp/mime_multisig.py +++ b/src/mailman_pgp/pgp/mime_multisig.py @@ -21,24 +21,62 @@ 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 MultipartDigestMessage, Message +from pgpy import PGPSignature, PGPDetachedSignature from mailman_pgp.pgp.mime import MIMEWrapper from mailman_pgp.utils.email import copy_headers 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.' + def is_signed(self): + """ + Whether the whole message is MIME 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())) + + 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 + def _wrap_signed_multiple(self, msg, signature): """ - As per https://tools.ietf.org/html/draft-ietf-openpgp-multsig-02. + As per draft-ietf-openpgp-multsig-02. :param msg: :param signature: @@ -67,15 +105,48 @@ class MIMEMultiSigWrapper(MIMEWrapper): return out def sign(self, key, hash=None): + """ + Sign a message with key. + + :param key: The key to sign with. + :type key: pgpy.PGPKey + :param hash: + :type hash: pgpy.constants.HashAlgorithm + :return: The signed message. + :rtype: mailman.email.message.Message + """ if self.is_signed(): payload = next(iter(self.get_signed())) - signature = next(iter(self.get_signature())) + signature = PGPDetachedSignature() + for sig in self.get_signature(): + signature |= sig signature |= key.sign(payload, hash=hash) return self._wrap_signed_multiple(self.msg, signature) else: super().sign(key, hash) + def verify(self, key): + """ + Verify the signature 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(): + yield key.verify(clear_text, signature) + def decrypt(self, key): + """ + Decrypt this message with key. + + :param key: The key to decrypt with. + :type key: pgpy.PGPKey + :return: The decrypted message. + :rtype: mailman.email.message.Message + """ pmsg = next(iter(self.get_encrypted())) decrypted = key.decrypt(pmsg) -- cgit v1.2.3-70-g09d2 From f190131409ada6126977965f6607224d4d97aa84 Mon Sep 17 00:00:00 2001 From: J08nY Date: Fri, 28 Jul 2017 00:32:47 +0200 Subject: Fix some API promises being violated in MIMEMultiSigWrapper. --- src/mailman_pgp/pgp/mime_multisig.py | 68 ++++++++------- .../pgp/tests/data/messages/mime_multisig.eml | 65 +++++++++++++++ .../tests/data/messages/mime_multisig_invalid.eml | 65 +++++++++++++++ src/mailman_pgp/pgp/tests/test_mime_multisig.py | 96 ++++++++++++++++++++++ 4 files changed, 263 insertions(+), 31 deletions(-) create mode 100644 src/mailman_pgp/pgp/tests/data/messages/mime_multisig.eml create mode 100644 src/mailman_pgp/pgp/tests/data/messages/mime_multisig_invalid.eml create mode 100644 src/mailman_pgp/pgp/tests/test_mime_multisig.py diff --git a/src/mailman_pgp/pgp/mime_multisig.py b/src/mailman_pgp/pgp/mime_multisig.py index ced90fa..c7edf60 100644 --- a/src/mailman_pgp/pgp/mime_multisig.py +++ b/src/mailman_pgp/pgp/mime_multisig.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License along with # this program. If not, see . -"""""" +"""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 @@ -23,8 +23,8 @@ from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.utils import collapse_rfc2231_value -from mailman.email.message import MultipartDigestMessage, Message -from pgpy import PGPSignature, PGPDetachedSignature +from mailman.email.message import Message, MultipartDigestMessage +from pgpy import PGPDetachedSignature, PGPSignature from mailman_pgp.pgp.mime import MIMEWrapper from mailman_pgp.utils.email import copy_headers @@ -41,7 +41,7 @@ class MIMEMultiSigWrapper(MIMEWrapper): def is_signed(self): """ - Whether the whole message is MIME signed as per draft-ietf-openpgp-multsig-02. + Whether the message is signed as per draft-ietf-openpgp-multsig-02. :return: If the message is MIME signed. :rtype: bool @@ -74,32 +74,36 @@ class MIMEMultiSigWrapper(MIMEWrapper): continue yield sig - def _wrap_signed_multiple(self, msg, signature): + def _wrap_signed_multiple(self, msg, payload_msg, signatures, signature): """ As per draft-ietf-openpgp-multsig-02. :param msg: + :param payload_msg: + :param signatures: :param signature: :return: """ micalg = ', '.join(self._micalg(sig.hash_algorithm) for sig in signature) out = MultipartDigestMessage('signed', micalg=micalg, - protocol=MIMEWrapper._signed_type) + protocol='multipart/mixed') out.preamble = MIMEMultiSigWrapper._signature_preamble second_part = MIMEMultipart() - for sig in signature: - sig_part = MIMEApplication(_data=str(sig), - _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.attach(sig_part) - out.attach(copy.deepcopy(msg)) + for sig in signatures: + second_part.attach(copy.deepcopy(sig)) + + 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.attach(sig_part) + out.attach(copy.deepcopy(payload_msg)) out.attach(second_part) copy_headers(msg, out) return out @@ -115,19 +119,21 @@ class MIMEMultiSigWrapper(MIMEWrapper): :return: The signed message. :rtype: mailman.email.message.Message """ + if self.is_signed(): - payload = next(iter(self.get_signed())) - signature = PGPDetachedSignature() - for sig in self.get_signature(): - signature |= sig - signature |= key.sign(payload, hash=hash) - return self._wrap_signed_multiple(self.msg, signature) + payload_msg = self.msg.get_payload(0) + signatures = [part for part in self.msg.get_payload(1)] else: - super().sign(key, hash) + payload_msg = self.msg + signatures = [] + signature = PGPDetachedSignature() + signature |= key.sign(payload_msg.as_string(), hash=hash) + return self._wrap_signed_multiple(self.msg, payload_msg, signatures, + signature) def verify(self, key): """ - Verify the signature of this message with key. + Verify the signatures of this message with key. :param key: The key to verify with. :type key: pgpy.PGPKey @@ -136,7 +142,11 @@ class MIMEMultiSigWrapper(MIMEWrapper): """ clear_text = next(iter(self.get_signed())) for signature in self.get_signature(): - yield key.verify(clear_text, signature) + try: + verification = key.verify(clear_text, signature) + except: + continue + yield verification def decrypt(self, key): """ @@ -156,10 +166,6 @@ class MIMEMultiSigWrapper(MIMEWrapper): out = message_from_string(dmsg, _class=Message) if decrypted.is_signed: - if len(decrypted.signatures) != 1: - out = self._wrap_signed_multiple(out, - decrypted.detached_signature) - else: - out = self._wrap_signed(out, decrypted.signatures.pop()) + out = self._wrap_signed_multiple(out, decrypted.detached_signature) copy_headers(self.msg, out) return out diff --git a/src/mailman_pgp/pgp/tests/data/messages/mime_multisig.eml b/src/mailman_pgp/pgp/tests/data/messages/mime_multisig.eml new file mode 100644 index 0000000..555411f --- /dev/null +++ b/src/mailman_pgp/pgp/tests/data/messages/mime_multisig.eml @@ -0,0 +1,65 @@ +To: nobody@example.org +From: RSA 1024b example +Subject: Some subject. +Message-ID: <76a591ed-bfc4-d08b-73d3-fc2489148fd7@example.org> +Date: Wed, 21 Jun 2017 13:50:59 +0200 +User-Agent: Mutt/1.7.2 (2016-11-26) +MIME-Version: 1.0 +Content-Type: multipart/signed; micalg="pgp-sha256, pgp-sha256"; + protocol="multipart/mixed"; + boundary="haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj" + +This is an OpenPGP/MIME signed message (RFC 4880 and 3156) +--haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj +Content-Type: multipart/mixed; boundary="A8WMQ249PdQmpiQhW1ELOnL2UctI16T1g"; + protected-headers="v1" +From: RSA 1024b example +To: nobody@example.org +Message-ID: <76a591ed-bfc4-d08b-73d3-fc2489148fd7@example.org> +Subject: Some subject. + +--A8WMQ249PdQmpiQhW1ELOnL2UctI16T1g +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Some signed text. + + +--A8WMQ249PdQmpiQhW1ELOnL2UctI16T1g-- + +--haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj +Content-Type: multipart/mixed; boundary="abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs" + +--abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iMoEAQEIADQWIQTUqUiGscoglqZFjlxH8QwIeIS3WAUCWUpjIBYccnNhLTEwMjRi +QGV4YW1wbGUub3JnAAoJEEfxDAh4hLdYb8wD/0AlaGxAhVGQqrXBuVXSDm4c49VI +6+DG8cMOCZEin6P96hrLbs4SAm61xivJHPueIRRQ1PfWTcElPn97WuQ48e+/5hhw +CZevF5CmyODGfriC78LwLRRvq2nF5n9iRww0lINPDyqrBr3mdY8QG+s8qBkTi7IG +dBBQH0jA6p2OJV72 +=1tln +-----END PGP SIGNATURE----- + +--abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iMoEAQEIADQWIQTUqUiGscoglqZFjlxH8QwIeIS3WAUCWUpjIBYccnNhLTEwMjRi +QGV4YW1wbGUub3JnAAoJEEfxDAh4hLdYb8wD/0AlaGxAhVGQqrXBuVXSDm4c49VI +6+DG8cMOCZEin6P96hrLbs4SAm61xivJHPueIRRQ1PfWTcElPn97WuQ48e+/5hhw +CZevF5CmyODGfriC78LwLRRvq2nF5n9iRww0lINPDyqrBr3mdY8QG+s8qBkTi7IG +dBBQH0jA6p2OJV72 +=1tln +-----END PGP SIGNATURE----- + +--abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs-- + +--haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj-- diff --git a/src/mailman_pgp/pgp/tests/data/messages/mime_multisig_invalid.eml b/src/mailman_pgp/pgp/tests/data/messages/mime_multisig_invalid.eml new file mode 100644 index 0000000..a13c1df --- /dev/null +++ b/src/mailman_pgp/pgp/tests/data/messages/mime_multisig_invalid.eml @@ -0,0 +1,65 @@ +To: nobody@example.org +From: RSA 1024b example +Subject: Some subject. +Message-ID: <76a591ed-bfc4-d08b-73d3-fc2489148fd7@example.org> +Date: Wed, 21 Jun 2017 13:50:59 +0200 +User-Agent: Mutt/1.7.2 (2016-11-26) +MIME-Version: 1.0 +Content-Type: multipart/signed; micalg="pgp-sha256, pgp-sha256"; + protocol="multipart/mixed"; + boundary="haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj" + +This is an OpenPGP/MIME signed message (RFC 4880 and 3156) +--haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj +Content-Type: multipart/mixed; boundary="A8WMQ249PdQmpiQhW1ELOnL2UctI16T1g"; + protected-headers="v1" +From: RSA 1024b example +To: nobody@example.org +Message-ID: <76a591ed-bfc4-d08b-73d3-fc2489148fd7@example.org> +Subject: Some subject. + +--A8WMQ249PdQmpiQhW1ELOnL2UctI16T1g +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Some signed text. With some INVALID text added!! + + +--A8WMQ249PdQmpiQhW1ELOnL2UctI16T1g-- + +--haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj +Content-Type: multipart/mixed; boundary="abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs" + +--abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iMoEAQEIADQWIQTUqUiGscoglqZFjlxH8QwIeIS3WAUCWUpjIBYccnNhLTEwMjRi +QGV4YW1wbGUub3JnAAoJEEfxDAh4hLdYb8wD/0AlaGxAhVGQqrXBuVXSDm4c49VI +6+DG8cMOCZEin6P96hrLbs4SAm61xivJHPueIRRQ1PfWTcElPn97WuQ48e+/5hhw +CZevF5CmyODGfriC78LwLRRvq2nF5n9iRww0lINPDyqrBr3mdY8QG+s8qBkTi7IG +dBBQH0jA6p2OJV72 +=1tln +-----END PGP SIGNATURE----- + +--abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iMoEAQEIADQWIQTUqUiGscoglqZFjlxH8QwIeIS3WAUCWUpjIBYccnNhLTEwMjRi +QGV4YW1wbGUub3JnAAoJEEfxDAh4hLdYb8wD/0AlaGxAhVGQqrXBuVXSDm4c49VI +6+DG8cMOCZEin6P96hrLbs4SAm61xivJHPueIRRQ1PfWTcElPn97WuQ48e+/5hhw +CZevF5CmyODGfriC78LwLRRvq2nF5n9iRww0lINPDyqrBr3mdY8QG+s8qBkTi7IG +dBBQH0jA6p2OJV72 +=1tln +-----END PGP SIGNATURE----- + +--abjqkjsfwqsfa546qw2wfq6sdq2sqwr56qqs-- + +--haWP9JQ7TiajUxWjooGlinHgq3IhJGnaj-- diff --git a/src/mailman_pgp/pgp/tests/test_mime_multisig.py b/src/mailman_pgp/pgp/tests/test_mime_multisig.py new file mode 100644 index 0000000..2c02a97 --- /dev/null +++ b/src/mailman_pgp/pgp/tests/test_mime_multisig.py @@ -0,0 +1,96 @@ +# 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 . + +"""Tests for the MultiSig wrapper.""" +from parameterized import parameterized + +from mailman_pgp.pgp.mime_multisig import MIMEMultiSigWrapper +from mailman_pgp.pgp.tests.base import load_key, load_message, WrapperTestCase + + +class MultiSigWrapperTestCase(WrapperTestCase): + wrapper = MIMEMultiSigWrapper + + +class TestSigning(MultiSigWrapperTestCase): + @parameterized.expand([ + (load_message('mime_signed.eml'), + False), + (load_message('mime_signed_invalid.eml'), + False), + (load_message('mime_multisig.eml'), + True), + (load_message('mime_multisig_invalid.eml'), + True), + (load_message('clear.eml'), + False), + (load_message('clear_multipart.eml'), + False) + ]) + def test_is_signed(self, message, signed): + self.is_signed(message, signed) + + @parameterized.expand([ + (load_message('mime_signed.eml'), + False), + (load_message('mime_signed_invalid.eml'), + False), + (load_message('mime_multisig.eml'), + True), + (load_message('mime_multisig_invalid.eml'), + True), + (load_message('clear.eml'), + False), + (load_message('clear_multipart.eml'), + False) + ]) + def test_has_signature(self, message, has): + self.has_signature(message, has) + + @parameterized.expand([ + (load_message('clear.eml'), + load_key('rsa_1024.priv.asc')), + (load_message('clear_multipart.eml'), + load_key('ecc_p256.priv.asc')) + ]) + def test_sign(self, message, key): + self.sign(message, key) + + @parameterized.expand([ + (load_message('clear.eml'), + load_key('rsa_1024.priv.asc'), + load_key('rsa_1024.pub.asc')), + (load_message('clear_multipart.eml'), + load_key('ecc_p256.priv.asc'), + load_key('ecc_p256.pub.asc')), + (load_message('mime_multisig.eml'), + load_key('ecc_p256.priv.asc'), + load_key('ecc_p256.pub.asc')) + ]) + def test_sign_verify(self, message, priv, pub): + self.sign_verify(message, priv, pub) + + @parameterized.expand([ + (load_message('mime_multisig.eml'), + load_key('rsa_1024.pub.asc'), + True), + (load_message('mime_multisig_invalid.eml'), + load_key('rsa_1024.pub.asc'), + False) + ]) + def test_verify(self, message, key, valid): + self.verify(message, key, valid) -- cgit v1.2.3-70-g09d2