aboutsummaryrefslogtreecommitdiff
path: root/src/mailman_pgp/pgp/mime_multisig.py
blob: ced90fa10c2da12b1d55668fcfc75f01959e85d0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# 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/>.

""""""
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 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 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):
        """
        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 = 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)

        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