~launchpad-pqm/launchpad/devel

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
166
167
168
169
170
171
172
# Copyright 2009 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Test the SignedMessage class."""

__metaclass__ = type

from email.Message import Message
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.Utils import (
    formatdate,
    make_msgid,
    )
from textwrap import dedent
import unittest

import gpgme
from zope.component import getUtility

from canonical.launchpad.ftests import (
    import_public_test_keys,
    import_secret_test_key,
    )
from canonical.launchpad.interfaces.gpghandler import IGPGHandler
from canonical.launchpad.interfaces.mail import IWeaklyAuthenticatedPrincipal
from canonical.launchpad.mail import signed_message_from_string
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.registry.interfaces.person import IPersonSet
from lp.services.mail.incoming import (
    authenticateEmail,
    canonicalise_line_endings,
    )
from lp.testing import TestCaseWithFactory
from lp.testing.factory import GPGSigningContext


class TestSignedMessage(TestCaseWithFactory):
    "Test SignedMessage class correctly extracts and verifies GPG signatures."

    layer = DatabaseFunctionalLayer

    def setUp(self):
        # Login with admin roles as we aren't testing access here.
        TestCaseWithFactory.setUp(self, 'admin@canonical.com')
        import_public_test_keys()

    def test_unsigned_message(self):
        # An unsigned message will not have a signature nor signed content,
        # and generates a weakly authenticated principle.
        sender = self.factory.makePerson()
        email_message = self.factory.makeEmailMessage(sender=sender)
        msg = signed_message_from_string(email_message.as_string())
        self.assertIs(None, msg.signedContent)
        self.assertIs(None, msg.signature)
        principle = authenticateEmail(msg)
        self.assertEqual(sender, principle.person)
        self.assertTrue(
            IWeaklyAuthenticatedPrincipal.providedBy(principle))
        self.assertIs(None, msg.signature)

    def _get_clearsigned_for_person(self, sender, body=None):
        # Create a signed message for the sender specified with the test
        # secret key.
        key = import_secret_test_key()
        signing_context = GPGSigningContext(key.fingerprint, password='test')
        if body is None:
            body = dedent("""\
                This is a multi-line body.

                Sincerely,
                Your friendly tester.
                """)
        msg = self.factory.makeSignedMessage(
            email_address=sender.preferredemail.email,
            body=body, signing_context=signing_context)
        self.assertFalse(msg.is_multipart())
        return signed_message_from_string(msg.as_string())

    def test_clearsigned_message_wrong_sender(self):
        # If the message is signed, but the key doesn't belong to the sender,
        # the principle is set to the sender, but weakly authenticated.
        sender = self.factory.makePerson()
        msg = self._get_clearsigned_for_person(sender)
        principle = authenticateEmail(msg)
        self.assertIsNot(None, msg.signature)
        self.assertEqual(sender, principle.person)
        self.assertTrue(
            IWeaklyAuthenticatedPrincipal.providedBy(principle))

    def test_clearsigned_message(self):
        # The test keys belong to Sample Person.
        sender = getUtility(IPersonSet).getByEmail('test@canonical.com')
        msg = self._get_clearsigned_for_person(sender)
        principle = authenticateEmail(msg)
        self.assertIsNot(None, msg.signature)
        self.assertEqual(sender, principle.person)
        self.assertFalse(
            IWeaklyAuthenticatedPrincipal.providedBy(principle))

    def test_trailing_whitespace(self):
        # Trailing whitespace should be ignored when verifying a message's
        # signature.
        sender = getUtility(IPersonSet).getByEmail('test@canonical.com')
        body = (
            'A message with trailing spaces.   \n'+
            'And tabs\t\t\n'+
            'Also mixed. \t ')
        msg = self._get_clearsigned_for_person(sender, body)
        principle = authenticateEmail(msg)
        self.assertIsNot(None, msg.signature)
        self.assertEqual(sender, principle.person)
        self.assertFalse(
            IWeaklyAuthenticatedPrincipal.providedBy(principle))

    def _get_detached_message_for_person(self, sender):
        # Return a signed message that contains a detached signature.
        body = dedent("""\
            This is a multi-line body.

            Sincerely,
            Your friendly tester.""")
        to = self.factory.getUniqueEmailAddress()

        msg = MIMEMultipart()
        msg['Message-Id'] = make_msgid('launchpad')
        msg['Date'] = formatdate()
        msg['To'] = to
        msg['From'] = sender.preferredemail.email
        msg['Subject'] = 'Sample'

        body_text = MIMEText(body)
        msg.attach(body_text)
        # A detached signature is calculated on the entire string content of
        # the body message part.
        key = import_secret_test_key()
        gpghandler = getUtility(IGPGHandler)
        signature = gpghandler.signContent(
            canonicalise_line_endings(body_text.as_string()),
            key.fingerprint, 'test', gpgme.SIG_MODE_DETACH)

        attachment = Message()
        attachment.set_payload(signature)
        attachment['Content-Type'] = 'application/pgp-signature'
        msg.attach(attachment)
        self.assertTrue(msg.is_multipart())
        return signed_message_from_string(msg.as_string())

    def test_detached_signature_message_wrong_sender(self):
        # If the message is signed, but the key doesn't belong to the sender,
        # the principle is set to the sender, but weakly authenticated.
        sender = self.factory.makePerson()
        msg = self._get_detached_message_for_person(sender)
        principle = authenticateEmail(msg)
        self.assertIsNot(None, msg.signature)
        self.assertEqual(sender, principle.person)
        self.assertTrue(
            IWeaklyAuthenticatedPrincipal.providedBy(principle))

    def test_detached_signature_message(self):
        # Test a detached correct signature.
        sender = getUtility(IPersonSet).getByEmail('test@canonical.com')
        msg = self._get_detached_message_for_person(sender)
        principle = authenticateEmail(msg)
        self.assertIsNot(None, msg.signature)
        self.assertEqual(sender, principle.person)
        self.assertFalse(
            IWeaklyAuthenticatedPrincipal.providedBy(principle))


def test_suite():
    return unittest.TestLoader().loadTestsFromName(__name__)