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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
|
Authentication of Emails
========================
When an email arrives in Launchpad the user who sent it needs to be
authenticated. This is done by authenticateEmail:
>>> from lp.services.mail.incoming import authenticateEmail
The only way of authenticating the user is by looking at the OpenPGP
signature. First we have to import the OpenPGP keys we will use in the
emails:
>>> from lp.services.config import config
>>> from canonical.database.sqlbase import commit
>>> from lp.testing.layers import LaunchpadZopelessLayer
>>> from lp.testing.gpgkeys import import_public_test_keys
>>> LaunchpadZopelessLayer.switchDbUser('launchpad')
>>> import_public_test_keys()
>>> commit()
>>> LaunchpadZopelessLayer.switchDbUser(config.processmail.dbuser)
For most of these tests, we don't care whether the timestamps are out of
date:
>>> def accept_any_timestamp(timestamp, context_message):
... pass
Now Sample Person and Foo Bar have one OpenPGP key each. Next, let's get
a test email that's signed and try to authenticate the user who sent it:
>>> from lp.services.mail.tests.helpers import read_test_message
>>> msg = read_test_message('signed_detached.txt')
>>> principal = authenticateEmail(msg, accept_any_timestamp)
If the user isn't registered in Launchpad, None is return, if it
succeeds the authenticated principal:
>>> principal is not None
True
We can check that the user really got authenticated by looking at the
user in the launch bag:
>>> import email
>>> from zope.component import getUtility
>>> from lp.services.webapp.interfaces import ILaunchBag
>>> from lp.registry.interfaces.person import IPersonSet
>>> launchbag = getUtility(ILaunchBag)
>>> name, addr = email.Utils.parseaddr(msg['From'])
>>> from_user = getUtility(IPersonSet).getByEmail(addr)
>>> launchbag.user == from_user
True
>>> launchbag.login
'test@canonical.com'
In the above email the GPG signature was detached from the actual
message. Inline signatures are supported as well.
>>> msg = read_test_message('signed_inline.txt')
>>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> principal is not None
True
>>> name, addr = email.Utils.parseaddr(msg['From'])
>>> from_user = getUtility(IPersonSet).getByEmail(addr)
>>> launchbag.user == from_user
True
>>> launchbag.login
'test@canonical.com'
As well as signed multipart messages:
>>> msg = read_test_message('signed_multipart.txt')
>>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> principal is not None
True
>>> name, addr = email.Utils.parseaddr(msg['From'])
>>> from_user = getUtility(IPersonSet).getByEmail(addr)
>>> launchbag.user == from_user
True
>>> launchbag.login
'foo.bar@canonical.com'
When dealing with inline signatures, lines that begin with a '-'
character in the signed content are required to be escaped, so we need
to deal with it if we receive a dash escaped message.
>>> msg = read_test_message('signed_dash_escaped.txt')
>>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> principal is not None
True
>>> name, addr = email.Utils.parseaddr(msg['From'])
>>> from_user = getUtility(IPersonSet).getByEmail(addr)
>>> launchbag.user == from_user
True
>>> launchbag.login
'test@canonical.com'
If the signature is invalid, that is it won't verify properly,
InvalidSignature will be raised:
>>> msg = read_test_message('signed_detached_invalid_signature.txt')
>>> name, addr = email.Utils.parseaddr(msg['From'])
>>> from_user = getUtility(IPersonSet).getByEmail(addr)
>>> principal = authenticateEmail(msg, accept_any_timestamp)
Traceback (most recent call last):
...
InvalidSignature:...
Before the signature is verified, the signed text's line endings should
be canonicalised to \r\n. In order to ensure that the line endings in
signed_canonicalised.txt are not already '\r\n', we recreate the test
message.
>>> from lp.services.mail.signedmessage import SignedMessage
>>> msg = read_test_message('signed_canonicalised.txt')
>>> msg_lines = msg.as_string().splitlines()
>>> msg = email.message_from_string(
... '\n'.join(msg_lines), _class=SignedMessage)
>>> msg.parsed_string = msg.as_string()
>>> from lp.services.gpg.interfaces import IGPGHandler
>>> getUtility(IGPGHandler).getVerifiedSignature(
... msg.signedContent, msg.signature)
Traceback (most recent call last):
...
GPGVerificationError: (7, 8, u'Bad signature')
>>> getUtility(IGPGHandler).getVerifiedSignature(
... msg.signedContent.replace('\n', '\r\n'), msg.signature)
<...PymeSignature...>
authenticateEmail() doesn't have any problems verifying the signature:
>>> from lp.registry.interfaces.person import IPerson
>>> for line_ending in '\n', '\r\n':
... msg = email.message_from_string(
... line_ending.join(msg_lines), _class=SignedMessage)
... msg.parsed_string = msg.as_string()
... principal = authenticateEmail(msg, accept_any_timestamp)
... authenticated_person = IPerson(principal)
... print authenticated_person.preferredemail.email
test@canonical.com
test@canonical.com
Python's email library unfolds the headers, which means that we have to
be careful when extracting the signed content when folded headers are
signed. This is done by manually parsing boundaries in
SignedMessage._getSignatureAndSignedContent. If the second test here
starts failing, Python is probably fixed, so the manual boundary parsing
hack can be removed.
>>> msg = read_test_message('signed_folded_header.txt')
>>> print msg.parsed_string #doctest: -NORMALIZE_WHITESPACE
Date:...
...
Content-Type: multipart/mixed;
boundary="--------------------EuxKj2iCbKjpUGkD"
...
>>> print msg.get_payload(i=0).as_string() #doctest: -NORMALIZE_WHITESPACE
Content-Type: multipart/mixed; boundary="--------------------EuxKj2iCbKjpUGkD"
...
>>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> print IPerson(principal).displayname
Sample Person
IWeaklyAuthenticatedPrincipal
-----------------------------
It's a huge difference to signing an email with a key that is associated
with the authenticated Person, and signing it with a key that isn't
associated with the Person. The latter is just as insecure as trusting
the From address. In order to let application code know about how the
currently logged in user got authenticated, the principal gets marked
with IWeaklyAuthenticatedPrincipal if only the From address was used,
this includes if the email was signed with a key that isn't associated
with the user in the From address.
An unsigned email:
>>> from lp.services.mail.interfaces import (
... IWeaklyAuthenticatedPrincipal)
>>> msg = read_test_message('unsigned_multipart.txt')
>>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
True
>>> print launchbag.user.displayname
Foo Bar
>>> launchbag.login
'foo.bar@canonical.com'
An email which is signed with a key that isn't associated with the
authenticated user:
>>> msg = read_test_message('signed_key_not_registered.txt')
>>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
True
>>> print launchbag.user.displayname
Sample Person
>>> launchbag.login
'testing@canonical.com'
Of course, if the email is signed with a key which is associated with
the user, IWeaklyAuthenticatedPrincipal won't be provided by the
principal.
>>> msg = read_test_message('signed_inline.txt')
>>> principal = authenticateEmail(msg, accept_any_timestamp)
>>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
False
>>> print launchbag.user.displayname
Sample Person
>>> launchbag.login
'test@canonical.com'
|